@aardworx/wombat.rendering 0.20.1 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/resources/renderPipeline.js +30 -6
- package/dist/resources/renderPipeline.js.map +1 -1
- package/dist/runtime/heapDecoder.d.ts +4 -0
- package/dist/runtime/heapDecoder.d.ts.map +1 -1
- package/dist/runtime/heapDecoder.js +44 -3
- package/dist/runtime/heapDecoder.js.map +1 -1
- package/dist/runtime/heapEffect.d.ts +1 -1
- package/dist/runtime/heapEffect.d.ts.map +1 -1
- package/dist/runtime/heapEffect.js +9 -2
- package/dist/runtime/heapEffect.js.map +1 -1
- package/dist/runtime/heapEffectIR.d.ts.map +1 -1
- package/dist/runtime/heapEffectIR.js +177 -13
- package/dist/runtime/heapEffectIR.js.map +1 -1
- package/package.json +2 -2
- package/src/resources/renderPipeline.ts +30 -6
- package/src/runtime/heapDecoder.ts +45 -3
- package/src/runtime/heapEffect.ts +9 -2
- package/src/runtime/heapEffectIR.ts +185 -13
|
@@ -56,6 +56,11 @@ import {
|
|
|
56
56
|
headersU32,
|
|
57
57
|
} from "./heapIrBuilders.js";
|
|
58
58
|
|
|
59
|
+
/** Carrier name for the per-draw header index handed to the FS (flat u32). */
|
|
60
|
+
export const HEAP_FS_DRAWIDX = "_h_drawIdx";
|
|
61
|
+
/** Carrier name for the instance index (only when per-instance uniforms exist). */
|
|
62
|
+
export const HEAP_FS_INSTID = "_h_instId";
|
|
63
|
+
|
|
59
64
|
// Build an Expr that returns the u32 value of a specific inline u32
|
|
60
65
|
// slot in the drawHeader: `headersU32[drawIdx * stride + offset_u32]`.
|
|
61
66
|
function readHeaderU32(drawIdx: Expr, byteOffset: number, strideU32: number): Expr {
|
|
@@ -247,10 +252,47 @@ export function synthesizeHeapDecoderModule(layout: BucketLayout, mode: HeapDeco
|
|
|
247
252
|
vidExpr = { kind: "ReadInput", scope: "Input", name: "vid", type: Tu32 };
|
|
248
253
|
}
|
|
249
254
|
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
255
|
+
// THE CARRIER CARRIES THE DRAW INDEX, NOT THE DRAW'S VALUES.
|
|
256
|
+
//
|
|
257
|
+
// Per-draw uniforms and atlas-texture sub-fields are CONSTANT across a draw:
|
|
258
|
+
// pushing each of them across the stage boundary as its own flat varying
|
|
259
|
+
// costs one inter-stage location per uniform and FOUR per texture, and WebGPU
|
|
260
|
+
// allows only 16 in total. Shaders with a handful of per-draw uniforms plus a
|
|
261
|
+
// texture ran into that ceiling — and exceeding it does not fail loudly: the
|
|
262
|
+
// pipeline is created INVALID, which poisons the whole command buffer, so an
|
|
263
|
+
// entire pass silently vanishes. It made the varying budget a hidden design
|
|
264
|
+
// constraint on every heap shader.
|
|
265
|
+
//
|
|
266
|
+
// So the decoder surfaces the DRAW INDEX (one flat u32) and the FS loads what
|
|
267
|
+
// it needs straight from the heap arena, exactly as the family-member path
|
|
268
|
+
// already does. Cost: scalar, flat, draw-uniform storage reads per fragment
|
|
269
|
+
// instead of per vertex. Varying count is now O(1) in uniforms and textures.
|
|
270
|
+
//
|
|
271
|
+
// Per-VERTEX attribute refs are a different animal — they genuinely vary
|
|
272
|
+
// across the primitive — so they keep their carrier slots below.
|
|
253
273
|
let nextLocation = 0;
|
|
274
|
+
outputs.push({
|
|
275
|
+
name: HEAP_FS_DRAWIDX,
|
|
276
|
+
type: Tu32,
|
|
277
|
+
semantic: HEAP_FS_DRAWIDX,
|
|
278
|
+
decorations: [
|
|
279
|
+
{ kind: "Location", value: nextLocation++ },
|
|
280
|
+
{ kind: "Interpolation", mode: "flat" },
|
|
281
|
+
],
|
|
282
|
+
});
|
|
283
|
+
stmts.push({ kind: "WriteOutput", name: HEAP_FS_DRAWIDX, value: { kind: "Expr", value: drawIdxExpr } });
|
|
284
|
+
if (layout.perInstanceUniforms.size > 0) {
|
|
285
|
+
outputs.push({
|
|
286
|
+
name: HEAP_FS_INSTID,
|
|
287
|
+
type: Tu32,
|
|
288
|
+
semantic: HEAP_FS_INSTID,
|
|
289
|
+
decorations: [
|
|
290
|
+
{ kind: "Location", value: nextLocation++ },
|
|
291
|
+
{ kind: "Interpolation", mode: "flat" },
|
|
292
|
+
],
|
|
293
|
+
});
|
|
294
|
+
stmts.push({ kind: "WriteOutput", name: HEAP_FS_INSTID, value: { kind: "Expr", value: instIdExpr } });
|
|
295
|
+
}
|
|
254
296
|
// Texture-ref drawHeader fields are grouped by `textureBindingName` —
|
|
255
297
|
// each atlas-routed texture binding produces four contiguous entries
|
|
256
298
|
// (pageRef / formatBits / origin / size). We emit them as four
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
// attribute reads with heap loads at the IR level.
|
|
17
17
|
|
|
18
18
|
import type { Effect } from "@aardworx/wombat.shader";
|
|
19
|
+
import { effectHoleKey } from "@aardworx/wombat.shader";
|
|
19
20
|
|
|
20
21
|
// ─── Schema (extracted from CompiledEffect.interface) ───────────────
|
|
21
22
|
|
|
@@ -108,7 +109,7 @@ export interface FragmentOutputLayout {
|
|
|
108
109
|
// and on quota; every failure simply degrades to "recompute".
|
|
109
110
|
|
|
110
111
|
/** Cache-generation stamp for the persistent (localStorage) tier. */
|
|
111
|
-
export const HEAP_PERSIST_VERSION = "
|
|
112
|
+
export const HEAP_PERSIST_VERSION = "h9"; // h9: standalone FS reads the heap directly (one _h_drawIdx varying)
|
|
112
113
|
|
|
113
114
|
/**
|
|
114
115
|
* Uniforms whose drawHeader word IS the u32 value (no arena
|
|
@@ -180,7 +181,13 @@ function isCompiledHeapEffect(v: unknown): v is CompiledHeapEffect {
|
|
|
180
181
|
}
|
|
181
182
|
|
|
182
183
|
export function compileHeapEffect(effect: Effect, fragmentOutputLayout?: FragmentOutputLayout): CompiledHeapEffect {
|
|
183
|
-
|
|
184
|
+
// `effect.id` hashes the TEMPLATE, not the emitted code: closure-hole values
|
|
185
|
+
// are specialised into the source as literals and do NOT move the id when a
|
|
186
|
+
// getter's value changes at runtime. Keying only on the id therefore serves
|
|
187
|
+
// WGSL built from a previous hole value — and because this cache is also
|
|
188
|
+
// persisted, the stale shader outlives the page. Pair the id with a
|
|
189
|
+
// fingerprint of the current holes (empty for the hole-free hot path).
|
|
190
|
+
const contentKey = effect.id + effectHoleKey(effect) + fboLayoutKey(fragmentOutputLayout);
|
|
184
191
|
const mem = _compiledHeapEffectMemCache.get(contentKey);
|
|
185
192
|
if (mem !== undefined) return mem;
|
|
186
193
|
const lsKey = persistKey(HEAP_PERSIST_VERSION, "che", contentKey);
|
|
@@ -17,12 +17,12 @@
|
|
|
17
17
|
// This module knows nothing about the runtime; the BucketLayout it
|
|
18
18
|
// consumes is identical to the one heapScene.ts already builds.
|
|
19
19
|
|
|
20
|
-
import { compileModule, stage as makeStage, effect as makeEffect } from "@aardworx/wombat.shader";
|
|
20
|
+
import { compileModule, stage as makeStage, effect as makeEffect, effectHoleKey } from "@aardworx/wombat.shader";
|
|
21
21
|
import { isInlineHeaderField } from "./heapEffect.js";
|
|
22
22
|
import type { Effect, CompileOptions } from "@aardworx/wombat.shader";
|
|
23
23
|
import { substituteInputsInStage, readInputs, mapExpr, mapStmt, liftReturns, uniformsToInputs, resolveHoles } from "@aardworx/wombat.shader/passes";
|
|
24
24
|
import type { HoleValue } from "@aardworx/wombat.shader/passes";
|
|
25
|
-
import { synthesizeHeapDecoderModule } from "./heapDecoder.js";
|
|
25
|
+
import { synthesizeHeapDecoderModule, HEAP_FS_DRAWIDX, HEAP_FS_INSTID } from "./heapDecoder.js";
|
|
26
26
|
import {
|
|
27
27
|
type Module, type Expr, type Stmt, type Type, type ValueDef,
|
|
28
28
|
type EntryDef, type EntryParameter, type ParamDecoration,
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
generateAtlasBindings, generateAtlasSwitch, generateAtlasPrelude,
|
|
34
34
|
HEAP_PERSIST_VERSION, persistKey, lsLoad, lsStore,
|
|
35
35
|
type BucketLayout,
|
|
36
|
+
type DrawHeaderField,
|
|
36
37
|
} from "./heapEffect.js";
|
|
37
38
|
import type { IntrinsicRef } from "@aardworx/wombat.shader/ir";
|
|
38
39
|
|
|
@@ -348,6 +349,23 @@ function addInterstageParams(
|
|
|
348
349
|
return { ...m, values };
|
|
349
350
|
}
|
|
350
351
|
|
|
352
|
+
/**
|
|
353
|
+
* Declare extra FRAGMENT inputs, leaving the vertex side alone. The decoder
|
|
354
|
+
* path already writes its carrier on the VS; the FS just needs the matching
|
|
355
|
+
* declaration to read it.
|
|
356
|
+
*/
|
|
357
|
+
function addFsInputs(m: Module, extras: readonly EntryParameter[]): Module {
|
|
358
|
+
if (extras.length === 0) return m;
|
|
359
|
+
const values = m.values.map((v): typeof v => {
|
|
360
|
+
if (v.kind !== "Entry" || v.entry.stage !== "fragment") return v;
|
|
361
|
+
const have = new Set(v.entry.inputs.map(p => p.name));
|
|
362
|
+
const add = extras.filter(p => !have.has(p.name));
|
|
363
|
+
if (add.length === 0) return v;
|
|
364
|
+
return { ...v, entry: { ...v.entry, inputs: [...v.entry.inputs, ...add] } };
|
|
365
|
+
});
|
|
366
|
+
return { ...m, values };
|
|
367
|
+
}
|
|
368
|
+
|
|
351
369
|
/**
|
|
352
370
|
* Prepend `stmts` to every VS entry's body. Flatten when the body is
|
|
353
371
|
* already a `Sequential` — CSE numbers `_cse0..N` per Sequential, so
|
|
@@ -407,7 +425,15 @@ function wgslTypeFromHeapField(wgslType: string): Type {
|
|
|
407
425
|
* emit `applyFamilyMemberFsShape` rewires the FS entry signature to
|
|
408
426
|
* surface them as plain u32 fn parameters.
|
|
409
427
|
*/
|
|
410
|
-
function rewriteFsUniformsDirect(
|
|
428
|
+
function rewriteFsUniformsDirect(
|
|
429
|
+
m: Module,
|
|
430
|
+
layout: BucketLayout,
|
|
431
|
+
// Where the FS gets the per-draw header index from. `family-member` receives
|
|
432
|
+
// it as a plain u32 fn parameter (a Var); `standalone` reads it from the one
|
|
433
|
+
// flat varying it threads. Everything else about the direct load is identical.
|
|
434
|
+
drawIdxExpr: Expr = { kind: "Var", var: { name: "heap_drawIdx", type: Tu32, mutable: false } } as Expr,
|
|
435
|
+
instIdExpr: Expr = { kind: "Var", var: { name: "instId", type: Tu32, mutable: false } } as Expr,
|
|
436
|
+
): Module {
|
|
411
437
|
const used = new Set<string>();
|
|
412
438
|
for (const v of m.values) {
|
|
413
439
|
if (v.kind !== "Entry" || v.entry.stage !== "fragment") continue;
|
|
@@ -418,8 +444,6 @@ function rewriteFsUniformsDirect(m: Module, layout: BucketLayout): Module {
|
|
|
418
444
|
if (used.size === 0) return m;
|
|
419
445
|
|
|
420
446
|
const fieldByName = new Map(layout.drawHeaderFields.map(f => [f.name, f]));
|
|
421
|
-
const drawIdxExpr: Expr = { kind: "Var", var: { name: "heap_drawIdx", type: Tu32, mutable: false } } as Expr;
|
|
422
|
-
const instIdExpr: Expr = { kind: "Var", var: { name: "instId", type: Tu32, mutable: false } } as Expr;
|
|
423
447
|
const fsSubst = new Map<string, Expr>();
|
|
424
448
|
for (const name of used) {
|
|
425
449
|
const f = fieldByName.get(name);
|
|
@@ -445,7 +469,11 @@ function rewriteFsUniformsDirect(m: Module, layout: BucketLayout): Module {
|
|
|
445
469
|
* `heap_drawIdx`. The wrapper-supplied `heap_drawIdx` u32 fn parameter
|
|
446
470
|
* carries the per-draw header index.
|
|
447
471
|
*/
|
|
448
|
-
function rewriteFsAtlasTexturesDirect(
|
|
472
|
+
function rewriteFsAtlasTexturesDirect(
|
|
473
|
+
m: Module,
|
|
474
|
+
layout: BucketLayout,
|
|
475
|
+
drawIdxExprIn: Expr = { kind: "Var", var: { name: "heap_drawIdx", type: Tu32, mutable: false } } as Expr,
|
|
476
|
+
): Module {
|
|
449
477
|
if (layout.atlasTextureBindings.size === 0) return m;
|
|
450
478
|
const used = new Set<string>();
|
|
451
479
|
for (const v of m.values) {
|
|
@@ -466,7 +494,7 @@ function rewriteFsAtlasTexturesDirect(m: Module, layout: BucketLayout): Module {
|
|
|
466
494
|
inner.set(sub, byteOffsetOf(f.byteOffset));
|
|
467
495
|
}
|
|
468
496
|
|
|
469
|
-
const drawIdxExpr: Expr =
|
|
497
|
+
const drawIdxExpr: Expr = drawIdxExprIn;
|
|
470
498
|
const stride = layout.strideU32;
|
|
471
499
|
const headerU32At = (offU32: number): Expr =>
|
|
472
500
|
item(headersU32, add(mul(drawIdxExpr, constU32(stride), Tu32), constU32(offU32), Tu32), Tu32);
|
|
@@ -499,6 +527,78 @@ function rewriteFsAtlasTexturesDirect(m: Module, layout: BucketLayout): Module {
|
|
|
499
527
|
}));
|
|
500
528
|
}
|
|
501
529
|
|
|
530
|
+
/**
|
|
531
|
+
* STANDALONE FS heap access — one varying, not N.
|
|
532
|
+
*
|
|
533
|
+
* The old standalone path threaded a VS→FS varying per FS-used uniform AND
|
|
534
|
+
* FOUR per atlas texture (`_h_<t>{PageRef,FormatBits,Origin,Size}`). WebGPU
|
|
535
|
+
* caps inter-stage variables at 16 locations, so a shader with a handful of
|
|
536
|
+
* per-draw uniforms and a texture ran into the ceiling — and blowing it does
|
|
537
|
+
* not fail loudly: the pipeline is created INVALID, which poisons the whole
|
|
538
|
+
* command buffer, so the entire pass silently disappears. It made the varying
|
|
539
|
+
* budget a design constraint on every shader written against the heap.
|
|
540
|
+
*
|
|
541
|
+
* `family-member` never had this problem: it loads uniforms and atlas headers
|
|
542
|
+
* straight out of the heap arena in the FS, keyed by the draw index. Standalone
|
|
543
|
+
* now does the same — it just has to carry the draw index itself, so it threads
|
|
544
|
+
* exactly ONE flat u32 varying (`_h_drawIdx`), plus `_iidx` when a per-instance
|
|
545
|
+
* uniform is actually read from the FS. Varying count is now O(1) in the number
|
|
546
|
+
* of uniforms and textures.
|
|
547
|
+
*
|
|
548
|
+
* The trade is per-fragment storage reads instead of per-vertex ones. They are
|
|
549
|
+
* scalar, flat, and uniform across a draw (so they hit cache), and this is the
|
|
550
|
+
* path `family-member` has been running in production all along.
|
|
551
|
+
*/
|
|
552
|
+
function rewriteFsHeapDirectStandalone(m: Module, layout: BucketLayout): Module {
|
|
553
|
+
// What does the FS actually touch?
|
|
554
|
+
const usedUniforms = new Set<string>();
|
|
555
|
+
const usedAtlas = new Set<string>();
|
|
556
|
+
for (const v of m.values) {
|
|
557
|
+
if (v.kind !== "Entry" || v.entry.stage !== "fragment") continue;
|
|
558
|
+
for (const sn of readInputs(v.entry.body).values()) {
|
|
559
|
+
if (sn.scope === "Uniform") usedUniforms.add(sn.name);
|
|
560
|
+
}
|
|
561
|
+
visitStmtExprs(v.entry.body, e => {
|
|
562
|
+
const hit = isAtlasSampleCall(e, layout.atlasTextureBindings);
|
|
563
|
+
if (hit !== null) usedAtlas.add(hit.name);
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
if (usedUniforms.size === 0 && usedAtlas.size === 0) return m;
|
|
567
|
+
|
|
568
|
+
// Thread the carrier(s). `_iidx` only when a per-instance uniform is read —
|
|
569
|
+
// otherwise it is a varying nobody needs.
|
|
570
|
+
const needsIidx = [...usedUniforms].some(n => layout.perInstanceUniforms.has(n));
|
|
571
|
+
const threadParams: EntryParameter[] = [{
|
|
572
|
+
name: "_h_drawIdx", type: Tu32, semantic: "_h_drawIdx",
|
|
573
|
+
decorations: [{ kind: "Interpolation", mode: "flat" } as ParamDecoration],
|
|
574
|
+
}];
|
|
575
|
+
const vsWrites: Stmt[] = [{
|
|
576
|
+
kind: "WriteOutput", name: "_h_drawIdx",
|
|
577
|
+
value: { kind: "Expr", value: drawIdxExprFor(layout) },
|
|
578
|
+
}];
|
|
579
|
+
if (needsIidx) {
|
|
580
|
+
threadParams.push({
|
|
581
|
+
name: "_iidx", type: Tu32, semantic: "_iidx",
|
|
582
|
+
decorations: [{ kind: "Interpolation", mode: "flat" } as ParamDecoration],
|
|
583
|
+
});
|
|
584
|
+
vsWrites.push({
|
|
585
|
+
kind: "WriteOutput", name: "_iidx",
|
|
586
|
+
value: { kind: "Expr", value: readScope("Builtin", "instance_index", Tu32) },
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const fsDrawIdx = readScope("Input", "_h_drawIdx", Tu32);
|
|
591
|
+
const fsInstId = readScope("Input", "_iidx", Tu32);
|
|
592
|
+
|
|
593
|
+
let out = m;
|
|
594
|
+
out = addInterstageParams(out, threadParams);
|
|
595
|
+
out = prependVsBodyStmts(out, vsWrites);
|
|
596
|
+
// Same loaders the family path uses — only the source of the draw index differs.
|
|
597
|
+
out = rewriteFsAtlasTexturesDirect(out, layout, fsDrawIdx);
|
|
598
|
+
out = rewriteFsUniformsDirect(out, layout, fsDrawIdx, fsInstId);
|
|
599
|
+
return out;
|
|
600
|
+
}
|
|
601
|
+
|
|
502
602
|
/**
|
|
503
603
|
* FS uniform threading: thread `_h_<name>Ref` (and `_iidx`, when
|
|
504
604
|
* needed) varyings from VS to FS, and rewrite FS `ReadInput("Uniform",
|
|
@@ -846,7 +946,10 @@ export function compileHeapEffectIR(
|
|
|
846
946
|
compileOptions: CompileOptions,
|
|
847
947
|
mode: HeapEffectEmitMode = "standalone",
|
|
848
948
|
): HeapEffectIR {
|
|
849
|
-
|
|
949
|
+
// Hole values are baked into the emitted code but do NOT move `effect.id`
|
|
950
|
+
// (see compileHeapEffect) — they must be part of the key, memory AND
|
|
951
|
+
// persisted, or a changed capture silently reuses the old shader.
|
|
952
|
+
const contentKey = `${userEffect.id}${effectHoleKey(userEffect)}|${layout.id}|${mode}|${compileOptionsKey(compileOptions)}`;
|
|
850
953
|
const mem = _heapIrMemCache.get(contentKey);
|
|
851
954
|
if (mem !== undefined) return mem;
|
|
852
955
|
const lsKey = persistKey(HEAP_PERSIST_VERSION, "ir", contentKey);
|
|
@@ -897,8 +1000,11 @@ export function compileHeapEffectIR(
|
|
|
897
1000
|
combined = rewriteFsAtlasTexturesDirect(combined, layout);
|
|
898
1001
|
combined = rewriteFsUniformsDirect(combined, layout);
|
|
899
1002
|
} else {
|
|
900
|
-
|
|
901
|
-
|
|
1003
|
+
// Standalone reads the heap directly too — it just carries the draw index
|
|
1004
|
+
// in ONE flat varying instead of receiving it as a parameter. (The old
|
|
1005
|
+
// per-uniform / 4-per-texture varying threading is gone: it hit WebGPU's
|
|
1006
|
+
// 16-location inter-stage cap and silently invalidated whole pipelines.)
|
|
1007
|
+
combined = rewriteFsHeapDirectStandalone(combined, layout);
|
|
902
1008
|
}
|
|
903
1009
|
// Add heap storage buffers as bindings.
|
|
904
1010
|
combined = { ...combined, values: [...heapStorageBufferDecls(), ...combined.values] };
|
|
@@ -971,6 +1077,67 @@ export function compileHeapEffectIR(
|
|
|
971
1077
|
* No post-emit string mangling — `applyMegacallToEmittedVs` /
|
|
972
1078
|
* `applyFamilyMemberShape` are gone for this path.
|
|
973
1079
|
*/
|
|
1080
|
+
/**
|
|
1081
|
+
* FS heap access for the DECODER path: read per-draw uniforms and atlas-texture
|
|
1082
|
+
* headers directly out of the arena, keyed by the draw index the decoder put on
|
|
1083
|
+
* the carrier (`_h_drawIdx`), instead of reading values the decoder pushed
|
|
1084
|
+
* across as one varying each.
|
|
1085
|
+
*
|
|
1086
|
+
* Only `uniform-ref` fields are substituted. `attribute-ref` fields are
|
|
1087
|
+
* per-VERTEX data — they must keep flowing through the carrier, and they were
|
|
1088
|
+
* renamed into `Input` scope by `uniformsToInputs` just like the uniforms, so
|
|
1089
|
+
* the substitution has to be name-driven, not scope-driven.
|
|
1090
|
+
*/
|
|
1091
|
+
function rewriteFsHeapDirectViaCarrier(m: Module, layout: BucketLayout): Module {
|
|
1092
|
+
const fsDrawIdx = readScope("Input", HEAP_FS_DRAWIDX, Tu32);
|
|
1093
|
+
const fsInstId = readScope("Input", HEAP_FS_INSTID, Tu32);
|
|
1094
|
+
|
|
1095
|
+
// Declare the carrier(s) as FRAGMENT INPUTS. The decoder already writes them
|
|
1096
|
+
// on the vertex side; without the matching input declaration the emitter has
|
|
1097
|
+
// no struct member to resolve the read against (it reaches for the output
|
|
1098
|
+
// struct and the module fails to parse).
|
|
1099
|
+
const fsInputs: EntryParameter[] = [{
|
|
1100
|
+
name: HEAP_FS_DRAWIDX, type: Tu32, semantic: HEAP_FS_DRAWIDX,
|
|
1101
|
+
decorations: [{ kind: "Interpolation", mode: "flat" } as ParamDecoration],
|
|
1102
|
+
}];
|
|
1103
|
+
if (layout.perInstanceUniforms.size > 0) {
|
|
1104
|
+
fsInputs.push({
|
|
1105
|
+
name: HEAP_FS_INSTID, type: Tu32, semantic: HEAP_FS_INSTID,
|
|
1106
|
+
decorations: [{ kind: "Interpolation", mode: "flat" } as ParamDecoration],
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
let out = addFsInputs(m, fsInputs);
|
|
1110
|
+
|
|
1111
|
+
// Atlas samples first (they read header sub-fields, not uniform values).
|
|
1112
|
+
out = rewriteFsAtlasTexturesDirect(out, layout, fsDrawIdx);
|
|
1113
|
+
|
|
1114
|
+
// Which per-draw uniforms does the FS still read?
|
|
1115
|
+
const uniformFields = new Map<string, DrawHeaderField>();
|
|
1116
|
+
for (const f of layout.drawHeaderFields) {
|
|
1117
|
+
if (f.kind === "uniform-ref") uniformFields.set(f.name, f);
|
|
1118
|
+
}
|
|
1119
|
+
const used = new Set<string>();
|
|
1120
|
+
for (const v of out.values) {
|
|
1121
|
+
if (v.kind !== "Entry" || v.entry.stage !== "fragment") continue;
|
|
1122
|
+
for (const sn of readInputs(v.entry.body).values()) {
|
|
1123
|
+
if (sn.scope === "Input" && uniformFields.has(sn.name)) used.add(sn.name);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
if (used.size === 0) return out;
|
|
1127
|
+
|
|
1128
|
+
const subst = new Map<string, Expr>();
|
|
1129
|
+
for (const name of used) {
|
|
1130
|
+
const f = uniformFields.get(name)!;
|
|
1131
|
+
const refExpr = loadHeaderRef(fsDrawIdx, f.byteOffset, layout.strideU32);
|
|
1132
|
+
subst.set(name, isInlineHeaderField(f)
|
|
1133
|
+
? refExpr
|
|
1134
|
+
: layout.perInstanceUniforms.has(name)
|
|
1135
|
+
? loadInstanceByRef(refExpr, fsInstId, f.uniformWgslType ?? "")
|
|
1136
|
+
: loadUniformByRef(refExpr, f.uniformWgslType ?? ""));
|
|
1137
|
+
}
|
|
1138
|
+
return substituteInputsInStage(out, "fragment", "Input", n => subst.get(n));
|
|
1139
|
+
}
|
|
1140
|
+
|
|
974
1141
|
function compileHeapEffectIRViaDecoder(
|
|
975
1142
|
userEffect: Effect,
|
|
976
1143
|
layout: BucketLayout,
|
|
@@ -1017,9 +1184,14 @@ function compileHeapEffectIRViaDecoder(
|
|
|
1017
1184
|
combined = liftReturns(combined);
|
|
1018
1185
|
combined = injectVsBuiltins(combined);
|
|
1019
1186
|
|
|
1020
|
-
// 6. FS-side
|
|
1021
|
-
//
|
|
1022
|
-
|
|
1187
|
+
// 6. FS-side heap access. The decoder no longer ships per-draw VALUES across
|
|
1188
|
+
// the stage boundary (that cost one varying per uniform and four per texture,
|
|
1189
|
+
// against WebGPU's 16-location cap — see heapDecoder). It ships the draw
|
|
1190
|
+
// INDEX, and the FS loads what it needs straight from the arena. Whatever the
|
|
1191
|
+
// fragment stage stops reading, `pruneCrossStage` then drops from the carrier
|
|
1192
|
+
// by itself, so the varying count collapses to what genuinely varies per
|
|
1193
|
+
// vertex.
|
|
1194
|
+
combined = rewriteFsHeapDirectViaCarrier(combined, layout);
|
|
1023
1195
|
|
|
1024
1196
|
// 6b. USER storage buffers (the OIT node pool & co): pin each to the
|
|
1025
1197
|
// slot the bucket's BGL committed to. `keep: true` makes the emitter
|