@aardworx/wombat.rendering 0.19.24 → 0.20.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/elementType.d.ts +2 -0
- package/dist/core/elementType.d.ts.map +1 -1
- package/dist/core/elementType.js +10 -0
- package/dist/core/elementType.js.map +1 -1
- package/dist/core/provider.d.ts.map +1 -1
- package/dist/core/provider.js +65 -32
- package/dist/core/provider.js.map +1 -1
- package/dist/core/renderObject.d.ts +18 -0
- package/dist/core/renderObject.d.ts.map +1 -1
- package/dist/resources/preparedRenderObject.d.ts.map +1 -1
- package/dist/resources/preparedRenderObject.js +28 -5
- package/dist/resources/preparedRenderObject.js.map +1 -1
- package/dist/resources/uniformBuffer.d.ts.map +1 -1
- package/dist/resources/uniformBuffer.js +3 -1
- package/dist/resources/uniformBuffer.js.map +1 -1
- package/dist/runtime/derivedModes/modeKeyCpu.d.ts +2 -12
- package/dist/runtime/derivedModes/modeKeyCpu.d.ts.map +1 -1
- package/dist/runtime/derivedModes/modeKeyCpu.js +30 -13
- package/dist/runtime/derivedModes/modeKeyCpu.js.map +1 -1
- package/dist/runtime/derivedModes/partitionDispatcher.d.ts +11 -0
- package/dist/runtime/derivedModes/partitionDispatcher.d.ts.map +1 -1
- package/dist/runtime/derivedModes/partitionDispatcher.js +31 -0
- package/dist/runtime/derivedModes/partitionDispatcher.js.map +1 -1
- package/dist/runtime/derivedUniforms/codegen.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/codegen.js +34 -13
- package/dist/runtime/derivedUniforms/codegen.js.map +1 -1
- package/dist/runtime/derivedUniforms/dispatch.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/dispatch.js +6 -0
- package/dist/runtime/derivedUniforms/dispatch.js.map +1 -1
- package/dist/runtime/derivedUniforms/records.d.ts +15 -0
- package/dist/runtime/derivedUniforms/records.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/records.js +81 -14
- package/dist/runtime/derivedUniforms/records.js.map +1 -1
- package/dist/runtime/derivedUniforms/sceneIntegration.d.ts +6 -0
- package/dist/runtime/derivedUniforms/sceneIntegration.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/sceneIntegration.js +18 -4
- package/dist/runtime/derivedUniforms/sceneIntegration.js.map +1 -1
- package/dist/runtime/flattenTree.d.ts.map +1 -1
- package/dist/runtime/flattenTree.js +46 -2
- package/dist/runtime/flattenTree.js.map +1 -1
- package/dist/runtime/heapAdapter.d.ts +3 -1
- package/dist/runtime/heapAdapter.d.ts.map +1 -1
- package/dist/runtime/heapAdapter.js +45 -4
- package/dist/runtime/heapAdapter.js.map +1 -1
- package/dist/runtime/heapDecoder.d.ts.map +1 -1
- package/dist/runtime/heapDecoder.js +8 -3
- package/dist/runtime/heapDecoder.js.map +1 -1
- package/dist/runtime/heapEffect.d.ts +36 -1
- package/dist/runtime/heapEffect.d.ts.map +1 -1
- package/dist/runtime/heapEffect.js +29 -2
- package/dist/runtime/heapEffect.js.map +1 -1
- package/dist/runtime/heapEffectIR.d.ts.map +1 -1
- package/dist/runtime/heapEffectIR.js +51 -11
- package/dist/runtime/heapEffectIR.js.map +1 -1
- package/dist/runtime/heapEligibility.d.ts.map +1 -1
- package/dist/runtime/heapEligibility.js +64 -12
- package/dist/runtime/heapEligibility.js.map +1 -1
- package/dist/runtime/heapIrBuilders.d.ts +8 -0
- package/dist/runtime/heapIrBuilders.d.ts.map +1 -1
- package/dist/runtime/heapIrBuilders.js +47 -2
- package/dist/runtime/heapIrBuilders.js.map +1 -1
- package/dist/runtime/heapScene/chunkedArena.d.ts +20 -28
- package/dist/runtime/heapScene/chunkedArena.d.ts.map +1 -1
- package/dist/runtime/heapScene/chunkedArena.js +28 -91
- package/dist/runtime/heapScene/chunkedArena.js.map +1 -1
- package/dist/runtime/heapScene/freelist.d.ts +1 -1
- package/dist/runtime/heapScene/freelist.js +3 -3
- package/dist/runtime/heapScene/freelist.js.map +1 -1
- package/dist/runtime/heapScene/pools.d.ts +103 -59
- package/dist/runtime/heapScene/pools.d.ts.map +1 -1
- package/dist/runtime/heapScene/pools.js +335 -194
- package/dist/runtime/heapScene/pools.js.map +1 -1
- package/dist/runtime/heapScene.d.ts +125 -1
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +671 -126
- package/dist/runtime/heapScene.js.map +1 -1
- package/dist/runtime/heapShaderFamily.d.ts.map +1 -1
- package/dist/runtime/heapShaderFamily.js +19 -0
- package/dist/runtime/heapShaderFamily.js.map +1 -1
- package/dist/runtime/hybridScene.d.ts +6 -0
- package/dist/runtime/hybridScene.d.ts.map +1 -1
- package/dist/runtime/hybridScene.js +36 -3
- package/dist/runtime/hybridScene.js.map +1 -1
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/renderTask.d.ts +6 -0
- package/dist/runtime/renderTask.d.ts.map +1 -1
- package/dist/runtime/renderTask.js +3 -0
- package/dist/runtime/renderTask.js.map +1 -1
- package/dist/runtime/runtime.d.ts +15 -0
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +3 -0
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.d.ts +19 -2
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.d.ts.map +1 -1
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.js +37 -6
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.js.map +1 -1
- package/dist/runtime/textureAtlas/atlasPool.d.ts +5 -0
- package/dist/runtime/textureAtlas/atlasPool.d.ts.map +1 -1
- package/dist/runtime/textureAtlas/atlasPool.js +25 -3
- package/dist/runtime/textureAtlas/atlasPool.js.map +1 -1
- package/dist/window/loop.d.ts.map +1 -1
- package/dist/window/loop.js +31 -4
- package/dist/window/loop.js.map +1 -1
- package/package.json +3 -3
- package/src/core/elementType.ts +10 -0
- package/src/core/provider.ts +56 -28
- package/src/core/renderObject.ts +18 -0
- package/src/resources/preparedRenderObject.ts +29 -6
- package/src/resources/uniformBuffer.ts +3 -1
- package/src/runtime/derivedModes/modeKeyCpu.ts +26 -11
- package/src/runtime/derivedModes/partitionDispatcher.ts +30 -0
- package/src/runtime/derivedUniforms/codegen.ts +34 -13
- package/src/runtime/derivedUniforms/dispatch.ts +6 -0
- package/src/runtime/derivedUniforms/records.ts +70 -15
- package/src/runtime/derivedUniforms/sceneIntegration.ts +19 -4
- package/src/runtime/flattenTree.ts +47 -2
- package/src/runtime/heapAdapter.ts +43 -5
- package/src/runtime/heapDecoder.ts +8 -3
- package/src/runtime/heapEffect.ts +44 -2
- package/src/runtime/heapEffectIR.ts +54 -11
- package/src/runtime/heapEligibility.ts +39 -12
- package/src/runtime/heapIrBuilders.ts +50 -2
- package/src/runtime/heapScene/chunkedArena.ts +29 -94
- package/src/runtime/heapScene/freelist.ts +3 -3
- package/src/runtime/heapScene/pools.ts +340 -218
- package/src/runtime/heapScene.ts +826 -142
- package/src/runtime/heapShaderFamily.ts +21 -0
- package/src/runtime/hybridScene.ts +43 -3
- package/src/runtime/index.ts +5 -0
- package/src/runtime/renderTask.ts +9 -0
- package/src/runtime/runtime.ts +18 -0
- package/src/runtime/textureAtlas/atlasMipGutterKernel.ts +52 -3
- package/src/runtime/textureAtlas/atlasPool.ts +28 -2
- package/src/window/loop.ts +32 -4
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
// group's bind-group layout; the user's FS WGSL declares them.
|
|
44
44
|
import { Trafo3d, V3d, V4f, M44d } from "@aardworx/wombat.base";
|
|
45
45
|
import { AVal, AdaptiveObject, AdaptiveToken, HashTable } from "@aardworx/wombat.adaptive";
|
|
46
|
-
import { buildBucketLayout, compileHeapEffect, } from "./heapEffect.js";
|
|
46
|
+
import { INLINE_HEADER_UNIFORMS, buildBucketLayout, compileHeapEffect, } from "./heapEffect.js";
|
|
47
47
|
import { compileHeapEffectIR } from "./heapEffectIR.js";
|
|
48
48
|
import { buildShaderFamily, compileShaderFamily, } from "./heapShaderFamily.js";
|
|
49
49
|
import { ATLAS_PAGE_FORMATS, atlasFormatIndex, } from "./textureAtlas/atlasPool.js";
|
|
@@ -52,7 +52,7 @@ import { DerivedUniformsScene, registerRoDerivations, deregisterRoDerivations, i
|
|
|
52
52
|
import { packerForWgslType, PACKER_MAT4 } from "./heapScene/packers.js";
|
|
53
53
|
import { HEAP_SCAN_WGSL, SCAN_TILE_SIZE, SCAN_WG_SIZE, SCAN_MAX_RECORDS, TILE_K, RECORD_U32, RECORD_BYTES, } from "./heapScene/scanKernel.js";
|
|
54
54
|
import { GrowBuffer, MIN_BUFFER_BYTES, POW2, ALIGN16, } from "./heapScene/growBuffer.js";
|
|
55
|
-
import { UniformPool, IndexPool, DrawHeap,
|
|
55
|
+
import { UniformPool, IndexPool, DrawHeap, buildArenaState, arenaBytes, writeAttribute, asAval, isBufferView, asFloat32, ALLOC_HEADER_BYTES, ALLOC_HEADER_PAD_TO, ENC_V3F_TIGHT, ENC_OCT32, ENC_C4B, SEM_POSITIONS, SEM_NORMALS, COMPACTION_WASTE_FLOOR_BYTES, } from "./heapScene/pools.js";
|
|
56
56
|
import { encodeModeKey } from "./pipelineCache/index.js";
|
|
57
57
|
import { snapshotDescriptor, ModeKeyTracker } from "./derivedModes/modeKeyCpu.js";
|
|
58
58
|
import { GpuPartitionScene } from "./derivedModes/partitionDispatcher.js";
|
|
@@ -60,6 +60,41 @@ import { emitPartitionKernel, substituteReadInputInStmt, collectUniformReadsInSt
|
|
|
60
60
|
import {} from "./derivedModes/rule.js";
|
|
61
61
|
import { addMarkingCallback } from "@aardworx/wombat.adaptive";
|
|
62
62
|
import { analyseOutputSet, evaluateStructural, evaluateStructuralSet, stableStringify, } from "@aardworx/wombat.shader/ir";
|
|
63
|
+
const NOOP = () => { };
|
|
64
|
+
const packPackedAttr = (val, dst, off) => {
|
|
65
|
+
const src = val.data;
|
|
66
|
+
new Uint32Array(dst.buffer, dst.byteOffset + off * 4, src.length).set(src);
|
|
67
|
+
};
|
|
68
|
+
const packF32Verbatim = (val, dst, off) => {
|
|
69
|
+
dst.set(val, off);
|
|
70
|
+
};
|
|
71
|
+
const packHostU32Bits = (val, dst, off) => {
|
|
72
|
+
const ibuf = val;
|
|
73
|
+
if (ibuf.kind !== "host") {
|
|
74
|
+
throw new Error("heapScene: packed BufferView aval flipped to native GPUBuffer");
|
|
75
|
+
}
|
|
76
|
+
const d = ibuf.data;
|
|
77
|
+
// reinterpret the u32 payload as f32 bits for the arena write
|
|
78
|
+
dst.set(new Float32Array(d.buffer, d.byteOffset, d.byteLength >>> 2), off);
|
|
79
|
+
};
|
|
80
|
+
const packHostF32 = (val, dst, off) => {
|
|
81
|
+
const ibuf = val;
|
|
82
|
+
if (ibuf.kind !== "host") {
|
|
83
|
+
throw new Error("heapScene: BufferView aval flipped to native GPUBuffer; not supported in heap path");
|
|
84
|
+
}
|
|
85
|
+
dst.set(asFloat32(ibuf.data), off);
|
|
86
|
+
};
|
|
87
|
+
/** Broadcast prefix copy — the only variant that genuinely captures. */
|
|
88
|
+
function packHostPrefix(limitFloats) {
|
|
89
|
+
return (val, dst, off) => {
|
|
90
|
+
const ibuf = val;
|
|
91
|
+
if (ibuf.kind !== "host") {
|
|
92
|
+
throw new Error("heapScene: BufferView aval flipped to native GPUBuffer; not supported in heap path");
|
|
93
|
+
}
|
|
94
|
+
const src = asFloat32(ibuf.data);
|
|
95
|
+
dst.set(src.subarray(0, limitFloats), off);
|
|
96
|
+
};
|
|
97
|
+
}
|
|
63
98
|
// GrowBuffer + ALIGN16 + MIN_BUFFER_BYTES + POW2 live in ./heapScene/growBuffer.
|
|
64
99
|
// Packers (WGSL-type → JS-value → arena bytes) live in ./heapScene/packers.
|
|
65
100
|
// ---------------------------------------------------------------------------
|
|
@@ -93,6 +128,57 @@ import { analyseOutputSet, evaluateStructural, evaluateStructuralSet, stableStri
|
|
|
93
128
|
* prelude then uses the local vertex index directly (no indexStorage lookup).
|
|
94
129
|
* Must match the literal in heapEffect.ts's `megacallSearchPrelude`. */
|
|
95
130
|
export const HEAP_NONINDEXED = 0xffffffff;
|
|
131
|
+
export function oct32(data) {
|
|
132
|
+
const u = data instanceof Uint32Array ? data : new Uint32Array(data.buffer, data.byteOffset, data.length);
|
|
133
|
+
return { kind: "heap-packed", enc: "oct32", data: u };
|
|
134
|
+
}
|
|
135
|
+
export function c4b(data) {
|
|
136
|
+
if (data instanceof Uint8Array) {
|
|
137
|
+
if (data.byteLength % 4 !== 0 || data.byteOffset % 4 !== 0) {
|
|
138
|
+
throw new Error("heapScene.c4b: RGBA8 data must be 4-byte sized and aligned");
|
|
139
|
+
}
|
|
140
|
+
return { kind: "heap-packed", enc: "c4b", data: new Uint32Array(data.buffer, data.byteOffset, data.byteLength / 4) };
|
|
141
|
+
}
|
|
142
|
+
return { kind: "heap-packed", enc: "c4b", data };
|
|
143
|
+
}
|
|
144
|
+
export function isHeapPackedAttribute(v) {
|
|
145
|
+
return typeof v === "object" && v !== null
|
|
146
|
+
&& v.kind === "heap-packed"
|
|
147
|
+
&& v.data instanceof Uint32Array;
|
|
148
|
+
}
|
|
149
|
+
/** Create a standalone `HeapStorage`. Pass it to `buildHeapScene` via
|
|
150
|
+
* `opts.storage` to share one arena across multiple heaps. */
|
|
151
|
+
export function createHeapStorage(device, opts = {}) {
|
|
152
|
+
void device;
|
|
153
|
+
// Initial capacities are hints; the buffers pow2-grow on demand.
|
|
154
|
+
const arena = buildArenaState(device, Math.max(64 * 1024, opts.initialArenaBytes ?? 0), 16 * 1024, "heapStorage", GPUBufferUsage.STORAGE, opts.maxChunkBytes);
|
|
155
|
+
const pool = new UniformPool();
|
|
156
|
+
const indexPool = new IndexPool();
|
|
157
|
+
const consumers = new Set();
|
|
158
|
+
return {
|
|
159
|
+
arena, pool, indexPool,
|
|
160
|
+
register(c) {
|
|
161
|
+
consumers.add(c);
|
|
162
|
+
return { dispose: () => { consumers.delete(c); } };
|
|
163
|
+
},
|
|
164
|
+
compact(dev, floor, force) {
|
|
165
|
+
arena.attrs.flush(dev); // GPU buffer current for the copy
|
|
166
|
+
const remaps = arena.attrs.compact(dev, floor, force);
|
|
167
|
+
for (const { chunkIdx, remap } of remaps) {
|
|
168
|
+
// Storage-level dedup pools first (uniform refs); then each scene
|
|
169
|
+
// re-seats its drawHeaders / derive / partition / index `_indexStart`
|
|
170
|
+
// (the index re-seat reads `indexPool.baseFor` at the OLD ref, so the
|
|
171
|
+
// index pool is remapped LAST, after all consumers).
|
|
172
|
+
pool.remapRefs(chunkIdx, remap);
|
|
173
|
+
for (const c of consumers)
|
|
174
|
+
c.applyRemap(chunkIdx, remap);
|
|
175
|
+
indexPool.remapRefs(chunkIdx, remap);
|
|
176
|
+
}
|
|
177
|
+
return remaps.length;
|
|
178
|
+
},
|
|
179
|
+
dispose() { arena.attrs.destroy(); },
|
|
180
|
+
};
|
|
181
|
+
}
|
|
96
182
|
export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
97
183
|
const atlasPool = opts.atlasPool;
|
|
98
184
|
const colorAttachmentName = sig.colorNames[0];
|
|
@@ -114,12 +200,21 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
114
200
|
colorTargets.push({ format: fmt });
|
|
115
201
|
}
|
|
116
202
|
const depthFormat = sig.depthStencil?.format;
|
|
117
|
-
// ───
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
|
|
203
|
+
// ─── Storage (arena + dedup pools) — private or shared ───────────
|
|
204
|
+
// The chunked arena (uniform/attribute data + index buffer) and the
|
|
205
|
+
// aval-keyed dedup pools live in a `HeapStorage`. Default: a private
|
|
206
|
+
// store this scene owns. `opts.storage`: a shared store (the same
|
|
207
|
+
// geometry/uniforms rendered by multiple heaps — e.g. shadow + color
|
|
208
|
+
// pass — are deduped once). The local `arena`/`pool`/`indexPool`
|
|
209
|
+
// bindings below keep every downstream reference unchanged.
|
|
210
|
+
const ownsStorage = opts.storage === undefined;
|
|
211
|
+
const storage = opts.storage ?? createHeapStorage(device, {
|
|
212
|
+
...(opts.maxChunkBytes !== undefined ? { maxChunkBytes: opts.maxChunkBytes } : {}),
|
|
213
|
+
...(opts.initialArenaBytes !== undefined ? { initialArenaBytes: opts.initialArenaBytes } : {}),
|
|
214
|
+
});
|
|
215
|
+
const arena = storage.arena;
|
|
216
|
+
const pool = storage.pool;
|
|
217
|
+
const indexPool = storage.indexPool;
|
|
123
218
|
// ─── Per-draw global bookkeeping (sparse, indexed by drawId) ──────
|
|
124
219
|
const drawIdToBucket = [];
|
|
125
220
|
const drawIdToLocalSlot = [];
|
|
@@ -291,6 +386,23 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
291
386
|
const p = packerForWgslType(f.uniformWgslType ?? "");
|
|
292
387
|
return { dataBytes: p.dataBytes, typeId: p.typeId, length: 1, pack: p.pack };
|
|
293
388
|
}
|
|
389
|
+
// Packed attribute encodings (oct32 unit vectors / C4b colors): one
|
|
390
|
+
// u32 per element, decoded by the VS's typeId-selected arm. 3× smaller
|
|
391
|
+
// in the arena AND host-side (the caller keeps the packed source).
|
|
392
|
+
if (isHeapPackedAttribute(value)) {
|
|
393
|
+
const pa = value;
|
|
394
|
+
const wantType = pa.enc === "oct32" ? "vec3<f32>" : "vec4<f32>";
|
|
395
|
+
if (f.attributeWgslType !== wantType) {
|
|
396
|
+
throw new Error(`heapScene: ${pa.enc} attribute supplied for '${f.attributeWgslType}' field — ` +
|
|
397
|
+
`oct32 requires vec3<f32>, c4b requires vec4<f32>`);
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
dataBytes: pa.data.byteLength,
|
|
401
|
+
typeId: pa.enc === "oct32" ? ENC_OCT32 : ENC_C4B,
|
|
402
|
+
length: pa.data.length,
|
|
403
|
+
pack: packPackedAttr,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
294
406
|
// attribute-ref: variable-size array; we copy verbatim into the
|
|
295
407
|
// arena. The current encoding is V3F_TIGHT (3 f32s per element).
|
|
296
408
|
const arr = value;
|
|
@@ -303,7 +415,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
303
415
|
dataBytes: arr.byteLength,
|
|
304
416
|
typeId: ENC_V3F_TIGHT,
|
|
305
417
|
length,
|
|
306
|
-
pack:
|
|
418
|
+
pack: packF32Verbatim,
|
|
307
419
|
};
|
|
308
420
|
}
|
|
309
421
|
/**
|
|
@@ -323,6 +435,36 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
323
435
|
if (offset !== 0) {
|
|
324
436
|
throw new Error(`heapScene: BufferView offset ${offset} > 0 not yet supported`);
|
|
325
437
|
}
|
|
438
|
+
// Packed encodings by ELEMENT TYPE (the BufferView analogue of the
|
|
439
|
+
// oct32()/c4b() HeapDrawSpec markers): one u32 per element in the
|
|
440
|
+
// arena, decoded by the VS typeId arm. Dedup stays aval-keyed like
|
|
441
|
+
// any other BufferView attribute.
|
|
442
|
+
const etName = bv.elementType.name;
|
|
443
|
+
if (etName === "oct32" || etName === "c4b") {
|
|
444
|
+
const want = etName === "oct32" ? "vec3<f32>" : "vec4<f32>";
|
|
445
|
+
if (f.attributeWgslType !== want) {
|
|
446
|
+
throw new Error(`heapScene: ${etName} BufferView requires a ${want} schema attribute (got ${f.attributeWgslType ?? "?"})`);
|
|
447
|
+
}
|
|
448
|
+
const strideP = bv.stride ?? 4;
|
|
449
|
+
const isBroadcastP = bv.singleValue !== undefined || strideP === 0;
|
|
450
|
+
if (!isBroadcastP && strideP !== 4) {
|
|
451
|
+
throw new Error(`heapScene: ${etName} BufferView stride ${strideP} not tight (4) and not a broadcast`);
|
|
452
|
+
}
|
|
453
|
+
const ibP = bv.buffer.force( /* allow-force */);
|
|
454
|
+
if (ibP.kind !== "host") {
|
|
455
|
+
throw new Error(`heapScene: ${etName} BufferView wraps a native GPUBuffer; not supported in heap path`);
|
|
456
|
+
}
|
|
457
|
+
const lengthP = isBroadcastP ? 1 : ibP.sizeBytes / 4;
|
|
458
|
+
const dataBytesP = 4 * lengthP;
|
|
459
|
+
return {
|
|
460
|
+
dataBytes: dataBytesP,
|
|
461
|
+
typeId: etName === "oct32" ? ENC_OCT32 : ENC_C4B,
|
|
462
|
+
length: lengthP,
|
|
463
|
+
// Tight case: the copy limit equals the source payload, so the
|
|
464
|
+
// static packer (which derives it from the buffer) is exact.
|
|
465
|
+
pack: isBroadcastP ? packHostPrefix(dataBytesP / 4) : packHostU32Bits,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
326
468
|
// In-arena element bytes: the storage layout the shader will read
|
|
327
469
|
// from. vec4 is always 16 (read via `heapV4f[idx]`), vec3 is 12
|
|
328
470
|
// (read as 3 f32s), vec2 is 8, scalar is 4. V3f source for a vec4
|
|
@@ -366,15 +508,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
366
508
|
dataBytes,
|
|
367
509
|
typeId: ENC_V3F_TIGHT,
|
|
368
510
|
length,
|
|
369
|
-
pack: (
|
|
370
|
-
const ibuf = val;
|
|
371
|
-
if (ibuf.kind !== "host") {
|
|
372
|
-
throw new Error("heapScene: BufferView aval flipped to native GPUBuffer; not supported in heap path");
|
|
373
|
-
}
|
|
374
|
-
const src = asFloat32(ibuf.data);
|
|
375
|
-
const limitFloats = dataBytes / 4;
|
|
376
|
-
dst.set(src.subarray(0, limitFloats), off);
|
|
377
|
-
},
|
|
511
|
+
pack: isBroadcast ? packHostPrefix(dataBytes / 4) : packHostF32,
|
|
378
512
|
};
|
|
379
513
|
}
|
|
380
514
|
/**
|
|
@@ -400,9 +534,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
400
534
|
},
|
|
401
535
|
};
|
|
402
536
|
}
|
|
403
|
-
//
|
|
404
|
-
const pool = new UniformPool();
|
|
405
|
-
const indexPool = new IndexPool();
|
|
537
|
+
// (`pool` / `indexPool` are owned by `storage`, bound above.)
|
|
406
538
|
// ─── Adaptive routing: aval marks → repack the pool entry ─────────
|
|
407
539
|
// With pool-managed per-draw uniforms, value changes don't touch
|
|
408
540
|
// any DrawHeader (refs stay constant) — only the arena allocation's
|
|
@@ -456,6 +588,9 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
456
588
|
// both. `derivedScene` is undefined when the option is off; nothing
|
|
457
589
|
// else in this file should run when it is. On by default — pass
|
|
458
590
|
// `enableDerivedUniforms: false` to opt out.
|
|
591
|
+
const enableCompaction = opts.enableCompaction !== false;
|
|
592
|
+
const releaseConstantAttributes = opts.releaseConstantAttributes === true;
|
|
593
|
+
const compactionWasteFloor = opts.compactionWasteFloorBytes ?? COMPACTION_WASTE_FLOOR_BYTES;
|
|
459
594
|
const enableDerivedUniforms = opts.enableDerivedUniforms !== false;
|
|
460
595
|
const derivedScene = enableDerivedUniforms
|
|
461
596
|
? new DerivedUniformsScene(device, {
|
|
@@ -544,6 +679,18 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
544
679
|
// that don't yet point at an allocated AtlasPage. WebGPU rejects
|
|
545
680
|
// bind-group entries with `undefined` resources, so every slot in
|
|
546
681
|
// the N-wide ladder must hold a valid view at all times.
|
|
682
|
+
// 4-byte placeholder storage buffer — binds user-storage slots on the
|
|
683
|
+
// seed bind group built before the first addDraw delivers the real
|
|
684
|
+
// buffers (rebuilt immediately after).
|
|
685
|
+
let storagePlaceholder;
|
|
686
|
+
function getStoragePlaceholder() {
|
|
687
|
+
if (storagePlaceholder === undefined) {
|
|
688
|
+
storagePlaceholder = device.createBuffer({
|
|
689
|
+
label: "heapScene/storagePlaceholder", size: 4, usage: GPUBufferUsage.STORAGE,
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
return storagePlaceholder;
|
|
693
|
+
}
|
|
547
694
|
let atlasPlaceholders;
|
|
548
695
|
function getAtlasPlaceholder(format) {
|
|
549
696
|
if (atlasPlaceholders === undefined) {
|
|
@@ -566,6 +713,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
566
713
|
function getBgl(layout, withAtlasArrays) {
|
|
567
714
|
const key = `t${layout.textureBindings.map(t => t.wgslType).join(",")}` +
|
|
568
715
|
`|s${layout.samplerBindings.map(s => s.wgslType).join(",")}` +
|
|
716
|
+
`|sb${layout.storageBindings.map(b => `${b.binding}${b.access === "read_write" ? "w" : "r"}`).join(",")}` +
|
|
569
717
|
`|a${withAtlasArrays ? 1 : 0}`;
|
|
570
718
|
let e = bglCache.get(key);
|
|
571
719
|
if (e !== undefined)
|
|
@@ -593,6 +741,14 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
593
741
|
sampler: { type: s.wgslType === "sampler_comparison" ? "comparison" : "filtering" },
|
|
594
742
|
});
|
|
595
743
|
}
|
|
744
|
+
for (const sb of layout.storageBindings) {
|
|
745
|
+
// FRAGMENT only: WebGPU forbids writable storage in vertex stages,
|
|
746
|
+
// and the OIT writers are fragment-side anyway.
|
|
747
|
+
entries.push({
|
|
748
|
+
binding: sb.binding, visibility: GPUShaderStage.FRAGMENT,
|
|
749
|
+
buffer: { type: sb.access === "read_write" ? "storage" : "read-only-storage" },
|
|
750
|
+
});
|
|
751
|
+
}
|
|
596
752
|
if (withAtlasArrays) {
|
|
597
753
|
// 2N + 1 entries: N linear textures, N srgb textures, 1 sampler.
|
|
598
754
|
for (let i = 0; i < ATLAS_ARRAY_SIZE; i++) {
|
|
@@ -697,11 +853,32 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
697
853
|
if (cached !== undefined)
|
|
698
854
|
return cached;
|
|
699
855
|
const { pipelineLayout } = getBgl(layout, isAtlasBucket);
|
|
856
|
+
// Per-descriptor color targets: blend + write mask come from the RO's
|
|
857
|
+
// PipelineState (`desc.attachments`, ordered by signature colorNames —
|
|
858
|
+
// the same values the modeKey encodes). Using the bare scene-level
|
|
859
|
+
// `colorTargets` here silently dropped blends/write-masks from every
|
|
860
|
+
// heap pipeline: transparent heap leaves painted opaque, and the OIT
|
|
861
|
+
// pick passes' write-mask-0 "Colors" leaked color onto the composite.
|
|
862
|
+
const targets = colorTargets.map((t, i) => {
|
|
863
|
+
const a = desc.attachments[i];
|
|
864
|
+
if (a === undefined)
|
|
865
|
+
return { format: t.format };
|
|
866
|
+
return {
|
|
867
|
+
format: t.format,
|
|
868
|
+
writeMask: a.writeMask,
|
|
869
|
+
...(a.enabled
|
|
870
|
+
? { blend: {
|
|
871
|
+
color: { operation: a.color.operation, srcFactor: a.color.srcFactor, dstFactor: a.color.dstFactor },
|
|
872
|
+
alpha: { operation: a.alpha.operation, srcFactor: a.alpha.srcFactor, dstFactor: a.alpha.dstFactor },
|
|
873
|
+
} }
|
|
874
|
+
: {}),
|
|
875
|
+
};
|
|
876
|
+
});
|
|
700
877
|
const pipeline = device.createRenderPipeline({
|
|
701
878
|
label: `heapScene/cached/${cacheKey}`,
|
|
702
879
|
layout: pipelineLayout,
|
|
703
880
|
vertex: { module: fam.vsModule, entryPoint: fam.vsEntryName, buffers: [] },
|
|
704
|
-
fragment: { module: fam.fsModule, entryPoint: fam.fsEntryName, targets
|
|
881
|
+
fragment: { module: fam.fsModule, entryPoint: fam.fsEntryName, targets },
|
|
705
882
|
primitive: { topology: desc.topology, cullMode: desc.cullMode, frontFace: desc.frontFace },
|
|
706
883
|
...(depthFormat !== undefined && desc.depth !== undefined
|
|
707
884
|
? { depthStencil: { format: depthFormat, depthWriteEnabled: desc.depth.write, depthCompare: desc.depth.compare } }
|
|
@@ -826,6 +1003,29 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
826
1003
|
}
|
|
827
1004
|
familyFor(effect); // throws if unknown
|
|
828
1005
|
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Reactive family rebuild: register a new effect's family when it first
|
|
1008
|
+
* appears after the initial freeze. Family-merge is disabled, so every
|
|
1009
|
+
* Effect already gets its OWN independent single-member family (its own
|
|
1010
|
+
* shader modules + buckets); a late effect just needs its family compiled
|
|
1011
|
+
* and added — no rebuild of existing families. Idempotent.
|
|
1012
|
+
*/
|
|
1013
|
+
function ensureFamilyForSpec(spec) {
|
|
1014
|
+
if (!familyBuilt) {
|
|
1015
|
+
buildFamilyFromSpecs([spec]);
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
if (familyByEffectId.has(spec.effect.id))
|
|
1019
|
+
return;
|
|
1020
|
+
const entry = { attributes: new Set(), uniforms: new Set() };
|
|
1021
|
+
if (spec.instanceAttributes !== undefined) {
|
|
1022
|
+
for (const name of Object.keys(spec.instanceAttributes))
|
|
1023
|
+
entry.attributes.add(name);
|
|
1024
|
+
}
|
|
1025
|
+
const singleMap = new Map();
|
|
1026
|
+
singleMap.set(spec.effect, entry);
|
|
1027
|
+
familyByEffectId.set(spec.effect.id, compileFamilyFor([spec.effect], singleMap));
|
|
1028
|
+
}
|
|
829
1029
|
// ─── id-of helpers ────────────────────────────────────────────────
|
|
830
1030
|
const idOf = (s) => `effect#${s.id}`;
|
|
831
1031
|
const textureIds = new WeakMap();
|
|
@@ -961,7 +1161,10 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
961
1161
|
// binary search. Minimum one zero-record to satisfy WebGPU non-
|
|
962
1162
|
// zero size constraint when the bucket is empty.
|
|
963
1163
|
const dtBytes = Math.max(RECORD_BYTES, s.recordCount * RECORD_BYTES);
|
|
964
|
-
entries.push({ binding: 4, resource: { buffer: s.drawTableBuf.buffer, offset: 0, size: dtBytes } },
|
|
1164
|
+
entries.push({ binding: 4, resource: { buffer: s.drawTableBuf.buffer, offset: 0, size: dtBytes } },
|
|
1165
|
+
// Index data lives in the attr arena now (the VS storage-decodes it);
|
|
1166
|
+
// bind the same buffer as the gather views.
|
|
1167
|
+
{ binding: 5, resource: { buffer: attrsBuf } }, { binding: 6, resource: { buffer: s.firstDrawInTileBuf.buffer } });
|
|
965
1168
|
s.renderBoundRecordCount = s.recordCount;
|
|
966
1169
|
}
|
|
967
1170
|
// Schema-driven texture + sampler entries. v1 user surface still
|
|
@@ -1009,6 +1212,21 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1009
1212
|
// here — the atlas slots come from the per-format page tracking
|
|
1010
1213
|
// below.
|
|
1011
1214
|
}
|
|
1215
|
+
for (const sb of bucket.layout.storageBindings) {
|
|
1216
|
+
const av = bucket.storageBuffers?.get(sb.name);
|
|
1217
|
+
if (av === undefined) {
|
|
1218
|
+
// Buffers arrive with the first addDraw; the seed slot built at
|
|
1219
|
+
// bucket creation is rebuilt right after they land. Bind a
|
|
1220
|
+
// 4-byte placeholder so the seed bind group validates.
|
|
1221
|
+
entries.push({ binding: sb.binding, resource: { buffer: getStoragePlaceholder() } });
|
|
1222
|
+
continue;
|
|
1223
|
+
}
|
|
1224
|
+
const ib = av.force( /* allow-force */);
|
|
1225
|
+
if (ib.kind !== "gpu") {
|
|
1226
|
+
throw new Error(`heapScene: user storage buffer '${sb.name}' must be a GPU-backed IBuffer`);
|
|
1227
|
+
}
|
|
1228
|
+
entries.push({ binding: sb.binding, resource: { buffer: ib.buffer } });
|
|
1229
|
+
}
|
|
1012
1230
|
if (bucket.isAtlasBucket) {
|
|
1013
1231
|
if (atlasPool === undefined) {
|
|
1014
1232
|
throw new Error("heapScene: atlas-variant bucket needs `atlasPool` in BuildHeapSceneOptions");
|
|
@@ -1081,15 +1299,6 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1081
1299
|
atlasPool.onPageAdded(f, () => rebuildAllBindGroups(true));
|
|
1082
1300
|
}
|
|
1083
1301
|
}
|
|
1084
|
-
arena.indices.onAnyResize((resizedChunkIdx) => {
|
|
1085
|
-
for (const b of buckets) {
|
|
1086
|
-
if (b.chunkIdx !== resizedChunkIdx)
|
|
1087
|
-
continue;
|
|
1088
|
-
for (let i = 0; i < b.slots.length; i++) {
|
|
1089
|
-
b.slots[i].bindGroup = buildBucketBindGroup(b, i);
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
});
|
|
1093
1302
|
// ─── findOrCreateBucket ───────────────────────────────────────────
|
|
1094
1303
|
// Slice 3c: the bucket key collapses to (familyId, pipelineState).
|
|
1095
1304
|
// Every member effect in the family shares one bucket per
|
|
@@ -1746,7 +1955,11 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1746
1955
|
if (cpuMigration !== undefined) {
|
|
1747
1956
|
let totalEmit = 0;
|
|
1748
1957
|
for (const m of cpuMigration) {
|
|
1749
|
-
const uRefs = bucket.uniformOrder.map(n =>
|
|
1958
|
+
const uRefs = bucket.uniformOrder.map(n => {
|
|
1959
|
+
const i = bucket.fieldIdx.get(n);
|
|
1960
|
+
const r = i === undefined ? -1 : m.perDrawRefs[i];
|
|
1961
|
+
return r < 0 ? 0 : r;
|
|
1962
|
+
});
|
|
1750
1963
|
const recIdx = partition.appendRecord(m.localSlot, m.firstIndex, m.indexCount, m.instanceCount, m.targetComboId, uRefs);
|
|
1751
1964
|
bucket.drawIdToRecord.set(m.drawId, recIdx);
|
|
1752
1965
|
bucket.recordToDrawId[recIdx] = m.drawId;
|
|
@@ -1857,8 +2070,11 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1857
2070
|
const isAtlasBucket = layout.atlasTextureBindings.size > 0;
|
|
1858
2071
|
const drawHeapBuf = new GrowBuffer(device, `heapScene/${bk}/drawHeap`, GPUBufferUsage.STORAGE, Math.max(layout.drawHeaderBytes, 64));
|
|
1859
2072
|
const drawHeap = new DrawHeap(drawHeapBuf, layout.drawHeaderBytes);
|
|
2073
|
+
const fieldIdx = new Map();
|
|
2074
|
+
layout.drawHeaderFields.forEach((f, i) => fieldIdx.set(f.name, i));
|
|
1860
2075
|
const bucket = {
|
|
1861
2076
|
label: bk, textures: undefined, layout, chunkIdx,
|
|
2077
|
+
storageBuffers: undefined, storageDirty: false, storageSubs: [],
|
|
1862
2078
|
slots: [], // populated lazily by ensureSlot per modeKey
|
|
1863
2079
|
bindGroup: null, // legacy field; slot.bindGroup is the real one
|
|
1864
2080
|
drawHeap,
|
|
@@ -1866,7 +2082,10 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1866
2082
|
headerDirtyMin: Infinity, headerDirtyMax: 0,
|
|
1867
2083
|
localPosRefs: [], localNorRefs: [],
|
|
1868
2084
|
localEntries: [], localToDrawId: [],
|
|
1869
|
-
localPerDrawAvals: [], localPerDrawRefs: [], localLayoutIds: [],
|
|
2085
|
+
localPerDrawAvals: [], localPerDrawRawFieldIdx: [], localPerDrawRefs: [], localLayoutIds: [],
|
|
2086
|
+
fieldIdx,
|
|
2087
|
+
posFieldIdx: fieldIdx.get("Positions") ?? -1,
|
|
2088
|
+
norFieldIdx: fieldIdx.get("Normals") ?? -1,
|
|
1870
2089
|
drawSlots: new Set(), dirty: new Set(),
|
|
1871
2090
|
isAtlasBucket,
|
|
1872
2091
|
localAtlasReleases: [],
|
|
@@ -1936,7 +2155,9 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1936
2155
|
const dst = bucket.drawHeaderStaging;
|
|
1937
2156
|
const u32 = new Uint32Array(dst.buffer, dst.byteOffset, dst.length);
|
|
1938
2157
|
const baseFloat = (localSlot * bucket.layout.drawHeaderBytes) / 4;
|
|
1939
|
-
|
|
2158
|
+
const fields = bucket.layout.drawHeaderFields;
|
|
2159
|
+
for (let i = 0; i < fields.length; i++) {
|
|
2160
|
+
const f = fields[i];
|
|
1940
2161
|
// texture-ref fields carry inline values (pageRef/formatBits as u32,
|
|
1941
2162
|
// origin/size as vec2<f32>) and are filled by packAtlasTextureFields.
|
|
1942
2163
|
if (f.kind === "texture-ref")
|
|
@@ -1950,15 +2171,11 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1950
2171
|
u32[fOff] = layoutId >>> 0;
|
|
1951
2172
|
continue;
|
|
1952
2173
|
}
|
|
1953
|
-
const ref = perDrawRefs
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
u32[fOff] = 0;
|
|
1959
|
-
continue;
|
|
1960
|
-
}
|
|
1961
|
-
u32[fOff] = ref;
|
|
2174
|
+
const ref = perDrawRefs[i];
|
|
2175
|
+
// Family-merge: a slot's effect doesn't populate every field of
|
|
2176
|
+
// the union (−1); leave the unused slots zero — the layoutId
|
|
2177
|
+
// switch ensures they're never read by the wrong effect's helper.
|
|
2178
|
+
u32[fOff] = ref < 0 ? 0 : ref;
|
|
1962
2179
|
}
|
|
1963
2180
|
}
|
|
1964
2181
|
// ─── Atlas drawHeader packing + page-set tracking ─────────────────
|
|
@@ -2043,6 +2260,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2043
2260
|
totalDraws: 0,
|
|
2044
2261
|
drawBytes: 0,
|
|
2045
2262
|
geometryBytes: 0,
|
|
2263
|
+
compactions: 0,
|
|
2046
2264
|
derivedPullMs: 0,
|
|
2047
2265
|
derivedUploadMs: 0,
|
|
2048
2266
|
derivedEncodeMs: 0,
|
|
@@ -2085,12 +2303,10 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2085
2303
|
// single spec when no batched lazy-build occurred earlier (e.g.
|
|
2086
2304
|
// direct addDraw at runtime). The aset / array initial-population
|
|
2087
2305
|
// paths build from the full effect set up front.
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
ensureFamilyKnowsEffect(spec.effect);
|
|
2093
|
-
}
|
|
2306
|
+
// Build the family lazily from this spec (first addDraw), or register a
|
|
2307
|
+
// newly-seen effect's family on the fly (reactive rebuild) — supports a
|
|
2308
|
+
// scene mixing multiple effects added over time (tiles + markers + …).
|
|
2309
|
+
ensureFamilyForSpec(spec);
|
|
2094
2310
|
const perInstanceUniforms = new Set();
|
|
2095
2311
|
const perInstanceAttributes = spec.instanceAttributes !== undefined
|
|
2096
2312
|
? new Set(Object.keys(spec.instanceAttributes))
|
|
@@ -2111,16 +2327,48 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2111
2327
|
token: outerTok,
|
|
2112
2328
|
});
|
|
2113
2329
|
}
|
|
2114
|
-
//
|
|
2115
|
-
//
|
|
2116
|
-
//
|
|
2117
|
-
//
|
|
2118
|
-
//
|
|
2119
|
-
//
|
|
2120
|
-
//
|
|
2121
|
-
//
|
|
2122
|
-
//
|
|
2123
|
-
|
|
2330
|
+
// ─── Group-fit page placement (§3) ───────────────────────────────
|
|
2331
|
+
// A draw's whole group — uniforms + attributes + INDEX data (all one
|
|
2332
|
+
// arena now) — must land on ONE chunk/page (the draw binds one buffer).
|
|
2333
|
+
// Estimate the group's arena bytes (exact per-field via the same
|
|
2334
|
+
// placement helpers, ignoring dedup ⇒ an upper bound on the NEW bytes a
|
|
2335
|
+
// chunk must absorb), then place the group on a chunk with room — prefer
|
|
2336
|
+
// one already holding a shared input, else the newest, else open a fresh
|
|
2337
|
+
// page. This replaces the old cross-chunk spill → throw, so a scene can
|
|
2338
|
+
// grow past one buffer's cap (WebGPU's storage-buffer binding limit).
|
|
2339
|
+
const fam = familyFor(spec.effect);
|
|
2340
|
+
const effectFields = fam.fieldsForEffect.get(spec.effect.id);
|
|
2341
|
+
const layout = fam.schema.drawHeaderUnion;
|
|
2342
|
+
const hasIndices = spec.indices !== undefined;
|
|
2343
|
+
const indicesAval = hasIndices ? asAval(spec.indices) : undefined;
|
|
2344
|
+
const indexArr = hasIndices ? readPlain(spec.indices) : undefined;
|
|
2345
|
+
const instanceCount = spec.instanceCount !== undefined
|
|
2346
|
+
? readPlain(spec.instanceCount) : 1;
|
|
2347
|
+
let estBytes = indexArr !== undefined ? ALIGN16(ALLOC_HEADER_PAD_TO + indexArr.byteLength) : 0;
|
|
2348
|
+
for (const f of layout.drawHeaderFields) {
|
|
2349
|
+
if (f.kind === "texture-ref" || f.name === "__layoutId" || INLINE_HEADER_UNIFORMS.has(f.name) || !effectFields.has(f.name))
|
|
2350
|
+
continue;
|
|
2351
|
+
if (ruleForUniform(spec, f.name) !== undefined) {
|
|
2352
|
+
estBytes += ALIGN16(ALLOC_HEADER_PAD_TO + PACKER_MAT4.dataBytes); // §7 derived dummy mat4
|
|
2353
|
+
continue;
|
|
2354
|
+
}
|
|
2355
|
+
const perInstUniform = f.kind === "uniform-ref" && perInstanceUniforms.has(f.name);
|
|
2356
|
+
const perInstAttr = f.kind === "attribute-ref" && perInstanceAttributes.has(f.name);
|
|
2357
|
+
const provided = perInstAttr ? spec.instanceAttributes[f.name] : spec.inputs[f.name];
|
|
2358
|
+
if (provided === undefined)
|
|
2359
|
+
continue; // the acquire loop throws with a precise message
|
|
2360
|
+
let dataBytes;
|
|
2361
|
+
if (f.kind === "attribute-ref" && !perInstUniform && isBufferView(provided)) {
|
|
2362
|
+
dataBytes = bufferViewPlacement(f, provided).dataBytes;
|
|
2363
|
+
}
|
|
2364
|
+
else {
|
|
2365
|
+
const value = asAval(provided).getValue(outerTok);
|
|
2366
|
+
dataBytes = (perInstUniform
|
|
2367
|
+
? perInstancePlacementFor(f, value, instanceCount)
|
|
2368
|
+
: poolPlacementFor(f, value)).dataBytes;
|
|
2369
|
+
}
|
|
2370
|
+
estBytes += ALIGN16(ALLOC_HEADER_PAD_TO + dataBytes);
|
|
2371
|
+
}
|
|
2124
2372
|
let chunkIdx;
|
|
2125
2373
|
{
|
|
2126
2374
|
let preferred;
|
|
@@ -2135,9 +2383,35 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2135
2383
|
}
|
|
2136
2384
|
}
|
|
2137
2385
|
}
|
|
2138
|
-
|
|
2386
|
+
const fits = (c) => arena.attrs.bumpHeadroom(c) >= estBytes;
|
|
2387
|
+
const newest = arena.attrs.chunkCount - 1;
|
|
2388
|
+
chunkIdx =
|
|
2389
|
+
preferred !== undefined && fits(preferred) ? preferred
|
|
2390
|
+
: fits(newest) ? newest
|
|
2391
|
+
: arena.attrs.openPage();
|
|
2139
2392
|
}
|
|
2140
2393
|
const bucket = findOrCreateBucket(spec.effect, spec.textures, spec.pipelineState, chunkIdx, precomputedDescriptor);
|
|
2394
|
+
// User storage buffers (layout.storageBindings): bucket-level, set once.
|
|
2395
|
+
if (bucket.layout.storageBindings.length > 0 && bucket.storageBuffers === undefined) {
|
|
2396
|
+
if (spec.storageBuffers === undefined) {
|
|
2397
|
+
throw new Error(`heapScene: bucket '${bucket.label}' needs storage buffers ` +
|
|
2398
|
+
`(${bucket.layout.storageBindings.map(b => b.name).join(", ")}) but spec.storageBuffers is undefined`);
|
|
2399
|
+
}
|
|
2400
|
+
bucket.storageBuffers = spec.storageBuffers;
|
|
2401
|
+
// A storage aval marking (e.g. the OIT head buffer reallocating on a
|
|
2402
|
+
// canvas resize) invalidates every slot bind group of this bucket.
|
|
2403
|
+
for (const av of spec.storageBuffers.values()) {
|
|
2404
|
+
const disposer = av
|
|
2405
|
+
.addMarkingCallback?.(() => { bucket.storageDirty = true; });
|
|
2406
|
+
if (disposer !== undefined)
|
|
2407
|
+
bucket.storageSubs.push(disposer);
|
|
2408
|
+
}
|
|
2409
|
+
// Slots seeded before the buffers were known carry incomplete bind
|
|
2410
|
+
// groups — rebuild them now.
|
|
2411
|
+
for (let i = 0; i < bucket.slots.length; i++) {
|
|
2412
|
+
bucket.slots[i].bindGroup = buildBucketBindGroup(bucket, i);
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2141
2415
|
// Phase 5c.3 — every RO carrying a derived-mode rule is registered
|
|
2142
2416
|
// on the bucket (lazy-promoting it to GPU-routed on the first
|
|
2143
2417
|
// call). ROs may carry DIFFERENT rules even when they share the
|
|
@@ -2169,18 +2443,16 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2169
2443
|
// tables every frame from the master.
|
|
2170
2444
|
const roSlot = bucket.gpuRouted ? bucket.slots[0] : ensureSlot(bucket, roDescriptor);
|
|
2171
2445
|
const roSlotIdx = bucket.slots.indexOf(roSlot);
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
//
|
|
2175
|
-
//
|
|
2176
|
-
//
|
|
2177
|
-
//
|
|
2178
|
-
//
|
|
2179
|
-
//
|
|
2180
|
-
const hasIndices = spec.indices !== undefined;
|
|
2181
|
-
const indicesAval = hasIndices ? asAval(spec.indices) : undefined;
|
|
2446
|
+
// Indices ride the same arena as attributes/uniforms (the VS storage-
|
|
2447
|
+
// decodes them). Aval-keyed: 19K instanced clones of the same mesh share
|
|
2448
|
+
// one index allocation + one upload. NON-INDEXED draws (spec.indices
|
|
2449
|
+
// omitted) skip the pool: the drawTable record stores firstIndex =
|
|
2450
|
+
// HEAP_NONINDEXED (sentinel) and indexCount = vertexCount, and the
|
|
2451
|
+
// megacall prelude uses the local vertex index directly. `indicesAval` /
|
|
2452
|
+
// `indexArr` were read above for the group estimate; the group landed
|
|
2453
|
+
// wholly on `bucket.chunkIdx`, so this allocation can't spill.
|
|
2182
2454
|
const idxAlloc = hasIndices
|
|
2183
|
-
? indexPool.acquire(device, arena.
|
|
2455
|
+
? indexPool.acquire(device, arena.attrs, indicesAval, bucket.chunkIdx, indexArr)
|
|
2184
2456
|
: undefined;
|
|
2185
2457
|
// Emit count per instance + the firstIndex field written to the record.
|
|
2186
2458
|
// Slice: emit only [firstIndex, firstIndex+indexCount) of the (shared,
|
|
@@ -2189,17 +2461,26 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2189
2461
|
const emitCount = hasIndices ? (spec.indexCount ?? idxAlloc.count) : (spec.vertexCount ?? 0);
|
|
2190
2462
|
const firstIndexField = hasIndices ? (idxAlloc.firstIndex + (spec.firstIndex ?? 0)) : HEAP_NONINDEXED;
|
|
2191
2463
|
const localSlot = bucket.drawHeap.alloc();
|
|
2192
|
-
// Per-RO instancing: read `spec.instanceCount` (defaults to 1).
|
|
2193
|
-
const instanceCount = spec.instanceCount !== undefined
|
|
2194
|
-
? readPlain(spec.instanceCount)
|
|
2195
|
-
: 1;
|
|
2196
2464
|
// Walk the bucket's schema-driven DrawHeader fields. Per-instance
|
|
2197
2465
|
// attributes pull from `spec.instanceAttributes` and pack into an
|
|
2198
2466
|
// array allocation; everything else pulls from `spec.inputs` and
|
|
2199
2467
|
// packs as a single value. Both go through the same pool — sharing
|
|
2200
2468
|
// emerges from aval identity either way.
|
|
2201
2469
|
const perDrawAvals = [];
|
|
2202
|
-
|
|
2470
|
+
let perDrawRawFieldIdx;
|
|
2471
|
+
const perDrawRefs = new Int32Array(bucket.layout.drawHeaderFields.length).fill(-1);
|
|
2472
|
+
const setRef = (name, ref) => {
|
|
2473
|
+
const i = bucket.fieldIdx.get(name);
|
|
2474
|
+
if (i !== undefined)
|
|
2475
|
+
perDrawRefs[i] = ref;
|
|
2476
|
+
};
|
|
2477
|
+
const getRef = (name) => {
|
|
2478
|
+
const i = bucket.fieldIdx.get(name);
|
|
2479
|
+
if (i === undefined)
|
|
2480
|
+
return undefined;
|
|
2481
|
+
const r = perDrawRefs[i];
|
|
2482
|
+
return r < 0 ? undefined : r;
|
|
2483
|
+
};
|
|
2203
2484
|
{
|
|
2204
2485
|
const tok = outerTok;
|
|
2205
2486
|
for (const f of bucket.layout.drawHeaderFields) {
|
|
@@ -2214,15 +2495,42 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2214
2495
|
continue;
|
|
2215
2496
|
if (!effectFields.has(f.name))
|
|
2216
2497
|
continue;
|
|
2498
|
+
// Inline header uniforms (PickId): the header word IS the u32
|
|
2499
|
+
// value — write it via perDrawRefs (packBucketHeader copies
|
|
2500
|
+
// refs verbatim), skip the pool entirely. A provider-supplied
|
|
2501
|
+
// PickId aval still works below as the (slow) fallback.
|
|
2502
|
+
if (INLINE_HEADER_UNIFORMS.has(f.name)) {
|
|
2503
|
+
const iv = f.name === "PickId" ? spec.pickId : undefined;
|
|
2504
|
+
if (iv !== undefined) {
|
|
2505
|
+
setRef(f.name, iv >>> 0);
|
|
2506
|
+
continue;
|
|
2507
|
+
}
|
|
2508
|
+
const provInline = spec.inputs[f.name];
|
|
2509
|
+
if (typeof provInline === "number") {
|
|
2510
|
+
setRef(f.name, provInline >>> 0);
|
|
2511
|
+
continue;
|
|
2512
|
+
}
|
|
2513
|
+
if (provInline === undefined) {
|
|
2514
|
+
// No id at all (picking disabled but the effect still
|
|
2515
|
+
// reads the uniform): 0 = "no pick", a reserved id.
|
|
2516
|
+
setRef(f.name, 0);
|
|
2517
|
+
continue;
|
|
2518
|
+
}
|
|
2519
|
+
// fall through: aval-provided → legacy pooled path
|
|
2520
|
+
}
|
|
2217
2521
|
// §7: derived-uniform fields are produced by the compute
|
|
2218
2522
|
// pre-pass. We still allocate an arena slot here so the
|
|
2219
2523
|
// drawHeader gets a valid ref written by packBucketHeader; §7
|
|
2220
2524
|
// overwrites the arena data each frame the inputs are dirty.
|
|
2221
2525
|
if (ruleForUniform(spec, f.name) !== undefined) {
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2526
|
+
// Raw (aval-less) slot: §7's compute pre-pass overwrites the
|
|
2527
|
+
// data every dirty frame, so a keyed pool entry (placeholder
|
|
2528
|
+
// ConstantVal + Map per draw) bought nothing. The ref lives
|
|
2529
|
+
// in the drawHeader cell (compaction re-seats it there);
|
|
2530
|
+
// removeDraw frees it via `perDrawRawFieldIdx`.
|
|
2531
|
+
const ref = pool.allocRaw(arena.attrs, bucket.chunkIdx, PACKER_MAT4.dataBytes, PACKER_MAT4.typeId, 1);
|
|
2532
|
+
setRef(f.name, ref);
|
|
2533
|
+
(perDrawRawFieldIdx ??= []).push(bucket.fieldIdx.get(f.name));
|
|
2226
2534
|
continue;
|
|
2227
2535
|
}
|
|
2228
2536
|
const isPerInstanceUniformField = f.kind === "uniform-ref" && perInstanceUniforms.has(f.name);
|
|
@@ -2249,12 +2557,37 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2249
2557
|
let av;
|
|
2250
2558
|
let value;
|
|
2251
2559
|
let placement;
|
|
2560
|
+
let releasable;
|
|
2252
2561
|
if (f.kind === "attribute-ref" && !isPerInstanceUniformField && isBufferView(provided)) {
|
|
2253
2562
|
const bv = provided;
|
|
2254
2563
|
placement = bufferViewPlacement(f, bv);
|
|
2255
2564
|
av = bv.buffer;
|
|
2256
2565
|
value = bv.buffer.getValue(tok);
|
|
2257
2566
|
}
|
|
2567
|
+
else if (releaseConstantAttributes
|
|
2568
|
+
&& f.kind === "attribute-ref"
|
|
2569
|
+
&& !isPerInstanceUniformField
|
|
2570
|
+
&& !(typeof provided === "object" && provided !== null
|
|
2571
|
+
&& typeof provided.getValue === "function")) {
|
|
2572
|
+
// Plain value (raw array / packed marker) → CONSTANT by definition.
|
|
2573
|
+
// Wrap in a droppable holder: after this field's arena write the GPU
|
|
2574
|
+
// copy is the only copy the heap ever reads again, so the payload is
|
|
2575
|
+
// released (real avals never take this branch — changeable data
|
|
2576
|
+
// stays alive by contract).
|
|
2577
|
+
const holder = {
|
|
2578
|
+
_v: provided,
|
|
2579
|
+
getValue() {
|
|
2580
|
+
if (holder._v === null) {
|
|
2581
|
+
throw new Error("heapScene: constant attribute payload was released after staging (releaseConstantAttributes)");
|
|
2582
|
+
}
|
|
2583
|
+
return holder._v;
|
|
2584
|
+
},
|
|
2585
|
+
};
|
|
2586
|
+
releasable = holder;
|
|
2587
|
+
av = holder;
|
|
2588
|
+
value = provided;
|
|
2589
|
+
placement = poolPlacementFor(f, value);
|
|
2590
|
+
}
|
|
2258
2591
|
else {
|
|
2259
2592
|
av = asAval(provided);
|
|
2260
2593
|
value = av.getValue(tok);
|
|
@@ -2263,18 +2596,21 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2263
2596
|
: poolPlacementFor(f, value);
|
|
2264
2597
|
}
|
|
2265
2598
|
const ref = pool.acquire(device, arena.attrs, av, bucket.chunkIdx, value, placement.dataBytes, placement.typeId, placement.length, placement.pack);
|
|
2266
|
-
|
|
2599
|
+
if (releasable !== undefined)
|
|
2600
|
+
releasable._v = null; // staged — drop the CPU copy
|
|
2601
|
+
setRef(f.name, ref);
|
|
2267
2602
|
perDrawAvals.push(av);
|
|
2268
2603
|
}
|
|
2269
2604
|
}
|
|
2270
|
-
bucket.localPosRefs[localSlot] =
|
|
2271
|
-
bucket.localNorRefs[localSlot] =
|
|
2605
|
+
bucket.localPosRefs[localSlot] = getRef("Positions");
|
|
2606
|
+
bucket.localNorRefs[localSlot] = getRef("Normals");
|
|
2272
2607
|
bucket.localEntries[localSlot] = {
|
|
2273
2608
|
indexCount: emitCount, firstIndex: firstIndexField, instanceCount,
|
|
2274
2609
|
};
|
|
2275
2610
|
bucket.localToDrawId[localSlot] = drawId;
|
|
2276
2611
|
bucket.drawSlots.add(localSlot);
|
|
2277
2612
|
bucket.localPerDrawAvals[localSlot] = perDrawAvals;
|
|
2613
|
+
bucket.localPerDrawRawFieldIdx[localSlot] = perDrawRawFieldIdx;
|
|
2278
2614
|
bucket.localPerDrawRefs[localSlot] = perDrawRefs;
|
|
2279
2615
|
const layoutId = fam.schema.layoutIdOf.get(spec.effect);
|
|
2280
2616
|
bucket.localLayoutIds[localSlot] = layoutId;
|
|
@@ -2377,7 +2713,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2377
2713
|
// don't carry a given name (e.g. their effect doesn't declare
|
|
2378
2714
|
// it) get 0 — the kernel only reads refs through combo fns that
|
|
2379
2715
|
// gate on `r.comboId`, so an unread 0 is harmless.
|
|
2380
|
-
const uniformRefs = (bucket.uniformOrder ?? []).map(name =>
|
|
2716
|
+
const uniformRefs = (bucket.uniformOrder ?? []).map(name => getRef(name) ?? 0);
|
|
2381
2717
|
partition.appendRecord(localSlot, firstIndexField, emitCount, instanceCount, roComboId, uniformRefs);
|
|
2382
2718
|
bucket.drawIdToRecord.set(drawId, recIdx);
|
|
2383
2719
|
bucket.recordToDrawId[recIdx] = drawId;
|
|
@@ -2477,7 +2813,11 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2477
2813
|
}
|
|
2478
2814
|
uniformAvals = m;
|
|
2479
2815
|
}
|
|
2480
|
-
const tracker = new ModeKeyTracker(spec.pipelineState, sig,
|
|
2816
|
+
const tracker = new ModeKeyTracker(spec.pipelineState, sig,
|
|
2817
|
+
// skipSubscribe means the tracker never invokes onDirty itself
|
|
2818
|
+
// (scene-level modeAvalToDrawIds dispatches instead) — a shared
|
|
2819
|
+
// noop avoids one closure per draw.
|
|
2820
|
+
NOOP, {
|
|
2481
2821
|
skipSubscribe: true,
|
|
2482
2822
|
...(spec.modeRules !== undefined ? { modeRules: spec.modeRules } : {}),
|
|
2483
2823
|
...(uniformAvals !== undefined ? { uniformAvals } : {}),
|
|
@@ -2509,14 +2849,14 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2509
2849
|
const ruleSubset = new Map();
|
|
2510
2850
|
const outOffsetByName = new Map();
|
|
2511
2851
|
for (const f of bucket.layout.drawHeaderFields) {
|
|
2512
|
-
if (f.name === "__layoutId")
|
|
2852
|
+
if (f.name === "__layoutId" || INLINE_HEADER_UNIFORMS.has(f.name))
|
|
2513
2853
|
continue;
|
|
2514
2854
|
if (!effectFields.has(f.name))
|
|
2515
2855
|
continue;
|
|
2516
2856
|
const rule = ruleForUniform(spec, f.name);
|
|
2517
2857
|
if (rule === undefined)
|
|
2518
2858
|
continue;
|
|
2519
|
-
const ref =
|
|
2859
|
+
const ref = getRef(f.name);
|
|
2520
2860
|
if (ref === undefined)
|
|
2521
2861
|
continue;
|
|
2522
2862
|
ruleSubset.set(f.name, rule);
|
|
@@ -2545,7 +2885,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2545
2885
|
trafoAvals,
|
|
2546
2886
|
...(spec.modelChain !== undefined ? { modelChain: spec.modelChain } : {}),
|
|
2547
2887
|
hostUniformOffset: (n) => {
|
|
2548
|
-
const r =
|
|
2888
|
+
const r = getRef(n);
|
|
2549
2889
|
return r === undefined ? undefined : r + ALLOC_HEADER_PAD_TO;
|
|
2550
2890
|
},
|
|
2551
2891
|
outputOffset: (n) => outOffsetByName.get(n),
|
|
@@ -2660,9 +3000,23 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2660
3000
|
if (avals !== undefined)
|
|
2661
3001
|
for (const av of avals)
|
|
2662
3002
|
pool.release(arena.attrs, av, bucket.chunkIdx);
|
|
3003
|
+
// Raw §7 slots: the live ref is the drawHeader cell (compaction
|
|
3004
|
+
// keeps it current); free it directly.
|
|
3005
|
+
const rawIdxs = bucket.localPerDrawRawFieldIdx[localSlot];
|
|
3006
|
+
if (rawIdxs !== undefined) {
|
|
3007
|
+
const rawRefs = bucket.localPerDrawRefs[localSlot];
|
|
3008
|
+
if (rawRefs !== undefined) {
|
|
3009
|
+
for (const fi of rawIdxs) {
|
|
3010
|
+
const ref = rawRefs[fi];
|
|
3011
|
+
if (ref >= 0)
|
|
3012
|
+
pool.releaseRaw(arena.attrs, bucket.chunkIdx, ref, PACKER_MAT4.dataBytes);
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
bucket.localPerDrawRawFieldIdx[localSlot] = undefined;
|
|
3016
|
+
}
|
|
2663
3017
|
const idxAval = drawIdToIndexAval[drawId];
|
|
2664
3018
|
if (idxAval !== undefined)
|
|
2665
|
-
indexPool.release(arena.
|
|
3019
|
+
indexPool.release(arena.attrs, idxAval, bucket.chunkIdx);
|
|
2666
3020
|
const atlasRel = bucket.localAtlasReleases[localSlot];
|
|
2667
3021
|
if (atlasRel !== undefined)
|
|
2668
3022
|
atlasRel();
|
|
@@ -2729,14 +3083,16 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2729
3083
|
if (asetReader === undefined)
|
|
2730
3084
|
return;
|
|
2731
3085
|
const delta = asetReader.getChanges(tok);
|
|
3086
|
+
// Process removals before adds. A remove+add in the same
|
|
3087
|
+
// transaction must free its slot / arena region / atlas tile
|
|
3088
|
+
// before the add allocates, so the freelist reuses the just-freed
|
|
3089
|
+
// block instead of growing the high-water toward
|
|
3090
|
+
// live + removed-this-tick. (Matches Aardvark's removals-first
|
|
3091
|
+
// delta discipline.)
|
|
3092
|
+
const adds = [];
|
|
2732
3093
|
delta.iter((op) => {
|
|
2733
3094
|
if (op.count > 0) {
|
|
2734
|
-
|
|
2735
|
-
// already sceneObj) — collapses N nested evaluateAlways into
|
|
2736
|
-
// one for a bulk add. Saves a per-RO setUnsafeEvaluationDepth
|
|
2737
|
-
// + outputs.add(sceneObj) round trip.
|
|
2738
|
-
const id = addDrawImpl(op.value, tok);
|
|
2739
|
-
specToDrawId.set(op.value, id);
|
|
3095
|
+
adds.push(op.value);
|
|
2740
3096
|
}
|
|
2741
3097
|
else {
|
|
2742
3098
|
const id = specToDrawId.get(op.value);
|
|
@@ -2746,6 +3102,13 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2746
3102
|
}
|
|
2747
3103
|
}
|
|
2748
3104
|
});
|
|
3105
|
+
// Re-use the outer evaluateAlways scope's token (caller is already
|
|
3106
|
+
// sceneObj) — collapses N nested evaluateAlways into one for a bulk
|
|
3107
|
+
// add. Saves a per-RO setUnsafeEvaluationDepth + outputs.add round trip.
|
|
3108
|
+
for (const value of adds) {
|
|
3109
|
+
const id = addDrawImpl(value, tok);
|
|
3110
|
+
specToDrawId.set(value, id);
|
|
3111
|
+
}
|
|
2749
3112
|
}
|
|
2750
3113
|
// ─── Initial population ───────────────────────────────────────────
|
|
2751
3114
|
if (Array.isArray(initialDraws)) {
|
|
@@ -2764,9 +3127,9 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2764
3127
|
asetReader = initialDraws.getReader();
|
|
2765
3128
|
sceneObj.evaluateAlways(AdaptiveToken.top, (tok) => {
|
|
2766
3129
|
// First drain: snapshot all add-ops, build the family from their
|
|
2767
|
-
// effects up front, then run addDraw. Subsequent drains
|
|
2768
|
-
//
|
|
2769
|
-
//
|
|
3130
|
+
// effects up front, then run addDraw. Subsequent drains reuse the
|
|
3131
|
+
// family, or register a newly-seen effect's family on the fly
|
|
3132
|
+
// (ensureFamilyForSpec, via addDrawImpl).
|
|
2770
3133
|
if (!familyBuilt) {
|
|
2771
3134
|
const reader = asetReader;
|
|
2772
3135
|
const delta = reader.getChanges(tok);
|
|
@@ -2798,6 +3161,148 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2798
3161
|
}
|
|
2799
3162
|
});
|
|
2800
3163
|
}
|
|
3164
|
+
// ─── Heap compaction (attribute arena + index buffer) ────────────
|
|
3165
|
+
/**
|
|
3166
|
+
* Compact every arena/index chunk whose fragmentation waste crosses the
|
|
3167
|
+
* floor (or unconditionally when `force`), then re-seat EVERY cached ref so
|
|
3168
|
+
* the relocated allocations stay addressable. Attribute arena:
|
|
3169
|
+
* • uniform-pool entries (`pool.remapRefs`)
|
|
3170
|
+
* • §7 derived-uniform record handles (`derivedScene.remapHostHeap`)
|
|
3171
|
+
* • modes partition master refs (`partitionScene.remapUniformRefs`)
|
|
3172
|
+
* • drawHeader cells + the Pos/Nor caches (re-`packBucketHeader`)
|
|
3173
|
+
* Index buffer (ref = `allocBase + slice`, held in the drawTable / partition
|
|
3174
|
+
* master + IndexPool entry):
|
|
3175
|
+
* • per-draw `indexStart` in the CPU drawTable or partition master
|
|
3176
|
+
* • `localEntries[slot].firstIndex` + IndexPool entry base
|
|
3177
|
+
* Must run with the buffers current (we flush first) and before the
|
|
3178
|
+
* per-bucket header / drawTable flush so the re-seated records upload.
|
|
3179
|
+
*/
|
|
3180
|
+
// Per-scene ref re-seat after the shared store relocates regions. Registered
|
|
3181
|
+
// with `storage`; the store calls it (for THIS scene + any others sharing the
|
|
3182
|
+
// store) inside `storage.compact`. Re-seats derived records, partition master
|
|
3183
|
+
// refs, drawHeader cells, and index `_indexStart` — everything this scene
|
|
3184
|
+
// caches that points into the arena. (The storage-level uniform/index pools
|
|
3185
|
+
// are re-seated by the store itself, around this call.)
|
|
3186
|
+
function applyRemap(chunkIdx, remap) {
|
|
3187
|
+
if (derivedScene !== undefined) {
|
|
3188
|
+
// §7 handles store DATA offsets (ref + header pad), not header refs.
|
|
3189
|
+
const dataRemap = new Map();
|
|
3190
|
+
for (const [o, n] of remap) {
|
|
3191
|
+
dataRemap.set(o + ALLOC_HEADER_PAD_TO, n + ALLOC_HEADER_PAD_TO);
|
|
3192
|
+
}
|
|
3193
|
+
derivedScene.remapHostHeap(chunkIdx, dataRemap);
|
|
3194
|
+
}
|
|
3195
|
+
{
|
|
3196
|
+
for (const bucket of buckets) {
|
|
3197
|
+
if (bucket.chunkIdx !== chunkIdx)
|
|
3198
|
+
continue;
|
|
3199
|
+
if (bucket.gpuRouted && bucket.partitionScene !== undefined) {
|
|
3200
|
+
if (bucket.partitionScene.remapUniformRefs(remap) > 0) {
|
|
3201
|
+
bucket.partitionDirty = true;
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
for (const localSlot of bucket.drawSlots) {
|
|
3205
|
+
// (a) Index region re-seat. Index data is an attr-arena region now,
|
|
3206
|
+
// so it relocates in THIS pass. Its draw stores `_indexStart` (an
|
|
3207
|
+
// element offset = allocBase + slice) in the drawTable / partition
|
|
3208
|
+
// master; shift it by the region's element delta. Independent of the
|
|
3209
|
+
// uniform re-seat below (a slot's indices may move while its uniforms
|
|
3210
|
+
// don't), so it runs before the `changed` gate.
|
|
3211
|
+
const drawId = bucket.localToDrawId[localSlot];
|
|
3212
|
+
if (drawId !== undefined) {
|
|
3213
|
+
const iav = drawIdToIndexAval[drawId];
|
|
3214
|
+
const oldRef = iav !== undefined ? indexPool.baseFor(iav, chunkIdx) : undefined;
|
|
3215
|
+
const newRef = oldRef !== undefined ? remap.get(oldRef) : undefined;
|
|
3216
|
+
const entry = bucket.localEntries[localSlot];
|
|
3217
|
+
if (oldRef !== undefined && newRef !== undefined && entry !== undefined) {
|
|
3218
|
+
const newStart = entry.firstIndex + ((newRef - oldRef) >> 2);
|
|
3219
|
+
entry.firstIndex = newStart;
|
|
3220
|
+
if (bucket.gpuRouted && bucket.partitionScene !== undefined) {
|
|
3221
|
+
const recIdx = bucket.drawIdToRecord?.get(drawId);
|
|
3222
|
+
if (recIdx !== undefined) {
|
|
3223
|
+
bucket.partitionScene.setRecordIndexStart(recIdx, newStart);
|
|
3224
|
+
bucket.partitionDirty = true;
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
else {
|
|
3228
|
+
const slotIdx = drawIdToSlotIdx[drawId];
|
|
3229
|
+
const slot = slotIdx !== undefined ? bucket.slots[slotIdx] : bucket.slots[0];
|
|
3230
|
+
const recIdx = slot?.slotToRecord[localSlot];
|
|
3231
|
+
if (slot !== undefined && recIdx !== undefined && recIdx >= 0) {
|
|
3232
|
+
slot.drawTableShadow[recIdx * RECORD_U32 + 2] = newStart >>> 0;
|
|
3233
|
+
const off = recIdx * RECORD_BYTES;
|
|
3234
|
+
if (off < slot.drawTableDirtyMin)
|
|
3235
|
+
slot.drawTableDirtyMin = off;
|
|
3236
|
+
if (off + RECORD_BYTES > slot.drawTableDirtyMax)
|
|
3237
|
+
slot.drawTableDirtyMax = off + RECORD_BYTES;
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
// (b) Uniform/attr drawHeader cells (+ Pos/Nor caches).
|
|
3243
|
+
const refs = bucket.localPerDrawRefs[localSlot];
|
|
3244
|
+
if (refs === undefined)
|
|
3245
|
+
continue;
|
|
3246
|
+
let changed = false;
|
|
3247
|
+
for (let i = 0; i < refs.length; i++) {
|
|
3248
|
+
const ref = refs[i];
|
|
3249
|
+
if (ref < 0)
|
|
3250
|
+
continue;
|
|
3251
|
+
const fld = bucket.layout.drawHeaderFields[i];
|
|
3252
|
+
// Inline header values (PickId/__layoutId) are NOT arena
|
|
3253
|
+
// refs — a numeric collision with an old offset must not
|
|
3254
|
+
// remap them.
|
|
3255
|
+
if (fld.name === "__layoutId" || INLINE_HEADER_UNIFORMS.has(fld.name))
|
|
3256
|
+
continue;
|
|
3257
|
+
const nn = remap.get(ref);
|
|
3258
|
+
if (nn !== undefined) {
|
|
3259
|
+
refs[i] = nn;
|
|
3260
|
+
changed = true;
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
if (!changed)
|
|
3264
|
+
continue;
|
|
3265
|
+
const pr = bucket.posFieldIdx >= 0 ? refs[bucket.posFieldIdx] : -1;
|
|
3266
|
+
const nr = bucket.norFieldIdx >= 0 ? refs[bucket.norFieldIdx] : -1;
|
|
3267
|
+
bucket.localPosRefs[localSlot] = pr < 0 ? undefined : pr;
|
|
3268
|
+
bucket.localNorRefs[localSlot] = nr < 0 ? undefined : nr;
|
|
3269
|
+
packBucketHeader(bucket, localSlot, refs, bucket.localLayoutIds[localSlot] ?? 0);
|
|
3270
|
+
if (bucket.isAtlasBucket) {
|
|
3271
|
+
const ts = bucket.localAtlasTextures[localSlot];
|
|
3272
|
+
if (ts !== undefined)
|
|
3273
|
+
packAtlasTextureFields(bucket, localSlot, ts);
|
|
3274
|
+
}
|
|
3275
|
+
const byteOff = localSlot * bucket.layout.drawHeaderBytes;
|
|
3276
|
+
if (byteOff < bucket.headerDirtyMin)
|
|
3277
|
+
bucket.headerDirtyMin = byteOff;
|
|
3278
|
+
const end = byteOff + bucket.layout.drawHeaderBytes;
|
|
3279
|
+
if (end > bucket.headerDirtyMax)
|
|
3280
|
+
bucket.headerDirtyMax = end;
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
// Register this scene as a consumer of the (possibly shared) store, so a
|
|
3286
|
+
// compaction — triggered by THIS scene or any other sharing the store —
|
|
3287
|
+
// re-seats this scene's refs. Disposed in `dispose()`.
|
|
3288
|
+
const storageReg = storage.register({ applyRemap });
|
|
3289
|
+
// ─── Compaction (delegates to the shared store) ──────────────────
|
|
3290
|
+
function runCompaction(force) {
|
|
3291
|
+
const compacted = storage.compact(device, compactionWasteFloor, force);
|
|
3292
|
+
stats.compactions += compacted;
|
|
3293
|
+
// update() flushes the re-seated drawHeaders via its own per-bucket loop
|
|
3294
|
+
// right after this returns; the forced (test) path runs outside update(),
|
|
3295
|
+
// so it flushes this scene's dirtied headers itself.
|
|
3296
|
+
if (force) {
|
|
3297
|
+
for (const bucket of buckets) {
|
|
3298
|
+
if (bucket.headerDirtyMax > bucket.headerDirtyMin) {
|
|
3299
|
+
device.queue.writeBuffer(bucket.drawHeap.buffer, bucket.headerDirtyMin, bucket.drawHeaderStaging.buffer, bucket.drawHeaderStaging.byteOffset + bucket.headerDirtyMin, bucket.headerDirtyMax - bucket.headerDirtyMin);
|
|
3300
|
+
bucket.headerDirtyMin = Infinity;
|
|
3301
|
+
bucket.headerDirtyMax = 0;
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
2801
3306
|
// ─── update / encodeIntoPass / frame / dispose ───────────────────
|
|
2802
3307
|
/**
|
|
2803
3308
|
* CPU-side data refresh: drain pending aset deltas, repack any
|
|
@@ -2810,6 +3315,16 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2810
3315
|
let totalDirtyBytes = 0;
|
|
2811
3316
|
sceneObj.evaluateAlways(token, (tok) => {
|
|
2812
3317
|
drainAsetWith(tok);
|
|
3318
|
+
// User storage buffers that marked (e.g. the OIT head buffer
|
|
3319
|
+
// reallocating on a resize) invalidate the bucket's bind groups.
|
|
3320
|
+
for (const b of buckets) {
|
|
3321
|
+
if (!b.storageDirty)
|
|
3322
|
+
continue;
|
|
3323
|
+
b.storageDirty = false;
|
|
3324
|
+
for (let i = 0; i < b.slots.length; i++) {
|
|
3325
|
+
b.slots[i].bindGroup = buildBucketBindGroup(b, i);
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
2813
3328
|
// 0. Reactive rebucket: any RO whose PipelineState modeKey
|
|
2814
3329
|
// changed since last frame moves to its new bucket.
|
|
2815
3330
|
// `dirtyModeKeyDrawIds` was populated by ModeKeyTracker's
|
|
@@ -3032,7 +3547,16 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3032
3547
|
// initial population for 10K ROs this collapses ~30K calls into
|
|
3033
3548
|
// ~5 (arena, indices, plus one per bucket).
|
|
3034
3549
|
arena.attrs.flush(device);
|
|
3035
|
-
arena
|
|
3550
|
+
// ─── Waste-triggered attribute-arena compaction ─────────────────
|
|
3551
|
+
// Relocate live allocations to the front of each chunk once
|
|
3552
|
+
// fragmentation waste crosses the floor — exact-size freelist reuse
|
|
3553
|
+
// can't reclaim a drifting size distribution, so the arena would
|
|
3554
|
+
// otherwise ratchet to its high-water forever. Runs AFTER the arena
|
|
3555
|
+
// flush (GPU buffer current) and BEFORE the header flush (so the
|
|
3556
|
+
// re-seated drawHeaders upload below). Indices live in a separate
|
|
3557
|
+
// buffer and are not compacted here.
|
|
3558
|
+
if (enableCompaction)
|
|
3559
|
+
runCompaction(false);
|
|
3036
3560
|
for (const bucket of buckets) {
|
|
3037
3561
|
if (bucket.headerDirtyMax > bucket.headerDirtyMin) {
|
|
3038
3562
|
device.queue.writeBuffer(bucket.drawHeap.buffer, bucket.headerDirtyMin, bucket.drawHeaderStaging.buffer, bucket.drawHeaderStaging.byteOffset + bucket.headerDirtyMin, bucket.headerDirtyMax - bucket.headerDirtyMin);
|
|
@@ -3232,14 +3756,44 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3232
3756
|
destroyBucketResources(b);
|
|
3233
3757
|
}
|
|
3234
3758
|
function dispose() {
|
|
3235
|
-
|
|
3236
|
-
|
|
3759
|
+
// Stop receiving compaction remaps for the shared store.
|
|
3760
|
+
storageReg.dispose();
|
|
3761
|
+
// Shared storage: release this scene's allocations so the shared pool's
|
|
3762
|
+
// refcounts stay correct (another scene may still hold the same regions),
|
|
3763
|
+
// but don't destroy the buffers — the storage owner does that. Private
|
|
3764
|
+
// storage: the whole arena is torn down wholesale, so per-draw release is
|
|
3765
|
+
// unnecessary.
|
|
3766
|
+
if (!ownsStorage) {
|
|
3767
|
+
for (let drawId = 0; drawId < drawIdToBucket.length; drawId++) {
|
|
3768
|
+
if (drawIdToBucket[drawId] !== undefined)
|
|
3769
|
+
removeDraw(drawId);
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3237
3772
|
for (const b of buckets)
|
|
3238
3773
|
destroyBucketResources(b);
|
|
3774
|
+
if (ownsStorage)
|
|
3775
|
+
storage.dispose();
|
|
3239
3776
|
}
|
|
3240
3777
|
// Test-only escape hatch for inspecting megacall bucket state. Not
|
|
3241
3778
|
// part of the public API surface — keep cast at use-site.
|
|
3242
3779
|
const _debug = {
|
|
3780
|
+
/** Force a full heap compaction pass (attribute arena + index buffer)
|
|
3781
|
+
* regardless of the waste heuristics (bypasses the floor + 50%-live
|
|
3782
|
+
* gate), re-seat all refs, flush. Returns residual waste afterwards in
|
|
3783
|
+
* bytes (attr bytes + index elements×4; 0 when fully packed). Test-only —
|
|
3784
|
+
* drives relocation deterministically without engineering megabytes of
|
|
3785
|
+
* fragmentation. Callers should `update()` afterward to flush drawTables /
|
|
3786
|
+
* re-dispatch the partition (the render path does this). */
|
|
3787
|
+
forceCompact() {
|
|
3788
|
+
runCompaction(true);
|
|
3789
|
+
return arena.attrs.totalWasteBytes();
|
|
3790
|
+
},
|
|
3791
|
+
/** Current arena fragmentation waste (bytes) — covers uniforms,
|
|
3792
|
+
* attributes, and index regions (all one arena now). */
|
|
3793
|
+
attrWasteBytes() { return arena.attrs.totalWasteBytes(); },
|
|
3794
|
+
/** Number of arena chunks (pages) currently open. >1 means group
|
|
3795
|
+
* placement rolled past one buffer cap. */
|
|
3796
|
+
pageCount() { return arena.attrs.chunkCount; },
|
|
3243
3797
|
bucketsForTest() {
|
|
3244
3798
|
return buckets.map(b => ({
|
|
3245
3799
|
indirectBuf: b.slots[0].indirectBuf,
|
|
@@ -3279,12 +3833,12 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3279
3833
|
// multi-chunk-aware validator needs per-bucket-chunkIdx
|
|
3280
3834
|
// staging which is a bigger refactor; assert for now so
|
|
3281
3835
|
// multi-chunk callers don't get false-passes.
|
|
3282
|
-
if (arena.attrs.chunkCount > 1
|
|
3836
|
+
if (arena.attrs.chunkCount > 1) {
|
|
3283
3837
|
throw new Error("heapScene/validateHeap: multi-chunk arenas not yet supported by this validator (chunk-0-only). " +
|
|
3284
3838
|
"Run with the default chunk cap (no second chunk opened) for now.");
|
|
3285
3839
|
}
|
|
3286
3840
|
const arenaSize = arena.attrs.chunk(0).buffer.size;
|
|
3287
|
-
const indicesSize = arena
|
|
3841
|
+
const indicesSize = arenaSize; // indices live in the attr arena now
|
|
3288
3842
|
const enc = device.createCommandEncoder({ label: "validateHeap" });
|
|
3289
3843
|
const stage = (src, size) => {
|
|
3290
3844
|
const c = device.createBuffer({
|
|
@@ -3295,7 +3849,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3295
3849
|
return c;
|
|
3296
3850
|
};
|
|
3297
3851
|
const arenaCopy = stage(arena.attrs.chunk(0).buffer, arenaSize);
|
|
3298
|
-
const indicesCopy =
|
|
3852
|
+
const indicesCopy = arenaCopy; // same buffer; _indexStart is an element offset into it
|
|
3299
3853
|
const TILE_K_LOCAL = 64;
|
|
3300
3854
|
const dcs = [];
|
|
3301
3855
|
for (const b of buckets) {
|
|
@@ -3348,7 +3902,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3348
3902
|
for (let slot = 0; slot < recordCount; slot++) {
|
|
3349
3903
|
const slotOff = slot * stride;
|
|
3350
3904
|
for (const f of dc.bucket.layout.drawHeaderFields) {
|
|
3351
|
-
if (f.name === "__layoutId")
|
|
3905
|
+
if (f.name === "__layoutId" || INLINE_HEADER_UNIFORMS.has(f.name))
|
|
3352
3906
|
continue;
|
|
3353
3907
|
if (f.kind !== "uniform-ref" && f.kind !== "attribute-ref")
|
|
3354
3908
|
continue;
|
|
@@ -3384,18 +3938,9 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3384
3938
|
? `prev=[${arenaU32[refU32 - 4]},${arenaU32[refU32 - 3]},${arenaU32[refU32 - 2]},${arenaU32[refU32 - 1]}] `
|
|
3385
3939
|
: "";
|
|
3386
3940
|
const here = `here=[${arenaU32[refU32]},${arenaU32[refU32 + 1]},${arenaU32[refU32 + 2]},${arenaU32[refU32 + 3]},${arenaU32[refU32 + 4]},${arenaU32[refU32 + 5]},${arenaU32[refU32 + 6]},${arenaU32[refU32 + 7]}]`;
|
|
3387
|
-
//
|
|
3388
|
-
//
|
|
3389
|
-
|
|
3390
|
-
const shadowChunk = arena.attrs.chunk(0);
|
|
3391
|
-
const shadowU32 = shadowChunk.peekShadowU32(ref, 8);
|
|
3392
|
-
const shadowStr = `shadow=[${shadowU32[0]},${shadowU32[1]},${shadowU32[2]},${shadowU32[3]},${shadowU32[4]},${shadowU32[5]},${shadowU32[6]},${shadowU32[7]}]`;
|
|
3393
|
-
const cpuMatches = shadowU32[0] === arenaU32[refU32]
|
|
3394
|
-
&& shadowU32[1] === arenaU32[refU32 + 1]
|
|
3395
|
-
&& shadowU32[2] === arenaU32[refU32 + 2]
|
|
3396
|
-
&& shadowU32[3] === arenaU32[refU32 + 3];
|
|
3397
|
-
const verdict = cpuMatches ? "GPU=CPU (likely CPU-write bug)" : "GPU≠CPU (likely GPU-write corruption)";
|
|
3398
|
-
push(`bucket#${bucketIdx} slot=${slot} field='${f.name}' alloc@0x${ref.toString(16)} typeId=${typeId} unknown ${verdict} ${before}${here} ${shadowStr}`);
|
|
3941
|
+
// (Mirror-less arena: no CPU shadow to cross-check against —
|
|
3942
|
+
// the GPU read-back is the only copy.)
|
|
3943
|
+
push(`bucket#${bucketIdx} slot=${slot} field='${f.name}' alloc@0x${ref.toString(16)} typeId=${typeId} unknown ${before}${here}`);
|
|
3399
3944
|
attrAllocsBad++;
|
|
3400
3945
|
continue;
|
|
3401
3946
|
}
|
|
@@ -3598,11 +4143,11 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3598
4143
|
const push = (s) => { if (issues.length < 30)
|
|
3599
4144
|
issues.push(s); };
|
|
3600
4145
|
// §3 v1: chunk-0-only like validateHeap.
|
|
3601
|
-
if (arena.attrs.chunkCount > 1
|
|
4146
|
+
if (arena.attrs.chunkCount > 1) {
|
|
3602
4147
|
throw new Error("heapScene/simulateDraws: multi-chunk arenas not yet supported (chunk-0-only).");
|
|
3603
4148
|
}
|
|
3604
4149
|
const arenaSize = arena.attrs.chunk(0).buffer.size;
|
|
3605
|
-
const indicesSize = arena
|
|
4150
|
+
const indicesSize = arenaSize; // indices live in the attr arena now
|
|
3606
4151
|
// Stage all buffers we'll need.
|
|
3607
4152
|
const enc = device.createCommandEncoder({ label: "simulateDraws" });
|
|
3608
4153
|
const stage = (src, size) => {
|
|
@@ -3613,7 +4158,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3613
4158
|
return c;
|
|
3614
4159
|
};
|
|
3615
4160
|
const arenaCopy = stage(arena.attrs.chunk(0).buffer, arenaSize);
|
|
3616
|
-
const indicesCopy =
|
|
4161
|
+
const indicesCopy = arenaCopy; // same buffer; _indexStart is an element offset into it
|
|
3617
4162
|
const dcs = [];
|
|
3618
4163
|
for (const b of buckets) {
|
|
3619
4164
|
if (b.slots[0].recordCount === 0 || b.slots[0].totalEmitEstimate === 0)
|
|
@@ -3654,7 +4199,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3654
4199
|
for (const dc of dcs) {
|
|
3655
4200
|
const fs = [];
|
|
3656
4201
|
for (const f of dc.bucket.layout.drawHeaderFields) {
|
|
3657
|
-
if (f.name === "__layoutId")
|
|
4202
|
+
if (f.name === "__layoutId" || INLINE_HEADER_UNIFORMS.has(f.name))
|
|
3658
4203
|
continue;
|
|
3659
4204
|
if (f.kind !== "uniform-ref" && f.kind !== "attribute-ref")
|
|
3660
4205
|
continue;
|
|
@@ -3865,11 +4410,11 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3865
4410
|
// cross-RO triangle symptom.
|
|
3866
4411
|
const allocClaims = [];
|
|
3867
4412
|
// §3 v1: chunk-0-only (see validateHeap).
|
|
3868
|
-
if (arena.attrs.chunkCount > 1
|
|
4413
|
+
if (arena.attrs.chunkCount > 1) {
|
|
3869
4414
|
throw new Error("heapScene/checkTriangleCoherence: multi-chunk arenas not yet supported (chunk-0-only).");
|
|
3870
4415
|
}
|
|
3871
4416
|
const arenaBuf0 = arena.attrs.chunk(0).buffer;
|
|
3872
|
-
const indicesBuf0 = arena.
|
|
4417
|
+
const indicesBuf0 = arena.attrs.chunk(0).buffer;
|
|
3873
4418
|
// Stage arena + arena.indices once (shared across buckets).
|
|
3874
4419
|
const arenaCopy = device.createBuffer({
|
|
3875
4420
|
size: arenaBuf0.size,
|
|
@@ -4322,7 +4867,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
4322
4867
|
{ binding: 0, resource: { buffer: target.slots[0].drawTableBuf.buffer } },
|
|
4323
4868
|
{ binding: 1, resource: { buffer: target.slots[0].firstDrawInTileBuf.buffer } },
|
|
4324
4869
|
{ binding: 2, resource: { buffer: sampleBuf } },
|
|
4325
|
-
{ binding: 3, resource: { buffer: arena.
|
|
4870
|
+
{ binding: 3, resource: { buffer: arena.attrs.chunk(target.chunkIdx).buffer } },
|
|
4326
4871
|
{ binding: 4, resource: { buffer: outBuf } },
|
|
4327
4872
|
{ binding: 5, resource: { buffer: paramBuf } },
|
|
4328
4873
|
],
|
|
@@ -4346,7 +4891,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
4346
4891
|
outBuf.destroy();
|
|
4347
4892
|
paramBuf.destroy();
|
|
4348
4893
|
// Read drawTable + indices for CPU expectation.
|
|
4349
|
-
const idxBuf = arena.
|
|
4894
|
+
const idxBuf = arena.attrs.chunk(target.chunkIdx).buffer;
|
|
4350
4895
|
const indicesCopy2 = device.createBuffer({
|
|
4351
4896
|
size: idxBuf.size,
|
|
4352
4897
|
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
|