use crate::descriptor_set::layout::DescriptorType;
use crate::image::view::ImageViewType;
use crate::shader::ShaderScalarType;
use crate::DeviceSize;
use crate::{
format::Format,
pipeline::layout::PipelineLayoutPcRange,
shader::{
spirv::{
Capability, Decoration, Dim, ExecutionMode, ExecutionModel, Id, ImageFormat,
Instruction, Spirv, StorageClass,
},
DescriptorRequirements, EntryPointInfo, GeometryShaderExecution, GeometryShaderInput,
ShaderExecution, ShaderInterface, ShaderInterfaceEntry, ShaderInterfaceEntryType,
ShaderStage, SpecializationConstantRequirements,
},
};
use fnv::FnvHashMap;
use std::borrow::Cow;
use std::collections::HashSet;
pub fn spirv_capabilities<'a>(spirv: &'a Spirv) -> impl Iterator<Item = &'a Capability> {
spirv
.iter_capability()
.filter_map(|instruction| match instruction {
Instruction::Capability { capability } => Some(capability),
_ => None,
})
}
pub fn spirv_extensions<'a>(spirv: &'a Spirv) -> impl Iterator<Item = &'a str> {
spirv
.iter_extension()
.filter_map(|instruction| match instruction {
Instruction::Extension { name } => Some(name.as_str()),
_ => None,
})
}
pub fn entry_points<'a>(
spirv: &'a Spirv,
exact_interface: bool,
) -> impl Iterator<Item = (String, ExecutionModel, EntryPointInfo)> + 'a {
spirv.iter_entry_point().filter_map(move |instruction| {
let (execution_model, function_id, entry_point_name, interface) = match instruction {
&Instruction::EntryPoint {
ref execution_model,
entry_point,
ref name,
ref interface,
..
} => (execution_model, entry_point, name, interface),
_ => return None,
};
let execution = shader_execution(&spirv, execution_model, function_id);
let stage = ShaderStage::from(execution);
let descriptor_requirements =
descriptor_requirements(&spirv, function_id, stage, interface, exact_interface);
let push_constant_requirements = push_constant_requirements(&spirv, stage);
let specialization_constant_requirements = specialization_constant_requirements(&spirv);
let input_interface = shader_interface(
&spirv,
interface,
StorageClass::Input,
matches!(
execution_model,
ExecutionModel::TessellationControl
| ExecutionModel::TessellationEvaluation
| ExecutionModel::Geometry
),
);
let output_interface = shader_interface(
&spirv,
interface,
StorageClass::Output,
matches!(execution_model, ExecutionModel::TessellationControl),
);
Some((
entry_point_name.clone(),
*execution_model,
EntryPointInfo {
execution,
descriptor_requirements,
push_constant_requirements,
specialization_constant_requirements,
input_interface,
output_interface,
},
))
})
}
fn shader_execution(
spirv: &Spirv,
execution_model: &ExecutionModel,
function_id: Id,
) -> ShaderExecution {
match execution_model {
ExecutionModel::Vertex => ShaderExecution::Vertex,
ExecutionModel::TessellationControl => ShaderExecution::TessellationControl,
ExecutionModel::TessellationEvaluation => ShaderExecution::TessellationEvaluation,
ExecutionModel::Geometry => {
let input = spirv
.iter_execution_mode()
.into_iter()
.find_map(|instruction| match instruction {
Instruction::ExecutionMode {
entry_point, mode, ..
} if *entry_point == function_id => match mode {
ExecutionMode::InputPoints => Some(GeometryShaderInput::Points),
ExecutionMode::InputLines => Some(GeometryShaderInput::Lines),
ExecutionMode::InputLinesAdjacency => {
Some(GeometryShaderInput::LinesWithAdjacency)
}
ExecutionMode::Triangles => Some(GeometryShaderInput::Triangles),
ExecutionMode::InputTrianglesAdjacency => {
Some(GeometryShaderInput::TrianglesWithAdjacency)
}
_ => todo!(),
},
_ => None,
})
.expect("Geometry shader does not have an input primitive ExecutionMode");
ShaderExecution::Geometry(GeometryShaderExecution { input })
}
ExecutionModel::Fragment => ShaderExecution::Fragment,
ExecutionModel::GLCompute => ShaderExecution::Compute,
ExecutionModel::Kernel
| ExecutionModel::TaskNV
| ExecutionModel::MeshNV
| ExecutionModel::RayGenerationKHR
| ExecutionModel::IntersectionKHR
| ExecutionModel::AnyHitKHR
| ExecutionModel::ClosestHitKHR
| ExecutionModel::MissKHR
| ExecutionModel::CallableKHR => {
todo!()
}
}
}
fn descriptor_requirements(
spirv: &Spirv,
function_id: Id,
stage: ShaderStage,
interface: &[Id],
exact: bool,
) -> FnvHashMap<(u32, u32), DescriptorRequirements> {
let variables = if exact {
let mut found_variables: HashSet<Id> = interface.iter().cloned().collect();
let mut inspected_functions: HashSet<Id> = HashSet::new();
find_variables_in_function(
&spirv,
function_id,
&mut inspected_functions,
&mut found_variables,
);
Some(found_variables)
} else {
None
};
spirv
.iter_global()
.filter_map(|instruction| {
let (variable_id, variable_type_id, storage_class) = match instruction {
Instruction::Variable {
result_id,
result_type_id,
..
} => {
let (real_type, storage_class) = match spirv
.id(*result_type_id)
.instruction()
{
Instruction::TypePointer {
ty, storage_class, ..
} => (ty, storage_class),
_ => panic!(
"Variable {} result_type_id does not refer to a TypePointer instruction", result_id
),
};
(*result_id, *real_type, storage_class)
}
_ => return None,
};
if exact && !variables.as_ref().unwrap().contains(&variable_id) {
return None;
}
let variable_id_info = spirv.id(variable_id);
let set_num = match variable_id_info
.iter_decoration()
.find_map(|instruction| match instruction {
Instruction::Decorate {
decoration: Decoration::DescriptorSet { descriptor_set },
..
} => Some(*descriptor_set),
_ => None,
}) {
Some(x) => x,
None => return None,
};
let binding_num = variable_id_info
.iter_decoration()
.find_map(|instruction| match instruction {
Instruction::Decorate {
decoration: Decoration::Binding { binding_point },
..
} => Some(*binding_point),
_ => None,
})
.unwrap();
let name = variable_id_info
.iter_name()
.find_map(|instruction| match instruction {
Instruction::Name { name, .. } => Some(name.as_str()),
_ => None,
})
.unwrap_or("__unnamed");
let nonwritable = variable_id_info.iter_decoration().any(|instruction| {
matches!(
instruction,
Instruction::Decorate {
decoration: Decoration::NonWritable,
..
}
)
});
let mut reqs =
descriptor_requirements_of(spirv, variable_type_id, storage_class, false).expect(&format!(
"Couldn't find relevant type for global variable `{}` (type {}, maybe unimplemented)",
name, variable_type_id,
));
reqs.stages = stage.into();
reqs.mutable &= !nonwritable;
Some(((set_num, binding_num), reqs))
})
.collect()
}
fn find_variables_in_function(
spirv: &Spirv,
function: Id,
inspected_functions: &mut HashSet<Id>,
found_variables: &mut HashSet<Id>,
) {
inspected_functions.insert(function);
let mut in_function = false;
for instruction in spirv.instructions() {
if !in_function {
match instruction {
Instruction::Function { result_id, .. } if result_id == &function => {
in_function = true;
}
_ => {}
}
} else {
match instruction {
Instruction::Load { pointer, .. } | Instruction::Store { pointer, .. } => {
found_variables.insert(*pointer);
}
Instruction::AccessChain { base, .. }
| Instruction::InBoundsAccessChain { base, .. } => {
found_variables.insert(*base);
}
Instruction::FunctionCall {
function,
arguments,
..
} => {
arguments.iter().for_each(|&x| {
found_variables.insert(x);
});
if !inspected_functions.contains(function) {
find_variables_in_function(
spirv,
*function,
inspected_functions,
found_variables,
);
}
}
Instruction::ImageTexelPointer {
image,
coordinate,
sample,
..
} => {
found_variables.insert(*image);
found_variables.insert(*coordinate);
found_variables.insert(*sample);
}
Instruction::CopyMemory { target, source, .. } => {
found_variables.insert(*target);
found_variables.insert(*source);
}
Instruction::CopyObject { operand, .. } => {
found_variables.insert(*operand);
}
Instruction::AtomicLoad { pointer, .. }
| Instruction::AtomicIIncrement { pointer, .. }
| Instruction::AtomicIDecrement { pointer, .. }
| Instruction::AtomicFlagTestAndSet { pointer, .. }
| Instruction::AtomicFlagClear { pointer, .. } => {
found_variables.insert(*pointer);
}
Instruction::AtomicStore { pointer, value, .. }
| Instruction::AtomicExchange { pointer, value, .. }
| Instruction::AtomicIAdd { pointer, value, .. }
| Instruction::AtomicISub { pointer, value, .. }
| Instruction::AtomicSMin { pointer, value, .. }
| Instruction::AtomicUMin { pointer, value, .. }
| Instruction::AtomicSMax { pointer, value, .. }
| Instruction::AtomicUMax { pointer, value, .. }
| Instruction::AtomicAnd { pointer, value, .. }
| Instruction::AtomicOr { pointer, value, .. }
| Instruction::AtomicXor { pointer, value, .. } => {
found_variables.insert(*pointer);
found_variables.insert(*value);
}
Instruction::AtomicCompareExchange {
pointer,
value,
comparator,
..
}
| Instruction::AtomicCompareExchangeWeak {
pointer,
value,
comparator,
..
} => {
found_variables.insert(*pointer);
found_variables.insert(*value);
found_variables.insert(*comparator);
}
Instruction::ExtInst { operands, .. } => {
operands.iter().for_each(|&o| {
found_variables.insert(o);
});
}
Instruction::FunctionEnd => return,
_ => {}
}
}
}
}
fn descriptor_requirements_of(
spirv: &Spirv,
pointed_ty: Id,
pointer_storage: &StorageClass,
force_combined_image_sampled: bool,
) -> Option<DescriptorRequirements> {
let id_info = spirv.id(pointed_ty);
match id_info.instruction() {
Instruction::TypeStruct { .. } => {
let decoration_block = id_info.iter_decoration().any(|instruction| {
matches!(
instruction,
Instruction::Decorate {
decoration: Decoration::Block,
..
}
)
});
let decoration_buffer_block = id_info.iter_decoration().any(|instruction| {
matches!(
instruction,
Instruction::Decorate {
decoration: Decoration::BufferBlock,
..
}
)
});
assert!(
decoration_block ^ decoration_buffer_block,
"Structs in shader interface are expected to be decorated with one of Block or BufferBlock"
);
let mut reqs = DescriptorRequirements {
descriptor_count: 1,
..Default::default()
};
if decoration_buffer_block
|| decoration_block && *pointer_storage == StorageClass::StorageBuffer
{
let nonwritable = id_info.iter_members().all(|member_info| {
member_info.iter_decoration().any(|instruction| {
matches!(
instruction,
Instruction::MemberDecorate {
decoration: Decoration::NonWritable,
..
}
)
})
});
reqs.descriptor_types = vec![
DescriptorType::StorageBuffer,
DescriptorType::StorageBufferDynamic,
];
reqs.mutable = !nonwritable;
} else {
reqs.descriptor_types = vec![
DescriptorType::UniformBuffer,
DescriptorType::UniformBufferDynamic,
];
};
Some(reqs)
}
&Instruction::TypeImage {
ref dim,
arrayed,
ms,
sampled,
ref image_format,
..
} => {
let multisampled = ms != 0;
assert!(sampled != 0, "Vulkan requires that variables of type OpTypeImage have a Sampled operand of 1 or 2");
let format: Option<Format> = image_format.clone().into();
match dim {
Dim::SubpassData => {
assert!(
!force_combined_image_sampled,
"An OpTypeSampledImage can't point to \
an OpTypeImage whose dimension is \
SubpassData"
);
assert!(
*image_format == ImageFormat::Unknown,
"If Dim is SubpassData, Image Format must be Unknown"
);
assert!(sampled == 2, "If Dim is SubpassData, Sampled must be 2");
assert!(arrayed == 0, "If Dim is SubpassData, Arrayed must be 0");
Some(DescriptorRequirements {
descriptor_types: vec![DescriptorType::InputAttachment],
descriptor_count: 1,
multisampled,
..Default::default()
})
}
Dim::Buffer => {
let mut reqs = DescriptorRequirements {
descriptor_count: 1,
format,
..Default::default()
};
if sampled == 1 {
reqs.descriptor_types = vec![DescriptorType::UniformTexelBuffer];
} else {
reqs.descriptor_types = vec![DescriptorType::StorageTexelBuffer];
reqs.mutable = true;
}
Some(reqs)
}
_ => {
let image_view_type = Some(match (dim, arrayed) {
(Dim::Dim1D, 0) => ImageViewType::Dim1d,
(Dim::Dim1D, 1) => ImageViewType::Dim1dArray,
(Dim::Dim2D, 0) => ImageViewType::Dim2d,
(Dim::Dim2D, 1) => ImageViewType::Dim2dArray,
(Dim::Dim3D, 0) => ImageViewType::Dim3d,
(Dim::Dim3D, 1) => panic!("Vulkan doesn't support arrayed 3D textures"),
(Dim::Cube, 0) => ImageViewType::Cube,
(Dim::Cube, 1) => ImageViewType::CubeArray,
(Dim::Rect, _) => panic!("Vulkan doesn't support rectangle textures"),
_ => unreachable!(),
});
let mut reqs = DescriptorRequirements {
descriptor_count: 1,
format,
multisampled,
image_view_type,
..Default::default()
};
if force_combined_image_sampled {
assert!(
sampled == 1,
"A combined image sampler must not reference a storage image"
);
reqs.descriptor_types = vec![DescriptorType::CombinedImageSampler];
} else {
if sampled == 1 {
reqs.descriptor_types = vec![DescriptorType::SampledImage];
} else {
reqs.descriptor_types = vec![DescriptorType::StorageImage];
reqs.mutable = true;
}
};
Some(reqs)
}
}
}
&Instruction::TypeSampledImage { image_type, .. } => {
descriptor_requirements_of(spirv, image_type, pointer_storage, true)
}
&Instruction::TypeSampler { .. } => Some(DescriptorRequirements {
descriptor_types: vec![DescriptorType::Sampler],
descriptor_count: 1,
..Default::default()
}),
&Instruction::TypeArray {
element_type,
length,
..
} => {
let reqs = match descriptor_requirements_of(spirv, element_type, pointer_storage, false)
{
None => return None,
Some(v) => v,
};
assert_eq!(reqs.descriptor_count, 1); let len = match spirv.id(length).instruction() {
&Instruction::Constant { ref value, .. } => value,
_ => panic!("failed to find array length"),
};
let len = len.iter().rev().fold(0, |a, &b| (a << 32) | b as u64);
Some(DescriptorRequirements {
descriptor_count: len as u32,
..reqs
})
}
&Instruction::TypeRuntimeArray { element_type, .. } => {
let reqs = match descriptor_requirements_of(spirv, element_type, pointer_storage, false)
{
None => return None,
Some(v) => v,
};
assert_eq!(reqs.descriptor_count, 1);
Some(DescriptorRequirements {
descriptor_count: 0,
..reqs
})
}
_ => None,
}
}
fn push_constant_requirements(spirv: &Spirv, stage: ShaderStage) -> Option<PipelineLayoutPcRange> {
spirv
.iter_global()
.find_map(|instruction| match instruction {
&Instruction::TypePointer {
ty,
storage_class: StorageClass::PushConstant,
..
} => {
let id_info = spirv.id(ty);
assert!(matches!(
id_info.instruction(),
Instruction::TypeStruct { .. }
));
let start = offset_of_struct(spirv, ty);
let end =
size_of_type(spirv, ty).expect("Found runtime-sized push constants") as u32;
Some(PipelineLayoutPcRange {
offset: start,
size: end - start,
stages: stage.into(),
})
}
_ => None,
})
}
fn specialization_constant_requirements(
spirv: &Spirv,
) -> FnvHashMap<u32, SpecializationConstantRequirements> {
spirv
.iter_global()
.filter_map(|instruction| {
match instruction {
&Instruction::SpecConstantTrue {
result_type_id,
result_id,
}
| &Instruction::SpecConstantFalse {
result_type_id,
result_id,
}
| &Instruction::SpecConstant {
result_type_id,
result_id,
..
}
| &Instruction::SpecConstantComposite {
result_type_id,
result_id,
..
} => spirv
.id(result_id)
.iter_decoration()
.find_map(|instruction| match instruction {
Instruction::Decorate {
decoration:
Decoration::SpecId {
specialization_constant_id,
},
..
} => Some(*specialization_constant_id),
_ => None,
})
.and_then(|constant_id| {
let size = match spirv.id(result_type_id).instruction() {
Instruction::TypeBool { .. } => {
std::mem::size_of::<ash::vk::Bool32>() as DeviceSize
}
_ => size_of_type(spirv, result_type_id)
.expect("Found runtime-sized specialization constant"),
};
Some((constant_id, SpecializationConstantRequirements { size }))
}),
_ => None,
}
})
.collect()
}
fn shader_interface(
spirv: &Spirv,
interface: &[Id],
filter_storage_class: StorageClass,
ignore_first_array: bool,
) -> ShaderInterface {
let elements: Vec<_> = interface
.iter()
.filter_map(|&id| {
let (result_type_id, result_id) = match spirv.id(id).instruction() {
&Instruction::Variable {
result_type_id,
result_id,
ref storage_class,
..
} if storage_class == &filter_storage_class => (result_type_id, result_id),
_ => return None,
};
if is_builtin(spirv, result_id) {
return None;
}
let id_info = spirv.id(result_id);
let name = id_info
.iter_name()
.find_map(|instruction| match instruction {
Instruction::Name { name, .. } => Some(Cow::Owned(name.to_owned())),
_ => None,
});
let location = id_info
.iter_decoration()
.find_map(|instruction| match instruction {
Instruction::Decorate {
decoration: Decoration::Location { location },
..
} => Some(*location),
_ => None,
})
.unwrap_or_else(|| {
panic!(
"Input/output variable with id {} (name {:?}) is missing a location",
result_id, name,
)
});
let component = id_info
.iter_decoration()
.find_map(|instruction| match instruction {
Instruction::Decorate {
decoration: Decoration::Component { component },
..
} => Some(*component),
_ => None,
})
.unwrap_or(0);
let ty = shader_interface_type_of(spirv, result_type_id, ignore_first_array);
assert!(ty.num_elements >= 1);
Some(ShaderInterfaceEntry {
location,
component,
ty,
name,
})
})
.collect();
for (offset, element1) in elements.iter().enumerate() {
for element2 in elements.iter().skip(offset + 1) {
if element1.location == element2.location
|| (element1.location < element2.location
&& element1.location + element1.ty.num_locations() > element2.location)
|| (element2.location < element1.location
&& element2.location + element2.ty.num_locations() > element1.location)
{
panic!(
"The locations of attributes `{:?}` ({}..{}) and `{:?}` ({}..{}) overlap",
element1.name,
element1.location,
element1.location + element1.ty.num_locations(),
element2.name,
element2.location,
element2.location + element2.ty.num_locations(),
);
}
}
}
ShaderInterface { elements }
}
fn size_of_type(spirv: &Spirv, id: Id) -> Option<DeviceSize> {
let id_info = spirv.id(id);
match id_info.instruction() {
Instruction::TypeBool { .. } => {
panic!("Can't put booleans in structs")
}
Instruction::TypeInt { width, .. } | Instruction::TypeFloat { width, .. } => {
assert!(width % 8 == 0);
Some(*width as DeviceSize / 8)
}
&Instruction::TypeVector {
component_type,
component_count,
..
} => size_of_type(spirv, component_type)
.map(|component_size| component_size * component_count as DeviceSize),
&Instruction::TypeMatrix {
column_type,
column_count,
..
} => {
size_of_type(spirv, column_type)
.map(|column_size| column_size * column_count as DeviceSize)
}
&Instruction::TypeArray { length, .. } => {
let stride = id_info
.iter_decoration()
.find_map(|instruction| match instruction {
Instruction::Decorate {
decoration: Decoration::ArrayStride { array_stride },
..
} => Some(*array_stride),
_ => None,
})
.unwrap();
let length = match spirv.id(length).instruction() {
&Instruction::Constant { ref value, .. } => Some(
value
.iter()
.rev()
.fold(0u64, |a, &b| (a << 32) | b as DeviceSize),
),
_ => None,
}
.unwrap();
Some(stride as DeviceSize * length)
}
Instruction::TypeRuntimeArray { .. } => None,
Instruction::TypeStruct { member_types, .. } => {
let mut end_of_struct = 0;
for (&member, member_info) in member_types.iter().zip(id_info.iter_members()) {
if member_info.iter_decoration().any(|instruction| {
matches!(
instruction,
Instruction::MemberDecorate {
decoration: Decoration::BuiltIn { .. },
..
}
)
}) {
return None;
}
let offset =
member_info
.iter_decoration()
.find_map(|instruction| match instruction {
Instruction::MemberDecorate {
decoration: Decoration::Offset { byte_offset },
..
} => Some(*byte_offset),
_ => None,
})?;
let size = size_of_type(spirv, member)?;
end_of_struct = end_of_struct.max(offset as DeviceSize + size);
}
Some(end_of_struct)
}
_ => panic!("Type {} not found", id),
}
}
fn offset_of_struct(spirv: &Spirv, id: Id) -> u32 {
spirv
.id(id)
.iter_members()
.map(|member_info| {
member_info
.iter_decoration()
.find_map(|instruction| match instruction {
Instruction::MemberDecorate {
decoration: Decoration::Offset { byte_offset },
..
} => Some(*byte_offset),
_ => None,
})
})
.flatten()
.min()
.unwrap_or(0)
}
fn shader_interface_type_of(
spirv: &Spirv,
id: Id,
ignore_first_array: bool,
) -> ShaderInterfaceEntryType {
match spirv.id(id).instruction() {
&Instruction::TypeInt {
width, signedness, ..
} => {
assert!(!ignore_first_array);
ShaderInterfaceEntryType {
base_type: match signedness {
0 => ShaderScalarType::Uint,
1 => ShaderScalarType::Sint,
_ => unreachable!(),
},
num_components: 1,
num_elements: 1,
is_64bit: match width {
8 | 16 | 32 => false,
64 => true,
_ => unimplemented!(),
},
}
}
&Instruction::TypeFloat { width, .. } => {
assert!(!ignore_first_array);
ShaderInterfaceEntryType {
base_type: ShaderScalarType::Float,
num_components: 1,
num_elements: 1,
is_64bit: match width {
16 | 32 => false,
64 => true,
_ => unimplemented!(),
},
}
}
&Instruction::TypeVector {
component_type,
component_count,
..
} => {
assert!(!ignore_first_array);
ShaderInterfaceEntryType {
num_components: component_count,
..shader_interface_type_of(spirv, component_type, false)
}
}
&Instruction::TypeMatrix {
column_type,
column_count,
..
} => {
assert!(!ignore_first_array);
ShaderInterfaceEntryType {
num_elements: column_count,
..shader_interface_type_of(spirv, column_type, false)
}
}
&Instruction::TypeArray {
element_type,
length,
..
} => {
if ignore_first_array {
shader_interface_type_of(spirv, element_type, false)
} else {
let mut ty = shader_interface_type_of(spirv, element_type, false);
let num_elements = spirv
.instructions()
.iter()
.filter_map(|e| match e {
&Instruction::Constant {
result_id,
ref value,
..
} if result_id == length => Some(value.clone()),
_ => None,
})
.next()
.expect("failed to find array length")
.iter()
.rev()
.fold(0u64, |a, &b| (a << 32) | b as u64)
as u32;
ty.num_elements *= num_elements;
ty
}
}
&Instruction::TypePointer { ty, .. } => {
shader_interface_type_of(spirv, ty, ignore_first_array)
}
_ => panic!("Type {} not found or invalid", id),
}
}
fn is_builtin(spirv: &Spirv, id: Id) -> bool {
let id_info = spirv.id(id);
if id_info.iter_decoration().any(|instruction| {
matches!(
instruction,
Instruction::Decorate {
decoration: Decoration::BuiltIn { .. },
..
}
)
}) {
return true;
}
if id_info
.iter_members()
.flat_map(|member_info| member_info.iter_decoration())
.any(|instruction| {
matches!(
instruction,
Instruction::MemberDecorate {
decoration: Decoration::BuiltIn { .. },
..
}
)
})
{
return true;
}
match id_info.instruction() {
Instruction::Variable { result_type_id, .. } => {
return is_builtin(spirv, *result_type_id);
}
Instruction::TypeArray { element_type, .. } => {
return is_builtin(spirv, *element_type);
}
Instruction::TypeRuntimeArray { element_type, .. } => {
return is_builtin(spirv, *element_type);
}
Instruction::TypeStruct { member_types, .. } => {
if member_types.iter().any(|ty| is_builtin(spirv, *ty)) {
return true;
}
}
Instruction::TypePointer { ty, .. } => {
return is_builtin(spirv, *ty);
}
_ => (),
}
false
}