@aardworx/wombat.rendering 0.8.0 → 0.9.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.
Files changed (136) hide show
  1. package/dist/core/command.d.ts +7 -9
  2. package/dist/core/command.d.ts.map +1 -1
  3. package/dist/core/renderTask.d.ts +64 -1
  4. package/dist/core/renderTask.d.ts.map +1 -1
  5. package/dist/core/renderTask.js +3 -1
  6. package/dist/core/renderTask.js.map +1 -1
  7. package/dist/resources/preparedRenderObject.js +3 -3
  8. package/dist/resources/preparedRenderObject.js.map +1 -1
  9. package/dist/resources/uniformBuffer.d.ts.map +1 -1
  10. package/dist/resources/uniformBuffer.js +17 -0
  11. package/dist/resources/uniformBuffer.js.map +1 -1
  12. package/dist/runtime/derivedUniforms/dispatch.d.ts +105 -0
  13. package/dist/runtime/derivedUniforms/dispatch.d.ts.map +1 -0
  14. package/dist/runtime/derivedUniforms/dispatch.js +355 -0
  15. package/dist/runtime/derivedUniforms/dispatch.js.map +1 -0
  16. package/dist/runtime/derivedUniforms/index.d.ts +6 -0
  17. package/dist/runtime/derivedUniforms/index.d.ts.map +1 -0
  18. package/dist/runtime/derivedUniforms/index.js +26 -0
  19. package/dist/runtime/derivedUniforms/index.js.map +1 -0
  20. package/dist/runtime/derivedUniforms/recipes.d.ts +42 -0
  21. package/dist/runtime/derivedUniforms/recipes.d.ts.map +1 -0
  22. package/dist/runtime/derivedUniforms/recipes.js +134 -0
  23. package/dist/runtime/derivedUniforms/recipes.js.map +1 -0
  24. package/dist/runtime/derivedUniforms/sceneIntegration.d.ts +56 -0
  25. package/dist/runtime/derivedUniforms/sceneIntegration.d.ts.map +1 -0
  26. package/dist/runtime/derivedUniforms/sceneIntegration.js +171 -0
  27. package/dist/runtime/derivedUniforms/sceneIntegration.js.map +1 -0
  28. package/dist/runtime/derivedUniforms/slotId.d.ts +15 -0
  29. package/dist/runtime/derivedUniforms/slotId.d.ts.map +1 -0
  30. package/dist/runtime/derivedUniforms/slotId.js +30 -0
  31. package/dist/runtime/derivedUniforms/slotId.js.map +1 -0
  32. package/dist/runtime/derivedUniforms/slots.d.ts +49 -0
  33. package/dist/runtime/derivedUniforms/slots.d.ts.map +1 -0
  34. package/dist/runtime/derivedUniforms/slots.js +123 -0
  35. package/dist/runtime/derivedUniforms/slots.js.map +1 -0
  36. package/dist/runtime/derivedUniforms/uberKernel.wgsl.d.ts +13 -0
  37. package/dist/runtime/derivedUniforms/uberKernel.wgsl.d.ts.map +1 -0
  38. package/dist/runtime/derivedUniforms/uberKernel.wgsl.js +218 -0
  39. package/dist/runtime/derivedUniforms/uberKernel.wgsl.js.map +1 -0
  40. package/dist/runtime/flattenTree.d.ts +5 -0
  41. package/dist/runtime/flattenTree.d.ts.map +1 -0
  42. package/dist/runtime/flattenTree.js +45 -0
  43. package/dist/runtime/flattenTree.js.map +1 -0
  44. package/dist/runtime/heapAdapter.d.ts +16 -0
  45. package/dist/runtime/heapAdapter.d.ts.map +1 -0
  46. package/dist/runtime/heapAdapter.js +263 -0
  47. package/dist/runtime/heapAdapter.js.map +1 -0
  48. package/dist/runtime/heapEffect.d.ts +235 -0
  49. package/dist/runtime/heapEffect.d.ts.map +1 -0
  50. package/dist/runtime/heapEffect.js +518 -0
  51. package/dist/runtime/heapEffect.js.map +1 -0
  52. package/dist/runtime/heapEffectIR.d.ts +34 -0
  53. package/dist/runtime/heapEffectIR.d.ts.map +1 -0
  54. package/dist/runtime/heapEffectIR.js +1161 -0
  55. package/dist/runtime/heapEffectIR.js.map +1 -0
  56. package/dist/runtime/heapEligibility.d.ts +9 -0
  57. package/dist/runtime/heapEligibility.d.ts.map +1 -0
  58. package/dist/runtime/heapEligibility.js +195 -0
  59. package/dist/runtime/heapEligibility.js.map +1 -0
  60. package/dist/runtime/heapScene.d.ts +240 -31
  61. package/dist/runtime/heapScene.d.ts.map +1 -1
  62. package/dist/runtime/heapScene.js +3334 -265
  63. package/dist/runtime/heapScene.js.map +1 -1
  64. package/dist/runtime/heapShaderFamily.d.ts +132 -0
  65. package/dist/runtime/heapShaderFamily.d.ts.map +1 -0
  66. package/dist/runtime/heapShaderFamily.js +866 -0
  67. package/dist/runtime/heapShaderFamily.js.map +1 -0
  68. package/dist/runtime/hybridScene.d.ts +133 -0
  69. package/dist/runtime/hybridScene.d.ts.map +1 -0
  70. package/dist/runtime/hybridScene.js +159 -0
  71. package/dist/runtime/hybridScene.js.map +1 -0
  72. package/dist/runtime/index.d.ts +8 -0
  73. package/dist/runtime/index.d.ts.map +1 -1
  74. package/dist/runtime/index.js +9 -0
  75. package/dist/runtime/index.js.map +1 -1
  76. package/dist/runtime/renderTask.d.ts +21 -4
  77. package/dist/runtime/renderTask.d.ts.map +1 -1
  78. package/dist/runtime/renderTask.js +127 -32
  79. package/dist/runtime/renderTask.js.map +1 -1
  80. package/dist/runtime/runtime.d.ts +33 -3
  81. package/dist/runtime/runtime.d.ts.map +1 -1
  82. package/dist/runtime/runtime.js +12 -3
  83. package/dist/runtime/runtime.js.map +1 -1
  84. package/dist/runtime/scenePass.d.ts +28 -3
  85. package/dist/runtime/scenePass.d.ts.map +1 -1
  86. package/dist/runtime/scenePass.js +37 -4
  87. package/dist/runtime/scenePass.js.map +1 -1
  88. package/dist/runtime/textureAtlas/atlasMipGutterKernel.d.ts +35 -0
  89. package/dist/runtime/textureAtlas/atlasMipGutterKernel.d.ts.map +1 -0
  90. package/dist/runtime/textureAtlas/atlasMipGutterKernel.js +247 -0
  91. package/dist/runtime/textureAtlas/atlasMipGutterKernel.js.map +1 -0
  92. package/dist/runtime/textureAtlas/atlasPool.d.ts +240 -0
  93. package/dist/runtime/textureAtlas/atlasPool.d.ts.map +1 -0
  94. package/dist/runtime/textureAtlas/atlasPool.js +798 -0
  95. package/dist/runtime/textureAtlas/atlasPool.js.map +1 -0
  96. package/dist/runtime/textureAtlas/bvhTree2d.d.ts +33 -0
  97. package/dist/runtime/textureAtlas/bvhTree2d.d.ts.map +1 -0
  98. package/dist/runtime/textureAtlas/bvhTree2d.js +501 -0
  99. package/dist/runtime/textureAtlas/bvhTree2d.js.map +1 -0
  100. package/dist/runtime/textureAtlas/packer.d.ts +50 -0
  101. package/dist/runtime/textureAtlas/packer.d.ts.map +1 -0
  102. package/dist/runtime/textureAtlas/packer.js +390 -0
  103. package/dist/runtime/textureAtlas/packer.js.map +1 -0
  104. package/dist/window/loop.d.ts +14 -0
  105. package/dist/window/loop.d.ts.map +1 -1
  106. package/dist/window/loop.js +18 -5
  107. package/dist/window/loop.js.map +1 -1
  108. package/package.json +5 -4
  109. package/src/core/command.ts +9 -9
  110. package/src/core/renderTask.ts +55 -2
  111. package/src/resources/preparedRenderObject.ts +3 -3
  112. package/src/resources/uniformBuffer.ts +15 -0
  113. package/src/runtime/derivedUniforms/dispatch.ts +407 -0
  114. package/src/runtime/derivedUniforms/index.ts +52 -0
  115. package/src/runtime/derivedUniforms/recipes.ts +159 -0
  116. package/src/runtime/derivedUniforms/sceneIntegration.ts +239 -0
  117. package/src/runtime/derivedUniforms/slotId.ts +35 -0
  118. package/src/runtime/derivedUniforms/slots.ts +155 -0
  119. package/src/runtime/derivedUniforms/uberKernel.wgsl.ts +220 -0
  120. package/src/runtime/flattenTree.ts +46 -0
  121. package/src/runtime/heapAdapter.ts +314 -0
  122. package/src/runtime/heapEffect.ts +719 -0
  123. package/src/runtime/heapEffectIR.ts +1211 -0
  124. package/src/runtime/heapEligibility.ts +189 -0
  125. package/src/runtime/heapScene.ts +3981 -349
  126. package/src/runtime/heapShaderFamily.ts +1102 -0
  127. package/src/runtime/hybridScene.ts +310 -0
  128. package/src/runtime/index.ts +46 -0
  129. package/src/runtime/renderTask.ts +167 -32
  130. package/src/runtime/runtime.ts +37 -4
  131. package/src/runtime/scenePass.ts +39 -4
  132. package/src/runtime/textureAtlas/atlasMipGutterKernel.ts +291 -0
  133. package/src/runtime/textureAtlas/atlasPool.ts +991 -0
  134. package/src/runtime/textureAtlas/bvhTree2d.ts +530 -0
  135. package/src/runtime/textureAtlas/packer.ts +429 -0
  136. package/src/window/loop.ts +32 -6
@@ -0,0 +1,1161 @@
1
+ // heapEffectIR — IR-level rewriter for the heap render path.
2
+ //
3
+ // Takes a wombat.shader Effect, transforms its IR Module so that:
4
+ // - Vertex attribute reads are replaced with heap-buffer pulls
5
+ // (cyclic via `vid % length`).
6
+ // - Uniform reads (in any stage) are replaced with heap-buffer
7
+ // decodes via per-draw refs from the headers buffer.
8
+ // - FS uniform decodes thread the per-uniform refs through VsOut
9
+ // as flat-interpolated `_h_<name>Ref: u32` varyings.
10
+ // - The heap arena's four typed views (heapU32, headersU32, heapF32,
11
+ // heapV4f) are added as storage-buffer bindings.
12
+ //
13
+ // The result is a fresh Module that compiles via the existing
14
+ // wombat.shader pipeline (DCE, cross-stage link, fragment-output
15
+ // linker, WGSL emit) — no string-regex post-processing.
16
+ //
17
+ // This module knows nothing about the runtime; the BucketLayout it
18
+ // consumes is identical to the one heapScene.ts already builds.
19
+ import { compileModule, stage as makeStage, effect as makeEffect } from "@aardworx/wombat.shader";
20
+ import { substituteInputsInStage, readInputs, mapExpr, mapStmt, liftReturns } from "@aardworx/wombat.shader/passes";
21
+ import { Tu32, Tf32, Vec, } from "@aardworx/wombat.shader/ir";
22
+ import { megacallSearchPrelude, atlasVaryingNames, generateAtlasBindings, generateAtlasSwitch, generateAtlasPrelude, } from "./heapEffect.js";
23
+ // ─── IR builders ────────────────────────────────────────────────────
24
+ const Tbool = { kind: "Bool" };
25
+ const constU32 = (n) => ({
26
+ kind: "Const", type: Tu32, value: { kind: "Int", signed: false, value: n },
27
+ });
28
+ const constF32 = (n) => ({
29
+ kind: "Const", type: Tf32, value: { kind: "Float", value: n },
30
+ });
31
+ const readScope = (scope, name, type) => ({
32
+ kind: "ReadInput", scope, name, type,
33
+ });
34
+ const item = (target, index, elemType) => ({
35
+ kind: "Item", target, index, type: elemType,
36
+ });
37
+ const add = (lhs, rhs, type) => ({ kind: "Add", lhs, rhs, type });
38
+ const mul = (lhs, rhs, type) => ({ kind: "Mul", lhs, rhs, type });
39
+ const div = (lhs, rhs, type) => ({ kind: "Div", lhs, rhs, type });
40
+ const mod = (lhs, rhs, type) => ({ kind: "Mod", lhs, rhs, type });
41
+ const eqU32 = (lhs, rhs) => ({ kind: "Eq", lhs, rhs, type: Tbool });
42
+ const select = (cond, ifTrue, ifFalse, type) => ({
43
+ kind: "Conditional", cond, ifTrue, ifFalse, type,
44
+ });
45
+ const newVec = (components, type) => ({
46
+ kind: "NewVector", components, type,
47
+ });
48
+ const matFromRows = (rows, type) => ({
49
+ kind: "MatrixFromRows", rows, type,
50
+ });
51
+ // Runtime-sized array types for the heap storage buffers.
52
+ const arrU32 = { kind: "Array", element: Tu32, length: "runtime" };
53
+ const arrF32 = { kind: "Array", element: Tf32, length: "runtime" };
54
+ const arrVec4 = { kind: "Array", element: Vec(Tf32, 4), length: "runtime" };
55
+ const Tvec2 = Vec(Tf32, 2);
56
+ const Tvec3 = Vec(Tf32, 3);
57
+ const Tvec4 = Vec(Tf32, 4);
58
+ // ─── Storage-buffer bindings ────────────────────────────────────────
59
+ //
60
+ // Bindings 0..3 are the four typed views of the heap arena. Bindings
61
+ // 4+ (textures/samplers) are added by the layout / runtime as needed.
62
+ function heapStorageBufferDecls() {
63
+ return [
64
+ { kind: "StorageBuffer", binding: { group: 0, slot: 0 }, name: "heapU32", layout: arrU32, access: "read" },
65
+ { kind: "StorageBuffer", binding: { group: 0, slot: 1 }, name: "headersU32", layout: arrU32, access: "read" },
66
+ { kind: "StorageBuffer", binding: { group: 0, slot: 2 }, name: "heapF32", layout: arrF32, access: "read" },
67
+ { kind: "StorageBuffer", binding: { group: 0, slot: 3 }, name: "heapV4f", layout: arrVec4, access: "read" },
68
+ ];
69
+ }
70
+ // ─── Heap-load IR builders ─────────────────────────────────────────
71
+ //
72
+ // Storage buffers are exposed in IR as `ReadInput("Uniform", name)`
73
+ // (the same scope WGSL uses for all bindings). Indexing is `Item`.
74
+ // Address arithmetic is plain Add/Mul on u32.
75
+ const heapU32 = readScope("Uniform", "heapU32", arrU32);
76
+ const headersU32 = readScope("Uniform", "headersU32", arrU32);
77
+ const heapF32 = readScope("Uniform", "heapF32", arrF32);
78
+ const heapV4f = readScope("Uniform", "heapV4f", arrVec4);
79
+ /** `headersU32[drawIdx * stride + offset]`. */
80
+ function loadHeaderRef(drawIdx, fieldByteOffset, strideU32) {
81
+ // u32 index = drawIdx * strideU32 + (fieldByteOffset / 4).
82
+ const offU32 = fieldByteOffset / 4;
83
+ const idx = add(mul(drawIdx, constU32(strideU32), Tu32), constU32(offU32), Tu32);
84
+ return item(headersU32, idx, Tu32);
85
+ }
86
+ /**
87
+ * Cyclic per-element index: `(elemIdx % count) * floatsPerElement`,
88
+ * where count = alloc-header length (u32 at offset 4, i.e. ref/4 + 1).
89
+ * For per-vertex attributes elemIdx = vid; for instance arrays =
90
+ * iidx (length is then the instance count, no cycling needed but the
91
+ * mod is harmless).
92
+ */
93
+ function cyclicElemIdx(refIdent, elemIdx, floatsPerElem) {
94
+ const count = item(heapU32, add(div(refIdent, constU32(4), Tu32), constU32(1), Tu32), Tu32);
95
+ const cycled = mod(elemIdx, count, Tu32);
96
+ return mul(cycled, constU32(floatsPerElem), Tu32);
97
+ }
98
+ /**
99
+ * Load a per-draw uniform value from the heap given its `u32` ref.
100
+ * Type-driven: matrix → 4 vec4 columns from heapV4f; vec4 → one
101
+ * heapV4f read; vec3/2/scalar → heapF32 reads + NewVector.
102
+ */
103
+ function loadUniformByRef(refIdent, wgslType) {
104
+ switch (wgslType) {
105
+ case "mat4x4<f32>": {
106
+ // base vec4 index = (ref + 16) / 16
107
+ const base = div(add(refIdent, constU32(16), Tu32), constU32(16), Tu32);
108
+ const cols = [];
109
+ for (let i = 0; i < 4; i++) {
110
+ const idx = i === 0 ? base : add(base, constU32(i), Tu32);
111
+ cols.push(item(heapV4f, idx, Tvec4));
112
+ }
113
+ return matFromRows(cols, { kind: "Matrix", element: Tf32, rows: 4, cols: 4 });
114
+ }
115
+ case "vec4<f32>":
116
+ return item(heapV4f, div(add(refIdent, constU32(16), Tu32), constU32(16), Tu32), Tvec4);
117
+ case "vec3<f32>": {
118
+ const base = div(add(refIdent, constU32(16), Tu32), constU32(4), Tu32);
119
+ return newVec([
120
+ item(heapF32, base, Tf32),
121
+ item(heapF32, add(base, constU32(1), Tu32), Tf32),
122
+ item(heapF32, add(base, constU32(2), Tu32), Tf32),
123
+ ], Tvec3);
124
+ }
125
+ case "vec2<f32>": {
126
+ const base = div(add(refIdent, constU32(16), Tu32), constU32(4), Tu32);
127
+ return newVec([
128
+ item(heapF32, base, Tf32),
129
+ item(heapF32, add(base, constU32(1), Tu32), Tf32),
130
+ ], Tvec2);
131
+ }
132
+ case "f32":
133
+ return item(heapF32, div(add(refIdent, constU32(16), Tu32), constU32(4), Tu32), Tf32);
134
+ case "u32":
135
+ return item(heapU32, div(add(refIdent, constU32(16), Tu32), constU32(4), Tu32), Tu32);
136
+ default:
137
+ throw new Error(`heapEffectIR: no IR loader for uniform type '${wgslType}'`);
138
+ }
139
+ }
140
+ /**
141
+ * Load an attribute value with cyclic addressing (`(idx % length) *
142
+ * stride`). The mod lets a length-1 broadcast replicate across all
143
+ * vertices. For vec4 the source can be V3-tight (12 B, .w = 1.0) or
144
+ * V4-tight (16 B, .w from data); we read the actual stride from the
145
+ * alloc header at offset 8, then use `select` for the .w fill.
146
+ */
147
+ function loadAttributeByRef(refIdent, idx, wgslType) {
148
+ const dataF32Base = div(add(refIdent, constU32(16), Tu32), constU32(4), Tu32);
149
+ switch (wgslType) {
150
+ case "vec3<f32>": {
151
+ const off = cyclicElemIdx(refIdent, idx, 3);
152
+ const base = add(dataF32Base, off, Tu32);
153
+ return newVec([
154
+ item(heapF32, base, Tf32),
155
+ item(heapF32, add(base, constU32(1), Tu32), Tf32),
156
+ item(heapF32, add(base, constU32(2), Tu32), Tf32),
157
+ ], Tvec3);
158
+ }
159
+ case "vec4<f32>": {
160
+ // strideF = stride_bytes / 4 (header u32 at ref/4 + 2).
161
+ const strideBytes = item(heapU32, add(div(refIdent, constU32(4), Tu32), constU32(2), Tu32), Tu32);
162
+ const strideF = div(strideBytes, constU32(4), Tu32);
163
+ const cycled = mod(idx, item(heapU32, add(div(refIdent, constU32(4), Tu32), constU32(1), Tu32), Tu32), Tu32);
164
+ const off = mul(cycled, strideF, Tu32);
165
+ const base = add(dataF32Base, off, Tu32);
166
+ const w = select(eqU32(strideF, constU32(4)), item(heapF32, add(base, constU32(3), Tu32), Tf32), constF32(1.0), Tf32);
167
+ return newVec([
168
+ item(heapF32, base, Tf32),
169
+ item(heapF32, add(base, constU32(1), Tu32), Tf32),
170
+ item(heapF32, add(base, constU32(2), Tu32), Tf32),
171
+ w,
172
+ ], Tvec4);
173
+ }
174
+ case "vec2<f32>": {
175
+ const off = cyclicElemIdx(refIdent, idx, 2);
176
+ const base = add(dataF32Base, off, Tu32);
177
+ return newVec([
178
+ item(heapF32, base, Tf32),
179
+ item(heapF32, add(base, constU32(1), Tu32), Tf32),
180
+ ], Tvec2);
181
+ }
182
+ default:
183
+ throw new Error(`heapEffectIR: no IR loader for attribute type '${wgslType}'`);
184
+ }
185
+ }
186
+ // ─── Instance-index / vertex-index built-in plumbing ────────────────
187
+ //
188
+ // The user's stages don't have @builtin(instance_index)/(vertex_index)
189
+ // inputs — they read inputs by name (vertexAttributes, uniforms).
190
+ // We need those builtins to address the heap. Inject them as
191
+ // EntryParameters on every vertex stage.
192
+ function entryHasBuiltin(e, semantic) {
193
+ return e.inputs.some(p => p.decorations.some(d => d.kind === "Builtin" && d.value === semantic));
194
+ }
195
+ function builtinParam(semantic) {
196
+ return {
197
+ name: semantic,
198
+ type: Tu32,
199
+ semantic,
200
+ decorations: [{ kind: "Builtin", value: semantic }],
201
+ };
202
+ }
203
+ function injectVsBuiltins(m) {
204
+ const values = m.values.map((v) => {
205
+ if (v.kind !== "Entry" || v.entry.stage !== "vertex")
206
+ return v;
207
+ let inputs = v.entry.inputs;
208
+ if (!entryHasBuiltin(v.entry, "vertex_index")) {
209
+ inputs = [...inputs, builtinParam("vertex_index")];
210
+ }
211
+ if (!entryHasBuiltin(v.entry, "instance_index")) {
212
+ inputs = [...inputs, builtinParam("instance_index")];
213
+ }
214
+ return { ...v, entry: { ...v.entry, inputs } };
215
+ });
216
+ return { ...m, values };
217
+ }
218
+ // ─── VS body rewrites ──────────────────────────────────────────────
219
+ //
220
+ // For each surviving uniform field, build a heap-load expression
221
+ // using `instance_index` as drawIdx. For each surviving attribute,
222
+ // use `vertex_index` and cyclic addressing.
223
+ //
224
+ // These map builders extract the (name → Expr) mapping that the VS
225
+ // substitution needs; the actual rewrite is delegated to
226
+ // `Effect.substitute({ vertex: { uniforms, inputs } })` in
227
+ // `compileHeapEffectIR`.
228
+ /**
229
+ * Per-RO selector expression. Always reads the `heap_drawIdx` local
230
+ * defined by the megacall prelude.
231
+ */
232
+ function drawIdxExprFor(_layout) {
233
+ return { kind: "Var", var: { name: "heap_drawIdx", type: Tu32, mutable: false } };
234
+ }
235
+ /**
236
+ * `(uniformName → loadUniformByRef(loadHeaderRef(...), wgslType))` for
237
+ * every uniform-ref field in `layout`. Used as the `vertex.uniforms`
238
+ * argument to `Effect.substitute`.
239
+ */
240
+ function buildVertexUniformMap(layout) {
241
+ const drawIdx = drawIdxExprFor(layout);
242
+ const iidx = readScope("Builtin", "instance_index", Tu32);
243
+ const stride = layout.strideU32;
244
+ const out = new Map();
245
+ for (const f of layout.drawHeaderFields) {
246
+ if (f.kind !== "uniform-ref")
247
+ continue;
248
+ const refExpr = loadHeaderRef(drawIdx, f.byteOffset, stride);
249
+ const value = layout.perInstanceUniforms.has(f.name)
250
+ ? loadInstanceByRef(refExpr, iidx, f.uniformWgslType ?? "")
251
+ : loadUniformByRef(refExpr, f.uniformWgslType ?? "");
252
+ out.set(f.name, value);
253
+ }
254
+ return out;
255
+ }
256
+ /**
257
+ * `(attributeName → loadAttributeByRef(loadHeaderRef(...), idx, wgslType))`
258
+ * for every attribute-ref field in `layout`. Used as the
259
+ * `vertex.inputs` argument to `Effect.substitute`.
260
+ */
261
+ function buildVertexAttributeMap(layout) {
262
+ const drawIdx = drawIdxExprFor(layout);
263
+ const iidx = readScope("Builtin", "instance_index", Tu32);
264
+ const vid = readScope("Builtin", "vertex_index", Tu32);
265
+ const stride = layout.strideU32;
266
+ const out = new Map();
267
+ for (const f of layout.drawHeaderFields) {
268
+ if (f.kind !== "attribute-ref")
269
+ continue;
270
+ const refExpr = loadHeaderRef(drawIdx, f.byteOffset, stride);
271
+ const value = layout.perInstanceAttributes.has(f.name)
272
+ ? loadAttributeByRef(refExpr, iidx, f.attributeWgslType ?? "")
273
+ : loadAttributeByRef(refExpr, vid, f.attributeWgslType ?? "");
274
+ out.set(f.name, value);
275
+ }
276
+ return out;
277
+ }
278
+ function mapVertexEntryBodies(m, fn) {
279
+ return mapStageEntryBodies(m, "vertex", fn);
280
+ }
281
+ function mapStageEntryBodies(m, stage, fn) {
282
+ const values = m.values.map((v) => {
283
+ if (v.kind !== "Entry" || v.entry.stage !== stage)
284
+ return v;
285
+ return { ...v, entry: { ...v.entry, body: fn(v.entry.body) } };
286
+ });
287
+ return { ...m, values };
288
+ }
289
+ /** Append `extras` to every VS entry's outputs and every FS entry's inputs. */
290
+ function addInterstageParams(m, extras) {
291
+ if (extras.length === 0)
292
+ return m;
293
+ const values = m.values.map((v) => {
294
+ if (v.kind !== "Entry")
295
+ return v;
296
+ if (v.entry.stage === "vertex") {
297
+ return { ...v, entry: { ...v.entry, outputs: [...v.entry.outputs, ...extras] } };
298
+ }
299
+ if (v.entry.stage === "fragment") {
300
+ return { ...v, entry: { ...v.entry, inputs: [...v.entry.inputs, ...extras] } };
301
+ }
302
+ return v;
303
+ });
304
+ return { ...m, values };
305
+ }
306
+ /**
307
+ * Prepend `stmts` to every VS entry's body. Flatten when the body is
308
+ * already a `Sequential` — CSE numbers `_cse0..N` per Sequential, so
309
+ * nested Sequentials produce colliding declarations in the emitted
310
+ * WGSL.
311
+ */
312
+ function prependVsBodyStmts(m, stmts) {
313
+ if (stmts.length === 0)
314
+ return m;
315
+ return mapVertexEntryBodies(m, body => {
316
+ if (body.kind === "Sequential") {
317
+ return { kind: "Sequential", body: [...stmts, ...body.body] };
318
+ }
319
+ return { kind: "Sequential", body: [...stmts, body] };
320
+ });
321
+ }
322
+ // ─── FS uniform threading ──────────────────────────────────────────
323
+ //
324
+ // FS can't access `instance_index` directly, so we thread the per-
325
+ // uniform refs through VsOut as flat-interpolated u32 varyings:
326
+ //
327
+ // - For each FS-used uniform `X` whose layout entry is uniform-ref:
328
+ // VS output: `_h_<X>Ref: u32 @interpolate(flat)`
329
+ // VS body: `WriteOutput("_h_<X>Ref", loadHeaderRef(drawIdx, X))`
330
+ // FS input: `_h_<X>Ref: u32 @interpolate(flat)`
331
+ // FS body: `ReadInput("Uniform", X)` →
332
+ // `loadUniformByRef(ReadInput("Input", "_h_<X>Ref"), …)`
333
+ //
334
+ // For per-instance uniforms (layout.perInstanceUniforms), iidx is also
335
+ // threaded as `_iidx: u32 @interpolate(flat)` and the FS uses
336
+ // `loadInstanceByRef` instead.
337
+ function wgslTypeFromHeapField(wgslType) {
338
+ switch (wgslType) {
339
+ case "mat4x4<f32>": return { kind: "Matrix", element: Tf32, rows: 4, cols: 4 };
340
+ case "vec4<f32>": return Tvec4;
341
+ case "vec3<f32>": return Tvec3;
342
+ case "vec2<f32>": return Tvec2;
343
+ case "f32": return Tf32;
344
+ case "u32": return Tu32;
345
+ default: throw new Error(`heapEffectIR: unknown uniform wgsl type '${wgslType}'`);
346
+ }
347
+ }
348
+ /**
349
+ * Family-member FS uniform rewrite (direct heap reads).
350
+ *
351
+ * The wrapper module threads `heap_drawIdx` and `instId` as plain u32
352
+ * flat varyings into the FS, then passes them to each per-effect FS
353
+ * helper. Inside the helper, every `ReadInput("Uniform", X)` is
354
+ * substituted with `loadUniformByRef(loadHeaderRef(heap_drawIdx, …), …)`
355
+ * (or `loadInstanceByRef(refExpr, instId, …)` for per-instance
356
+ * uniforms). No VS-side WriteOutput stmts and no `_h_*Ref` varyings —
357
+ * the carrier is the single `heap_drawIdx` u32.
358
+ *
359
+ * `heap_drawIdx` and `instId` are referenced as `Var` exprs; the post-
360
+ * emit `applyFamilyMemberFsShape` rewires the FS entry signature to
361
+ * surface them as plain u32 fn parameters.
362
+ */
363
+ function rewriteFsUniformsDirect(m, layout) {
364
+ const used = new Set();
365
+ for (const v of m.values) {
366
+ if (v.kind !== "Entry" || v.entry.stage !== "fragment")
367
+ continue;
368
+ for (const sn of readInputs(v.entry.body).values()) {
369
+ if (sn.scope === "Uniform")
370
+ used.add(sn.name);
371
+ }
372
+ }
373
+ if (used.size === 0)
374
+ return m;
375
+ const fieldByName = new Map(layout.drawHeaderFields.map(f => [f.name, f]));
376
+ const drawIdxExpr = { kind: "Var", var: { name: "heap_drawIdx", type: Tu32, mutable: false } };
377
+ const instIdExpr = { kind: "Var", var: { name: "instId", type: Tu32, mutable: false } };
378
+ const fsSubst = new Map();
379
+ for (const name of used) {
380
+ const f = fieldByName.get(name);
381
+ if (f === undefined || f.kind !== "uniform-ref")
382
+ continue;
383
+ const refExpr = loadHeaderRef(drawIdxExpr, f.byteOffset, layout.strideU32);
384
+ const value = layout.perInstanceUniforms.has(name)
385
+ ? loadInstanceByRef(refExpr, instIdExpr, f.uniformWgslType ?? "")
386
+ : loadUniformByRef(refExpr, f.uniformWgslType ?? "");
387
+ fsSubst.set(name, value);
388
+ }
389
+ return substituteInputsInStage(m, "fragment", "Uniform", n => fsSubst.get(n));
390
+ }
391
+ /**
392
+ * Family-member FS atlas rewrite (direct heap reads).
393
+ *
394
+ * Like `rewriteFsAtlasTextures` but instead of threading
395
+ * `_h_<name>{PageRef,FormatBits,Origin,Size}` flat varyings from VS,
396
+ * the substituted `atlasSample(pageRef, formatBits, origin, size, uv)`
397
+ * call reads each header field inline from `headersU32` via
398
+ * `heap_drawIdx`. The wrapper-supplied `heap_drawIdx` u32 fn parameter
399
+ * carries the per-draw header index.
400
+ */
401
+ function rewriteFsAtlasTexturesDirect(m, layout) {
402
+ if (layout.atlasTextureBindings.size === 0)
403
+ return m;
404
+ const used = new Set();
405
+ for (const v of m.values) {
406
+ if (v.kind !== "Entry" || v.entry.stage !== "fragment")
407
+ continue;
408
+ visitStmtExprs(v.entry.body, e => {
409
+ const hit = isAtlasSampleCall(e, layout.atlasTextureBindings);
410
+ if (hit !== null)
411
+ used.add(hit.name);
412
+ });
413
+ }
414
+ if (used.size === 0)
415
+ return m;
416
+ const fieldByAtlas = new Map();
417
+ for (const f of layout.drawHeaderFields) {
418
+ if (f.kind !== "texture-ref" || f.textureBindingName === undefined)
419
+ continue;
420
+ const sub = f.textureSub;
421
+ let inner = fieldByAtlas.get(f.textureBindingName);
422
+ if (inner === undefined) {
423
+ inner = new Map();
424
+ fieldByAtlas.set(f.textureBindingName, inner);
425
+ }
426
+ inner.set(sub, byteOffsetOf(f.byteOffset));
427
+ }
428
+ const drawIdxExpr = { kind: "Var", var: { name: "heap_drawIdx", type: Tu32, mutable: false } };
429
+ const stride = layout.strideU32;
430
+ const headerU32At = (offU32) => item(headersU32, add(mul(drawIdxExpr, constU32(stride), Tu32), constU32(offU32), Tu32), Tu32);
431
+ const headerF32At = (offU32) => ({
432
+ kind: "CallIntrinsic",
433
+ op: { name: "bitcast<f32>", returnTypeOf: () => Tf32, pure: true,
434
+ emit: { glsl: "intBitsToFloat", wgsl: "bitcast<f32>" } },
435
+ args: [headerU32At(offU32)],
436
+ type: Tf32,
437
+ });
438
+ return mapStageEntryBodies(m, "fragment", body => mapStmt(body, {
439
+ expr: e => mapExpr(e, sub => {
440
+ const hit = isAtlasSampleCall(sub, layout.atlasTextureBindings);
441
+ if (hit === null)
442
+ return sub;
443
+ const fields = fieldByAtlas.get(hit.name);
444
+ const pageRefOff = fields.get("pageRef").u32;
445
+ const formatBitsOff = fields.get("formatBits").u32;
446
+ const originOff = fields.get("origin").u32;
447
+ const sizeOff = fields.get("size").u32;
448
+ const args = [
449
+ headerU32At(pageRefOff),
450
+ headerU32At(formatBitsOff),
451
+ newVec([headerF32At(originOff), headerF32At(originOff + 1)], Tvec2),
452
+ newVec([headerF32At(sizeOff), headerF32At(sizeOff + 1)], Tvec2),
453
+ hit.uv,
454
+ ];
455
+ return { kind: "CallIntrinsic", op: ATLAS_SAMPLE_INTRINSIC, args, type: Tvec4 };
456
+ }),
457
+ }));
458
+ }
459
+ /**
460
+ * FS uniform threading: thread `_h_<name>Ref` (and `_iidx`, when
461
+ * needed) varyings from VS to FS, and rewrite FS `ReadInput("Uniform",
462
+ * X)` into a heap-load through the threaded ref.
463
+ *
464
+ * This pass stays at Module-level rather than going through the
465
+ * Effect.substitute API for two reasons:
466
+ *
467
+ * 1. The VS side adds *Stmts* (WriteOutput) and entry parameters,
468
+ * not Expr substitutions — outside the substitute API's scope.
469
+ * 2. Even the FS Expr substitution can't run pre-merge: the
470
+ * frontend hides `return { outColor: ... }` field exprs in a
471
+ * `_record` carrier that mapExpr doesn't traverse, so any
472
+ * uniform read inside the returned record is invisible until
473
+ * `liftReturns` rewrites it into explicit WriteOutput stmts.
474
+ * We run after liftReturns; the matching VS-side write injection
475
+ * lives in the same pass to keep the `_h_<X>Ref` names paired.
476
+ */
477
+ function rewriteFsUniforms(m, layout) {
478
+ // Scan FS bodies for uniform-scope reads.
479
+ const used = new Set();
480
+ for (const v of m.values) {
481
+ if (v.kind !== "Entry" || v.entry.stage !== "fragment")
482
+ continue;
483
+ for (const sn of readInputs(v.entry.body).values()) {
484
+ if (sn.scope === "Uniform")
485
+ used.add(sn.name);
486
+ }
487
+ }
488
+ if (used.size === 0)
489
+ return m;
490
+ const fieldByName = new Map(layout.drawHeaderFields.map(f => [f.name, f]));
491
+ const drawIdxExpr = drawIdxExprFor(layout);
492
+ const threadParams = [];
493
+ const vsWrites = [];
494
+ const fsSubst = new Map();
495
+ for (const name of used) {
496
+ const f = fieldByName.get(name);
497
+ if (f === undefined || f.kind !== "uniform-ref")
498
+ continue;
499
+ const refParamName = `_h_${name}Ref`;
500
+ const refExprWriter = loadHeaderRef(drawIdxExpr, f.byteOffset, layout.strideU32);
501
+ threadParams.push({
502
+ name: refParamName, type: Tu32, semantic: refParamName,
503
+ decorations: [{ kind: "Interpolation", mode: "flat" }],
504
+ });
505
+ vsWrites.push({
506
+ kind: "WriteOutput", name: refParamName,
507
+ value: { kind: "Expr", value: refExprWriter },
508
+ });
509
+ const refReadFs = readScope("Input", refParamName, Tu32);
510
+ const value = layout.perInstanceUniforms.has(name)
511
+ ? loadInstanceByRef(refReadFs, readScope("Input", "_iidx", Tu32), f.uniformWgslType ?? "")
512
+ : loadUniformByRef(refReadFs, f.uniformWgslType ?? "");
513
+ fsSubst.set(name, value);
514
+ }
515
+ // Per-instance: also thread iidx if any FS-used uniform is per-instance.
516
+ const fsHasPerInstance = [...used].some(n => layout.perInstanceUniforms.has(n));
517
+ if (fsHasPerInstance) {
518
+ threadParams.push({
519
+ name: "_iidx", type: Tu32, semantic: "_iidx",
520
+ decorations: [{ kind: "Interpolation", mode: "flat" }],
521
+ });
522
+ vsWrites.push({
523
+ kind: "WriteOutput", name: "_iidx",
524
+ value: { kind: "Expr", value: readScope("Builtin", "instance_index", Tu32) },
525
+ });
526
+ }
527
+ let out = m;
528
+ out = addInterstageParams(out, threadParams);
529
+ out = prependVsBodyStmts(out, vsWrites);
530
+ out = substituteInputsInStage(out, "fragment", "Uniform", n => fsSubst.get(n));
531
+ return out;
532
+ }
533
+ /**
534
+ * Per-instance uniform load: indexes into a packed array allocation
535
+ * by `iidx` (instance index in [0, instanceCount-1]).
536
+ */
537
+ function loadInstanceByRef(refIdent, iidx, wgslType) {
538
+ const baseV4 = div(add(refIdent, constU32(16), Tu32), constU32(16), Tu32);
539
+ const baseF32 = div(add(refIdent, constU32(16), Tu32), constU32(4), Tu32);
540
+ switch (wgslType) {
541
+ case "mat4x4<f32>": {
542
+ const start = add(baseV4, mul(iidx, constU32(4), Tu32), Tu32);
543
+ const cols = [];
544
+ for (let i = 0; i < 4; i++) {
545
+ cols.push(item(heapV4f, i === 0 ? start : add(start, constU32(i), Tu32), Tvec4));
546
+ }
547
+ return matFromRows(cols, { kind: "Matrix", element: Tf32, rows: 4, cols: 4 });
548
+ }
549
+ case "vec4<f32>":
550
+ return item(heapV4f, add(baseV4, iidx, Tu32), Tvec4);
551
+ case "vec3<f32>": {
552
+ const off = mul(iidx, constU32(3), Tu32);
553
+ const base = add(baseF32, off, Tu32);
554
+ return newVec([
555
+ item(heapF32, base, Tf32),
556
+ item(heapF32, add(base, constU32(1), Tu32), Tf32),
557
+ item(heapF32, add(base, constU32(2), Tu32), Tf32),
558
+ ], Tvec3);
559
+ }
560
+ case "vec2<f32>": {
561
+ const off = mul(iidx, constU32(2), Tu32);
562
+ const base = add(baseF32, off, Tu32);
563
+ return newVec([
564
+ item(heapF32, base, Tf32),
565
+ item(heapF32, add(base, constU32(1), Tu32), Tf32),
566
+ ], Tvec2);
567
+ }
568
+ case "f32":
569
+ return item(heapF32, add(baseF32, iidx, Tu32), Tf32);
570
+ case "u32":
571
+ return item(heapU32, add(baseF32, iidx, Tu32), Tu32);
572
+ default:
573
+ throw new Error(`heapEffectIR: no IR loader for per-instance uniform type '${wgslType}'`);
574
+ }
575
+ }
576
+ // `wgslTypeFromHeapField` will be used once we propagate proper types
577
+ // onto the synthesised threading params; for now we only emit u32-typed
578
+ // refs, so the type lookup isn't called from the generated IR. Keep
579
+ // the import alive.
580
+ void wgslTypeFromHeapField;
581
+ // ─── FS atlas-texture rewrite (textureSample → atlasSample) ─────────
582
+ //
583
+ // For each atlas-routed binding actually called from FS as
584
+ // `textureSample(<name>, smp, uv)`:
585
+ // - Thread 4 flat-interpolated VsOut varyings: `_h_<name>PageRef`,
586
+ // `_h_<name>FormatBits`, `_h_<name>Origin`, `_h_<name>Size`.
587
+ // - VS body writes them from the drawHeader (`headersU32` reads,
588
+ // bitcast for the f32 fields since the headers buffer is u32).
589
+ // - FS substitutes the textureSample call with a `CallIntrinsic`
590
+ // emitting `atlasSample(pageRef, formatBits, origin, size, uv)`.
591
+ //
592
+ // The `atlasSample` helper is declared by the prelude string the post-
593
+ // emit injector prepends to the FS source — the IR doesn't need to
594
+ // know about its body.
595
+ /** Synthetic `atlasSample` intrinsic for the IR's CallIntrinsic emit hook. */
596
+ const ATLAS_SAMPLE_INTRINSIC = {
597
+ name: "atlasSample",
598
+ returnTypeOf: () => Tvec4,
599
+ pure: true,
600
+ emit: { glsl: "atlasSample", wgsl: "atlasSample" },
601
+ };
602
+ function isAtlasSampleCall(e, atlasNames) {
603
+ if (e.kind !== "CallIntrinsic")
604
+ return null;
605
+ if (e.op.emit.wgsl !== "textureSample")
606
+ return null;
607
+ if (e.args.length < 2)
608
+ return null;
609
+ const tex = e.args[0];
610
+ // Pre-legaliseTypes the sampler arg is a Var or ReadInput referencing
611
+ // the Sampler ValueDef by its schema name. Match either shape.
612
+ let name;
613
+ if (tex.kind === "ReadInput" && tex.scope === "Uniform")
614
+ name = tex.name;
615
+ else if (tex.kind === "Var")
616
+ name = tex.var.name;
617
+ if (name === undefined)
618
+ return null;
619
+ // The schema (extracted post-legaliseTypes) names the binding
620
+ // `<sampler>_view`; the IR pre-pass sees the user-written sampler
621
+ // name. Match either form so the rewrite fires regardless of which
622
+ // shape the layout's atlas-binding set carries.
623
+ const matched = atlasNames.has(name) ? name :
624
+ atlasNames.has(`${name}_view`) ? `${name}_view` :
625
+ undefined;
626
+ if (matched === undefined)
627
+ return null;
628
+ // texture(sampler, uv) parses as a 2-arg call (no separate sampler/
629
+ // texture pair until splitWgslSamplers runs). The uv is args[1].
630
+ const uv = e.args[1];
631
+ return { name: matched, uv };
632
+ }
633
+ function rewriteFsAtlasTextures(m, layout) {
634
+ if (layout.atlasTextureBindings.size === 0)
635
+ return m;
636
+ // Scan FS bodies to find which atlas bindings are actually used.
637
+ const used = new Set();
638
+ for (const v of m.values) {
639
+ if (v.kind !== "Entry" || v.entry.stage !== "fragment")
640
+ continue;
641
+ visitStmtExprs(v.entry.body, e => {
642
+ const hit = isAtlasSampleCall(e, layout.atlasTextureBindings);
643
+ if (hit !== null)
644
+ used.add(hit.name);
645
+ });
646
+ }
647
+ if (used.size === 0)
648
+ return m;
649
+ const fieldByAtlas = new Map();
650
+ for (const f of layout.drawHeaderFields) {
651
+ if (f.kind !== "texture-ref" || f.textureBindingName === undefined)
652
+ continue;
653
+ const sub = f.textureSub;
654
+ let inner = fieldByAtlas.get(f.textureBindingName);
655
+ if (inner === undefined) {
656
+ inner = new Map();
657
+ fieldByAtlas.set(f.textureBindingName, inner);
658
+ }
659
+ inner.set(sub, byteOffsetOf(f.byteOffset));
660
+ }
661
+ const drawIdxExpr = drawIdxExprFor(layout);
662
+ const threadParams = [];
663
+ const vsWrites = [];
664
+ for (const name of used) {
665
+ const v = atlasVaryingNames(name);
666
+ const fields = fieldByAtlas.get(name);
667
+ const stride = layout.strideU32;
668
+ const headerU32At = (offU32) => item(headersU32, add(mul(drawIdxExpr, constU32(stride), Tu32), constU32(offU32), Tu32), Tu32);
669
+ const headerF32At = (offU32) => ({
670
+ kind: "CallIntrinsic",
671
+ op: { name: "bitcast<f32>", returnTypeOf: () => Tf32, pure: true,
672
+ emit: { glsl: "intBitsToFloat", wgsl: "bitcast<f32>" } },
673
+ args: [headerU32At(offU32)],
674
+ type: Tf32,
675
+ });
676
+ const pageRefOff = fields.get("pageRef").u32;
677
+ const formatBitsOff = fields.get("formatBits").u32;
678
+ const originOff = fields.get("origin").u32;
679
+ const sizeOff = fields.get("size").u32;
680
+ threadParams.push({
681
+ name: v.pageRef, type: Tu32, semantic: v.pageRef,
682
+ decorations: [{ kind: "Interpolation", mode: "flat" }],
683
+ });
684
+ threadParams.push({
685
+ name: v.formatBits, type: Tu32, semantic: v.formatBits,
686
+ decorations: [{ kind: "Interpolation", mode: "flat" }],
687
+ });
688
+ threadParams.push({
689
+ name: v.origin, type: Tvec2, semantic: v.origin,
690
+ decorations: [{ kind: "Interpolation", mode: "flat" }],
691
+ });
692
+ threadParams.push({
693
+ name: v.size, type: Tvec2, semantic: v.size,
694
+ decorations: [{ kind: "Interpolation", mode: "flat" }],
695
+ });
696
+ vsWrites.push({ kind: "WriteOutput", name: v.pageRef,
697
+ value: { kind: "Expr", value: headerU32At(pageRefOff) } });
698
+ vsWrites.push({ kind: "WriteOutput", name: v.formatBits,
699
+ value: { kind: "Expr", value: headerU32At(formatBitsOff) } });
700
+ vsWrites.push({ kind: "WriteOutput", name: v.origin,
701
+ value: { kind: "Expr", value: newVec([headerF32At(originOff), headerF32At(originOff + 1)], Tvec2) } });
702
+ vsWrites.push({ kind: "WriteOutput", name: v.size,
703
+ value: { kind: "Expr", value: newVec([headerF32At(sizeOff), headerF32At(sizeOff + 1)], Tvec2) } });
704
+ }
705
+ // Rewrite FS textureSample(...) → atlasSample(...) for atlas-routed names.
706
+ let out = m;
707
+ out = addInterstageParams(out, threadParams);
708
+ out = prependVsBodyStmts(out, vsWrites);
709
+ out = mapStageEntryBodies(out, "fragment", body => mapStmt(body, {
710
+ expr: e => mapExpr(e, sub => {
711
+ const hit = isAtlasSampleCall(sub, layout.atlasTextureBindings);
712
+ if (hit === null)
713
+ return sub;
714
+ const v = atlasVaryingNames(hit.name);
715
+ const args = [
716
+ readScope("Input", v.pageRef, Tu32),
717
+ readScope("Input", v.formatBits, Tu32),
718
+ readScope("Input", v.origin, Tvec2),
719
+ readScope("Input", v.size, Tvec2),
720
+ hit.uv,
721
+ ];
722
+ return { kind: "CallIntrinsic", op: ATLAS_SAMPLE_INTRINSIC, args, type: Tvec4 };
723
+ }),
724
+ }));
725
+ return out;
726
+ }
727
+ /** Visit every Expr inside a Stmt tree, read-only. */
728
+ function visitStmtExprs(s, fn) {
729
+ mapStmt(s, {
730
+ expr: e => mapExpr(e, sub => { fn(sub); return sub; }),
731
+ });
732
+ }
733
+ function byteOffsetOf(byteOffset) {
734
+ return { u32: byteOffset / 4 };
735
+ }
736
+ export function compileHeapEffectIR(userEffect, layout, compileOptions, mode = "standalone") {
737
+ // VS-side input/uniform substitution: pure read-rewrites, expressed
738
+ // via the Effect.substitute API.
739
+ const heapShaped = userEffect.substitute({
740
+ vertex: {
741
+ inputs: buildVertexAttributeMap(layout),
742
+ uniforms: buildVertexUniformMap(layout),
743
+ },
744
+ });
745
+ // Build a single Module from all of the effect's stages by
746
+ // composing them. Then apply the remaining heap rewrites to that
747
+ // combined module. The composed module preserves stage entry
748
+ // boundaries.
749
+ let combined = mergeStages(heapShaped);
750
+ // Lift `return { ... }` into explicit WriteOutput stmts so the FS
751
+ // uniform pass below can see (and rewrite) ReadInputs that feed the
752
+ // returned record fields. Without this, ObjectLiteral exprs hide
753
+ // their fields on a `_record` carrier that mapExpr doesn't traverse.
754
+ combined = liftReturns(combined);
755
+ combined = injectVsBuiltins(combined);
756
+ // FS uniform threading must run BEFORE atlas FS rewriting in source
757
+ // order? No — order historically: atlas first, then uniforms. Both
758
+ // add interstage params and VS WriteOutput stmts; their write sets
759
+ // are disjoint, so the order is interchangeable in practice. Keep
760
+ // the original order for byte-for-byte equivalence.
761
+ if (mode === "family-member") {
762
+ // Direct heap-read FS path: substitute uniforms / atlas samples
763
+ // directly using the wrapper-supplied `heap_drawIdx` / `instId`
764
+ // u32 fn parameters. No VS→FS varying threading on the heap side.
765
+ combined = rewriteFsAtlasTexturesDirect(combined, layout);
766
+ combined = rewriteFsUniformsDirect(combined, layout);
767
+ }
768
+ else {
769
+ combined = rewriteFsAtlasTextures(combined, layout);
770
+ combined = rewriteFsUniforms(combined, layout);
771
+ }
772
+ // Add heap storage buffers as bindings.
773
+ combined = { ...combined, values: [...heapStorageBufferDecls(), ...combined.values] };
774
+ // Skip frontend resolveHoles / liftReturns since they expect the
775
+ // template not to have been pre-modified. Compile via compileModule
776
+ // directly which still runs assignLocations + linkCrossStage + DCE +
777
+ // emit. (`makeStage` wrapper goes through Effect.compile which calls
778
+ // compileModule too.)
779
+ const compiled = compileModule(combined, compileOptions);
780
+ const vsStage = compiled.stages.find(s => s.stage === "vertex");
781
+ const fsStage = compiled.stages.find(s => s.stage === "fragment");
782
+ let vsSrc = vsStage?.source ?? "";
783
+ let fsSrc = fsStage?.source ?? "";
784
+ if (vsSrc.length > 0) {
785
+ vsSrc = mode === "family-member"
786
+ ? applyFamilyMemberShape(vsSrc)
787
+ : applyMegacallToEmittedVs(vsSrc);
788
+ }
789
+ if (mode === "family-member" && fsSrc.length > 0) {
790
+ fsSrc = applyFamilyMemberFsShape(fsSrc);
791
+ }
792
+ if (layout.atlasTextureBindings.size > 0) {
793
+ // Strip user-emitted texture/sampler binding decls for atlas-routed
794
+ // names — the IR's FS still emits `var <name>: texture_2d<f32>;` even
795
+ // though we've replaced every textureSample call referencing it.
796
+ // The render BGL has no slot for these bindings.
797
+ if (vsSrc.length > 0)
798
+ vsSrc = stripAtlasTextureSamplerDecls(vsSrc, layout.atlasTextureBindings);
799
+ if (fsSrc.length > 0) {
800
+ fsSrc = stripAtlasTextureSamplerDecls(fsSrc, layout.atlasTextureBindings);
801
+ fsSrc = prependAtlasPrelude(fsSrc);
802
+ }
803
+ }
804
+ return {
805
+ vs: vsSrc,
806
+ fs: fsSrc,
807
+ vsEntry: vsStage?.entryName ?? "vs",
808
+ fsEntry: fsStage?.entryName ?? "fs",
809
+ preludeWgsl: "",
810
+ };
811
+ }
812
+ /**
813
+ * Drop `@group(N) @binding(M) var <name>: texture_*<...>;` and the
814
+ * matching sampler decl(s) for atlas-routed binding names. We keep
815
+ * other texture/sampler decls intact since they belong to the standalone
816
+ * texture path.
817
+ *
818
+ * Sampler-name detection: WGSL emit pairs samplers with their textures
819
+ * positionally in the IR; the user's WGSL can name them anything. To
820
+ * be safe we also strip any sampler decl that is *only* referenced by
821
+ * a now-rewritten textureSample call. Rather than do data-flow, we
822
+ * pattern-match: if the FS has zero remaining textureSample calls AND
823
+ * a single sampler decl, drop it. Otherwise leave it. Practically the
824
+ * v1 surface has 0..1 atlas-routed binding per shader, so this is
825
+ * sufficient.
826
+ */
827
+ function stripAtlasTextureSamplerDecls(src, atlasNames) {
828
+ let out = src;
829
+ for (const name of atlasNames) {
830
+ const re = new RegExp(`@group\\(\\d+\\)\\s*@binding\\(\\d+\\)\\s*var\\s+${name}\\s*:\\s*texture_\\w+(?:<[^>]*>)?\\s*;\\s*`, "g");
831
+ out = out.replace(re, "");
832
+ }
833
+ // If textureSample no longer appears anywhere, drop the (single)
834
+ // sampler decl too. Conservative: we leave samplers alone if they
835
+ // could still be in use.
836
+ if (!/textureSample\s*\(/.test(out)) {
837
+ out = out.replace(/@group\(\d+\)\s*@binding\(\d+\)\s*var\s+\w+\s*:\s*sampler(?:_comparison)?\s*;\s*/g, "");
838
+ }
839
+ return out;
840
+ }
841
+ /**
842
+ * Prepend the atlas texture-array decls + `atlasSample` helper to
843
+ * the emitted FS source. The helper signatures match the post-
844
+ * rewrite `atlasSample(pageRef, formatBits, origin, size, uv)`
845
+ * calls the IR pass produces.
846
+ */
847
+ function prependAtlasPrelude(fs) {
848
+ // Delegate to the VS prelude generator — both stages need the same
849
+ // atlas-sample helper, and consolidating prevents the two copies
850
+ // from drifting.
851
+ return generateAtlasPrelude() + fs;
852
+ }
853
+ /**
854
+ * Megacall post-processing for the IR-emitted VS source. The IR uses
855
+ * `@builtin(vertex_index) vertex_index` + `@builtin(instance_index)
856
+ * instance_index` as the param names; rather than rewrite the IR
857
+ * (which would require building the binary-search loop in IR), we
858
+ * post-process the emitted WGSL: strip instance_index from the
859
+ * @vertex signature, rename vertex_index to emitIdx, and inject a
860
+ * prelude that defines `instance_index` + `vertex_index` as locals
861
+ * via the drawTable binary search so existing body references keep
862
+ * resolving. Also injects the drawTable/indexStorage binding decls.
863
+ */
864
+ function applyMegacallToEmittedVs(vs) {
865
+ let s = vs;
866
+ // Megacall storage bindings only — the shared values (`heap_drawIdx`,
867
+ // `instId`, `vid`) are declared as `let` locals in the @vertex body
868
+ // and threaded as parameters into any helper fn that references them.
869
+ // No module-scope `var<private>` (Safari/WebKit rejects reading
870
+ // those from helper fn bodies).
871
+ const decl = `\n@group(0) @binding(4) var<storage, read> drawTable: array<u32>;\n@group(0) @binding(5) var<storage, read> indexStorage: array<u32>;\n@group(0) @binding(6) var<storage, read> firstDrawInTile: array<u32>;\n`;
872
+ // Locate the @vertex fn header, then balance parens manually since
873
+ // params can carry `@builtin(name)` decorations (regex `[^)]*` would
874
+ // halt at the first inner `)`).
875
+ const startRe = /@vertex\s+fn\s+(\w+)\s*\(/;
876
+ const startMatch = s.match(startRe);
877
+ if (startMatch === null)
878
+ return s;
879
+ const fnName = startMatch[1];
880
+ const paramOpen = startMatch.index + startMatch[0].length;
881
+ let depth = 1;
882
+ let i = paramOpen;
883
+ for (; i < s.length && depth > 0; i++) {
884
+ const c = s[i];
885
+ if (c === "(")
886
+ depth++;
887
+ else if (c === ")")
888
+ depth--;
889
+ }
890
+ if (depth !== 0)
891
+ return s;
892
+ const paramList = s.slice(paramOpen, i - 1);
893
+ // After the closing `)` we expect `\s*->\s*<retType>\s*\{`.
894
+ const tailRe = /\s*->\s*([\w<>,\s]+?)\s*\{/y;
895
+ tailRe.lastIndex = i;
896
+ const tailMatch = tailRe.exec(s);
897
+ if (tailMatch === null)
898
+ return s;
899
+ const retType = tailMatch[1].trim();
900
+ const headerEnd = tailRe.lastIndex;
901
+ const headerStart = startMatch.index;
902
+ const params = paramList.split(",").map(p => p.trim()).filter(p => p.length > 0);
903
+ const kept = [];
904
+ for (const p of params) {
905
+ if (/@builtin\(\s*instance_index\s*\)/.test(p))
906
+ continue;
907
+ if (/@builtin\(\s*vertex_index\s*\)/.test(p)) {
908
+ kept.push("@builtin(vertex_index) emitIdx: u32");
909
+ continue;
910
+ }
911
+ kept.push(p);
912
+ }
913
+ // megacallSearchPrelude exposes `heap_drawIdx`, `instId`, `vid`. The
914
+ // IR rewrites bind header-selector reads to `heap_drawIdx` directly
915
+ // (loadHeaderRef takes an Expr arg); the remaining bindings expose
916
+ // `instance_index` (= per-RO instance idx) and `vertex_index` for
917
+ // user code that reads them by builtin.
918
+ const newHeader = `@vertex fn ${fnName}(${kept.join(", ")}) -> ${retType} {\n${megacallSearchPrelude()} let instance_index: u32 = instId;\n let vertex_index: u32 = vid;\n`;
919
+ s = s.slice(0, headerStart) + newHeader + s.slice(headerEnd);
920
+ s = threadMegacallParamsThroughHelpers(s);
921
+ return decl + s;
922
+ }
923
+ /**
924
+ * Family-member shape: the wrapper performs the megacall binary search
925
+ * and invokes this VS as a regular function, passing `heap_drawIdx`,
926
+ * `instId`, and `vid` as plain `u32` parameters. We rewrite the emitted
927
+ * @vertex signature so its first three params are exactly those names
928
+ * (no `@builtin` decorations), and inject local aliases so any body
929
+ * references to `vertex_index` / `instance_index` (the names produced
930
+ * by IR `Builtin` reads + `injectVsBuiltins`) resolve to the new
931
+ * parameters.
932
+ *
933
+ * The megacall storage-buffer bindings (drawTable / indexStorage /
934
+ * firstDrawInTile) are NOT emitted — the wrapper module owns them.
935
+ *
936
+ * `threadMegacallParamsThroughHelpers` is run unchanged: helper fns
937
+ * extracted by composeStages may still reference `heap_drawIdx`,
938
+ * `instId`, `vid` after CSE; threading the params is a pure text walk
939
+ * and does not depend on the entry shape.
940
+ */
941
+ function applyFamilyMemberShape(vs) {
942
+ let s = vs;
943
+ // Locate the @vertex fn header, then balance parens manually since
944
+ // params can carry `@builtin(name)` decorations.
945
+ const startRe = /@vertex\s+fn\s+(\w+)\s*\(/;
946
+ const startMatch = s.match(startRe);
947
+ if (startMatch === null)
948
+ return s;
949
+ const fnName = startMatch[1];
950
+ const paramOpen = startMatch.index + startMatch[0].length;
951
+ let depth = 1;
952
+ let i = paramOpen;
953
+ for (; i < s.length && depth > 0; i++) {
954
+ const c = s[i];
955
+ if (c === "(")
956
+ depth++;
957
+ else if (c === ")")
958
+ depth--;
959
+ }
960
+ if (depth !== 0)
961
+ return s;
962
+ const paramList = s.slice(paramOpen, i - 1);
963
+ const tailRe = /\s*->\s*([\w<>,\s]+?)\s*\{/y;
964
+ tailRe.lastIndex = i;
965
+ const tailMatch = tailRe.exec(s);
966
+ if (tailMatch === null)
967
+ return s;
968
+ const retType = tailMatch[1].trim();
969
+ const headerEnd = tailRe.lastIndex;
970
+ const headerStart = startMatch.index;
971
+ // Drop ALL @vertex fn params: the megacall builtins
972
+ // (instance_index, vertex_index) are not needed because we re-bind
973
+ // them from incoming u32 args below, and any @location(...) attribute
974
+ // params have already had their body reads substituted with
975
+ // heap-load expressions by `substituteInputsInStage` — the params
976
+ // themselves are dead. The wrapper calls the helper with exactly
977
+ // three megacall args.
978
+ const newParamList = ["heap_drawIdx: u32", "instId: u32", "vid: u32"].join(", ");
979
+ void paramList;
980
+ // Body aliases: IR-emitted body code reads `vertex_index` and
981
+ // `instance_index` (the names of the @builtin params standalone mode
982
+ // adds). Re-bind those names to the incoming `vid` / `instId`.
983
+ const bodyAliases = ` let instance_index: u32 = instId;\n` +
984
+ ` let vertex_index: u32 = vid;\n`;
985
+ const newHeader = `@vertex fn ${fnName}(${newParamList}) -> ${retType} {\n${bodyAliases}`;
986
+ s = s.slice(0, headerStart) + newHeader + s.slice(headerEnd);
987
+ s = threadMegacallParamsThroughHelpers(s);
988
+ return s;
989
+ }
990
+ /**
991
+ * Family-member FS shape: rewrite the emitted `@fragment fn` signature
992
+ * to take `heap_drawIdx: u32, instId: u32` as additional plain u32 fn
993
+ * parameters (after the existing FsIn struct param). The body uses
994
+ * `heap_drawIdx` / `instId` as `Var` reads — the wrapper passes them
995
+ * in from flat-interpolated u32 inputs on the family FsIn.
996
+ *
997
+ * `threadMegacallParamsThroughHelpers` is run as well in case any FS
998
+ * helper fn extracted by composeStages references those identifiers.
999
+ */
1000
+ function applyFamilyMemberFsShape(fs) {
1001
+ let s = fs;
1002
+ const startRe = /@fragment\s+fn\s+(\w+)\s*\(/;
1003
+ const startMatch = s.match(startRe);
1004
+ if (startMatch === null)
1005
+ return s;
1006
+ const fnName = startMatch[1];
1007
+ const paramOpen = startMatch.index + startMatch[0].length;
1008
+ let depth = 1;
1009
+ let i = paramOpen;
1010
+ for (; i < s.length && depth > 0; i++) {
1011
+ const c = s[i];
1012
+ if (c === "(")
1013
+ depth++;
1014
+ else if (c === ")")
1015
+ depth--;
1016
+ }
1017
+ if (depth !== 0)
1018
+ return s;
1019
+ const paramList = s.slice(paramOpen, i - 1);
1020
+ const tailRe = /\s*->\s*([\w<>,\s]+?)\s*\{/y;
1021
+ tailRe.lastIndex = i;
1022
+ const tailMatch = tailRe.exec(s);
1023
+ if (tailMatch === null)
1024
+ return s;
1025
+ const retType = tailMatch[1].trim();
1026
+ const headerEnd = tailRe.lastIndex;
1027
+ const headerStart = startMatch.index;
1028
+ const existing = paramList.split(",").map(p => p.trim()).filter(p => p.length > 0);
1029
+ const newParamList = [...existing, "heap_drawIdx: u32", "instId: u32"].join(", ");
1030
+ const newHeader = `@fragment fn ${fnName}(${newParamList}) -> ${retType} {\n`;
1031
+ s = s.slice(0, headerStart) + newHeader + s.slice(headerEnd);
1032
+ s = threadMegacallParamsThroughHelpers(s);
1033
+ return s;
1034
+ }
1035
+ /**
1036
+ * Post-emit text rewrite that threads `heap_drawIdx`, `instId`, and
1037
+ * `vid` through any helper function whose body references them. The
1038
+ * IR's composeStages pass extracts same-stage entries into helper fns
1039
+ * (`fn _<name>(s_in: <State>) -> <State>`) called from the wrapper
1040
+ * `@vertex` fn. Those helpers can carry CSE-extracted expressions that
1041
+ * reference `heap_drawIdx`/`instId`/`vid` — declared as locals in the
1042
+ * wrapper, those identifiers are out of scope inside helpers, so we
1043
+ * append them as `u32` parameters and pass them at every call site.
1044
+ *
1045
+ * Module-scope `var<private>` would also work but Safari/WebKit's WGSL
1046
+ * parser rejects helper-fn reads of module-scope private vars in some
1047
+ * configurations; explicit parameter passing is portable across all
1048
+ * conforming WGSL implementations.
1049
+ */
1050
+ function threadMegacallParamsThroughHelpers(src) {
1051
+ const idents = ["heap_drawIdx", "instId", "vid"];
1052
+ // Find every `fn _<name>(...) -> <ret> { ... }` declaration. We only
1053
+ // touch fns whose name starts with `_` (the convention used by
1054
+ // extractFusedEntry's helpers); user code with a leading-underscore
1055
+ // name is not expected in IR-emitted WGSL.
1056
+ const fnRe = /\bfn\s+(_\w+)\s*\(/g;
1057
+ const edits = [];
1058
+ let m;
1059
+ while ((m = fnRe.exec(src)) !== null) {
1060
+ const name = m[1];
1061
+ const paramOpen = m.index + m[0].length; // position after `(`
1062
+ // Balance parens to find the matching `)`.
1063
+ let depth = 1;
1064
+ let i = paramOpen;
1065
+ for (; i < src.length && depth > 0; i++) {
1066
+ const c = src[i];
1067
+ if (c === "(")
1068
+ depth++;
1069
+ else if (c === ")")
1070
+ depth--;
1071
+ }
1072
+ if (depth !== 0)
1073
+ continue;
1074
+ const paramClose = i - 1; // position of the matching `)`
1075
+ // Find the function body `{ ... }` after the return type.
1076
+ const afterParens = src.slice(i);
1077
+ const braceIdx = afterParens.indexOf("{");
1078
+ if (braceIdx < 0)
1079
+ continue;
1080
+ const bodyOpen = i + braceIdx;
1081
+ let bdepth = 1;
1082
+ let j = bodyOpen + 1;
1083
+ for (; j < src.length && bdepth > 0; j++) {
1084
+ const c = src[j];
1085
+ if (c === "{")
1086
+ bdepth++;
1087
+ else if (c === "}")
1088
+ bdepth--;
1089
+ }
1090
+ if (bdepth !== 0)
1091
+ continue;
1092
+ const bodyClose = j - 1;
1093
+ const body = src.slice(bodyOpen + 1, bodyClose);
1094
+ const needed = idents.filter(id => new RegExp(`\\b${id}\\b`).test(body));
1095
+ if (needed.length === 0)
1096
+ continue;
1097
+ edits.push({ name, needed, paramOpen, paramClose });
1098
+ }
1099
+ if (edits.length === 0)
1100
+ return src;
1101
+ // Apply param-list edits from the back so earlier offsets stay valid.
1102
+ const helperNeeds = new Map();
1103
+ let out = src;
1104
+ for (let k = edits.length - 1; k >= 0; k--) {
1105
+ const e = edits[k];
1106
+ helperNeeds.set(e.name, e.needed);
1107
+ const existing = out.slice(e.paramOpen, e.paramClose).trim();
1108
+ const extra = e.needed.map(id => `${id}: u32`).join(", ");
1109
+ const newParams = existing.length === 0 ? extra : `${existing}, ${extra}`;
1110
+ out = out.slice(0, e.paramOpen) + newParams + out.slice(e.paramClose);
1111
+ }
1112
+ // Rewrite call sites for each touched helper. Match `<name>(<args>)`
1113
+ // and append the corresponding identifiers. Match the bare name so
1114
+ // we don't accidentally hit a substring inside another identifier.
1115
+ for (const [name, needed] of helperNeeds) {
1116
+ const callRe = new RegExp(`\\b${name}\\s*\\(`, "g");
1117
+ let result = "";
1118
+ let lastIdx = 0;
1119
+ let cm;
1120
+ while ((cm = callRe.exec(out)) !== null) {
1121
+ const callOpen = cm.index + cm[0].length;
1122
+ // Skip if this is the `fn <name>(` declaration itself (preceded
1123
+ // by `fn`).
1124
+ const before = out.slice(0, cm.index).trimEnd();
1125
+ if (/\bfn$/.test(before))
1126
+ continue;
1127
+ // Balance parens to find matching `)`.
1128
+ let depth = 1;
1129
+ let p = callOpen;
1130
+ for (; p < out.length && depth > 0; p++) {
1131
+ const c = out[p];
1132
+ if (c === "(")
1133
+ depth++;
1134
+ else if (c === ")")
1135
+ depth--;
1136
+ }
1137
+ if (depth !== 0)
1138
+ continue;
1139
+ const callClose = p - 1;
1140
+ const args = out.slice(callOpen, callClose).trim();
1141
+ const extras = needed.join(", ");
1142
+ const newArgs = args.length === 0 ? extras : `${args}, ${extras}`;
1143
+ result += out.slice(lastIdx, callOpen) + newArgs + ")";
1144
+ lastIdx = p; // p is one past the `)`
1145
+ }
1146
+ if (lastIdx > 0) {
1147
+ result += out.slice(lastIdx);
1148
+ out = result;
1149
+ }
1150
+ }
1151
+ return out;
1152
+ }
1153
+ function mergeStages(eff) {
1154
+ // Combine all stages' templates into one module by concatenating
1155
+ // their values + types arrays. The compile pipeline's composeStages
1156
+ // pass will fuse same-stage entries afterwards.
1157
+ const types = eff.stages.flatMap(s => [...s.template.types]);
1158
+ const values = eff.stages.flatMap(s => [...s.template.values]);
1159
+ return { types, values };
1160
+ }
1161
+ //# sourceMappingURL=heapEffectIR.js.map