/// How to derive the type of `OpAccessChain` instructions from Naga IR. /// /// Most of the time, we compile Naga IR to SPIR-V instructions whose result /// types are simply the direct SPIR-V analog of the Naga IR's. But in some /// cases, the Naga IR and SPIR-V types need to diverge. /// /// This enum specifies how [`BlockContext::write_access_chain`] should /// choose a SPIR-V result type for the `OpAccessChain` it generates, based on /// the type of the given Naga IR [`Expression`] it's generating code for. /// /// [`Expression`]: crate::Expression #[derive(Copy, Clone)] enum AccessTypeAdjustment { /// No adjustment needed: the SPIR-V type should be the direct /// analog of the Naga IR expression type. /// /// For most access chains, this is the right thing: the Naga IR access /// expression produces a [`Pointer`] to the element / component, and the /// SPIR-V `OpAccessChain` instruction does the same. /// /// [`Pointer`]: crate::TypeInner::Pointer
None,
/// The SPIR-V type should be an `OpPointer` to the direct analog of the /// Naga IR expression's type. /// /// This is necessary for indexing binding arrays in the [`Handle`] address /// space: /// /// - In Naga IR, referencing a binding array [`GlobalVariable`] in the /// [`Handle`] address space produces a value of type [`BindingArray`], /// not a pointer to such. And [`Access`] and [`AccessIndex`] expressions /// operate on handle binding arrays by value, and produce handle values, /// not pointers. /// /// - In SPIR-V, a binding array `OpVariable` produces a pointer to an /// array, and `OpAccessChain` instructions operate on pointers, /// regardless of whether the elements are opaque types or not. /// /// See also the documentation for [`BindingArray`]. /// /// [`Handle`]: crate::AddressSpace::Handle /// [`GlobalVariable`]: crate::GlobalVariable /// [`BindingArray`]: crate::TypeInner::BindingArray /// [`Access`]: crate::Expression::Access /// [`AccessIndex`]: crate::Expression::AccessIndex
IntroducePointer(spirv::StorageClass),
/// The SPIR-V type should be an `OpPointer` to the std140 layout /// compatible variant of the Naga IR expression's base type. /// /// This is used when accessing a type through an [`AddressSpace::Uniform`] /// pointer in cases where the original type is incompatible with std140 /// layout requirements and we have therefore declared the uniform to be of /// an alternative std140 compliant type. /// /// [`AddressSpace::Uniform`]: crate::AddressSpace::Uniform
UseStd140CompatType,
}
/// The results of emitting code for a left-hand-side expression. /// /// On success, `write_access_chain` returns one of these. enum ExpressionPointer { /// The pointer to the expression's value is available, as the value of the /// expression with the given id.
Ready { pointer_id: Word },
/// The access expression must be conditional on the value of `condition`, a boolean /// expression that is true if all indices are in bounds. If `condition` is true, then /// `access` is an `OpAccessChain` instruction that will compute a pointer to the /// expression's value. If `condition` is false, then executing `access` would be /// undefined behavior.
Conditional {
condition: Word,
access: Instruction,
},
}
/// The termination statement to be added to the end of the block enum BlockExit { /// Generates an OpReturn (void return) Return, /// Generates an OpBranch to the specified block
Branch { /// The branch target block
target: Word,
}, /// Translates a loop `break if` into an `OpBranchConditional` to the /// merge block if true (the merge block is passed through [`LoopContext::break_id`] /// or else to the loop header (passed through [`preamble_id`]) /// /// [`preamble_id`]: Self::BreakIf::preamble_id
BreakIf { /// The condition of the `break if`
condition: Handle<crate::Expression>, /// The loop header block id
preamble_id: Word,
},
}
/// What code generation did with a provided [`BlockExit`] value. /// /// A function that accepts a [`BlockExit`] argument should return a value of /// this type, to indicate whether the code it generated ended up using the /// provided exit, or ignored it and did a non-local exit of some other kind /// (say, [`Break`] or [`Continue`]). Some callers must use this information to /// decide whether to generate the target block at all. /// /// [`Break`]: Statement::Break /// [`Continue`]: Statement::Continue #[must_use] enum BlockExitDisposition { /// The generated code used the provided `BlockExit` value. If it included a /// block label, the caller should be sure to actually emit the block it /// refers to.
Used,
/// The generated code did not use the provided `BlockExit` value. If it /// included a block label, the caller should not bother to actually emit /// the block it refers to, unless it knows the block is needed for /// something else.
Discarded,
}
impl Writer { // Flip Y coordinate to adjust for coordinate space difference // between SPIR-V and our IR. // The `position_id` argument is a pointer to a `vecN<f32>`, // whose `y` component we will negate. fn write_epilogue_position_y_flip(
&mutself,
position_id: Word,
body: &mut Vec<Instruction>,
) -> Result<(), Error> { let float_ptr_type_id = self.get_f32_pointer_type_id(spirv::StorageClass::Output); let index_y_id = self.get_index_constant(1); let access_id = self.id_gen.next();
body.push(Instruction::access_chain(
float_ptr_type_id,
access_id,
position_id,
&[index_y_id],
));
let float_type_id = self.get_f32_type_id(); let load_id = self.id_gen.next();
body.push(Instruction::load(float_type_id, load_id, access_id, None));
let neg_id = self.id_gen.next();
body.push(Instruction::unary(
spirv::Op::FNegate,
float_type_id,
neg_id,
load_id,
));
// Clamp fragment depth between 0 and 1. fn write_epilogue_frag_depth_clamp(
&mutself,
frag_depth_id: Word,
body: &mut Vec<Instruction>,
) -> Result<(), Error> { let float_type_id = self.get_f32_type_id(); let zero_scalar_id = self.get_constant_scalar(crate::Literal::F32(0.0)); let one_scalar_id = self.get_constant_scalar(crate::Literal::F32(1.0));
let original_id = self.id_gen.next();
body.push(Instruction::load(
float_type_id,
original_id,
frag_depth_id,
None,
));
impl BlockContext<'_> { /// Generates code to ensure that a loop is bounded. Should be called immediately /// after adding the OpLoopMerge instruction to `block`. This function will /// [`consume()`](crate::back::spv::Function::consume) `block` and append its /// instructions to a new [`Block`], which will be returned to the caller for it to /// consumed prior to writing the loop body. /// /// Additionally this function will populate [`force_loop_bounding_vars`](crate::back::spv::Function::force_loop_bounding_vars), /// ensuring that [`Function::to_words()`](crate::back::spv::Function::to_words) will /// declare the required variables. /// /// See [`crate::back::msl::Writer::gen_force_bounded_loop_statements`] for details /// of why this is required. fn write_force_bounded_loop_instructions(&mutself, mut block: Block, merge_id: Word) -> Block { let uint_type_id = self.writer.get_u32_type_id(); let uint2_type_id = self.writer.get_vec2u_type_id(); let uint2_ptr_type_id = self
.writer
.get_vec2u_pointer_type_id(spirv::StorageClass::Function); let bool_type_id = self.writer.get_bool_type_id(); let bool2_type_id = self.writer.get_vec2_bool_type_id(); let zero_uint_const_id = self.writer.get_constant_scalar(crate::Literal::U32(0)); let zero_uint2_const_id = self.writer.get_constant_composite(
LookupType::Local(LocalType::Numeric(NumericType::Vector {
size: crate::VectorSize::Bi,
scalar: crate::Scalar::U32,
})),
&[zero_uint_const_id, zero_uint_const_id],
); let one_uint_const_id = self.writer.get_constant_scalar(crate::Literal::U32(1)); let max_uint_const_id = self
.writer
.get_constant_scalar(crate::Literal::U32(u32::MAX)); let max_uint2_const_id = self.writer.get_constant_composite(
LookupType::Local(LocalType::Numeric(NumericType::Vector {
size: crate::VectorSize::Bi,
scalar: crate::Scalar::U32,
})),
&[max_uint_const_id, max_uint_const_id],
);
let loop_counter_var_id = self.gen_id(); ifself.writer.flags.contains(WriterFlags::DEBUG) { self.writer
.debugs
.push(Instruction::name(loop_counter_var_id, "loop_bound"));
} let var = super::LocalVariable {
id: loop_counter_var_id,
instruction: Instruction::variable(
uint2_ptr_type_id,
loop_counter_var_id,
spirv::StorageClass::Function,
Some(max_uint2_const_id),
),
}; self.function.force_loop_bounding_vars.push(var);
// Load the current loop counter value from its variable. We use a vec2<u32> to // simulate a 64-bit counter. let load_id = self.gen_id();
block.body.push(Instruction::load(
uint2_type_id,
load_id,
loop_counter_var_id,
None,
));
// If both the high and low u32s have reached 0 then break. ie // if (all(eq(loop_counter, vec2(0)))) { break; } let eq_id = self.gen_id();
block.body.push(Instruction::binary(
spirv::Op::IEqual,
bool2_type_id,
eq_id,
zero_uint2_const_id,
load_id,
)); let all_eq_id = self.gen_id();
block.body.push(Instruction::relational(
spirv::Op::All,
bool_type_id,
all_eq_id,
eq_id,
));
// To simulate a 64-bit counter we always decrement the low u32, and decrement // the high u32 when the low u32 overflows. ie // counter -= vec2(select(0u, 1u, counter.y == 0), 1u); // Count down from u32::MAX rather than up from 0 to avoid hang on // certain Intel drivers. See <https://github.com/gfx-rs/wgpu/issues/7319>. let low_id = self.gen_id();
block.body.push(Instruction::composite_extract(
uint_type_id,
low_id,
load_id,
&[1],
)); let low_overflow_id = self.gen_id();
block.body.push(Instruction::binary(
spirv::Op::IEqual,
bool_type_id,
low_overflow_id,
low_id,
zero_uint_const_id,
)); let carry_bit_id = self.gen_id();
block.body.push(Instruction::select(
uint_type_id,
carry_bit_id,
low_overflow_id,
one_uint_const_id,
zero_uint_const_id,
)); let decrement_id = self.gen_id();
block.body.push(Instruction::composite_construct(
uint2_type_id,
decrement_id,
&[carry_bit_id, one_uint_const_id],
)); let result_id = self.gen_id();
block.body.push(Instruction::binary(
spirv::Op::ISub,
uint2_type_id,
result_id,
load_id,
decrement_id,
));
block
.body
.push(Instruction::store(loop_counter_var_id, result_id, None));
block
}
/// If `pointer` refers to an access chain that contains a dynamic indexing /// of a two-row matrix in the [`Uniform`] address space, write code to /// access the value returning the ID of the result. Else return None. /// /// Two-row matrices in the uniform address space will have been declared /// using a alternative std140 layout compatible type, where each column is /// a member of a containing struct. As a result, SPIR-V is unable to access /// its columns with a non-constant index. To work around this limitation /// this function will call [`Self::write_checked_load()`] to load the /// matrix itself, which handles conversion from the std140 compatible type /// to the real matrix type. It then calls a [`wrapper function`] to obtain /// the correct column from the matrix, and possibly extracts a component /// from the vector too. /// /// [`Uniform`]: crate::AddressSpace::Uniform /// [`wrapper function`]: super::Writer::write_wrapped_matcx2_get_column fn maybe_write_uniform_matcx2_dynamic_access(
&mutself,
pointer: Handle<crate::Expression>,
block: &mut Block,
) -> Result<Option<Word>, Error> { // If this access chain contains a dynamic matrix access, `pointer` is // either a pointer to a vector (the column) or a scalar (a component // within the column). In either case grab the pointer to the column, // and remember the component index if there is one. If `pointer` // points to any other type we're not interested. let (column_pointer, component_index) = matchself.fun_info[pointer]
.ty
.inner_with(&self.ir_module.types)
.pointer_base_type()
{
Some(resolution) => match *resolution.inner_with(&self.ir_module.types) { crate::TypeInner::Scalar(_) => matchself.ir_function.expressions[pointer] { crate::Expression::Access { base, index } => {
(base, Some(GuardedIndex::Expression(index)))
} crate::Expression::AccessIndex { base, index } => {
(base, Some(GuardedIndex::Known(index)))
}
_ => return Ok(None),
}, crate::TypeInner::Vector { .. } => (pointer, None),
_ => return Ok(None),
},
None => return Ok(None),
};
// Ensure the column is accessed with a dynamic index (i.e. // `Expression::Access`), and grab the pointer to the matrix. letcrate::Expression::Access {
base: matrix_pointer,
index: column_index,
} = self.ir_function.expressions[column_pointer] else { return Ok(None);
};
// Ensure the matrix pointer is in the uniform address space. letcrate::TypeInner::Pointer {
base: matrix_pointer_base_type,
space: crate::AddressSpace::Uniform,
} = *self.fun_info[matrix_pointer]
.ty
.inner_with(&self.ir_module.types) else { return Ok(None);
};
// Ensure the matrix pointer actually points to a Cx2 matrix. letcrate::TypeInner::Matrix {
columns,
rows: rows @ crate::VectorSize::Bi,
scalar,
} = self.ir_module.types[matrix_pointer_base_type].inner else { return Ok(None);
};
let matrix_type_id = self.get_numeric_type_id(NumericType::Matrix {
columns,
rows,
scalar,
}); let column_type_id = self.get_numeric_type_id(NumericType::Vector { size: rows, scalar }); let component_type_id = self.get_numeric_type_id(NumericType::Scalar(scalar)); let get_column_function_id = self.writer.wrapped_functions
[&WrappedFunction::MatCx2GetColumn {
r#type: matrix_pointer_base_type,
}];
let matrix_load_id = self.write_checked_load(
matrix_pointer,
block,
AccessTypeAdjustment::None,
matrix_type_id,
)?;
// Naga IR allows the index to be either an I32 or U32 but our wrapper // function expects a U32 argument, so convert it if required. let column_index_id = match *self.fun_info[column_index]
.ty
.inner_with(&self.ir_module.types)
{ crate::TypeInner::Scalar(crate::Scalar {
kind: crate::ScalarKind::Uint,
..
}) => self.cached[column_index], crate::TypeInner::Scalar(crate::Scalar {
kind: crate::ScalarKind::Sint,
..
}) => { let cast_id = self.gen_id(); let u32_type_id = self.writer.get_u32_type_id();
block.body.push(Instruction::unary(
spirv::Op::Bitcast,
u32_type_id,
cast_id, self.cached[column_index],
));
cast_id
}
_ => return Err(Error::Validation("Matrix access index must be u32 or i32")),
}; let column_id = self.gen_id();
block.body.push(Instruction::function_call(
column_type_id,
column_id,
get_column_function_id,
&[matrix_load_id, column_index_id],
)); let result_id = match component_index {
Some(index) => self.write_vector_access(
component_type_id,
column_pointer,
Some(column_id),
index,
block,
)?,
None => column_id,
};
Ok(Some(result_id))
}
/// If `pointer` refers to two-row matrix that is a member of a struct in /// the [`Uniform`] address space, write code to load the matrix returning /// the ID of the result. Else return None. /// /// Two-row matrices that are struct members in the uniform address space /// will have been decomposed such that the struct contains a separate /// vector member for each column of the matrix. This function will load /// each column separately from the containing struct, then composite them /// into the real matrix type. /// /// [`Uniform`]: crate::AddressSpace::Uniform fn maybe_write_load_uniform_matcx2_struct_member(
&mutself,
pointer: Handle<crate::Expression>,
block: &mut Block,
) -> Result<Option<Word>, Error> { // Check this is a uniform address space pointer to a two-row matrix. letcrate::TypeInner::Pointer {
base: matrix_type,
space: space @ crate::AddressSpace::Uniform,
} = *self.fun_info[pointer].ty.inner_with(&self.ir_module.types) else { return Ok(None);
};
// Check this is a struct member. Note struct members can only be // accessed with `AccessIndex`. letcrate::Expression::AccessIndex {
base: struct_pointer,
index: member_index,
} = self.ir_function.expressions[pointer] else { return Ok(None);
};
let matrix_type_id = self.get_numeric_type_id(NumericType::Matrix {
columns,
rows,
scalar,
}); let column_type_id = self.get_numeric_type_id(NumericType::Vector { size: rows, scalar }); let column_pointer_type_id = self.get_pointer_type_id(column_type_id, map_storage_class(space)); let column0_index = self.writer.std140_compat_uniform_types[&struct_type].member_indices
[member_index as usize]; let column_indices = (0..columns as u32)
.map(|c| self.get_index_constant(column0_index + c))
.collect::<ArrayVec<_, 4>>();
// Load each column from the struct, then composite into the real // matrix type. let load_mat_from_struct =
|struct_pointer_id: Word, id_gen: &mut IdGenerator, block: &style='color:red'>mut Block| -> Word { letmut column_ids: ArrayVec<Word, 4> = ArrayVec::new(); for index in &column_indices { let column_pointer_id = id_gen.next();
block.body.push(Instruction::access_chain(
column_pointer_type_id,
column_pointer_id,
struct_pointer_id,
&[*index],
)); let column_id = id_gen.next();
block.body.push(Instruction::load(
column_type_id,
column_id,
column_pointer_id,
None,
));
column_ids.push(column_id);
} let result_id = id_gen.next();
block.body.push(Instruction::composite_construct(
matrix_type_id,
result_id,
&column_ids,
));
result_id
};
let id = self.gen_id();
block.body.push(Instruction::composite_construct(
result_type_id,
id,
&self.temp_list,
));
id
}
} crate::Expression::Splat { size, value } => { let value_id = self.cached[value]; let components = &[value_id; 4][..size as usize];
ifself.expression_constness.is_const(expr_handle) { let ty = self
.writer
.get_expression_lookup_type(&self.fun_info[expr_handle].ty); self.writer.get_constant_composite(ty, components)
} else { let id = self.gen_id();
block.body.push(Instruction::composite_construct(
result_type_id,
id,
components,
));
id
}
} crate::Expression::Access { base, index } => { let base_ty_inner = self.fun_info[base].ty.inner_with(&self.ir_module.types); match *base_ty_inner { crate::TypeInner::Pointer { .. } | crate::TypeInner::ValuePointer { .. } => { // When we have a chain of `Access` and `AccessIndex` expressions // operating on pointers, we want to generate a single // `OpAccessChain` instruction for the whole chain. Put off // generating any code for this until we find the `Expression` // that actually dereferences the pointer. 0
}
_ ifself.function.spilled_accesses.contains(base) => { // As far as Naga IR is concerned, this expression does not yield // a pointer (we just checked, above), but this backend spilled it // to a temporary variable, so SPIR-V thinks we're accessing it // via a pointer.
// Since the base expression was spilled, mark this access to it // as spilled, too. self.function.spilled_accesses.insert(expr_handle); self.maybe_access_spilled_composite(expr_handle, block, result_type_id)?
} crate::TypeInner::Vector { .. } => self.write_vector_access(
result_type_id,
base,
None,
GuardedIndex::Expression(index),
block,
)?, crate::TypeInner::Array { .. } | crate::TypeInner::Matrix { .. } => { // See if `index` is known at compile time. match GuardedIndex::from_expression(
index,
&self.ir_function.expressions, self.ir_module,
) {
GuardedIndex::Known(value) => { // If `index` is known and in bounds, we can just use // `OpCompositeExtract`. // // At the moment, validation rejects programs if this // index is out of bounds, so we don't need bounds checks. // However, that rejection is incorrect, since WGSL says // that `let` bindings are not constant expressions // (#6396). So eventually we will need to emulate bounds // checks here. let id = self.gen_id(); let base_id = self.cached[base];
block.body.push(Instruction::composite_extract(
result_type_id,
id,
base_id,
&[value],
));
id
}
GuardedIndex::Expression(_) => { // We are subscripting an array or matrix that is not // behind a pointer, using an index computed at runtime. // SPIR-V has no instructions that do this, so the best we // can do is spill the value to a new temporary variable, // at which point we can get a pointer to that and just // use `OpAccessChain` in the usual way. self.spill_to_internal_variable(base, block);
// Since the base was spilled, mark this access to it as // spilled, too. self.function.spilled_accesses.insert(expr_handle); self.maybe_access_spilled_composite(
expr_handle,
block,
result_type_id,
)?
}
}
} crate::TypeInner::BindingArray {
base: binding_type, ..
} => { // Only binding arrays in the `Handle` address space will take // this path, since we handled the `Pointer` case above. let result_id = matchself.write_access_chain(
expr_handle,
block,
AccessTypeAdjustment::IntroducePointer(
spirv::StorageClass::UniformConstant,
),
)? {
ExpressionPointer::Ready { pointer_id } => pointer_id,
ExpressionPointer::Conditional { .. } => { return Err(Error::FeatureNotImplemented( "Texture array out-of-bounds handling",
));
}
};
let binding_type_id = self.get_handle_type_id(binding_type);
let load_id = self.gen_id();
block.body.push(Instruction::load(
binding_type_id,
load_id,
result_id,
None,
));
// Subsequent image operations require the image/sampler to be decorated as NonUniform // if the image/sampler binding array was accessed with a non-uniform index // see VUID-RuntimeSpirv-NonUniform-06274 ifself.fun_info[index].uniformity.non_uniform_result.is_some() { self.writer
.decorate_non_uniform_binding_array_access(load_id)?;
}
load_id
} ref other => {
log::error!( "Unable to access base {:?} of type {:?}", self.ir_function.expressions[base],
other
); return Err(Error::Validation( "only vectors and arrays may be dynamically indexed by value",
));
}
}
} crate::Expression::AccessIndex { base, index } => { match *self.fun_info[base].ty.inner_with(&self.ir_module.types) { crate::TypeInner::Pointer { .. } | crate::TypeInner::ValuePointer { .. } => { // When we have a chain of `Access` and `AccessIndex` expressions // operating on pointers, we want to generate a single // `OpAccessChain` instruction for the whole chain. Put off // generating any code for this until we find the `Expression` // that actually dereferences the pointer. 0
}
_ ifself.function.spilled_accesses.contains(base) => { // As far as Naga IR is concerned, this expression does not yield // a pointer (we just checked, above), but this backend spilled it // to a temporary variable, so SPIR-V thinks we're accessing it // via a pointer.
// Since the base expression was spilled, mark this access to it // as spilled, too. self.function.spilled_accesses.insert(expr_handle); self.maybe_access_spilled_composite(expr_handle, block, result_type_id)?
} crate::TypeInner::Vector { .. }
| crate::TypeInner::Matrix { .. }
| crate::TypeInner::Array { .. }
| crate::TypeInner::Struct { .. } => { // We never need bounds checks here: dynamically sized arrays can // only appear behind pointers, and are thus handled by the // `is_intermediate` case above. Everything else's size is // statically known and checked in validation. let id = self.gen_id(); let base_id = self.cached[base];
block.body.push(Instruction::composite_extract(
result_type_id,
id,
base_id,
&[index],
));
id
} crate::TypeInner::BindingArray {
base: binding_type, ..
} => { // Only binding arrays in the `Handle` address space will take // this path, since we handled the `Pointer` case above. let result_id = matchself.write_access_chain(
expr_handle,
block,
AccessTypeAdjustment::IntroducePointer(
spirv::StorageClass::UniformConstant,
),
)? {
ExpressionPointer::Ready { pointer_id } => pointer_id,
ExpressionPointer::Conditional { .. } => { return Err(Error::FeatureNotImplemented( "Texture array out-of-bounds handling",
));
}
};
let binding_type_id = self.get_handle_type_id(binding_type);
let load_id = self.gen_id();
block.body.push(Instruction::load(
binding_type_id,
load_id,
result_id,
None,
));
load_id
} ref other => {
log::error!("Unable to access index of {other:?}"); return Err(Error::FeatureNotImplemented("access index for type"));
}
}
} crate::Expression::GlobalVariable(handle) => { self.writer.global_variables[handle].access_id
} crate::Expression::Swizzle {
size,
vector,
pattern,
} => { let vector_id = self.cached[vector]; self.temp_list.clear(); for &sc in pattern[..size as usize].iter() { self.temp_list.push(sc as Word);
} let id = self.gen_id();
block.body.push(Instruction::vector_shuffle(
result_type_id,
id,
vector_id,
vector_id,
&self.temp_list,
));
id
} crate::Expression::Unary { op, expr } => { let id = self.gen_id(); let expr_id = self.cached[expr]; let expr_ty_inner = self.fun_info[expr].ty.inner_with(&self.ir_module.types);
let spirv_op = match op { crate::UnaryOperator::Negate => match expr_ty_inner.scalar_kind() {
Some(crate::ScalarKind::Float) => spirv::Op::FNegate,
Some(crate::ScalarKind::Sint) => spirv::Op::SNegate,
_ => return Err(Error::Validation("Unexpected kind for negation")),
}, crate::UnaryOperator::LogicalNot => spirv::Op::LogicalNot, crate::UnaryOperator::BitwiseNot => spirv::Op::Not,
};
block
.body
.push(Instruction::unary(spirv_op, result_type_id, id, expr_id));
id
} crate::Expression::Binary { op, left, right } => { let id = self.gen_id(); let left_id = self.cached[left]; let right_id = self.cached[right]; let left_type_id = self.get_expression_type_id(&self.fun_info[left].ty); let right_type_id = self.get_expression_type_id(&self.fun_info[right].ty);
let arg0_id = self.cached[arg]; let arg_ty = self.fun_info[arg].ty.inner_with(&self.ir_module.types); let arg_scalar_kind = arg_ty.scalar_kind(); let arg1_id = match arg1 {
Some(handle) => self.cached[handle],
None => 0,
}; let arg2_id = match arg2 {
Some(handle) => self.cached[handle],
None => 0,
}; let arg3_id = match arg3 {
Some(handle) => self.cached[handle],
None => 0,
};
let id = self.gen_id(); let math_op = match fun { // comparison
Mf::Abs => { match arg_scalar_kind {
Some(crate::ScalarKind::Float) => {
MathOp::Ext(spirv::GlslStd450Op::FAbs)
}
Some(crate::ScalarKind::Sint) => MathOp::Ext(spirv::GlslStd450Op::SAbs),
Some(crate::ScalarKind::Uint) => {
MathOp::Custom(Instruction::unary(
spirv::Op::CopyObject, // do nothing
result_type_id,
id,
arg0_id,
))
}
other => unimplemented!("Unexpected abs({:?})", other),
}
}
Mf::Min => MathOp::Ext(match arg_scalar_kind {
Some(crate::ScalarKind::Float) => spirv::GlslStd450Op::FMin,
Some(crate::ScalarKind::Sint) => spirv::GlslStd450Op::SMin,
Some(crate::ScalarKind::Uint) => spirv::GlslStd450Op::UMin,
other => unimplemented!("Unexpected min({:?})", other),
}),
Mf::Max => MathOp::Ext(match arg_scalar_kind {
Some(crate::ScalarKind::Float) => spirv::GlslStd450Op::FMax,
Some(crate::ScalarKind::Sint) => spirv::GlslStd450Op::SMax,
Some(crate::ScalarKind::Uint) => spirv::GlslStd450Op::UMax,
other => unimplemented!("Unexpected max({:?})", other),
}),
Mf::Clamp => match arg_scalar_kind { // Clamp is undefined if min > max. In practice this means it can use a median-of-three // instruction to determine the value. This is fine according to the WGSL spec for float // clamp, but integer clamp _must_ use min-max. As such we write out min/max.
Some(crate::ScalarKind::Float) => MathOp::Ext(spirv::GlslStd450Op::FClamp),
Some(_) => { let (min_op, max_op) = match arg_scalar_kind {
Some(crate::ScalarKind::Sint) => {
(spirv::GlslStd450Op::SMin, spirv::GlslStd450Op::SMax)
}
Some(crate::ScalarKind::Uint) => {
(spirv::GlslStd450Op::UMin, spirv::GlslStd450Op::UMax)
}
_ => unreachable!(),
};
let op = match fun {
Mf::Dot4I8Packed => spirv::Op::SDot,
Mf::Dot4U8Packed => spirv::Op::UDot,
_ => unreachable!(),
};
block.body.push(Instruction::ternary(
op,
result_type_id,
id,
arg0_id,
arg1_id,
spirv::PackedVectorFormat::PackedVectorFormat4x8Bit as Word,
));
} else { // Fall back to a polyfill since `PackedVectorFormat4x8Bit` is not available. let (extract_op, arg0_id, arg1_id) = match fun {
Mf::Dot4U8Packed => (spirv::Op::BitFieldUExtract, arg0_id, arg1_id),
Mf::Dot4I8Packed => { // Convert both packed arguments to signed integers so that we can apply the // `BitFieldSExtract` operation on them in `write_dot_product` below. let new_arg0_id = self.gen_id();
block.body.push(Instruction::unary(
spirv::Op::Bitcast,
result_type_id,
new_arg0_id,
arg0_id,
));
let new_arg1_id = self.gen_id();
block.body.push(Instruction::unary(
spirv::Op::Bitcast,
result_type_id,
new_arg1_id,
arg1_id,
));
MathOp::Custom(Instruction::binary(
spirv::Op::ISub,
result_type_id,
id,
int_id,
msb_id,
))
}
Mf::CountOneBits => MathOp::Custom(Instruction::unary(
spirv::Op::BitCount,
result_type_id,
id,
arg0_id,
)),
Mf::ExtractBits => { let op = match arg_scalar_kind {
Some(crate::ScalarKind::Uint) => spirv::Op::BitFieldUExtract,
Some(crate::ScalarKind::Sint) => spirv::Op::BitFieldSExtract,
other => unimplemented!("Unexpected sign({:?})", other),
};
// The behavior of ExtractBits is undefined when offset + count > bit_width. We need // to first sanitize the offset and count first. If we don't do this, AMD and Intel // will return out-of-spec values if the extracted range is not within the bit width. // // This encodes the exact formula specified by the wgsl spec: // https://gpuweb.github.io/gpuweb/wgsl/#extractBits-unsigned-builtin // // w = sizeof(x) * 8 // o = min(offset, w) // tmp = w - o // c = min(count, tmp) // // bitfieldExtract(x, o, c)
let bit_width = arg_ty.scalar_width().unwrap() * 8; let width_constant = self
.writer
.get_constant_scalar(crate::Literal::U32(bit_width as u32));
let u32_type = self.get_numeric_type_id(NumericType::Scalar(crate::Scalar::U32));
// o = min(offset, w) let offset_id = self.gen_id();
block.body.push(Instruction::ext_inst_gl_op( self.writer.gl450_ext_inst_id,
spirv::GlslStd450Op::UMin,
u32_type,
offset_id,
&[arg1_id, width_constant],
));
// tmp = w - o let max_count_id = self.gen_id();
block.body.push(Instruction::binary(
spirv::Op::ISub,
u32_type,
max_count_id,
width_constant,
offset_id,
));
// c = min(count, tmp) let count_id = self.gen_id();
block.body.push(Instruction::ext_inst_gl_op( self.writer.gl450_ext_inst_id,
spirv::GlslStd450Op::UMin,
u32_type,
count_id,
&[arg2_id, max_count_id],
));
MathOp::Custom(Instruction::ternary(
op,
result_type_id,
id,
arg0_id,
offset_id,
count_id,
))
}
Mf::InsertBits => { // The behavior of InsertBits has the same undefined behavior as ExtractBits.
let bit_width = arg_ty.scalar_width().unwrap() * 8; let width_constant = self
.writer
.get_constant_scalar(crate::Literal::U32(bit_width as u32));
let u32_type = self.get_numeric_type_id(NumericType::Scalar(crate::Scalar::U32));
// o = min(offset, w) let offset_id = self.gen_id();
block.body.push(Instruction::ext_inst_gl_op( self.writer.gl450_ext_inst_id,
spirv::GlslStd450Op::UMin,
u32_type,
offset_id,
&[arg2_id, width_constant],
));
// tmp = w - o let max_count_id = self.gen_id();
block.body.push(Instruction::binary(
spirv::Op::ISub,
u32_type,
max_count_id,
width_constant,
offset_id,
));
// c = min(count, tmp) let count_id = self.gen_id();
block.body.push(Instruction::ext_inst_gl_op( self.writer.gl450_ext_inst_id,
spirv::GlslStd450Op::UMin,
u32_type,
count_id,
&[arg3_id, max_count_id],
));
MathOp::Custom(Instruction::quaternary(
spirv::Op::BitFieldInsert,
result_type_id,
id,
arg0_id,
arg1_id,
offset_id,
count_id,
))
}
Mf::FirstTrailingBit => MathOp::Ext(spirv::GlslStd450Op::FindILsb),
Mf::FirstLeadingBit => { if arg_ty.scalar_width() == Some(4) { let thing = match arg_scalar_kind {
Some(crate::ScalarKind::Uint) => spirv::GlslStd450Op::FindUMsb,
Some(crate::ScalarKind::Sint) => spirv::GlslStd450Op::FindSMsb,
other => unimplemented!("Unexpected firstLeadingBit({:?})", other),
};
MathOp::Ext(thing)
} else {
unreachable!("This is validated out until a polyfill is implemented. https://github.com/gfx-rs/wgpu/issues/5276");
}
}
Mf::Pack4x8unorm => MathOp::Ext(spirv::GlslStd450Op::PackUnorm4x8),
Mf::Pack4x8snorm => MathOp::Ext(spirv::GlslStd450Op::PackSnorm4x8),
Mf::Pack2x16float => MathOp::Ext(spirv::GlslStd450Op::PackHalf2x16),
Mf::Pack2x16unorm => MathOp::Ext(spirv::GlslStd450Op::PackUnorm2x16),
Mf::Pack2x16snorm => MathOp::Ext(spirv::GlslStd450Op::PackSnorm2x16),
fun @ (Mf::Pack4xI8 | Mf::Pack4xU8 | Mf::Pack4xI8Clamp | Mf::Pack4xU8Clamp) => { let is_signed = matches!(fun, Mf::Pack4xI8 | Mf::Pack4xI8Clamp); let should_clamp = matches!(fun, Mf::Pack4xI8Clamp | Mf::Pack4xU8Clamp);
let condition_ty = self.fun_info[condition]
.ty
.inner_with(&self.ir_module.types); let object_ty = self.fun_info[accept].ty.inner_with(&self.ir_module.types);
let bool_vector_type_id = self.get_numeric_type_id(NumericType::Vector {
size,
scalar: condition_scalar,
});
let id = self.gen_id();
block.body.push(Instruction::composite_construct(
bool_vector_type_id,
id,
&self.temp_list,
));
condition_id = id
}
let instruction =
Instruction::select(result_type_id, id, condition_id, accept_id, reject_id);
block.body.push(instruction);
id
} crate::Expression::Derivative { axis, ctrl, expr } => { usecrate::{DerivativeAxis as Axis, DerivativeControl as Ctrl}; match ctrl {
Ctrl::Coarse | Ctrl::Fine => { self.writer.require_any( "DerivativeControl",
&[spirv::Capability::DerivativeControl],
)?;
}
Ctrl::None => {}
} let id = self.gen_id(); let expr_id = self.cached[expr]; let op = match (axis, ctrl) {
(Axis::X, Ctrl::Coarse) => spirv::Op::DPdxCoarse,
(Axis::X, Ctrl::Fine) => spirv::Op::DPdxFine,
(Axis::X, Ctrl::None) => spirv::Op::DPdx,
(Axis::Y, Ctrl::Coarse) => spirv::Op::DPdyCoarse,
(Axis::Y, Ctrl::Fine) => spirv::Op::DPdyFine,
(Axis::Y, Ctrl::None) => spirv::Op::DPdy,
(Axis::Width, Ctrl::Coarse) => spirv::Op::FwidthCoarse,
(Axis::Width, Ctrl::Fine) => spirv::Op::FwidthFine,
(Axis::Width, Ctrl::None) => spirv::Op::Fwidth,
};
block
.body
.push(Instruction::derivative(op, result_type_id, id, expr_id));
id
} crate::Expression::ImageQuery { image, query } => { self.write_image_query(result_type_id, image, query, block)?
} crate::Expression::Relational { fun, argument } => { usecrate::RelationalFunction as Rf; let arg_id = self.cached[argument]; let op = match fun {
Rf::All => spirv::Op::All,
Rf::Any => spirv::Op::Any,
Rf::IsNan => spirv::Op::IsNan,
Rf::IsInf => spirv::Op::IsInf,
}; let id = self.gen_id();
block
.body
.push(Instruction::relational(op, result_type_id, id, arg_id));
id
} crate::Expression::ArrayLength(expr) => self.write_runtime_array_length(expr, block)?, crate::Expression::RayQueryGetIntersection { query, committed } => { let query_id = self.cached[query]; let init_tracker_id = *self
.ray_query_tracker_expr
.get(&query)
.expect("not a cached ray query"); let func_id = self
.writer
.write_ray_query_get_intersection_function(committed, self.ir_module); let ray_intersection = self.ir_module.special_types.ray_intersection.unwrap(); let intersection_type_id = self.get_handle_type_id(ray_intersection); let id = self.gen_id();
block.body.push(Instruction::function_call(
intersection_type_id,
id,
func_id,
&[query_id, init_tracker_id.initialized_tracker],
));
id
} crate::Expression::RayQueryVertexPositions { query, committed } => { self.writer.require_any( "RayQueryVertexPositions",
&[spirv::Capability::RayQueryPositionFetchKHR],
)?; self.write_ray_query_return_vertex_position(query, block, committed)
} crate::Expression::CooperativeLoad { ref data, .. } => { self.writer.require_any( "CooperativeMatrix",
&[spirv::Capability::CooperativeMatrixKHR],
)?; let layout = if data.row_major {
spirv::CooperativeMatrixLayout::RowMajorKHR
} else {
spirv::CooperativeMatrixLayout::ColumnMajorKHR
}; let layout_id = self.get_index_constant(layout as u32); let stride_id = self.cached[data.stride]; matchself.write_access_chain(data.pointer, block, AccessTypeAdjustment::None)? {
ExpressionPointer::Ready { pointer_id } => { let id = self.gen_id();
block.body.push(Instruction::coop_load(
result_type_id,
id,
pointer_id,
layout_id,
stride_id,
));
id
}
ExpressionPointer::Conditional { condition, access } => self
.write_conditional_indexed_load(
result_type_id,
condition,
block,
|id_gen, block| { let pointer_id = access.result_id.unwrap();
block.body.push(access); let id = id_gen.next();
block.body.push(Instruction::coop_load(
result_type_id,
id,
pointer_id,
layout_id,
stride_id,
));
id
},
),
}
} crate::Expression::CooperativeMultiplyAdd { a, b, c } => { self.writer.require_any( "CooperativeMatrix",
&[spirv::Capability::CooperativeMatrixKHR],
)?; let a_id = self.cached[a]; let b_id = self.cached[b]; let c_id = self.cached[c]; let id = self.gen_id();
block.body.push(Instruction::coop_mul_add(
result_type_id,
id,
a_id,
b_id,
c_id,
));
id
}
};
self.cached[expr_handle] = id;
Ok(())
}
/// Helper which focuses on generating the `As` expressions and the various conversions /// that need to happen because of that. fn write_as_expression(
&mutself,
expr: Handle<crate::Expression>,
convert: Option<u8>,
kind: crate::ScalarKind,
block: &mut Block,
result_type_id: u32,
) -> Result<u32, Error> { usecrate::ScalarKind as Sk; let expr_id = self.cached[expr]; let ty = self.fun_info[expr].ty.inner_with(&self.ir_module.types);
// Matrix casts needs special treatment in SPIR-V, as the cast functions // can take vectors or scalars, but not matrices. In order to cast a matrix // we need to cast each column of the matrix individually and construct a new // matrix from the converted columns. ifletcrate::TypeInner::Matrix {
columns,
rows,
scalar,
} = *ty
{ let Some(convert) = convert else { // No conversion needs to be done, passes through. return Ok(expr_id);
};
if convert == scalar.width { // No conversion needs to be done, passes through. return Ok(expr_id);
}
if kind != Sk::Float { // Only float conversions are supported for matrices. return Err(Error::Validation("Matrices must be floats"));
}
// Type of each extracted column let column_src_ty = self.get_type_id(LookupType::Local(LocalType::Numeric(NumericType::Vector {
size: rows,
scalar,
})));
// Type of the column after conversion let column_dst_ty = self.get_type_id(LookupType::Local(LocalType::Numeric(NumericType::Vector {
size: rows,
scalar: crate::Scalar {
kind,
width: convert,
},
})));
letmut components = ArrayVec::<Word, 4>::new();
for column in0..columns as usize { let column_id = self.gen_id();
block.body.push(Instruction::composite_extract(
column_src_ty,
column_id,
expr_id,
&[column as u32],
));
let column_conv_id = self.gen_id();
block.body.push(Instruction::unary(
spirv::Op::FConvert,
column_dst_ty,
column_conv_id,
column_id,
));
Cast::Ternary(spirv::Op::Select, expr_id, accept_id, reject_id)
} // Avoid undefined behaviour when casting from a float to integer // when the value is out of range for the target type. Additionally // ensure we clamp to the correct value as per the WGSL spec. // // https://www.w3.org/TR/WGSL/#floating-point-conversion: // * If X is exactly representable in the target type T, then the // result is that value. // * Otherwise, the result is the value in T closest to // truncate(X) and also exactly representable in the original // floating point type.
(Sk::Float, Sk::Sint | Sk::Uint, Some(width)) => { let dst_scalar = crate::Scalar { kind, width }; let (min, max) = crate::proc::min_max_float_representable_by(src_scalar, dst_scalar);
let expr_type_id = self.get_expression_type_id(&self.fun_info[expr].ty);
let maybe_splat_const = |writer: &mut Writer, const_id| match src_size {
None => const_id,
Some(size) => {
let constituent_ids = [const_id; crate::VectorSize::MAX];
writer.get_constant_composite(
LookupType::Local(LocalType::Numeric(NumericType::Vector {
size,
scalar: src_scalar,
})),
&constituent_ids[..size as usize],
)
}
};
let min_const_id = self.writer.get_constant_scalar(min);
let min_const_id = maybe_splat_const(self.writer, min_const_id);
let max_const_id = self.writer.get_constant_scalar(max);
let max_const_id = maybe_splat_const(self.writer, max_const_id);
let op = match dst_scalar.kind {
crate::ScalarKind::Sint => spirv::Op::ConvertFToS,
crate::ScalarKind::Uint => spirv::Op::ConvertFToU,
_ => unreachable!(),
};
Cast::Unary(op, clamp_id)
}
(Sk::Float, Sk::Float, Some(dst_width)) if src_scalar.width != dst_width => {
Cast::Unary(spirv::Op::FConvert, expr_id)
}
(Sk::Sint, Sk::Float, Some(_)) => Cast::Unary(spirv::Op::ConvertSToF, expr_id),
(Sk::Sint, Sk::Sint, Some(dst_width)) if src_scalar.width != dst_width => {
Cast::Unary(spirv::Op::SConvert, expr_id)
}
(Sk::Uint, Sk::Float, Some(_)) => Cast::Unary(spirv::Op::ConvertUToF, expr_id),
(Sk::Uint, Sk::Uint, Some(dst_width)) if src_scalar.width != dst_width => {
Cast::Unary(spirv::Op::UConvert, expr_id)
}
(Sk::Uint, Sk::Sint, Some(dst_width)) if src_scalar.width != dst_width => {
Cast::Unary(spirv::Op::SConvert, expr_id)
}
(Sk::Sint, Sk::Uint, Some(dst_width)) if src_scalar.width != dst_width => {
Cast::Unary(spirv::Op::UConvert, expr_id)
}
// We assume it's either an identity cast, or int-uint.
_ => Cast::Unary(spirv::Op::Bitcast, expr_id),
};
Ok(match cast {
Cast::Identity(expr) => expr,
Cast::Unary(op, op1) => {
let id = self.gen_id();
block
.body
.push(Instruction::unary(op, result_type_id, id, op1));
id
}
Cast::Binary(op, op1, op2) => {
let id = self.gen_id();
block
.body
.push(Instruction::binary(op, result_type_id, id, op1, op2));
id
}
Cast::Ternary(op, op1, op2, op3) => {
let id = self.gen_id();
block
.body
.push(Instruction::ternary(op, result_type_id, id, op1, op2, op3));
id
}
})
}
/// Build an `OpAccessChain` instruction.
///
/// Emit any needed bounds-checking expressions to `block`.
///
/// Give the `OpAccessChain` a result type based on `expr_handle`, adjusted
/// according to `type_adjustment`; see the documentation for
/// [`AccessTypeAdjustment`] for details.
///
/// On success, the return value is an [`ExpressionPointer`] value; see the
/// documentation for that type.
fn write_access_chain(
&mut self,
mut expr_handle: Handle<crate::Expression>,
block: &mut Block,
type_adjustment: AccessTypeAdjustment,
) -> Result<ExpressionPointer, Error> {
let result_type_id = {
let resolution = &self.fun_info[expr_handle].ty;
match type_adjustment {
AccessTypeAdjustment::None => self.writer.get_expression_type_id(resolution),
AccessTypeAdjustment::IntroducePointer(class) => {
self.writer.get_resolution_pointer_id(resolution, class)
}
AccessTypeAdjustment::UseStd140CompatType => {
match *resolution.inner_with(&self.ir_module.types) {
crate::TypeInner::Pointer {
base,
space: space @ crate::AddressSpace::Uniform,
} => self.writer.get_pointer_type_id(
self.writer.std140_compat_uniform_types[&base].type_id,
map_storage_class(space),
),
_ => unreachable!(
"`UseStd140CompatType` must only be used with uniform pointer types"
),
}
}
}
};
// The id of the boolean `and` of all dynamic bounds checks up to this point.
//
// See `extend_bounds_check_condition_chain` for a full explanation.
let mut accumulated_checks = None;
// Is true if we are accessing into a binding array with a non-uniform index.
let mut is_non_uniform_binding_array = false;
// The index value if the previously encountered expression was an
// `AccessIndex` of a matrix which has been decomposed into individual
// column vectors directly in the containing struct. The subsequent
// iteration will append the correct index to the list for accessing
// said column from the containing struct.
let mut prev_decomposed_matrix_index = None;
self.temp_list.clear();
let root_id = loop {
// If `expr_handle` was spilled, then the temporary variable has exactly
// the value we want to start from.
if let Some(spilled) = self.function.spilled_composites.get(&expr_handle) {
// The root id of the `OpAccessChain` instruction is the temporary
// variable we spilled the composite to.
break spilled.id;
}
expr_handle = match self.ir_function.expressions[expr_handle] {
crate::Expression::Access { base, index } => {
is_non_uniform_binding_array |=
self.is_nonuniform_binding_array_access(base, index);
let index = GuardedIndex::Expression(index);
let index_id =
self.write_access_chain_index(base, index, &mut accumulated_checks, block)?;
self.temp_list.push(index_id);
base
}
crate::Expression::AccessIndex { base, index } => {
// Decide whether we're indexing a struct (bounds checks
// forbidden) or anything else (bounds checks required).
let mut base_ty = self.fun_info[base].ty.inner_with(&self.ir_module.types);
let mut base_ty_handle = self.fun_info[base].ty.handle();
let mut pointer_space = None;
if let crate::TypeInner::Pointer { base, space } = *base_ty {
base_ty = &self.ir_module.types[base].inner;
base_ty_handle = Some(base);
pointer_space = Some(space);
}
match *base_ty {
// When indexing a struct bounds checks are forbidden. If accessing the
// struct through a uniform address space pointer, where the struct has
// been declared with an alternative std140 compatible layout, we must use
// the remapped member index. Additionally if the previous iteration was
// accessing a column of a matrix member which has been decomposed directly
// into the struct, we must ensure we access the correct column.
crate::TypeInner::Struct { .. } => {
let index = match base_ty_handle.and_then(|handle| {
self.writer.std140_compat_uniform_types.get(&handle)
}) {
Some(std140_type_info)
if pointer_space == Some(crate::AddressSpace::Uniform) =>
{
std140_type_info.member_indices[index as usize]
+ prev_decomposed_matrix_index.take().unwrap_or(0)
}
_ => index,
};
let index_id = self.get_index_constant(index);
self.temp_list.push(index_id);
}
// Bounds checks are not required when indexing a matrix. If indexing a
// two-row matrix contained within a struct through a uniform address space
// pointer then the matrix' columns will have been decomposed directly into
// the containing struct. We skip adding an index to the list on this
// iteration and instead adjust the index on the next iteration when
// accessing the struct member.
_ if is_uniform_matcx2_struct_member_access(
self.ir_function,
self.fun_info,
self.ir_module,
base,
) =>
{
assert!(prev_decomposed_matrix_index.is_none());
prev_decomposed_matrix_index = Some(index);
}
_ => {
// `index` is constant, so this can't possibly require
// setting `is_nonuniform_binding_array_access`.
// Even though the index value is statically known, `base`
// may be a runtime-sized array, so we still need to go
// through the bounds check process.
let index_id = self.write_access_chain_index(
base,
GuardedIndex::Known(index),
&mut accumulated_checks,
block,
)?;
self.temp_list.push(index_id);
}
}
base
}
crate::Expression::GlobalVariable(handle) => {
let gv = &self.writer.global_variables[handle];
break gv.access_id;
}
crate::Expression::LocalVariable(variable) => {
let local_var = &self.function.variables[&variable];
break local_var.id;
}
crate::Expression::FunctionArgument(index) => {
break self.function.parameter_id(index);
}
ref other => unimplemented!("Unexpected pointer expression {:?}", other),
}
};
let (pointer_id, expr_pointer) = if self.temp_list.is_empty() {
(
root_id,
ExpressionPointer::Ready {
pointer_id: root_id,
},
)
} else {
self.temp_list.reverse();
let pointer_id = self.gen_id();
let access =
Instruction::access_chain(result_type_id, pointer_id, root_id, &self.temp_list);
// If we generated some bounds checks, we need to leave it to our
// caller to generate the branch, the access, the load or store, and
// the zero value (for loads). Otherwise, we can emit the access
// ourselves, and just hand them the id of the pointer.
let expr_pointer = match accumulated_checks {
Some(condition) => ExpressionPointer::Conditional { condition, access },
None => {
block.body.push(access);
ExpressionPointer::Ready { pointer_id }
}
};
(pointer_id, expr_pointer)
};
// Subsequent load, store and atomic operations require the pointer to be decorated as NonUniform
// if the binding array was accessed with a non-uniform index
// see VUID-RuntimeSpirv-NonUniform-06274
if is_non_uniform_binding_array {
self.writer
.decorate_non_uniform_binding_array_access(pointer_id)?;
}
// The access chain needs to be decorated as NonUniform
// see VUID-RuntimeSpirv-NonUniform-06274
let gvar = &self.ir_module.global_variables[var_handle];
let crate::TypeInner::BindingArray { .. } = self.ir_module.types[gvar.ty].inner else {
return false;
};
/// Compute a single index operand to an `OpAccessChain` instruction.
///
/// Given that we are indexing `base` with `index`, apply the appropriate
/// bounds check policies, emitting code to `block` to clamp `index` or
/// determine whether it's in bounds. Return the SPIR-V instruction id of
/// the index value we should actually use.
///
/// Extend `accumulated_checks` to include the results of any needed bounds
/// checks. See [`BlockContext::extend_bounds_check_condition_chain`].
fn write_access_chain_index(
&mut self,
base: Handle<crate::Expression>,
index: GuardedIndex,
accumulated_checks: &mut Option<Word>,
block: &mut Block,
) -> Result<Word, Error> {
match self.write_bounds_check(base, index, block)? {
BoundsCheckResult::KnownInBounds(known_index) => {
// Even if the index is known, `OpAccessChain`
// requires expression operands, not literals.
let scalar = crate::Literal::U32(known_index);
Ok(self.writer.get_constant_scalar(scalar))
}
BoundsCheckResult::Computed(computed_index_id) => Ok(computed_index_id),
BoundsCheckResult::Conditional {
condition_id: condition,
index_id: index,
} => {
self.extend_bounds_check_condition_chain(accumulated_checks, condition, block);
// Use the index from the `Access` expression unchanged.
Ok(index)
}
}
}
/// Add a condition to a chain of bounds checks.
///
/// As we build an `OpAccessChain` instruction govered by
/// [`BoundsCheckPolicy::ReadZeroSkipWrite`], we accumulate a chain of
/// dynamic bounds checks, one for each index in the chain, which must all
/// be true for that `OpAccessChain`'s execution to be well-defined. This
/// function adds the boolean instruction id `comparison_id` to `chain`.
///
/// If `chain` is `None`, that means there are no bounds checks in the chain
/// yet. If chain is `Some(id)`, then `id` is the conjunction of all the
/// bounds checks in the chain.
///
/// When we have multiple bounds checks, we combine them with
/// `OpLogicalAnd`, not a short-circuit branch. This means we might do
/// comparisons we don't need to, but we expect these checks to almost
/// always succeed, and keeping branches to a minimum is essential.
///
/// [`BoundsCheckPolicy::ReadZeroSkipWrite`]: crate::proc::BoundsCheckPolicy
fn extend_bounds_check_condition_chain(
&mut self,
chain: &mut Option<Word>,
comparison_id: Word,
block: &mut Block,
) {
match *chain {
Some(ref mut prior_checks) => {
let combined = self.gen_id();
block.body.push(Instruction::binary(
spirv::Op::LogicalAnd,
self.writer.get_bool_type_id(),
combined,
*prior_checks,
comparison_id,
));
*prior_checks = combined;
}
None => {
// Start a fresh chain of checks.
*chain = Some(comparison_id);
}
}
}
fn write_checked_load(
&mut self,
pointer: Handle<crate::Expression>,
block: &mut Block,
access_type_adjustment: AccessTypeAdjustment,
result_type_id: Word,
) -> Result<Word, Error> {
if let Some(result_id) = self.maybe_write_uniform_matcx2_dynamic_access(pointer, block)? {
Ok(result_id)
} else if let Some(result_id) =
self.maybe_write_load_uniform_matcx2_struct_member(pointer, block)?
{
Ok(result_id)
} else {
// If `pointer` refers to a uniform address space pointer to a type
// which was declared using a std140 compatible type variant (i.e.
// is a two-row matrix, or a struct or array containing such a
// matrix) we must ensure the access chain and the type of the load
// instruction use the std140 compatible type variant.
struct WrappedLoad {
access_type_adjustment: AccessTypeAdjustment,
r#type: Handle<crate::Type>,
}
let mut wrapped_load = None;
if let crate::TypeInner::Pointer {
base: pointer_base_type,
space: crate::AddressSpace::Uniform,
} = *self.fun_info[pointer].ty.inner_with(&self.ir_module.types)
{
if self
.writer
.std140_compat_uniform_types
.contains_key(&pointer_base_type)
{
wrapped_load = Some(WrappedLoad {
access_type_adjustment: AccessTypeAdjustment::UseStd140CompatType,
r#type: pointer_base_type,
});
};
};
let (load_type_id, access_type_adjustment) = match wrapped_load {
Some(ref wrapped_load) => (
self.writer.std140_compat_uniform_types[&wrapped_load.r#type].type_id,
wrapped_load.access_type_adjustment,
),
None => (result_type_id, access_type_adjustment),
};
let load_id = match self.write_access_chain(pointer, block, access_type_adjustment)? {
ExpressionPointer::Ready { pointer_id } => {
let id = self.gen_id();
let atomic_space =
match *self.fun_info[pointer].ty.inner_with(&self.ir_module.types) {
crate::TypeInner::Pointer { base, space } => {
match self.ir_module.types[base].inner {
crate::TypeInner::Atomic { .. } => Some(space),
_ => None,
}
}
_ => None,
};
let instruction = if let Some(space) = atomic_space {
let (semantics, scope) = space.to_spirv_semantics_and_scope();
let scope_constant_id = self.get_scope_constant(scope as u32);
let semantics_id = self.get_index_constant(semantics.bits());
Instruction::atomic_load(
result_type_id,
id,
pointer_id,
scope_constant_id,
semantics_id,
)
} else {
Instruction::load(load_type_id, id, pointer_id, None)
};
block.body.push(instruction);
id
}
ExpressionPointer::Conditional { condition, access } => {
//TODO: support atomics?
self.write_conditional_indexed_load(
load_type_id,
condition,
block,
move |id_gen, block| {
// The in-bounds path. Perform the access and the load.
let pointer_id = access.result_id.unwrap();
let value_id = id_gen.next();
block.body.push(access);
block.body.push(Instruction::load(
load_type_id,
value_id,
pointer_id,
None,
));
value_id
},
)
}
};
match wrapped_load {
Some(ref wrapped_load) => {
// If we loaded a std140 compat type then we must call the
// function to convert the loaded value to the regular type.
let result_id = self.gen_id();
let function_id = self.writer.wrapped_functions
[&WrappedFunction::ConvertFromStd140CompatType {
r#type: wrapped_load.r#type,
}];
block.body.push(Instruction::function_call(
result_type_id,
result_id,
function_id,
&[load_id],
));
Ok(result_id)
}
None => Ok(load_id),
}
}
}
// Make sure we have an internal variable to spill `base` to.
let spill_variable_id = match self.function.spilled_composites.entry(base) {
Entry::Occupied(preexisting) => preexisting.get().id,
Entry::Vacant(vacant) => {
// Generate a new internal variable of the appropriate
// type for `base`.
let pointer_type_id = self.writer.get_resolution_pointer_id(
&self.fun_info[base].ty,
spirv::StorageClass::Function,
);
let id = self.writer.id_gen.next();
vacant.insert(super::LocalVariable {
id,
instruction: Instruction::variable(
pointer_type_id,
id,
spirv::StorageClass::Function,
None,
),
});
id
}
};
// Perform the store even if we already had a spill variable for `base`.
// Consider this code:
//
// var x = ...;
// var y = ...;
// var z = ...;
// for (i = 0; i<2; i++) {
// let a = array(i, i, i);
// if (i == 0) {
// x += a[y];
// } else [
// x += a[z];
// }
// }
//
// The value of `a` needs to be spilled so we can subscript it with `y` and `z`.
//
// When we generate SPIR-V for `a[y]`, we will create the spill
// variable, and store `a`'s value in it.
//
// When we generate SPIR-V for `a[z]`, we will notice that the spill
// variable for `a` has already been declared, but it is still essential
// that we store `a` into it, so that `a[z]` sees this iteration's value
// of `a`.
let base_id = self.cached[base];
block
.body
.push(Instruction::store(spill_variable_id, base_id, None));
}
/// Generate an access to a spilled temporary, if necessary.
///
/// Given `access`, an [`Access`] or [`AccessIndex`] expression that refers
/// to a component of a composite value that has been spilled to a temporary
/// variable, determine whether other expressions are going to use
/// `access`'s value:
///
/// - If so, perform the access and cache that as the value of `access`.
///
/// - Otherwise, generate no code and cache no value for `access`.
///
/// Return `Ok(0)` if no value was fetched, or `Ok(id)` if we loaded it into
/// the instruction given by `id`.
///
/// [`Access`]: crate::Expression::Access
/// [`AccessIndex`]: crate::Expression::AccessIndex
fn maybe_access_spilled_composite(
&mut self,
access: Handle<crate::Expression>,
block: &mut Block,
result_type_id: Word,
) -> Result<Word, Error> {
let access_uses = self.function.access_uses.get(&access).map_or(0, |r| *r);
if access_uses == self.fun_info[access].ref_count {
// This expression is only used by other `Access` and
// `AccessIndex` expressions, so we don't need to cache a
// value for it yet.
Ok(0)
} else {
// There are other expressions that are going to expect this
// expression's value to be cached, not just other `Access` or
// `AccessIndex` expressions. We must actually perform the
// access on the spill variable now.
self.write_checked_load(
access,
block,
AccessTypeAdjustment::IntroducePointer(spirv::StorageClass::Function),
result_type_id,
)
}
}
/// Build the instructions for matrix - matrix column operations
#[allow(clippy::too_many_arguments)]
fn write_matrix_matrix_column_op(
&mut self,
block: &mut Block,
result_id: Word,
result_type_id: Word,
left_id: Word,
right_id: Word,
columns: crate::VectorSize,
rows: crate::VectorSize,
width: u8,
op: spirv::Op,
) {
self.temp_list.clear();
let vector_type_id = self.get_numeric_type_id(NumericType::Vector {
size: rows,
scalar: crate::Scalar::float(width),
});
for index in 0..columns as u32 {
let column_id_left = self.gen_id();
let column_id_right = self.gen_id();
let column_id_res = self.gen_id();
/// Build the instructions for the arithmetic expression of a dot product
///
/// The argument `extractor` is a function that maps `(result_id,
/// composite_id, index)` to an instruction that extracts the `index`th
/// entry of the value with ID `composite_id` and assigns it to the slot
/// with id `result_id` (which must have type `result_type_id`).
#[expect(clippy::too_many_arguments)]
fn write_dot_product(
&mut self,
result_id: Word,
result_type_id: Word,
arg0_id: Word,
arg1_id: Word,
size: u32,
block: &mut Block,
extractor: impl Fn(Word, Word, Word) -> Instruction,
) {
let mut partial_sum = self.writer.get_constant_null(result_type_id);
let last_component = size - 1;
for index in 0..=last_component {
// compute the product of the current components
let a_id = self.gen_id();
block.body.push(extractor(a_id, arg0_id, index));
let b_id = self.gen_id();
block.body.push(extractor(b_id, arg1_id, index));
let prod_id = self.gen_id();
block.body.push(Instruction::binary(
spirv::Op::IMul,
result_type_id,
prod_id,
a_id,
b_id,
));
// choose the id for the next sum, depending on current index
let id = if index == last_component {
result_id
} else {
self.gen_id()
};
// sum the computed product with the partial sum
block.body.push(Instruction::binary(
spirv::Op::IAdd,
result_type_id,
id,
partial_sum,
prod_id,
));
// set the id of the result as the previous partial sum
partial_sum = id;
}
}
/// Emit code for `pack4x{I,U}8[Clamp]` if capability "Int8" is available.
fn write_pack4x8_optimized(
&mut self,
block: &mut Block,
result_type_id: u32,
arg0_id: u32,
id: u32,
is_signed: bool,
should_clamp: bool,
) -> Instruction {
let int_type = if is_signed {
crate::ScalarKind::Sint
} else {
crate::ScalarKind::Uint
};
let wide_vector_type = NumericType::Vector {
size: crate::VectorSize::Quad,
scalar: crate::Scalar {
kind: int_type,
width: 4,
},
};
let wide_vector_type_id = self.get_numeric_type_id(wide_vector_type);
let packed_vector_type_id = self.get_numeric_type_id(NumericType::Vector {
size: crate::VectorSize::Quad,
scalar: crate::Scalar {
kind: crate::ScalarKind::Uint,
width: 1,
},
});
let mut wide_vector = arg0_id;
if should_clamp {
let (min, max, clamp_op) = if is_signed {
(
crate::Literal::I32(-128),
crate::Literal::I32(127),
spirv::GlslStd450Op::SClamp,
)
} else {
(
crate::Literal::U32(0),
crate::Literal::U32(255),
spirv::GlslStd450Op::UClamp,
)
};
let [min, max] = [min, max].map(|lit| {
let scalar = self.writer.get_constant_scalar(lit);
self.writer.get_constant_composite(
LookupType::Local(LocalType::Numeric(wide_vector_type)),
&[scalar; 4],
)
});
let packed_vector = self.gen_id();
block.body.push(Instruction::unary(
spirv::Op::UConvert, // We truncate, so `UConvert` and `SConvert` behave identically.
packed_vector_type_id,
packed_vector,
wide_vector,
));
/// Generate one or more SPIR-V blocks for `naga_block`.
///
/// Use `label_id` as the label for the SPIR-V entry point block.
///
/// If control reaches the end of the SPIR-V block, terminate it according
/// to `exit`. This function's return value indicates whether it acted on
/// this parameter or not; see [`BlockExitDisposition`].
///
/// If the block contains [`Break`] or [`Continue`] statements,
/// `loop_context` supplies the labels of the SPIR-V blocks to jump to. If
/// either of these labels are `None`, then it should have been a Naga
/// validation error for the corresponding statement to occur in this
/// context.
///
/// [`Break`]: Statement::Break
/// [`Continue`]: Statement::Continue
fn write_block(
&mut self,
label_id: Word,
naga_block: &crate::Block,
exit: BlockExit,
loop_context: LoopContext,
debug_info: Option<&DebugInfoInner>,
) -> Result<BlockExitDisposition, Error> {
let mut block = Block::new(label_id);
for (statement, span) in naga_block.span_iter() {
if let (Some(debug_info), false) = (
debug_info,
matches!(
statement,
&(Statement::Block(..)
| Statement::Break
| Statement::Continue
| Statement::Kill
| Statement::Return { .. }
| Statement::Loop { .. })
),
) {
let loc: crate::SourceLocation = span.location(debug_info.source_code);
block.body.push(Instruction::line(
debug_info.source_file_id,
loc.line_number,
loc.line_position,
));
};
match *statement {
Statement::Emit(ref range) => {
for handle in range.clone() {
// omit const expressions as we've already cached those
if !self.expression_constness.is_const(handle) {
self.cache_expression_value(handle, &mut block)?;
}
}
}
Statement::Block(ref block_statements) => {
let scope_id = self.gen_id();
self.function.consume(block, Instruction::branch(scope_id));
let merge_id = self.gen_id();
let merge_used = self.write_block(
scope_id,
block_statements,
BlockExit::Branch { target: merge_id },
loop_context,
debug_info,
)?;
match merge_used {
BlockExitDisposition::Used => {
block = Block::new(merge_id);
}
BlockExitDisposition::Discarded => {
return Ok(BlockExitDisposition::Discarded);
}
}
}
Statement::If {
condition,
ref accept,
ref reject,
} => {
// In spirv 1.6, in a conditional branch the two block ids
// of the branches can't have the same label. If `accept`
// and `reject` are both empty (e.g. in `if (condition) {}`)
// merge id will be both labels. Because both branches are
// empty, we can skip the if statement.
if !(accept.is_empty() && reject.is_empty()) {
let condition_id = self.cached[condition];
let merge_id = self.gen_id();
block.body.push(Instruction::selection_merge(
merge_id,
spirv::SelectionControl::NONE,
));
let accept_id = if accept.is_empty() {
None
} else {
Some(self.gen_id())
};
let reject_id = if reject.is_empty() {
None
} else {
Some(self.gen_id())
};
if let Some(block_id) = accept_id {
// We can ignore the `BlockExitDisposition` returned here because,
// even if `merge_id` is not actually reachable, it is always
// referred to by the `OpSelectionMerge` instruction we emitted
// earlier.
let _ = self.write_block(
block_id,
accept,
BlockExit::Branch { target: merge_id },
loop_context,
debug_info,
)?;
}
if let Some(block_id) = reject_id {
// We can ignore the `BlockExitDisposition` returned here because,
// even if `merge_id` is not actually reachable, it is always
// referred to by the `OpSelectionMerge` instruction we emitted
// earlier.
let _ = self.write_block(
block_id,
reject,
BlockExit::Branch { target: merge_id },
loop_context,
debug_info,
)?;
}
let merge_id = self.gen_id();
block.body.push(Instruction::selection_merge(
merge_id,
spirv::SelectionControl::NONE,
));
let mut default_id = None;
// id of previous empty fall-through case
let mut last_id = None;
let mut raw_cases = Vec::with_capacity(cases.len());
let mut case_ids = Vec::with_capacity(cases.len());
for case in cases.iter() {
// take id of previous empty fall-through case or generate a new one
let label_id = last_id.take().unwrap_or_else(|| self.gen_id());
if case.fall_through && case.body.is_empty() {
last_id = Some(label_id);
}
case_ids.push(label_id);
match case.value {
crate::SwitchValue::I32(value) => {
raw_cases.push(super::instructions::Case {
value: value as Word,
label_id,
});
}
crate::SwitchValue::U32(value) => {
raw_cases.push(super::instructions::Case { value, label_id });
}
crate::SwitchValue::Default => {
default_id = Some(label_id);
}
}
}
let inner_context = LoopContext {
break_id: Some(merge_id),
..loop_context
};
for (i, (case, label_id)) in cases
.iter()
.zip(case_ids.iter())
.filter(|&(case, _)| !(case.fall_through && case.body.is_empty()))
.enumerate()
{
let case_finish_id = if case.fall_through {
case_ids[i + 1]
} else {
merge_id
};
// We can ignore the `BlockExitDisposition` returned here because
// `case_finish_id` is always referred to by either:
//
// - the `OpSwitch`, if it's the next case's label for a
// fall-through, or
//
// - the `OpSelectionMerge`, if it's the switch's overall merge
// block because there's no fall-through.
let _ = self.write_block(
*label_id,
&case.body,
BlockExit::Branch {
target: case_finish_id,
},
inner_context,
debug_info,
)?;
}
let merge_id = self.gen_id();
let body_id = self.gen_id();
let continuing_id = self.gen_id();
// SPIR-V requires the continuing to the `OpLoopMerge`,
// so we have to start a new block with it.
block = Block::new(preamble_id);
// HACK the loop statement is begin with branch instruction,
// so we need to put `OpLine` debug info before merge instruction
if let Some(debug_info) = debug_info {
let loc: crate::SourceLocation = span.location(debug_info.source_code);
block.body.push(Instruction::line(
debug_info.source_file_id,
loc.line_number,
loc.line_position,
))
}
block.body.push(Instruction::loop_merge(
merge_id,
continuing_id,
spirv::SelectionControl::NONE,
));
if self.force_loop_bounding {
block = self.write_force_bounded_loop_instructions(block, merge_id);
}
self.function.consume(block, Instruction::branch(body_id));
// We can ignore the `BlockExitDisposition` returned here because,
// even if `continuing_id` is not actually reachable, it is always
// referred to by the `OpLoopMerge` instruction we emitted earlier.
let _ = self.write_block(
body_id,
body,
BlockExit::Branch {
target: continuing_id,
},
LoopContext {
continuing_id: Some(continuing_id),
break_id: Some(merge_id),
},
debug_info,
)?;
// We can ignore the `BlockExitDisposition` returned here because,
// even if `merge_id` is not actually reachable, it is always referred
// to by the `OpLoopMerge` instruction we emitted earlier.
let _ = self.write_block(
continuing_id,
continuing,
exit,
LoopContext {
continuing_id: None,
break_id: Some(merge_id),
},
debug_info,
)?;
block = Block::new(merge_id);
}
Statement::Break => {
self.function
.consume(block, Instruction::branch(loop_context.break_id.unwrap()));
return Ok(BlockExitDisposition::Discarded);
}
Statement::Continue => {
self.function.consume(
block,
Instruction::branch(loop_context.continuing_id.unwrap()),
);
return Ok(BlockExitDisposition::Discarded);
}
Statement::Return { value: Some(value) } => {
let value_id = self.cached[value];
let instruction = match self.function.entry_point_context {
// If this is an entry point, and we need to return anything,
// let's instead store the output variables and return `void`.
Some(ref context) => self.writer.write_entry_point_return(
value_id,
self.ir_function.result.as_ref().unwrap(),
&context.results,
&mut block.body,
)?,
None => Instruction::return_value(value_id),
};
self.function.consume(block, instruction);
return Ok(BlockExitDisposition::Discarded);
}
Statement::Return { value: None } => {
self.function.consume(block, Instruction::return_void());
return Ok(BlockExitDisposition::Discarded);
}
Statement::Kill => {
self.function.consume(block, Instruction::kill());
return Ok(BlockExitDisposition::Discarded);
}
Statement::ControlBarrier(flags) => {
self.writer.write_control_barrier(flags, &mut block.body);
}
Statement::MemoryBarrier(flags) => {
self.writer.write_memory_barrier(flags, &mut block);
}
Statement::Store { pointer, value } => {
let value_id = self.cached[value];
match self.write_access_chain(
pointer,
&mut block,
AccessTypeAdjustment::None,
)? {
ExpressionPointer::Ready { pointer_id } => {
let atomic_space = match *self.fun_info[pointer]
.ty
.inner_with(&self.ir_module.types)
{
crate::TypeInner::Pointer { base, space } => {
match self.ir_module.types[base].inner {
crate::TypeInner::Atomic { .. } => Some(space),
_ => None,
}
}
_ => None,
};
let instruction = if let Some(space) = atomic_space {
let (semantics, scope) = space.to_spirv_semantics_and_scope();
let scope_constant_id = self.get_scope_constant(scope as u32);
let semantics_id = self.get_index_constant(semantics.bits());
Instruction::atomic_store(
pointer_id,
scope_constant_id,
semantics_id,
value_id,
)
} else {
Instruction::store(pointer_id, value_id, None)
};
block.body.push(instruction);
}
ExpressionPointer::Conditional { condition, access } => {
let mut selection = Selection::start(&mut block, ());
selection.if_true(self, condition, ());
// The in-bounds path. Perform the access and the store.
let pointer_id = access.result_id.unwrap();
selection.block().body.push(access);
selection
.block()
.body
.push(Instruction::store(pointer_id, value_id, None));
// Finish the in-bounds block and start the merge block. This
// is the block we'll leave current on return.
selection.finish(self, ());
}
};
}
Statement::ImageStore {
image,
coordinate,
array_index,
value,
} => self.write_image_store(image, coordinate, array_index, value, &mut block)?,
Statement::Call {
function: local_function,
ref arguments,
result,
} => {
let id = self.gen_id();
self.temp_list.clear();
for &argument in arguments {
self.temp_list.push(self.cached[argument]);
}
let type_id = match result {
Some(expr) => {
self.cached[expr] = id;
self.get_expression_type_id(&self.fun_info[expr].ty)
}
None => self.writer.void_type,
};
block.body.push(Instruction::function_call(
type_id,
id,
self.writer.lookup_function[&local_function],
&self.temp_list,
));
}
Statement::Atomic {
pointer,
ref fun,
value,
result,
} => {
let id = self.gen_id();
// Compare-and-exchange operations produce a struct result,
// so use `result`'s type if it is available. For no-result
// operations, fall back to `value`'s type.
let result_type_id =
self.get_expression_type_id(&self.fun_info[result.unwrap_or(value)].ty);
if let Some(result) = result {
self.cached[result] = id;
}
let space = self.fun_info[pointer]
.ty
.inner_with(&self.ir_module.types)
.pointer_space()
.unwrap();
let (semantics, scope) = space.to_spirv_semantics_and_scope();
let scope_constant_id = self.get_scope_constant(scope as u32);
let semantics_id = self.get_index_constant(semantics.bits());
let value_id = self.cached[value];
let value_inner = self.fun_info[value].ty.inner_with(&self.ir_module.types);
let crate::TypeInner::Scalar(scalar) = *value_inner else {
return Err(Error::FeatureNotImplemented(
"Atomics with non-scalar values",
));
};
let cas_result_id = self.gen_id();
let equality_result_id = self.gen_id();
let equality_operator = match scalar.kind {
crate::ScalarKind::Sint | crate::ScalarKind::Uint => {
spirv::Op::IEqual
}
_ => unimplemented!(),
};
let mut cas_instr = Instruction::new(spirv::Op::AtomicCompareExchange);
cas_instr.set_type(scalar_type_id);
cas_instr.set_result(cas_result_id);
cas_instr.add_operand(pointer_id);
cas_instr.add_operand(scope_constant_id);
cas_instr.add_operand(semantics_id); // semantics if equal
cas_instr.add_operand(semantics_id); // semantics if not equal
cas_instr.add_operand(value_id);
cas_instr.add_operand(self.cached[cmp]);
block.body.push(cas_instr);
block.body.push(Instruction::binary(
equality_operator,
bool_type_id,
equality_result_id,
cas_result_id,
self.cached[cmp],
));
Instruction::composite_construct(
result_type_id,
id,
&[cas_result_id, equality_result_id],
)
}
};
block.body.push(instruction);
}
Statement::ImageAtomic {
image,
coordinate,
array_index,
fun,
value,
} => {
self.write_image_atomic(
image,
coordinate,
array_index,
fun,
value,
&mut block,
)?;
}
Statement::WorkGroupUniformLoad { pointer, result } => {
self.writer
.write_control_barrier(crate::Barrier::WORK_GROUP, &mut block.body);
let result_type_id = self.get_expression_type_id(&self.fun_info[result].ty);
// Match `Expression::Load` behavior, including `OpAtomicLoad` when
// loading from a pointer to `atomic<T>`.
let id = self.write_checked_load(
pointer,
&mut block,
AccessTypeAdjustment::None,
result_type_id,
)?;
self.cached[result] = id;
self.writer
.write_control_barrier(crate::Barrier::WORK_GROUP, &mut block.body);
}
Statement::RayQuery { query, ref fun } => {
self.write_ray_query_function(query, fun, &mut block);
}
Statement::SubgroupBallot {
result,
ref predicate,
} => {
self.write_subgroup_ballot(predicate, result, &mut block)?;
}
Statement::SubgroupCollectiveOperation {
ref op,
ref collective_op,
argument,
result,
} => {
self.write_subgroup_operation(op, collective_op, argument, result, &mut block)?;
}
Statement::SubgroupGather {
ref mode,
argument,
result,
} => {
self.write_subgroup_gather(mode, argument, result, &mut block)?;
}
Statement::CooperativeStore { target, ref data } => {
let target_id = self.cached[target];
let layout = if data.row_major {
spirv::CooperativeMatrixLayout::RowMajorKHR
} else {
spirv::CooperativeMatrixLayout::ColumnMajorKHR
};
let layout_id = self.get_index_constant(layout as u32);
let stride_id = self.cached[data.stride];
match self.write_access_chain(
data.pointer,
&mut block,
AccessTypeAdjustment::None,
)? {
ExpressionPointer::Ready { pointer_id } => {
block.body.push(Instruction::coop_store(
target_id, pointer_id, layout_id, stride_id,
));
}
ExpressionPointer::Conditional { condition, access } => {
let mut selection = Selection::start(&mut block, ());
selection.if_true(self, condition, ());
// The in-bounds path. Perform the access and the store.
let pointer_id = access.result_id.unwrap();
selection.block().body.push(access);
selection.block().body.push(Instruction::coop_store(
target_id, pointer_id, layout_id, stride_id,
));
// Finish the in-bounds block and start the merge block. This
// is the block we'll leave current on return.
selection.finish(self, ());
}
};
}
Statement::RayPipelineFunction(ref fun) => {
self.write_ray_tracing_pipeline_function(fun, &mut block);
}
}
}
let termination = match exit {
// We're generating code for the top-level Block of the function, so we
// need to end it with some kind of return instruction.
BlockExit::Return => match self.ir_function.result {
Some(ref result) if self.function.entry_point_context.is_none() => {
let type_id = self.get_handle_type_id(result.ty);
let null_id = self.writer.get_constant_null(type_id);
Instruction::return_value(null_id)
}
_ => Instruction::return_void(),
},
BlockExit::Branch { target } => Instruction::branch(target),
BlockExit::BreakIf {
condition,
preamble_id,
} => {
let condition_id = self.cached[condition];
pub(super) fn write_function_body(
&mut self,
entry_id: Word,
debug_info: Option<&DebugInfoInner>,
) -> Result<(), Error> {
// We can ignore the `BlockExitDisposition` returned here because
// `BlockExit::Return` doesn't refer to a block.
let _ = self.write_block(
entry_id,
&self.ir_function.body,
BlockExit::Return,
LoopContext::default(),
debug_info,
)?;
Ok(())
}
}
Messung V0.5 in Prozent
¤ Diese beiden folgenden Angebotsgruppen bietet das Unternehmen0.171Angebot
(Wie Sie bei der Firma Beratungs- und Dienstleistungen beauftragen können 2026-08-26)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.