D100 - Three wavefront models, kept as a differential oracle
decided ยท 2026-08-20
Fidelity selects how the wavefront is modelled, as a
field of Strategy::Predicated rather than a parameter
beside it - structured reconstruction implies the per-lane model and
nothing else, so making it a field means an invalid pairing cannot be
written down.
| Level | Model | State |
|---|---|---|
Lane |
one invocation per lane; lanes never interact | built |
Wavefront |
one invocation simulates all lanes; the mask is a value | built |
Subgroup |
one invocation per lane; mask via subgroup ballot | stub |
They are not a fallback ladder. Building all three was proposed as insurance and justified as something better: run one shader at two levels and disagreement means the faster one has a bug, localised to that shader and bisectable to an instruction - with no reference hardware, no console and no title. The same trick the decoder's differential test plays, one layer up, and it already passes across four programs.
Why the slowest first. Wavefront is the
simplest of the three - no subgroup operations, no ballot, no size to
negotiate - and it is the oracle the other two are judged against.
Building the hard one first would mean building it with nothing to check
it against.
Three things inside it worth keeping.
The mask lives in the scalar register file at the
two indices the architecture reserves, rather than in a variable of its
own. The guest addresses exec_lo and exec_hi
as ordinary scalar registers and manipulates them as 32-bit halves, so
modelling them that way means guest code touching the mask needs no
special translation - and it sidesteps 64-bit integers entirely, on a
value the guest never treats as one.
Masked writes are a select, not a branch. Read the old value, compute the new one, keep whichever the mask calls for. Same result, no merge blocks, and it keeps the level whose purpose is obvious correctness free of the one thing that makes SPIR-V generation hard.
The observation layout is identical across levels - lane zero's vector registers, then the scalars. That is a requirement rather than a coincidence: two levels can only be diffed if they report in the same shape.
Configurable, but not freely. Auto
picks the cheapest valid level and the level actually used is reported,
because "slow" and "quietly dropped a level" look identical otherwise. A
caller may pin a level; if it is not built, that is an error naming what
it would have done. Never a silent substitution - and the reason is
sharper than for the strategy stub, since these levels differ in
correctness rather than only speed, so a substitution renders
something wrong rather than something slow.
Auto resolves to Lane today, and the
reasoning is worth recording because it is safety by
accident: a shader needing more than the lane model needs a
cross-lane instruction or mask arithmetic, and every such instruction is
already refused as untranslatable. The lane model therefore cannot
currently be chosen for a shader it would get wrong - not by analysis,
but because the translator stops first. That stops holding the moment
those instructions are implemented, and resolve carries a
note saying so.