Add specialization constants via #[spirv(spec_constant(id = 123))] x: u32 entry-point inputs.

This commit is contained in:
Eduard-Mihai Burtescu 2023-07-19 18:00:54 +03:00 committed by Eduard-Mihai Burtescu
parent 55edc4e6b4
commit af2a9ee445
8 changed files with 352 additions and 86 deletions

View File

@ -30,6 +30,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added ⭐
- [PR#1081](https://github.com/EmbarkStudios/rust-gpu/pull/1081) added the ability
to access SPIR-V specialization constants (`OpSpecConstant`) via entry-point
inputs declared as `#[spirv(spec_constant(id = ..., default = ...))] x: u32`
(see also [the `#[spirv(spec_constant)]` attribute documentation](docs/src/attributes.md#specialization-constants))
- [PR#1036](https://github.com/EmbarkStudios/rust-gpu/pull/1036) added a `--force-spirv-passthru` flag to `example-runner-wgpu`, to bypass Naga (`wgpu`'s shader translator),
used it to test `debugPrintf` for `wgpu`, and updated `ShaderPanicStrategy::DebugPrintfThenExit` docs to reflect what "enabling `debugPrintf`" looks like for `wgpu`
<sub><sup>(e.g. `VK_LOADER_LAYERS_ENABLE=VK_LAYER_KHRONOS_validation VK_LAYER_ENABLES=VK_VALIDATION_FEATURE_ENABLE_DEBUG_PRINTF_EXT DEBUG_PRINTF_TO_STDOUT=1`)</sup></sub>

View File

@ -68,6 +68,12 @@ pub enum IntrinsicType {
Matrix,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct SpecConstant {
pub id: u32,
pub default: Option<u32>,
}
// NOTE(eddyb) when adding new `#[spirv(...)]` attributes, the tests found inside
// `tests/ui/spirv-attr` should be updated (and new ones added if necessary).
#[derive(Debug, Clone)]
@ -87,6 +93,7 @@ pub enum SpirvAttribute {
Flat,
Invariant,
InputAttachmentIndex(u32),
SpecConstant(SpecConstant),
// `fn`/closure attributes:
BufferLoadIntrinsic,
@ -121,6 +128,7 @@ pub struct AggregatedSpirvAttributes {
pub flat: Option<Spanned<()>>,
pub invariant: Option<Spanned<()>>,
pub input_attachment_index: Option<Spanned<u32>>,
pub spec_constant: Option<Spanned<SpecConstant>>,
// `fn`/closure attributes:
pub buffer_load_intrinsic: Option<Spanned<()>>,
@ -211,6 +219,12 @@ impl AggregatedSpirvAttributes {
span,
"#[spirv(attachment_index)]",
),
SpecConstant(value) => try_insert(
&mut self.spec_constant,
value,
span,
"#[spirv(spec_constant)]",
),
BufferLoadIntrinsic => try_insert(
&mut self.buffer_load_intrinsic,
(),
@ -300,7 +314,8 @@ impl CheckSpirvAttrVisitor<'_> {
| SpirvAttribute::Binding(_)
| SpirvAttribute::Flat
| SpirvAttribute::Invariant
| SpirvAttribute::InputAttachmentIndex(_) => match target {
| SpirvAttribute::InputAttachmentIndex(_)
| SpirvAttribute::SpecConstant(_) => match target {
Target::Param => {
let parent_hir_id = self.tcx.hir().parent_id(hir_id);
let parent_is_entry_point =

View File

@ -1,6 +1,6 @@
use super::CodegenCx;
use crate::abi::ConvSpirvType;
use crate::attr::{AggregatedSpirvAttributes, Entry, Spanned};
use crate::attr::{AggregatedSpirvAttributes, Entry, Spanned, SpecConstant};
use crate::builder::Builder;
use crate::builder_spirv::{SpirvValue, SpirvValueExt};
use crate::spirv_type::SpirvType;
@ -40,7 +40,10 @@ struct EntryParamDeducedFromRustRefOrValue<'tcx> {
/// The SPIR-V storage class to declare the shader interface variable in,
/// either deduced from the type (e.g. opaque handles use `UniformConstant`),
/// provided via `#[spirv(...)]` attributes, or an `Input`/`Output` default.
storage_class: StorageClass,
//
// HACK(eddyb) this can be `Err(SpecConstant)` to indicate this is actually
// an `OpSpecConstant` being exposed as if it were an `Input`
storage_class: Result<StorageClass, SpecConstant>,
/// Whether this entry-point parameter doesn't allow writes to the underlying
/// shader interface variable (i.e. is by-value, or `&T` where `T: Freeze`).
@ -387,6 +390,30 @@ impl<'tcx> CodegenCx<'tcx> {
}
}
// HACK(eddyb) only handle `attrs.spec_constant` after everything above
// would've assumed it was actually an implicitly-`Input`.
let mut storage_class = Ok(storage_class);
if let Some(spec_constant) = attrs.spec_constant {
if ref_or_value_layout.ty != self.tcx.types.u32 {
self.tcx.sess.span_err(
hir_param.ty_span,
format!(
"unsupported `#[spirv(spec_constant)]` type `{}` (expected `{}`)",
ref_or_value_layout.ty, self.tcx.types.u32
),
);
} else if let Some(storage_class) = attrs.storage_class {
self.tcx.sess.span_err(
storage_class.span,
"`#[spirv(spec_constant)]` cannot have a storage class",
);
} else {
assert_eq!(storage_class, Ok(StorageClass::Input));
assert!(!is_ref);
storage_class = Err(spec_constant.value);
}
}
EntryParamDeducedFromRustRefOrValue {
value_layout,
storage_class,
@ -407,9 +434,6 @@ impl<'tcx> CodegenCx<'tcx> {
) {
let attrs = AggregatedSpirvAttributes::parse(self, self.tcx.hir().attrs(hir_param.hir_id));
// Pre-allocate the module-scoped `OpVariable`'s *Result* ID.
let var = self.emit_global().id();
let EntryParamDeducedFromRustRefOrValue {
value_layout,
storage_class,
@ -417,14 +441,35 @@ impl<'tcx> CodegenCx<'tcx> {
} = self.entry_param_deduce_from_rust_ref_or_value(entry_arg_abi.layout, hir_param, &attrs);
let value_spirv_type = value_layout.spirv_type(hir_param.ty_span, self);
let (var_id, spec_const_id) = match storage_class {
// Pre-allocate the module-scoped `OpVariable` *Result* ID.
Ok(_) => (
Ok(self.emit_global().id()),
Err("entry-point interface variable is not a `#[spirv(spec_constant)]`"),
),
Err(SpecConstant { id, default }) => {
let mut emit = self.emit_global();
let spec_const_id = emit.spec_constant_u32(value_spirv_type, default.unwrap_or(0));
emit.decorate(
spec_const_id,
Decoration::SpecId,
[Operand::LiteralInt32(id)],
);
(
Err("`#[spirv(spec_constant)]` is not an entry-point interface variable"),
Ok(spec_const_id),
)
}
};
// Emit decorations deduced from the reference/value Rust type.
if read_only {
// NOTE(eddyb) it appears only `StorageBuffer`s simultaneously:
// - allow `NonWritable` decorations on shader interface variables
// - default to writable (i.e. the decoration actually has an effect)
if storage_class == StorageClass::StorageBuffer {
if storage_class == Ok(StorageClass::StorageBuffer) {
self.emit_global()
.decorate(var, Decoration::NonWritable, []);
.decorate(var_id.unwrap(), Decoration::NonWritable, []);
}
}
@ -454,14 +499,20 @@ impl<'tcx> CodegenCx<'tcx> {
}
let var_ptr_spirv_type;
let (value_ptr, value_len) = match storage_class {
StorageClass::PushConstant | StorageClass::Uniform | StorageClass::StorageBuffer => {
Ok(
StorageClass::PushConstant | StorageClass::Uniform | StorageClass::StorageBuffer,
) => {
let var_spirv_type = SpirvType::InterfaceBlock {
inner_type: value_spirv_type,
}
.def(hir_param.span, self);
var_ptr_spirv_type = self.type_ptr_to(var_spirv_type);
let value_ptr = bx.struct_gep(var_spirv_type, var.with_type(var_ptr_spirv_type), 0);
let value_ptr = bx.struct_gep(
var_spirv_type,
var_id.unwrap().with_type(var_ptr_spirv_type),
0,
);
let value_len = if is_unsized_with_len {
match self.lookup_type(value_spirv_type) {
@ -478,7 +529,7 @@ impl<'tcx> CodegenCx<'tcx> {
let len_spirv_type = self.type_isize();
let len = bx
.emit()
.array_length(len_spirv_type, None, var, 0)
.array_length(len_spirv_type, None, var_id.unwrap(), 0)
.unwrap();
Some(len.with_type(len_spirv_type))
@ -493,9 +544,9 @@ impl<'tcx> CodegenCx<'tcx> {
None
};
(value_ptr, value_len)
(Ok(value_ptr), value_len)
}
StorageClass::UniformConstant => {
Ok(StorageClass::UniformConstant) => {
var_ptr_spirv_type = self.type_ptr_to(value_spirv_type);
match self.lookup_type(value_spirv_type) {
@ -524,7 +575,7 @@ impl<'tcx> CodegenCx<'tcx> {
None
};
(var.with_type(var_ptr_spirv_type), value_len)
(Ok(var_id.unwrap().with_type(var_ptr_spirv_type)), value_len)
}
_ => {
var_ptr_spirv_type = self.type_ptr_to(value_spirv_type);
@ -533,12 +584,19 @@ impl<'tcx> CodegenCx<'tcx> {
self.tcx.sess.span_fatal(
hir_param.ty_span,
format!(
"unsized types are not supported for storage class {storage_class:?}"
"unsized types are not supported for {}",
match storage_class {
Ok(storage_class) => format!("storage class {storage_class:?}"),
Err(SpecConstant { .. }) => "`#[spirv(spec_constant)]`".into(),
},
),
);
}
(var.with_type(var_ptr_spirv_type), None)
(
var_id.map(|var_id| var_id.with_type(var_ptr_spirv_type)),
None,
)
}
};
@ -546,21 +604,26 @@ impl<'tcx> CodegenCx<'tcx> {
// starting from the `value_ptr` pointing to a `value_spirv_type`
// (e.g. `Input` doesn't use indirection, so we have to load from it).
if let ty::Ref(..) = entry_arg_abi.layout.ty.kind() {
call_args.push(value_ptr);
call_args.push(value_ptr.unwrap());
match entry_arg_abi.mode {
PassMode::Direct(_) => assert_eq!(value_len, None),
PassMode::Pair(..) => call_args.push(value_len.unwrap()),
_ => unreachable!(),
}
} else {
assert_eq!(storage_class, StorageClass::Input);
assert_matches!(entry_arg_abi.mode, PassMode::Direct(_));
let value = bx.load(
let value = match storage_class {
Ok(_) => {
assert_eq!(storage_class, Ok(StorageClass::Input));
bx.load(
entry_arg_abi.layout.spirv_type(hir_param.ty_span, bx),
value_ptr,
value_ptr.unwrap(),
entry_arg_abi.layout.align.abi,
);
)
}
Err(SpecConstant { .. }) => spec_const_id.unwrap().with_type(value_spirv_type),
};
call_args.push(value);
assert_eq!(value_len, None);
}
@ -573,48 +636,76 @@ impl<'tcx> CodegenCx<'tcx> {
// name (e.g. "foo" for `foo: Vec3`). While `OpName` is *not* suppposed
// to be semantic, OpenGL and some tooling rely on it for reflection.
if let hir::PatKind::Binding(_, _, ident, _) = &hir_param.pat.kind {
self.emit_global().name(var, ident.to_string());
self.emit_global()
.name(var_id.or(spec_const_id).unwrap(), ident.to_string());
}
// Emit `OpDecorate`s based on attributes.
let mut decoration_supersedes_location = false;
if let Some(builtin) = attrs.builtin.map(|attr| attr.value) {
if let Some(builtin) = attrs.builtin {
if let Err(SpecConstant { .. }) = storage_class {
self.tcx.sess.span_fatal(
builtin.span,
format!(
"`#[spirv(spec_constant)]` cannot be `{:?}` builtin",
builtin.value
),
);
}
self.emit_global().decorate(
var,
var_id.unwrap(),
Decoration::BuiltIn,
std::iter::once(Operand::BuiltIn(builtin)),
std::iter::once(Operand::BuiltIn(builtin.value)),
);
decoration_supersedes_location = true;
}
if let Some(index) = attrs.descriptor_set.map(|attr| attr.value) {
if let Some(descriptor_set) = attrs.descriptor_set {
if let Err(SpecConstant { .. }) = storage_class {
self.tcx.sess.span_fatal(
descriptor_set.span,
"`#[spirv(descriptor_set = ...)]` cannot apply to `#[spirv(spec_constant)]`",
);
}
self.emit_global().decorate(
var,
var_id.unwrap(),
Decoration::DescriptorSet,
std::iter::once(Operand::LiteralInt32(index)),
std::iter::once(Operand::LiteralInt32(descriptor_set.value)),
);
decoration_supersedes_location = true;
}
if let Some(index) = attrs.binding.map(|attr| attr.value) {
if let Some(binding) = attrs.binding {
if let Err(SpecConstant { .. }) = storage_class {
self.tcx.sess.span_fatal(
binding.span,
"`#[spirv(binding = ...)]` cannot apply to `#[spirv(spec_constant)]`",
);
}
self.emit_global().decorate(
var,
var_id.unwrap(),
Decoration::Binding,
std::iter::once(Operand::LiteralInt32(index)),
std::iter::once(Operand::LiteralInt32(binding.value)),
);
decoration_supersedes_location = true;
}
if attrs.flat.is_some() {
if let Some(flat) = attrs.flat {
if let Err(SpecConstant { .. }) = storage_class {
self.tcx.sess.span_fatal(
flat.span,
"`#[spirv(flat)]` cannot apply to `#[spirv(spec_constant)]`",
);
}
self.emit_global()
.decorate(var, Decoration::Flat, std::iter::empty());
.decorate(var_id.unwrap(), Decoration::Flat, std::iter::empty());
}
if let Some(invariant) = attrs.invariant {
self.emit_global()
.decorate(var, Decoration::Invariant, std::iter::empty());
if storage_class != StorageClass::Output {
self.tcx.sess.span_err(
if storage_class != Ok(StorageClass::Output) {
self.tcx.sess.span_fatal(
invariant.span,
"#[spirv(invariant)] is only valid on Output variables",
"`#[spirv(invariant)]` is only valid on Output variables",
);
}
self.emit_global()
.decorate(var_id.unwrap(), Decoration::Invariant, std::iter::empty());
}
let is_subpass_input = match self.lookup_type(value_spirv_type) {
@ -635,7 +726,7 @@ impl<'tcx> CodegenCx<'tcx> {
if let Some(attachment_index) = attrs.input_attachment_index {
if is_subpass_input && self.builder.has_capability(Capability::InputAttachment) {
self.emit_global().decorate(
var,
var_id.unwrap(),
Decoration::InputAttachmentIndex,
std::iter::once(Operand::LiteralInt32(attachment_index.value)),
);
@ -657,6 +748,7 @@ impl<'tcx> CodegenCx<'tcx> {
);
}
if let Ok(storage_class) = storage_class {
self.check_for_bad_types(
execution_model,
hir_param.ty_span,
@ -665,6 +757,7 @@ impl<'tcx> CodegenCx<'tcx> {
attrs.builtin.is_some(),
attrs.flat,
);
}
// Assign locations from left to right, incrementing each storage class
// individually.
@ -673,21 +766,25 @@ impl<'tcx> CodegenCx<'tcx> {
let has_location = !decoration_supersedes_location
&& matches!(
storage_class,
StorageClass::Input | StorageClass::Output | StorageClass::UniformConstant
Ok(StorageClass::Input | StorageClass::Output | StorageClass::UniformConstant)
);
if has_location {
let location = decoration_locations
.entry(storage_class)
.entry(storage_class.unwrap())
.or_insert_with(|| 0);
self.emit_global().decorate(
var,
var_id.unwrap(),
Decoration::Location,
std::iter::once(Operand::LiteralInt32(*location)),
);
*location += 1;
}
// Emit the `OpVariable` with its *Result* ID set to `var`.
match storage_class {
Ok(storage_class) => {
let var = var_id.unwrap();
// Emit the `OpVariable` with its *Result* ID set to `var_id`.
self.emit_global()
.variable(var_ptr_spirv_type, Some(var), storage_class, None);
@ -698,11 +795,16 @@ impl<'tcx> CodegenCx<'tcx> {
op_entry_point_interface_operands.push(var);
} else {
// SPIR-V <= v1.3 only includes Input and Output in the interface.
if storage_class == StorageClass::Input || storage_class == StorageClass::Output {
if storage_class == StorageClass::Input || storage_class == StorageClass::Output
{
op_entry_point_interface_operands.push(var);
}
}
}
// Emitted earlier.
Err(SpecConstant { .. }) => {}
}
}
// Booleans are only allowed in some storage classes. Error if they're in others.
// Integers and `f64`s must be decorated with `#[spirv(flat)]`.

View File

@ -1,4 +1,4 @@
use crate::attr::{Entry, ExecutionModeExtra, IntrinsicType, SpirvAttribute};
use crate::attr::{Entry, ExecutionModeExtra, IntrinsicType, SpecConstant, SpirvAttribute};
use crate::builder::libm_intrinsics;
use rspirv::spirv::{BuiltIn, ExecutionMode, ExecutionModel, StorageClass};
use rustc_ast::ast::{AttrKind, Attribute, LitIntType, LitKind, MetaItemLit, NestedMetaItem};
@ -30,6 +30,10 @@ pub struct Symbols {
binding: Symbol,
input_attachment_index: Symbol,
spec_constant: Symbol,
id: Symbol,
default: Symbol,
attributes: FxHashMap<Symbol, SpirvAttribute>,
execution_modes: FxHashMap<Symbol, (ExecutionMode, ExecutionModeExtraDim)>,
pub libm_intrinsics: FxHashMap<Symbol, libm_intrinsics::LibmIntrinsic>,
@ -392,6 +396,10 @@ impl Symbols {
binding: Symbol::intern("binding"),
input_attachment_index: Symbol::intern("input_attachment_index"),
spec_constant: Symbol::intern("spec_constant"),
id: Symbol::intern("id"),
default: Symbol::intern("default"),
attributes,
execution_modes,
libm_intrinsics,
@ -466,6 +474,8 @@ pub(crate) fn parse_attrs_for_checking<'a>(
SpirvAttribute::Binding(parse_attr_int_value(arg)?)
} else if arg.has_name(sym.input_attachment_index) {
SpirvAttribute::InputAttachmentIndex(parse_attr_int_value(arg)?)
} else if arg.has_name(sym.spec_constant) {
SpirvAttribute::SpecConstant(parse_spec_constant_attr(sym, arg)?)
} else {
let name = match arg.ident() {
Some(i) => i,
@ -494,6 +504,38 @@ pub(crate) fn parse_attrs_for_checking<'a>(
})
}
fn parse_spec_constant_attr(
sym: &Symbols,
arg: &NestedMetaItem,
) -> Result<SpecConstant, ParseAttrError> {
let mut id = None;
let mut default = None;
if let Some(attrs) = arg.meta_item_list() {
for attr in attrs {
if attr.has_name(sym.id) {
if id.is_none() {
id = Some(parse_attr_int_value(attr)?);
} else {
return Err((attr.span(), "`id` may only be specified once".into()));
}
} else if attr.has_name(sym.default) {
if default.is_none() {
default = Some(parse_attr_int_value(attr)?);
} else {
return Err((attr.span(), "`default` may only be specified once".into()));
}
} else {
return Err((attr.span(), "expected `id = ...` or `default = ...`".into()));
}
}
}
Ok(SpecConstant {
id: id.ok_or_else(|| (arg.span(), "expected `spec_constant(id = ...)`".into()))?,
default,
})
}
fn parse_attr_int_value(arg: &NestedMetaItem) -> Result<u32, ParseAttrError> {
let arg = match arg.meta_item() {
Some(arg) => arg,

View File

@ -110,3 +110,47 @@ fn main(#[spirv(workgroup)] var: &mut [Vec4; 4]) { }
## Generic storage classes
The SPIR-V storage class of types is inferred for function signatures. The inference logic can be guided by attributes on the interface specification in the entry points. This also means it needs to be clear from the documentation if an API requires a certain storage class (e.g `workgroup`) for a variable. Storage class attributes are only permitted on entry points.
## Specialization constants
Entry point inputs also allow access to [SPIR-V "specialization constants"](https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.html#SpecializationSection),
which are each associated with an user-specified numeric "ID" (SPIR-V `SpecId`),
used to override them later ("specializing" the shader):
* in Vulkan: [during pipeline creation, via `VkSpecializationInfo`](https://registry.khronos.org/vulkan/specs/1.3-extensions/html/chap10.html#pipelines-specialization-constants)
* in WebGPU: [during pipeline creation, via `GPUProgrammableStage`<i>`#constants`</i>](https://www.w3.org/TR/webgpu/#gpuprogrammablestage)
* note: WebGPU calls them ["pipeline-overridable constants"](https://gpuweb.github.io/gpuweb/wgsl/#pipeline-overridable)
* in OpenCL: [via `clSetProgramSpecializationConstant()` calls, before `clBuildProgram()`](https://registry.khronos.org/OpenCL/sdk/3.0/docs/man/html/clSetProgramSpecializationConstant.html)
If a "specialization constant" is not overriden, it falls back to its *default*
value, which is either user-specified (via `default = ...`), or `0` otherwise.
While only "specialization constants" of type `u32` are currently supported, it's
always possible to *manually* create values of other types, from one or more `u32`s.
Example:
```rust
#[spirv(vertex)]
fn main(
// Default is implicitly `0`, if not specified.
#[spirv(spec_constant(id = 1))] no_default: u32,
// IDs don't need to be sequential or obey any order.
#[spirv(spec_constant(id = 9000, default = 123))] default_123: u32,
// Assembling a larger value out of multiple `u32` is also possible.
#[spirv(spec_constant(id = 100))] x_u64_lo: u32,
#[spirv(spec_constant(id = 101))] x_u64_hi: u32,
) {
let x_u64 = ((x_u64_hi as u64) << 32) | (x_u64_lo as u64);
}
```
<sub>**Note**: despite the name "constants", they are *runtime values* from the
perspective of compiled Rust code (or at most similar to "link-time constants"),
and as such have no connection to *Rust constants*, especially not Rust type-level
constants and `const` generics - while specializing some e.g. `fn foo<const N: u32>`
by `N` long after it was compiled to SPIR-V, or using "specialization constants"
as Rust array lengths, Rust would sadly require *dependent types* to type-check
such code (as it would for e.g. expressing C `T[n]` types with runtime `n`),
and the main benefit over truly dynamic inputs is a (potential) performance boost.<sub>

View File

@ -0,0 +1,29 @@
#![crate_name = "spec_constant_attr"]
// Tests the various forms of `#[spirv(spec_constant)]`.
// build-pass
// compile-flags: -C llvm-args=--disassemble-globals
// normalize-stderr-test "OpCapability VulkanMemoryModel\n" -> ""
// normalize-stderr-test "OpSource .*\n" -> ""
// normalize-stderr-test "OpExtension .SPV_KHR_vulkan_memory_model.\n" -> ""
// normalize-stderr-test "OpMemoryModel Logical Vulkan" -> "OpMemoryModel Logical Simple"
// FIXME(eddyb) this should use revisions to track both the `vulkan1.2` output
// and the pre-`vulkan1.2` output, but per-revisions `{only,ignore}-*` directives
// are not supported in `compiletest-rs`.
// ignore-vulkan1.2
use spirv_std::spirv;
#[spirv(fragment)]
pub fn main(
#[spirv(spec_constant(id = 1))] no_default: u32,
#[spirv(spec_constant(id = 2, default = 0))] default_0: u32,
#[spirv(spec_constant(id = 123, default = 123))] default_123: u32,
#[spirv(spec_constant(id = 0xffff_ffff, default = 0xffff_ffff))] max_id_and_default: u32,
out: &mut u32,
) {
*out = no_default + default_0 + default_123 + max_id_and_default;
}

View File

@ -0,0 +1,30 @@
OpCapability Shader
OpCapability Float64
OpCapability Int64
OpCapability Int16
OpCapability Int8
OpCapability ShaderClockKHR
OpExtension "SPV_KHR_shader_clock"
OpMemoryModel Logical Simple
OpEntryPoint Fragment %1 "main" %2
OpExecutionMode %1 OriginUpperLeft
%3 = OpString "$OPSTRING_FILENAME/spec_constant-attr.rs"
OpName %4 "no_default"
OpName %5 "default_0"
OpName %6 "default_123"
OpName %7 "max_id_and_default"
OpName %2 "out"
OpDecorate %4 SpecId 1
OpDecorate %5 SpecId 2
OpDecorate %6 SpecId 123
OpDecorate %7 SpecId 4294967295
OpDecorate %2 Location 0
%8 = OpTypeInt 32 0
%9 = OpTypePointer Output %8
%10 = OpTypeVoid
%11 = OpTypeFunction %10
%4 = OpSpecConstant %8 0
%5 = OpSpecConstant %8 0
%6 = OpSpecConstant %8 123
%7 = OpSpecConstant %8 4294967295
%2 = OpVariable %9 Output

View File

@ -1,4 +1,4 @@
error: #[spirv(invariant)] is only valid on Output variables
error: `#[spirv(invariant)]` is only valid on Output variables
--> $DIR/invariant-invalid.rs:7:21
|
7 | pub fn main(#[spirv(invariant)] input: f32) {}