GPU Resources and Descriptor Set Layout
Shaders read and write GPU memory through three kinds of bindings: uniforms (small, read-only constants), storage buffers (arbitrary-size, read or read-write arrays), and push constants (tiny, fast, write-once-per-dispatch). Wyn surfaces uniforms and storage buffers as entry-point parameters decorated with binding attributes; push constants are not user-declared (the compiler synthesizes them for non-array compute entry parameters that need to broadcast a scalar to every invocation).
Note: these attributes go on entry-point parameters, not on top-level
defs.#[uniform(...)] def x: Tand#[storage(...)] def x: Tare compile-time errors (“only valid on entry-point parameters”).
Grammar
binding_attr ::= "#[" "uniform" "(" set_param? "binding" "=" decimal ")" "]"
| "#[" "storage" "(" set_param? "binding" "=" decimal
("," "layout" "=" layout_kind)?
("," "access" "=" access_kind)?
")" "]"
set_param ::= "set" "=" decimal ","
layout_kind ::= "std430" | "std140"
access_kind ::= "read" | "write" | "readwrite"
A binding_attr prefixes an entry-point parameter:
binding_attr identifier ":" type.
Examples:
#[fragment]
entry main(
#[uniform(set=1, binding=0)] iResolution: vec3f32,
#[uniform(binding=1)] iTime: f32, -- set defaults to 1
#[builtin(frag_coord)] fragCoord: vec4f32
) #[location(0)] vec4f32 = ...
#[compute]
entry sim(
#[storage(set=2, binding=0, access=read)] particles: []vec4f32
) ... = ...
Descriptor Set Layout
Every binding lives in a numbered descriptor set; each set is a
separate bind group at runtime. Wyn reserves the bottom of the set
namespace for the compiler and gives the rest to the user:
- Set 0 is reserved for compiler-allocated storage. Compute
entry-input and entry-output buffers (one per field of a tuple-of-
arrays input after SoA splitting), multi-stage SOAC intermediates
(e.g. partials buffers between phases of a parallelized
reduce), and graphical-entry-lift prepass results all live on set 0. The compiler unconditionally allocates(set=0, binding=N)starting atbinding=0; it does not consult user state. - Set 1 and higher are for user-declared
#[uniform]and#[storage]. Whensetis omitted from one of those attributes, it defaults to 1. #[uniform(set=0, ...)]and#[storage(set=0, ...)]are compile-time errors. The error names the offending decl’s source span.
This split exists because the compiler’s allocator and the user’s decls are written without knowledge of each other. Splitting the set namespace removes the only failure mode where a host-runtime would silently bind two different resources to the same descriptor slot.
The convention is enforced statically — there is no runtime fallback or “best-effort” behavior. The diagnostic guides users to renumber their decls; once the user keeps off set 0, no collision is possible.
Compiler-Allocated Bindings
Set 0 holds the bindings derived from each entry point’s parameters and return value. A tuple-of-arrays input is split into one binding per element. For example,
#[compute]
entry price_options(
#[uniform(set=1, binding=0)] now: f32,
#[uniform(set=1, binding=1)] rfr: f32,
opts: [](f32, f32, i32, f32, f32)
) []f32 = ...
allocates five storage bindings on set 0 for the SoA-split input
(opts_0 through opts_4) and one for the output (<entry>_output).
The two user-declared uniforms remain on set 1 as written.