@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.
- package/dist/core/command.d.ts +7 -9
- package/dist/core/command.d.ts.map +1 -1
- package/dist/core/renderTask.d.ts +64 -1
- package/dist/core/renderTask.d.ts.map +1 -1
- package/dist/core/renderTask.js +3 -1
- package/dist/core/renderTask.js.map +1 -1
- package/dist/resources/preparedRenderObject.js +3 -3
- package/dist/resources/preparedRenderObject.js.map +1 -1
- package/dist/resources/uniformBuffer.d.ts.map +1 -1
- package/dist/resources/uniformBuffer.js +17 -0
- package/dist/resources/uniformBuffer.js.map +1 -1
- package/dist/runtime/derivedUniforms/dispatch.d.ts +105 -0
- package/dist/runtime/derivedUniforms/dispatch.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/dispatch.js +355 -0
- package/dist/runtime/derivedUniforms/dispatch.js.map +1 -0
- package/dist/runtime/derivedUniforms/index.d.ts +6 -0
- package/dist/runtime/derivedUniforms/index.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/index.js +26 -0
- package/dist/runtime/derivedUniforms/index.js.map +1 -0
- package/dist/runtime/derivedUniforms/recipes.d.ts +42 -0
- package/dist/runtime/derivedUniforms/recipes.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/recipes.js +134 -0
- package/dist/runtime/derivedUniforms/recipes.js.map +1 -0
- package/dist/runtime/derivedUniforms/sceneIntegration.d.ts +56 -0
- package/dist/runtime/derivedUniforms/sceneIntegration.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/sceneIntegration.js +171 -0
- package/dist/runtime/derivedUniforms/sceneIntegration.js.map +1 -0
- package/dist/runtime/derivedUniforms/slotId.d.ts +15 -0
- package/dist/runtime/derivedUniforms/slotId.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/slotId.js +30 -0
- package/dist/runtime/derivedUniforms/slotId.js.map +1 -0
- package/dist/runtime/derivedUniforms/slots.d.ts +49 -0
- package/dist/runtime/derivedUniforms/slots.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/slots.js +123 -0
- package/dist/runtime/derivedUniforms/slots.js.map +1 -0
- package/dist/runtime/derivedUniforms/uberKernel.wgsl.d.ts +13 -0
- package/dist/runtime/derivedUniforms/uberKernel.wgsl.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/uberKernel.wgsl.js +218 -0
- package/dist/runtime/derivedUniforms/uberKernel.wgsl.js.map +1 -0
- package/dist/runtime/flattenTree.d.ts +5 -0
- package/dist/runtime/flattenTree.d.ts.map +1 -0
- package/dist/runtime/flattenTree.js +45 -0
- package/dist/runtime/flattenTree.js.map +1 -0
- package/dist/runtime/heapAdapter.d.ts +16 -0
- package/dist/runtime/heapAdapter.d.ts.map +1 -0
- package/dist/runtime/heapAdapter.js +263 -0
- package/dist/runtime/heapAdapter.js.map +1 -0
- package/dist/runtime/heapEffect.d.ts +235 -0
- package/dist/runtime/heapEffect.d.ts.map +1 -0
- package/dist/runtime/heapEffect.js +518 -0
- package/dist/runtime/heapEffect.js.map +1 -0
- package/dist/runtime/heapEffectIR.d.ts +34 -0
- package/dist/runtime/heapEffectIR.d.ts.map +1 -0
- package/dist/runtime/heapEffectIR.js +1161 -0
- package/dist/runtime/heapEffectIR.js.map +1 -0
- package/dist/runtime/heapEligibility.d.ts +9 -0
- package/dist/runtime/heapEligibility.d.ts.map +1 -0
- package/dist/runtime/heapEligibility.js +195 -0
- package/dist/runtime/heapEligibility.js.map +1 -0
- package/dist/runtime/heapScene.d.ts +240 -31
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +3334 -265
- package/dist/runtime/heapScene.js.map +1 -1
- package/dist/runtime/heapShaderFamily.d.ts +132 -0
- package/dist/runtime/heapShaderFamily.d.ts.map +1 -0
- package/dist/runtime/heapShaderFamily.js +866 -0
- package/dist/runtime/heapShaderFamily.js.map +1 -0
- package/dist/runtime/hybridScene.d.ts +133 -0
- package/dist/runtime/hybridScene.d.ts.map +1 -0
- package/dist/runtime/hybridScene.js +159 -0
- package/dist/runtime/hybridScene.js.map +1 -0
- package/dist/runtime/index.d.ts +8 -0
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +9 -0
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/renderTask.d.ts +21 -4
- package/dist/runtime/renderTask.d.ts.map +1 -1
- package/dist/runtime/renderTask.js +127 -32
- package/dist/runtime/renderTask.js.map +1 -1
- package/dist/runtime/runtime.d.ts +33 -3
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +12 -3
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/scenePass.d.ts +28 -3
- package/dist/runtime/scenePass.d.ts.map +1 -1
- package/dist/runtime/scenePass.js +37 -4
- package/dist/runtime/scenePass.js.map +1 -1
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.d.ts +35 -0
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.d.ts.map +1 -0
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.js +247 -0
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.js.map +1 -0
- package/dist/runtime/textureAtlas/atlasPool.d.ts +240 -0
- package/dist/runtime/textureAtlas/atlasPool.d.ts.map +1 -0
- package/dist/runtime/textureAtlas/atlasPool.js +798 -0
- package/dist/runtime/textureAtlas/atlasPool.js.map +1 -0
- package/dist/runtime/textureAtlas/bvhTree2d.d.ts +33 -0
- package/dist/runtime/textureAtlas/bvhTree2d.d.ts.map +1 -0
- package/dist/runtime/textureAtlas/bvhTree2d.js +501 -0
- package/dist/runtime/textureAtlas/bvhTree2d.js.map +1 -0
- package/dist/runtime/textureAtlas/packer.d.ts +50 -0
- package/dist/runtime/textureAtlas/packer.d.ts.map +1 -0
- package/dist/runtime/textureAtlas/packer.js +390 -0
- package/dist/runtime/textureAtlas/packer.js.map +1 -0
- package/dist/window/loop.d.ts +14 -0
- package/dist/window/loop.d.ts.map +1 -1
- package/dist/window/loop.js +18 -5
- package/dist/window/loop.js.map +1 -1
- package/package.json +5 -4
- package/src/core/command.ts +9 -9
- package/src/core/renderTask.ts +55 -2
- package/src/resources/preparedRenderObject.ts +3 -3
- package/src/resources/uniformBuffer.ts +15 -0
- package/src/runtime/derivedUniforms/dispatch.ts +407 -0
- package/src/runtime/derivedUniforms/index.ts +52 -0
- package/src/runtime/derivedUniforms/recipes.ts +159 -0
- package/src/runtime/derivedUniforms/sceneIntegration.ts +239 -0
- package/src/runtime/derivedUniforms/slotId.ts +35 -0
- package/src/runtime/derivedUniforms/slots.ts +155 -0
- package/src/runtime/derivedUniforms/uberKernel.wgsl.ts +220 -0
- package/src/runtime/flattenTree.ts +46 -0
- package/src/runtime/heapAdapter.ts +314 -0
- package/src/runtime/heapEffect.ts +719 -0
- package/src/runtime/heapEffectIR.ts +1211 -0
- package/src/runtime/heapEligibility.ts +189 -0
- package/src/runtime/heapScene.ts +3981 -349
- package/src/runtime/heapShaderFamily.ts +1102 -0
- package/src/runtime/hybridScene.ts +310 -0
- package/src/runtime/index.ts +46 -0
- package/src/runtime/renderTask.ts +167 -32
- package/src/runtime/runtime.ts +37 -4
- package/src/runtime/scenePass.ts +39 -4
- package/src/runtime/textureAtlas/atlasMipGutterKernel.ts +291 -0
- package/src/runtime/textureAtlas/atlasPool.ts +991 -0
- package/src/runtime/textureAtlas/bvhTree2d.ts +530 -0
- package/src/runtime/textureAtlas/packer.ts +429 -0
- package/src/window/loop.ts +32 -6
|
@@ -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
|