@aardworx/wombat.rendering 0.8.1 → 0.9.1

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 (141) 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 +529 -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 -1
  73. package/dist/runtime/index.d.ts.map +1 -1
  74. package/dist/runtime/index.js +9 -1
  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 +6 -5
  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 +728 -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 +45 -1
  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
  137. package/dist/runtime/heapShader.d.ts +0 -12
  138. package/dist/runtime/heapShader.d.ts.map +0 -1
  139. package/dist/runtime/heapShader.js +0 -157
  140. package/dist/runtime/heapShader.js.map +0 -1
  141. package/src/runtime/heapShader.ts +0 -185
@@ -0,0 +1,866 @@
1
+ // heapShaderFamily — analysis pass for §6 family-merge.
2
+ //
3
+ // Given a set of `Effect`s, build a `ShaderFamilySchema` that describes
4
+ // the family-wide layout the rest of §6 will plumb into the heap path:
5
+ //
6
+ // - deterministic `id` over the sorted-by-effect.id member set
7
+ // - `layoutIdOf` mapping each effect to a 0..N-1 layoutId
8
+ // - per-effect varying slot maps (anonymous Varying0..N packing)
9
+ // - unified `drawHeaderUnion` (union of every effect's BucketLayout)
10
+ // - per-effect `HeapEffectSchema` for downstream use
11
+ //
12
+ // This file is a pure analysis pass. It does NOT touch heapScene, does
13
+ // NOT compile WGSL, does NOT rewrite IR. It only computes the
14
+ // load-bearing descriptor that subsequent slices of §6 will consume.
15
+ import { combineHashes } from "@aardworx/wombat.shader/ir";
16
+ import { compileHeapEffect, buildBucketLayout, megacallSearchPrelude, } from "./heapEffect.js";
17
+ import { compileHeapEffectIR } from "./heapEffectIR.js";
18
+ export function buildShaderFamily(effects, fragmentOutputLayout, slotAssigner, options = {}) {
19
+ // 1. Deterministic ordering. Sort by `effect.id` (lexicographic).
20
+ // `Array.prototype.sort` is mutating, so copy first.
21
+ const sortedEffects = [...effects].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
22
+ const layoutIdOf = new Map();
23
+ for (let i = 0; i < sortedEffects.length; i++) {
24
+ layoutIdOf.set(sortedEffects[i], i);
25
+ }
26
+ // 2. Compile every effect's schema. Don't run `compileHeapEffectIR`
27
+ // here — that does heap-shape rewriting; we just want the schema.
28
+ const perEffectSchema = new Map();
29
+ for (const e of sortedEffects) {
30
+ const compiled = compileHeapEffect(e, fragmentOutputLayout);
31
+ perEffectSchema.set(e, compiled.schema);
32
+ }
33
+ // 3. Family hash: combine the (sorted) member ids deterministically.
34
+ const id = combineHashes(...sortedEffects.map(e => e.id));
35
+ // 4. Cross-effect varying type-equality check. Same-name varying ⇒
36
+ // same WGSL type (and same builtin-or-not flavor).
37
+ checkVaryingTypeAgreement(sortedEffects, perEffectSchema);
38
+ // 5. Per-effect varying slot packing.
39
+ const perEffectSlotMap = new Map();
40
+ let varyingSlots = 0;
41
+ for (const e of sortedEffects) {
42
+ const schema = perEffectSchema.get(e);
43
+ const slotMap = packVaryings(e, schema.varyings, slotAssigner);
44
+ perEffectSlotMap.set(e, slotMap);
45
+ let maxSlot = 0;
46
+ for (const s of slotMap.values()) {
47
+ if (s.slot + 1 > maxSlot)
48
+ maxSlot = s.slot + 1;
49
+ }
50
+ if (maxSlot > varyingSlots)
51
+ varyingSlots = maxSlot;
52
+ }
53
+ // 6. drawHeader union across all effects, then append layoutId.
54
+ const drawHeaderUnion = unionDrawHeaders(sortedEffects, perEffectSchema, options.atlasizeAllTextures === true, options.perEffectPerInstance);
55
+ return {
56
+ id,
57
+ effects: sortedEffects,
58
+ layoutIdOf,
59
+ varyingSlots,
60
+ perEffectSlotMap,
61
+ // `heapVaryingSlots` and `perEffectHeapSlotMap` are populated by
62
+ // `compileShaderFamily` from IR-emitted per-effect VS output
63
+ // structs (the names of heap-injected `_h_...` fields are not
64
+ // known until IR rewrite runs). Empty here so the schema is
65
+ // useable for analysis-only callers.
66
+ heapVaryingSlots: 0,
67
+ perEffectHeapSlotMap: new Map(),
68
+ drawHeaderUnion,
69
+ perEffectSchema,
70
+ };
71
+ }
72
+ // ─── Varying packing ────────────────────────────────────────────────
73
+ /** Float-count of a WGSL varying type (vec4=4, vec3=3, vec2=2, scalar=1). */
74
+ function wgslFloatCount(wgslType) {
75
+ // Cover the realistic varying-typed surface: f32, vec2/3/4<f32>,
76
+ // and the scalar-int variants (u32, i32 — used for flat-interpolated
77
+ // atlas threading and discrete payloads). Matrices are not legal as
78
+ // an inter-stage type per WGSL, so we don't need to handle them.
79
+ if (wgslType === "f32" || wgslType === "u32" || wgslType === "i32" || wgslType === "bool")
80
+ return 1;
81
+ const m = /^vec([234])<\s*[fui]32\s*>$/.exec(wgslType);
82
+ if (m !== null)
83
+ return Number(m[1]);
84
+ throw new Error(`buildShaderFamily: cannot determine slot size for varying type '${wgslType}'`);
85
+ }
86
+ /**
87
+ * Greedy packing: walk the effect's non-builtin varyings in order;
88
+ * place each in the first slot that has enough remaining float-room.
89
+ * Open a fresh slot when the current one can't fit. Per-effect — each
90
+ * effect's slot space starts at slot 0; the family's `varyingSlots` is
91
+ * the max across effects.
92
+ */
93
+ function packVaryings(effect, varyings, slotAssigner) {
94
+ const result = new Map();
95
+ const slotFill = []; // floats consumed in each slot so far
96
+ for (const v of varyings) {
97
+ if (v.builtin !== undefined)
98
+ continue; // gl_Position etc. — not in family VsOut
99
+ const size = wgslFloatCount(v.wgslType);
100
+ // Caller override path.
101
+ if (slotAssigner !== undefined) {
102
+ const override = slotAssigner(effect, v.name, v.wgslType);
103
+ if (override !== undefined) {
104
+ if (override.size !== size) {
105
+ throw new Error(`buildShaderFamily: slotAssigner returned size ${override.size} for varying ` +
106
+ `'${v.name}: ${v.wgslType}' (expected ${size})`);
107
+ }
108
+ if (override.offset < 0 || override.offset + override.size > 4) {
109
+ throw new Error(`buildShaderFamily: slotAssigner returned out-of-range slot ` +
110
+ `(offset=${override.offset}, size=${override.size}) for varying '${v.name}'`);
111
+ }
112
+ result.set(v.name, override);
113
+ // Track the slot fill for callers that mix override + default.
114
+ while (slotFill.length <= override.slot)
115
+ slotFill.push(0);
116
+ const used = slotFill[override.slot];
117
+ const top = override.offset + override.size;
118
+ if (top > used)
119
+ slotFill[override.slot] = top;
120
+ continue;
121
+ }
122
+ }
123
+ // Default greedy: first-fit by remaining room.
124
+ let placed = false;
125
+ for (let i = 0; i < slotFill.length; i++) {
126
+ const used = slotFill[i];
127
+ if (used + size <= 4) {
128
+ result.set(v.name, { slot: i, offset: used, size });
129
+ slotFill[i] = used + size;
130
+ placed = true;
131
+ break;
132
+ }
133
+ }
134
+ if (!placed) {
135
+ const slot = slotFill.length;
136
+ slotFill.push(size);
137
+ result.set(v.name, { slot, offset: 0, size });
138
+ }
139
+ }
140
+ return result;
141
+ }
142
+ // ─── DrawHeader union ───────────────────────────────────────────────
143
+ /**
144
+ * Union the per-effect BucketLayouts into a single drawHeader. Same-
145
+ * name field across effects ⇒ same WGSL type (else throws). The
146
+ * result has every unique field laid out in encounter order, then
147
+ * `layoutId: u32` appended at a fresh aligned offset.
148
+ *
149
+ * `perInstanceUniforms` and `perInstanceAttributes` unions across all
150
+ * effects (a uniform/attribute that's per-instance in any effect is
151
+ * per-instance in the family). Texture/sampler bindings union by name
152
+ * with the same type-agreement check; conflicting types throw.
153
+ */
154
+ function unionDrawHeaders(sortedEffects, perEffectSchema, atlasizeAllTextures, perEffectPerInstance) {
155
+ // Build each effect's BucketLayout with default opts. The v1 PoC's
156
+ // family-build call site (a future slice) will pass the real
157
+ // perInstance / atlas opts; here we just want the schema-driven
158
+ // union.
159
+ const perEffectLayout = new Map();
160
+ for (const e of sortedEffects) {
161
+ const schema = perEffectSchema.get(e);
162
+ const atlasNames = atlasizeAllTextures
163
+ ? new Set(schema.textures.map(t => t.name))
164
+ : new Set();
165
+ const perI = perEffectPerInstance?.get(e);
166
+ perEffectLayout.set(e, buildBucketLayout(schema, false, {
167
+ atlasTextureBindings: atlasNames,
168
+ ...(perI?.attributes !== undefined ? { perInstanceAttributes: perI.attributes } : {}),
169
+ ...(perI?.uniforms !== undefined ? { perInstanceUniforms: perI.uniforms } : {}),
170
+ }));
171
+ }
172
+ // Union drawHeaderFields by name. First-seen layout wins for the
173
+ // field's metadata; we just verify type-agreement on subsequent
174
+ // appearances. We DON'T trust the per-effect byteOffsets — those are
175
+ // recomputed for the unified layout below.
176
+ const fieldByName = new Map();
177
+ for (const e of sortedEffects) {
178
+ const layout = perEffectLayout.get(e);
179
+ for (const f of layout.drawHeaderFields) {
180
+ const existing = fieldByName.get(f.name);
181
+ if (existing === undefined) {
182
+ fieldByName.set(f.name, { field: f, ownerId: e.id });
183
+ }
184
+ else {
185
+ // Compare the underlying logical type, not the ref-slot type
186
+ // (which is always "u32" for uniform-ref / attribute-ref).
187
+ const existingLogical = logicalFieldType(existing.field);
188
+ const incomingLogical = logicalFieldType(f);
189
+ if (existingLogical !== incomingLogical) {
190
+ throw new Error(`buildShaderFamily: drawHeader field '${f.name}' has conflicting WGSL type ` +
191
+ `across effects: '${existingLogical}' (effect ${existing.ownerId}) vs ` +
192
+ `'${incomingLogical}' (effect ${e.id}). Disambiguate via effect.rename(...).`);
193
+ }
194
+ if (existing.field.kind !== f.kind) {
195
+ throw new Error(`buildShaderFamily: drawHeader field '${f.name}' has conflicting kind across ` +
196
+ `effects: '${existing.field.kind}' (effect ${existing.ownerId}) vs ` +
197
+ `'${f.kind}' (effect ${e.id}).`);
198
+ }
199
+ }
200
+ }
201
+ }
202
+ // Re-lay out the unioned fields with fresh byte offsets. Order is
203
+ // deterministic: encounter order over the sorted effects + their
204
+ // own field order.
205
+ const ordered = [];
206
+ const seen = new Set();
207
+ for (const e of sortedEffects) {
208
+ const layout = perEffectLayout.get(e);
209
+ for (const f of layout.drawHeaderFields) {
210
+ if (seen.has(f.name))
211
+ continue;
212
+ seen.add(f.name);
213
+ ordered.push(f);
214
+ }
215
+ }
216
+ const fields = [];
217
+ let off = 0;
218
+ for (const f of ordered) {
219
+ off = roundUp(off, Math.min(f.byteSize, 16));
220
+ const next = {
221
+ ...f,
222
+ byteOffset: off,
223
+ };
224
+ fields.push(next);
225
+ off += f.byteSize;
226
+ }
227
+ // Append the family-wide layoutId selector. u32 → 4-byte align.
228
+ off = roundUp(off, 4);
229
+ const layoutIdField = {
230
+ name: "__layoutId",
231
+ wgslName: "layoutId",
232
+ wgslType: "u32",
233
+ byteOffset: off,
234
+ byteSize: 4,
235
+ kind: "uniform-ref",
236
+ // Logical type for the heap-load IR builder. Without this,
237
+ // `compileHeapEffectIR` would fail on the family layout because
238
+ // `loadUniformByRef` is dispatched on `uniformWgslType`.
239
+ uniformWgslType: "u32",
240
+ };
241
+ fields.push(layoutIdField);
242
+ off += 4;
243
+ const drawHeaderBytes = roundUp(off, 16);
244
+ const strideU32 = drawHeaderBytes / 4;
245
+ // Union perInstance sets and texture/sampler bindings.
246
+ const perInstanceUniforms = new Set();
247
+ const perInstanceAttributes = new Set();
248
+ for (const e of sortedEffects) {
249
+ const layout = perEffectLayout.get(e);
250
+ for (const n of layout.perInstanceUniforms)
251
+ perInstanceUniforms.add(n);
252
+ for (const n of layout.perInstanceAttributes)
253
+ perInstanceAttributes.add(n);
254
+ }
255
+ const textureByName = new Map();
256
+ const samplerByName = new Map();
257
+ const atlasUnion = new Set();
258
+ for (const e of sortedEffects) {
259
+ const layout = perEffectLayout.get(e);
260
+ for (const t of layout.textureBindings) {
261
+ const existing = textureByName.get(t.name);
262
+ if (existing === undefined)
263
+ textureByName.set(t.name, { ...t });
264
+ else if (existing.wgslType !== t.wgslType) {
265
+ throw new Error(`buildShaderFamily: texture binding '${t.name}' has conflicting WGSL type ` +
266
+ `across effects: '${existing.wgslType}' vs '${t.wgslType}'.`);
267
+ }
268
+ }
269
+ for (const s of layout.samplerBindings) {
270
+ const existing = samplerByName.get(s.name);
271
+ if (existing === undefined)
272
+ samplerByName.set(s.name, { ...s });
273
+ else if (existing.wgslType !== s.wgslType) {
274
+ throw new Error(`buildShaderFamily: sampler binding '${s.name}' has conflicting WGSL type ` +
275
+ `across effects: '${existing.wgslType}' vs '${s.wgslType}'.`);
276
+ }
277
+ }
278
+ for (const n of layout.atlasTextureBindings)
279
+ atlasUnion.add(n);
280
+ }
281
+ // Renumber bindings deterministically — first the unioned textures,
282
+ // then samplers, in encounter order over the sorted effects.
283
+ const orderedTextures = [];
284
+ const seenTex = new Set();
285
+ for (const e of sortedEffects) {
286
+ const layout = perEffectLayout.get(e);
287
+ for (const t of layout.textureBindings) {
288
+ if (seenTex.has(t.name))
289
+ continue;
290
+ seenTex.add(t.name);
291
+ orderedTextures.push(textureByName.get(t.name));
292
+ }
293
+ }
294
+ const orderedSamplers = [];
295
+ const seenSmp = new Set();
296
+ for (const e of sortedEffects) {
297
+ const layout = perEffectLayout.get(e);
298
+ for (const s of layout.samplerBindings) {
299
+ if (seenSmp.has(s.name))
300
+ continue;
301
+ seenSmp.add(s.name);
302
+ orderedSamplers.push(samplerByName.get(s.name));
303
+ }
304
+ }
305
+ // We don't reassign binding numbers here — the v1 PoC slice that
306
+ // wires this into a real WGSL prelude will do that. For the
307
+ // descriptor-only purpose of this slice, the per-effect-allocated
308
+ // numbers are retained as-is on each entry.
309
+ return {
310
+ drawHeaderFields: fields,
311
+ drawHeaderBytes,
312
+ preludeWgsl: "", // Family-level prelude is a future slice; not produced here.
313
+ strideU32,
314
+ perInstanceUniforms,
315
+ perInstanceAttributes,
316
+ textureBindings: orderedTextures,
317
+ samplerBindings: orderedSamplers,
318
+ atlasTextureBindings: atlasUnion,
319
+ };
320
+ }
321
+ function checkVaryingTypeAgreement(sortedEffects, perEffectSchema) {
322
+ const seen = new Map();
323
+ for (const e of sortedEffects) {
324
+ const schema = perEffectSchema.get(e);
325
+ for (const v of schema.varyings) {
326
+ const existing = seen.get(v.name);
327
+ const isBuiltin = v.builtin !== undefined;
328
+ if (existing === undefined) {
329
+ seen.set(v.name, { wgslType: v.wgslType, isBuiltin, ownerId: e.id });
330
+ }
331
+ else {
332
+ if (existing.wgslType !== v.wgslType || existing.isBuiltin !== isBuiltin) {
333
+ throw new Error(`buildShaderFamily: varying '${v.name}' has conflicting type across effects: ` +
334
+ `'${existing.wgslType}'${existing.isBuiltin ? " (builtin)" : ""} ` +
335
+ `(effect ${existing.ownerId}) vs '${v.wgslType}'${isBuiltin ? " (builtin)" : ""} ` +
336
+ `(effect ${e.id}). Disambiguate via effect.rename({varyings: ...}).`);
337
+ }
338
+ }
339
+ }
340
+ }
341
+ }
342
+ function roundUp(value, mult) {
343
+ return Math.ceil(value / mult) * mult;
344
+ }
345
+ /**
346
+ * Logical (semantic) WGSL type of a drawHeader field. For uniform-ref
347
+ * and attribute-ref, the field's `wgslType` is always `"u32"` (the
348
+ * pool ref); the real semantic type lives in
349
+ * `uniformWgslType` / `attributeWgslType`. For texture-ref and any
350
+ * other inline kinds, fall back to `wgslType`.
351
+ */
352
+ function logicalFieldType(f) {
353
+ if (f.kind === "uniform-ref" && f.uniformWgslType !== undefined)
354
+ return f.uniformWgslType;
355
+ if (f.kind === "attribute-ref" && f.attributeWgslType !== undefined)
356
+ return f.attributeWgslType;
357
+ return f.wgslType;
358
+ }
359
+ /**
360
+ * Synthesise the merged VS+FS pair for a `ShaderFamilySchema`.
361
+ *
362
+ * The output is one self-contained WGSL module per stage. The VS
363
+ * carries the megacall search prelude + all bindings (heap arena 0..3
364
+ * plus drawTable / indexStorage / firstDrawInTile at 4..6 — same shape
365
+ * as the standalone path); the FS carries no megacall bindings (it
366
+ * receives the layoutId via `@interpolate(flat)`).
367
+ *
368
+ * Slice 3b is a pure analysis pass — it does NOT wire into heapScene.
369
+ * Slice 3c will adopt this output behind the existing per-effect
370
+ * pipeline.
371
+ */
372
+ export function compileShaderFamily(family, fragmentOutputLayout) {
373
+ const opts = fragmentOutputLayout !== undefined
374
+ ? { target: "wgsl", fragmentOutputLayout }
375
+ : { target: "wgsl" };
376
+ // 1. Per-effect rewrite + compile in family-member mode.
377
+ // Each effect's entries get renamed to `family_vs_${K}` /
378
+ // `family_fs_${K}` so they don't collide at module scope when
379
+ // we concatenate them. The auto-generated VsOut / FsIn / FsOut
380
+ // structs derive from the entry names, so they're disambiguated
381
+ // automatically (e.g. `Family_vs_0Output`).
382
+ const perEffectVs = [];
383
+ const perEffectFs = [];
384
+ for (const e of family.effects) {
385
+ const k = family.layoutIdOf.get(e);
386
+ // Composed effects (multiple same-stage Entries that compose into
387
+ // one fused entry) need each source Entry renamed to a UNIQUE
388
+ // target — `renameEntries` rejects collisions, and `composeStages`
389
+ // joins source names into the fused entry's name. After IR compile
390
+ // we read the actual emitted @vertex / @fragment name from the
391
+ // WGSL and use that as the helper name.
392
+ const vsEntries = findStageEntryNames(e, "vertex");
393
+ const fsEntries = findStageEntryNames(e, "fragment");
394
+ const vsPrefix = `family_vs_${k}`;
395
+ const fsPrefix = `family_fs_${k}`;
396
+ const entryMap = new Map();
397
+ if (vsEntries.length === 1) {
398
+ entryMap.set(vsEntries[0], vsPrefix);
399
+ }
400
+ else {
401
+ for (let i = 0; i < vsEntries.length; i++) {
402
+ entryMap.set(vsEntries[i], `${vsPrefix}_p${i}`);
403
+ }
404
+ }
405
+ if (fsEntries.length === 1) {
406
+ entryMap.set(fsEntries[0], fsPrefix);
407
+ }
408
+ else {
409
+ for (let i = 0; i < fsEntries.length; i++) {
410
+ entryMap.set(fsEntries[i], `${fsPrefix}_p${i}`);
411
+ }
412
+ }
413
+ const renamed = e.rename({ entries: entryMap });
414
+ const ir = compileHeapEffectIR(renamed, family.drawHeaderUnion, opts, "family-member");
415
+ const vsHelper = readEntryName(ir.vs, "vertex") ?? vsPrefix;
416
+ const fsHelper = readEntryName(ir.fs, "fragment") ?? fsPrefix;
417
+ // Auto-generated struct names = `${capitalise(entryName)}Output` /
418
+ // `${capitalise(entryName)}Input` (see wombat.shader/wgsl/emit).
419
+ const outStruct = capitalise(vsHelper) + "Output";
420
+ const fsInStruct = capitalise(fsHelper) + "Input";
421
+ const fsOutStruct = capitalise(fsHelper) + "Output";
422
+ perEffectVs.push({ layoutId: k, vs: ir.vs, fsHelperName: fsHelper, vsHelperName: vsHelper, outStruct, fsInStruct, fsOutStruct });
423
+ perEffectFs.push(ir.fs);
424
+ }
425
+ // 2. Strip `@vertex`/`@fragment` decorations from the per-effect
426
+ // helpers (they're regular fns now) and split each output into a
427
+ // set of top-level decls.
428
+ const vsParts = perEffectVs.map(p => splitWgslTopLevel(stripStageDecoration(p.vs)));
429
+ const fsParts = perEffectFs.map(s => splitWgslTopLevel(stripStageDecoration(s)));
430
+ // 3. Dedup module-scope decls (storage/uniform bindings, atlas
431
+ // helpers, structs etc). The per-effect helper functions stay —
432
+ // they're already uniquely named.
433
+ const vsDecls = dedupModuleDecls(vsParts.flat(), "VS");
434
+ const fsDecls = dedupModuleDecls(fsParts.flat(), "FS");
435
+ // 4. Compute drawHeader stride + layoutId offset for the wrapper.
436
+ const familyStrideU32 = family.drawHeaderUnion.strideU32;
437
+ const layoutIdField = family.drawHeaderUnion.drawHeaderFields.find(f => f.name === "__layoutId");
438
+ if (layoutIdField === undefined) {
439
+ throw new Error("compileShaderFamily: drawHeaderUnion missing '__layoutId' field");
440
+ }
441
+ const layoutIdOffsetU32 = layoutIdField.byteOffset / 4;
442
+ // 4b. Family-merge direct-heap-read path: per-effect FS reads the heap
443
+ // headers directly using the wrapper-supplied `heap_drawIdx` /
444
+ // `instId` u32 fn parameters. The carrier on the cross-stage
445
+ // boundary is a single `heap_drawIdx_out: u32` (and
446
+ // `instId_out: u32`) flat varying — no per-uniform `_h_*Ref`
447
+ // varyings, no `vec4<u32>` packing. Keep the schema fields
448
+ // populated for backward compatibility (zero / empty).
449
+ family.heapVaryingSlots = 0;
450
+ family.perEffectHeapSlotMap = new Map();
451
+ // 5. Synthesize the wrapper VS and FS.
452
+ const vs = synthesizeFamilyVs(family, perEffectVs, vsDecls, familyStrideU32, layoutIdOffsetU32);
453
+ const fs = synthesizeFamilyFs(family, perEffectVs, fsDecls);
454
+ return {
455
+ vs,
456
+ fs,
457
+ fragmentOutputLayout,
458
+ };
459
+ }
460
+ // ─── Helpers (slice 3b) ─────────────────────────────────────────────
461
+ function capitalise(s) {
462
+ return s.length === 0 ? s : s[0].toUpperCase() + s.slice(1);
463
+ }
464
+ /**
465
+ * Find the name of the `@vertex` / `@fragment` fn in a WGSL source.
466
+ * Returns undefined when no such decl is present.
467
+ */
468
+ function readEntryName(src, stage) {
469
+ const re = new RegExp(`@${stage}\\s+fn\\s+(\\w+)\\b`);
470
+ const m = re.exec(src);
471
+ return m === null ? undefined : m[1];
472
+ }
473
+ /**
474
+ * Find stage entry names for the given stage by scanning the effect's
475
+ * stage templates for `Entry` ValueDefs. Returns a list (typically
476
+ * length 1, but composed effects may have more).
477
+ */
478
+ function findStageEntryNames(effect, stage) {
479
+ const names = [];
480
+ for (const s of effect.stages) {
481
+ for (const v of s.template.values) {
482
+ if (v.kind !== "Entry")
483
+ continue;
484
+ if (v.entry.stage !== stage)
485
+ continue;
486
+ names.push(v.entry.name);
487
+ }
488
+ }
489
+ return names;
490
+ }
491
+ /**
492
+ * Strip the `@vertex` / `@fragment` decoration from each stage entry
493
+ * function. The decoration is always immediately followed by `fn`
494
+ * (possibly across whitespace/newlines).
495
+ */
496
+ function stripStageDecoration(src) {
497
+ return src.replace(/@(?:vertex|fragment|compute)\s+(?=fn\s)/g, "");
498
+ }
499
+ /**
500
+ * Split a WGSL source into top-level declarations. We walk the source
501
+ * tracking brace depth to find statement boundaries:
502
+ * - `@…` decorators + `var …;` / `const …;` / `alias …;` (one
503
+ * statement up to the terminating `;` at depth 0).
504
+ * - `struct Name { … };` (block, brace-balanced, optional trailing
505
+ * `;`).
506
+ * - `fn Name(…) { … }` (block, brace-balanced).
507
+ * Comments / blank lines between decls are dropped — we only retain
508
+ * the declaration text.
509
+ */
510
+ function splitWgslTopLevel(src) {
511
+ const decls = [];
512
+ let i = 0;
513
+ const n = src.length;
514
+ while (i < n) {
515
+ // Skip whitespace and line comments.
516
+ while (i < n && /\s/.test(src[i]))
517
+ i++;
518
+ if (i < n && src[i] === "/" && src[i + 1] === "/") {
519
+ while (i < n && src[i] !== "\n")
520
+ i++;
521
+ continue;
522
+ }
523
+ if (i >= n)
524
+ break;
525
+ const start = i;
526
+ // Find the end of this declaration. Walk forward, tracking braces.
527
+ // A decl ends either at `;` (depth 0) for non-block decls, or at
528
+ // the matching `}` (with optional trailing `;`) for block decls.
529
+ let depth = 0;
530
+ let sawBrace = false;
531
+ while (i < n) {
532
+ const c = src[i];
533
+ if (c === "{") {
534
+ depth++;
535
+ sawBrace = true;
536
+ i++;
537
+ continue;
538
+ }
539
+ if (c === "}") {
540
+ depth--;
541
+ i++;
542
+ if (depth === 0 && sawBrace) {
543
+ // Optional trailing semicolon.
544
+ while (i < n && /\s/.test(src[i]))
545
+ i++;
546
+ if (i < n && src[i] === ";")
547
+ i++;
548
+ break;
549
+ }
550
+ continue;
551
+ }
552
+ if (c === ";" && depth === 0 && !sawBrace) {
553
+ i++;
554
+ break;
555
+ }
556
+ i++;
557
+ }
558
+ const decl = src.slice(start, i).trim();
559
+ if (decl.length > 0)
560
+ decls.push(decl);
561
+ }
562
+ return decls;
563
+ }
564
+ /**
565
+ * Classify a top-level decl by kind + identifier (for dedup keying).
566
+ * Returns `{ key, kind }`. Decls with the same `key` must have the
567
+ * same text (else we throw — the family has structurally incompatible
568
+ * per-effect outputs).
569
+ */
570
+ function classifyDecl(decl) {
571
+ // Binding: `@group(N) @binding(M) var<…> name : type;`
572
+ let m = /^\s*@group\(\s*\d+\s*\)\s*@binding\(\s*\d+\s*\)\s*var(?:<[^>]*>)?\s+(\w+)\s*:/.exec(decl);
573
+ if (m !== null)
574
+ return { key: `binding:${m[1]}`, kind: "binding" };
575
+ // Struct
576
+ m = /^\s*struct\s+(\w+)\s*\{/.exec(decl);
577
+ if (m !== null)
578
+ return { key: `struct:${m[1]}`, kind: "struct" };
579
+ // Fn
580
+ m = /^\s*fn\s+(\w+)\s*\(/.exec(decl);
581
+ if (m !== null)
582
+ return { key: `fn:${m[1]}`, kind: "fn" };
583
+ // var<private>/var
584
+ m = /^\s*var(?:<[^>]*>)?\s+(\w+)\s*:/.exec(decl);
585
+ if (m !== null)
586
+ return { key: `var:${m[1]}`, kind: "var" };
587
+ // alias
588
+ m = /^\s*alias\s+(\w+)\s*=/.exec(decl);
589
+ if (m !== null)
590
+ return { key: `alias:${m[1]}`, kind: "alias" };
591
+ return { key: `other:${decl.slice(0, 32)}`, kind: "other" };
592
+ }
593
+ /**
594
+ * Dedup a flat list of top-level decls. Same-key decls must have the
595
+ * same canonical text (whitespace-normalised); throws on conflict.
596
+ * Per-effect helper fns (named `family_vs_${K}` / `family_fs_${K}`)
597
+ * are unique by construction so they pass through.
598
+ */
599
+ function dedupModuleDecls(decls, stageLabel) {
600
+ const seen = new Map();
601
+ const ordered = [];
602
+ for (const d of decls) {
603
+ const { key } = classifyDecl(d);
604
+ const norm = d.replace(/\s+/g, " ").trim();
605
+ const existing = seen.get(key);
606
+ if (existing === undefined) {
607
+ seen.set(key, norm);
608
+ ordered.push(d);
609
+ }
610
+ else if (existing !== norm) {
611
+ throw new Error(`compileShaderFamily: ${stageLabel} declaration '${key}' conflicts across effects:\n` +
612
+ ` A: ${existing}\n` +
613
+ ` B: ${norm}\n` +
614
+ `Disambiguate via Effect.rename(...) or align bindings before merging.`);
615
+ }
616
+ }
617
+ return ordered;
618
+ }
619
+ /**
620
+ * Build the family `FamilyVsOut` struct text. One `vec4<f32>` per
621
+ * user-varying slot, then three flat-interpolated u32 carriers:
622
+ * `heap_drawIdx_out`, `instId_out`, and `layoutIdOut`. The FS reads
623
+ * the heap headers directly via these u32 indices — no per-uniform
624
+ * ref varyings.
625
+ */
626
+ function familyVsOutStruct(slots) {
627
+ const lines = [];
628
+ lines.push("struct FamilyVsOut {");
629
+ lines.push(" @builtin(position) gl_Position: vec4<f32>,");
630
+ for (let i = 0; i < slots; i++) {
631
+ lines.push(` @location(${i}) Varying${i}: vec4<f32>,`);
632
+ }
633
+ lines.push(` @interpolate(flat) @location(${slots}) heap_drawIdx_out: u32,`);
634
+ lines.push(` @interpolate(flat) @location(${slots + 1}) instId_out: u32,`);
635
+ lines.push(` @interpolate(flat) @location(${slots + 2}) layoutIdOut: u32,`);
636
+ lines.push("};");
637
+ return lines.join("\n");
638
+ }
639
+ function familyFsInStruct(slots) {
640
+ const lines = [];
641
+ lines.push("struct FamilyFsIn {");
642
+ for (let i = 0; i < slots; i++) {
643
+ lines.push(` @location(${i}) Varying${i}: vec4<f32>,`);
644
+ }
645
+ lines.push(` @interpolate(flat) @location(${slots}) heap_drawIdx_in: u32,`);
646
+ lines.push(` @interpolate(flat) @location(${slots + 1}) instId_in: u32,`);
647
+ lines.push(` @interpolate(flat) @location(${slots + 2}) layoutIdIn: u32,`);
648
+ lines.push("};");
649
+ return lines.join("\n");
650
+ }
651
+ /**
652
+ * Component-suffix for an offset+size into a vec4 slot. `(0,3)` →
653
+ * `.xyz`, `(0,4)` → `` (whole vec4), `(1,2)` → `.yz`, etc. v1: each
654
+ * varying takes the prefix `(0, size)` of a dedicated slot, but the
655
+ * helper accepts arbitrary offsets so future packing works.
656
+ */
657
+ function vec4Swizzle(offset, size) {
658
+ if (offset === 0 && size === 4)
659
+ return "";
660
+ const comps = ["x", "y", "z", "w"];
661
+ return "." + comps.slice(offset, offset + size).join("");
662
+ }
663
+ /**
664
+ * Per-varying read/write expression. For vec3/vec2/scalar, the source
665
+ * value is a vec3/vec2/f32; we need to read/write the appropriate
666
+ * `.xyz` / `.xy` / `.x` slice of the slot's `vec4<f32>`.
667
+ */
668
+ function slotComponentExpr(slotName, offset, size) {
669
+ return slotName + vec4Swizzle(offset, size);
670
+ }
671
+ /**
672
+ * Build a dispatch case for the VS wrapper. Calls `family_vs_${K}` and
673
+ * packs its named-varying outputs into the family's anonymous slots.
674
+ */
675
+ function buildVsCase(k, vsHelperName, outStruct, effect, family) {
676
+ const slotMap = family.perEffectSlotMap.get(effect);
677
+ const lines = [];
678
+ lines.push(` case ${k}u: {`);
679
+ lines.push(` let r: ${outStruct} = ${vsHelperName}(heap_drawIdx, instId, vid);`);
680
+ // The @builtin(position) field on the per-effect VsOut struct is
681
+ // whatever the user named it (`gl_Position`, `position`, `clipPos`,
682
+ // …). Discover from the schema rather than hardcoding `gl_Position`
683
+ // — assuming the legacy spelling silently breaks any effect that
684
+ // uses a different identifier.
685
+ const schema = family.perEffectSchema.get(effect);
686
+ const positionVarying = schema.varyings.find(v => v.builtin === "position");
687
+ const positionField = positionVarying?.name ?? "gl_Position";
688
+ lines.push(` out.gl_Position = r.${positionField};`);
689
+ for (const v of schema.varyings) {
690
+ if (v.builtin !== undefined)
691
+ continue;
692
+ const slot = slotMap.get(v.name);
693
+ if (slot === undefined)
694
+ continue;
695
+ // WGSL spec: multi-component swizzles (`.xyz`/`.xy`) are r-values,
696
+ // not assignable l-values. Tint accepts swizzle assignment as an
697
+ // extension; WebKit/Safari does not. Reconstruct the whole vec4
698
+ // from components so the expression is spec-compliant everywhere.
699
+ const slotName = `out.Varying${slot.slot}`;
700
+ lines.push(` ${slotName} = ${packVec4Components(`r.${v.name}`, slot.offset, slot.size)};`);
701
+ }
702
+ lines.push(` }`);
703
+ return lines.join("\n");
704
+ }
705
+ /**
706
+ * Build a `vec4<f32>(c0, c1, c2, c3)` literal where the source value
707
+ * occupies `[offset..offset+size)` and the rest is `0.0`. The source
708
+ * is a scalar (size=1), vec2 (size=2), vec3 (size=3), or vec4 (size=4).
709
+ * Avoids multi-component swizzle l-values which Safari rejects.
710
+ */
711
+ function packVec4Components(rhsExpr, offset, size) {
712
+ const comps = ["0.0", "0.0", "0.0", "0.0"];
713
+ const swiz = ["x", "y", "z", "w"];
714
+ for (let i = 0; i < size; i++) {
715
+ comps[offset + i] = size === 1 ? rhsExpr : `${rhsExpr}.${swiz[i]}`;
716
+ }
717
+ return `vec4<f32>(${comps.join(", ")})`;
718
+ }
719
+ function buildFsCase(k, fsHelperName, fsInStruct, fsOutStruct, effect, family) {
720
+ const slotMap = family.perEffectSlotMap.get(effect);
721
+ const schema = family.perEffectSchema.get(effect);
722
+ const lines = [];
723
+ lines.push(` case ${k}u: {`);
724
+ lines.push(` var fin: ${fsInStruct};`);
725
+ for (const v of schema.varyings) {
726
+ if (v.builtin !== undefined)
727
+ continue;
728
+ const slot = slotMap.get(v.name);
729
+ if (slot === undefined)
730
+ continue;
731
+ const slotName = `in.Varying${slot.slot}`;
732
+ const rhs = slotComponentExpr(slotName, slot.offset, slot.size);
733
+ lines.push(` fin.${v.name} = ${rhs};`);
734
+ }
735
+ lines.push(` let r: ${fsOutStruct} = ${fsHelperName}(fin, heap_drawIdx, instId);`);
736
+ // Copy r's fields into the family FsOut. v1 assumes per-effect FS
737
+ // output structs all carry the same fields (driven by
738
+ // `fragmentOutputLayout`); we just map by name. Fragment outputs
739
+ // appear on the schema as `fragmentOutputs`.
740
+ for (const fo of schema.fragmentOutputs) {
741
+ lines.push(` fout.${fo.name} = r.${fo.name};`);
742
+ }
743
+ lines.push(` }`);
744
+ return lines.join("\n");
745
+ }
746
+ function familyFsOutStruct(family) {
747
+ const byName = new Map();
748
+ for (const e of family.effects) {
749
+ const sc = family.perEffectSchema.get(e);
750
+ for (const fo of sc.fragmentOutputs) {
751
+ const existing = byName.get(fo.name);
752
+ if (existing === undefined) {
753
+ byName.set(fo.name, { name: fo.name, location: fo.location, wgslType: fo.wgslType });
754
+ }
755
+ else if (existing.wgslType !== fo.wgslType || existing.location !== fo.location) {
756
+ throw new Error(`compileShaderFamily: fragmentOutput '${fo.name}' conflicts across effects: ` +
757
+ `'${existing.wgslType}@${existing.location}' vs '${fo.wgslType}@${fo.location}'`);
758
+ }
759
+ }
760
+ }
761
+ const ordered = [...byName.values()].sort((a, b) => a.location - b.location);
762
+ const lines = [];
763
+ lines.push("struct FamilyFsOut {");
764
+ for (const fo of ordered) {
765
+ lines.push(` @location(${fo.location}) ${fo.name}: ${fo.wgslType},`);
766
+ }
767
+ lines.push("};");
768
+ return lines.join("\n");
769
+ }
770
+ function synthesizeFamilyVs(family, perEffect, dedupedDecls, familyStrideU32, layoutIdOffsetU32) {
771
+ const slots = family.varyingSlots;
772
+ const lines = [];
773
+ lines.push("// Family-merged vertex shader (direct-heap-read FS path).");
774
+ // Heap arena + megacall storage-buffer bindings declared explicitly
775
+ // by the wrapper. The wrapper itself references `headersU32` for
776
+ // the layoutId lookup, so these MUST be present even when no
777
+ // per-effect helper happens to reference them (in which case DCE
778
+ // in the helper compile would otherwise drop them). Hand-declared
779
+ // strings; per-effect helpers' duplicate decls of the same names
780
+ // are filtered out of dedupedDecls below.
781
+ lines.push("@group(0) @binding(0) var<storage, read> heapU32: array<u32>;");
782
+ lines.push("@group(0) @binding(1) var<storage, read> headersU32: array<u32>;");
783
+ lines.push("@group(0) @binding(2) var<storage, read> heapF32: array<f32>;");
784
+ lines.push("@group(0) @binding(3) var<storage, read> heapV4f: array<vec4<f32>>;");
785
+ lines.push("@group(0) @binding(4) var<storage, read> drawTable: array<u32>;");
786
+ lines.push("@group(0) @binding(5) var<storage, read> indexStorage: array<u32>;");
787
+ lines.push("@group(0) @binding(6) var<storage, read> firstDrawInTile: array<u32>;");
788
+ lines.push("");
789
+ // Deduped module-scope decls (user uniforms, per-effect structs,
790
+ // per-effect helper fns). Filter the heap-arena bindings out since
791
+ // we hand-declare them above; otherwise they'd appear twice.
792
+ const heapArenaNames = new Set(["heapU32", "headersU32", "heapF32", "heapV4f"]);
793
+ for (const d of dedupedDecls) {
794
+ const m = /^\s*@group\(\s*\d+\s*\)\s*@binding\(\s*\d+\s*\)\s*var(?:<[^>]*>)?\s+(\w+)\s*:/.exec(d);
795
+ if (m !== null && heapArenaNames.has(m[1]))
796
+ continue;
797
+ lines.push(d);
798
+ lines.push("");
799
+ }
800
+ // Family VsOut struct.
801
+ lines.push(familyVsOutStruct(slots));
802
+ lines.push("");
803
+ // Wrapper @vertex.
804
+ lines.push("@vertex fn family_vs_main(@builtin(vertex_index) emitIdx: u32) -> FamilyVsOut {");
805
+ // Megacall search prelude — defines heap_drawIdx, instId, vid as
806
+ // locals in this function's scope.
807
+ lines.push(megacallSearchPrelude());
808
+ // layoutId from drawHeader.
809
+ lines.push(` let layoutId: u32 = headersU32[(heap_drawIdx * ${familyStrideU32}u) + ${layoutIdOffsetU32}u];`);
810
+ lines.push(" var out: FamilyVsOut;");
811
+ // Initialise all slots to zero so the WGSL out-of-init-store rule
812
+ // is satisfied across switch arms that don't write every slot.
813
+ lines.push(" out.gl_Position = vec4<f32>(0.0);");
814
+ for (let i = 0; i < slots; i++) {
815
+ lines.push(` out.Varying${i} = vec4<f32>(0.0);`);
816
+ }
817
+ lines.push(" out.heap_drawIdx_out = heap_drawIdx;");
818
+ lines.push(" out.instId_out = instId;");
819
+ lines.push(" out.layoutIdOut = layoutId;");
820
+ lines.push(" switch (layoutId) {");
821
+ for (const p of perEffect) {
822
+ const e = family.effects[p.layoutId];
823
+ lines.push(buildVsCase(p.layoutId, p.vsHelperName, p.outStruct, e, family));
824
+ }
825
+ lines.push(" default: { }");
826
+ lines.push(" }");
827
+ lines.push(" return out;");
828
+ lines.push("}");
829
+ return lines.join("\n");
830
+ }
831
+ function synthesizeFamilyFs(family, perEffect, dedupedDecls) {
832
+ const slots = family.varyingSlots;
833
+ const lines = [];
834
+ // Suppress Tint's conservative derivative-uniformity analysis: layoutIdIn is
835
+ // @interpolate(flat) and uniform per primitive in practice, but Tint treats
836
+ // fragment-in params as non-uniform, rejecting dpdx/dpdy reachable from the
837
+ // switch. v1 concession; v2 may hoist derivatives via parameter-threading.
838
+ lines.push("diagnostic(off, derivative_uniformity);");
839
+ lines.push("");
840
+ lines.push("// Family-merged fragment shader (direct-heap-read FS path).");
841
+ for (const d of dedupedDecls) {
842
+ lines.push(d);
843
+ lines.push("");
844
+ }
845
+ lines.push(familyFsInStruct(slots));
846
+ lines.push("");
847
+ lines.push(familyFsOutStruct(family));
848
+ lines.push("");
849
+ lines.push("@fragment fn family_fs_main(in: FamilyFsIn) -> FamilyFsOut {");
850
+ lines.push(" var fout: FamilyFsOut;");
851
+ // Hoist heap_drawIdx / instId out of the FsIn carrier into locals so
852
+ // each per-effect dispatch case can pass them by name to the helper.
853
+ lines.push(" let heap_drawIdx: u32 = in.heap_drawIdx_in;");
854
+ lines.push(" let instId: u32 = in.instId_in;");
855
+ lines.push(" switch (in.layoutIdIn) {");
856
+ for (const p of perEffect) {
857
+ const e = family.effects[p.layoutId];
858
+ lines.push(buildFsCase(p.layoutId, p.fsHelperName, p.fsInStruct, p.fsOutStruct, e, family));
859
+ }
860
+ lines.push(" default: { }");
861
+ lines.push(" }");
862
+ lines.push(" return fout;");
863
+ lines.push("}");
864
+ return lines.join("\n");
865
+ }
866
+ //# sourceMappingURL=heapShaderFamily.js.map