Skip to content

Commit 1462c31

Browse files
nv-slang-bot[bot]szihs
authored andcommitted
WGSL: emit runtime-indexable static const arrays as var<private> (#11628)
## Motivation When targeting WGSL, a module-scope `static const` array is emitted as a WGSL `const`. But a WGSL `const` is a compile-time value (closer to C++ `constexpr` than to `const`): the WGSL spec permits a *value* of array type to be indexed only by a const-expression. So indexing such a global by a runtime value is rejected by the WGSL validator. Motivating case (from the issue): ```hlsl static const float2 positions[] = { float2(-1.0f, -1.0f), float2(1.0f, -1.0f), float2(1.0f, 1.0f), float2(-1.0f, 1.0f) }; [shader("vertex")] VertexOutput vertexMain(uint vertexID : SV_VertexID) { VertexOutput o; o.position = float4(positions[vertexID], 0.0f, 1.0f); // runtime index return o; } ``` emits `const positions_0 : array<vec2<f32>, i32(4)> = ...;` then `positions_0[vertexID_0]`, which naga/tint reject: *"The expression may only be indexed by a constant."* ## Proposed solution Emit module-scope `static const` globals of **array** type as WGSL `var<private>` (a private module-scope variable), keeping their inline const-expression initializer, instead of `const`. A `var<private>` holds the same value but is addressable, so it is runtime-indexable. After the fix the array emits `var<private> positions_0 : array<vec2<f32>, i32(4)> = array<vec2<f32>, i32(4)>( ... );` and `positions_0[vertexID_0]` is valid. Only arrays are converted. A WGSL matrix *value* is itself runtime-indexable, so a `const` matrix needs no conversion; scalars/vectors stay `const` (a vector value is also dynamically indexable), and function-local constants are unaffected. Nested aggregates need one more step. A nested `static const` (e.g. `int grid[2][3]`) lowers to nested `MakeArray` insts, each a module-scope array constant emitted as a *separate named* declaration. Converting all of them to `var<private>` would make the outer initializer reference inner `var<private>`s — illegal WGSL (a `var<private>` initializer must be a const-expression and cannot read another `var`). The fix folds the inner constituents inline so the outermost runtime-indexed array becomes a single `var<private>` with a self-contained initializer: ```wgsl var<private> grid_0 : array<array<i32, i32(3)>, i32(2)> = array<array<i32, i32(3)>, i32(2)>( array<i32, i32(3)>( i32(1), i32(2), i32(3) ), array<i32, i32(3)>( i32(4), i32(5), i32(6) ) ); ``` Alternatives ruled out: a `static`-only (non-`const`) global lowers its initializer into an init function, producing the broken `const _S3 = ...;` statements seen in the issue thread; keeping the initializer inline avoids that. A precise "is this global ever dynamically indexed" use-analysis is unnecessary — constant-indexed reads are already constant-folded before emit (see Process report). ## Change summary | File | Change | | --- | --- | | `source/slang/slang-emit-wgsl.cpp` | `emitVarKeywordImpl`: a module-scope **array** constant emits `var` + `<private>` instead of `const`. `shouldFoldInstIntoUseSites`: for WGSL, fold a module-scope `MakeArray`/`MakeStruct`/`MakeArrayFromElement` inline when it is only a nested constituent of another aggregate, so the outermost array's initializer stays self-contained. | | `tests/wgsl/static-const-array-indexing.slang` | Runtime-indexed array → `var<private>`; `+ wgsl-spirv-asm` (Tint) validation directive. | | `tests/wgsl/static-const-array-nested.slang` | New: nested 2D-array regression — one inline `var<private>`, no separate inner-array decls; `+ wgsl-spirv-asm` directive. | | `tests/wgsl/static-const-matrix.slang` | New: a runtime-indexed `static const` matrix stays an inline `const` value (not `var<private>`); `+ wgsl-spirv-asm` directive validates it through Tint on CI. | | `tests/wgsl/static-const-array-const-index.slang` | A constant-indexed element folds away and leaves no `const`/`var` reading another variable; tightened anchors + negative guard. | | `docs/user-guide/a2-03-wgsl-target-specific.md` | Document the module-scope `const` (scalar/vector/matrix) vs `var<private>` (array) split. | ## Concepts and vocabulary - **WGSL `const` vs `var<private>`** — a WGSL `const` is a compile-time value; a `const` value of array type may only be indexed by a const-expression. A `var<private>` is an addressable module-scope variable, indexable by a runtime value, whose initializer must still be a const-expression. - **`GlobalConstant` / `MakeArray`** — a `static const` array lowers to an `IRGlobalConstant` wrapping an `IRMakeArray`; nested arrays nest `MakeArray`s. - **`replaceGlobalConstants` / peephole fold** — `replaceGlobalConstants` inlines a global constant's value into its uses; the peephole pass folds `GetElement(MakeArray, constIndex)` to the element, leaving a runtime index intact. - **`shouldFoldInstIntoUseSites`** — decides whether an inst is emitted inline at its use or as its own declaration. WGSL can fold `MakeArray`/`MakeStruct` (constructor expressions, valid in any expression context) where the base class cannot (C/HLSL initializer lists). ## Process report **`emitVarKeywordImpl` (`source/slang/slang-emit-wgsl.cpp`).** The declared inst for a module-scope `static const` array is an ordinary module-scope instruction; `emitInstResultDecl` (`slang-emit-c-like.cpp`) emits `<keyword> <name> : <type> = <initializer>`, with the keyword from `emitVarKeywordImpl`. Previously the `default:` arm emitted `const` for any module-scope constant. The predicate `emitModuleScopeArrayConstAsPrivateVar` reuses the existing `isStaticConst` helper and is true when the inst is a module-scope constant (`isStaticConst(varDecl)`), is not a `GlobalParam`, and has type `kIROp_ArrayType`; it then emits `var` and `<private>` (joining the existing `kIROp_GlobalVar` storage-space branch, after the array-of-ConstantBuffer/structured-buffer branches, so resource arrays are unaffected). The `!= kIROp_GlobalParam` guard is load-bearing: the predicate is also read in the address-space chain (which runs for all ops), where a module-scope `GlobalParam` array — e.g. a descriptor array like `Texture2D t[8]`, whose element type isn't ConstantBuffer/structured so it falls through to the final branch — would otherwise wrongly get `<private>` instead of its handle address space. (`isStaticConst` is true for any module child, including `GlobalParam`/`GlobalVar`, so the guard is required; a local `Var` is not module-scope and is already excluded.) **Why a type-based conversion is safe (no const-expression chain breaks).** A constant index into a static-const array is constant-folded before emit, so no `const` is left whose initializer reads the converted `var`: `replaceGlobalConstants` inlines `GlobalConstant` values into uses before `simplifyIR`, then the peephole `kIROp_GetElement` case folds `GetElement(MakeArray, IRIntLit)` to the element while a non-constant (runtime) index hits `if (!index) break;` and is left intact. `tests/wgsl/static-const-array-const-index.slang` guards this. **Nested aggregates — `shouldFoldInstIntoUseSites`.** The base class never-folds `MakeArray`/`MakeStruct`/`MakeArrayFromElement` because in C/HLSL they lower to initializer lists, invalid in a general expression context. WGSL instead emits them as constructor expressions (`type(args...)`, `tryEmitInstExprImpl`), valid anywhere. So a nested `static const` otherwise emits each inner `MakeArray` as a separate named module-scope decl, and converting them all to `var<private>` yields an outer initializer that references inner `var<private>`s — illegal. The override folds a module-scope constituent inline when every use of it is an operand of another `MakeArray`/`MakeStruct`/`MakeArrayFromElement` (checked by a small inlined loop), so the outermost aggregate — used directly, e.g. by a runtime `GetElement` — stays a declaration eligible for `var<private>` while its constituents inline. Function-local aggregates are left to the base policy; resource/global-parameter arrays keep their existing declaration paths. `tests/wgsl/static-const-array-nested.slang` covers a runtime-indexed 2D array. *Known limitation:* an aggregate that is both a nested constituent and used directly — a named `static const` array shared as an element of another `static const` array *and* independently runtime-indexed — is not folded, so it stays a separate `var<private>` declaration that the enclosing converted array's initializer references by name (invalid WGSL). This is not constructible from typical anonymous nested literals, and is not a regression for accepted WGSL output (such a constant was a rejected runtime-indexed `const` before this change too, though the failure now takes a different form); fully handling it would require inlining a duplicate copy of the constituent into the enclosing initializer. **Input-shape check (right layer?).** Yes. The IR for a `static const` array is correct (`IRGlobalConstant` wrapping `IRMakeArray`); the defects are WGSL keyword selection and WGSL fold policy, both target-specific. The fix belongs in the WGSL emitter. **Verification.** Built a debug `slangc`: the flat and nested cases emit single self-contained `var<private>` declarations indexed by name; a `static const` matrix is emitted as an inline matrix value indexed in place (`mat2x2<…>(…)[i]`), not promoted to `var<private>`. The full `tests/wgsl/` suite passes (49/49). Note: `slangc` emitting WGSL is not the same as a validator accepting it — Tint is not available in the build environment, so the `wgsl-spirv-asm` directives (which round-trip the emitted WGSL through Tint) are ignored locally and run on CI. The matrix test's directive is what would confirm or refute that a runtime-indexed `const` matrix is valid WGSL. **Known limitation — arrays nested inside a `static const` struct.** A `static const` struct with an array field, runtime-indexed via `g.arr[i]`, is *not* addressed: the struct is not `ArrayType`, so it stays `const`, and it lowers through a constructor function (`g.arr[i]` → `Foo_init(...).arr[i]`) whose result is an array value runtime-indexed — the same #6747 rejection. This is a realistic lookup-table shape but a distinct (struct-valued) problem from the top-level arrays #6747 reports, and is left as a follow-up. Fixes #6747. --------- Co-authored-by: nv-slang-bot[bot] <274397474+nv-slang-bot[bot]@users.noreply.github.com> Co-authored-by: Harsh Aggarwal <haaggarwal@nvidia.com>
1 parent 1286697 commit 1462c31

6 files changed

Lines changed: 208 additions & 3 deletions

File tree

docs/user-guide/a2-03-wgsl-target-specific.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,9 @@ WGSL requires explicit address space qualifiers. Slang automatically assigns app
158158
| Group Shared | `workgroup` |
159159
| Parameter Blocks | `uniform` |
160160

161+
Module-scope `static const` globals are emitted as a WGSL `const` (a compile-time value) when their type is a scalar, vector, or matrix.
162+
A `static const` global of **array** type is instead emitted as a `var<private>` (with the same initializer), because a WGSL `const` is a compile-time value that may only be indexed by a const-expression, whereas a `var<private>` is addressable and so can be indexed by a runtime value (for example `positions[vertexID]`).
163+
161164

162165
Matrix type translation
163166
-----------------------

source/slang/slang-emit-wgsl.cpp

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,20 @@ static bool isStaticConst(IRInst* inst)
808808

809809
void WGSLSourceEmitter::emitVarKeywordImpl(IRType* type, IRInst* varDecl)
810810
{
811+
// A module-scope `static const` array is emitted as `var<private>`, not `const`: a WGSL
812+
// `const` value of array type may only be indexed by a const-expression, so a constant array
813+
// indexed by a runtime value (e.g. `positions[SV_VertexID]`) is rejected by the validator. A
814+
// `var<private>` takes the same const-expression initializer but, being addressable, is
815+
// runtime-indexable. Only arrays are converted -- a scalar/vector/matrix *value* is already
816+
// runtime-indexable in WGSL. The type-based conversion is safe because constant-indexed reads
817+
// fold away before emit (see the PR description). The `!= kIROp_GlobalParam` guard is
818+
// load-bearing: this predicate is reused in the address-space chain below, where a
819+
// `GlobalParam` array (e.g. a descriptor array) must keep its own address space, not
820+
// `<private>`.
821+
const bool emitModuleScopeArrayConstAsPrivateVar = isStaticConst(varDecl) &&
822+
varDecl->getOp() != kIROp_GlobalParam &&
823+
type->getOp() == kIROp_ArrayType;
824+
811825
switch (varDecl->getOp())
812826
{
813827
case kIROp_GlobalParam:
@@ -824,7 +838,12 @@ void WGSLSourceEmitter::emitVarKeywordImpl(IRType* type, IRInst* varDecl)
824838
}
825839
break;
826840
default:
827-
if (isStaticConst(varDecl))
841+
// When this emits `var`, the matching `<private>` address space is emitted by the
842+
// storage-space chain below (the two must stay in lockstep — a module-scope `var`
843+
// without an address space is invalid WGSL).
844+
if (emitModuleScopeArrayConstAsPrivateVar)
845+
m_writer->emit("var");
846+
else if (isStaticConst(varDecl))
828847
m_writer->emit("const");
829848
else
830849
m_writer->emit("var");
@@ -872,9 +891,11 @@ void WGSLSourceEmitter::emitVarKeywordImpl(IRType* type, IRInst* varDecl)
872891
m_writer->emit("storage, read");
873892
m_writer->emit(">");
874893
}
875-
else if (varDecl->getOp() == kIROp_GlobalVar)
894+
else if (varDecl->getOp() == kIROp_GlobalVar || emitModuleScopeArrayConstAsPrivateVar)
876895
{
877-
// Global ("module-scope") non-handle variables need to specify storage space
896+
// Global ("module-scope") non-handle variables need to specify storage space. This also
897+
// covers an array constant converted to `var<private>` above (which is not a GlobalVar
898+
// but is likewise emitted as a module-scope private variable).
878899

879900
// https://www.w3.org/TR/WGSL/#var-decls
880901
// "
@@ -1386,6 +1407,34 @@ void WGSLSourceEmitter::emitCallArg(IRInst* inst)
13861407

13871408
bool WGSLSourceEmitter::shouldFoldInstIntoUseSites(IRInst* inst)
13881409
{
1410+
// WGSL emits MakeArray/MakeStruct as constructor expressions, valid in any expression context
1411+
// (the base class never folds them because C/HLSL initializer lists are not). Fold a
1412+
// module-scope aggregate constant inline when it is used only as a constituent of another
1413+
// aggregate, so a nested `static const` (e.g. `int g[2][3]`) does not emit its inner arrays as
1414+
// separate named decls that the outermost array's `var<private>` initializer would illegally
1415+
// reference; the outermost (used directly, e.g. runtime-indexed) one stays a declaration.
1416+
switch (inst->getOp())
1417+
{
1418+
case kIROp_MakeArray:
1419+
case kIROp_MakeStruct:
1420+
case kIROp_MakeArrayFromElement:
1421+
if (inst->getParent() && inst->getParent()->getOp() == kIROp_ModuleInst)
1422+
{
1423+
bool onlyConstituent = inst->firstUse != nullptr;
1424+
for (auto use = inst->firstUse; onlyConstituent && use; use = use->nextUse)
1425+
{
1426+
auto userOp = use->getUser()->getOp();
1427+
onlyConstituent = userOp == kIROp_MakeArray || userOp == kIROp_MakeStruct ||
1428+
userOp == kIROp_MakeArrayFromElement;
1429+
}
1430+
if (onlyConstituent)
1431+
return true;
1432+
}
1433+
break;
1434+
default:
1435+
break;
1436+
}
1437+
13891438
bool result = CLikeSourceEmitter::shouldFoldInstIntoUseSites(inst);
13901439
if (result)
13911440
{
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//TEST:SIMPLE(filecheck=WGSL): -stage vertex -entry vertexMain -target wgsl
2+
3+
// Regression guard for issue #6747.
4+
//
5+
// Companion to static-const-array-indexing.slang. That test verifies a runtime-indexed
6+
// `static const` array is emitted as `var<private>`. This test verifies the opposite case
7+
// is not broken by that change: an element of a `static const` array, read at a *constant*
8+
// index, must constant-fold (the peephole pass folds `GetElement(MakeArray, constIndex)` to
9+
// the element) and therefore must NOT leave behind a module-scope `const` whose initializer
10+
// indexes a runtime variable, which would be illegal WGSL (a `const` initializer must be a
11+
// const-expression and cannot read a `var`). The array, being only constant-indexed, folds
12+
// away entirely.
13+
14+
static const float weights[3] = {0.25f, 0.5f, 0.25f};
15+
16+
// `weights[1]` is a constant index, so it folds to the literal `0.5` and `weights` is
17+
// dead-code-eliminated; it must not survive as a variable that `midWeight` indexes.
18+
static const float midWeight = weights[1];
19+
20+
struct VertexOutput
21+
{
22+
float4 position : SV_POSITION;
23+
};
24+
25+
VertexOutput vertexMain(uint vertexID : SV_VertexID)
26+
{
27+
VertexOutput output;
28+
output.position = float4(float(vertexID) * midWeight, 0.0f, 0.0f, 1.0f);
29+
return output;
30+
}
31+
32+
// The constant-indexed array must fold away; the identifier `weights` (a unique substring here)
33+
// must not survive. If it had instead become a `var<private>` that `midWeight` indexes, both
34+
// `weights` and a `var<private>` declaration would appear -- so the bare `weights` negative also
35+
// covers the spurious-promotion case (a promoted decl would carry the array's name).
36+
// WGSL-NOT: weights
37+
// ...and the dependent constant must survive as the folded element value (weights[1] == 0.5f).
38+
// WGSL: 0.5f
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//TEST:SIMPLE(filecheck=WGSL): -stage vertex -entry vertexMain -target wgsl
2+
// This second directive hands the emitted WGSL to Tint (via wgsl-spirv-asm) so we validate that
3+
// the output actually compiles, not just that the keyword flipped. It is exercised on CI where
4+
// Tint is available, and is ignored where the downstream compiler is absent.
5+
//TEST:SIMPLE(filecheck=SPV): -stage vertex -entry vertexMain -target wgsl-spirv-asm
6+
7+
// Regression test for issue #6747.
8+
//
9+
// A `static const` array that is indexed by a runtime value must be emitted as a WGSL
10+
// `var<private>`, not as a WGSL `const`. A WGSL `const` is a compile-time value (closer to
11+
// C++ `constexpr`); the WGSL spec only allows a value of array type to be indexed by a
12+
// const-expression, so `const positions[vertexID]` (a runtime index) is rejected by the
13+
// WGSL validator with "The expression may only be indexed by a constant". `var<private>`
14+
// keeps the same inline initializer but is addressable and therefore runtime-indexable.
15+
16+
static const float2 positions[] = {
17+
float2(-1.0f, -1.0f),
18+
float2( 1.0f, -1.0f),
19+
float2( 1.0f, 1.0f),
20+
float2(-1.0f, 1.0f)
21+
};
22+
23+
// The constant array must not be emitted as a WGSL `const`...
24+
// WGSL-NOT: const positions
25+
// ...but as a private module-scope variable that keeps its inline initializer.
26+
// WGSL: var<private> positions{{.*}} : array<vec2<f32>, i32(4)> = array<vec2<f32>, i32(4)>(
27+
28+
struct VertexOutput
29+
{
30+
float4 position : SV_POSITION;
31+
};
32+
33+
VertexOutput vertexMain(uint vertexID : SV_VertexID)
34+
{
35+
VertexOutput output;
36+
// Indexing the global with a runtime value is exactly what a WGSL `const` forbids.
37+
// WGSL: positions{{.*}}[vertexID
38+
// SPV: OpEntryPoint
39+
output.position = float4(positions[vertexID], 0.0f, 1.0f);
40+
return output;
41+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//TEST:SIMPLE(filecheck=WGSL): -stage vertex -entry vertexMain -target wgsl
2+
// Validate the emitted WGSL through Tint on CI (ignored where the downstream compiler is absent).
3+
//TEST:SIMPLE(filecheck=SPV): -stage vertex -entry vertexMain -target wgsl-spirv-asm
4+
5+
// Regression test for issue #6747 — nested aggregates.
6+
//
7+
// A *nested* `static const` array (here a 2D array) lowers to nested `MakeArray` insts, each of
8+
// which is a module-scope array constant. Naively emitting each as its own module-scope decl and
9+
// converting all of them to `var<private>` would make the outer `var<private>` initializer
10+
// reference the inner `var<private>`s — which is illegal WGSL (a `var<private>` initializer must
11+
// be a const-expression and cannot read another `var`). The fix folds the inner constituents
12+
// inline (shouldFoldInstIntoUseSites) so only the outermost runtime-indexed array becomes a
13+
// `var<private>`, with a self-contained inline initializer.
14+
15+
static const int grid[2][3] = {
16+
{1, 2, 3},
17+
{4, 5, 6}
18+
};
19+
20+
// Exactly one module-scope private variable, whose initializer inlines the inner arrays as
21+
// constructor expressions (NOT as references to separately-declared variables).
22+
// WGSL: var<private> grid{{.*}} : array<array<i32, i32(3)>, i32(2)> = array<array<i32, i32(3)>, i32(2)>( array<i32, i32(3)>( i32(1), i32(2), i32(3) ), array<i32, i32(3)>( i32(4), i32(5), i32(6) ) );
23+
24+
struct VertexOutput
25+
{
26+
float4 position : SV_POSITION;
27+
};
28+
29+
VertexOutput vertexMain(uint vertexID : SV_VertexID)
30+
{
31+
VertexOutput output;
32+
// Runtime index of the outer array — the construct that requires `grid` to be `var<private>`.
33+
// WGSL: grid{{.*}}[vertexID
34+
// SPV: OpEntryPoint
35+
output.position = float4(float(grid[vertexID][1]), 0.0f, 0.0f, 1.0f);
36+
return output;
37+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//TEST:SIMPLE(filecheck=WGSL): -stage vertex -entry vertexMain -target wgsl
2+
// Validate the emitted WGSL through Tint on CI (ignored where the downstream compiler is absent).
3+
//TEST:SIMPLE(filecheck=SPV): -stage vertex -entry vertexMain -target wgsl-spirv-asm
4+
5+
// Regression test for issue #6747 — a `static const` MATRIX is NOT converted to `var<private>`.
6+
//
7+
// Only `static const` array globals are converted: a WGSL `const` value of array type may only
8+
// be indexed by a const-expression. A matrix value, like a vector value, IS runtime-indexable in
9+
// WGSL, so a `static const` matrix stays a `const` and is emitted as an inline value — it must
10+
// NOT be promoted to `var<private>`. The `wgsl-spirv-asm` directive confirms Tint accepts the
11+
// runtime row-index on CI; if it ever rejected it, matrices would be an instance of the same bug
12+
// to convert too.
13+
14+
static const float2x2 M = {
15+
{1.0f, 2.0f},
16+
{3.0f, 4.0f}
17+
};
18+
19+
// The matrix must NOT be promoted to a module-scope private variable. Placed before the positive
20+
// check so the negative covers the whole globals region (where a promoted decl would appear).
21+
// WGSL-NOT: var<private>{{.*}}mat2x2
22+
23+
struct VertexOutput
24+
{
25+
float4 position : SV_POSITION;
26+
};
27+
28+
VertexOutput vertexMain(uint i : SV_VertexID)
29+
{
30+
VertexOutput output;
31+
// Runtime row-index of the matrix: it emits as an inline `mat2x2<...>(...)[i]`, not as a
32+
// `var<private>` referenced by name.
33+
// WGSL: mat2x2{{.*}}[i
34+
// SPV: OpEntryPoint
35+
output.position = float4(M[i % 2], 0.0f, 0.0f);
36+
return output;
37+
}

0 commit comments

Comments
 (0)