@aardworx/wombat.rendering 0.19.7 → 0.19.9
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/runtime/heapAdapter.d.ts.map +1 -1
- package/dist/runtime/heapAdapter.js +115 -17
- package/dist/runtime/heapAdapter.js.map +1 -1
- package/dist/runtime/heapEffect.js +1 -1
- package/dist/runtime/heapEffect.js.map +1 -1
- package/dist/runtime/heapEligibility.d.ts.map +1 -1
- package/dist/runtime/heapEligibility.js +32 -49
- package/dist/runtime/heapEligibility.js.map +1 -1
- package/dist/runtime/heapIrBuilders.d.ts.map +1 -1
- package/dist/runtime/heapIrBuilders.js +7 -2
- package/dist/runtime/heapIrBuilders.js.map +1 -1
- package/dist/runtime/heapScene.d.ts +21 -1
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +58 -12
- package/dist/runtime/heapScene.js.map +1 -1
- package/package.json +1 -1
- package/src/runtime/heapAdapter.ts +113 -20
- package/src/runtime/heapEffect.ts +1 -1
- package/src/runtime/heapEligibility.ts +27 -41
- package/src/runtime/heapIrBuilders.ts +12 -2
- package/src/runtime/heapScene.ts +47 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aardworx/wombat.rendering",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.9",
|
|
4
4
|
"description": "WebGPU rendering layer for the Wombat TypeScript stack — RenderObject/RenderTask/runtime + window glue, port of Aardvark.Rendering's lower layers on top of WebGPU and wombat.shader.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "krauthaufen",
|
|
@@ -27,13 +27,14 @@
|
|
|
27
27
|
// `ro.samplers.count <= 1` are classifier invariants.
|
|
28
28
|
|
|
29
29
|
import { type aval, type AdaptiveToken } from "@aardworx/wombat.adaptive";
|
|
30
|
-
import
|
|
31
|
-
import
|
|
30
|
+
import { IBuffer, type HostBufferSource } from "../core/buffer.js";
|
|
31
|
+
import { BufferView } from "../core/bufferView.js";
|
|
32
32
|
import { ITexture, type HostTextureSource } from "../core/texture.js";
|
|
33
33
|
import { ISampler } from "../core/sampler.js";
|
|
34
34
|
import type { RenderObject } from "../core/renderObject.js";
|
|
35
35
|
import { asAttributeProvider, asUniformProvider } from "../core/provider.js";
|
|
36
36
|
import type { HeapDrawSpec, HeapTextureSet } from "./heapScene.js";
|
|
37
|
+
import { isBufferView } from "./heapScene/pools.js";
|
|
37
38
|
import { compileHeapEffect } from "./heapEffect.js";
|
|
38
39
|
import { AtlasPool } from "./textureAtlas/atlasPool.js";
|
|
39
40
|
|
|
@@ -50,13 +51,29 @@ function asUint32(data: HostBufferSource): Uint32Array {
|
|
|
50
51
|
return new Uint32Array(data);
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
// Widen 16-bit indices to u32 (the heap's indexStorage is u32). Copy, not a
|
|
55
|
+
// reinterpret view — each u16 index becomes a u32 element.
|
|
56
|
+
function widenU16(data: HostBufferSource): Uint32Array {
|
|
57
|
+
const u16 = data instanceof Uint16Array
|
|
58
|
+
? data
|
|
59
|
+
: ArrayBuffer.isView(data)
|
|
60
|
+
? new Uint16Array(data.buffer, data.byteOffset, data.byteLength / 2)
|
|
61
|
+
: new Uint16Array(data);
|
|
62
|
+
return Uint32Array.from(u16);
|
|
63
|
+
}
|
|
64
|
+
|
|
53
65
|
// IndexPool keys on aval identity → ROs sharing the same
|
|
54
66
|
// `ro.indices.buffer` must produce the SAME downstream
|
|
55
67
|
// `aval<Uint32Array>`. A fresh `.map(…)` per RO would defeat the
|
|
56
68
|
// pool's identity-based sharing and balloon GPU memory (e.g. 11K
|
|
57
69
|
// spheres each allocating their own copy of the 6144-index buffer
|
|
58
70
|
// → 270 MB).
|
|
59
|
-
|
|
71
|
+
// Keyed by (buffer aval, baseVertex). baseVertex is baked into the index
|
|
72
|
+
// VALUES (index[i] + baseVertex), so a buffer drawn with two different
|
|
73
|
+
// baseVertex offsets needs two distinct transcoded arrays — but the common
|
|
74
|
+
// case (baseVertex 0, or per-primitive index buffers that aren't shared)
|
|
75
|
+
// keeps one entry per buffer and preserves the pool's identity sharing.
|
|
76
|
+
const indicesAvalCache = new WeakMap<aval<IBuffer>, Map<number, aval<Uint32Array>>>();
|
|
60
77
|
|
|
61
78
|
// Per-aval shared "current pool ref" cell. ROs that share an
|
|
62
79
|
// `aval<ITexture>` (and therefore one AtlasPool entry) all read
|
|
@@ -71,20 +88,79 @@ function currentRefCellFor(av: aval<ITexture>, initial: number): { ref: number }
|
|
|
71
88
|
}
|
|
72
89
|
return c;
|
|
73
90
|
}
|
|
74
|
-
function indicesAvalFor(ibAval: aval<IBuffer
|
|
75
|
-
let
|
|
91
|
+
function indicesAvalFor(ibAval: aval<IBuffer>, indexFormat: GPUIndexFormat, baseVertex: number): aval<Uint32Array> {
|
|
92
|
+
let byBase = indicesAvalCache.get(ibAval);
|
|
93
|
+
if (byBase === undefined) {
|
|
94
|
+
byBase = new Map();
|
|
95
|
+
indicesAvalCache.set(ibAval, byBase);
|
|
96
|
+
}
|
|
97
|
+
let av = byBase.get(baseVertex);
|
|
76
98
|
if (av === undefined) {
|
|
77
99
|
av = ibAval.map((ib: IBuffer) => {
|
|
78
100
|
if (ib.kind !== "host") {
|
|
79
101
|
throw new Error("heapAdapter: index buffer flipped to native GPUBuffer; classifier should have caught this");
|
|
80
102
|
}
|
|
81
|
-
|
|
103
|
+
const u32 = indexFormat === "uint16" ? widenU16(ib.data) : asUint32(ib.data);
|
|
104
|
+
if (baseVertex === 0) return u32;
|
|
105
|
+
// Fold baseVertex into the index values so the megacall decode
|
|
106
|
+
// (vid = indexStorage[...]) lands on the right vertex with no
|
|
107
|
+
// extra record field. Copy (don't mutate the shared view).
|
|
108
|
+
const out = new Uint32Array(u32.length);
|
|
109
|
+
for (let i = 0; i < u32.length; i++) out[i] = u32[i]! + baseVertex;
|
|
110
|
+
return out;
|
|
82
111
|
});
|
|
83
|
-
|
|
112
|
+
byBase.set(baseVertex, av);
|
|
84
113
|
}
|
|
85
114
|
return av;
|
|
86
115
|
}
|
|
87
116
|
|
|
117
|
+
// Byte view over a host buffer source (for the de-interleave gather).
|
|
118
|
+
function bytesOf(data: HostBufferSource): Uint8Array {
|
|
119
|
+
if (data instanceof Uint8Array) return data;
|
|
120
|
+
if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
121
|
+
return new Uint8Array(data);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// De-interleaved (tight, offset-0) views, keyed by (source buffer aval,
|
|
125
|
+
// offset, stride) so ROs sharing one interleaved buffer + the same attribute
|
|
126
|
+
// share a single gathered allocation.
|
|
127
|
+
const deinterleaveCache = new WeakMap<aval<IBuffer>, Map<string, aval<IBuffer>>>();
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Make a tight, offset-0 BufferView the arena can ingest. Tight/broadcast
|
|
131
|
+
* views pass through unchanged; interleaved or offset views are gathered
|
|
132
|
+
* into a fresh packed host buffer (every `stride` bytes from `offset`,
|
|
133
|
+
* `byteSize` each) wrapped as a new whole-buffer view.
|
|
134
|
+
*/
|
|
135
|
+
function tightenBufferView(bv: BufferView): BufferView {
|
|
136
|
+
if (bv.singleValue !== undefined) return bv; // broadcast → lowered to a uniform
|
|
137
|
+
const byteSize = bv.elementType.byteSize;
|
|
138
|
+
const offset = bv.offset ?? 0;
|
|
139
|
+
const stride = (bv.stride !== undefined && bv.stride > 0) ? bv.stride : byteSize;
|
|
140
|
+
if (offset === 0 && stride === byteSize) return bv; // already tight
|
|
141
|
+
let byKey = deinterleaveCache.get(bv.buffer);
|
|
142
|
+
if (byKey === undefined) { byKey = new Map(); deinterleaveCache.set(bv.buffer, byKey); }
|
|
143
|
+
const key = `${offset}:${stride}:${byteSize}`;
|
|
144
|
+
let tight = byKey.get(key);
|
|
145
|
+
if (tight === undefined) {
|
|
146
|
+
tight = bv.buffer.map((ib: IBuffer): IBuffer => {
|
|
147
|
+
if (ib.kind !== "host") {
|
|
148
|
+
throw new Error("heapAdapter: interleaved attribute buffer flipped to native GPUBuffer; classifier should have caught this");
|
|
149
|
+
}
|
|
150
|
+
const src = bytesOf(ib.data);
|
|
151
|
+
const count = Math.max(0, Math.floor((src.byteLength - offset - byteSize) / stride) + 1);
|
|
152
|
+
const out = new Uint8Array(count * byteSize);
|
|
153
|
+
for (let i = 0; i < count; i++) {
|
|
154
|
+
const s = offset + i * stride;
|
|
155
|
+
out.set(src.subarray(s, s + byteSize), i * byteSize);
|
|
156
|
+
}
|
|
157
|
+
return IBuffer.fromHost(out);
|
|
158
|
+
});
|
|
159
|
+
byKey.set(key, tight);
|
|
160
|
+
}
|
|
161
|
+
return BufferView.ofBuffer(tight, bv.elementType);
|
|
162
|
+
}
|
|
163
|
+
|
|
88
164
|
/**
|
|
89
165
|
* Pull `(format, width, height, mipLevelCount)` out of an ITexture so
|
|
90
166
|
* we can run Tier-S eligibility against it. Both variants expose this:
|
|
@@ -197,7 +273,7 @@ export function renderObjectToHeapSpec(
|
|
|
197
273
|
const inputs: { [name: string]: aval<unknown> | unknown } = {};
|
|
198
274
|
for (const a of schema.attributes) {
|
|
199
275
|
const bv = vAttr.tryGet(a.name);
|
|
200
|
-
if (bv !== undefined) inputs[a.name] = bv;
|
|
276
|
+
if (bv !== undefined) inputs[a.name] = isBufferView(bv) ? tightenBufferView(bv) : bv;
|
|
201
277
|
}
|
|
202
278
|
const pullUniform = (name: string): void => {
|
|
203
279
|
if (Object.prototype.hasOwnProperty.call(inputs, name)) return;
|
|
@@ -222,17 +298,25 @@ export function renderObjectToHeapSpec(
|
|
|
222
298
|
instanceAttributes = {};
|
|
223
299
|
for (const n of names) {
|
|
224
300
|
const bv = iAttr.tryGet(n);
|
|
225
|
-
if (bv !== undefined) instanceAttributes[n] = bv;
|
|
301
|
+
if (bv !== undefined) instanceAttributes[n] = isBufferView(bv) ? tightenBufferView(bv) : bv;
|
|
226
302
|
}
|
|
227
303
|
}
|
|
228
304
|
}
|
|
229
305
|
|
|
306
|
+
// DrawCall: read once (snapshot — like instanceCount/vertexCount).
|
|
307
|
+
// Slice offsets fold in here: firstIndex → record indexStart,
|
|
308
|
+
// indexCount → record slice length, baseVertex → baked into the
|
|
309
|
+
// transcoded index values (so it needs no record field).
|
|
310
|
+
const dc = ro.drawCall.getValue(token);
|
|
311
|
+
const baseVertex = dc.kind === "indexed" ? dc.baseVertex : 0;
|
|
312
|
+
|
|
230
313
|
// 2. Indices: BufferView → aval<Uint32Array>. Map the underlying
|
|
231
|
-
// IBuffer aval; the heap path's IndexPool will key on this aval
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const indices: aval<Uint32Array> =
|
|
314
|
+
// IBuffer aval; the heap path's IndexPool will key on this aval
|
|
315
|
+
// (and on baseVertex, since it's baked into the values).
|
|
316
|
+
// Non-indexed ROs (no `ro.indices`) carry no index aval — the spec
|
|
317
|
+
// then sets `vertexCount` and the megacall decodes the vertex directly.
|
|
318
|
+
const indices: aval<Uint32Array> | undefined =
|
|
319
|
+
ro.indices !== undefined ? indicesAvalFor(ro.indices.buffer, ro.indices.elementType.indexFormat ?? "uint32", baseVertex) : undefined;
|
|
236
320
|
|
|
237
321
|
// 3. Texture/sampler: single-pair v1. The Sg compile layer binds
|
|
238
322
|
// each texture aval under both `name` and `${name}_view` so the
|
|
@@ -355,12 +439,21 @@ export function renderObjectToHeapSpec(
|
|
|
355
439
|
);
|
|
356
440
|
}
|
|
357
441
|
|
|
358
|
-
// 4. DrawCall
|
|
359
|
-
// compatible (
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
442
|
+
// 4. DrawCall geometry. Classifier validates fields are heap-
|
|
443
|
+
// compatible (instanceCount≥1, firstInstance=0, non-indexed firstVertex=0).
|
|
444
|
+
if (dc.kind === "indexed" && indices === undefined) {
|
|
445
|
+
throw new Error("heapAdapter: indexed drawCall without indices");
|
|
446
|
+
}
|
|
447
|
+
if (dc.kind === "non-indexed" && indices !== undefined) {
|
|
448
|
+
throw new Error("heapAdapter: non-indexed drawCall with an index buffer");
|
|
363
449
|
}
|
|
450
|
+
// Indexed → carry the index aval + the firstIndex/indexCount slice (folded
|
|
451
|
+
// into the record's indexStart/indexCount; baseVertex is already baked into
|
|
452
|
+
// the index values). Non-indexed → carry the vertex count; the megacall uses
|
|
453
|
+
// the local vertex index directly (no index lookup).
|
|
454
|
+
const geom = dc.kind === "indexed"
|
|
455
|
+
? { indices: indices!, firstIndex: dc.firstIndex, indexCount: dc.indexCount }
|
|
456
|
+
: { vertexCount: dc.vertexCount };
|
|
364
457
|
|
|
365
458
|
return {
|
|
366
459
|
effect: ro.effect,
|
|
@@ -368,7 +461,7 @@ export function renderObjectToHeapSpec(
|
|
|
368
461
|
inputs,
|
|
369
462
|
...(instanceAttributes !== undefined ? { instanceAttributes } : {}),
|
|
370
463
|
...(dc.instanceCount > 1 ? { instanceCount: dc.instanceCount } : {}),
|
|
371
|
-
|
|
464
|
+
...geom,
|
|
372
465
|
...(textures !== undefined ? { textures } : {}),
|
|
373
466
|
...(ro.modeRules !== undefined ? { modeRules: ro.modeRules } : {}),
|
|
374
467
|
// Pass `active` through unforced so the heap path can subscribe
|
|
@@ -839,5 +839,5 @@ export function megacallSearchPrelude(): string {
|
|
|
839
839
|
// to receive them as `u32` parameters — module-scope `var<private>`
|
|
840
840
|
// is rejected by some WGSL parsers (Safari/WebKit), so we thread the
|
|
841
841
|
// values through helper signatures instead.
|
|
842
|
-
return ` let _tileIdx = emitIdx >> 6u;\n var lo: u32 = firstDrawInTile[_tileIdx];\n var hi: u32 = firstDrawInTile[_tileIdx + 1u];\n loop {\n if (lo >= hi) { break; }\n let _mid = (lo + hi + 1u) >> 1u;\n if (drawTable[_mid * 5u] <= emitIdx) { lo = _mid; } else { hi = _mid - 1u; }\n }\n let _slot = lo;\n let _firstEmit = drawTable[_slot * 5u + 0u];\n let heap_drawIdx: u32 = drawTable[_slot * 5u + 1u];\n let _indexStart = drawTable[_slot * 5u + 2u];\n let _indexCount = drawTable[_slot * 5u + 3u];\n let _local = emitIdx - _firstEmit;\n let instId: u32 = _local / _indexCount;\n let vid: u32 = indexStorage[_indexStart +
|
|
842
|
+
return ` let _tileIdx = emitIdx >> 6u;\n var lo: u32 = firstDrawInTile[_tileIdx];\n var hi: u32 = firstDrawInTile[_tileIdx + 1u];\n loop {\n if (lo >= hi) { break; }\n let _mid = (lo + hi + 1u) >> 1u;\n if (drawTable[_mid * 5u] <= emitIdx) { lo = _mid; } else { hi = _mid - 1u; }\n }\n let _slot = lo;\n let _firstEmit = drawTable[_slot * 5u + 0u];\n let heap_drawIdx: u32 = drawTable[_slot * 5u + 1u];\n let _indexStart = drawTable[_slot * 5u + 2u];\n let _indexCount = drawTable[_slot * 5u + 3u];\n let _local = emitIdx - _firstEmit;\n let instId: u32 = _local / _indexCount;\n let _li: u32 = _local % _indexCount;\n var vid: u32 = _li;\n if (_indexStart != 0xffffffffu) { vid = indexStorage[_indexStart + _li]; }\n`;
|
|
843
843
|
}
|
|
@@ -185,27 +185,17 @@ export function isHeapEligible(ro: RenderObject): aval<boolean> {
|
|
|
185
185
|
return AVal.constant(false);
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
// 2.
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
// unsupported. Per-instance attributes follow the same rule.
|
|
194
|
-
let strideBlocked = false;
|
|
195
|
-
const checkStride = (v: BufferView): void => {
|
|
196
|
-
const stride = v.stride ?? v.elementType.byteSize;
|
|
197
|
-
const isBroadcast = v.singleValue !== undefined || stride === 0;
|
|
198
|
-
if (!isBroadcast && stride !== v.elementType.byteSize) strideBlocked = true;
|
|
199
|
-
};
|
|
200
|
-
for (const v of attrViews(asAttributeProvider(ro.vertexAttributes))) checkStride(v);
|
|
201
|
-
if (ro.instanceAttributes !== undefined) {
|
|
202
|
-
for (const v of attrViews(asAttributeProvider(ro.instanceAttributes))) checkStride(v);
|
|
203
|
-
}
|
|
204
|
-
if (strideBlocked) return AVal.constant(false);
|
|
188
|
+
// 2. Interleaved / offset attributes are handled by de-interleaving at
|
|
189
|
+
// ingest (heapAdapter.tightenBufferView gathers a tight, offset-0
|
|
190
|
+
// copy keyed by (buffer, offset, stride) so sharing is preserved), so
|
|
191
|
+
// no stride/offset wedge remains. Broadcasts (singleValue / stride 0)
|
|
192
|
+
// still pass through as a degenerate length-1 case.
|
|
205
193
|
|
|
206
|
-
// 3. Index-format wedge — heap stores indices as u32
|
|
207
|
-
|
|
208
|
-
|
|
194
|
+
// 3. Index-format wedge — heap stores indices as u32; u16 is widened
|
|
195
|
+
// to u32 at ingest (heapAdapter.widenU16). Anything else is rejected.
|
|
196
|
+
if (ro.indices !== undefined) {
|
|
197
|
+
const fmt = ro.indices.elementType.indexFormat;
|
|
198
|
+
if (fmt !== "uint32" && fmt !== "uint16") return AVal.constant(false);
|
|
209
199
|
}
|
|
210
200
|
|
|
211
201
|
const buffers = bufferAvals(ro);
|
|
@@ -229,29 +219,25 @@ export function isHeapEligible(ro: RenderObject): aval<boolean> {
|
|
|
229
219
|
for (const av of textures) if (!isHeapServableTexture(av.getValue(token))) return false;
|
|
230
220
|
for (const av of samplers) if (!isHeapServableSampler(av.getValue(token))) return false;
|
|
231
221
|
const dc = ro.drawCall.getValue(token);
|
|
232
|
-
if (dc.kind !== "indexed") return false;
|
|
233
222
|
if (dc.instanceCount < 1) return false;
|
|
234
|
-
if (dc.baseVertex !== 0) return false;
|
|
235
|
-
if (dc.firstIndex !== 0) return false;
|
|
236
223
|
if (dc.firstInstance !== 0) return false;
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
224
|
+
if (dc.kind === "indexed") {
|
|
225
|
+
if (ro.indices === undefined) return false;
|
|
226
|
+
// Offsets and shared sub-buffer slices are all eligible now: the heap
|
|
227
|
+
// still ingests the WHOLE index + vertex buffers (one shared arena
|
|
228
|
+
// allocation per buffer aval — sharing is the heap's whole point), but
|
|
229
|
+
// the record now honours the drawCall slice. firstIndex folds into the
|
|
230
|
+
// record's indexStart, indexCount becomes the slice length (so a single
|
|
231
|
+
// glyph of a multi-glyph run reads only its run of indices), and
|
|
232
|
+
// baseVertex is baked into the transcoded index values at ingest
|
|
233
|
+
// (heapAdapter.indicesAvalFor). No drawCall wedge remains here.
|
|
234
|
+
} else {
|
|
235
|
+
// Non-indexed: the megacall uses the local vertex index directly, so a
|
|
236
|
+
// prefix read is safe (vid = 0..vertexCount-1). firstVertex must be 0 —
|
|
237
|
+
// a non-indexed vertex offset would need a per-record field (the index
|
|
238
|
+
// bake-in trick doesn't apply with no index buffer), so it's deferred.
|
|
239
|
+
if (ro.indices !== undefined) return false;
|
|
240
|
+
if (dc.firstVertex !== 0) return false;
|
|
255
241
|
}
|
|
256
242
|
return true;
|
|
257
243
|
});
|
|
@@ -354,11 +354,21 @@ export function buildMegacallPrelude(emitIdx: Expr): { stmts: Stmt[]; locals: Me
|
|
|
354
354
|
stmts.push({ kind: "Declare", var: localOff, init: { kind: "Expr", value: sub(emitIdx, varExpr(firstEmit), Tu32) } });
|
|
355
355
|
// let instId = _local / _indexCount;
|
|
356
356
|
stmts.push({ kind: "Declare", var: instId, init: { kind: "Expr", value: div(varExpr(localOff), varExpr(indexCount), Tu32) } });
|
|
357
|
-
//
|
|
357
|
+
// Non-indexed records carry _indexStart == 0xffffffff (HEAP_NONINDEXED): the
|
|
358
|
+
// vertex is the local index directly; otherwise decode through indexStorage.
|
|
359
|
+
// let vid = (_indexStart == 0xffffffff)
|
|
360
|
+
// ? (_local % _indexCount)
|
|
361
|
+
// : indexStorage[_indexStart + (_local % _indexCount)];
|
|
362
|
+
const liExpr = (): Expr => mod(varExpr(localOff), varExpr(indexCount), Tu32);
|
|
358
363
|
stmts.push({
|
|
359
364
|
kind: "Declare",
|
|
360
365
|
var: vid,
|
|
361
|
-
init: { kind: "Expr", value:
|
|
366
|
+
init: { kind: "Expr", value: select(
|
|
367
|
+
eqU32(varExpr(indexStart), constU32(0xffffffff)),
|
|
368
|
+
liExpr(),
|
|
369
|
+
item(indexStorage, add(varExpr(indexStart), liExpr(), Tu32), Tu32),
|
|
370
|
+
Tu32,
|
|
371
|
+
) },
|
|
362
372
|
});
|
|
363
373
|
|
|
364
374
|
return { stmts, locals: { heapDrawIdx, instId, vid } };
|
package/src/runtime/heapScene.ts
CHANGED
|
@@ -143,6 +143,11 @@ import {
|
|
|
143
143
|
* Geometry triple. Tightly-packed Float32 positions / normals (3
|
|
144
144
|
* floats per vertex) plus Uint32 indices.
|
|
145
145
|
*/
|
|
146
|
+
/** drawTable `firstIndex` sentinel marking a non-indexed record: the megacall
|
|
147
|
+
* prelude then uses the local vertex index directly (no indexStorage lookup).
|
|
148
|
+
* Must match the literal in heapEffect.ts's `megacallSearchPrelude`. */
|
|
149
|
+
export const HEAP_NONINDEXED = 0xffffffff;
|
|
150
|
+
|
|
146
151
|
export interface HeapGeometry {
|
|
147
152
|
readonly positions: Float32Array;
|
|
148
153
|
readonly normals: Float32Array;
|
|
@@ -453,8 +458,24 @@ export interface HeapDrawSpec {
|
|
|
453
458
|
/**
|
|
454
459
|
* Index buffer for this draw. Indices live in their own `INDEX`-
|
|
455
460
|
* usage `GPUBuffer` (WebGPU forces this), separate from the arena.
|
|
461
|
+
* Omit for a NON-INDEXED draw — then `vertexCount` is required and the
|
|
462
|
+
* megacall uses the local vertex index directly (no index-buffer lookup,
|
|
463
|
+
* no index-pool allocation). See the megacall prelude's `vid` select.
|
|
464
|
+
*/
|
|
465
|
+
readonly indices?: aval<Uint32Array> | Uint32Array;
|
|
466
|
+
/** Vertex count for a non-indexed draw (when `indices` is omitted). */
|
|
467
|
+
readonly vertexCount?: number;
|
|
468
|
+
/**
|
|
469
|
+
* Optional index slice into a (possibly shared) index buffer. The heap
|
|
470
|
+
* still allocates/uploads the WHOLE `indices` buffer once per aval (so N
|
|
471
|
+
* draws sharing it cost one allocation), but this draw emits only
|
|
472
|
+
* `[firstIndex, firstIndex+indexCount)`. Folds into the record's
|
|
473
|
+
* indexStart (`alloc.firstIndex + firstIndex`) and indexCount. Omit for a
|
|
474
|
+
* whole-buffer draw (defaults: firstIndex 0, indexCount = buffer length).
|
|
475
|
+
* (baseVertex is handled upstream by baking it into the index values.)
|
|
456
476
|
*/
|
|
457
|
-
readonly
|
|
477
|
+
readonly firstIndex?: number;
|
|
478
|
+
readonly indexCount?: number;
|
|
458
479
|
/**
|
|
459
480
|
* Optional texture set. When present, adds a `texture_2d<f32>` at
|
|
460
481
|
* binding 4 and a sampler at binding 5; the FS must declare them.
|
|
@@ -3005,10 +3026,21 @@ export function buildHeapScene(
|
|
|
3005
3026
|
|
|
3006
3027
|
// Indices live in their own INDEX-usage buffer (WebGPU constraint).
|
|
3007
3028
|
// Aval-keyed: 19K instanced clones of the same mesh share one
|
|
3008
|
-
// index allocation + one upload.
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3029
|
+
// index allocation + one upload. NON-INDEXED draws (spec.indices
|
|
3030
|
+
// omitted) skip the index pool entirely: the drawTable record stores
|
|
3031
|
+
// firstIndex = HEAP_NONINDEXED (sentinel) and indexCount = vertexCount,
|
|
3032
|
+
// and the megacall prelude uses the local vertex index directly.
|
|
3033
|
+
const hasIndices = spec.indices !== undefined;
|
|
3034
|
+
const indicesAval = hasIndices ? (asAval(spec.indices!) as aval<Uint32Array>) : undefined;
|
|
3035
|
+
const idxAlloc = hasIndices
|
|
3036
|
+
? indexPool.acquire(device, arena.indices, indicesAval!, bucket.chunkIdx, readPlain(spec.indices!) as Uint32Array)
|
|
3037
|
+
: undefined;
|
|
3038
|
+
// Emit count per instance + the firstIndex field written to the record.
|
|
3039
|
+
// Slice: emit only [firstIndex, firstIndex+indexCount) of the (shared,
|
|
3040
|
+
// whole-buffer) index allocation. firstIndex folds into indexStart;
|
|
3041
|
+
// indexCount defaults to the whole buffer.
|
|
3042
|
+
const emitCount = hasIndices ? (spec.indexCount ?? idxAlloc!.count) : (spec.vertexCount ?? 0);
|
|
3043
|
+
const firstIndexField = hasIndices ? (idxAlloc!.firstIndex + (spec.firstIndex ?? 0)) : HEAP_NONINDEXED;
|
|
3012
3044
|
|
|
3013
3045
|
const localSlot = bucket.drawHeap.alloc();
|
|
3014
3046
|
// Per-RO instancing: read `spec.instanceCount` (defaults to 1).
|
|
@@ -3101,7 +3133,7 @@ export function buildHeapScene(
|
|
|
3101
3133
|
bucket.localPosRefs[localSlot] = perDrawRefs.get("Positions");
|
|
3102
3134
|
bucket.localNorRefs[localSlot] = perDrawRefs.get("Normals");
|
|
3103
3135
|
bucket.localEntries[localSlot] = {
|
|
3104
|
-
indexCount:
|
|
3136
|
+
indexCount: emitCount, firstIndex: firstIndexField, instanceCount,
|
|
3105
3137
|
};
|
|
3106
3138
|
bucket.localToDrawId[localSlot] = drawId;
|
|
3107
3139
|
bucket.drawSlots.add(localSlot);
|
|
@@ -3217,7 +3249,7 @@ export function buildHeapScene(
|
|
|
3217
3249
|
const uniformRefs: number[] = (bucket.uniformOrder ?? []).map(
|
|
3218
3250
|
name => perDrawRefs.get(name) ?? 0,
|
|
3219
3251
|
);
|
|
3220
|
-
partition.appendRecord(localSlot,
|
|
3252
|
+
partition.appendRecord(localSlot, firstIndexField, emitCount, instanceCount, roComboId, uniformRefs);
|
|
3221
3253
|
bucket.drawIdToRecord!.set(drawId, recIdx);
|
|
3222
3254
|
bucket.recordToDrawId![recIdx] = drawId;
|
|
3223
3255
|
bucket.drawIdToComboId!.set(drawId, roComboId);
|
|
@@ -3228,7 +3260,7 @@ export function buildHeapScene(
|
|
|
3228
3260
|
const numRecords = recIdx + 1;
|
|
3229
3261
|
const recBytes = numRecords * RECORD_BYTES;
|
|
3230
3262
|
const needBlocks = Math.max(1, Math.ceil(numRecords / SCAN_TILE_SIZE));
|
|
3231
|
-
const emitDelta =
|
|
3263
|
+
const emitDelta = emitCount * instanceCount;
|
|
3232
3264
|
for (const s of bucket.slots) {
|
|
3233
3265
|
s.drawTableBuf!.ensureCapacity(recBytes);
|
|
3234
3266
|
s.drawTableBuf!.setUsed(Math.max(s.drawTableBuf!.usedBytes, recBytes));
|
|
@@ -3261,15 +3293,15 @@ export function buildHeapScene(
|
|
|
3261
3293
|
// firstEmit is GPU-overwritten by the prefix-sum pass; 0 is fine.
|
|
3262
3294
|
shadow[recIdx * RECORD_U32 + 0] = 0;
|
|
3263
3295
|
shadow[recIdx * RECORD_U32 + 1] = localSlot;
|
|
3264
|
-
shadow[recIdx * RECORD_U32 + 2] =
|
|
3265
|
-
shadow[recIdx * RECORD_U32 + 3] =
|
|
3296
|
+
shadow[recIdx * RECORD_U32 + 2] = firstIndexField;
|
|
3297
|
+
shadow[recIdx * RECORD_U32 + 3] = emitCount;
|
|
3266
3298
|
shadow[recIdx * RECORD_U32 + 4] = instanceCount;
|
|
3267
3299
|
roSlot.recordCount = recIdx + 1;
|
|
3268
3300
|
roSlot.slotToRecord[localSlot] = recIdx;
|
|
3269
3301
|
roSlot.recordToSlot[recIdx] = localSlot;
|
|
3270
3302
|
if (byteOff < roSlot.drawTableDirtyMin) roSlot.drawTableDirtyMin = byteOff;
|
|
3271
3303
|
if (byteOff + RECORD_BYTES > roSlot.drawTableDirtyMax) roSlot.drawTableDirtyMax = byteOff + RECORD_BYTES;
|
|
3272
|
-
roSlot.totalEmitEstimate +=
|
|
3304
|
+
roSlot.totalEmitEstimate += emitCount * instanceCount;
|
|
3273
3305
|
const newNumTiles = Math.max(1, Math.ceil(roSlot.totalEmitEstimate / TILE_K));
|
|
3274
3306
|
roSlot.firstDrawInTileBuf!.ensureCapacity((newNumTiles + 1) * 4);
|
|
3275
3307
|
roSlot.scanDirty = true;
|
|
@@ -3286,7 +3318,7 @@ export function buildHeapScene(
|
|
|
3286
3318
|
// `subscribeActive` → `activeDirty` and drained in `update()`.
|
|
3287
3319
|
if (spec.active !== undefined) {
|
|
3288
3320
|
drawIdToActiveAval.set(drawId, spec.active);
|
|
3289
|
-
drawIdToOrigIndexCount.set(drawId,
|
|
3321
|
+
drawIdToOrigIndexCount.set(drawId, emitCount);
|
|
3290
3322
|
const initial = spec.active.getValue(outerTok);
|
|
3291
3323
|
subscribeActive(spec.active, drawId);
|
|
3292
3324
|
if (initial === false) {
|
|
@@ -5157,7 +5189,9 @@ export function buildHeapScene(
|
|
|
5157
5189
|
let _instCount = drawTable[_slot * 5u + 4u];
|
|
5158
5190
|
let _local = emitIdx - _firstEmit;
|
|
5159
5191
|
let _instId = _local / _indexCount;
|
|
5160
|
-
let
|
|
5192
|
+
let _li = _local % _indexCount;
|
|
5193
|
+
var _vid: u32 = _li;
|
|
5194
|
+
if (_indexStart != 0xffffffffu) { _vid = indexStorage[_indexStart + _li]; }
|
|
5161
5195
|
let base = i * 8u;
|
|
5162
5196
|
outRows[base + 0u] = _slot;
|
|
5163
5197
|
outRows[base + 1u] = _drawIdx;
|