use super::*;
use crate::meta::{
parse_air_fragment_meta, parse_air_vertex_meta, EmbeddedTexture, KernMeta, KernRole,
};
const FRAG_LL: &str = r#"
!air.fragment = !{!15}
!15 = !{ptr @F, !16, !18}
!16 = !{!17}
!17 = !{!"air.render_target", i32 0, i32 0}
!18 = !{!19, !20, !21, !22, !23}
!19 = !{i32 0, !"air.position", !"air.center"}
!20 = !{i32 1, !"air.fragment_input", !"generated", !"air.arg_type_name", !"float2", !"air.arg_name", !"texCoord"}
!21 = !{i32 2, !"air.texture", !"air.location_index", i32 0, i32 1, !"air.arg_type_name", !"texture2d<float, sample>"}
!22 = !{i32 3, !"air.buffer", !"air.buffer_size", i32 32, !"air.location_index", i32 5, i32 1}
!23 = !{i32 4, !"air.sampler", !"air.location_index", i32 2, i32 1}
"#;
#[test]
fn fragment_reflection_matches_abi_convention() {
let meta = parse_air_fragment_meta(FRAG_LL).unwrap();
let r = ShaderReflection::from_fragment(&meta, Some("myFragment"));
assert_eq!(r.reflection_version, REFLECTION_VERSION);
assert_eq!(r.stage, ShaderStage::Fragment);
assert_eq!(r.entry_point.as_deref(), Some("myFragment"));
let tex = r.binding_at(ResourceKind::Texture, 0).expect("texture 0");
assert_eq!(
tex.descriptor,
Some(DescriptorLocation {
set: 0,
binding: TEXTURE_BINDING_BASE
})
);
assert_eq!(tex.param_index, Some(2));
assert_eq!(tex.type_name.as_deref(), Some("texture2d<float, sample>"));
assert_eq!(tex.access, Some(ResourceAccess::Sampled));
let buf = r.binding_at(ResourceKind::Buffer, 5).expect("buffer 5");
assert_eq!(
buf.descriptor,
Some(DescriptorLocation { set: 0, binding: 5 })
);
assert_eq!(buf.declared_size, Some(32));
let smp = r.binding_at(ResourceKind::Sampler, 2).expect("sampler 2");
assert_eq!(
smp.descriptor,
Some(DescriptorLocation {
set: 0,
binding: SAMPLER_BINDING_BASE + 2
})
);
let v = r
.varyings
.iter()
.find(|v| v.location == 0)
.expect("varying 0");
assert_eq!(v.type_name.as_deref(), Some("float2"));
assert_eq!(v.name.as_deref(), Some("texCoord"));
assert_eq!(v.user_semantic.as_deref(), Some("generated"));
assert_eq!(r.render_targets.len(), 1);
assert_eq!(r.render_targets[0].member_index, 0);
assert_eq!(r.render_targets[0].location, 0);
assert!(r
.bindings
.iter()
.all(|b| b.kind != ResourceKind::ColorInput));
}
#[test]
fn fragment_and_vertex_buffer_address_space_exported_when_known() {
use crate::meta::{FragRole, VertRole};
let mut frag = crate::meta::FragMeta {
roles: vec![(0, FragRole::Buffer(0)), (1, FragRole::Buffer(1))],
..Default::default()
};
frag.buffer_address_spaces.insert(0, 2); frag.buffer_type_sizes.insert(0, 64);
let rf = ShaderReflection::from_fragment(&frag, Some("f"));
let b0 = rf.binding_at(ResourceKind::Buffer, 0).expect("buffer 0");
assert_eq!(b0.address_space, Some(2));
assert_eq!(b0.declared_size, Some(64));
let b1 = rf.binding_at(ResourceKind::Buffer, 1).expect("buffer 1");
assert_eq!(b1.address_space, None);
assert_eq!(b1.declared_size, None);
let mut vert = crate::meta::VertMeta {
roles: vec![(0, VertRole::Buffer(3))],
..Default::default()
};
vert.buffer_address_spaces.insert(0, 1); let rv = ShaderReflection::from_vertex(&vert, Some("v"));
let vb = rv
.binding_at(ResourceKind::Buffer, 3)
.expect("vertex buffer 3");
assert_eq!(vb.address_space, Some(1));
}
const VERT_LL: &str = r#"
!air.vertex = !{!5}
!5 = !{ptr @V, !6, !8}
!6 = !{!7}
!7 = !{i32 0, !"air.position", !"air.center"}
!8 = !{!9, !10}
!9 = !{i32 0, !"air.vertex_input", !"air.arg_type_name", !"float4", !"air.arg_name", !"pos", !"air.location_index", i32 0, i32 1}
!10 = !{i32 1, !"air.buffer", !"air.location_index", i32 2, i32 1}
"#;
#[test]
fn vertex_reflection_exports_attributes_and_buffer() {
let meta = parse_air_vertex_meta(VERT_LL).unwrap();
let r = ShaderReflection::from_vertex(&meta, Some("myVertex"));
assert_eq!(r.stage, ShaderStage::Vertex);
let buf = r.binding_at(ResourceKind::Buffer, 2).expect("buffer 2");
assert_eq!(
buf.descriptor,
Some(DescriptorLocation { set: 0, binding: 2 })
);
let attr = r
.vertex_attributes
.iter()
.find(|a| a.location == 0)
.expect("attribute 0");
assert_eq!(attr.type_name.as_deref(), Some("float4"));
assert_eq!(attr.name.as_deref(), Some("pos"));
}
#[test]
fn kernel_reflection_threadgroup_buffer_has_no_descriptor() {
let mut meta = KernMeta {
roles: vec![
(0, KernRole::Buffer(0)),
(1, KernRole::Buffer(1)),
(2, KernRole::Texture(0)),
],
..Default::default()
};
meta.buffer_address_spaces.insert(0, 1); meta.buffer_address_spaces
.insert(1, ADDRESS_SPACE_THREADGROUP);
meta.buffer_type_sizes.insert(0, 64);
meta.texture_type_names
.insert(2, "texture2d<float, sample>".to_string());
meta.embedded_textures.push(EmbeddedTexture {
buffer_index: 0,
field_offset: 8,
dim: spirv::Dim::Dim2D,
comp: crate::passes::ImageComp::Float,
synthetic_texture_index: 3,
});
let r = ShaderReflection::from_kernel(&meta, Some("myKernel"), [64, 1, 1]);
assert_eq!(r.stage, ShaderStage::Kernel);
assert_eq!(r.local_size, Some([64, 1, 1]));
let dev = r
.binding_at(ResourceKind::Buffer, 0)
.expect("device buffer");
assert_eq!(
dev.descriptor,
Some(DescriptorLocation { set: 0, binding: 0 })
);
assert_eq!(dev.declared_size, Some(64));
assert_eq!(dev.address_space, Some(1));
let tg = r
.binding_at(ResourceKind::ThreadgroupBuffer, 1)
.expect("threadgroup buffer");
assert_eq!(tg.descriptor, None);
assert_eq!(tg.address_space, Some(ADDRESS_SPACE_THREADGROUP));
let tex = r.binding_at(ResourceKind::Texture, 0).expect("texture");
assert_eq!(
tex.descriptor,
Some(DescriptorLocation {
set: 0,
binding: TEXTURE_BINDING_BASE
})
);
let emb = r
.binding_at(ResourceKind::EmbeddedArgBufferTexture, 3)
.expect("embedded texture");
assert_eq!(
emb.descriptor,
Some(DescriptorLocation {
set: 0,
binding: TEXTURE_BINDING_BASE + 3
})
);
assert_eq!(emb.param_index, None);
assert_eq!(
emb.embedded_source,
Some(EmbeddedArgBuffer {
buffer_index: 0,
field_offset: 8
})
);
let shape = emb.texture_shape.expect("embedded texture_shape");
assert_eq!(shape.dimension, crate::meta::TextureDimension::D2);
assert_eq!(shape.component, crate::meta::TextureComponent::Float);
assert!(!shape.writable);
}
#[test]
fn imageblock_layouts_exported_for_kernel() {
use crate::meta::{AirScalar, AirType};
let mut meta = KernMeta {
roles: vec![(0, KernRole::Buffer(0))],
..Default::default()
};
let tile = AirType::Vec {
scalar: AirScalar::Float,
lanes: 4,
};
meta.imageblock_layouts.insert(2, tile.clone());
let r = ShaderReflection::from_kernel(&meta, Some("k"), [8, 8, 1]);
assert_eq!(r.imageblock_layouts.len(), 1);
assert_eq!(r.imageblock_layouts[0].param_index, 2);
assert_eq!(r.imageblock_layouts[0].type_layout, tile);
let frag = parse_air_fragment_meta(FRAG_LL).unwrap();
assert!(ShaderReflection::from_fragment(&frag, None)
.imageblock_layouts
.is_empty());
}
#[test]
fn vertex_builtins_report_index_and_position_usage() {
let mut meta = crate::meta::VertMeta {
roles: vec![
(0, VertRole::VertexId),
(1, VertRole::InstanceId),
(2, VertRole::Buffer(0)),
],
..Default::default()
};
meta.output_roles = vec![crate::meta::VertOutRole::Position];
let r = ShaderReflection::from_vertex(&meta, Some("v"));
let b = r.vertex_builtins.expect("vertex_builtins present");
assert!(b.uses_vertex_index);
assert!(b.uses_instance_index);
assert!(b.writes_position);
let plain = crate::meta::VertMeta {
roles: vec![(0, VertRole::Buffer(0))],
..Default::default()
};
let rp = ShaderReflection::from_vertex(&plain, Some("v"));
let bp = rp.vertex_builtins.expect("vertex_builtins present");
assert_eq!(bp, VertexBuiltins::default());
let kern = KernMeta::default();
assert_eq!(
ShaderReflection::from_kernel(&kern, Some("k"), [1, 1, 1]).vertex_builtins,
None
);
}
#[test]
fn texture_access_classification_matches_declared_qualifier() {
let cases = [
(
0u32,
"texture2d<float, sample>",
ResourceKind::Texture,
ResourceAccess::Sampled,
),
(
1,
"texture2d<uint, read>",
ResourceKind::Texture,
ResourceAccess::Sampled,
),
(
2,
"texture2d<uint, write>",
ResourceKind::StorageImage,
ResourceAccess::Storage,
),
(
3,
"texture2d<float, read_write>",
ResourceKind::StorageImage,
ResourceAccess::Storage,
),
(
4,
"array_ref<texture2d<float, sample>>",
ResourceKind::TextureArray,
ResourceAccess::Sampled,
),
];
let mut meta = KernMeta {
roles: cases
.iter()
.enumerate()
.map(|(i, (n, ..))| (i as u32, KernRole::Texture(*n)))
.collect(),
..Default::default()
};
for (i, (_, name, ..)) in cases.iter().enumerate() {
meta.texture_type_names.insert(i as u32, name.to_string());
}
let r = ShaderReflection::from_kernel(&meta, Some("k"), [1, 1, 1]);
for (n, _, kind, access) in cases {
let b = r
.binding_at(kind, n)
.unwrap_or_else(|| panic!("binding {n}"));
assert_eq!(b.kind, kind, "kind for texture {n}");
assert_eq!(b.access, Some(access), "access for texture {n}");
}
}
#[test]
fn texture_shape_exports_dimensionality_arrayed_multisampled_component() {
use crate::meta::{TextureComponent as TC, TextureDimension as TD};
let cases = [
(
0u32,
"texture1d<float, sample>",
TD::D1,
false,
false,
TC::Float,
),
(
1,
"texture1d_array<float, sample>",
TD::D1,
true,
false,
TC::Float,
),
(2, "texture2d<uint, read>", TD::D2, false, false, TC::Uint),
(
3,
"texture2d_array<int, sample>",
TD::D2,
true,
false,
TC::Sint,
),
(
4,
"texture2d_ms<float, read>",
TD::D2,
false,
true,
TC::Float,
),
(
5,
"texture3d<half, sample>",
TD::D3,
false,
false,
TC::Float,
),
(
6,
"texturecube<float, sample>",
TD::Cube,
false,
false,
TC::Float,
),
(
7,
"texturecube_array<float, sample>",
TD::Cube,
true,
false,
TC::Float,
),
(
8,
"texture_buffer<uint, read>",
TD::Buffer,
false,
false,
TC::Uint,
),
];
let mut meta = KernMeta {
roles: cases
.iter()
.enumerate()
.map(|(i, (n, ..))| (i as u32, KernRole::Texture(*n)))
.collect(),
..Default::default()
};
for (i, (_, name, ..)) in cases.iter().enumerate() {
meta.texture_type_names.insert(i as u32, name.to_string());
}
let r = ShaderReflection::from_kernel(&meta, Some("k"), [1, 1, 1]);
for (n, name, dim, arrayed, ms, comp) in cases {
let b = r
.bindings
.iter()
.find(|b| b.metal_index == n && b.texture_shape.is_some())
.unwrap_or_else(|| panic!("texture binding {n} ({name})"));
let shape = b.texture_shape.expect("texture_shape present");
assert_eq!(shape.dimension, dim, "dim for {name}");
assert_eq!(shape.arrayed, arrayed, "arrayed for {name}");
assert_eq!(shape.multisampled, ms, "multisampled for {name}");
assert_eq!(shape.component, comp, "component for {name}");
assert_eq!(shape.storage_format, None, "storage_format for {name}");
}
}
#[test]
fn storage_image_texel_format_exported() {
use crate::meta::TextureFormat as TF;
let cases = [
(0u32, "texture2d<float, write>", TF::Rgba32f),
(1, "texture2d<half, write>", TF::Rgba16f),
(2, "texture2d<uint, write>", TF::Rgba8ui),
(3, "texture2d<ushort, read_write>", TF::Rgba16ui),
(4, "texture2d<int, write>", TF::Rgba8i),
];
let mut meta = KernMeta {
roles: cases
.iter()
.enumerate()
.map(|(i, (n, ..))| (i as u32, KernRole::Texture(*n)))
.collect(),
..Default::default()
};
for (i, (_, name, _)) in cases.iter().enumerate() {
meta.texture_type_names.insert(i as u32, name.to_string());
}
let r = ShaderReflection::from_kernel(&meta, Some("k"), [1, 1, 1]);
for (n, name, fmt) in cases {
let b = r
.binding_at(ResourceKind::StorageImage, n)
.unwrap_or_else(|| panic!("storage image {n} ({name})"));
let shape = b.texture_shape.expect("texture_shape present");
assert_eq!(shape.storage_format, Some(fmt), "storage_format for {name}");
assert!(shape.writable, "writable for {name}");
}
}
#[test]
fn constant_buffer_is_read_only_device_is_unknown() {
let mut meta = KernMeta {
roles: vec![(0, KernRole::Buffer(0)), (1, KernRole::Buffer(1))],
..Default::default()
};
meta.buffer_address_spaces.insert(0, ADDRESS_SPACE_CONSTANT);
meta.buffer_address_spaces.insert(1, 1); let r = ShaderReflection::from_kernel(&meta, Some("k"), [1, 1, 1]);
assert_eq!(
r.binding_at(ResourceKind::Buffer, 0).unwrap().access,
Some(ResourceAccess::ReadOnly)
);
assert_eq!(r.binding_at(ResourceKind::Buffer, 1).unwrap().access, None);
}
#[cfg(feature = "serde")]
#[test]
fn reflection_serde_round_trips() {
let meta = parse_air_fragment_meta(FRAG_LL).unwrap();
let r = ShaderReflection::from_fragment(&meta, Some("myFragment"));
let json = serde_json::to_string(&r).expect("serialize reflection");
let back: ShaderReflection = serde_json::from_str(&json).expect("deserialize reflection");
assert_eq!(r, back);
assert_eq!(back.reflection_version, REFLECTION_VERSION);
}
#[cfg(feature = "serde")]
#[test]
fn reflection_serde_covers_every_v2_field() {
use crate::meta::{texture_shape_from_name, AirScalar, AirType, FunctionConstant};
let storage_tex = texture_shape_from_name("texture2d<float, write>");
assert!(storage_tex.writable && storage_tex.storage_format.is_some());
let r = ShaderReflection {
reflection_version: REFLECTION_VERSION,
stage: ShaderStage::Kernel,
entry_point: Some("k".to_string()),
bindings: vec![
ResourceBinding {
kind: ResourceKind::Buffer,
metal_index: 0,
descriptor: ResourceBinding::descriptor_at(BUFFER_BINDING_BASE, 0),
param_index: Some(0),
address_space: Some(2),
declared_size: Some(64),
type_layout: Some(AirType::Vec {
scalar: AirScalar::Float,
lanes: 4,
}),
type_name: Some("float4".to_string()),
texture_shape: None,
embedded_source: None,
access: Some(ResourceAccess::ReadOnly),
},
ResourceBinding {
kind: ResourceKind::Texture,
metal_index: 0,
descriptor: ResourceBinding::descriptor_at(TEXTURE_BINDING_BASE, 0),
param_index: None,
address_space: None,
declared_size: None,
type_layout: None,
type_name: Some("texture2d<float, write>".to_string()),
texture_shape: Some(storage_tex),
embedded_source: Some(EmbeddedArgBuffer {
buffer_index: 0,
field_offset: 8,
}),
access: Some(ResourceAccess::Storage),
},
],
vertex_attributes: vec![VertexAttribute {
location: 0,
type_name: Some("float4".to_string()),
name: Some("pos".to_string()),
}],
varyings: vec![Varying {
location: 0,
type_name: Some("float2".to_string()),
name: Some("uv".to_string()),
user_semantic: Some("user(uv)".to_string()),
}],
render_targets: vec![RenderTarget {
member_index: 0,
location: 0,
type_name: Some("float4".to_string()),
}],
depth_members: vec![1],
stencil_members: vec![2],
local_size: Some([8, 8, 1]),
vertex_builtins: Some(VertexBuiltins {
uses_vertex_index: true,
uses_instance_index: true,
writes_position: true,
}),
imageblock_layouts: vec![ImageblockLayout {
param_index: 3,
type_layout: AirType::Scalar(AirScalar::Float),
}],
datalayout: Some("e-p:64:64-i64:64-n32:64".to_string()),
function_constants: vec![FunctionConstant {
index: 0,
name: "myConst".to_string(),
type_name: "i32".to_string(),
}],
};
let json = serde_json::to_string(&r).expect("serialize reflection");
let back: ShaderReflection = serde_json::from_str(&json).expect("deserialize reflection");
assert_eq!(r, back);
assert_eq!(back.reflection_version, 2);
}
#[test]
fn abi_base_constants_are_the_contract() {
assert_eq!(RESOURCE_DESCRIPTOR_SET, 0);
assert_eq!(BUFFER_BINDING_BASE, 0);
assert_eq!(TEXTURE_BINDING_BASE, 32);
assert_eq!(SAMPLER_BINDING_BASE, 64);
assert_eq!(COLOR_INPUT_BINDING_BASE, 96);
}