@aardworx/wombat.rendering 0.19.23 → 0.19.25
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 +9 -0
- package/dist/core/renderObject.d.ts.map +1 -1
- package/dist/resources/preparedRenderObject.d.ts.map +1 -1
- package/dist/resources/preparedRenderObject.js +19 -2
- package/dist/resources/preparedRenderObject.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/dispatch.d.ts +5 -1
- package/dist/runtime/derivedUniforms/dispatch.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/dispatch.js +28 -13
- 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 +9 -0
- 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 +42 -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 +14 -1
- package/dist/runtime/heapEffect.d.ts.map +1 -1
- package/dist/runtime/heapEffect.js +13 -1
- package/dist/runtime/heapEffect.js.map +1 -1
- package/dist/runtime/heapEffectIR.d.ts.map +1 -1
- package/dist/runtime/heapEffectIR.js +18 -9
- 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 +116 -1
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +581 -125
- package/dist/runtime/heapScene.js.map +1 -1
- package/dist/runtime/hybridScene.d.ts.map +1 -1
- package/dist/runtime/hybridScene.js +19 -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/package.json +1 -1
- package/src/core/elementType.ts +10 -0
- package/src/core/provider.ts +56 -28
- package/src/core/renderObject.ts +9 -0
- package/src/resources/preparedRenderObject.ts +22 -3
- package/src/runtime/derivedModes/modeKeyCpu.ts +26 -11
- package/src/runtime/derivedModes/partitionDispatcher.ts +30 -0
- package/src/runtime/derivedUniforms/dispatch.ts +26 -10
- package/src/runtime/derivedUniforms/records.ts +70 -15
- package/src/runtime/derivedUniforms/sceneIntegration.ts +10 -0
- package/src/runtime/flattenTree.ts +47 -2
- package/src/runtime/heapAdapter.ts +40 -5
- package/src/runtime/heapDecoder.ts +8 -3
- package/src/runtime/heapEffect.ts +15 -1
- package/src/runtime/heapEffectIR.ts +18 -9
- 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 +717 -141
- package/src/runtime/hybridScene.ts +20 -3
- package/src/runtime/index.ts +5 -0
|
@@ -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, {
|
|
@@ -826,6 +961,29 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
826
961
|
}
|
|
827
962
|
familyFor(effect); // throws if unknown
|
|
828
963
|
}
|
|
964
|
+
/**
|
|
965
|
+
* Reactive family rebuild: register a new effect's family when it first
|
|
966
|
+
* appears after the initial freeze. Family-merge is disabled, so every
|
|
967
|
+
* Effect already gets its OWN independent single-member family (its own
|
|
968
|
+
* shader modules + buckets); a late effect just needs its family compiled
|
|
969
|
+
* and added — no rebuild of existing families. Idempotent.
|
|
970
|
+
*/
|
|
971
|
+
function ensureFamilyForSpec(spec) {
|
|
972
|
+
if (!familyBuilt) {
|
|
973
|
+
buildFamilyFromSpecs([spec]);
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
if (familyByEffectId.has(spec.effect.id))
|
|
977
|
+
return;
|
|
978
|
+
const entry = { attributes: new Set(), uniforms: new Set() };
|
|
979
|
+
if (spec.instanceAttributes !== undefined) {
|
|
980
|
+
for (const name of Object.keys(spec.instanceAttributes))
|
|
981
|
+
entry.attributes.add(name);
|
|
982
|
+
}
|
|
983
|
+
const singleMap = new Map();
|
|
984
|
+
singleMap.set(spec.effect, entry);
|
|
985
|
+
familyByEffectId.set(spec.effect.id, compileFamilyFor([spec.effect], singleMap));
|
|
986
|
+
}
|
|
829
987
|
// ─── id-of helpers ────────────────────────────────────────────────
|
|
830
988
|
const idOf = (s) => `effect#${s.id}`;
|
|
831
989
|
const textureIds = new WeakMap();
|
|
@@ -961,7 +1119,10 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
961
1119
|
// binary search. Minimum one zero-record to satisfy WebGPU non-
|
|
962
1120
|
// zero size constraint when the bucket is empty.
|
|
963
1121
|
const dtBytes = Math.max(RECORD_BYTES, s.recordCount * RECORD_BYTES);
|
|
964
|
-
entries.push({ binding: 4, resource: { buffer: s.drawTableBuf.buffer, offset: 0, size: dtBytes } },
|
|
1122
|
+
entries.push({ binding: 4, resource: { buffer: s.drawTableBuf.buffer, offset: 0, size: dtBytes } },
|
|
1123
|
+
// Index data lives in the attr arena now (the VS storage-decodes it);
|
|
1124
|
+
// bind the same buffer as the gather views.
|
|
1125
|
+
{ binding: 5, resource: { buffer: attrsBuf } }, { binding: 6, resource: { buffer: s.firstDrawInTileBuf.buffer } });
|
|
965
1126
|
s.renderBoundRecordCount = s.recordCount;
|
|
966
1127
|
}
|
|
967
1128
|
// Schema-driven texture + sampler entries. v1 user surface still
|
|
@@ -1081,15 +1242,6 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1081
1242
|
atlasPool.onPageAdded(f, () => rebuildAllBindGroups(true));
|
|
1082
1243
|
}
|
|
1083
1244
|
}
|
|
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
1245
|
// ─── findOrCreateBucket ───────────────────────────────────────────
|
|
1094
1246
|
// Slice 3c: the bucket key collapses to (familyId, pipelineState).
|
|
1095
1247
|
// Every member effect in the family shares one bucket per
|
|
@@ -1746,7 +1898,11 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1746
1898
|
if (cpuMigration !== undefined) {
|
|
1747
1899
|
let totalEmit = 0;
|
|
1748
1900
|
for (const m of cpuMigration) {
|
|
1749
|
-
const uRefs = bucket.uniformOrder.map(n =>
|
|
1901
|
+
const uRefs = bucket.uniformOrder.map(n => {
|
|
1902
|
+
const i = bucket.fieldIdx.get(n);
|
|
1903
|
+
const r = i === undefined ? -1 : m.perDrawRefs[i];
|
|
1904
|
+
return r < 0 ? 0 : r;
|
|
1905
|
+
});
|
|
1750
1906
|
const recIdx = partition.appendRecord(m.localSlot, m.firstIndex, m.indexCount, m.instanceCount, m.targetComboId, uRefs);
|
|
1751
1907
|
bucket.drawIdToRecord.set(m.drawId, recIdx);
|
|
1752
1908
|
bucket.recordToDrawId[recIdx] = m.drawId;
|
|
@@ -1857,6 +2013,8 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1857
2013
|
const isAtlasBucket = layout.atlasTextureBindings.size > 0;
|
|
1858
2014
|
const drawHeapBuf = new GrowBuffer(device, `heapScene/${bk}/drawHeap`, GPUBufferUsage.STORAGE, Math.max(layout.drawHeaderBytes, 64));
|
|
1859
2015
|
const drawHeap = new DrawHeap(drawHeapBuf, layout.drawHeaderBytes);
|
|
2016
|
+
const fieldIdx = new Map();
|
|
2017
|
+
layout.drawHeaderFields.forEach((f, i) => fieldIdx.set(f.name, i));
|
|
1860
2018
|
const bucket = {
|
|
1861
2019
|
label: bk, textures: undefined, layout, chunkIdx,
|
|
1862
2020
|
slots: [], // populated lazily by ensureSlot per modeKey
|
|
@@ -1866,7 +2024,10 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1866
2024
|
headerDirtyMin: Infinity, headerDirtyMax: 0,
|
|
1867
2025
|
localPosRefs: [], localNorRefs: [],
|
|
1868
2026
|
localEntries: [], localToDrawId: [],
|
|
1869
|
-
localPerDrawAvals: [], localPerDrawRefs: [], localLayoutIds: [],
|
|
2027
|
+
localPerDrawAvals: [], localPerDrawRawFieldIdx: [], localPerDrawRefs: [], localLayoutIds: [],
|
|
2028
|
+
fieldIdx,
|
|
2029
|
+
posFieldIdx: fieldIdx.get("Positions") ?? -1,
|
|
2030
|
+
norFieldIdx: fieldIdx.get("Normals") ?? -1,
|
|
1870
2031
|
drawSlots: new Set(), dirty: new Set(),
|
|
1871
2032
|
isAtlasBucket,
|
|
1872
2033
|
localAtlasReleases: [],
|
|
@@ -1936,7 +2097,9 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1936
2097
|
const dst = bucket.drawHeaderStaging;
|
|
1937
2098
|
const u32 = new Uint32Array(dst.buffer, dst.byteOffset, dst.length);
|
|
1938
2099
|
const baseFloat = (localSlot * bucket.layout.drawHeaderBytes) / 4;
|
|
1939
|
-
|
|
2100
|
+
const fields = bucket.layout.drawHeaderFields;
|
|
2101
|
+
for (let i = 0; i < fields.length; i++) {
|
|
2102
|
+
const f = fields[i];
|
|
1940
2103
|
// texture-ref fields carry inline values (pageRef/formatBits as u32,
|
|
1941
2104
|
// origin/size as vec2<f32>) and are filled by packAtlasTextureFields.
|
|
1942
2105
|
if (f.kind === "texture-ref")
|
|
@@ -1950,15 +2113,11 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1950
2113
|
u32[fOff] = layoutId >>> 0;
|
|
1951
2114
|
continue;
|
|
1952
2115
|
}
|
|
1953
|
-
const ref = perDrawRefs
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
u32[fOff] = 0;
|
|
1959
|
-
continue;
|
|
1960
|
-
}
|
|
1961
|
-
u32[fOff] = ref;
|
|
2116
|
+
const ref = perDrawRefs[i];
|
|
2117
|
+
// Family-merge: a slot's effect doesn't populate every field of
|
|
2118
|
+
// the union (−1); leave the unused slots zero — the layoutId
|
|
2119
|
+
// switch ensures they're never read by the wrong effect's helper.
|
|
2120
|
+
u32[fOff] = ref < 0 ? 0 : ref;
|
|
1962
2121
|
}
|
|
1963
2122
|
}
|
|
1964
2123
|
// ─── Atlas drawHeader packing + page-set tracking ─────────────────
|
|
@@ -2043,6 +2202,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2043
2202
|
totalDraws: 0,
|
|
2044
2203
|
drawBytes: 0,
|
|
2045
2204
|
geometryBytes: 0,
|
|
2205
|
+
compactions: 0,
|
|
2046
2206
|
derivedPullMs: 0,
|
|
2047
2207
|
derivedUploadMs: 0,
|
|
2048
2208
|
derivedEncodeMs: 0,
|
|
@@ -2085,12 +2245,10 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2085
2245
|
// single spec when no batched lazy-build occurred earlier (e.g.
|
|
2086
2246
|
// direct addDraw at runtime). The aset / array initial-population
|
|
2087
2247
|
// paths build from the full effect set up front.
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
ensureFamilyKnowsEffect(spec.effect);
|
|
2093
|
-
}
|
|
2248
|
+
// Build the family lazily from this spec (first addDraw), or register a
|
|
2249
|
+
// newly-seen effect's family on the fly (reactive rebuild) — supports a
|
|
2250
|
+
// scene mixing multiple effects added over time (tiles + markers + …).
|
|
2251
|
+
ensureFamilyForSpec(spec);
|
|
2094
2252
|
const perInstanceUniforms = new Set();
|
|
2095
2253
|
const perInstanceAttributes = spec.instanceAttributes !== undefined
|
|
2096
2254
|
? new Set(Object.keys(spec.instanceAttributes))
|
|
@@ -2111,16 +2269,48 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2111
2269
|
token: outerTok,
|
|
2112
2270
|
});
|
|
2113
2271
|
}
|
|
2114
|
-
//
|
|
2115
|
-
//
|
|
2116
|
-
//
|
|
2117
|
-
//
|
|
2118
|
-
//
|
|
2119
|
-
//
|
|
2120
|
-
//
|
|
2121
|
-
//
|
|
2122
|
-
//
|
|
2123
|
-
|
|
2272
|
+
// ─── Group-fit page placement (§3) ───────────────────────────────
|
|
2273
|
+
// A draw's whole group — uniforms + attributes + INDEX data (all one
|
|
2274
|
+
// arena now) — must land on ONE chunk/page (the draw binds one buffer).
|
|
2275
|
+
// Estimate the group's arena bytes (exact per-field via the same
|
|
2276
|
+
// placement helpers, ignoring dedup ⇒ an upper bound on the NEW bytes a
|
|
2277
|
+
// chunk must absorb), then place the group on a chunk with room — prefer
|
|
2278
|
+
// one already holding a shared input, else the newest, else open a fresh
|
|
2279
|
+
// page. This replaces the old cross-chunk spill → throw, so a scene can
|
|
2280
|
+
// grow past one buffer's cap (WebGPU's storage-buffer binding limit).
|
|
2281
|
+
const fam = familyFor(spec.effect);
|
|
2282
|
+
const effectFields = fam.fieldsForEffect.get(spec.effect.id);
|
|
2283
|
+
const layout = fam.schema.drawHeaderUnion;
|
|
2284
|
+
const hasIndices = spec.indices !== undefined;
|
|
2285
|
+
const indicesAval = hasIndices ? asAval(spec.indices) : undefined;
|
|
2286
|
+
const indexArr = hasIndices ? readPlain(spec.indices) : undefined;
|
|
2287
|
+
const instanceCount = spec.instanceCount !== undefined
|
|
2288
|
+
? readPlain(spec.instanceCount) : 1;
|
|
2289
|
+
let estBytes = indexArr !== undefined ? ALIGN16(ALLOC_HEADER_PAD_TO + indexArr.byteLength) : 0;
|
|
2290
|
+
for (const f of layout.drawHeaderFields) {
|
|
2291
|
+
if (f.kind === "texture-ref" || f.name === "__layoutId" || INLINE_HEADER_UNIFORMS.has(f.name) || !effectFields.has(f.name))
|
|
2292
|
+
continue;
|
|
2293
|
+
if (ruleForUniform(spec, f.name) !== undefined) {
|
|
2294
|
+
estBytes += ALIGN16(ALLOC_HEADER_PAD_TO + PACKER_MAT4.dataBytes); // §7 derived dummy mat4
|
|
2295
|
+
continue;
|
|
2296
|
+
}
|
|
2297
|
+
const perInstUniform = f.kind === "uniform-ref" && perInstanceUniforms.has(f.name);
|
|
2298
|
+
const perInstAttr = f.kind === "attribute-ref" && perInstanceAttributes.has(f.name);
|
|
2299
|
+
const provided = perInstAttr ? spec.instanceAttributes[f.name] : spec.inputs[f.name];
|
|
2300
|
+
if (provided === undefined)
|
|
2301
|
+
continue; // the acquire loop throws with a precise message
|
|
2302
|
+
let dataBytes;
|
|
2303
|
+
if (f.kind === "attribute-ref" && !perInstUniform && isBufferView(provided)) {
|
|
2304
|
+
dataBytes = bufferViewPlacement(f, provided).dataBytes;
|
|
2305
|
+
}
|
|
2306
|
+
else {
|
|
2307
|
+
const value = asAval(provided).getValue(outerTok);
|
|
2308
|
+
dataBytes = (perInstUniform
|
|
2309
|
+
? perInstancePlacementFor(f, value, instanceCount)
|
|
2310
|
+
: poolPlacementFor(f, value)).dataBytes;
|
|
2311
|
+
}
|
|
2312
|
+
estBytes += ALIGN16(ALLOC_HEADER_PAD_TO + dataBytes);
|
|
2313
|
+
}
|
|
2124
2314
|
let chunkIdx;
|
|
2125
2315
|
{
|
|
2126
2316
|
let preferred;
|
|
@@ -2135,7 +2325,12 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2135
2325
|
}
|
|
2136
2326
|
}
|
|
2137
2327
|
}
|
|
2138
|
-
|
|
2328
|
+
const fits = (c) => arena.attrs.bumpHeadroom(c) >= estBytes;
|
|
2329
|
+
const newest = arena.attrs.chunkCount - 1;
|
|
2330
|
+
chunkIdx =
|
|
2331
|
+
preferred !== undefined && fits(preferred) ? preferred
|
|
2332
|
+
: fits(newest) ? newest
|
|
2333
|
+
: arena.attrs.openPage();
|
|
2139
2334
|
}
|
|
2140
2335
|
const bucket = findOrCreateBucket(spec.effect, spec.textures, spec.pipelineState, chunkIdx, precomputedDescriptor);
|
|
2141
2336
|
// Phase 5c.3 — every RO carrying a derived-mode rule is registered
|
|
@@ -2169,18 +2364,16 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2169
2364
|
// tables every frame from the master.
|
|
2170
2365
|
const roSlot = bucket.gpuRouted ? bucket.slots[0] : ensureSlot(bucket, roDescriptor);
|
|
2171
2366
|
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;
|
|
2367
|
+
// Indices ride the same arena as attributes/uniforms (the VS storage-
|
|
2368
|
+
// decodes them). Aval-keyed: 19K instanced clones of the same mesh share
|
|
2369
|
+
// one index allocation + one upload. NON-INDEXED draws (spec.indices
|
|
2370
|
+
// omitted) skip the pool: the drawTable record stores firstIndex =
|
|
2371
|
+
// HEAP_NONINDEXED (sentinel) and indexCount = vertexCount, and the
|
|
2372
|
+
// megacall prelude uses the local vertex index directly. `indicesAval` /
|
|
2373
|
+
// `indexArr` were read above for the group estimate; the group landed
|
|
2374
|
+
// wholly on `bucket.chunkIdx`, so this allocation can't spill.
|
|
2182
2375
|
const idxAlloc = hasIndices
|
|
2183
|
-
? indexPool.acquire(device, arena.
|
|
2376
|
+
? indexPool.acquire(device, arena.attrs, indicesAval, bucket.chunkIdx, indexArr)
|
|
2184
2377
|
: undefined;
|
|
2185
2378
|
// Emit count per instance + the firstIndex field written to the record.
|
|
2186
2379
|
// Slice: emit only [firstIndex, firstIndex+indexCount) of the (shared,
|
|
@@ -2189,17 +2382,26 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2189
2382
|
const emitCount = hasIndices ? (spec.indexCount ?? idxAlloc.count) : (spec.vertexCount ?? 0);
|
|
2190
2383
|
const firstIndexField = hasIndices ? (idxAlloc.firstIndex + (spec.firstIndex ?? 0)) : HEAP_NONINDEXED;
|
|
2191
2384
|
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
2385
|
// Walk the bucket's schema-driven DrawHeader fields. Per-instance
|
|
2197
2386
|
// attributes pull from `spec.instanceAttributes` and pack into an
|
|
2198
2387
|
// array allocation; everything else pulls from `spec.inputs` and
|
|
2199
2388
|
// packs as a single value. Both go through the same pool — sharing
|
|
2200
2389
|
// emerges from aval identity either way.
|
|
2201
2390
|
const perDrawAvals = [];
|
|
2202
|
-
|
|
2391
|
+
let perDrawRawFieldIdx;
|
|
2392
|
+
const perDrawRefs = new Int32Array(bucket.layout.drawHeaderFields.length).fill(-1);
|
|
2393
|
+
const setRef = (name, ref) => {
|
|
2394
|
+
const i = bucket.fieldIdx.get(name);
|
|
2395
|
+
if (i !== undefined)
|
|
2396
|
+
perDrawRefs[i] = ref;
|
|
2397
|
+
};
|
|
2398
|
+
const getRef = (name) => {
|
|
2399
|
+
const i = bucket.fieldIdx.get(name);
|
|
2400
|
+
if (i === undefined)
|
|
2401
|
+
return undefined;
|
|
2402
|
+
const r = perDrawRefs[i];
|
|
2403
|
+
return r < 0 ? undefined : r;
|
|
2404
|
+
};
|
|
2203
2405
|
{
|
|
2204
2406
|
const tok = outerTok;
|
|
2205
2407
|
for (const f of bucket.layout.drawHeaderFields) {
|
|
@@ -2214,15 +2416,42 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2214
2416
|
continue;
|
|
2215
2417
|
if (!effectFields.has(f.name))
|
|
2216
2418
|
continue;
|
|
2419
|
+
// Inline header uniforms (PickId): the header word IS the u32
|
|
2420
|
+
// value — write it via perDrawRefs (packBucketHeader copies
|
|
2421
|
+
// refs verbatim), skip the pool entirely. A provider-supplied
|
|
2422
|
+
// PickId aval still works below as the (slow) fallback.
|
|
2423
|
+
if (INLINE_HEADER_UNIFORMS.has(f.name)) {
|
|
2424
|
+
const iv = f.name === "PickId" ? spec.pickId : undefined;
|
|
2425
|
+
if (iv !== undefined) {
|
|
2426
|
+
setRef(f.name, iv >>> 0);
|
|
2427
|
+
continue;
|
|
2428
|
+
}
|
|
2429
|
+
const provInline = spec.inputs[f.name];
|
|
2430
|
+
if (typeof provInline === "number") {
|
|
2431
|
+
setRef(f.name, provInline >>> 0);
|
|
2432
|
+
continue;
|
|
2433
|
+
}
|
|
2434
|
+
if (provInline === undefined) {
|
|
2435
|
+
// No id at all (picking disabled but the effect still
|
|
2436
|
+
// reads the uniform): 0 = "no pick", a reserved id.
|
|
2437
|
+
setRef(f.name, 0);
|
|
2438
|
+
continue;
|
|
2439
|
+
}
|
|
2440
|
+
// fall through: aval-provided → legacy pooled path
|
|
2441
|
+
}
|
|
2217
2442
|
// §7: derived-uniform fields are produced by the compute
|
|
2218
2443
|
// pre-pass. We still allocate an arena slot here so the
|
|
2219
2444
|
// drawHeader gets a valid ref written by packBucketHeader; §7
|
|
2220
2445
|
// overwrites the arena data each frame the inputs are dirty.
|
|
2221
2446
|
if (ruleForUniform(spec, f.name) !== undefined) {
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2447
|
+
// Raw (aval-less) slot: §7's compute pre-pass overwrites the
|
|
2448
|
+
// data every dirty frame, so a keyed pool entry (placeholder
|
|
2449
|
+
// ConstantVal + Map per draw) bought nothing. The ref lives
|
|
2450
|
+
// in the drawHeader cell (compaction re-seats it there);
|
|
2451
|
+
// removeDraw frees it via `perDrawRawFieldIdx`.
|
|
2452
|
+
const ref = pool.allocRaw(arena.attrs, bucket.chunkIdx, PACKER_MAT4.dataBytes, PACKER_MAT4.typeId, 1);
|
|
2453
|
+
setRef(f.name, ref);
|
|
2454
|
+
(perDrawRawFieldIdx ??= []).push(bucket.fieldIdx.get(f.name));
|
|
2226
2455
|
continue;
|
|
2227
2456
|
}
|
|
2228
2457
|
const isPerInstanceUniformField = f.kind === "uniform-ref" && perInstanceUniforms.has(f.name);
|
|
@@ -2249,12 +2478,37 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2249
2478
|
let av;
|
|
2250
2479
|
let value;
|
|
2251
2480
|
let placement;
|
|
2481
|
+
let releasable;
|
|
2252
2482
|
if (f.kind === "attribute-ref" && !isPerInstanceUniformField && isBufferView(provided)) {
|
|
2253
2483
|
const bv = provided;
|
|
2254
2484
|
placement = bufferViewPlacement(f, bv);
|
|
2255
2485
|
av = bv.buffer;
|
|
2256
2486
|
value = bv.buffer.getValue(tok);
|
|
2257
2487
|
}
|
|
2488
|
+
else if (releaseConstantAttributes
|
|
2489
|
+
&& f.kind === "attribute-ref"
|
|
2490
|
+
&& !isPerInstanceUniformField
|
|
2491
|
+
&& !(typeof provided === "object" && provided !== null
|
|
2492
|
+
&& typeof provided.getValue === "function")) {
|
|
2493
|
+
// Plain value (raw array / packed marker) → CONSTANT by definition.
|
|
2494
|
+
// Wrap in a droppable holder: after this field's arena write the GPU
|
|
2495
|
+
// copy is the only copy the heap ever reads again, so the payload is
|
|
2496
|
+
// released (real avals never take this branch — changeable data
|
|
2497
|
+
// stays alive by contract).
|
|
2498
|
+
const holder = {
|
|
2499
|
+
_v: provided,
|
|
2500
|
+
getValue() {
|
|
2501
|
+
if (holder._v === null) {
|
|
2502
|
+
throw new Error("heapScene: constant attribute payload was released after staging (releaseConstantAttributes)");
|
|
2503
|
+
}
|
|
2504
|
+
return holder._v;
|
|
2505
|
+
},
|
|
2506
|
+
};
|
|
2507
|
+
releasable = holder;
|
|
2508
|
+
av = holder;
|
|
2509
|
+
value = provided;
|
|
2510
|
+
placement = poolPlacementFor(f, value);
|
|
2511
|
+
}
|
|
2258
2512
|
else {
|
|
2259
2513
|
av = asAval(provided);
|
|
2260
2514
|
value = av.getValue(tok);
|
|
@@ -2263,18 +2517,21 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2263
2517
|
: poolPlacementFor(f, value);
|
|
2264
2518
|
}
|
|
2265
2519
|
const ref = pool.acquire(device, arena.attrs, av, bucket.chunkIdx, value, placement.dataBytes, placement.typeId, placement.length, placement.pack);
|
|
2266
|
-
|
|
2520
|
+
if (releasable !== undefined)
|
|
2521
|
+
releasable._v = null; // staged — drop the CPU copy
|
|
2522
|
+
setRef(f.name, ref);
|
|
2267
2523
|
perDrawAvals.push(av);
|
|
2268
2524
|
}
|
|
2269
2525
|
}
|
|
2270
|
-
bucket.localPosRefs[localSlot] =
|
|
2271
|
-
bucket.localNorRefs[localSlot] =
|
|
2526
|
+
bucket.localPosRefs[localSlot] = getRef("Positions");
|
|
2527
|
+
bucket.localNorRefs[localSlot] = getRef("Normals");
|
|
2272
2528
|
bucket.localEntries[localSlot] = {
|
|
2273
2529
|
indexCount: emitCount, firstIndex: firstIndexField, instanceCount,
|
|
2274
2530
|
};
|
|
2275
2531
|
bucket.localToDrawId[localSlot] = drawId;
|
|
2276
2532
|
bucket.drawSlots.add(localSlot);
|
|
2277
2533
|
bucket.localPerDrawAvals[localSlot] = perDrawAvals;
|
|
2534
|
+
bucket.localPerDrawRawFieldIdx[localSlot] = perDrawRawFieldIdx;
|
|
2278
2535
|
bucket.localPerDrawRefs[localSlot] = perDrawRefs;
|
|
2279
2536
|
const layoutId = fam.schema.layoutIdOf.get(spec.effect);
|
|
2280
2537
|
bucket.localLayoutIds[localSlot] = layoutId;
|
|
@@ -2377,7 +2634,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2377
2634
|
// don't carry a given name (e.g. their effect doesn't declare
|
|
2378
2635
|
// it) get 0 — the kernel only reads refs through combo fns that
|
|
2379
2636
|
// gate on `r.comboId`, so an unread 0 is harmless.
|
|
2380
|
-
const uniformRefs = (bucket.uniformOrder ?? []).map(name =>
|
|
2637
|
+
const uniformRefs = (bucket.uniformOrder ?? []).map(name => getRef(name) ?? 0);
|
|
2381
2638
|
partition.appendRecord(localSlot, firstIndexField, emitCount, instanceCount, roComboId, uniformRefs);
|
|
2382
2639
|
bucket.drawIdToRecord.set(drawId, recIdx);
|
|
2383
2640
|
bucket.recordToDrawId[recIdx] = drawId;
|
|
@@ -2477,7 +2734,11 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2477
2734
|
}
|
|
2478
2735
|
uniformAvals = m;
|
|
2479
2736
|
}
|
|
2480
|
-
const tracker = new ModeKeyTracker(spec.pipelineState, sig,
|
|
2737
|
+
const tracker = new ModeKeyTracker(spec.pipelineState, sig,
|
|
2738
|
+
// skipSubscribe means the tracker never invokes onDirty itself
|
|
2739
|
+
// (scene-level modeAvalToDrawIds dispatches instead) — a shared
|
|
2740
|
+
// noop avoids one closure per draw.
|
|
2741
|
+
NOOP, {
|
|
2481
2742
|
skipSubscribe: true,
|
|
2482
2743
|
...(spec.modeRules !== undefined ? { modeRules: spec.modeRules } : {}),
|
|
2483
2744
|
...(uniformAvals !== undefined ? { uniformAvals } : {}),
|
|
@@ -2509,14 +2770,14 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2509
2770
|
const ruleSubset = new Map();
|
|
2510
2771
|
const outOffsetByName = new Map();
|
|
2511
2772
|
for (const f of bucket.layout.drawHeaderFields) {
|
|
2512
|
-
if (f.name === "__layoutId")
|
|
2773
|
+
if (f.name === "__layoutId" || INLINE_HEADER_UNIFORMS.has(f.name))
|
|
2513
2774
|
continue;
|
|
2514
2775
|
if (!effectFields.has(f.name))
|
|
2515
2776
|
continue;
|
|
2516
2777
|
const rule = ruleForUniform(spec, f.name);
|
|
2517
2778
|
if (rule === undefined)
|
|
2518
2779
|
continue;
|
|
2519
|
-
const ref =
|
|
2780
|
+
const ref = getRef(f.name);
|
|
2520
2781
|
if (ref === undefined)
|
|
2521
2782
|
continue;
|
|
2522
2783
|
ruleSubset.set(f.name, rule);
|
|
@@ -2545,7 +2806,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2545
2806
|
trafoAvals,
|
|
2546
2807
|
...(spec.modelChain !== undefined ? { modelChain: spec.modelChain } : {}),
|
|
2547
2808
|
hostUniformOffset: (n) => {
|
|
2548
|
-
const r =
|
|
2809
|
+
const r = getRef(n);
|
|
2549
2810
|
return r === undefined ? undefined : r + ALLOC_HEADER_PAD_TO;
|
|
2550
2811
|
},
|
|
2551
2812
|
outputOffset: (n) => outOffsetByName.get(n),
|
|
@@ -2660,9 +2921,23 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2660
2921
|
if (avals !== undefined)
|
|
2661
2922
|
for (const av of avals)
|
|
2662
2923
|
pool.release(arena.attrs, av, bucket.chunkIdx);
|
|
2924
|
+
// Raw §7 slots: the live ref is the drawHeader cell (compaction
|
|
2925
|
+
// keeps it current); free it directly.
|
|
2926
|
+
const rawIdxs = bucket.localPerDrawRawFieldIdx[localSlot];
|
|
2927
|
+
if (rawIdxs !== undefined) {
|
|
2928
|
+
const rawRefs = bucket.localPerDrawRefs[localSlot];
|
|
2929
|
+
if (rawRefs !== undefined) {
|
|
2930
|
+
for (const fi of rawIdxs) {
|
|
2931
|
+
const ref = rawRefs[fi];
|
|
2932
|
+
if (ref >= 0)
|
|
2933
|
+
pool.releaseRaw(arena.attrs, bucket.chunkIdx, ref, PACKER_MAT4.dataBytes);
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
bucket.localPerDrawRawFieldIdx[localSlot] = undefined;
|
|
2937
|
+
}
|
|
2663
2938
|
const idxAval = drawIdToIndexAval[drawId];
|
|
2664
2939
|
if (idxAval !== undefined)
|
|
2665
|
-
indexPool.release(arena.
|
|
2940
|
+
indexPool.release(arena.attrs, idxAval, bucket.chunkIdx);
|
|
2666
2941
|
const atlasRel = bucket.localAtlasReleases[localSlot];
|
|
2667
2942
|
if (atlasRel !== undefined)
|
|
2668
2943
|
atlasRel();
|
|
@@ -2729,14 +3004,16 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2729
3004
|
if (asetReader === undefined)
|
|
2730
3005
|
return;
|
|
2731
3006
|
const delta = asetReader.getChanges(tok);
|
|
3007
|
+
// Process removals before adds. A remove+add in the same
|
|
3008
|
+
// transaction must free its slot / arena region / atlas tile
|
|
3009
|
+
// before the add allocates, so the freelist reuses the just-freed
|
|
3010
|
+
// block instead of growing the high-water toward
|
|
3011
|
+
// live + removed-this-tick. (Matches Aardvark's removals-first
|
|
3012
|
+
// delta discipline.)
|
|
3013
|
+
const adds = [];
|
|
2732
3014
|
delta.iter((op) => {
|
|
2733
3015
|
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);
|
|
3016
|
+
adds.push(op.value);
|
|
2740
3017
|
}
|
|
2741
3018
|
else {
|
|
2742
3019
|
const id = specToDrawId.get(op.value);
|
|
@@ -2746,6 +3023,13 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2746
3023
|
}
|
|
2747
3024
|
}
|
|
2748
3025
|
});
|
|
3026
|
+
// Re-use the outer evaluateAlways scope's token (caller is already
|
|
3027
|
+
// sceneObj) — collapses N nested evaluateAlways into one for a bulk
|
|
3028
|
+
// add. Saves a per-RO setUnsafeEvaluationDepth + outputs.add round trip.
|
|
3029
|
+
for (const value of adds) {
|
|
3030
|
+
const id = addDrawImpl(value, tok);
|
|
3031
|
+
specToDrawId.set(value, id);
|
|
3032
|
+
}
|
|
2749
3033
|
}
|
|
2750
3034
|
// ─── Initial population ───────────────────────────────────────────
|
|
2751
3035
|
if (Array.isArray(initialDraws)) {
|
|
@@ -2764,9 +3048,9 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2764
3048
|
asetReader = initialDraws.getReader();
|
|
2765
3049
|
sceneObj.evaluateAlways(AdaptiveToken.top, (tok) => {
|
|
2766
3050
|
// First drain: snapshot all add-ops, build the family from their
|
|
2767
|
-
// effects up front, then run addDraw. Subsequent drains
|
|
2768
|
-
//
|
|
2769
|
-
//
|
|
3051
|
+
// effects up front, then run addDraw. Subsequent drains reuse the
|
|
3052
|
+
// family, or register a newly-seen effect's family on the fly
|
|
3053
|
+
// (ensureFamilyForSpec, via addDrawImpl).
|
|
2770
3054
|
if (!familyBuilt) {
|
|
2771
3055
|
const reader = asetReader;
|
|
2772
3056
|
const delta = reader.getChanges(tok);
|
|
@@ -2798,6 +3082,148 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2798
3082
|
}
|
|
2799
3083
|
});
|
|
2800
3084
|
}
|
|
3085
|
+
// ─── Heap compaction (attribute arena + index buffer) ────────────
|
|
3086
|
+
/**
|
|
3087
|
+
* Compact every arena/index chunk whose fragmentation waste crosses the
|
|
3088
|
+
* floor (or unconditionally when `force`), then re-seat EVERY cached ref so
|
|
3089
|
+
* the relocated allocations stay addressable. Attribute arena:
|
|
3090
|
+
* • uniform-pool entries (`pool.remapRefs`)
|
|
3091
|
+
* • §7 derived-uniform record handles (`derivedScene.remapHostHeap`)
|
|
3092
|
+
* • modes partition master refs (`partitionScene.remapUniformRefs`)
|
|
3093
|
+
* • drawHeader cells + the Pos/Nor caches (re-`packBucketHeader`)
|
|
3094
|
+
* Index buffer (ref = `allocBase + slice`, held in the drawTable / partition
|
|
3095
|
+
* master + IndexPool entry):
|
|
3096
|
+
* • per-draw `indexStart` in the CPU drawTable or partition master
|
|
3097
|
+
* • `localEntries[slot].firstIndex` + IndexPool entry base
|
|
3098
|
+
* Must run with the buffers current (we flush first) and before the
|
|
3099
|
+
* per-bucket header / drawTable flush so the re-seated records upload.
|
|
3100
|
+
*/
|
|
3101
|
+
// Per-scene ref re-seat after the shared store relocates regions. Registered
|
|
3102
|
+
// with `storage`; the store calls it (for THIS scene + any others sharing the
|
|
3103
|
+
// store) inside `storage.compact`. Re-seats derived records, partition master
|
|
3104
|
+
// refs, drawHeader cells, and index `_indexStart` — everything this scene
|
|
3105
|
+
// caches that points into the arena. (The storage-level uniform/index pools
|
|
3106
|
+
// are re-seated by the store itself, around this call.)
|
|
3107
|
+
function applyRemap(chunkIdx, remap) {
|
|
3108
|
+
if (derivedScene !== undefined) {
|
|
3109
|
+
// §7 handles store DATA offsets (ref + header pad), not header refs.
|
|
3110
|
+
const dataRemap = new Map();
|
|
3111
|
+
for (const [o, n] of remap) {
|
|
3112
|
+
dataRemap.set(o + ALLOC_HEADER_PAD_TO, n + ALLOC_HEADER_PAD_TO);
|
|
3113
|
+
}
|
|
3114
|
+
derivedScene.remapHostHeap(chunkIdx, dataRemap);
|
|
3115
|
+
}
|
|
3116
|
+
{
|
|
3117
|
+
for (const bucket of buckets) {
|
|
3118
|
+
if (bucket.chunkIdx !== chunkIdx)
|
|
3119
|
+
continue;
|
|
3120
|
+
if (bucket.gpuRouted && bucket.partitionScene !== undefined) {
|
|
3121
|
+
if (bucket.partitionScene.remapUniformRefs(remap) > 0) {
|
|
3122
|
+
bucket.partitionDirty = true;
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
for (const localSlot of bucket.drawSlots) {
|
|
3126
|
+
// (a) Index region re-seat. Index data is an attr-arena region now,
|
|
3127
|
+
// so it relocates in THIS pass. Its draw stores `_indexStart` (an
|
|
3128
|
+
// element offset = allocBase + slice) in the drawTable / partition
|
|
3129
|
+
// master; shift it by the region's element delta. Independent of the
|
|
3130
|
+
// uniform re-seat below (a slot's indices may move while its uniforms
|
|
3131
|
+
// don't), so it runs before the `changed` gate.
|
|
3132
|
+
const drawId = bucket.localToDrawId[localSlot];
|
|
3133
|
+
if (drawId !== undefined) {
|
|
3134
|
+
const iav = drawIdToIndexAval[drawId];
|
|
3135
|
+
const oldRef = iav !== undefined ? indexPool.baseFor(iav, chunkIdx) : undefined;
|
|
3136
|
+
const newRef = oldRef !== undefined ? remap.get(oldRef) : undefined;
|
|
3137
|
+
const entry = bucket.localEntries[localSlot];
|
|
3138
|
+
if (oldRef !== undefined && newRef !== undefined && entry !== undefined) {
|
|
3139
|
+
const newStart = entry.firstIndex + ((newRef - oldRef) >> 2);
|
|
3140
|
+
entry.firstIndex = newStart;
|
|
3141
|
+
if (bucket.gpuRouted && bucket.partitionScene !== undefined) {
|
|
3142
|
+
const recIdx = bucket.drawIdToRecord?.get(drawId);
|
|
3143
|
+
if (recIdx !== undefined) {
|
|
3144
|
+
bucket.partitionScene.setRecordIndexStart(recIdx, newStart);
|
|
3145
|
+
bucket.partitionDirty = true;
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
else {
|
|
3149
|
+
const slotIdx = drawIdToSlotIdx[drawId];
|
|
3150
|
+
const slot = slotIdx !== undefined ? bucket.slots[slotIdx] : bucket.slots[0];
|
|
3151
|
+
const recIdx = slot?.slotToRecord[localSlot];
|
|
3152
|
+
if (slot !== undefined && recIdx !== undefined && recIdx >= 0) {
|
|
3153
|
+
slot.drawTableShadow[recIdx * RECORD_U32 + 2] = newStart >>> 0;
|
|
3154
|
+
const off = recIdx * RECORD_BYTES;
|
|
3155
|
+
if (off < slot.drawTableDirtyMin)
|
|
3156
|
+
slot.drawTableDirtyMin = off;
|
|
3157
|
+
if (off + RECORD_BYTES > slot.drawTableDirtyMax)
|
|
3158
|
+
slot.drawTableDirtyMax = off + RECORD_BYTES;
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
// (b) Uniform/attr drawHeader cells (+ Pos/Nor caches).
|
|
3164
|
+
const refs = bucket.localPerDrawRefs[localSlot];
|
|
3165
|
+
if (refs === undefined)
|
|
3166
|
+
continue;
|
|
3167
|
+
let changed = false;
|
|
3168
|
+
for (let i = 0; i < refs.length; i++) {
|
|
3169
|
+
const ref = refs[i];
|
|
3170
|
+
if (ref < 0)
|
|
3171
|
+
continue;
|
|
3172
|
+
const fld = bucket.layout.drawHeaderFields[i];
|
|
3173
|
+
// Inline header values (PickId/__layoutId) are NOT arena
|
|
3174
|
+
// refs — a numeric collision with an old offset must not
|
|
3175
|
+
// remap them.
|
|
3176
|
+
if (fld.name === "__layoutId" || INLINE_HEADER_UNIFORMS.has(fld.name))
|
|
3177
|
+
continue;
|
|
3178
|
+
const nn = remap.get(ref);
|
|
3179
|
+
if (nn !== undefined) {
|
|
3180
|
+
refs[i] = nn;
|
|
3181
|
+
changed = true;
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
if (!changed)
|
|
3185
|
+
continue;
|
|
3186
|
+
const pr = bucket.posFieldIdx >= 0 ? refs[bucket.posFieldIdx] : -1;
|
|
3187
|
+
const nr = bucket.norFieldIdx >= 0 ? refs[bucket.norFieldIdx] : -1;
|
|
3188
|
+
bucket.localPosRefs[localSlot] = pr < 0 ? undefined : pr;
|
|
3189
|
+
bucket.localNorRefs[localSlot] = nr < 0 ? undefined : nr;
|
|
3190
|
+
packBucketHeader(bucket, localSlot, refs, bucket.localLayoutIds[localSlot] ?? 0);
|
|
3191
|
+
if (bucket.isAtlasBucket) {
|
|
3192
|
+
const ts = bucket.localAtlasTextures[localSlot];
|
|
3193
|
+
if (ts !== undefined)
|
|
3194
|
+
packAtlasTextureFields(bucket, localSlot, ts);
|
|
3195
|
+
}
|
|
3196
|
+
const byteOff = localSlot * bucket.layout.drawHeaderBytes;
|
|
3197
|
+
if (byteOff < bucket.headerDirtyMin)
|
|
3198
|
+
bucket.headerDirtyMin = byteOff;
|
|
3199
|
+
const end = byteOff + bucket.layout.drawHeaderBytes;
|
|
3200
|
+
if (end > bucket.headerDirtyMax)
|
|
3201
|
+
bucket.headerDirtyMax = end;
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
// Register this scene as a consumer of the (possibly shared) store, so a
|
|
3207
|
+
// compaction — triggered by THIS scene or any other sharing the store —
|
|
3208
|
+
// re-seats this scene's refs. Disposed in `dispose()`.
|
|
3209
|
+
const storageReg = storage.register({ applyRemap });
|
|
3210
|
+
// ─── Compaction (delegates to the shared store) ──────────────────
|
|
3211
|
+
function runCompaction(force) {
|
|
3212
|
+
const compacted = storage.compact(device, compactionWasteFloor, force);
|
|
3213
|
+
stats.compactions += compacted;
|
|
3214
|
+
// update() flushes the re-seated drawHeaders via its own per-bucket loop
|
|
3215
|
+
// right after this returns; the forced (test) path runs outside update(),
|
|
3216
|
+
// so it flushes this scene's dirtied headers itself.
|
|
3217
|
+
if (force) {
|
|
3218
|
+
for (const bucket of buckets) {
|
|
3219
|
+
if (bucket.headerDirtyMax > bucket.headerDirtyMin) {
|
|
3220
|
+
device.queue.writeBuffer(bucket.drawHeap.buffer, bucket.headerDirtyMin, bucket.drawHeaderStaging.buffer, bucket.drawHeaderStaging.byteOffset + bucket.headerDirtyMin, bucket.headerDirtyMax - bucket.headerDirtyMin);
|
|
3221
|
+
bucket.headerDirtyMin = Infinity;
|
|
3222
|
+
bucket.headerDirtyMax = 0;
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
2801
3227
|
// ─── update / encodeIntoPass / frame / dispose ───────────────────
|
|
2802
3228
|
/**
|
|
2803
3229
|
* CPU-side data refresh: drain pending aset deltas, repack any
|
|
@@ -3032,7 +3458,16 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3032
3458
|
// initial population for 10K ROs this collapses ~30K calls into
|
|
3033
3459
|
// ~5 (arena, indices, plus one per bucket).
|
|
3034
3460
|
arena.attrs.flush(device);
|
|
3035
|
-
arena
|
|
3461
|
+
// ─── Waste-triggered attribute-arena compaction ─────────────────
|
|
3462
|
+
// Relocate live allocations to the front of each chunk once
|
|
3463
|
+
// fragmentation waste crosses the floor — exact-size freelist reuse
|
|
3464
|
+
// can't reclaim a drifting size distribution, so the arena would
|
|
3465
|
+
// otherwise ratchet to its high-water forever. Runs AFTER the arena
|
|
3466
|
+
// flush (GPU buffer current) and BEFORE the header flush (so the
|
|
3467
|
+
// re-seated drawHeaders upload below). Indices live in a separate
|
|
3468
|
+
// buffer and are not compacted here.
|
|
3469
|
+
if (enableCompaction)
|
|
3470
|
+
runCompaction(false);
|
|
3036
3471
|
for (const bucket of buckets) {
|
|
3037
3472
|
if (bucket.headerDirtyMax > bucket.headerDirtyMin) {
|
|
3038
3473
|
device.queue.writeBuffer(bucket.drawHeap.buffer, bucket.headerDirtyMin, bucket.drawHeaderStaging.buffer, bucket.drawHeaderStaging.byteOffset + bucket.headerDirtyMin, bucket.headerDirtyMax - bucket.headerDirtyMin);
|
|
@@ -3232,14 +3667,44 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3232
3667
|
destroyBucketResources(b);
|
|
3233
3668
|
}
|
|
3234
3669
|
function dispose() {
|
|
3235
|
-
|
|
3236
|
-
|
|
3670
|
+
// Stop receiving compaction remaps for the shared store.
|
|
3671
|
+
storageReg.dispose();
|
|
3672
|
+
// Shared storage: release this scene's allocations so the shared pool's
|
|
3673
|
+
// refcounts stay correct (another scene may still hold the same regions),
|
|
3674
|
+
// but don't destroy the buffers — the storage owner does that. Private
|
|
3675
|
+
// storage: the whole arena is torn down wholesale, so per-draw release is
|
|
3676
|
+
// unnecessary.
|
|
3677
|
+
if (!ownsStorage) {
|
|
3678
|
+
for (let drawId = 0; drawId < drawIdToBucket.length; drawId++) {
|
|
3679
|
+
if (drawIdToBucket[drawId] !== undefined)
|
|
3680
|
+
removeDraw(drawId);
|
|
3681
|
+
}
|
|
3682
|
+
}
|
|
3237
3683
|
for (const b of buckets)
|
|
3238
3684
|
destroyBucketResources(b);
|
|
3685
|
+
if (ownsStorage)
|
|
3686
|
+
storage.dispose();
|
|
3239
3687
|
}
|
|
3240
3688
|
// Test-only escape hatch for inspecting megacall bucket state. Not
|
|
3241
3689
|
// part of the public API surface — keep cast at use-site.
|
|
3242
3690
|
const _debug = {
|
|
3691
|
+
/** Force a full heap compaction pass (attribute arena + index buffer)
|
|
3692
|
+
* regardless of the waste heuristics (bypasses the floor + 50%-live
|
|
3693
|
+
* gate), re-seat all refs, flush. Returns residual waste afterwards in
|
|
3694
|
+
* bytes (attr bytes + index elements×4; 0 when fully packed). Test-only —
|
|
3695
|
+
* drives relocation deterministically without engineering megabytes of
|
|
3696
|
+
* fragmentation. Callers should `update()` afterward to flush drawTables /
|
|
3697
|
+
* re-dispatch the partition (the render path does this). */
|
|
3698
|
+
forceCompact() {
|
|
3699
|
+
runCompaction(true);
|
|
3700
|
+
return arena.attrs.totalWasteBytes();
|
|
3701
|
+
},
|
|
3702
|
+
/** Current arena fragmentation waste (bytes) — covers uniforms,
|
|
3703
|
+
* attributes, and index regions (all one arena now). */
|
|
3704
|
+
attrWasteBytes() { return arena.attrs.totalWasteBytes(); },
|
|
3705
|
+
/** Number of arena chunks (pages) currently open. >1 means group
|
|
3706
|
+
* placement rolled past one buffer cap. */
|
|
3707
|
+
pageCount() { return arena.attrs.chunkCount; },
|
|
3243
3708
|
bucketsForTest() {
|
|
3244
3709
|
return buckets.map(b => ({
|
|
3245
3710
|
indirectBuf: b.slots[0].indirectBuf,
|
|
@@ -3279,12 +3744,12 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3279
3744
|
// multi-chunk-aware validator needs per-bucket-chunkIdx
|
|
3280
3745
|
// staging which is a bigger refactor; assert for now so
|
|
3281
3746
|
// multi-chunk callers don't get false-passes.
|
|
3282
|
-
if (arena.attrs.chunkCount > 1
|
|
3747
|
+
if (arena.attrs.chunkCount > 1) {
|
|
3283
3748
|
throw new Error("heapScene/validateHeap: multi-chunk arenas not yet supported by this validator (chunk-0-only). " +
|
|
3284
3749
|
"Run with the default chunk cap (no second chunk opened) for now.");
|
|
3285
3750
|
}
|
|
3286
3751
|
const arenaSize = arena.attrs.chunk(0).buffer.size;
|
|
3287
|
-
const indicesSize = arena
|
|
3752
|
+
const indicesSize = arenaSize; // indices live in the attr arena now
|
|
3288
3753
|
const enc = device.createCommandEncoder({ label: "validateHeap" });
|
|
3289
3754
|
const stage = (src, size) => {
|
|
3290
3755
|
const c = device.createBuffer({
|
|
@@ -3295,7 +3760,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3295
3760
|
return c;
|
|
3296
3761
|
};
|
|
3297
3762
|
const arenaCopy = stage(arena.attrs.chunk(0).buffer, arenaSize);
|
|
3298
|
-
const indicesCopy =
|
|
3763
|
+
const indicesCopy = arenaCopy; // same buffer; _indexStart is an element offset into it
|
|
3299
3764
|
const TILE_K_LOCAL = 64;
|
|
3300
3765
|
const dcs = [];
|
|
3301
3766
|
for (const b of buckets) {
|
|
@@ -3348,7 +3813,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3348
3813
|
for (let slot = 0; slot < recordCount; slot++) {
|
|
3349
3814
|
const slotOff = slot * stride;
|
|
3350
3815
|
for (const f of dc.bucket.layout.drawHeaderFields) {
|
|
3351
|
-
if (f.name === "__layoutId")
|
|
3816
|
+
if (f.name === "__layoutId" || INLINE_HEADER_UNIFORMS.has(f.name))
|
|
3352
3817
|
continue;
|
|
3353
3818
|
if (f.kind !== "uniform-ref" && f.kind !== "attribute-ref")
|
|
3354
3819
|
continue;
|
|
@@ -3384,18 +3849,9 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3384
3849
|
? `prev=[${arenaU32[refU32 - 4]},${arenaU32[refU32 - 3]},${arenaU32[refU32 - 2]},${arenaU32[refU32 - 1]}] `
|
|
3385
3850
|
: "";
|
|
3386
3851
|
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}`);
|
|
3852
|
+
// (Mirror-less arena: no CPU shadow to cross-check against —
|
|
3853
|
+
// the GPU read-back is the only copy.)
|
|
3854
|
+
push(`bucket#${bucketIdx} slot=${slot} field='${f.name}' alloc@0x${ref.toString(16)} typeId=${typeId} unknown ${before}${here}`);
|
|
3399
3855
|
attrAllocsBad++;
|
|
3400
3856
|
continue;
|
|
3401
3857
|
}
|
|
@@ -3598,11 +4054,11 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3598
4054
|
const push = (s) => { if (issues.length < 30)
|
|
3599
4055
|
issues.push(s); };
|
|
3600
4056
|
// §3 v1: chunk-0-only like validateHeap.
|
|
3601
|
-
if (arena.attrs.chunkCount > 1
|
|
4057
|
+
if (arena.attrs.chunkCount > 1) {
|
|
3602
4058
|
throw new Error("heapScene/simulateDraws: multi-chunk arenas not yet supported (chunk-0-only).");
|
|
3603
4059
|
}
|
|
3604
4060
|
const arenaSize = arena.attrs.chunk(0).buffer.size;
|
|
3605
|
-
const indicesSize = arena
|
|
4061
|
+
const indicesSize = arenaSize; // indices live in the attr arena now
|
|
3606
4062
|
// Stage all buffers we'll need.
|
|
3607
4063
|
const enc = device.createCommandEncoder({ label: "simulateDraws" });
|
|
3608
4064
|
const stage = (src, size) => {
|
|
@@ -3613,7 +4069,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3613
4069
|
return c;
|
|
3614
4070
|
};
|
|
3615
4071
|
const arenaCopy = stage(arena.attrs.chunk(0).buffer, arenaSize);
|
|
3616
|
-
const indicesCopy =
|
|
4072
|
+
const indicesCopy = arenaCopy; // same buffer; _indexStart is an element offset into it
|
|
3617
4073
|
const dcs = [];
|
|
3618
4074
|
for (const b of buckets) {
|
|
3619
4075
|
if (b.slots[0].recordCount === 0 || b.slots[0].totalEmitEstimate === 0)
|
|
@@ -3654,7 +4110,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3654
4110
|
for (const dc of dcs) {
|
|
3655
4111
|
const fs = [];
|
|
3656
4112
|
for (const f of dc.bucket.layout.drawHeaderFields) {
|
|
3657
|
-
if (f.name === "__layoutId")
|
|
4113
|
+
if (f.name === "__layoutId" || INLINE_HEADER_UNIFORMS.has(f.name))
|
|
3658
4114
|
continue;
|
|
3659
4115
|
if (f.kind !== "uniform-ref" && f.kind !== "attribute-ref")
|
|
3660
4116
|
continue;
|
|
@@ -3865,11 +4321,11 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3865
4321
|
// cross-RO triangle symptom.
|
|
3866
4322
|
const allocClaims = [];
|
|
3867
4323
|
// §3 v1: chunk-0-only (see validateHeap).
|
|
3868
|
-
if (arena.attrs.chunkCount > 1
|
|
4324
|
+
if (arena.attrs.chunkCount > 1) {
|
|
3869
4325
|
throw new Error("heapScene/checkTriangleCoherence: multi-chunk arenas not yet supported (chunk-0-only).");
|
|
3870
4326
|
}
|
|
3871
4327
|
const arenaBuf0 = arena.attrs.chunk(0).buffer;
|
|
3872
|
-
const indicesBuf0 = arena.
|
|
4328
|
+
const indicesBuf0 = arena.attrs.chunk(0).buffer;
|
|
3873
4329
|
// Stage arena + arena.indices once (shared across buckets).
|
|
3874
4330
|
const arenaCopy = device.createBuffer({
|
|
3875
4331
|
size: arenaBuf0.size,
|
|
@@ -4322,7 +4778,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
4322
4778
|
{ binding: 0, resource: { buffer: target.slots[0].drawTableBuf.buffer } },
|
|
4323
4779
|
{ binding: 1, resource: { buffer: target.slots[0].firstDrawInTileBuf.buffer } },
|
|
4324
4780
|
{ binding: 2, resource: { buffer: sampleBuf } },
|
|
4325
|
-
{ binding: 3, resource: { buffer: arena.
|
|
4781
|
+
{ binding: 3, resource: { buffer: arena.attrs.chunk(target.chunkIdx).buffer } },
|
|
4326
4782
|
{ binding: 4, resource: { buffer: outBuf } },
|
|
4327
4783
|
{ binding: 5, resource: { buffer: paramBuf } },
|
|
4328
4784
|
],
|
|
@@ -4346,7 +4802,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
4346
4802
|
outBuf.destroy();
|
|
4347
4803
|
paramBuf.destroy();
|
|
4348
4804
|
// Read drawTable + indices for CPU expectation.
|
|
4349
|
-
const idxBuf = arena.
|
|
4805
|
+
const idxBuf = arena.attrs.chunk(target.chunkIdx).buffer;
|
|
4350
4806
|
const indicesCopy2 = device.createBuffer({
|
|
4351
4807
|
size: idxBuf.size,
|
|
4352
4808
|
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
|