@aardworx/wombat.rendering 0.9.2 → 0.9.4
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/buffer.d.ts.map +1 -1
- package/dist/core/buffer.js +65 -5
- package/dist/core/buffer.js.map +1 -1
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/provider.d.ts +38 -0
- package/dist/core/provider.d.ts.map +1 -0
- package/dist/core/provider.js +104 -0
- package/dist/core/provider.js.map +1 -0
- package/dist/core/renderObject.d.ts +17 -10
- package/dist/core/renderObject.d.ts.map +1 -1
- package/dist/core/sampler.d.ts.map +1 -1
- package/dist/core/sampler.js +71 -2
- package/dist/core/sampler.js.map +1 -1
- package/dist/core/texture.d.ts +9 -0
- package/dist/core/texture.d.ts.map +1 -1
- package/dist/core/texture.js +0 -0
- package/dist/core/texture.js.map +1 -1
- package/dist/resources/adaptiveTexture.d.ts.map +1 -1
- package/dist/resources/adaptiveTexture.js +7 -0
- package/dist/resources/adaptiveTexture.js.map +1 -1
- package/dist/resources/preparedRenderObject.d.ts.map +1 -1
- package/dist/resources/preparedRenderObject.js +20 -18
- package/dist/resources/preparedRenderObject.js.map +1 -1
- package/dist/resources/uniformBuffer.d.ts.map +1 -1
- package/dist/resources/uniformBuffer.js +23 -0
- package/dist/resources/uniformBuffer.js.map +1 -1
- package/dist/runtime/derivedUniforms/slots.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/slots.js +4 -0
- package/dist/runtime/derivedUniforms/slots.js.map +1 -1
- package/dist/runtime/heapAdapter.d.ts.map +1 -1
- package/dist/runtime/heapAdapter.js +72 -29
- package/dist/runtime/heapAdapter.js.map +1 -1
- package/dist/runtime/heapDecoder.d.ts +52 -0
- package/dist/runtime/heapDecoder.d.ts.map +1 -0
- package/dist/runtime/heapDecoder.js +363 -0
- package/dist/runtime/heapDecoder.js.map +1 -0
- package/dist/runtime/heapEffectIR.d.ts.map +1 -1
- package/dist/runtime/heapEffectIR.js +300 -3
- package/dist/runtime/heapEffectIR.js.map +1 -1
- package/dist/runtime/heapEligibility.d.ts.map +1 -1
- package/dist/runtime/heapEligibility.js +42 -5
- package/dist/runtime/heapEligibility.js.map +1 -1
- package/dist/runtime/heapIrBuilders.d.ts +64 -0
- package/dist/runtime/heapIrBuilders.d.ts.map +1 -0
- package/dist/runtime/heapIrBuilders.js +311 -0
- package/dist/runtime/heapIrBuilders.js.map +1 -0
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +329 -112
- package/dist/runtime/heapScene.js.map +1 -1
- package/dist/runtime/textureAtlas/atlasPool.d.ts +67 -4
- package/dist/runtime/textureAtlas/atlasPool.d.ts.map +1 -1
- package/dist/runtime/textureAtlas/atlasPool.js +152 -18
- package/dist/runtime/textureAtlas/atlasPool.js.map +1 -1
- package/package.json +3 -3
- package/src/core/buffer.ts +58 -6
- package/src/core/index.ts +11 -0
- package/src/core/provider.ts +143 -0
- package/src/core/renderObject.ts +17 -10
- package/src/core/sampler.ts +64 -2
- package/src/core/texture.ts +0 -0
- package/src/resources/adaptiveTexture.ts +7 -0
- package/src/resources/preparedRenderObject.ts +21 -16
- package/src/resources/uniformBuffer.ts +21 -0
- package/src/runtime/derivedUniforms/slots.ts +4 -0
- package/src/runtime/heapAdapter.ts +63 -29
- package/src/runtime/heapDecoder.ts +446 -0
- package/src/runtime/heapEffectIR.ts +314 -3
- package/src/runtime/heapEligibility.ts +34 -5
- package/src/runtime/heapIrBuilders.ts +365 -0
- package/src/runtime/heapScene.ts +338 -96
- package/src/runtime/textureAtlas/atlasPool.ts +152 -16
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
// heapDecoder — synthesise a vertex stage that loads every input/uniform
|
|
2
|
+
// the user effect reads from the heap and surfaces them on the inter-
|
|
3
|
+
// stage carrier.
|
|
4
|
+
//
|
|
5
|
+
// Composition model:
|
|
6
|
+
//
|
|
7
|
+
// effect(decoderEffect, ...userEffect.stages)
|
|
8
|
+
//
|
|
9
|
+
// `composeStages` fuses the decoder VS with the user VS into a single
|
|
10
|
+
// merged entry. The decoder writes `Position`, `ModelTrafo`, etc.; the
|
|
11
|
+
// user VS reads those names from `Input` scope (after `uniformsToInputs`
|
|
12
|
+
// renames its `Uniform`-scope reads to `Input`). Stage fusion's
|
|
13
|
+
// state-struct path threads the values through helpers naturally — no
|
|
14
|
+
// in-place IR rewriting, no `extractFusedEntry` wrapper-init gap, no
|
|
15
|
+
// post-emit string mangling.
|
|
16
|
+
//
|
|
17
|
+
// The decoder is an IR Module: storage-buffer bindings for heap arena +
|
|
18
|
+
// megacall lookup buffers, plus a single vertex Entry. Body shape:
|
|
19
|
+
//
|
|
20
|
+
// @vertex fn heap_decode(@builtin(vertex_index) emitIdx: u32) -> Out {
|
|
21
|
+
// // megacall binary search → heap_drawIdx / instId / vid
|
|
22
|
+
// ...
|
|
23
|
+
// out.<name> = loadAttributeByRef(...); // attribute-ref fields
|
|
24
|
+
// out.<name> = loadUniformByRef(...); // uniform-ref fields
|
|
25
|
+
// return out;
|
|
26
|
+
// }
|
|
27
|
+
//
|
|
28
|
+
// gl_Position is NOT written here — the user VS owns it, and the fused
|
|
29
|
+
// entry's last writer wins (linkCrossStage semantics).
|
|
30
|
+
//
|
|
31
|
+
// Texture-ref drawHeader fields ARE surfaced through the carrier as
|
|
32
|
+
// flat-interpolated varyings — four per atlas-routed texture binding
|
|
33
|
+
// (`<name>__atlasPageRef: u32`, `<name>__atlasFormatBits: u32`,
|
|
34
|
+
// `<name>__atlasOrigin: vec2<f32>`, `<name>__atlasSize: vec2<f32>`).
|
|
35
|
+
// The matching FS-side rewrite turns each `textureSample(t, smp, uv)`
|
|
36
|
+
// into `atlasSample(<t>__atlasPageRef, …, uv)` reading from the
|
|
37
|
+
// carrier — see `rewriteFsAtlasTexturesViaCarrier` below.
|
|
38
|
+
|
|
39
|
+
import {
|
|
40
|
+
stage as makeStage,
|
|
41
|
+
type Effect,
|
|
42
|
+
} from "@aardworx/wombat.shader";
|
|
43
|
+
import {
|
|
44
|
+
type Module, type Expr, type Stmt, type Type, type ValueDef,
|
|
45
|
+
type EntryDef, type EntryParameter, type Var,
|
|
46
|
+
Tu32, Tf32,
|
|
47
|
+
} from "@aardworx/wombat.shader/ir";
|
|
48
|
+
import type { BucketLayout, DrawHeaderField } from "./heapEffect.js";
|
|
49
|
+
import {
|
|
50
|
+
Tvec2, Tvec3, Tvec4, Tmat4, TarrU32,
|
|
51
|
+
constU32, add, mul, div, item, newVec, readScope,
|
|
52
|
+
loadHeaderRef, loadUniformByRef, loadAttributeByRef, loadInstanceByRef,
|
|
53
|
+
buildMegacallPrelude,
|
|
54
|
+
heapArenaStorageDecls, megacallLookupStorageDecls,
|
|
55
|
+
headersU32,
|
|
56
|
+
} from "./heapIrBuilders.js";
|
|
57
|
+
|
|
58
|
+
// Build an Expr that returns the u32 value of a specific inline u32
|
|
59
|
+
// slot in the drawHeader: `headersU32[drawIdx * stride + offset_u32]`.
|
|
60
|
+
function readHeaderU32(drawIdx: Expr, byteOffset: number, strideU32: number): Expr {
|
|
61
|
+
if ((byteOffset % 4) !== 0) {
|
|
62
|
+
throw new Error(`readHeaderU32: byte offset ${byteOffset} not 4-aligned`);
|
|
63
|
+
}
|
|
64
|
+
const fieldU32 = byteOffset / 4;
|
|
65
|
+
const base = mul(drawIdx, constU32(strideU32), Tu32);
|
|
66
|
+
const idx = fieldU32 === 0 ? base : add(base, constU32(fieldU32), Tu32);
|
|
67
|
+
return item(headersU32, idx, Tu32);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Read two contiguous u32 slots, reinterpret as f32, return as vec2<f32>.
|
|
71
|
+
// Used for the inline `origin: vec2<f32>` and `size: vec2<f32>` atlas
|
|
72
|
+
// drawHeader entries — the runtime packs them via Float32Array view
|
|
73
|
+
// over the same backing buffer, so on the GPU side the bits at u32
|
|
74
|
+
// offset are valid f32 values; we just need to address them and
|
|
75
|
+
// bitcast.
|
|
76
|
+
function readHeaderVec2F(drawIdx: Expr, byteOffset: number, strideU32: number): Expr {
|
|
77
|
+
if ((byteOffset % 4) !== 0) {
|
|
78
|
+
throw new Error(`readHeaderVec2F: byte offset ${byteOffset} not 4-aligned`);
|
|
79
|
+
}
|
|
80
|
+
const fieldU32 = byteOffset / 4;
|
|
81
|
+
const base = mul(drawIdx, constU32(strideU32), Tu32);
|
|
82
|
+
const idx0 = fieldU32 === 0 ? base : add(base, constU32(fieldU32), Tu32);
|
|
83
|
+
const idx1 = add(base, constU32(fieldU32 + 1), Tu32);
|
|
84
|
+
// headersU32 is u32; we need f32. Use the same trick the runtime does:
|
|
85
|
+
// expose a parallel `headersF32` view over the same buffer? That
|
|
86
|
+
// doesn't exist yet — for now bitcast via a CallIntrinsic on `bitcast<f32>`.
|
|
87
|
+
const bitcast = (e: Expr): Expr => ({
|
|
88
|
+
kind: "CallIntrinsic",
|
|
89
|
+
op: {
|
|
90
|
+
name: "bitcast<f32>",
|
|
91
|
+
returnTypeOf: () => Tf32,
|
|
92
|
+
pure: true,
|
|
93
|
+
emit: { wgsl: "bitcast<f32>", glsl: "uintBitsToFloat" },
|
|
94
|
+
},
|
|
95
|
+
args: [e],
|
|
96
|
+
type: Tf32,
|
|
97
|
+
} as Expr);
|
|
98
|
+
return newVec(
|
|
99
|
+
[bitcast(item(headersU32, idx0, Tu32)), bitcast(item(headersU32, idx1, Tu32))],
|
|
100
|
+
Tvec2,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Decoder-side carrier name for an atlas texture sub-field. Mirrors the
|
|
106
|
+
* existing `_h_<name><Sub>` naming used by `atlasVaryingNames` in
|
|
107
|
+
* heapEffect.ts so the FS-side `textureSample → atlasSample` rewrite
|
|
108
|
+
* can keep reading them by the same names.
|
|
109
|
+
*/
|
|
110
|
+
export function atlasCarrierName(textureBindingName: string, sub: "pageRef" | "formatBits" | "origin" | "size"): string {
|
|
111
|
+
const cap = sub.charAt(0).toUpperCase() + sub.slice(1);
|
|
112
|
+
return `_h_${textureBindingName}${cap}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─── wgsl-type-string → IR Type ──────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Convert the WGSL type-string carried on `DrawHeaderField.{uniformWgslType,
|
|
119
|
+
* attributeWgslType}` back into an IR `Type`. The string set is closed:
|
|
120
|
+
* it's whatever `irTypeToWgsl` in heapEffect.ts emits when populating
|
|
121
|
+
* the bucket schema, so the reverse map is finite and exact.
|
|
122
|
+
*/
|
|
123
|
+
function wgslTypeToIrType(wgsl: string): Type {
|
|
124
|
+
switch (wgsl) {
|
|
125
|
+
case "f32": return Tf32;
|
|
126
|
+
case "u32": return Tu32;
|
|
127
|
+
case "i32": return { kind: "Int", signed: true, width: 32 } as Type;
|
|
128
|
+
case "vec2<f32>": return Tvec2;
|
|
129
|
+
case "vec3<f32>": return Tvec3;
|
|
130
|
+
case "vec4<f32>": return Tvec4;
|
|
131
|
+
case "mat4x4<f32>": return Tmat4;
|
|
132
|
+
default:
|
|
133
|
+
throw new Error(`heapDecoder: cannot translate WGSL type '${wgsl}' to IR Type`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ─── Decoder synthesis ───────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Decoder emission shape.
|
|
141
|
+
*
|
|
142
|
+
* - `"standalone"`: the decoder owns the megacall binary search.
|
|
143
|
+
* `@builtin(vertex_index)` is its sole entry input, and the body's
|
|
144
|
+
* prelude computes `heap_drawIdx` / `instId` / `vid` as local
|
|
145
|
+
* `let`s. Used by the canonical heap path.
|
|
146
|
+
*
|
|
147
|
+
* - `"family-member"`: the family wrapper VS already ran the
|
|
148
|
+
* megacall search and dispatches into per-effect helpers passing
|
|
149
|
+
* `heap_drawIdx`, `instId`, `vid` as plain `u32` args. The decoder
|
|
150
|
+
* consumes those as `EntryParameter`s (non-builtin) and skips the
|
|
151
|
+
* prelude entirely. After `extractFusedEntry` collapses the
|
|
152
|
+
* decoder + user stages, the per-effect helper exposed to the
|
|
153
|
+
* wrapper has exactly three `u32` parameters and one output struct
|
|
154
|
+
* return.
|
|
155
|
+
*/
|
|
156
|
+
export type HeapDecoderMode = "standalone" | "family-member";
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Build a fresh wombat.shader Effect containing exactly one vertex
|
|
160
|
+
* stage — the heap-decoder. Compose it with the user effect's stages
|
|
161
|
+
* via `effect(decoder, ...userEffect.stages)`; let composeStages do
|
|
162
|
+
* the rest.
|
|
163
|
+
*
|
|
164
|
+
* Required input: the bucket layout. Reading the user's iface to
|
|
165
|
+
* derive it happens upstream (heapScene already builds the layout from
|
|
166
|
+
* `effect.compile(...).interface`).
|
|
167
|
+
*
|
|
168
|
+
* Each `attribute-ref` field in the layout produces a per-vertex (or
|
|
169
|
+
* per-instance, depending on `layout.perInstanceAttributes`) load. Each
|
|
170
|
+
* `uniform-ref` field produces a per-draw broadcast load (or per-
|
|
171
|
+
* instance from `layout.perInstanceUniforms`). Atlas texture-ref
|
|
172
|
+
* fields surface as four flat varyings per binding so the FS-side
|
|
173
|
+
* atlas-sample rewrite can read them from the carrier.
|
|
174
|
+
*/
|
|
175
|
+
export function synthesizeHeapDecoderEffect(layout: BucketLayout, mode: HeapDecoderMode = "standalone"): Effect {
|
|
176
|
+
return makeStage(synthesizeHeapDecoderModule(layout, mode));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Like `synthesizeHeapDecoderEffect` but returns the raw Module so
|
|
181
|
+
* callers can splice it into an existing module-merge pipeline.
|
|
182
|
+
*/
|
|
183
|
+
export function synthesizeHeapDecoderModule(layout: BucketLayout, mode: HeapDecoderMode = "standalone"): Module {
|
|
184
|
+
// Storage bindings: arena always required; megacall lookup buffers
|
|
185
|
+
// only when the decoder itself owns the binary search (standalone
|
|
186
|
+
// mode). Family-member mode receives the resolved triple from the
|
|
187
|
+
// wrapper VS via plain u32 fn parameters, so it doesn't need the
|
|
188
|
+
// lookup tables in its own module-scope binding set.
|
|
189
|
+
const storageDecls: ValueDef[] = mode === "standalone"
|
|
190
|
+
? [...heapArenaStorageDecls(), ...megacallLookupStorageDecls()]
|
|
191
|
+
: [...heapArenaStorageDecls()];
|
|
192
|
+
|
|
193
|
+
const inputs: EntryParameter[] = [];
|
|
194
|
+
const outputs: EntryParameter[] = [];
|
|
195
|
+
const stmts: Stmt[] = [];
|
|
196
|
+
|
|
197
|
+
// Locals (or input params) that downstream WriteOutput stmts pull
|
|
198
|
+
// from. Populated below depending on `mode`.
|
|
199
|
+
let drawIdxExpr: Expr;
|
|
200
|
+
let instIdExpr: Expr;
|
|
201
|
+
let vidExpr: Expr;
|
|
202
|
+
|
|
203
|
+
if (mode === "standalone") {
|
|
204
|
+
// Single vertex entry input: the megacall builtin. WGSL emits
|
|
205
|
+
// builtin reads as the *semantic* name; `name === semantic`
|
|
206
|
+
// keeps the @vertex param identifier matching the body.
|
|
207
|
+
inputs.push({
|
|
208
|
+
name: "vertex_index",
|
|
209
|
+
type: Tu32,
|
|
210
|
+
semantic: "vertex_index",
|
|
211
|
+
decorations: [{ kind: "Builtin", value: "vertex_index" }],
|
|
212
|
+
});
|
|
213
|
+
// Read via ReadInput("Builtin", ...): extractFusedEntry detects
|
|
214
|
+
// builtin reads through this scope; a bare `Var` reference would
|
|
215
|
+
// slip past its bodyReads analysis and the fused @vertex
|
|
216
|
+
// wouldn't surface the builtin at all.
|
|
217
|
+
const emitIdxExpr: Expr = { kind: "ReadInput", scope: "Builtin", name: "vertex_index", type: Tu32 };
|
|
218
|
+
// Megacall search → locals heap_drawIdx, instId, vid.
|
|
219
|
+
const { stmts: megacallStmts, locals } = buildMegacallPrelude(emitIdxExpr);
|
|
220
|
+
stmts.push(...megacallStmts);
|
|
221
|
+
drawIdxExpr = { kind: "Var", var: locals.heapDrawIdx, type: Tu32 };
|
|
222
|
+
instIdExpr = { kind: "Var", var: locals.instId, type: Tu32 };
|
|
223
|
+
vidExpr = { kind: "Var", var: locals.vid, type: Tu32 };
|
|
224
|
+
} else {
|
|
225
|
+
// Family-member mode: receive the megacall outputs as plain u32
|
|
226
|
+
// params from the wrapper. Declare them as EntryParameters
|
|
227
|
+
// (location-decorated so they survive composeStages' attribute
|
|
228
|
+
// classification), then read them via ReadInput("Input", ...)
|
|
229
|
+
// inside the body. After composition the fused entry surfaces
|
|
230
|
+
// them as inputs that the wrapper passes by name.
|
|
231
|
+
const declParam = (name: string, location: number): EntryParameter => ({
|
|
232
|
+
name,
|
|
233
|
+
type: Tu32,
|
|
234
|
+
semantic: name,
|
|
235
|
+
decorations: [
|
|
236
|
+
{ kind: "Location", value: location },
|
|
237
|
+
// u32 inter-stage IO must be flat-interpolated.
|
|
238
|
+
{ kind: "Interpolation", mode: "flat" },
|
|
239
|
+
],
|
|
240
|
+
});
|
|
241
|
+
inputs.push(declParam("heap_drawIdx", 0));
|
|
242
|
+
inputs.push(declParam("instId", 1));
|
|
243
|
+
inputs.push(declParam("vid", 2));
|
|
244
|
+
drawIdxExpr = { kind: "ReadInput", scope: "Input", name: "heap_drawIdx", type: Tu32 };
|
|
245
|
+
instIdExpr = { kind: "ReadInput", scope: "Input", name: "instId", type: Tu32 };
|
|
246
|
+
vidExpr = { kind: "ReadInput", scope: "Input", name: "vid", type: Tu32 };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Walk the bucket layout's drawHeader fields. The order doesn't
|
|
250
|
+
// matter for correctness — every surfaced name becomes a unique
|
|
251
|
+
// output and the carrier carries them all.
|
|
252
|
+
let nextLocation = 0;
|
|
253
|
+
// Texture-ref drawHeader fields are grouped by `textureBindingName` —
|
|
254
|
+
// each atlas-routed texture binding produces four contiguous entries
|
|
255
|
+
// (pageRef / formatBits / origin / size). We emit them as four
|
|
256
|
+
// distinct carrier outputs `<name>__atlasPageRef`, etc., so the
|
|
257
|
+
// FS-side `textureSample(t, …)` rewrite can read them by name.
|
|
258
|
+
for (const f of layout.drawHeaderFields) {
|
|
259
|
+
if (f.kind === "uniform-ref") {
|
|
260
|
+
addUniformOutput(f, layout, drawIdxExpr, instIdExpr, outputs, stmts, nextLocation++);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (f.kind === "attribute-ref") {
|
|
264
|
+
addAttributeOutput(f, layout, drawIdxExpr, instIdExpr, vidExpr, outputs, stmts, nextLocation++);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (f.kind === "texture-ref") {
|
|
268
|
+
addTextureSubOutput(f, layout, drawIdxExpr, outputs, stmts, nextLocation++);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const entry: EntryDef = {
|
|
274
|
+
name: "heap_decode_vs",
|
|
275
|
+
stage: "vertex",
|
|
276
|
+
inputs,
|
|
277
|
+
outputs,
|
|
278
|
+
arguments: [],
|
|
279
|
+
returnType: { kind: "Void" } as Type,
|
|
280
|
+
body: { kind: "Sequential", body: stmts },
|
|
281
|
+
decorations: [],
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
types: [],
|
|
286
|
+
values: [...storageDecls, { kind: "Entry", entry }],
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Emit a single `WriteOutput(name, loadUniformByRef(...))` for a
|
|
292
|
+
* `uniform-ref` field. Surfaces a per-name output the user VS will
|
|
293
|
+
* read as `ReadInput("Input", name)` post-uniformsToInputs.
|
|
294
|
+
*/
|
|
295
|
+
function addUniformOutput(
|
|
296
|
+
f: DrawHeaderField,
|
|
297
|
+
layout: BucketLayout,
|
|
298
|
+
drawIdxExpr: Expr,
|
|
299
|
+
instIdExpr: Expr,
|
|
300
|
+
outputs: EntryParameter[],
|
|
301
|
+
stmts: Stmt[],
|
|
302
|
+
location: number,
|
|
303
|
+
): void {
|
|
304
|
+
const wgslType = f.uniformWgslType;
|
|
305
|
+
if (wgslType === undefined) {
|
|
306
|
+
throw new Error(`heapDecoder: uniform-ref field '${f.name}' has no uniformWgslType`);
|
|
307
|
+
}
|
|
308
|
+
const irType = wgslTypeToIrType(wgslType);
|
|
309
|
+
const refExpr = loadHeaderRef(drawIdxExpr, f.byteOffset, layout.strideU32);
|
|
310
|
+
const value = layout.perInstanceUniforms.has(f.name)
|
|
311
|
+
? loadInstanceByRef(refExpr, instIdExpr, wgslType)
|
|
312
|
+
: loadUniformByRef(refExpr, wgslType);
|
|
313
|
+
|
|
314
|
+
outputs.push({
|
|
315
|
+
name: f.name,
|
|
316
|
+
type: irType,
|
|
317
|
+
semantic: f.name,
|
|
318
|
+
decorations: [
|
|
319
|
+
{ kind: "Location", value: location },
|
|
320
|
+
// Per-draw broadcast — uniform across all vertices of one draw.
|
|
321
|
+
// `flat` interpolation skips the rasterizer's perspective-correct
|
|
322
|
+
// interpolation; required for u32 carriers anyway, harmless for
|
|
323
|
+
// float carriers that are constant across the draw.
|
|
324
|
+
{ kind: "Interpolation", mode: "flat" },
|
|
325
|
+
],
|
|
326
|
+
});
|
|
327
|
+
stmts.push({
|
|
328
|
+
kind: "WriteOutput",
|
|
329
|
+
name: f.name,
|
|
330
|
+
value: { kind: "Expr", value },
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Emit a `WriteOutput` for a single atlas texture-ref sub-field.
|
|
336
|
+
* Atlas drawHeader entries carry the value INLINE (not as a heap ref).
|
|
337
|
+
* Layout (matches `buildBucketLayout` in heapEffect.ts):
|
|
338
|
+
* pageRef : u32 — 4 bytes
|
|
339
|
+
* formatBits : u32 — 4 bytes
|
|
340
|
+
* (padding to 8-aligned)
|
|
341
|
+
* origin : vec2<f32> — 8 bytes
|
|
342
|
+
* size : vec2<f32> — 8 bytes
|
|
343
|
+
*
|
|
344
|
+
* The carrier output name follows `<textureBindingName>__atlas<Sub>`.
|
|
345
|
+
*/
|
|
346
|
+
function addTextureSubOutput(
|
|
347
|
+
f: DrawHeaderField,
|
|
348
|
+
layout: BucketLayout,
|
|
349
|
+
drawIdxExpr: Expr,
|
|
350
|
+
outputs: EntryParameter[],
|
|
351
|
+
stmts: Stmt[],
|
|
352
|
+
location: number,
|
|
353
|
+
): void {
|
|
354
|
+
const sub = f.textureSub;
|
|
355
|
+
const tex = f.textureBindingName;
|
|
356
|
+
if (sub === undefined || tex === undefined) {
|
|
357
|
+
throw new Error(`heapDecoder: texture-ref field '${f.name}' missing textureSub/textureBindingName`);
|
|
358
|
+
}
|
|
359
|
+
const carrierName = atlasCarrierName(tex, sub);
|
|
360
|
+
let value: Expr;
|
|
361
|
+
let irType: Type;
|
|
362
|
+
switch (sub) {
|
|
363
|
+
case "pageRef":
|
|
364
|
+
case "formatBits": {
|
|
365
|
+
irType = Tu32;
|
|
366
|
+
value = readHeaderU32(drawIdxExpr, f.byteOffset, layout.strideU32);
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
case "origin":
|
|
370
|
+
case "size": {
|
|
371
|
+
irType = Tvec2;
|
|
372
|
+
value = readHeaderVec2F(drawIdxExpr, f.byteOffset, layout.strideU32);
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
outputs.push({
|
|
377
|
+
name: carrierName,
|
|
378
|
+
type: irType,
|
|
379
|
+
semantic: carrierName,
|
|
380
|
+
decorations: [
|
|
381
|
+
{ kind: "Location", value: location },
|
|
382
|
+
// Atlas params are constant across all fragments of a primitive
|
|
383
|
+
// — flat interpolation skips the rasterizer's per-pixel math and
|
|
384
|
+
// is mandatory for u32 carriers.
|
|
385
|
+
{ kind: "Interpolation", mode: "flat" },
|
|
386
|
+
],
|
|
387
|
+
});
|
|
388
|
+
stmts.push({
|
|
389
|
+
kind: "WriteOutput",
|
|
390
|
+
name: carrierName,
|
|
391
|
+
value: { kind: "Expr", value },
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Emit a single `WriteOutput(name, loadAttributeByRef(...))` for an
|
|
397
|
+
* `attribute-ref` field. Uses per-vertex `vid` for plain attributes
|
|
398
|
+
* and per-instance `instId` for instance-attributes.
|
|
399
|
+
*/
|
|
400
|
+
function addAttributeOutput(
|
|
401
|
+
f: DrawHeaderField,
|
|
402
|
+
layout: BucketLayout,
|
|
403
|
+
drawIdxExpr: Expr,
|
|
404
|
+
instIdExpr: Expr,
|
|
405
|
+
vidExpr: Expr,
|
|
406
|
+
outputs: EntryParameter[],
|
|
407
|
+
stmts: Stmt[],
|
|
408
|
+
location: number,
|
|
409
|
+
): void {
|
|
410
|
+
const wgslType = f.attributeWgslType;
|
|
411
|
+
if (wgslType === undefined) {
|
|
412
|
+
throw new Error(`heapDecoder: attribute-ref field '${f.name}' has no attributeWgslType`);
|
|
413
|
+
}
|
|
414
|
+
const irType = wgslTypeToIrType(wgslType);
|
|
415
|
+
const refExpr = loadHeaderRef(drawIdxExpr, f.byteOffset, layout.strideU32);
|
|
416
|
+
const idx = layout.perInstanceAttributes.has(f.name) ? instIdExpr : vidExpr;
|
|
417
|
+
const value = loadAttributeByRef(refExpr, idx, wgslType);
|
|
418
|
+
|
|
419
|
+
// Per-vertex attributes naturally vary across a primitive — perspective-
|
|
420
|
+
// correct interpolation is the right default. Per-instance attributes
|
|
421
|
+
// are constant across a primitive's vertices but in this fused-stage
|
|
422
|
+
// model they still flow through the carrier, so flat is the safe
|
|
423
|
+
// policy (no semantic difference for genuinely-constant values).
|
|
424
|
+
// Integer-typed inter-stage IO MUST be `flat` (WGSL constraint).
|
|
425
|
+
const isInteger =
|
|
426
|
+
irType.kind === "Int" || irType.kind === "Bool"
|
|
427
|
+
|| (irType.kind === "Vector"
|
|
428
|
+
&& (irType.element.kind === "Int" || irType.element.kind === "Bool"));
|
|
429
|
+
const interp: "smooth" | "flat" =
|
|
430
|
+
layout.perInstanceAttributes.has(f.name) || isInteger ? "flat" : "smooth";
|
|
431
|
+
|
|
432
|
+
outputs.push({
|
|
433
|
+
name: f.name,
|
|
434
|
+
type: irType,
|
|
435
|
+
semantic: f.name,
|
|
436
|
+
decorations: [
|
|
437
|
+
{ kind: "Location", value: location },
|
|
438
|
+
{ kind: "Interpolation", mode: interp },
|
|
439
|
+
],
|
|
440
|
+
});
|
|
441
|
+
stmts.push({
|
|
442
|
+
kind: "WriteOutput",
|
|
443
|
+
name: f.name,
|
|
444
|
+
value: { kind: "Expr", value },
|
|
445
|
+
});
|
|
446
|
+
}
|