Skip to content

Make Scope and Semantics normal arguments rather than const generics #414

Description

@nazar-pc

The usage of atomics is quite awkward:

let position_in_bucket = unsafe {
    atomic_i_add::<_, { Scope::QueueFamily as u32 }, { Semantics::NONE.bits() }>(
        bucket_count,
        1,
    )
};

Not only one needs to know what each u32 is, it is not even consistent with the need to cast in one case and calling .bits() in another.

Similarly, barriers are awkward too:

control_barrier::<
    { Scope::Workgroup as u32 },
    { Scope::Workgroup as u32 },
    { Semantics::NONE.bits() },
>();

It is even worse when memory semantics is involved (though many of such cases have helper methods):

control_barrier::<
    { Scope::Workgroup as u32 },
    { Scope::Workgroup as u32 },
    {
        Semantics::WORKGROUP_MEMORY.bits() | Semantics::ACQUIRE_RELEASE.bits()
    },
>();

Subjectively, three u32s that need to be composed in a very particular way are quite ugly.

Consider changing API to use normal types for these arguments, so we can all enjoy autocomplete in IDE, better formatting and implement ability to combine semantics variants, like this:

let position_in_bucket = unsafe {
    atomic_i_add(
        bucket_count,
        1,
        Scope::QueueFamily,
        Semantics::None,
    )
};

control_barrier(
    Scope::Workgroup,
    Scope::Workgroup,
    Semantics::WorkgroupMemory | Semantics::AcquireRelease,
);

The result of Semantics::WorkgroupMemory | Semantics::AcquireRelease can be Semantics::Combined(u32) for example, with Debug implementation overridden to render human-readable set of enabled flags.

Alternatively, a builder pattern could be used for Semantics with const fn to have guarantees of sane composition in case some invariants fundamentally do not make sense.

At some point non-integer const generics will become available, but even then I see no benefit from const generics for this particular use case.

Activity

  1. Firestar99 commented on Oct 10, 2025

    @Firestar99
    Member

    Scope and Memory Semantics args are impossible

    Scope and Memory Semantics must be a constant and cannot be runtime dynamic args, see spec:

    All used for Scope and Memory Semantics must be of an OpConstant.

    https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.html#_validation_rules_for_shader_capabilities

    Which forces us to use const generics and forward them to the asm! block as constants:

    pub fn control_barrier<
    const EXECUTION: u32, // Scope
    const MEMORY: u32, // Scope
    const SEMANTICS: u32, // Semantics
    >() {
    unsafe {
    asm! {
    "%u32 = OpTypeInt 32 0",
    "%execution = OpConstant %u32 {execution}",
    "%memory = OpConstant %u32 {memory}",
    "%semantics = OpConstant %u32 {semantics}",
    "OpControlBarrier %execution %memory %semantics",
    execution = const EXECUTION,
    memory = const MEMORY,
    semantics = const SEMANTICS,
    }
    }
    }

    I also tried replacing the OpConstant with ordinary OpLoads, but that fails validation:

    asm! {
        "%u32 = OpTypeInt 32 0",
        "%execution = OpLoad %u32 {execution}",
        "%memory = OpLoad %u32 {memory}",
        "%semantics = OpLoad %u32 {semantics}",
        "OpControlBarrier %execution %memory %semantics",
        execution = in(reg) &EXECUTION,
        memory = in(reg) &MEMORY,
        semantics = in(reg) &SEMANTICS,
    }
    error: error:0:0 - Scope ids must be OpConstant when Shader capability is present
             OpControlBarrier %21 %22 %23
    

    So we're unfortunately tied to constants by the spec.

  2. nazar-pc commented on Oct 10, 2025

    @nazar-pc
    ContributorAuthor

    This is in interesting constraint. I believe it'll be possible to use const generics with arbitrary types in the future, but we're not there yet (or maybe codegen backend can opt-in into this experimental feature already given the narrow scope?).

    I'm wondering if const {} can be exploited here somehow and/or #[inline(always)] (I'm just brainstorming here, but the intuition is that at some abstraction level arguments will become inlined constants). Or maybe special-case these functions for compiler somehow such that it is a normal argument, but compiler knows the arguments must be known at compile time. There are a few tricks of this nature in Rust standard library that are not directly implementable in normal Rust code.

    Or maybe replace these functions with macros, such that it is possible to use normal types and have compile-time guarantees of type safety.

  3. Firestar99 commented on Oct 10, 2025

    @Firestar99
    Member

    Alternatives

    Const generics with enums / ADT types

    This would solve this issues at once, but it looks like rustc is still quite a ways off...

    Wrapping functions

    You've already mentioned them, they are probably the easiest way to wrap the const complexity

    pub fn workgroup_memory_barrier_with_group_sync() {
    control_barrier::<
    { crate::memory::Scope::Workgroup as u32 },
    { crate::memory::Scope::Workgroup as u32 },
    {
    crate::memory::Semantics::WORKGROUP_MEMORY.bits()
    | crate::memory::Semantics::ACQUIRE_RELEASE.bits()
    },
    >();
    }

    Image! macro

    Images are similarly full of const generics that are effectively enums:

    pub struct Image<
    SampledType: SampleType<FORMAT, COMPONENTS>,
    const DIM: u32, // Dimensionality,
    const DEPTH: u32, // ImageDepth,
    const ARRAYED: u32, // Arrayed,
    const MULTISAMPLED: u32, // Multisampled,
    const SAMPLED: u32, // Sampled,
    const FORMAT: u32, // ImageFormat,
    const COMPONENTS: u32, // NumberOfComponents,
    > {

    In fact there are so many const generics, that we can't reasonably assume users will correctly specify them. So the Image! macro exists to correctly specify them, and we have some common image types typedef'ed:

    /// A 1d image used with a sampler.
    pub type Image1d = crate::Image!(1D, type=f32, sampled, __crate_root=crate);
    /// A 2d image used with a sampler. This is pretty typical and probably what you want.
    pub type Image2d = crate::Image!(2D, type=f32, sampled, __crate_root=crate);
    /// A 3d image used with a sampler.
    pub type Image3d = crate::Image!(3D, type=f32, sampled, __crate_root=crate);

    Type wrapping

    This is something I've used in my own engine with great success, and could be a model for rust-gpu as well?

    • Declare an enum with all the variants:
    #[repr(u8)]
    #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, FromPrimitive, ToPrimitive)]
    pub enum BufferAccess {
    	ShaderRead,,
    	ShaderReadWrite,
    	[....]
    }
    • Create a trait with an associated constant of said enum:
    pub unsafe trait BufferAccessType {
    	const BUFFER_ACCESS: BufferAccess;
    }
    • Define a type that implements said trait:
    pub struct ShaderRead;
    unsafe impl BufferAccessType for ShaderRead {
    	const BUFFER_ACCESS: BufferAccess = BufferAccess::ShaderRead;
    }
    • preferably implement this with some macro_rules!
    • a function can then be declared like this, which asm! should hopefully also accept as a const:
    fn my_func<A: BufferAccessType>() {
    	let buffer_access = A::BUFFER_ACCESS;
    }
    
    my_func::<ShaderRead>()

    @eddyb @LegNeato @schell @nnethercote Design thoughts on where we should head to in the future?

  4. nazar-pc commented on Aug 4, 2026

    @nazar-pc
    ContributorAuthor

    Implemented adt_const_params alternative in #635

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions