/// 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 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 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_type_id(LookupType::Local(LocalType::LocalPointer {
base: NumericType::Scalar(crate::Scalar::F32),
class: 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_type_id(LookupType::Local(LocalType::Numeric(
NumericType::Scalar(crate::Scalar::F32),
))); 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_type_id(LookupType::Local(LocalType::Numeric(
NumericType::Scalar(crate::Scalar::F32),
))); 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,
));
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(expr_handle, base, 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_type_id(LookupType::Handle(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_type_id(LookupType::Handle(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_ty_inner = self.fun_info[left].ty.inner_with(&self.ir_module.types); let right_ty_inner = self.fun_info[right].ty.inner_with(&self.ir_module.types);
let left_dimension = get_dimension(left_ty_inner); let right_dimension = get_dimension(right_ty_inner);
letmut reverse_operands = false;
let spirv_op = match op { crate::BinaryOperator::Add => match *left_ty_inner { crate::TypeInner::Scalar(scalar)
| crate::TypeInner::Vector { scalar, .. } => match scalar.kind { crate::ScalarKind::Float => spirv::Op::FAdd,
_ => spirv::Op::IAdd,
}, crate::TypeInner::Matrix {
columns,
rows,
scalar,
} => { self.write_matrix_matrix_column_op(
block,
id,
result_type_id,
left_id,
right_id,
columns,
rows,
scalar.width,
spirv::Op::FAdd,
);
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::GLOp::FAbs),
Some(crate::ScalarKind::Sint) => MathOp::Ext(spirv::GLOp::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::GLOp::FMin,
Some(crate::ScalarKind::Sint) => spirv::GLOp::SMin,
Some(crate::ScalarKind::Uint) => spirv::GLOp::UMin,
other => unimplemented!("Unexpected min({:?})", other),
}),
Mf::Max => MathOp::Ext(match arg_scalar_kind {
Some(crate::ScalarKind::Float) => spirv::GLOp::FMax,
Some(crate::ScalarKind::Sint) => spirv::GLOp::SMax,
Some(crate::ScalarKind::Uint) => spirv::GLOp::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::GLOp::FClamp),
Some(_) => { let (min_op, max_op) = match arg_scalar_kind {
Some(crate::ScalarKind::Sint) => {
(spirv::GLOp::SMin, spirv::GLOp::SMax)
}
Some(crate::ScalarKind::Uint) => {
(spirv::GLOp::UMin, spirv::GLOp::UMax)
}
_ => unreachable!(),
};
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_type_id(LookupType::Local(LocalType::Numeric(
NumericType::Scalar(crate::Scalar::U32),
)));
// o = min(offset, w) let offset_id = self.gen_id();
block.body.push(Instruction::ext_inst( self.writer.gl450_ext_inst_id,
spirv::GLOp::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( self.writer.gl450_ext_inst_id,
spirv::GLOp::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_type_id(LookupType::Local(LocalType::Numeric(
NumericType::Scalar(crate::Scalar::U32),
)));
// o = min(offset, w) let offset_id = self.gen_id();
block.body.push(Instruction::ext_inst( self.writer.gl450_ext_inst_id,
spirv::GLOp::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( self.writer.gl450_ext_inst_id,
spirv::GLOp::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::GLOp::FindILsb),
Mf::FirstLeadingBit => { if arg_ty.scalar_width() == Some(4) { let thing = match arg_scalar_kind {
Some(crate::ScalarKind::Uint) => spirv::GLOp::FindUMsb,
Some(crate::ScalarKind::Sint) => spirv::GLOp::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::GLOp::PackUnorm4x8),
Mf::Pack4x8snorm => MathOp::Ext(spirv::GLOp::PackSnorm4x8),
Mf::Pack2x16float => MathOp::Ext(spirv::GLOp::PackHalf2x16),
Mf::Pack2x16unorm => MathOp::Ext(spirv::GLOp::PackUnorm2x16),
Mf::Pack2x16snorm => MathOp::Ext(spirv::GLOp::PackSnorm2x16),
fun @ (Mf::Pack4xI8 | Mf::Pack4xU8) => { let (int_type, is_signed) = match fun {
Mf::Pack4xI8 => (crate::ScalarKind::Sint, true),
Mf::Pack4xU8 => (crate::ScalarKind::Uint, false),
_ => unreachable!(),
}; let uint_type_id = self.get_type_id(LookupType::Local(LocalType::Numeric(
NumericType::Scalar(crate::Scalar::U32),
)));
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 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 } => { self.write_ray_query_get_intersection(query, block, committed)
}
};
self.cached[expr_handle] = id;
Ok(())
}
/// 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(
&mutself, 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)
}
}
};
// 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. letmut accumulated_checks = None;
// Is true if we are accessing into a binding array with a non-uniform index. letmut is_non_uniform_binding_array = false;
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. iflet 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;
}
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). letmut base_ty = self.fun_info[base].ty.inner_with(&self.ir_module.types); ifletcrate::TypeInner::Pointer { base, .. } = *base_ty {
base_ty = &self.ir_module.types[base].inner;
} let index_id = ifletcrate::TypeInner::Struct { .. } = *base_ty { self.get_index_constant(index)
} else { // `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. 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) => { breakself.function.parameter_id(index);
} ref other => unimplemented!("Unexpected pointer expression {:?}", other),
}
};
let (pointer_id, expr_pointer) = ifself.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]; letcrate::TypeInner::BindingArray { .. } = self.ir_module.types[gvar.ty].inner else { returnfalse;
};
/// 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(
&mutself,
base: Handle<crate::Expression>,
index: GuardedIndex,
accumulated_checks: &mut Option<Word>,
block: &mut Block,
) -> Result<Word, Error> { matchself.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(
&mutself,
chain: &mut Option<Word>,
comparison_id: Word,
block: &mut Block,
) { match *chain {
Some(refmut 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(
&mutself,
pointer: Handle<crate::Expression>,
block: &mut Block,
access_type_adjustment: AccessTypeAdjustment,
result_type_id: Word,
) -> Result<Word, Error> { matchself.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 } => { matchself.ir_module.types[base].inner { crate::TypeInner::Atomic { .. } => Some(space),
_ => None,
}
}
_ => None,
}; let instruction = iflet 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(result_type_id, id, pointer_id, None)
};
block.body.push(instruction);
Ok(id)
}
ExpressionPointer::Conditional { condition, access } => { //TODO: support atomics? let value = self.write_conditional_indexed_load(
result_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(
result_type_id,
value_id,
pointer_id,
None,
));
value_id
},
);
Ok(value)
}
}
}
fn spill_to_internal_variable(&mutself, base: Handle<crate::Expression>, block: &mut Block) { // Generate an internal variable of the appropriate type for `base`. let variable_id = self.writer.id_gen.next(); let pointer_type_id = self
.writer
.get_resolution_pointer_id(&self.fun_info[base].ty, spirv::StorageClass::Function); let variable = super::LocalVariable {
id: variable_id,
instruction: Instruction::variable(
pointer_type_id,
variable_id,
spirv::StorageClass::Function,
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(
&mutself,
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(
&mutself,
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_type_id(LookupType::Local(LocalType::Numeric(NumericType::Vector {
size: rows,
scalar: crate::Scalar::float(width),
})));
for index in0..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 fn write_dot_product(
&mutself,
result_id: Word,
result_type_id: Word,
arg0_id: Word,
arg1_id: Word,
size: u32,
block: &mut Block,
) { letmut partial_sum = self.writer.get_constant_null(result_type_id); let last_component = size - 1; for index in0..=last_component { // compute the product of the current components let a_id = self.gen_id();
block.body.push(Instruction::composite_extract(
result_type_id,
a_id,
arg0_id,
&[index],
)); let b_id = self.gen_id();
block.body.push(Instruction::composite_extract(
result_type_id,
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;
}
}
/// 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(
&mutself,
label_id: Word,
naga_block: &crate::Block,
exit: BlockExit,
loop_context: LoopContext,
debug_info: Option<&DebugInfoInner>,
) -> Result<BlockExitDisposition, Error> { letmut block = Block::new(label_id); for (statement, span) in naga_block.span_iter() { iflet (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,
)?;
iflet 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,
)?;
} iflet 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,
));
letmut default_id = None; // id of previous empty fall-through case letmut last_id = None;
letmut raw_cases = Vec::with_capacity(cases.len()); letmut 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 iflet 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,
)); 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 = matchself.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,
)?;
Instruction::return_void()
}
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::Barrier(flags) => { self.writer.write_barrier(flags, &mut block);
}
Statement::Store { pointer, value } => { let value_id = self.cached[value]; matchself.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 } => { matchself.ir_module.types[base].inner { crate::TypeInner::Atomic { .. } => Some(space),
_ => None,
}
}
_ => None,
}; let instruction = iflet 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 } => { letmut 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);
iflet 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 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 => matchself.ir_function.result {
Some(ref result) ifself.function.entry_point_context.is_none() => { let type_id = self.get_type_id(LookupType::Handle(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(
&mutself,
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
¤ Dauer der Verarbeitung: 0.108 Sekunden
(vorverarbeitet am 2026-06-18)
¤
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.