usesuper::{sampler as sm, Error, LocationMode, Options, PipelineOptions, TranslationInfo}; usecrate::{
arena::{Handle, HandleSet},
back::{self, Baked},
proc::index,
proc::{self, NameKey, TypeResolution},
valid, FastHashMap, FastHashSet,
}; #[cfg(test)] use std::ptr; use std::{
fmt::{Display, Error as FmtError, Formatter, Write},
iter,
};
/// Shorthand result used internally by the backend type BackendResult = Result<(), Error>;
const NAMESPACE: &str = "metal"; // The name of the array member of the Metal struct types we generate to // represent Naga `Array` types. See the comments in `Writer::write_type_defs` // for details. const WRAPPED_ARRAY_FIELD: &str = "inner"; // This is a hack: we need to pass a pointer to an atomic, // but generally the backend isn't putting "&" in front of every pointer. // Some more general handling of pointers is needed to be implemented here. const ATOMIC_REFERENCE: &str = "&";
pub(crate) const ATOMIC_COMP_EXCH_FUNCTION: &str = "naga_atomic_compare_exchange_weak_explicit"; pub(crate) const MODF_FUNCTION: &str = "naga_modf"; pub(crate) const FREXP_FUNCTION: &str = "naga_frexp"; /// For some reason, Metal does not let you have `metal::texture<..>*` as a buffer argument. /// However, if you put that texture inside a struct, everything is totally fine. This /// baffles me to no end. /// /// As such, we wrap all argument buffers in a struct that has a single generic `<T>` field. /// This allows `NagaArgumentBufferWrapper<metal::texture<..>>*` to work. The astute among /// you have noticed that this should be exactly the same to the compiler, and you're correct. pub(crate) const ARGUMENT_BUFFER_WRAPPER_STRUCT: &str = "NagaArgumentBufferWrapper";
/// Write the Metal name for a Naga numeric type: scalar, vector, or matrix. /// /// The `sizes` slice determines whether this function writes a /// scalar, vector, or matrix type: /// /// - An empty slice produces a scalar type. /// - A one-element slice produces a vector type. /// - A two element slice `[ROWS COLUMNS]` produces a matrix of the given size. fn put_numeric_type(
out: &mutimpl Write,
scalar: crate::Scalar,
sizes: &[crate::VectorSize],
) -> Result<(), FmtError> { match (scalar, sizes) {
(scalar, &[]) => {
write!(out, "{}", scalar.to_msl_name())
}
(scalar, &[rows]) => {
write!(
out, "{}::{}{}",
NAMESPACE,
scalar.to_msl_name(),
back::vector_size_str(rows)
)
}
(scalar, &[rows, columns]) => {
write!(
out, "{}::{}{}x{}",
NAMESPACE,
scalar.to_msl_name(),
back::vector_size_str(columns),
back::vector_size_str(rows)
)
}
(_, _) => Ok(()), // not meaningful
}
}
/// Prefix for cached clamped level-of-detail values for `ImageLoad` expressions. const CLAMPED_LOD_LOAD_PREFIX: &str = "clamped_lod_e";
/// Wrapper for identifier names for clamped level-of-detail values /// /// Values of this type implement [`std::fmt::Display`], formatting as /// the name of the variable used to hold the cached clamped /// level-of-detail value for an `ImageLoad` expression. struct ClampedLod(Handle<crate::Expression>);
/// Wrapper for generating `struct _mslBufferSizes` member names for /// runtime-sized array lengths. /// /// On Metal, `wgpu_hal` passes the element counts for all runtime-sized arrays /// as an argument to the entry point. This argument's type in the MSL is /// `struct _mslBufferSizes`, a Naga-synthesized struct with a `uint` member for /// each global variable containing a runtime-sized array. /// /// If `global` is a [`Handle`] for a [`GlobalVariable`] that contains a /// runtime-sized array, then the value `ArraySize(global)` implements /// [`std::fmt::Display`], formatting as the name of the struct member carrying /// the number of elements in that runtime-sized array. /// /// [`GlobalVariable`]: crate::GlobalVariable struct ArraySizeMember(Handle<crate::GlobalVariable>);
impl TypedGlobalVariable<'_> { fn try_fmt<W: Write>(&self, out: &mut W) -> BackendResult { let var = &self.module.global_variables[self.handle]; let name = &self.names[&NameKey::GlobalVariable(self.handle)];
pubstruct Writer<W> {
out: W,
names: FastHashMap<NameKey, String>,
named_expressions: crate::NamedExpressions, /// Set of expressions that need to be baked to avoid unnecessary repetition in output
need_bake_expressions: back::NeedBakeExpressions,
namer: proc::Namer, #[cfg(test)]
put_expression_stack_pointers: FastHashSet<*const ()>, #[cfg(test)]
put_block_stack_pointers: FastHashSet<*const ()>, /// Set of (struct type, struct field index) denoting which fields require /// padding inserted **before** them (i.e. between fields at index - 1 and index)
struct_member_pads: FastHashSet<(Handle<crate::Type>, u32)>,
/// Name of the force-bounded-loop macro. /// /// See `emit_force_bounded_loop_macro` for details.
force_bounded_loop_macro_name: String,
}
implcrate::AddressSpace { /// Returns true if global variables in this address space are /// passed in function arguments. These arguments need to be /// passed through any functions called from the entry point. constfn needs_pass_through(&self) -> bool { match *self { Self::Uniform
| Self::Storage { .. }
| Self::Private
| Self::WorkGroup
| Self::PushConstant
| Self::Handle => true, Self::Function => false,
}
}
/// Returns true if the address space may need a "const" qualifier. constfn needs_access_qualifier(&self) -> bool { match *self { //Note: we are ignoring the storage access here, and instead // rely on the actual use of a global by functions. This means we // may end up with "const" even if the binding is read-write, // and that should be OK. Self::Storage { .. } => true, // These should always be read-write. Self::Private | Self::WorkGroup => false, // These translate to `constant` address space, no need for qualifiers. Self::Uniform | Self::PushConstant => false, // Not applicable. Self::Handle | Self::Function => false,
}
}
implcrate::Type { // Returns `true` if we need to emit an alias for this type. constfn needs_alias(&self) -> bool { usecrate::TypeInner as Ti;
matchself.inner { // value types are concise enough, we only alias them if they are named
Ti::Scalar(_)
| Ti::Vector { .. }
| Ti::Matrix { .. }
| Ti::Atomic(_)
| Ti::Pointer { .. }
| Ti::ValuePointer { .. } => self.name.is_some(), // composite types are better to be aliased, regardless of the name
Ti::Struct { .. } | Ti::Array { .. } => true, // handle types may be different, depending on the global var access, so we always inline them
Ti::Image { .. }
| Ti::Sampler { .. }
| Ti::AccelerationStructure
| Ti::RayQuery
| Ti::BindingArray { .. } => false,
}
}
}
/// A level of detail argument. /// /// When [`BoundsCheckPolicy::Restrict`] applies to an [`ImageLoad`] access, we /// save the clamped level of detail in a temporary variable whose name is based /// on the handle of the `ImageLoad` expression. But for other policies, we just /// use the expression directly. /// /// [`BoundsCheckPolicy::Restrict`]: index::BoundsCheckPolicy::Restrict /// [`ImageLoad`]: crate::Expression::ImageLoad #[derive(Clone, Copy)] enum LevelOfDetail {
Direct(Handle<crate::Expression>),
Restricted(Handle<crate::Expression>),
}
/// Values needed to select a particular texel for [`ImageLoad`] and [`ImageStore`]. /// /// When this is used in code paths unconcerned with the `Restrict` bounds check /// policy, the `LevelOfDetail` enum introduces an unneeded match, since `level` /// will always be either `None` or `Some(Direct(_))`. But this turns out not to /// be too awkward. If that changes, we can revisit. /// /// [`ImageLoad`]: crate::Expression::ImageLoad /// [`ImageStore`]: crate::Statement::ImageStore struct TexelAddress {
coordinate: Handle<crate::Expression>,
array_index: Option<Handle<crate::Expression>>,
sample: Option<Handle<crate::Expression>>,
level: Option<LevelOfDetail>,
}
/// The set of expressions used as indices in `ReadZeroSkipWrite`-policy /// accesses. These may need to be cached in temporary variables. See /// `index::find_checked_indexes` for details.
guarded_indices: HandleSet<crate::Expression>, /// See [`Writer::emit_force_bounded_loop_macro`] for details.
force_loop_bounding: bool,
}
/// Return true if calls to `image`'s `read` and `write` methods should supply a level of detail. /// /// Only mipmapped images need to specify a level of detail. Since 1D /// textures cannot have mipmaps, MSL requires that the level argument to /// texture1d queries and accesses must be a constexpr 0. It's easiest /// just to omit the level entirely for 1D textures. fn image_needs_lod(&self, image: Handle<crate::Expression>) -> bool { let image_ty = self.resolve_type(image); ifletcrate::TypeInner::Image { dim, class, .. } = *image_ty {
class.is_mipmapped() && dim != crate::ImageDimension::D1
} else { false
}
}
/// Define a macro to invoke at the bottom of each loop body, to /// defeat MSL infinite loop reasoning. /// /// If we haven't done so already, emit the definition of a preprocessor /// macro to be invoked at the end of each loop body in the generated MSL, /// to ensure that the MSL compiler's optimizations do not remove bounds /// checks. /// /// Only the first call to this function for a given module actually causes /// the macro definition to be written. Subsequent loops can simply use the /// prior macro definition, since macros aren't block-scoped. /// /// # What is this trying to solve? /// /// In Metal Shading Language, an infinite loop has undefined behavior. /// (This rule is inherited from C++14.) This means that, if the MSL /// compiler determines that a given loop will never exit, it may assume /// that it is never reached. It may thus assume that any conditions /// sufficient to cause the loop to be reached must be false. Like many /// optimizing compilers, MSL uses this kind of analysis to establish limits /// on the range of values variables involved in those conditions might /// hold. /// /// For example, suppose the MSL compiler sees the code: /// /// ```ignore /// if (i >= 10) { /// while (true) { } /// } /// ``` /// /// It will recognize that the `while` loop will never terminate, conclude /// that it must be unreachable, and thus infer that, if this code is /// reached, then `i < 10` at that point. /// /// Now suppose that, at some point where `i` has the same value as above, /// the compiler sees the code: /// /// ```ignore /// if (i < 10) { /// a[i] = 1; /// } /// ``` /// /// Because the compiler is confident that `i < 10`, it will make the /// assignment to `a[i]` unconditional, rewriting this code as, simply: /// /// ```ignore /// a[i] = 1; /// ``` /// /// If that `if` condition was injected by Naga to implement a bounds check, /// the MSL compiler's optimizations could allow out-of-bounds array /// accesses to occur. /// /// Naga cannot feasibly anticipate whether the MSL compiler will determine /// that a loop is infinite, so an attacker could craft a Naga module /// containing an infinite loop protected by conditions that cause the Metal /// compiler to remove bounds checks that Naga injected elsewhere in the /// function. /// /// This rewrite could occur even if the conditional assignment appears /// *before* the `while` loop, as long as `i < 10` by the time the loop is /// reached. This would allow the attacker to save the results of /// unauthorized reads somewhere accessible before entering the infinite /// loop. But even worse, the MSL compiler has been observed to simply /// delete the infinite loop entirely, so that even code dominated by the /// loop becomes reachable. This would make the attack even more flexible, /// since shaders that would appear to never terminate would actually exit /// nicely, after having stolen data from elsewhere in the GPU address /// space. /// /// To avoid UB, Naga must persuade the MSL compiler that no loop Naga /// generates is infinite. One approach would be to add inline assembly to /// each loop that is annotated as potentially branching out of the loop, /// but which in fact generates no instructions. Unfortunately, inline /// assembly is not handled correctly by some Metal device drivers. /// /// Instead, we add the following code to the bottom of every loop: /// /// ```ignore /// if (volatile bool unpredictable = false; unpredictable) /// break; /// ``` /// /// Although the `if` condition will always be false in any real execution, /// the `volatile` qualifier prevents the compiler from assuming this. Thus, /// it must assume that the `break` might be reached, and hence that the /// loop is not unbounded. This prevents the range analysis impact described /// above. /// /// Unfortunately, what makes this a kludge, not a hack, is that this /// solution leaves the GPU executing a pointless conditional branch, at /// runtime, in every iteration of the loop. There's no part of the system /// that has a global enough view to be sure that `unpredictable` is true, /// and remove it from the code. Adding the branch also affects /// optimization: for example, it's impossible to unroll this loop. This /// transformation has been observed to significantly hurt performance. /// /// To make our output a bit more legible, we pull the condition out into a /// preprocessor macro defined at the top of the module. /// /// This approach is also used by Chromium WebGPU's Dawn shader compiler: /// <https://dawn.googlesource.com/dawn/+/a37557db581c2b60fb1cd2c01abdb232927dd961/src/tint/lang/msl/writer/printer/printer.cc#222> fn emit_force_bounded_loop_macro(&mutself) -> BackendResult { if !self.force_bounded_loop_macro_name.is_empty() { return Ok(());
}
fn put_image_size_query(
&mutself,
image: Handle<crate::Expression>,
level: Option<LevelOfDetail>,
kind: crate::ScalarKind,
context: &ExpressionContext,
) -> BackendResult { //Note: MSL only has separate width/height/depth queries, // so compose the result of them. let dim = match *context.resolve_type(image) { crate::TypeInner::Image { dim, .. } => dim, ref other => unreachable!("Unexpected type {:?}", other),
}; let scalar = crate::Scalar { kind, width: 4 }; let coordinate_type = scalar.to_msl_name(); match dim { crate::ImageDimension::D1 => { // Since 1D textures never have mipmaps, MSL requires that the // `level` argument be a constexpr 0. It's simplest for us just // to pass `None` and omit the level entirely. if kind == crate::ScalarKind::Uint { // No need to construct a vector. No cast needed. self.put_image_query(image, "width", None, context)?;
} else { // There's no definition for `int` in the `metal` namespace.
write!(self.out, "int(")?; self.put_image_query(image, "width", None, context)?;
write!(self.out, ")")?;
}
} crate::ImageDimension::D2 => {
write!(self.out, "{NAMESPACE}::{coordinate_type}2(")?; self.put_image_query(image, "width", level, context)?;
write!(self.out, ", ")?; self.put_image_query(image, "height", level, context)?;
write!(self.out, ")")?;
} crate::ImageDimension::D3 => {
write!(self.out, "{NAMESPACE}::{coordinate_type}3(")?; self.put_image_query(image, "width", level, context)?;
write!(self.out, ", ")?; self.put_image_query(image, "height", level, context)?;
write!(self.out, ", ")?; self.put_image_query(image, "depth", level, context)?;
write!(self.out, ")")?;
} crate::ImageDimension::Cube => {
write!(self.out, "{NAMESPACE}::{coordinate_type}2(")?; self.put_image_query(image, "width", level, context)?;
write!(self.out, ")")?;
}
}
Ok(())
}
fn put_cast_to_uint_scalar_or_vector(
&mutself,
expr: Handle<crate::Expression>,
context: &ExpressionContext,
) -> BackendResult { // coordinates in IR are int, but Metal expects uint match *context.resolve_type(expr) { crate::TypeInner::Scalar(_) => {
put_numeric_type(&mutself.out, crate::Scalar::U32, &[])?
} crate::TypeInner::Vector { size, .. } => {
put_numeric_type(&mutself.out, crate::Scalar::U32, &[size])?
}
_ => { return Err(Error::GenericValidation( "Invalid type for image coordinate".into(),
))
}
};
// Write the array index, if present. iflet Some(array_index) = address.array_index {
write!(self.out, ", ")?; self.put_restricted_scalar_image_index(image, array_index, "get_array_size", context)?;
}
// Write the sample index, if present. iflet Some(sample) = address.sample {
write!(self.out, ", ")?; self.put_restricted_scalar_image_index(image, sample, "get_num_samples", context)?;
}
// The level of detail should be clamped and cached by // `put_cache_restricted_level`, so we don't need to clamp it here. iflet Some(level) = address.level {
write!(self.out, ", ")?; self.put_level_of_detail(level, context)?;
}
Ok(())
}
/// Write an expression that is true if the given image access is in bounds. fn put_image_access_bounds_check(
&mutself,
image: Handle<crate::Expression>,
address: &TexelAddress,
context: &ExpressionContext,
) -> BackendResult { letmut conjunction = "";
// First, check the level of detail. Only if that is in bounds can we // use it to find the appropriate bounds for the coordinates. let level = iflet Some(level) = address.level {
write!(self.out, "uint(")?; self.put_level_of_detail(level, context)?;
write!(self.out, ") < ")?; self.put_expression(image, context, true)?;
write!(self.out, ".get_num_mip_levels()")?;
conjunction = " && ";
Some(level)
} else {
None
};
/// Write the maximum valid index of the dynamically sized array at the end of `handle`. /// /// The 'maximum valid index' is simply one less than the array's length. /// /// This emits an expression of the form `a / b`, so the caller must /// parenthesize its output if it will be applying operators of higher /// precedence. /// /// `handle` must be the handle of a global variable whose final member is a /// dynamically sized array. fn put_dynamic_array_max_index(
&mutself,
handle: Handle<crate::GlobalVariable>,
context: &ExpressionContext,
) -> BackendResult { let global = &context.module.global_variables[handle]; let (offset, array_ty) = match context.module.types[global.ty].inner { crate::TypeInner::Struct { ref members, .. } => match members.last() {
Some(&crate::StructMember { offset, ty, .. }) => (offset, ty),
None => return Err(Error::GenericValidation("Struct has no members".into())),
}, crate::TypeInner::Array {
size: crate::ArraySize::Dynamic,
..
} => (0, global.ty), ref ty => { return Err(Error::GenericValidation(format!( "Expected type with dynamic array, got {ty:?}"
)))
}
};
// When the stride length is larger than the size, the final element's stride of // bytes would have padding following the value. But the buffer size in // `buffer_sizes.sizeN` may not include this padding - it only needs to be large // enough to hold the actual values' bytes. // // So subtract off the size to get a byte size that falls at the start or within // the final element. Then divide by the stride size, to get one less than the // length, and then add one. This works even if the buffer size does include the // stride padding, since division rounds towards zero (MSL 2.4 §6.1). It will fail // if there are zero elements in the array, but the WebGPU `validating shader binding` // rules, together with draw-time validation when `minBindingSize` is zero, // prevent that.
write!( self.out, "(_buffer_sizes.{member} - {offset} - {size}) / {stride}",
member = ArraySizeMember(handle),
offset = offset,
size = size,
stride = stride,
)?;
Ok(())
}
/// Emit code for the arithmetic expression of the dot product. /// fn put_dot_product(
&mutself,
arg: Handle<crate::Expression>,
arg1: Handle<crate::Expression>,
size: usize,
context: &ExpressionContext,
) -> BackendResult { // Write parentheses around the dot product expression to prevent operators // with different precedences from applying earlier.
write!(self.out, "(")?;
// Cycle through all the components of the vector for index in0..size { let component = back::COMPONENTS[index]; // Write the addition to the previous product // This will print an extra '+' at the beginning but that is fine in msl
write!(self.out, " + ")?; // Write the first vector expression, this expression is marked to be // cached so unless it can't be cached (for example, it's a Constant) // it shouldn't produce large expressions. self.put_expression(arg, context, true)?; // Access the current component on the first vector
write!(self.out, ".{component} * ")?; // Write the second vector expression, this expression is marked to be // cached so unless it can't be cached (for example, it's a Constant) // it shouldn't produce large expressions. self.put_expression(arg1, context, true)?; // Access the current component on the second vector
write!(self.out, ".{component}")?;
}
/// Emit code for the expression `expr_handle`. /// /// The `is_scoped` argument is true if the surrounding operators have the /// precedence of the comma operator, or lower. So, for example: /// /// - Pass `true` for `is_scoped` when writing function arguments, an /// expression statement, an initializer expression, or anything already /// wrapped in parenthesis. /// /// - Pass `false` if it is an operand of a `?:` operator, a `[]`, or really /// almost anything else. fn put_expression(
&mutself,
expr_handle: Handle<crate::Expression>,
context: &ExpressionContext,
is_scoped: bool,
) -> BackendResult { // Add to the set in order to track the stack size. #[cfg(test)] self.put_expression_stack_pointers
.insert(ptr::from_ref(&expr_handle).cast());
let expression = &context.function.expressions[expr_handle];
log::trace!("expression {:?} = {:?}", expr_handle, expression); match *expression { crate::Expression::Literal(_)
| crate::Expression::Constant(_)
| crate::Expression::ZeroValue(_)
| crate::Expression::Compose { .. }
| crate::Expression::Splat { .. } => { self.put_possibly_const_expression(
expr_handle,
&context.function.expressions,
context.module,
context.mod_info,
context,
|context, expr: Handle<crate::Expression>| &context.info[expr].ty,
|writer, context, expr| writer.put_expression(expr, context, true),
)?;
} crate::Expression::Override(_) => return Err(Error::Override), crate::Expression::Access { base, .. }
| crate::Expression::AccessIndex { base, .. } => { // This is an acceptable place to generate a `ReadZeroSkipWrite` check. // Since `put_bounds_checks` and `put_access_chain` handle an entire // access chain at a time, recursing back through `put_expression` only // for index expressions and the base object, we will never see intermediate // `Access` or `AccessIndex` expressions here. let policy = context.choose_bounds_check_policy(base); if policy == index::BoundsCheckPolicy::ReadZeroSkipWrite
&& self.put_bounds_checks(
expr_handle,
context,
back::Level(0), if is_scoped { "" } else { "(" },
)?
{
write!(self.out, " ? ")?; self.put_access_chain(expr_handle, policy, context)?;
write!(self.out, " : DefaultConstructible()")?;
match gather {
None | Some(crate::SwizzleComponent::X) => {}
Some(component) => { let is_cube_map = match *context.resolve_type(image) { crate::TypeInner::Image {
dim: crate::ImageDimension::Cube,
..
} => true,
_ => false,
}; // Offset always comes before the gather, except // in cube maps where it's not applicable if offset.is_none() && !is_cube_map {
write!(self.out, ", {NAMESPACE}::int2(0)")?;
} let letter = back::COMPONENTS[component as usize];
write!(self.out, ", {NAMESPACE}::component::{letter}")?;
}
}
write!(self.out, ")")?;
} crate::Expression::ImageLoad {
image,
coordinate,
array_index,
sample,
level,
} => { let address = TexelAddress {
coordinate,
array_index,
sample,
level: level.map(LevelOfDetail::Direct),
}; self.put_image_load(expr_handle, image, address, context)?;
} //Note: for all the queries, the signed integers are expected, // so a conversion is needed. crate::Expression::ImageQuery { image, query } => match query { crate::ImageQuery::Size { level } => { self.put_image_size_query(
image,
level.map(LevelOfDetail::Direct), crate::ScalarKind::Uint,
context,
)?;
} crate::ImageQuery::NumLevels => { self.put_expression(image, context, false)?;
write!(self.out, ".get_num_mip_levels()")?;
} crate::ImageQuery::NumLayers => { self.put_expression(image, context, false)?;
write!(self.out, ".get_array_size()")?;
} crate::ImageQuery::NumSamples => { self.put_expression(image, context, false)?;
write!(self.out, ".get_num_samples()")?;
}
}, crate::Expression::Unary { op, expr } => { let op_str = match op { crate::UnaryOperator::Negate => "-", crate::UnaryOperator::LogicalNot => "!", crate::UnaryOperator::BitwiseNot => "~",
};
write!(self.out, "{op_str}(")?; self.put_expression(expr, context, false)?;
write!(self.out, ")")?;
} crate::Expression::Binary { op, left, right } => { let op_str = back::binary_operation_str(op); let kind = context
.resolve_type(left)
.scalar_kind()
.ok_or(Error::UnsupportedBinaryOp(op))?;
// TODO: handle undefined behavior of BinaryOperator::Modulo // // sint: // if right == 0 return 0 // if left == min(type_of(left)) && right == -1 return 0 // if sign(left) == -1 || sign(right) == -1 return result as defined by WGSL // // uint: // if right == 0 return 0 // // float: // if right == 0 return ? see https://github.com/gpuweb/gpuweb/issues/2798
// or metal will complain that select is ambiguous match *inner { crate::TypeInner::Vector { size, scalar } => { let size = back::vector_size_str(size); let name = scalar.to_msl_name();
write!(self.out, "{name}{size}")?;
} crate::TypeInner::Scalar(scalar) => { let name = scalar.to_msl_name();
write!(self.out, "{name}")?;
}
_ => (),
}
write!(self.out, "(-1), ")?; self.put_expression(arg, context, true)?;
write!(self.out, " == 0 || ")?; self.put_expression(arg, context, true)?;
write!(self.out, " == -1)")?;
}
Mf::Unpack2x16float => {
write!(self.out, "float2(as_type<half2>(")?; self.put_expression(arg, context, false)?;
write!(self.out, "))")?;
}
Mf::Pack2x16float => {
write!(self.out, "as_type<uint>(half2(")?; self.put_expression(arg, context, false)?;
write!(self.out, "))")?;
}
Mf::ExtractBits => { // 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, Apple chips // 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, without temporary values: // 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) // // extract_bits(e, min(offset, w), min(count, w - min(offset, w))))
let scalar_bits = context.resolve_type(arg).scalar_width().unwrap() * 8;
write!(self.out, "{NAMESPACE}::extract_bits(")?; self.put_expression(arg, context, true)?;
write!(self.out, ", {NAMESPACE}::min(")?; self.put_expression(arg1.unwrap(), context, true)?;
write!(self.out, ", {scalar_bits}u), {NAMESPACE}::min(")?; self.put_expression(arg2.unwrap(), context, true)?;
write!(self.out, ", {scalar_bits}u - {NAMESPACE}::min(")?; self.put_expression(arg1.unwrap(), context, true)?;
write!(self.out, ", {scalar_bits}u)))")?;
}
Mf::InsertBits => { // The behavior of InsertBits has the same issue as ExtractBits. // // insertBits(e, newBits, min(offset, w), min(count, w - min(offset, w))))
let scalar_bits = context.resolve_type(arg).scalar_width().unwrap() * 8;
let ty = context.module.special_types.ray_intersection.unwrap(); let type_name = &self.names[&NameKey::Type(ty)];
write!(self.out, "{type_name} {{{RAY_QUERY_FUN_MAP_INTERSECTION}(")?; self.put_expression(query, context, true)?;
write!(self.out, ".{RAY_QUERY_FIELD_INTERSECTION}.type)")?; let fields = [ "distance", "user_instance_id", // req Metal 2.4 "instance_id", "", // SBT offset "geometry_id", "primitive_id", "triangle_barycentric_coord", "triangle_front_facing", "", // padding "object_to_world_transform", // req Metal 2.4 "world_to_object_transform", // req Metal 2.4
]; for field in fields {
write!(self.out, ", ")?; if field.is_empty() {
write!(self.out, "{{}}")?;
} else { self.put_expression(query, context, true)?;
write!(self.out, ".{RAY_QUERY_FIELD_INTERSECTION}.{field}")?;
}
}
write!(self.out, "}}")?;
}
}
Ok(())
}
/// Used by expressions like Swizzle and Binary since they need packed_vec3's to be casted to a vec3 fn put_wrapped_expression_for_packed_vec3_access(
&mutself,
expr_handle: Handle<crate::Expression>,
context: &ExpressionContext,
is_scoped: bool,
) -> BackendResult { iflet Some(scalar) = context.get_packed_vec_kind(expr_handle) {
write!(self.out, "{}::{}3(", NAMESPACE, scalar.to_msl_name())?; self.put_expression(expr_handle, context, is_scoped)?;
write!(self.out, ")")?;
} else { self.put_expression(expr_handle, context, is_scoped)?;
}
Ok(())
}
/// Write a `GuardedIndex` as a Metal expression. fn put_index(
&mutself,
index: index::GuardedIndex,
context: &ExpressionContext,
is_scoped: bool,
) -> BackendResult { match index {
index::GuardedIndex::Expression(expr) => { self.put_expression(expr, context, is_scoped)?
}
index::GuardedIndex::Known(value) => write!(self.out, "{value}")?,
}
Ok(())
}
/// Emit an index bounds check condition for `chain`, if required. /// /// `chain` is a subtree of `Access` and `AccessIndex` expressions, /// operating either on a pointer to a value, or on a value directly. If we cannot /// statically determine that all indexing operations in `chain` are within /// bounds, then write a conditional expression to check them dynamically, /// and return true. All accesses in the chain are checked by the generated /// expression. /// /// This assumes that the [`BoundsCheckPolicy`] for `chain` is [`ReadZeroSkipWrite`]. /// /// The text written is of the form: /// /// ```ignore /// {level}{prefix}uint(i) < 4 && uint(j) < 10 /// ``` /// /// where `{level}` and `{prefix}` are the arguments to this function. For [`Store`] /// statements, presumably these arguments start an indented `if` statement; for /// [`Load`] expressions, the caller is probably building up a ternary `?:` /// expression. In either case, what is written is not a complete syntactic structure /// in its own right, and the caller will have to finish it off if we return `true`. /// /// If no expression is written, return false. /// /// [`BoundsCheckPolicy`]: index::BoundsCheckPolicy /// [`ReadZeroSkipWrite`]: index::BoundsCheckPolicy::ReadZeroSkipWrite /// [`Store`]: crate::Statement::Store /// [`Load`]: crate::Expression::Load #[allow(unused_variables)] fn put_bounds_checks(
&mutself, mut chain: Handle<crate::Expression>,
context: &ExpressionContext,
level: back::Level,
prefix: &'static str,
) -> Result<bool, Error> { letmut check_written = false;
// Iterate over the access chain, handling each expression. loop { // Produce a `GuardedIndex`, so we can shared code between the // `Access` and `AccessIndex` cases. let (base, guarded_index) = match context.function.expressions[chain] { crate::Expression::Access { base, index } => {
(base, Some(index::GuardedIndex::Expression(index)))
} crate::Expression::AccessIndex { base, index } => { // Don't try to check indices into structs. Validation already took // care of them, and index::needs_guard doesn't handle that case. letmut base_inner = context.resolve_type(base); ifletcrate::TypeInner::Pointer { base, .. } = *base_inner {
base_inner = &context.module.types[base].inner;
} match *base_inner { crate::TypeInner::Struct { .. } => (base, None),
_ => (base, Some(index::GuardedIndex::Known(index))),
}
}
_ => break,
};
// Check that the index falls within bounds. Do this with a single // comparison, by casting the index to `uint` first, so that negative // indices become large positive values.
write!(self.out, "uint(")?; self.put_index(index, context, true)?; self.out.write_str(") < ")?; match length {
index::IndexableLength::Known(value) => write!(self.out, "{value}")?,
index::IndexableLength::Pending => unreachable!(),
index::IndexableLength::Dynamic => { let global =
context.function.originating_global(base).ok_or_else(|| {
Error::GenericValidation( "Could not find originating global".into(),
)
})?;
write!(self.out, "1 + ")?; self.put_dynamic_array_max_index(global, context)?
}
}
}
}
chain = base
}
Ok(check_written)
}
/// Write the access chain `chain`. /// /// `chain` is a subtree of [`Access`] and [`AccessIndex`] expressions, /// operating either on a pointer to a value, or on a value directly. /// /// Generate bounds checks code only if `policy` is [`Restrict`]. The /// [`ReadZeroSkipWrite`] policy requires checks before any accesses take place, so /// that must be handled in the caller. /// /// Handle the entire chain, recursing back into `put_expression` only for index /// expressions and the base expression that originates the pointer or composite value /// being accessed. This allows `put_expression` to assume that any `Access` or /// `AccessIndex` expressions it sees are the top of a chain, so it can emit /// `ReadZeroSkipWrite` checks. /// /// [`Access`]: crate::Expression::Access /// [`AccessIndex`]: crate::Expression::AccessIndex /// [`Restrict`]: crate::proc::index::BoundsCheckPolicy::Restrict /// [`ReadZeroSkipWrite`]: crate::proc::index::BoundsCheckPolicy::ReadZeroSkipWrite fn put_access_chain(
&mutself,
chain: Handle<crate::Expression>,
policy: index::BoundsCheckPolicy,
context: &ExpressionContext,
) -> BackendResult { match context.function.expressions[chain] { crate::Expression::Access { base, index } => { letmut base_ty = context.resolve_type(base);
// Look through any pointers to see what we're really indexing. ifletcrate::TypeInner::Pointer { base, space: _ } = *base_ty {
base_ty = &context.module.types[base].inner;
}
// Look through any pointers to see what we're really indexing. ifletcrate::TypeInner::Pointer { base, space: _ } = *base_ty {
base_ty = &context.module.types[base].inner;
base_ty_handle = Some(base);
}
// Handle structs and anything else that can use `.x` syntax here, so // `put_subscripted_access_chain` won't have to handle the absurd case of // indexing a struct with an expression. match *base_ty { crate::TypeInner::Struct { .. } => { let base_ty = base_ty_handle.unwrap(); self.put_access_chain(base, policy, context)?; let name = &self.names[&NameKey::StructMember(base_ty, index)];
write!(self.out, ".{name}")?;
} crate::TypeInner::ValuePointer { .. } | crate::TypeInner::Vector { .. } => { self.put_access_chain(base, policy, context)?; // Prior to Metal v2.1 component access for packed vectors wasn't available // however array indexing is if context.get_packed_vec_kind(base).is_some() {
write!(self.out, "[{index}]")?;
} else {
write!(self.out, ".{}", back::COMPONENTS[index as usize])?;
}
}
_ => { self.put_subscripted_access_chain(
base,
base_ty,
index::GuardedIndex::Known(index),
policy,
context,
)?;
}
}
}
_ => self.put_expression(chain, context, false)?,
}
Ok(())
}
/// Write a `[]`-style access of `base` by `index`. /// /// If `policy` is [`Restrict`], then generate code as needed to force all index /// values within bounds. /// /// The `base_ty` argument must be the type we are actually indexing, like [`Array`] or /// [`Vector`]. In other words, it's `base`'s type with any surrounding [`Pointer`] /// removed. Our callers often already have this handy. /// /// This only emits `[]` expressions; it doesn't handle struct member accesses or /// referencing vector components by name. /// /// [`Restrict`]: crate::proc::index::BoundsCheckPolicy::Restrict /// [`Array`]: crate::TypeInner::Array /// [`Vector`]: crate::TypeInner::Vector /// [`Pointer`]: crate::TypeInner::Pointer fn put_subscripted_access_chain(
&mutself,
base: Handle<crate::Expression>,
base_ty: &crate::TypeInner,
index: index::GuardedIndex,
policy: index::BoundsCheckPolicy,
context: &ExpressionContext,
) -> BackendResult { let accessing_wrapped_array = match *base_ty { crate::TypeInner::Array {
size: crate::ArraySize::Constant(_),
..
} => true,
_ => false,
}; let accessing_wrapped_binding_array =
matches!(*base_ty, crate::TypeInner::BindingArray { .. });
if is_atomic_pointer {
write!( self.out, "{NAMESPACE}::atomic_load_explicit({ATOMIC_REFERENCE}"
)?; self.put_access_chain(pointer, policy, context)?;
write!(self.out, ", {NAMESPACE}::memory_order_relaxed)")?;
} else { // We don't do any dereferencing with `*` here as pointer arguments to functions // are done by `&` references and not `*` pointers. These do not need to be // dereferenced. self.put_access_chain(pointer, policy, context)?;
}
for (index, member) in members.iter().enumerate() { iflet Some(crate::Binding::BuiltIn(crate::BuiltIn::PointSize)) =
member.binding
{
has_point_size = true; if !context.pipeline_options.allow_and_force_point_size { continue;
}
}
let comma = if is_first { "" } else { "," };
is_first = false; let name = &self.names[&NameKey::StructMember(result_ty, index as u32)]; // HACK: we are forcefully deduplicating the expression here // to convert from a wrapped struct to a raw array, e.g. // `float gl_ClipDistance1 [[clip_distance]] [1];`. ifletcrate::TypeInner::Array {
size: crate::ArraySize::Constant(size),
..
} = context.module.types[member.ty].inner
{
write!(self.out, "{comma} {{")?; for j in0..size.get() { if j != 0 {
write!(self.out, ",")?;
}
write!(self.out, "{tmp}.{name}.{WRAPPED_ARRAY_FIELD}[{j}]")?;
}
write!(self.out, "}}")?;
} else {
write!(self.out, "{comma} {tmp}.{name}")?;
}
}
}
_ => {
write!(self.out, "{level}return {struct_name} {{ ")?; self.put_expression(expr_handle, context, true)?;
}
}
iflet FunctionOrigin::EntryPoint(ep_index) = context.origin { let stage = context.module.entry_points[ep_index as usize].stage; if context.pipeline_options.allow_and_force_point_size
&& stage == crate::ShaderStage::Vertex
&& !has_point_size
{ // point size was injected and comes last
write!(self.out, ", 1.0")?;
}
}
write!(self.out, " }}")?;
}
None => {
write!(self.out, "{level}return ")?; self.put_expression(expr_handle, context, true)?;
}
}
writeln!(self.out, ";")?;
Ok(())
}
/// Helper method used to find which expressions of a given function require baking /// /// # Notes /// This function overwrites the contents of `self.need_bake_expressions` fn update_expressions_to_bake(
&mutself,
func: &crate::Function,
info: &valid::FunctionInfo,
context: &ExpressionContext,
) { usecrate::Expression; self.need_bake_expressions.clear();
for (expr_handle, expr) in func.expressions.iter() { // Expressions whose reference count is above the // threshold should always be stored in temporaries. let expr_info = &info[expr_handle]; let min_ref_count = func.expressions[expr_handle].bake_ref_count(); if min_ref_count <= expr_info.ref_count { self.need_bake_expressions.insert(expr_handle);
} else { match expr_info.ty { // force ray desc to be baked: it's used multiple times internally
TypeResolution::Handle(h) if Some(h) == context.module.special_types.ray_desc =>
{ self.need_bake_expressions.insert(expr_handle);
}
_ => {}
}
}
iflet Expression::Math {
fun,
arg,
arg1,
arg2,
..
} = *expr
{ match fun { crate::MathFunction::Dot => { // WGSL's `dot` function works on any `vecN` type, but Metal's only // works on floating-point vectors, so we emit inline code for // integer vector `dot` calls. But that code uses each argument `N` // times, once for each component (see `put_dot_product`), so to // avoid duplicated evaluation, we must bake integer operands.
// check what kind of product this is depending // on the resolve type of the Dot function itself let inner = context.resolve_type(expr_handle); ifletcrate::TypeInner::Scalar(scalar) = *inner { match scalar.kind { crate::ScalarKind::Sint | crate::ScalarKind::Uint => { self.need_bake_expressions.insert(arg); self.need_bake_expressions.insert(arg1.unwrap());
}
_ => {}
}
}
} crate::MathFunction::FirstLeadingBit
| crate::MathFunction::Pack4xI8
| crate::MathFunction::Pack4xU8
| crate::MathFunction::Unpack4xI8
| crate::MathFunction::Unpack4xU8 => { self.need_bake_expressions.insert(arg);
} crate::MathFunction::ExtractBits => { // Only argument 1 is re-used. self.need_bake_expressions.insert(arg1.unwrap());
} crate::MathFunction::InsertBits => { // Only argument 2 is re-used. self.need_bake_expressions.insert(arg2.unwrap());
} crate::MathFunction::Sign => { // WGSL's `sign` function works also on signed ints, but Metal's only // works on floating points, so we emit inline code for integer `sign` // calls. But that code uses each argument 2 times (see `put_isign`), // so to avoid duplicated evaluation, we must bake the argument. let inner = context.resolve_type(expr_handle); if inner.scalar_kind() == Some(crate::ScalarKind::Sint) { self.need_bake_expressions.insert(arg);
}
}
_ => {}
}
}
}
}
//TODO: figure out the naming scheme that wouldn't collide with user names.
write!(self.out, " {name} = ")?;
Ok(())
}
/// Cache a clamped level of detail value, if necessary. /// /// [`ImageLoad`] accesses covered by [`BoundsCheckPolicy::Restrict`] use a /// properly clamped level of detail value both in the access itself, and /// for fetching the size of the requested MIP level, needed to clamp the /// coordinates. To avoid recomputing this clamped level of detail, we cache /// it in a temporary variable, as part of the [`Emit`] statement covering /// the [`ImageLoad`] expression. /// /// [`ImageLoad`]: crate::Expression::ImageLoad /// [`BoundsCheckPolicy::Restrict`]: index::BoundsCheckPolicy::Restrict /// [`Emit`]: crate::Statement::Emit fn put_cache_restricted_level(
&mutself,
load: Handle<crate::Expression>,
image: Handle<crate::Expression>,
mip_level: Option<Handle<crate::Expression>>,
indent: back::Level,
context: &StatementContext,
) -> BackendResult { // Does this image access actually require (or even permit) a // level-of-detail, and does the policy require us to restrict it? let level_of_detail = match mip_level {
Some(level) => level,
None => return Ok(()),
};
if context.expression.policies.image_load != index::BoundsCheckPolicy::Restrict
|| !context.expression.image_needs_lod(image)
{ return Ok(());
}
fn put_block(
&mutself,
level: back::Level,
statements: &[crate::Statement],
context: &StatementContext,
) -> BackendResult { // Add to the set in order to track the stack size. #[cfg(test)] self.put_block_stack_pointers
.insert(ptr::from_ref(&level).cast());
for statement in statements {
log::trace!("statement[{}] {:?}", level.0, statement); match *statement { crate::Statement::Emit(ref range) => { for handle in range.clone() { // `ImageLoad` expressions covered by the `Restrict` bounds check policy // may need to cache a clamped version of their level-of-detail argument. ifletcrate::Expression::ImageLoad {
image,
level: mip_level,
..
} = context.expression.function.expressions[handle]
{ self.put_cache_restricted_level(
handle, image, mip_level, level, context,
)?;
}
let ptr_class = context.expression.resolve_type(handle).pointer_space(); let expr_name = if ptr_class.is_some() {
None // don't bake pointer expressions (just yet)
} elseiflet Some(name) =
context.expression.function.named_expressions.get(&handle)
{ // The `crate::Function::named_expressions` table holds // expressions that should be saved in temporaries once they // are `Emit`ted. We only add them to `self.named_expressions` // when we reach the `Emit` that covers them, so that we don't // try to use their names before we've actually initialized // the temporary that holds them. // // Don't assume the names in `named_expressions` are unique, // or even valid. Use the `Namer`.
Some(self.namer.call(name))
} else { // If this expression is an index that we're going to first compare // against a limit, and then actually use as an index, then we may // want to cache it in a temporary, to avoid evaluating it twice. let bake = if context.expression.guarded_indices.contains(handle) { true
} else { self.need_bake_expressions.contains(&handle)
};
if bake {
Some(Baked(handle).to_string())
} else {
None
}
};
// This backend supports `SHADER_INT64_ATOMIC_MIN_MAX` but not // `SHADER_INT64_ATOMIC_ALL_OPS`, so we can assume that if `result` is // `Some`, we are not operating on a 64-bit value, and that if we are // operating on a 64-bit value, `result` is `None`.
write!(self.out, "{level}")?; let fun_key = iflet Some(result) = result { let res_name = Baked(result).to_string(); self.start_baking_expression(result, context, &res_name)?; self.named_expressions.insert(result, res_name);
fun.to_msl()
} elseif context.resolve_type(value).scalar_width() == Some(8) {
fun.to_msl_64_bit()?
} else {
fun.to_msl()
};
// If the pointer we're passing to the atomic operation needs to be conditional // for `ReadZeroSkipWrite`, the condition needs to *surround* the atomic op, and // the pointer operand should be unchecked. let policy = context.choose_bounds_check_policy(pointer); let checked = policy == index::BoundsCheckPolicy::ReadZeroSkipWrite
&& self.put_bounds_checks(pointer, context, back::Level(0), "")?;
// If requested and successfully put bounds checks, continue the ternary expression. if checked {
write!(self.out, " ? ")?;
}
// un-emit expressions //TODO: take care of loop/continuing? for statement in statements { ifletcrate::Statement::Emit(ref range) = *statement { for handle in range.clone() { self.named_expressions.shift_remove(&handle);
}
}
}
Ok(())
}
writeln!( self.out, "// language: metal{}.{}",
options.lang_version.0, options.lang_version.1
)?;
writeln!(self.out, "#include <metal_stdlib>")?;
writeln!(self.out, "#include <simd/simd.h>")?;
writeln!(self.out)?; // Work around Metal bug where `uint` is not available by default
writeln!(self.out, "using {NAMESPACE}::uint;")?;
letmut uses_ray_query = false; for (_, ty) in module.types.iter() { match ty.inner { crate::TypeInner::AccelerationStructure => { if options.lang_version < (2, 4) { return Err(Error::UnsupportedRayTracing);
}
} crate::TypeInner::RayQuery => { if options.lang_version < (2, 4) { return Err(Error::UnsupportedRayTracing);
}
uses_ray_query = true;
}
_ => (),
}
}
if module.special_types.ray_desc.is_some()
|| module.special_types.ray_intersection.is_some()
{ if options.lang_version < (2, 4) { return Err(Error::UnsupportedRayTracing);
}
}
if uses_ray_query { self.put_ray_query_type()?;
}
if options
.bounds_check_policies
.contains(index::BoundsCheckPolicy::ReadZeroSkipWrite)
{ self.put_default_constructible()?;
}
writeln!(self.out)?;
{ // Make a `Vec` of all the `GlobalVariable`s that contain // runtime-sized arrays. let globals: Vec<Handle<crate::GlobalVariable>> = module
.global_variables
.iter()
.filter(|&(_, var)| needs_array_length(var.ty, &module.types))
.map(|(handle, _)| handle)
.collect();
letmut buffer_indices = vec![]; for vbm in &pipeline_options.vertex_buffer_mappings {
buffer_indices.push(vbm.id);
}
if !globals.is_empty() || !buffer_indices.is_empty() {
writeln!(self.out, "struct _mslBufferSizes {{")?;
for global in globals {
writeln!( self.out, "{}uint {};",
back::INDENT,
ArraySizeMember(global)
)?;
}
for idx in buffer_indices {
writeln!(self.out, "{}uint buffer_size{};", back::INDENT, idx)?;
}
/// Write the definition for the `DefaultConstructible` class. /// /// The [`ReadZeroSkipWrite`] bounds check policy requires us to be able to /// produce 'zero' values for any type, including structs, arrays, and so /// on. We could do this by emitting default constructor applications, but /// that would entail printing the name of the type, which is more trouble /// than you'd think. Instead, we just construct this magic C++14 class that /// can be converted to any type that can be default constructed, using /// template parameter inference to detect which type is needed, so we don't /// have to figure out the name. /// /// [`ReadZeroSkipWrite`]: index::BoundsCheckPolicy::ReadZeroSkipWrite fn put_default_constructible(&mutself) -> BackendResult { let tab = back::INDENT;
writeln!(self.out, "struct DefaultConstructible {{")?;
writeln!(self.out, "{tab}template<typename T>")?;
writeln!(self.out, "{tab}operator T() && {{")?;
writeln!(self.out, "{tab}{tab}return T {{}};")?;
writeln!(self.out, "{tab}}}")?;
writeln!(self.out, "}};")?;
Ok(())
}
if !ty.needs_alias() { continue;
} let name = &self.names[&NameKey::Type(handle)]; match ty.inner { // Naga IR can pass around arrays by value, but Metal, following // C++, performs an array-to-pointer conversion (C++ [conv.array]) // on expressions of array type, so assigning the array by value // isn't possible. However, Metal *does* assign structs by // value. So in our Metal output, we wrap all array types in // synthetic struct types: // // struct type1 { // float inner[10] // }; // // Then we carefully include `.inner` (`WRAPPED_ARRAY_FIELD`) in // any expression that actually wants access to the array. crate::TypeInner::Array {
base,
size,
stride: _,
} => { let base_name = TypeContext {
handle: base,
gctx: module.to_ctx(),
names: &self.names,
access: crate::StorageAccess::empty(),
binding: None,
first_time: false,
};
// Define a struct to hold a named reference to a byte-unpacking function. struct UnpackingFunction {
name: String,
byte_count: u32,
dimension: u32,
} letmut unpacking_functions = FastHashMap::<VertexFormat, UnpackingFunction>::default();
// Check if we are attempting vertex pulling. If we are, generate some // names we'll need, and iterate the vertex buffer mappings to output // all the conversion functions we'll need to unpack the attribute data. // We can re-use these names for all entry points that need them, since // those entry points also use self.namer. letmut needs_vertex_id = false; let v_id = self.namer.call("v_id");
letmut needs_instance_id = false; let i_id = self.namer.call("i_id"); if pipeline_options.vertex_pulling_transform { for vbm in &pipeline_options.vertex_buffer_mappings { let buffer_id = vbm.id; let buffer_stride = vbm.stride;
let buffer_ty = self.namer.call(format!("vb_{buffer_id}_type").as_str()); let buffer_param = self.namer.call(format!("vb_{buffer_id}_in").as_str()); let buffer_elem = self.namer.call(format!("vb_{buffer_id}_elem").as_str());
letmut info = TranslationInfo {
entry_point_names: Vec::with_capacity(module.entry_points.len()),
}; for (ep_index, ep) in module.entry_points.iter().enumerate() { let fun = &ep.function; let fun_info = mod_info.get_entry_point(ep_index); letmut ep_error = None;
// For vertex_id and instance_id arguments, presume that we'll // use our generated names, but switch to the name of an // existing @builtin param, if we find one. letmut v_existing_id = None; letmut i_existing_id = None;
log::trace!( "entry point {:?}, index {:?}",
fun.name.as_deref().unwrap_or("(anonymous)"),
ep_index
);
// Should this entry point be modified to do vertex pulling? let do_vertex_pulling = can_vertex_pull
&& pipeline_options.vertex_pulling_transform
&& !pipeline_options.vertex_buffer_mappings.is_empty();
// Is any global variable used by this entry point dynamically sized? let needs_buffer_sizes = do_vertex_pulling
|| module
.global_variables
.iter()
.filter(|&(handle, _)| !fun_info[handle].is_empty())
.any(|(_, var)| needs_array_length(var.ty, &module.types));
// skip this entry point if any global bindings are missing, // or their types are incompatible. if !options.fake_missing_bindings { for (var_handle, var) in module.global_variables.iter() { if fun_info[var_handle].is_empty() { continue;
} match var.space { crate::AddressSpace::Uniform
| crate::AddressSpace::Storage { .. }
| crate::AddressSpace::Handle => { let br = match var.binding {
Some(ref br) => br,
None => { let var_name = var.name.clone().unwrap_or_default();
ep_error =
Some(super::EntryPointError::MissingBinding(var_name)); break;
}
}; let target = options.get_resource_binding_target(ep, br); let good = match target {
Some(target) => { // We intentionally don't dereference binding_arrays here, // so that binding arrays fall to the buffer location.
iflet Some(err) = ep_error {
info.entry_point_names.push(Err(err)); continue;
} let fun_name = &self.names[&NameKey::EntryPoint(ep_index as _)];
info.entry_point_names.push(Ok(fun_name.clone()));
writeln!(self.out)?;
// Since `Namer.reset` wasn't expecting struct members to be // suddenly injected into another namespace like this, // `self.names` doesn't keep them distinct from other variables. // Generate fresh names for these arguments, and remember the // mapping. letmut flattened_member_names = FastHashMap::default(); // Varyings' members get their own namespace letmut varyings_namer = proc::Namer::default();
// List all the Naga `EntryPoint`'s `Function`'s arguments, // flattening structs into their members. In Metal, we will pass // each of these values to the entry point as a separate argument— // except for the varyings, handled next. letmut flattened_arguments = Vec::new(); for (arg_index, arg) in fun.arguments.iter().enumerate() { match module.types[arg.ty].inner { crate::TypeInner::Struct { ref members, .. } => { for (member_index, member) in members.iter().enumerate() { let member_index = member_index as u32;
flattened_arguments.push((
NameKey::StructMember(arg.ty, member_index),
member.ty,
member.binding.as_ref(),
)); let name_key = NameKey::StructMember(arg.ty, member_index); let name = match member.binding {
Some(crate::Binding::Location { .. }) => { if do_vertex_pulling { self.namer.call(&self.names[&name_key])
} else {
varyings_namer.call(&self.names[&name_key])
}
}
_ => self.namer.call(&self.names[&name_key]),
};
flattened_member_names.insert(name_key, name);
}
}
_ => flattened_arguments.push((
NameKey::EntryPointArgument(ep_index as _, arg_index as u32),
arg.ty,
arg.binding.as_ref(),
)),
}
}
// Identify the varyings among the argument values, and maybe emit // a struct type named `<fun>Input` to hold them. If we are doing // vertex pulling, we instead update our attribute mapping to // note the types, names, and zero values of the attributes. let stage_in_name = self.namer.call(&format!("{fun_name}Input")); let varyings_member_name = self.namer.call("varyings"); letmut has_varyings = false; if !flattened_arguments.is_empty() { if !do_vertex_pulling {
writeln!(self.out, "struct {stage_in_name} {{")?;
} for &(ref name_key, ty, binding) in flattened_arguments.iter() { let (binding, location) = match binding {
Some(ref binding @ &crate::Binding::Location { location, .. }) => {
(binding, location)
}
_ => continue,
}; let name = match *name_key {
NameKey::StructMember(..) => &flattened_member_names[name_key],
_ => &self.names[name_key],
}; let ty_name = TypeContext {
handle: ty,
gctx: module.to_ctx(),
names: &self.names,
access: crate::StorageAccess::empty(),
binding: None,
first_time: false,
}; let resolved = options.resolve_local_binding(binding, in_mode)?; if do_vertex_pulling { // Update our attribute mapping.
am_resolved.insert(
location,
AttributeMappingResolved {
ty_name: ty_name.to_string(),
dimension: ty_name.vertex_input_dimension(),
ty_is_int: ty_name.scalar().is_some_and(scalar_is_int),
name: name.to_string(),
},
);
} else {
has_varyings = true;
write!(self.out, "{}{} {}", back::INDENT, ty_name, name)?;
resolved.try_fmt(&mutself.out)?;
writeln!(self.out, ";")?;
}
} if !do_vertex_pulling {
writeln!(self.out, "}};")?;
}
}
// Define a struct type named for the return value, if any, named // `<fun>Output`. let stage_out_name = self.namer.call(&format!("{fun_name}Output")); let result_member_name = self.namer.call("member"); let result_type_name = match fun.result {
Some(ref result) => { letmut result_members = Vec::new(); ifletcrate::TypeInner::Struct { ref members, .. } =
module.types[result.ty].inner
{ for (member_index, member) in members.iter().enumerate() {
result_members.push((
&self.names[&NameKey::StructMember(result.ty, member_index as u32)],
member.ty,
member.binding.as_ref(),
));
}
} else {
result_members.push((
&result_member_name,
result.ty,
result.binding.as_ref(),
));
}
if pipeline_options.allow_and_force_point_size
&& ep.stage == crate::ShaderStage::Vertex
&& !has_point_size
{ // inject the point size output last
writeln!( self.out, "{}float _point_size [[point_size]];",
back::INDENT
)?;
}
writeln!(self.out, "}};")?;
&stage_out_name
}
None => "void",
};
// If we're doing a vertex pulling transform, define the buffer // structure types. if do_vertex_pulling { for vbm in &vbm_resolved { let buffer_stride = vbm.stride; let buffer_ty = &vbm.ty_name;
// Define a structure of bytes of the appropriate size. // When we access the attributes, we'll be unpacking these // bytes at some offset.
writeln!( self.out, "struct {buffer_ty} {{ metal::uchar data[{buffer_stride}]; }};"
)?;
}
}
// Write the entry point function's name, and begin its argument list.
writeln!(self.out, "{em_str} {result_type_name} {fun_name}(")?; letmut is_first_argument = true;
// If we have produced a struct holding the `EntryPoint`'s // `Function`'s arguments' varyings, pass that struct first. if has_varyings {
writeln!( self.out, " {stage_in_name} {varyings_member_name} [[stage_in]]"
)?;
is_first_argument = false;
}
letmut local_invocation_id = None;
// Then pass the remaining arguments not included in the varyings // struct. for &(ref name_key, ty, binding) in flattened_arguments.iter() { let binding = match binding {
Some(binding @ &crate::Binding::BuiltIn { .. }) => binding,
_ => continue,
}; let name = match *name_key {
NameKey::StructMember(..) => &flattened_member_names[name_key],
_ => &self.names[name_key],
};
if binding == &crate::Binding::BuiltIn(crate::BuiltIn::LocalInvocationId) {
local_invocation_id = Some(name_key);
}
let resolved = options.resolve_local_binding(binding, in_mode)?; let separator = if is_first_argument {
is_first_argument = false; ' '
} else { ','
};
write!(self.out, "{separator} {ty_name} {name}")?;
resolved.try_fmt(&mutself.out)?;
writeln!(self.out)?;
}
let need_workgroup_variables_initialization = self.need_workgroup_variables_initialization(options, ep, module, fun_info);
if need_workgroup_variables_initialization && local_invocation_id.is_none() { let separator = if is_first_argument {
is_first_argument = false; ' '
} else { ','
};
writeln!( self.out, "{separator} {NAMESPACE}::uint3 __local_invocation_id [[thread_position_in_threadgroup]]"
)?;
}
// Those global variables used by this entry point and its callees // get passed as arguments. `Private` globals are an exception, they // don't outlive this invocation, so we declare them below as locals // within the entry point. for (handle, var) in module.global_variables.iter() { let usage = fun_info[handle]; if usage.is_empty() || var.space == crate::AddressSpace::Private { continue;
}
if options.lang_version < (1, 2) { match var.space { // This restriction is not documented in the MSL spec // but validation will fail if it is not upheld. // // We infer the required version from the "Function // Buffer Read-Writes" section of [what's new], where // the feature sets listed correspond with the ones // supporting MSL 1.2. // // [what's new]: https://developer.apple.com/library/archive/documentation/Miscellaneous/Conceptual/MetalProgrammingGuide/WhatsNewiniOS10tvOS10andOSX1012/WhatsNewiniOS10tvOS10andOSX1012.html crate::AddressSpace::Storage { access } if access.contains(crate::StorageAccess::STORE)
&& ep.stage == crate::ShaderStage::Fragment =>
{ return Err(Error::UnsupportedWriteableStorageBuffer)
} crate::AddressSpace::Handle => { match module.types[var.ty].inner { crate::TypeInner::Image {
class: crate::ImageClass::Storage { access, .. },
..
} => { // This restriction is not documented in the MSL spec // but validation will fail if it is not upheld. // // We infer the required version from the "Function // Texture Read-Writes" section of [what's new], where // the feature sets listed correspond with the ones // supporting MSL 1.2. // // [what's new]: https://developer.apple.com/library/archive/documentation/Miscellaneous/Conceptual/MetalProgrammingGuide/WhatsNewiniOS10tvOS10andOSX1012/WhatsNewiniOS10tvOS10andOSX1012.html if access.contains(crate::StorageAccess::STORE)
&& (ep.stage == crate::ShaderStage::Vertex
|| ep.stage == crate::ShaderStage::Fragment)
{ return Err(Error::UnsupportedWriteableStorageTexture(
ep.stage,
));
}
// the resolves have already been checked for `!fake_missing_bindings` case let resolved = match var.space { crate::AddressSpace::PushConstant => options.resolve_push_constants(ep).ok(), crate::AddressSpace::WorkGroup => None,
_ => options
.resolve_resource_binding(ep, var.binding.as_ref().unwrap())
.ok(),
}; iflet Some(ref resolved) = resolved { // Inline samplers are be defined in the EP body if resolved.as_inline_sampler(options).is_some() { continue;
}
}
// Iterate vbm_resolved, output one argument for every vertex buffer, // using the names we generated earlier. for vbm in &vbm_resolved { let id = &vbm.id; let ty_name = &vbm.ty_name; let param_name = &vbm.param_name;
writeln!( self.out, ", const device {ty_name}* {param_name} [[buffer({id})]]"
)?;
}
}
// If this entry uses any variable-length arrays, their sizes are // passed as a final struct-typed argument. if needs_buffer_sizes { // this is checked earlier let resolved = options.resolve_sizes_buffer(ep).unwrap(); let separator = if is_first_argument { ' ' } else { ',' };
write!( self.out, "{separator} constant _mslBufferSizes& _buffer_sizes",
)?;
resolved.try_fmt(&mutself.out)?;
writeln!(self.out)?;
}
// end of the entry point argument list
writeln!(self.out, ") {{")?;
// Starting the function body. if do_vertex_pulling { // Provide zero values for all the attributes, which we will overwrite with // real data from the vertex attribute buffers, if the indices are in-bounds. for vbm in &vbm_resolved { for attribute in vbm.attributes { let location = attribute.shader_location; let am_option = am_resolved.get(&location); if am_option.is_none() { // This bound attribute isn't used in this entry point, so // don't bother zero-initializing it. continue;
} let am = am_option.unwrap(); let attribute_ty_name = &am.ty_name; let attribute_name = &am.name;
// Output a bounds check block that will set real values for the // attributes, if the bounds are satisfied.
write!(self.out, "{}if (", back::Level(1))?;
let idx = &vbm.id; let stride = &vbm.stride; let index_name = if vbm.indexed_by_vertex { iflet Some(ref name) = v_existing_id {
name
} else {
&v_id
}
} elseiflet Some(ref name) = i_existing_id {
name
} else {
&i_id
};
write!( self.out, "{index_name} < (_buffer_sizes.buffer_size{idx} / {stride})"
)?;
writeln!(self.out, ") {{")?;
// Pull the bytes out of the vertex buffer. let ty_name = &vbm.ty_name; let elem_name = &vbm.elem_name; let param_name = &vbm.param_name;
// Now set real values for each of the attributes, by unpacking the data // from the buffer elements. for attribute in vbm.attributes { let location = attribute.shader_location; let am_option = am_resolved.get(&location); if am_option.is_none() { // This bound attribute isn't used in this entry point, so // don't bother extracting the data. Too bad we emitted the // unpacking function earlier -- it might not get used. continue;
} let am = am_option.unwrap(); let attribute_name = &am.name; let attribute_ty_name = &am.ty_name;
let offset = attribute.offset; let func = unpacking_functions
.get(&attribute.format)
.expect("Should have generated this unpacking function earlier."); let func_name = &func.name;
// Check dimensionality of the attribute compared to the unpacking // function. If attribute dimension is < unpack dimension, then // we need to explicitly cast down the result. Otherwise, if attribute // dimension > unpack dimension, we have to pad out the unpack value // from a vec4(0, 0, 0, 1) of matching scalar type.
let needs_truncate_or_padding = am.dimension != func.dimension; if needs_truncate_or_padding {
write!(self.out, "{attribute_ty_name}(")?;
}
write!(self.out, "{func_name}({elem_name}.data[{offset}]",)?; for i in (offset + 1)..(offset + func.byte_count) {
write!(self.out, ", {elem_name}.data[{i}]")?;
}
write!(self.out, ")")?;
if needs_truncate_or_padding { let zero_value = if am.ty_is_int { "0" } else { "0.0" }; let one_value = if am.ty_is_int { "1" } else { "1.0" }; for i in func.dimension..am.dimension {
write!( self.out, ", {}", if i == 3 { one_value } else { zero_value }
)?;
}
write!(self.out, ")")?;
}
writeln!(self.out, ";")?;
}
// End the bounds check / attribute setting block.
writeln!(self.out, "{}}}", back::Level(1))?;
}
}
if need_workgroup_variables_initialization { self.write_workgroup_variables_initialization(
module,
mod_info,
fun_info,
local_invocation_id,
)?;
}
// Metal doesn't support private mutable variables outside of functions, // so we put them here, just like the locals. for (handle, var) in module.global_variables.iter() { let usage = fun_info[handle]; if usage.is_empty() { continue;
} if var.space == crate::AddressSpace::Private { let tyvar = TypedGlobalVariable {
module,
names: &self.names,
handle,
usage,
binding: None,
reference: false,
};
write!(self.out, "{}", back::INDENT)?;
tyvar.try_fmt(&mutself.out)?; match var.init {
Some(value) => {
write!(self.out, " = ")?; self.put_const_expression(value, module, mod_info)?;
writeln!(self.out, ";")?;
}
None => {
writeln!(self.out, " = {{}};")?;
}
};
} elseiflet Some(ref binding) = var.binding { // write an inline sampler let resolved = options.resolve_resource_binding(ep, binding).unwrap(); iflet Some(sampler) = resolved.as_inline_sampler(options) { let name = &self.names[&NameKey::GlobalVariable(handle)];
writeln!( self.out, "{}constexpr {}::sampler {}(",
back::INDENT,
NAMESPACE,
name
)?; self.put_inline_sampler_properties(back::Level(2), sampler)?;
writeln!(self.out, "{});", back::INDENT)?;
}
}
}
// Now take the arguments that we gathered into structs, and the // structs that we flattened into arguments, and emit local // variables with initializers that put everything back the way the // body code expects. // // If we had to generate fresh names for struct members passed as // arguments, be sure to use those names when rebuilding the struct. // // "Each day, I change some zeros to ones, and some ones to zeros. // The rest, I leave alone." for (arg_index, arg) in fun.arguments.iter().enumerate() { let arg_name =
&self.names[&NameKey::EntryPointArgument(ep_index as _, arg_index as u32)]; match module.types[arg.ty].inner { crate::TypeInner::Struct { ref members, .. } => { let struct_name = &self.names[&NameKey::Type(arg.ty)];
write!( self.out, "{}const {} {} = {{ ",
back::INDENT,
struct_name,
arg_name
)?; for (member_index, member) in members.iter().enumerate() { let key = NameKey::StructMember(arg.ty, member_index as u32); let name = &flattened_member_names[&key]; if member_index != 0 {
write!(self.out, ", ")?;
} // insert padding initialization, if needed ifself
.struct_member_pads
.contains(&(arg.ty, member_index as u32))
{
write!(self.out, "{{}}, ")?;
} iflet Some(crate::Binding::Location { .. }) = member.binding { if has_varyings {
write!(self.out, "{varyings_member_name}.")?;
}
}
write!(self.out, "{name}")?;
}
writeln!(self.out, " }};")?;
}
_ => { iflet Some(crate::Binding::Location { .. }) = arg.binding { if has_varyings {
writeln!( self.out, "{}const auto {} = {}.{};",
back::INDENT,
arg_name,
varyings_member_name,
arg_name
)?;
}
}
}
}
}
let guarded_indices =
index::find_checked_indexes(module, fun, fun_info, options.bounds_check_policies);
// Finally, declare all the local variables that we need //TODO: we can postpone this till the relevant expressions are emitted for (local_handle, local) in fun.local_variables.iter() { let name = &self.names[&NameKey::EntryPointLocal(ep_index as _, local_handle)]; let ty_name = TypeContext {
handle: local.ty,
gctx: module.to_ctx(),
names: &self.names,
access: crate::StorageAccess::empty(),
binding: None,
first_time: false,
};
write!(self.out, "{}{} {}", back::INDENT, ty_name, name)?; match local.init {
Some(value) => {
write!(self.out, " = ")?; self.put_expression(value, &context.expression, true)?;
}
None => {
write!(self.out, " = {{}}")?;
}
};
writeln!(self.out, ";")?;
}
fn write_barrier(&mutself, flags: crate::Barrier, level: back::Level) -> BackendResult { // Note: OR-ring bitflags requires `__HAVE_MEMFLAG_OPERATORS__`, // so we try to avoid it here. if flags.is_empty() {
writeln!( self.out, "{level}{NAMESPACE}::threadgroup_barrier({NAMESPACE}::mem_flags::mem_none);",
)?;
} if flags.contains(crate::Barrier::STORAGE) {
writeln!( self.out, "{level}{NAMESPACE}::threadgroup_barrier({NAMESPACE}::mem_flags::mem_device);",
)?;
} if flags.contains(crate::Barrier::WORK_GROUP) {
writeln!( self.out, "{level}{NAMESPACE}::threadgroup_barrier({NAMESPACE}::mem_flags::mem_threadgroup);",
)?;
} if flags.contains(crate::Barrier::SUB_GROUP) {
writeln!( self.out, "{level}{NAMESPACE}::simdgroup_barrier({NAMESPACE}::mem_flags::mem_threadgroup);",
)?;
}
Ok(())
}
}
/// Initializing workgroup variables is more tricky for Metal because we have to deal /// with atomics at the type-level (which don't have a copy constructor). mod workgroup_mem_init { usecrate::EntryPoint;
¤ 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.0.240Bemerkung:
(vorverarbeitet am 2026-06-19)
¤
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.