@aardworx/wombat.rendering 0.19.24 → 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/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/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
package/src/runtime/heapScene.ts
CHANGED
|
@@ -51,7 +51,7 @@ import type { Effect, CompileOptions } from "@aardworx/wombat.shader";
|
|
|
51
51
|
import type { PipelineState } from "../core/pipelineState.js";
|
|
52
52
|
import type { BufferView } from "../core/bufferView.js";
|
|
53
53
|
import type { IBuffer, HostBufferSource } from "../core/buffer.js";
|
|
54
|
-
import {
|
|
54
|
+
import { INLINE_HEADER_UNIFORMS,
|
|
55
55
|
buildBucketLayout, compileHeapEffect,
|
|
56
56
|
type BucketLayout, type FragmentOutputLayout,
|
|
57
57
|
type HeapEffectSchema,
|
|
@@ -85,11 +85,11 @@ import {
|
|
|
85
85
|
} from "./heapScene/growBuffer.js";
|
|
86
86
|
import {
|
|
87
87
|
UniformPool, IndexPool, DrawHeap,
|
|
88
|
-
AttributeArena, IndexAllocator,
|
|
89
88
|
buildArenaState, arenaBytes, writeAttribute,
|
|
90
89
|
asAval, isBufferView, asFloat32,
|
|
91
90
|
ALLOC_HEADER_BYTES, ALLOC_HEADER_PAD_TO,
|
|
92
|
-
ENC_V3F_TIGHT, SEM_POSITIONS, SEM_NORMALS,
|
|
91
|
+
ENC_V3F_TIGHT, ENC_OCT32, ENC_C4B, SEM_POSITIONS, SEM_NORMALS,
|
|
92
|
+
COMPACTION_WASTE_FLOOR_BYTES,
|
|
93
93
|
type ArenaState,
|
|
94
94
|
} from "./heapScene/pools.js";
|
|
95
95
|
import { encodeModeKey, type PipelineStateDescriptor } from "./pipelineCache/index.js";
|
|
@@ -110,6 +110,49 @@ import {
|
|
|
110
110
|
type Expr, type Stmt,
|
|
111
111
|
} from "@aardworx/wombat.shader/ir";
|
|
112
112
|
|
|
113
|
+
const NOOP = (): void => {};
|
|
114
|
+
|
|
115
|
+
// ─── Shared pool packers ──────────────────────────────────────────────
|
|
116
|
+
// Static functions where possible: a fresh closure per pool entry was
|
|
117
|
+
// measurable ballast at heap scale (3 closures/leaf). Only the
|
|
118
|
+
// broadcast prefix variant needs a capture (the copy limit differs
|
|
119
|
+
// from the source buffer's size).
|
|
120
|
+
type PoolPackFn = (val: unknown, dst: Float32Array, off: number) => void;
|
|
121
|
+
const packPackedAttr: PoolPackFn = (val, dst, off) => {
|
|
122
|
+
const src = (val as HeapPackedAttribute).data;
|
|
123
|
+
new Uint32Array(dst.buffer, dst.byteOffset + off * 4, src.length).set(src);
|
|
124
|
+
};
|
|
125
|
+
const packF32Verbatim: PoolPackFn = (val, dst, off) => {
|
|
126
|
+
dst.set(val as Float32Array, off);
|
|
127
|
+
};
|
|
128
|
+
const packHostU32Bits: PoolPackFn = (val, dst, off) => {
|
|
129
|
+
const ibuf = val as IBuffer;
|
|
130
|
+
if (ibuf.kind !== "host") {
|
|
131
|
+
throw new Error("heapScene: packed BufferView aval flipped to native GPUBuffer");
|
|
132
|
+
}
|
|
133
|
+
const d = ibuf.data as ArrayBufferView;
|
|
134
|
+
// reinterpret the u32 payload as f32 bits for the arena write
|
|
135
|
+
dst.set(new Float32Array(d.buffer, d.byteOffset, d.byteLength >>> 2), off);
|
|
136
|
+
};
|
|
137
|
+
const packHostF32: PoolPackFn = (val, dst, off) => {
|
|
138
|
+
const ibuf = val as IBuffer;
|
|
139
|
+
if (ibuf.kind !== "host") {
|
|
140
|
+
throw new Error("heapScene: BufferView aval flipped to native GPUBuffer; not supported in heap path");
|
|
141
|
+
}
|
|
142
|
+
dst.set(asFloat32(ibuf.data), off);
|
|
143
|
+
};
|
|
144
|
+
/** Broadcast prefix copy — the only variant that genuinely captures. */
|
|
145
|
+
function packHostPrefix(limitFloats: number): PoolPackFn {
|
|
146
|
+
return (val, dst, off) => {
|
|
147
|
+
const ibuf = val as IBuffer;
|
|
148
|
+
if (ibuf.kind !== "host") {
|
|
149
|
+
throw new Error("heapScene: BufferView aval flipped to native GPUBuffer; not supported in heap path");
|
|
150
|
+
}
|
|
151
|
+
const src = asFloat32(ibuf.data);
|
|
152
|
+
dst.set(src.subarray(0, limitFloats), off);
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
113
156
|
// GrowBuffer + ALIGN16 + MIN_BUFFER_BYTES + POW2 live in ./heapScene/growBuffer.
|
|
114
157
|
// Packers (WGSL-type → JS-value → arena bytes) live in ./heapScene/packers.
|
|
115
158
|
|
|
@@ -148,6 +191,41 @@ import {
|
|
|
148
191
|
* Must match the literal in heapEffect.ts's `megacallSearchPrelude`. */
|
|
149
192
|
export const HEAP_NONINDEXED = 0xffffffff;
|
|
150
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Packed attribute source — stored in the arena at 4 B/element (one u32)
|
|
196
|
+
* instead of the 12 B tight-f32 expansion, decoded by the VS's
|
|
197
|
+
* typeId-selected arm. Use for memory-bound scenes (phones): both the
|
|
198
|
+
* host-side aval payload AND the GPU arena shrink 3×.
|
|
199
|
+
*
|
|
200
|
+
* - `oct32(...)` — unit vectors (normals): 2×unorm16 octahedral, for
|
|
201
|
+
* `vec3<f32>` fields. Matches build_vienna.py's `oct_pack`.
|
|
202
|
+
* - `c4b(...)` — RGBA8-unorm colors, for `vec4<f32>` fields
|
|
203
|
+
* (`unpack4x8unorm` in the shader; alpha rides along).
|
|
204
|
+
*/
|
|
205
|
+
export interface HeapPackedAttribute {
|
|
206
|
+
readonly kind: "heap-packed";
|
|
207
|
+
readonly enc: "oct32" | "c4b";
|
|
208
|
+
readonly data: Uint32Array;
|
|
209
|
+
}
|
|
210
|
+
export function oct32(data: Int32Array | Uint32Array): HeapPackedAttribute {
|
|
211
|
+
const u = data instanceof Uint32Array ? data : new Uint32Array(data.buffer, data.byteOffset, data.length);
|
|
212
|
+
return { kind: "heap-packed", enc: "oct32", data: u };
|
|
213
|
+
}
|
|
214
|
+
export function c4b(data: Uint8Array | Uint32Array): HeapPackedAttribute {
|
|
215
|
+
if (data instanceof Uint8Array) {
|
|
216
|
+
if (data.byteLength % 4 !== 0 || data.byteOffset % 4 !== 0) {
|
|
217
|
+
throw new Error("heapScene.c4b: RGBA8 data must be 4-byte sized and aligned");
|
|
218
|
+
}
|
|
219
|
+
return { kind: "heap-packed", enc: "c4b", data: new Uint32Array(data.buffer, data.byteOffset, data.byteLength / 4) };
|
|
220
|
+
}
|
|
221
|
+
return { kind: "heap-packed", enc: "c4b", data };
|
|
222
|
+
}
|
|
223
|
+
export function isHeapPackedAttribute(v: unknown): v is HeapPackedAttribute {
|
|
224
|
+
return typeof v === "object" && v !== null
|
|
225
|
+
&& (v as { kind?: unknown }).kind === "heap-packed"
|
|
226
|
+
&& (v as { data?: unknown }).data instanceof Uint32Array;
|
|
227
|
+
}
|
|
228
|
+
|
|
151
229
|
export interface HeapGeometry {
|
|
152
230
|
readonly positions: Float32Array;
|
|
153
231
|
readonly normals: Float32Array;
|
|
@@ -299,13 +377,24 @@ interface Bucket {
|
|
|
299
377
|
* keyed by aval identity.
|
|
300
378
|
*/
|
|
301
379
|
readonly localPerDrawAvals: (aval<unknown>[] | undefined)[];
|
|
380
|
+
/** Field indices (into `layout.drawHeaderFields`) whose drawHeader
|
|
381
|
+
* cells hold RAW arena refs (§7 output slots, no pool entry). */
|
|
382
|
+
readonly localPerDrawRawFieldIdx: (number[] | undefined)[];
|
|
302
383
|
/**
|
|
303
|
-
* Per-draw uniform refs
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
384
|
+
* Per-draw uniform refs for this slot, indexed by the bucket
|
|
385
|
+
* layout's drawHeaderFields ORDER (−1 = field not populated by
|
|
386
|
+
* this slot's effect). A flat Int32Array instead of a
|
|
387
|
+
* Map<string, number> — at heap scale (hundreds of thousands of
|
|
388
|
+
* draws) the per-draw Map was one of the largest JS-memory items.
|
|
389
|
+
* Stable during the slot's lifetime; used to re-pack the
|
|
390
|
+
* DrawHeader after the drawHeap GrowBuffer reallocates.
|
|
307
391
|
*/
|
|
308
|
-
readonly localPerDrawRefs: (
|
|
392
|
+
readonly localPerDrawRefs: (Int32Array | undefined)[];
|
|
393
|
+
/** drawHeaderFields name → array index (built once per bucket). */
|
|
394
|
+
readonly fieldIdx: Map<string, number>;
|
|
395
|
+
/** fieldIdx.get("Positions") / ("Normals"), −1 when absent. */
|
|
396
|
+
readonly posFieldIdx: number;
|
|
397
|
+
readonly norFieldIdx: number;
|
|
309
398
|
/**
|
|
310
399
|
* Per-local-slot layoutId for the §6 family-merge selector. Stable
|
|
311
400
|
* during the slot's lifetime; written into the drawHeader's
|
|
@@ -412,6 +501,12 @@ export interface HeapDrawSpec {
|
|
|
412
501
|
* share a pipeline + bucket.
|
|
413
502
|
*/
|
|
414
503
|
readonly effect: Effect;
|
|
504
|
+
/**
|
|
505
|
+
* Optional picking id (u32 < 2^24), written INLINE into the
|
|
506
|
+
* drawHeader's `PickId` field (see `INLINE_HEADER_UNIFORMS`) — no
|
|
507
|
+
* pool entry, no per-draw aval.
|
|
508
|
+
*/
|
|
509
|
+
readonly pickId?: number;
|
|
415
510
|
/**
|
|
416
511
|
* Per-name inputs covering both vertex attributes (e.g. `Positions`,
|
|
417
512
|
* `Normals`) and uniforms (e.g. `ModelTrafo`, `Color`, `ViewProjTrafo`)
|
|
@@ -552,6 +647,10 @@ export interface HeapSceneStats {
|
|
|
552
647
|
totalDraws: number;
|
|
553
648
|
drawBytes: number;
|
|
554
649
|
geometryBytes: number;
|
|
650
|
+
/** Cumulative count of waste-triggered arena compactions performed
|
|
651
|
+
* (one per chunk per `update` that compacted). Stays 0 when the
|
|
652
|
+
* arena never ratchets past the waste floor. */
|
|
653
|
+
compactions: number;
|
|
555
654
|
/** §7 derived-uniforms per-frame breakdown (last frame). */
|
|
556
655
|
derivedPullMs: number;
|
|
557
656
|
derivedUploadMs: number;
|
|
@@ -752,6 +851,127 @@ export interface BuildHeapSceneOptions {
|
|
|
752
851
|
* chunks duplicate their pool entries (accepted trade-off).
|
|
753
852
|
*/
|
|
754
853
|
readonly maxChunkBytes?: number;
|
|
854
|
+
/**
|
|
855
|
+
* Waste-triggered attribute-arena compaction. When enabled (default),
|
|
856
|
+
* `update()` relocates live arena allocations to the front of each chunk
|
|
857
|
+
* once fragmentation waste crosses `compactionWasteFloorBytes` and at least
|
|
858
|
+
* half the extent is wasted — preventing the high-water ratchet that
|
|
859
|
+
* exact-size freelist reuse can't fix under a drifting allocation-size
|
|
860
|
+
* distribution. Pass `false` to disable.
|
|
861
|
+
*/
|
|
862
|
+
readonly enableCompaction?: boolean;
|
|
863
|
+
/**
|
|
864
|
+
* Override the fragmentation-waste floor (bytes) below which compaction is
|
|
865
|
+
* skipped. Default `COMPACTION_WASTE_FLOOR_BYTES` (4 MiB). Tests pass a
|
|
866
|
+
* small value to exercise the path without realistic memory pressure.
|
|
867
|
+
*/
|
|
868
|
+
readonly compactionWasteFloorBytes?: number;
|
|
869
|
+
/**
|
|
870
|
+
* Pre-size the arena chunks (bytes). When the caller can estimate the
|
|
871
|
+
* scene's total arena footprint up front (e.g. from an asset manifest),
|
|
872
|
+
* passing it here allocates each chunk at its final size immediately —
|
|
873
|
+
* eliminating the pow2-grow copies during ingest, whose old+new-buffer
|
|
874
|
+
* transient is what OOM-kills memory-tight targets (phones). Clamped to
|
|
875
|
+
* the per-chunk cap; harmless to overshoot.
|
|
876
|
+
*/
|
|
877
|
+
readonly initialArenaBytes?: number;
|
|
878
|
+
/**
|
|
879
|
+
* Release constant attribute payloads after staging (aardvark's
|
|
880
|
+
* "StageOnce"). When a `HeapDrawSpec` attribute is supplied as a PLAIN
|
|
881
|
+
* value (raw typed array / packed marker — not a real aval), the heap
|
|
882
|
+
* wraps it in a constant holder; a constant is provably never re-read
|
|
883
|
+
* after its arena write (repack fires only on `inputChanged`, which
|
|
884
|
+
* constants never raise; compaction and growth move bytes GPU-side).
|
|
885
|
+
* With this flag the holder drops its payload right after staging, so
|
|
886
|
+
* the GPU arena becomes the ONLY copy of constant geometry — halving a
|
|
887
|
+
* static scene's memory. Real avals (cvals etc.) are never touched:
|
|
888
|
+
* changeable data stays alive by definition. Default false.
|
|
889
|
+
*/
|
|
890
|
+
readonly releaseConstantAttributes?: boolean;
|
|
891
|
+
/**
|
|
892
|
+
* Share a `HeapStorage` (arena + dedup pools) across multiple heap scenes.
|
|
893
|
+
* When omitted, the scene creates a private store it owns and disposes.
|
|
894
|
+
* When supplied (via `createHeapStorage`), two or more scenes — e.g. the
|
|
895
|
+
* same geometry rendered by a shadow pass and a color pass with different
|
|
896
|
+
* effects — share one deduped/refcounted store, so common geometry and
|
|
897
|
+
* uniforms are stored ONCE. The caller owns the store's lifetime.
|
|
898
|
+
*/
|
|
899
|
+
readonly storage?: HeapStorage;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* The data-placement layer behind a heap scene: the chunked arena (uniform /
|
|
904
|
+
* attribute data + the index buffer) plus the aval-keyed dedup pools. Owns
|
|
905
|
+
* the GPU buffers; one or more `buildHeapScene` calls draw against it. Create
|
|
906
|
+
* with `createHeapStorage` and pass via `BuildHeapSceneOptions.storage` to
|
|
907
|
+
* share geometry/uniforms across heaps (shadow + color pass, picking, …).
|
|
908
|
+
*/
|
|
909
|
+
/** A heap scene's hook into a shared store. When a compaction relocates arena
|
|
910
|
+
* regions, the store calls every registered consumer's `applyRemap` so each
|
|
911
|
+
* scene re-seats its OWN refs (drawHeader cells, derived records, partition
|
|
912
|
+
* master, index `_indexStart`). `remap` maps OLD→NEW byte header offset. */
|
|
913
|
+
export interface HeapStorageConsumer {
|
|
914
|
+
applyRemap(chunkIdx: number, remap: ReadonlyMap<number, number>): void;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
export interface HeapStorage {
|
|
918
|
+
readonly arena: ArenaState;
|
|
919
|
+
readonly pool: UniformPool;
|
|
920
|
+
readonly indexPool: IndexPool;
|
|
921
|
+
/** Register a scene as a consumer (so compaction re-seats its refs). */
|
|
922
|
+
register(consumer: HeapStorageConsumer): IDisposable;
|
|
923
|
+
/**
|
|
924
|
+
* Waste-triggered compaction of the shared arena. Relocates live regions,
|
|
925
|
+
* re-seats the storage-level dedup pools, and notifies every registered
|
|
926
|
+
* consumer to re-seat its scene-level refs. Returns the number of chunks
|
|
927
|
+
* compacted. Must run with pending writes flushable (it flushes first).
|
|
928
|
+
*/
|
|
929
|
+
compact(device: GPUDevice, wasteFloorBytes: number, force: boolean): number;
|
|
930
|
+
/** Destroy the arena's GPU buffers. Only call once no scene references it. */
|
|
931
|
+
dispose(): void;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
export interface HeapStorageOptions {
|
|
935
|
+
/** Per-arena-chunk size cap in bytes. See `BuildHeapSceneOptions.maxChunkBytes`. */
|
|
936
|
+
readonly maxChunkBytes?: number;
|
|
937
|
+
/** Pre-size chunks (bytes) — see `BuildHeapSceneOptions.initialArenaBytes`. */
|
|
938
|
+
readonly initialArenaBytes?: number;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/** Create a standalone `HeapStorage`. Pass it to `buildHeapScene` via
|
|
942
|
+
* `opts.storage` to share one arena across multiple heaps. */
|
|
943
|
+
export function createHeapStorage(device: GPUDevice, opts: HeapStorageOptions = {}): HeapStorage {
|
|
944
|
+
void device;
|
|
945
|
+
// Initial capacities are hints; the buffers pow2-grow on demand.
|
|
946
|
+
const arena = buildArenaState(
|
|
947
|
+
device, Math.max(64 * 1024, opts.initialArenaBytes ?? 0), 16 * 1024, "heapStorage",
|
|
948
|
+
GPUBufferUsage.STORAGE, opts.maxChunkBytes,
|
|
949
|
+
);
|
|
950
|
+
const pool = new UniformPool();
|
|
951
|
+
const indexPool = new IndexPool();
|
|
952
|
+
const consumers = new Set<HeapStorageConsumer>();
|
|
953
|
+
return {
|
|
954
|
+
arena, pool, indexPool,
|
|
955
|
+
register(c: HeapStorageConsumer): IDisposable {
|
|
956
|
+
consumers.add(c);
|
|
957
|
+
return { dispose: () => { consumers.delete(c); } };
|
|
958
|
+
},
|
|
959
|
+
compact(dev: GPUDevice, floor: number, force: boolean): number {
|
|
960
|
+
arena.attrs.flush(dev); // GPU buffer current for the copy
|
|
961
|
+
const remaps = arena.attrs.compact(dev, floor, force);
|
|
962
|
+
for (const { chunkIdx, remap } of remaps) {
|
|
963
|
+
// Storage-level dedup pools first (uniform refs); then each scene
|
|
964
|
+
// re-seats its drawHeaders / derive / partition / index `_indexStart`
|
|
965
|
+
// (the index re-seat reads `indexPool.baseFor` at the OLD ref, so the
|
|
966
|
+
// index pool is remapped LAST, after all consumers).
|
|
967
|
+
pool.remapRefs(chunkIdx, remap);
|
|
968
|
+
for (const c of consumers) c.applyRemap(chunkIdx, remap);
|
|
969
|
+
indexPool.remapRefs(chunkIdx, remap);
|
|
970
|
+
}
|
|
971
|
+
return remaps.length;
|
|
972
|
+
},
|
|
973
|
+
dispose(): void { arena.attrs.destroy(); },
|
|
974
|
+
};
|
|
755
975
|
}
|
|
756
976
|
|
|
757
977
|
export function buildHeapScene(
|
|
@@ -781,16 +1001,21 @@ export function buildHeapScene(
|
|
|
781
1001
|
}
|
|
782
1002
|
const depthFormat = sig.depthStencil?.format;
|
|
783
1003
|
|
|
784
|
-
// ───
|
|
785
|
-
//
|
|
786
|
-
//
|
|
787
|
-
//
|
|
788
|
-
//
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
1004
|
+
// ─── Storage (arena + dedup pools) — private or shared ───────────
|
|
1005
|
+
// The chunked arena (uniform/attribute data + index buffer) and the
|
|
1006
|
+
// aval-keyed dedup pools live in a `HeapStorage`. Default: a private
|
|
1007
|
+
// store this scene owns. `opts.storage`: a shared store (the same
|
|
1008
|
+
// geometry/uniforms rendered by multiple heaps — e.g. shadow + color
|
|
1009
|
+
// pass — are deduped once). The local `arena`/`pool`/`indexPool`
|
|
1010
|
+
// bindings below keep every downstream reference unchanged.
|
|
1011
|
+
const ownsStorage = opts.storage === undefined;
|
|
1012
|
+
const storage = opts.storage ?? createHeapStorage(device, {
|
|
1013
|
+
...(opts.maxChunkBytes !== undefined ? { maxChunkBytes: opts.maxChunkBytes } : {}),
|
|
1014
|
+
...(opts.initialArenaBytes !== undefined ? { initialArenaBytes: opts.initialArenaBytes } : {}),
|
|
1015
|
+
});
|
|
1016
|
+
const arena = storage.arena;
|
|
1017
|
+
const pool = storage.pool;
|
|
1018
|
+
const indexPool = storage.indexPool;
|
|
794
1019
|
|
|
795
1020
|
// ─── Per-draw global bookkeeping (sparse, indexed by drawId) ──────
|
|
796
1021
|
const drawIdToBucket: (Bucket | undefined)[] = [];
|
|
@@ -959,6 +1184,25 @@ export function buildHeapScene(
|
|
|
959
1184
|
const p = packerForWgslType(f.uniformWgslType ?? "");
|
|
960
1185
|
return { dataBytes: p.dataBytes, typeId: p.typeId, length: 1, pack: p.pack };
|
|
961
1186
|
}
|
|
1187
|
+
// Packed attribute encodings (oct32 unit vectors / C4b colors): one
|
|
1188
|
+
// u32 per element, decoded by the VS's typeId-selected arm. 3× smaller
|
|
1189
|
+
// in the arena AND host-side (the caller keeps the packed source).
|
|
1190
|
+
if (isHeapPackedAttribute(value)) {
|
|
1191
|
+
const pa = value;
|
|
1192
|
+
const wantType = pa.enc === "oct32" ? "vec3<f32>" : "vec4<f32>";
|
|
1193
|
+
if (f.attributeWgslType !== wantType) {
|
|
1194
|
+
throw new Error(
|
|
1195
|
+
`heapScene: ${pa.enc} attribute supplied for '${f.attributeWgslType}' field — ` +
|
|
1196
|
+
`oct32 requires vec3<f32>, c4b requires vec4<f32>`,
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
return {
|
|
1200
|
+
dataBytes: pa.data.byteLength,
|
|
1201
|
+
typeId: pa.enc === "oct32" ? ENC_OCT32 : ENC_C4B,
|
|
1202
|
+
length: pa.data.length,
|
|
1203
|
+
pack: packPackedAttr,
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
962
1206
|
// attribute-ref: variable-size array; we copy verbatim into the
|
|
963
1207
|
// arena. The current encoding is V3F_TIGHT (3 f32s per element).
|
|
964
1208
|
const arr = value as Float32Array;
|
|
@@ -972,7 +1216,7 @@ export function buildHeapScene(
|
|
|
972
1216
|
dataBytes: arr.byteLength,
|
|
973
1217
|
typeId: ENC_V3F_TIGHT,
|
|
974
1218
|
length,
|
|
975
|
-
pack:
|
|
1219
|
+
pack: packF32Verbatim,
|
|
976
1220
|
};
|
|
977
1221
|
}
|
|
978
1222
|
|
|
@@ -997,6 +1241,39 @@ export function buildHeapScene(
|
|
|
997
1241
|
throw new Error(`heapScene: BufferView offset ${offset} > 0 not yet supported`);
|
|
998
1242
|
}
|
|
999
1243
|
|
|
1244
|
+
// Packed encodings by ELEMENT TYPE (the BufferView analogue of the
|
|
1245
|
+
// oct32()/c4b() HeapDrawSpec markers): one u32 per element in the
|
|
1246
|
+
// arena, decoded by the VS typeId arm. Dedup stays aval-keyed like
|
|
1247
|
+
// any other BufferView attribute.
|
|
1248
|
+
const etName = bv.elementType.name;
|
|
1249
|
+
if (etName === "oct32" || etName === "c4b") {
|
|
1250
|
+
const want = etName === "oct32" ? "vec3<f32>" : "vec4<f32>";
|
|
1251
|
+
if (f.attributeWgslType !== want) {
|
|
1252
|
+
throw new Error(
|
|
1253
|
+
`heapScene: ${etName} BufferView requires a ${want} schema attribute (got ${f.attributeWgslType ?? "?"})`,
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
const strideP = bv.stride ?? 4;
|
|
1257
|
+
const isBroadcastP = bv.singleValue !== undefined || strideP === 0;
|
|
1258
|
+
if (!isBroadcastP && strideP !== 4) {
|
|
1259
|
+
throw new Error(`heapScene: ${etName} BufferView stride ${strideP} not tight (4) and not a broadcast`);
|
|
1260
|
+
}
|
|
1261
|
+
const ibP = bv.buffer.force(/* allow-force */);
|
|
1262
|
+
if (ibP.kind !== "host") {
|
|
1263
|
+
throw new Error(`heapScene: ${etName} BufferView wraps a native GPUBuffer; not supported in heap path`);
|
|
1264
|
+
}
|
|
1265
|
+
const lengthP = isBroadcastP ? 1 : ibP.sizeBytes / 4;
|
|
1266
|
+
const dataBytesP = 4 * lengthP;
|
|
1267
|
+
return {
|
|
1268
|
+
dataBytes: dataBytesP,
|
|
1269
|
+
typeId: etName === "oct32" ? ENC_OCT32 : ENC_C4B,
|
|
1270
|
+
length: lengthP,
|
|
1271
|
+
// Tight case: the copy limit equals the source payload, so the
|
|
1272
|
+
// static packer (which derives it from the buffer) is exact.
|
|
1273
|
+
pack: isBroadcastP ? packHostPrefix(dataBytesP / 4) : packHostU32Bits,
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1000
1277
|
// In-arena element bytes: the storage layout the shader will read
|
|
1001
1278
|
// from. vec4 is always 16 (read via `heapV4f[idx]`), vec3 is 12
|
|
1002
1279
|
// (read as 3 f32s), vec2 is 8, scalar is 4. V3f source for a vec4
|
|
@@ -1050,15 +1327,7 @@ export function buildHeapScene(
|
|
|
1050
1327
|
dataBytes,
|
|
1051
1328
|
typeId: ENC_V3F_TIGHT,
|
|
1052
1329
|
length,
|
|
1053
|
-
pack: (
|
|
1054
|
-
const ibuf = val as IBuffer;
|
|
1055
|
-
if (ibuf.kind !== "host") {
|
|
1056
|
-
throw new Error("heapScene: BufferView aval flipped to native GPUBuffer; not supported in heap path");
|
|
1057
|
-
}
|
|
1058
|
-
const src = asFloat32(ibuf.data);
|
|
1059
|
-
const limitFloats = dataBytes / 4;
|
|
1060
|
-
dst.set(src.subarray(0, limitFloats), off);
|
|
1061
|
-
},
|
|
1330
|
+
pack: isBroadcast ? packHostPrefix(dataBytes / 4) : packHostF32,
|
|
1062
1331
|
};
|
|
1063
1332
|
}
|
|
1064
1333
|
|
|
@@ -1091,9 +1360,7 @@ export function buildHeapScene(
|
|
|
1091
1360
|
};
|
|
1092
1361
|
}
|
|
1093
1362
|
|
|
1094
|
-
//
|
|
1095
|
-
const pool = new UniformPool();
|
|
1096
|
-
const indexPool = new IndexPool();
|
|
1363
|
+
// (`pool` / `indexPool` are owned by `storage`, bound above.)
|
|
1097
1364
|
|
|
1098
1365
|
// ─── Adaptive routing: aval marks → repack the pool entry ─────────
|
|
1099
1366
|
// With pool-managed per-draw uniforms, value changes don't touch
|
|
@@ -1157,6 +1424,9 @@ export function buildHeapScene(
|
|
|
1157
1424
|
// both. `derivedScene` is undefined when the option is off; nothing
|
|
1158
1425
|
// else in this file should run when it is. On by default — pass
|
|
1159
1426
|
// `enableDerivedUniforms: false` to opt out.
|
|
1427
|
+
const enableCompaction = opts.enableCompaction !== false;
|
|
1428
|
+
const releaseConstantAttributes = opts.releaseConstantAttributes === true;
|
|
1429
|
+
const compactionWasteFloor = opts.compactionWasteFloorBytes ?? COMPACTION_WASTE_FLOOR_BYTES;
|
|
1160
1430
|
const enableDerivedUniforms = opts.enableDerivedUniforms !== false;
|
|
1161
1431
|
const derivedScene: DerivedUniformsScene | undefined = enableDerivedUniforms
|
|
1162
1432
|
? new DerivedUniformsScene(device, {
|
|
@@ -1436,8 +1706,9 @@ export function buildHeapScene(
|
|
|
1436
1706
|
|
|
1437
1707
|
// ─── Family state (§6 family-merge, slice 3c) ─────────────────────
|
|
1438
1708
|
// Built lazily on the first addDraw batch from the union of all
|
|
1439
|
-
// effects in that batch
|
|
1440
|
-
//
|
|
1709
|
+
// effects in that batch. A NEW effect appearing later is registered
|
|
1710
|
+
// on the fly (ensureFamilyForSpec) — families are independent per
|
|
1711
|
+
// effect (merge disabled), so the scene can mix effects added over time.
|
|
1441
1712
|
//
|
|
1442
1713
|
// Bucket key collapses to (familyId, pipelineState): every effect
|
|
1443
1714
|
// in the family shares one bucket per pipelineState. The family
|
|
@@ -1581,6 +1852,25 @@ export function buildHeapScene(
|
|
|
1581
1852
|
familyFor(effect); // throws if unknown
|
|
1582
1853
|
}
|
|
1583
1854
|
|
|
1855
|
+
/**
|
|
1856
|
+
* Reactive family rebuild: register a new effect's family when it first
|
|
1857
|
+
* appears after the initial freeze. Family-merge is disabled, so every
|
|
1858
|
+
* Effect already gets its OWN independent single-member family (its own
|
|
1859
|
+
* shader modules + buckets); a late effect just needs its family compiled
|
|
1860
|
+
* and added — no rebuild of existing families. Idempotent.
|
|
1861
|
+
*/
|
|
1862
|
+
function ensureFamilyForSpec(spec: HeapDrawSpec): void {
|
|
1863
|
+
if (!familyBuilt) { buildFamilyFromSpecs([spec]); return; }
|
|
1864
|
+
if (familyByEffectId.has(spec.effect.id)) return;
|
|
1865
|
+
const entry = { attributes: new Set<string>(), uniforms: new Set<string>() };
|
|
1866
|
+
if (spec.instanceAttributes !== undefined) {
|
|
1867
|
+
for (const name of Object.keys(spec.instanceAttributes)) entry.attributes.add(name);
|
|
1868
|
+
}
|
|
1869
|
+
const singleMap = new Map<Effect, { attributes: Set<string>; uniforms: Set<string> }>();
|
|
1870
|
+
singleMap.set(spec.effect, entry);
|
|
1871
|
+
familyByEffectId.set(spec.effect.id, compileFamilyFor([spec.effect], singleMap));
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1584
1874
|
// ─── id-of helpers ────────────────────────────────────────────────
|
|
1585
1875
|
const idOf = (s: Effect): string => `effect#${s.id}`;
|
|
1586
1876
|
const textureIds = new WeakMap<HeapTextureSet, string>();
|
|
@@ -1716,7 +2006,9 @@ export function buildHeapScene(
|
|
|
1716
2006
|
const dtBytes = Math.max(RECORD_BYTES, s.recordCount * RECORD_BYTES);
|
|
1717
2007
|
entries.push(
|
|
1718
2008
|
{ binding: 4, resource: { buffer: s.drawTableBuf.buffer, offset: 0, size: dtBytes } },
|
|
1719
|
-
|
|
2009
|
+
// Index data lives in the attr arena now (the VS storage-decodes it);
|
|
2010
|
+
// bind the same buffer as the gather views.
|
|
2011
|
+
{ binding: 5, resource: { buffer: attrsBuf } },
|
|
1720
2012
|
{ binding: 6, resource: { buffer: s.firstDrawInTileBuf!.buffer } },
|
|
1721
2013
|
);
|
|
1722
2014
|
s.renderBoundRecordCount = s.recordCount;
|
|
@@ -1849,15 +2141,6 @@ export function buildHeapScene(
|
|
|
1849
2141
|
atlasPool.onPageAdded(f, () => rebuildAllBindGroups(true));
|
|
1850
2142
|
}
|
|
1851
2143
|
}
|
|
1852
|
-
arena.indices.onAnyResize((resizedChunkIdx) => {
|
|
1853
|
-
for (const b of buckets) {
|
|
1854
|
-
if (b.chunkIdx !== resizedChunkIdx) continue;
|
|
1855
|
-
for (let i = 0; i < b.slots.length; i++) {
|
|
1856
|
-
b.slots[i]!.bindGroup = buildBucketBindGroup(b, i);
|
|
1857
|
-
}
|
|
1858
|
-
}
|
|
1859
|
-
});
|
|
1860
|
-
|
|
1861
2144
|
// ─── findOrCreateBucket ───────────────────────────────────────────
|
|
1862
2145
|
// Slice 3c: the bucket key collapses to (familyId, pipelineState).
|
|
1863
2146
|
// Every member effect in the family shares one bucket per
|
|
@@ -2416,7 +2699,7 @@ export function buildHeapScene(
|
|
|
2416
2699
|
firstIndex: number;
|
|
2417
2700
|
indexCount: number;
|
|
2418
2701
|
instanceCount: number;
|
|
2419
|
-
perDrawRefs:
|
|
2702
|
+
perDrawRefs: Int32Array;
|
|
2420
2703
|
descriptor: PipelineStateDescriptor;
|
|
2421
2704
|
/** Filled in below after synthetic combos are registered. */
|
|
2422
2705
|
targetComboId: number;
|
|
@@ -2596,7 +2879,11 @@ export function buildHeapScene(
|
|
|
2596
2879
|
if (cpuMigration !== undefined) {
|
|
2597
2880
|
let totalEmit = 0;
|
|
2598
2881
|
for (const m of cpuMigration) {
|
|
2599
|
-
const uRefs = bucket.uniformOrder!.map(n =>
|
|
2882
|
+
const uRefs = bucket.uniformOrder!.map(n => {
|
|
2883
|
+
const i = bucket.fieldIdx.get(n);
|
|
2884
|
+
const r = i === undefined ? -1 : m.perDrawRefs[i]!;
|
|
2885
|
+
return r < 0 ? 0 : r;
|
|
2886
|
+
});
|
|
2600
2887
|
const recIdx = partition.appendRecord(
|
|
2601
2888
|
m.localSlot, m.firstIndex, m.indexCount, m.instanceCount, m.targetComboId, uRefs,
|
|
2602
2889
|
);
|
|
@@ -2717,6 +3004,8 @@ export function buildHeapScene(
|
|
|
2717
3004
|
);
|
|
2718
3005
|
const drawHeap = new DrawHeap(drawHeapBuf, layout.drawHeaderBytes);
|
|
2719
3006
|
|
|
3007
|
+
const fieldIdx = new Map<string, number>();
|
|
3008
|
+
layout.drawHeaderFields.forEach((f, i) => fieldIdx.set(f.name, i));
|
|
2720
3009
|
const bucket: Bucket = {
|
|
2721
3010
|
label: bk, textures: undefined, layout, chunkIdx,
|
|
2722
3011
|
slots: [], // populated lazily by ensureSlot per modeKey
|
|
@@ -2726,7 +3015,10 @@ export function buildHeapScene(
|
|
|
2726
3015
|
headerDirtyMin: Infinity, headerDirtyMax: 0,
|
|
2727
3016
|
localPosRefs: [], localNorRefs: [],
|
|
2728
3017
|
localEntries: [], localToDrawId: [],
|
|
2729
|
-
localPerDrawAvals: [], localPerDrawRefs: [], localLayoutIds: [],
|
|
3018
|
+
localPerDrawAvals: [], localPerDrawRawFieldIdx: [], localPerDrawRefs: [], localLayoutIds: [],
|
|
3019
|
+
fieldIdx,
|
|
3020
|
+
posFieldIdx: fieldIdx.get("Positions") ?? -1,
|
|
3021
|
+
norFieldIdx: fieldIdx.get("Normals") ?? -1,
|
|
2730
3022
|
drawSlots: new Set<number>(), dirty: new Set<number>(),
|
|
2731
3023
|
isAtlasBucket,
|
|
2732
3024
|
localAtlasReleases: [],
|
|
@@ -2796,13 +3088,15 @@ export function buildHeapScene(
|
|
|
2796
3088
|
// callers writeBuffer the result to the GPU.
|
|
2797
3089
|
function packBucketHeader(
|
|
2798
3090
|
bucket: Bucket, localSlot: number,
|
|
2799
|
-
perDrawRefs:
|
|
3091
|
+
perDrawRefs: Int32Array,
|
|
2800
3092
|
layoutId: number,
|
|
2801
3093
|
): void {
|
|
2802
3094
|
const dst = bucket.drawHeaderStaging;
|
|
2803
3095
|
const u32 = new Uint32Array(dst.buffer, dst.byteOffset, dst.length);
|
|
2804
3096
|
const baseFloat = (localSlot * bucket.layout.drawHeaderBytes) / 4;
|
|
2805
|
-
|
|
3097
|
+
const fields = bucket.layout.drawHeaderFields;
|
|
3098
|
+
for (let i = 0; i < fields.length; i++) {
|
|
3099
|
+
const f = fields[i]!;
|
|
2806
3100
|
// texture-ref fields carry inline values (pageRef/formatBits as u32,
|
|
2807
3101
|
// origin/size as vec2<f32>) and are filled by packAtlasTextureFields.
|
|
2808
3102
|
if (f.kind === "texture-ref") continue;
|
|
@@ -2815,15 +3109,11 @@ export function buildHeapScene(
|
|
|
2815
3109
|
u32[fOff] = layoutId >>> 0;
|
|
2816
3110
|
continue;
|
|
2817
3111
|
}
|
|
2818
|
-
const ref = perDrawRefs
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
u32[fOff] = 0;
|
|
2824
|
-
continue;
|
|
2825
|
-
}
|
|
2826
|
-
u32[fOff] = ref;
|
|
3112
|
+
const ref = perDrawRefs[i]!;
|
|
3113
|
+
// Family-merge: a slot's effect doesn't populate every field of
|
|
3114
|
+
// the union (−1); leave the unused slots zero — the layoutId
|
|
3115
|
+
// switch ensures they're never read by the wrong effect's helper.
|
|
3116
|
+
u32[fOff] = ref < 0 ? 0 : ref;
|
|
2827
3117
|
}
|
|
2828
3118
|
}
|
|
2829
3119
|
|
|
@@ -2916,6 +3206,7 @@ export function buildHeapScene(
|
|
|
2916
3206
|
totalDraws: 0,
|
|
2917
3207
|
drawBytes: 0,
|
|
2918
3208
|
geometryBytes: 0,
|
|
3209
|
+
compactions: 0,
|
|
2919
3210
|
derivedPullMs: 0,
|
|
2920
3211
|
derivedUploadMs: 0,
|
|
2921
3212
|
derivedEncodeMs: 0,
|
|
@@ -2958,11 +3249,10 @@ export function buildHeapScene(
|
|
|
2958
3249
|
// single spec when no batched lazy-build occurred earlier (e.g.
|
|
2959
3250
|
// direct addDraw at runtime). The aset / array initial-population
|
|
2960
3251
|
// paths build from the full effect set up front.
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
}
|
|
3252
|
+
// Build the family lazily from this spec (first addDraw), or register a
|
|
3253
|
+
// newly-seen effect's family on the fly (reactive rebuild) — supports a
|
|
3254
|
+
// scene mixing multiple effects added over time (tiles + markers + …).
|
|
3255
|
+
ensureFamilyForSpec(spec);
|
|
2966
3256
|
const perInstanceUniforms = new Set<string>();
|
|
2967
3257
|
const perInstanceAttributes = spec.instanceAttributes !== undefined
|
|
2968
3258
|
? new Set(Object.keys(spec.instanceAttributes))
|
|
@@ -2984,16 +3274,47 @@ export function buildHeapScene(
|
|
|
2984
3274
|
token: outerTok,
|
|
2985
3275
|
});
|
|
2986
3276
|
}
|
|
2987
|
-
//
|
|
2988
|
-
//
|
|
2989
|
-
//
|
|
2990
|
-
//
|
|
2991
|
-
//
|
|
2992
|
-
//
|
|
2993
|
-
//
|
|
2994
|
-
//
|
|
2995
|
-
//
|
|
2996
|
-
|
|
3277
|
+
// ─── Group-fit page placement (§3) ───────────────────────────────
|
|
3278
|
+
// A draw's whole group — uniforms + attributes + INDEX data (all one
|
|
3279
|
+
// arena now) — must land on ONE chunk/page (the draw binds one buffer).
|
|
3280
|
+
// Estimate the group's arena bytes (exact per-field via the same
|
|
3281
|
+
// placement helpers, ignoring dedup ⇒ an upper bound on the NEW bytes a
|
|
3282
|
+
// chunk must absorb), then place the group on a chunk with room — prefer
|
|
3283
|
+
// one already holding a shared input, else the newest, else open a fresh
|
|
3284
|
+
// page. This replaces the old cross-chunk spill → throw, so a scene can
|
|
3285
|
+
// grow past one buffer's cap (WebGPU's storage-buffer binding limit).
|
|
3286
|
+
const fam = familyFor(spec.effect);
|
|
3287
|
+
const effectFields = fam.fieldsForEffect.get(spec.effect.id)!;
|
|
3288
|
+
const layout = fam.schema.drawHeaderUnion;
|
|
3289
|
+
const hasIndices = spec.indices !== undefined;
|
|
3290
|
+
const indicesAval = hasIndices ? (asAval(spec.indices!) as aval<Uint32Array>) : undefined;
|
|
3291
|
+
const indexArr = hasIndices ? (readPlain(spec.indices!) as Uint32Array) : undefined;
|
|
3292
|
+
const instanceCount = spec.instanceCount !== undefined
|
|
3293
|
+
? (readPlain(spec.instanceCount) as number) : 1;
|
|
3294
|
+
|
|
3295
|
+
let estBytes = indexArr !== undefined ? ALIGN16(ALLOC_HEADER_PAD_TO + indexArr.byteLength) : 0;
|
|
3296
|
+
for (const f of layout.drawHeaderFields) {
|
|
3297
|
+
if (f.kind === "texture-ref" || f.name === "__layoutId" || INLINE_HEADER_UNIFORMS.has(f.name) || !effectFields.has(f.name)) continue;
|
|
3298
|
+
if (ruleForUniform(spec, f.name) !== undefined) {
|
|
3299
|
+
estBytes += ALIGN16(ALLOC_HEADER_PAD_TO + PACKER_MAT4.dataBytes); // §7 derived dummy mat4
|
|
3300
|
+
continue;
|
|
3301
|
+
}
|
|
3302
|
+
const perInstUniform = f.kind === "uniform-ref" && perInstanceUniforms.has(f.name);
|
|
3303
|
+
const perInstAttr = f.kind === "attribute-ref" && perInstanceAttributes.has(f.name);
|
|
3304
|
+
const provided = perInstAttr ? spec.instanceAttributes![f.name] : spec.inputs[f.name];
|
|
3305
|
+
if (provided === undefined) continue; // the acquire loop throws with a precise message
|
|
3306
|
+
let dataBytes: number;
|
|
3307
|
+
if (f.kind === "attribute-ref" && !perInstUniform && isBufferView(provided)) {
|
|
3308
|
+
dataBytes = bufferViewPlacement(f, provided).dataBytes;
|
|
3309
|
+
} else {
|
|
3310
|
+
const value = asAval(provided as aval<unknown> | unknown).getValue(outerTok);
|
|
3311
|
+
dataBytes = (perInstUniform
|
|
3312
|
+
? perInstancePlacementFor(f, value, instanceCount)
|
|
3313
|
+
: poolPlacementFor(f, value)).dataBytes;
|
|
3314
|
+
}
|
|
3315
|
+
estBytes += ALIGN16(ALLOC_HEADER_PAD_TO + dataBytes);
|
|
3316
|
+
}
|
|
3317
|
+
|
|
2997
3318
|
let chunkIdx: number;
|
|
2998
3319
|
{
|
|
2999
3320
|
let preferred: number | undefined;
|
|
@@ -3005,7 +3326,12 @@ export function buildHeapScene(
|
|
|
3005
3326
|
if (where !== undefined) { preferred = where; break; }
|
|
3006
3327
|
}
|
|
3007
3328
|
}
|
|
3008
|
-
|
|
3329
|
+
const fits = (c: number): boolean => arena.attrs.bumpHeadroom(c) >= estBytes;
|
|
3330
|
+
const newest = arena.attrs.chunkCount - 1;
|
|
3331
|
+
chunkIdx =
|
|
3332
|
+
preferred !== undefined && fits(preferred) ? preferred
|
|
3333
|
+
: fits(newest) ? newest
|
|
3334
|
+
: arena.attrs.openPage();
|
|
3009
3335
|
}
|
|
3010
3336
|
const bucket = findOrCreateBucket(spec.effect, spec.textures, spec.pipelineState, chunkIdx, precomputedDescriptor);
|
|
3011
3337
|
// Phase 5c.3 — every RO carrying a derived-mode rule is registered
|
|
@@ -3038,19 +3364,17 @@ export function buildHeapScene(
|
|
|
3038
3364
|
// tables every frame from the master.
|
|
3039
3365
|
const roSlot = bucket.gpuRouted ? bucket.slots[0]! : ensureSlot(bucket, roDescriptor);
|
|
3040
3366
|
const roSlotIdx = bucket.slots.indexOf(roSlot);
|
|
3041
|
-
const fam = familyFor(spec.effect);
|
|
3042
|
-
const effectFields = fam.fieldsForEffect.get(spec.effect.id)!;
|
|
3043
3367
|
|
|
3044
|
-
// Indices
|
|
3045
|
-
// Aval-keyed: 19K instanced clones of the same mesh share
|
|
3046
|
-
// index allocation + one upload. NON-INDEXED draws (spec.indices
|
|
3047
|
-
// omitted) skip the
|
|
3048
|
-
//
|
|
3049
|
-
//
|
|
3050
|
-
|
|
3051
|
-
|
|
3368
|
+
// Indices ride the same arena as attributes/uniforms (the VS storage-
|
|
3369
|
+
// decodes them). Aval-keyed: 19K instanced clones of the same mesh share
|
|
3370
|
+
// one index allocation + one upload. NON-INDEXED draws (spec.indices
|
|
3371
|
+
// omitted) skip the pool: the drawTable record stores firstIndex =
|
|
3372
|
+
// HEAP_NONINDEXED (sentinel) and indexCount = vertexCount, and the
|
|
3373
|
+
// megacall prelude uses the local vertex index directly. `indicesAval` /
|
|
3374
|
+
// `indexArr` were read above for the group estimate; the group landed
|
|
3375
|
+
// wholly on `bucket.chunkIdx`, so this allocation can't spill.
|
|
3052
3376
|
const idxAlloc = hasIndices
|
|
3053
|
-
? indexPool.acquire(device, arena.
|
|
3377
|
+
? indexPool.acquire(device, arena.attrs, indicesAval!, bucket.chunkIdx, indexArr!)
|
|
3054
3378
|
: undefined;
|
|
3055
3379
|
// Emit count per instance + the firstIndex field written to the record.
|
|
3056
3380
|
// Slice: emit only [firstIndex, firstIndex+indexCount) of the (shared,
|
|
@@ -3060,10 +3384,6 @@ export function buildHeapScene(
|
|
|
3060
3384
|
const firstIndexField = hasIndices ? (idxAlloc!.firstIndex + (spec.firstIndex ?? 0)) : HEAP_NONINDEXED;
|
|
3061
3385
|
|
|
3062
3386
|
const localSlot = bucket.drawHeap.alloc();
|
|
3063
|
-
// Per-RO instancing: read `spec.instanceCount` (defaults to 1).
|
|
3064
|
-
const instanceCount = spec.instanceCount !== undefined
|
|
3065
|
-
? readPlain(spec.instanceCount) as number
|
|
3066
|
-
: 1;
|
|
3067
3387
|
|
|
3068
3388
|
// Walk the bucket's schema-driven DrawHeader fields. Per-instance
|
|
3069
3389
|
// attributes pull from `spec.instanceAttributes` and pack into an
|
|
@@ -3071,7 +3391,18 @@ export function buildHeapScene(
|
|
|
3071
3391
|
// packs as a single value. Both go through the same pool — sharing
|
|
3072
3392
|
// emerges from aval identity either way.
|
|
3073
3393
|
const perDrawAvals: aval<unknown>[] = [];
|
|
3074
|
-
|
|
3394
|
+
let perDrawRawFieldIdx: number[] | undefined;
|
|
3395
|
+
const perDrawRefs = new Int32Array(bucket.layout.drawHeaderFields.length).fill(-1);
|
|
3396
|
+
const setRef = (name: string, ref: number): void => {
|
|
3397
|
+
const i = bucket.fieldIdx.get(name);
|
|
3398
|
+
if (i !== undefined) perDrawRefs[i] = ref;
|
|
3399
|
+
};
|
|
3400
|
+
const getRef = (name: string): number | undefined => {
|
|
3401
|
+
const i = bucket.fieldIdx.get(name);
|
|
3402
|
+
if (i === undefined) return undefined;
|
|
3403
|
+
const r = perDrawRefs[i]!;
|
|
3404
|
+
return r < 0 ? undefined : r;
|
|
3405
|
+
};
|
|
3075
3406
|
{
|
|
3076
3407
|
const tok = outerTok;
|
|
3077
3408
|
for (const f of bucket.layout.drawHeaderFields) {
|
|
@@ -3083,18 +3414,45 @@ export function buildHeapScene(
|
|
|
3083
3414
|
// layoutId branch never reads them, so the slot stays zero.
|
|
3084
3415
|
if (f.name === "__layoutId") continue;
|
|
3085
3416
|
if (!effectFields.has(f.name)) continue;
|
|
3417
|
+
// Inline header uniforms (PickId): the header word IS the u32
|
|
3418
|
+
// value — write it via perDrawRefs (packBucketHeader copies
|
|
3419
|
+
// refs verbatim), skip the pool entirely. A provider-supplied
|
|
3420
|
+
// PickId aval still works below as the (slow) fallback.
|
|
3421
|
+
if (INLINE_HEADER_UNIFORMS.has(f.name)) {
|
|
3422
|
+
const iv = f.name === "PickId" ? spec.pickId : undefined;
|
|
3423
|
+
if (iv !== undefined) {
|
|
3424
|
+
setRef(f.name, iv >>> 0);
|
|
3425
|
+
continue;
|
|
3426
|
+
}
|
|
3427
|
+
const provInline = spec.inputs[f.name];
|
|
3428
|
+
if (typeof provInline === "number") {
|
|
3429
|
+
setRef(f.name, provInline >>> 0);
|
|
3430
|
+
continue;
|
|
3431
|
+
}
|
|
3432
|
+
if (provInline === undefined) {
|
|
3433
|
+
// No id at all (picking disabled but the effect still
|
|
3434
|
+
// reads the uniform): 0 = "no pick", a reserved id.
|
|
3435
|
+
setRef(f.name, 0);
|
|
3436
|
+
continue;
|
|
3437
|
+
}
|
|
3438
|
+
// fall through: aval-provided → legacy pooled path
|
|
3439
|
+
}
|
|
3086
3440
|
// §7: derived-uniform fields are produced by the compute
|
|
3087
3441
|
// pre-pass. We still allocate an arena slot here so the
|
|
3088
3442
|
// drawHeader gets a valid ref written by packBucketHeader; §7
|
|
3089
3443
|
// overwrites the arena data each frame the inputs are dirty.
|
|
3090
3444
|
if (ruleForUniform(spec, f.name) !== undefined) {
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3445
|
+
// Raw (aval-less) slot: §7's compute pre-pass overwrites the
|
|
3446
|
+
// data every dirty frame, so a keyed pool entry (placeholder
|
|
3447
|
+
// ConstantVal + Map per draw) bought nothing. The ref lives
|
|
3448
|
+
// in the drawHeader cell (compaction re-seats it there);
|
|
3449
|
+
// removeDraw frees it via `perDrawRawFieldIdx`.
|
|
3450
|
+
const ref = pool.allocRaw(
|
|
3451
|
+
arena.attrs, bucket.chunkIdx,
|
|
3452
|
+
PACKER_MAT4.dataBytes, PACKER_MAT4.typeId, 1,
|
|
3095
3453
|
);
|
|
3096
|
-
|
|
3097
|
-
|
|
3454
|
+
setRef(f.name, ref);
|
|
3455
|
+
(perDrawRawFieldIdx ??= []).push(bucket.fieldIdx.get(f.name)!);
|
|
3098
3456
|
continue;
|
|
3099
3457
|
}
|
|
3100
3458
|
const isPerInstanceUniformField =
|
|
@@ -3126,11 +3484,37 @@ export function buildHeapScene(
|
|
|
3126
3484
|
let av: aval<unknown>;
|
|
3127
3485
|
let value: unknown;
|
|
3128
3486
|
let placement: ReturnType<typeof poolPlacementFor>;
|
|
3487
|
+
let releasable: { _v: unknown } | undefined;
|
|
3129
3488
|
if (f.kind === "attribute-ref" && !isPerInstanceUniformField && isBufferView(provided)) {
|
|
3130
3489
|
const bv = provided;
|
|
3131
3490
|
placement = bufferViewPlacement(f, bv);
|
|
3132
3491
|
av = bv.buffer as aval<unknown>;
|
|
3133
3492
|
value = bv.buffer.getValue(tok);
|
|
3493
|
+
} else if (
|
|
3494
|
+
releaseConstantAttributes
|
|
3495
|
+
&& f.kind === "attribute-ref"
|
|
3496
|
+
&& !isPerInstanceUniformField
|
|
3497
|
+
&& !(typeof provided === "object" && provided !== null
|
|
3498
|
+
&& typeof (provided as { getValue?: unknown }).getValue === "function")
|
|
3499
|
+
) {
|
|
3500
|
+
// Plain value (raw array / packed marker) → CONSTANT by definition.
|
|
3501
|
+
// Wrap in a droppable holder: after this field's arena write the GPU
|
|
3502
|
+
// copy is the only copy the heap ever reads again, so the payload is
|
|
3503
|
+
// released (real avals never take this branch — changeable data
|
|
3504
|
+
// stays alive by contract).
|
|
3505
|
+
const holder = {
|
|
3506
|
+
_v: provided as unknown,
|
|
3507
|
+
getValue(): unknown {
|
|
3508
|
+
if (holder._v === null) {
|
|
3509
|
+
throw new Error("heapScene: constant attribute payload was released after staging (releaseConstantAttributes)");
|
|
3510
|
+
}
|
|
3511
|
+
return holder._v;
|
|
3512
|
+
},
|
|
3513
|
+
};
|
|
3514
|
+
releasable = holder;
|
|
3515
|
+
av = holder as unknown as aval<unknown>;
|
|
3516
|
+
value = provided;
|
|
3517
|
+
placement = poolPlacementFor(f, value);
|
|
3134
3518
|
} else {
|
|
3135
3519
|
av = asAval(provided as aval<unknown> | unknown);
|
|
3136
3520
|
value = av.getValue(tok);
|
|
@@ -3142,19 +3526,21 @@ export function buildHeapScene(
|
|
|
3142
3526
|
device, arena.attrs, av, bucket.chunkIdx, value,
|
|
3143
3527
|
placement.dataBytes, placement.typeId, placement.length, placement.pack,
|
|
3144
3528
|
);
|
|
3145
|
-
|
|
3529
|
+
if (releasable !== undefined) releasable._v = null; // staged — drop the CPU copy
|
|
3530
|
+
setRef(f.name, ref);
|
|
3146
3531
|
perDrawAvals.push(av);
|
|
3147
3532
|
}
|
|
3148
3533
|
}
|
|
3149
3534
|
|
|
3150
|
-
bucket.localPosRefs[localSlot] =
|
|
3151
|
-
bucket.localNorRefs[localSlot] =
|
|
3535
|
+
bucket.localPosRefs[localSlot] = getRef("Positions");
|
|
3536
|
+
bucket.localNorRefs[localSlot] = getRef("Normals");
|
|
3152
3537
|
bucket.localEntries[localSlot] = {
|
|
3153
3538
|
indexCount: emitCount, firstIndex: firstIndexField, instanceCount,
|
|
3154
3539
|
};
|
|
3155
3540
|
bucket.localToDrawId[localSlot] = drawId;
|
|
3156
3541
|
bucket.drawSlots.add(localSlot);
|
|
3157
3542
|
bucket.localPerDrawAvals[localSlot] = perDrawAvals;
|
|
3543
|
+
bucket.localPerDrawRawFieldIdx[localSlot] = perDrawRawFieldIdx;
|
|
3158
3544
|
bucket.localPerDrawRefs[localSlot] = perDrawRefs;
|
|
3159
3545
|
const layoutId = fam.schema.layoutIdOf.get(spec.effect)!;
|
|
3160
3546
|
bucket.localLayoutIds[localSlot] = layoutId;
|
|
@@ -3264,7 +3650,7 @@ export function buildHeapScene(
|
|
|
3264
3650
|
// it) get 0 — the kernel only reads refs through combo fns that
|
|
3265
3651
|
// gate on `r.comboId`, so an unread 0 is harmless.
|
|
3266
3652
|
const uniformRefs: number[] = (bucket.uniformOrder ?? []).map(
|
|
3267
|
-
name =>
|
|
3653
|
+
name => getRef(name) ?? 0,
|
|
3268
3654
|
);
|
|
3269
3655
|
partition.appendRecord(localSlot, firstIndexField, emitCount, instanceCount, roComboId, uniformRefs);
|
|
3270
3656
|
bucket.drawIdToRecord!.set(drawId, recIdx);
|
|
@@ -3369,7 +3755,10 @@ export function buildHeapScene(
|
|
|
3369
3755
|
}
|
|
3370
3756
|
const tracker = new ModeKeyTracker(
|
|
3371
3757
|
spec.pipelineState, sig,
|
|
3372
|
-
|
|
3758
|
+
// skipSubscribe means the tracker never invokes onDirty itself
|
|
3759
|
+
// (scene-level modeAvalToDrawIds dispatches instead) — a shared
|
|
3760
|
+
// noop avoids one closure per draw.
|
|
3761
|
+
NOOP,
|
|
3373
3762
|
{
|
|
3374
3763
|
skipSubscribe: true,
|
|
3375
3764
|
...(spec.modeRules !== undefined ? { modeRules: spec.modeRules } : {}),
|
|
@@ -3404,11 +3793,11 @@ export function buildHeapScene(
|
|
|
3404
3793
|
const ruleSubset = new Map<string, DerivedRule>();
|
|
3405
3794
|
const outOffsetByName = new Map<string, number>();
|
|
3406
3795
|
for (const f of bucket.layout.drawHeaderFields) {
|
|
3407
|
-
if (f.name === "__layoutId") continue;
|
|
3796
|
+
if (f.name === "__layoutId" || INLINE_HEADER_UNIFORMS.has(f.name)) continue;
|
|
3408
3797
|
if (!effectFields.has(f.name)) continue;
|
|
3409
3798
|
const rule = ruleForUniform(spec, f.name);
|
|
3410
3799
|
if (rule === undefined) continue;
|
|
3411
|
-
const ref =
|
|
3800
|
+
const ref = getRef(f.name);
|
|
3412
3801
|
if (ref === undefined) continue;
|
|
3413
3802
|
ruleSubset.set(f.name, rule);
|
|
3414
3803
|
outOffsetByName.set(f.name, ref + ALLOC_HEADER_PAD_TO);
|
|
@@ -3434,7 +3823,7 @@ export function buildHeapScene(
|
|
|
3434
3823
|
trafoAvals,
|
|
3435
3824
|
...(spec.modelChain !== undefined ? { modelChain: spec.modelChain } : {}),
|
|
3436
3825
|
hostUniformOffset: (n) => {
|
|
3437
|
-
const r =
|
|
3826
|
+
const r = getRef(n);
|
|
3438
3827
|
return r === undefined ? undefined : r + ALLOC_HEADER_PAD_TO;
|
|
3439
3828
|
},
|
|
3440
3829
|
outputOffset: (n) => outOffsetByName.get(n),
|
|
@@ -3545,8 +3934,21 @@ export function buildHeapScene(
|
|
|
3545
3934
|
// Release pool entries — refcount drops; if zero, allocation freed.
|
|
3546
3935
|
const avals = bucket.localPerDrawAvals[localSlot];
|
|
3547
3936
|
if (avals !== undefined) for (const av of avals) pool.release(arena.attrs, av, bucket.chunkIdx);
|
|
3937
|
+
// Raw §7 slots: the live ref is the drawHeader cell (compaction
|
|
3938
|
+
// keeps it current); free it directly.
|
|
3939
|
+
const rawIdxs = bucket.localPerDrawRawFieldIdx[localSlot];
|
|
3940
|
+
if (rawIdxs !== undefined) {
|
|
3941
|
+
const rawRefs = bucket.localPerDrawRefs[localSlot];
|
|
3942
|
+
if (rawRefs !== undefined) {
|
|
3943
|
+
for (const fi of rawIdxs) {
|
|
3944
|
+
const ref = rawRefs[fi]!;
|
|
3945
|
+
if (ref >= 0) pool.releaseRaw(arena.attrs, bucket.chunkIdx, ref, PACKER_MAT4.dataBytes);
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
bucket.localPerDrawRawFieldIdx[localSlot] = undefined;
|
|
3949
|
+
}
|
|
3548
3950
|
const idxAval = drawIdToIndexAval[drawId];
|
|
3549
|
-
if (idxAval !== undefined) indexPool.release(arena.
|
|
3951
|
+
if (idxAval !== undefined) indexPool.release(arena.attrs, idxAval, bucket.chunkIdx);
|
|
3550
3952
|
const atlasRel = bucket.localAtlasReleases[localSlot];
|
|
3551
3953
|
if (atlasRel !== undefined) atlasRel();
|
|
3552
3954
|
// Drop atlas-aval ref (if any). When the last ref is dropped we
|
|
@@ -3616,14 +4018,16 @@ export function buildHeapScene(
|
|
|
3616
4018
|
function drainAsetWith(tok: AdaptiveToken): void {
|
|
3617
4019
|
if (asetReader === undefined) return;
|
|
3618
4020
|
const delta = asetReader.getChanges(tok);
|
|
4021
|
+
// Process removals before adds. A remove+add in the same
|
|
4022
|
+
// transaction must free its slot / arena region / atlas tile
|
|
4023
|
+
// before the add allocates, so the freelist reuses the just-freed
|
|
4024
|
+
// block instead of growing the high-water toward
|
|
4025
|
+
// live + removed-this-tick. (Matches Aardvark's removals-first
|
|
4026
|
+
// delta discipline.)
|
|
4027
|
+
const adds: HeapDrawSpec[] = [];
|
|
3619
4028
|
delta.iter((op: { value: HeapDrawSpec; count: number }) => {
|
|
3620
4029
|
if (op.count > 0) {
|
|
3621
|
-
|
|
3622
|
-
// already sceneObj) — collapses N nested evaluateAlways into
|
|
3623
|
-
// one for a bulk add. Saves a per-RO setUnsafeEvaluationDepth
|
|
3624
|
-
// + outputs.add(sceneObj) round trip.
|
|
3625
|
-
const id = addDrawImpl(op.value, tok);
|
|
3626
|
-
specToDrawId.set(op.value, id);
|
|
4030
|
+
adds.push(op.value);
|
|
3627
4031
|
} else {
|
|
3628
4032
|
const id = specToDrawId.get(op.value);
|
|
3629
4033
|
if (id !== undefined) {
|
|
@@ -3632,6 +4036,13 @@ export function buildHeapScene(
|
|
|
3632
4036
|
}
|
|
3633
4037
|
}
|
|
3634
4038
|
});
|
|
4039
|
+
// Re-use the outer evaluateAlways scope's token (caller is already
|
|
4040
|
+
// sceneObj) — collapses N nested evaluateAlways into one for a bulk
|
|
4041
|
+
// add. Saves a per-RO setUnsafeEvaluationDepth + outputs.add round trip.
|
|
4042
|
+
for (const value of adds) {
|
|
4043
|
+
const id = addDrawImpl(value, tok);
|
|
4044
|
+
specToDrawId.set(value, id);
|
|
4045
|
+
}
|
|
3635
4046
|
}
|
|
3636
4047
|
|
|
3637
4048
|
// ─── Initial population ───────────────────────────────────────────
|
|
@@ -3648,9 +4059,9 @@ export function buildHeapScene(
|
|
|
3648
4059
|
asetReader = (initialDraws as aset<HeapDrawSpec>).getReader();
|
|
3649
4060
|
sceneObj.evaluateAlways(AdaptiveToken.top, (tok) => {
|
|
3650
4061
|
// First drain: snapshot all add-ops, build the family from their
|
|
3651
|
-
// effects up front, then run addDraw. Subsequent drains
|
|
3652
|
-
//
|
|
3653
|
-
//
|
|
4062
|
+
// effects up front, then run addDraw. Subsequent drains reuse the
|
|
4063
|
+
// family, or register a newly-seen effect's family on the fly
|
|
4064
|
+
// (ensureFamilyForSpec, via addDrawImpl).
|
|
3654
4065
|
if (!familyBuilt) {
|
|
3655
4066
|
const reader = asetReader!;
|
|
3656
4067
|
const delta = reader.getChanges(tok);
|
|
@@ -3679,6 +4090,142 @@ export function buildHeapScene(
|
|
|
3679
4090
|
});
|
|
3680
4091
|
}
|
|
3681
4092
|
|
|
4093
|
+
// ─── Heap compaction (attribute arena + index buffer) ────────────
|
|
4094
|
+
/**
|
|
4095
|
+
* Compact every arena/index chunk whose fragmentation waste crosses the
|
|
4096
|
+
* floor (or unconditionally when `force`), then re-seat EVERY cached ref so
|
|
4097
|
+
* the relocated allocations stay addressable. Attribute arena:
|
|
4098
|
+
* • uniform-pool entries (`pool.remapRefs`)
|
|
4099
|
+
* • §7 derived-uniform record handles (`derivedScene.remapHostHeap`)
|
|
4100
|
+
* • modes partition master refs (`partitionScene.remapUniformRefs`)
|
|
4101
|
+
* • drawHeader cells + the Pos/Nor caches (re-`packBucketHeader`)
|
|
4102
|
+
* Index buffer (ref = `allocBase + slice`, held in the drawTable / partition
|
|
4103
|
+
* master + IndexPool entry):
|
|
4104
|
+
* • per-draw `indexStart` in the CPU drawTable or partition master
|
|
4105
|
+
* • `localEntries[slot].firstIndex` + IndexPool entry base
|
|
4106
|
+
* Must run with the buffers current (we flush first) and before the
|
|
4107
|
+
* per-bucket header / drawTable flush so the re-seated records upload.
|
|
4108
|
+
*/
|
|
4109
|
+
// Per-scene ref re-seat after the shared store relocates regions. Registered
|
|
4110
|
+
// with `storage`; the store calls it (for THIS scene + any others sharing the
|
|
4111
|
+
// store) inside `storage.compact`. Re-seats derived records, partition master
|
|
4112
|
+
// refs, drawHeader cells, and index `_indexStart` — everything this scene
|
|
4113
|
+
// caches that points into the arena. (The storage-level uniform/index pools
|
|
4114
|
+
// are re-seated by the store itself, around this call.)
|
|
4115
|
+
function applyRemap(chunkIdx: number, remap: ReadonlyMap<number, number>): void {
|
|
4116
|
+
if (derivedScene !== undefined) {
|
|
4117
|
+
// §7 handles store DATA offsets (ref + header pad), not header refs.
|
|
4118
|
+
const dataRemap = new Map<number, number>();
|
|
4119
|
+
for (const [o, n] of remap) {
|
|
4120
|
+
dataRemap.set(o + ALLOC_HEADER_PAD_TO, n + ALLOC_HEADER_PAD_TO);
|
|
4121
|
+
}
|
|
4122
|
+
derivedScene.remapHostHeap(chunkIdx, dataRemap);
|
|
4123
|
+
}
|
|
4124
|
+
{
|
|
4125
|
+
for (const bucket of buckets) {
|
|
4126
|
+
if (bucket.chunkIdx !== chunkIdx) continue;
|
|
4127
|
+
if (bucket.gpuRouted && bucket.partitionScene !== undefined) {
|
|
4128
|
+
if (bucket.partitionScene.remapUniformRefs(remap) > 0) {
|
|
4129
|
+
bucket.partitionDirty = true;
|
|
4130
|
+
}
|
|
4131
|
+
}
|
|
4132
|
+
for (const localSlot of bucket.drawSlots) {
|
|
4133
|
+
// (a) Index region re-seat. Index data is an attr-arena region now,
|
|
4134
|
+
// so it relocates in THIS pass. Its draw stores `_indexStart` (an
|
|
4135
|
+
// element offset = allocBase + slice) in the drawTable / partition
|
|
4136
|
+
// master; shift it by the region's element delta. Independent of the
|
|
4137
|
+
// uniform re-seat below (a slot's indices may move while its uniforms
|
|
4138
|
+
// don't), so it runs before the `changed` gate.
|
|
4139
|
+
const drawId = bucket.localToDrawId[localSlot];
|
|
4140
|
+
if (drawId !== undefined) {
|
|
4141
|
+
const iav = drawIdToIndexAval[drawId];
|
|
4142
|
+
const oldRef = iav !== undefined ? indexPool.baseFor(iav, chunkIdx) : undefined;
|
|
4143
|
+
const newRef = oldRef !== undefined ? remap.get(oldRef) : undefined;
|
|
4144
|
+
const entry = bucket.localEntries[localSlot];
|
|
4145
|
+
if (oldRef !== undefined && newRef !== undefined && entry !== undefined) {
|
|
4146
|
+
const newStart = entry.firstIndex + ((newRef - oldRef) >> 2);
|
|
4147
|
+
entry.firstIndex = newStart;
|
|
4148
|
+
if (bucket.gpuRouted && bucket.partitionScene !== undefined) {
|
|
4149
|
+
const recIdx = bucket.drawIdToRecord?.get(drawId);
|
|
4150
|
+
if (recIdx !== undefined) {
|
|
4151
|
+
bucket.partitionScene.setRecordIndexStart(recIdx, newStart);
|
|
4152
|
+
bucket.partitionDirty = true;
|
|
4153
|
+
}
|
|
4154
|
+
} else {
|
|
4155
|
+
const slotIdx = drawIdToSlotIdx[drawId];
|
|
4156
|
+
const slot = slotIdx !== undefined ? bucket.slots[slotIdx] : bucket.slots[0];
|
|
4157
|
+
const recIdx = slot?.slotToRecord[localSlot];
|
|
4158
|
+
if (slot !== undefined && recIdx !== undefined && recIdx >= 0) {
|
|
4159
|
+
slot.drawTableShadow![recIdx * RECORD_U32 + 2] = newStart >>> 0;
|
|
4160
|
+
const off = recIdx * RECORD_BYTES;
|
|
4161
|
+
if (off < slot.drawTableDirtyMin) slot.drawTableDirtyMin = off;
|
|
4162
|
+
if (off + RECORD_BYTES > slot.drawTableDirtyMax) slot.drawTableDirtyMax = off + RECORD_BYTES;
|
|
4163
|
+
}
|
|
4164
|
+
}
|
|
4165
|
+
}
|
|
4166
|
+
}
|
|
4167
|
+
// (b) Uniform/attr drawHeader cells (+ Pos/Nor caches).
|
|
4168
|
+
const refs = bucket.localPerDrawRefs[localSlot];
|
|
4169
|
+
if (refs === undefined) continue;
|
|
4170
|
+
let changed = false;
|
|
4171
|
+
for (let i = 0; i < refs.length; i++) {
|
|
4172
|
+
const ref = refs[i]!;
|
|
4173
|
+
if (ref < 0) continue;
|
|
4174
|
+
const fld = bucket.layout.drawHeaderFields[i]!;
|
|
4175
|
+
// Inline header values (PickId/__layoutId) are NOT arena
|
|
4176
|
+
// refs — a numeric collision with an old offset must not
|
|
4177
|
+
// remap them.
|
|
4178
|
+
if (fld.name === "__layoutId" || INLINE_HEADER_UNIFORMS.has(fld.name)) continue;
|
|
4179
|
+
const nn = remap.get(ref);
|
|
4180
|
+
if (nn !== undefined) { refs[i] = nn; changed = true; }
|
|
4181
|
+
}
|
|
4182
|
+
if (!changed) continue;
|
|
4183
|
+
const pr = bucket.posFieldIdx >= 0 ? refs[bucket.posFieldIdx]! : -1;
|
|
4184
|
+
const nr = bucket.norFieldIdx >= 0 ? refs[bucket.norFieldIdx]! : -1;
|
|
4185
|
+
bucket.localPosRefs[localSlot] = pr < 0 ? undefined : pr;
|
|
4186
|
+
bucket.localNorRefs[localSlot] = nr < 0 ? undefined : nr;
|
|
4187
|
+
packBucketHeader(bucket, localSlot, refs, bucket.localLayoutIds[localSlot] ?? 0);
|
|
4188
|
+
if (bucket.isAtlasBucket) {
|
|
4189
|
+
const ts = bucket.localAtlasTextures[localSlot];
|
|
4190
|
+
if (ts !== undefined) packAtlasTextureFields(bucket, localSlot, ts);
|
|
4191
|
+
}
|
|
4192
|
+
const byteOff = localSlot * bucket.layout.drawHeaderBytes;
|
|
4193
|
+
if (byteOff < bucket.headerDirtyMin) bucket.headerDirtyMin = byteOff;
|
|
4194
|
+
const end = byteOff + bucket.layout.drawHeaderBytes;
|
|
4195
|
+
if (end > bucket.headerDirtyMax) bucket.headerDirtyMax = end;
|
|
4196
|
+
}
|
|
4197
|
+
}
|
|
4198
|
+
}
|
|
4199
|
+
}
|
|
4200
|
+
|
|
4201
|
+
// Register this scene as a consumer of the (possibly shared) store, so a
|
|
4202
|
+
// compaction — triggered by THIS scene or any other sharing the store —
|
|
4203
|
+
// re-seats this scene's refs. Disposed in `dispose()`.
|
|
4204
|
+
const storageReg = storage.register({ applyRemap });
|
|
4205
|
+
|
|
4206
|
+
// ─── Compaction (delegates to the shared store) ──────────────────
|
|
4207
|
+
function runCompaction(force: boolean): void {
|
|
4208
|
+
const compacted = storage.compact(device, compactionWasteFloor, force);
|
|
4209
|
+
stats.compactions += compacted;
|
|
4210
|
+
// update() flushes the re-seated drawHeaders via its own per-bucket loop
|
|
4211
|
+
// right after this returns; the forced (test) path runs outside update(),
|
|
4212
|
+
// so it flushes this scene's dirtied headers itself.
|
|
4213
|
+
if (force) {
|
|
4214
|
+
for (const bucket of buckets) {
|
|
4215
|
+
if (bucket.headerDirtyMax > bucket.headerDirtyMin) {
|
|
4216
|
+
device.queue.writeBuffer(
|
|
4217
|
+
bucket.drawHeap.buffer, bucket.headerDirtyMin,
|
|
4218
|
+
bucket.drawHeaderStaging.buffer,
|
|
4219
|
+
bucket.drawHeaderStaging.byteOffset + bucket.headerDirtyMin,
|
|
4220
|
+
bucket.headerDirtyMax - bucket.headerDirtyMin,
|
|
4221
|
+
);
|
|
4222
|
+
bucket.headerDirtyMin = Infinity;
|
|
4223
|
+
bucket.headerDirtyMax = 0;
|
|
4224
|
+
}
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
}
|
|
4228
|
+
|
|
3682
4229
|
// ─── update / encodeIntoPass / frame / dispose ───────────────────
|
|
3683
4230
|
/**
|
|
3684
4231
|
* CPU-side data refresh: drain pending aset deltas, repack any
|
|
@@ -3908,7 +4455,17 @@ export function buildHeapScene(
|
|
|
3908
4455
|
// initial population for 10K ROs this collapses ~30K calls into
|
|
3909
4456
|
// ~5 (arena, indices, plus one per bucket).
|
|
3910
4457
|
arena.attrs.flush(device);
|
|
3911
|
-
|
|
4458
|
+
|
|
4459
|
+
// ─── Waste-triggered attribute-arena compaction ─────────────────
|
|
4460
|
+
// Relocate live allocations to the front of each chunk once
|
|
4461
|
+
// fragmentation waste crosses the floor — exact-size freelist reuse
|
|
4462
|
+
// can't reclaim a drifting size distribution, so the arena would
|
|
4463
|
+
// otherwise ratchet to its high-water forever. Runs AFTER the arena
|
|
4464
|
+
// flush (GPU buffer current) and BEFORE the header flush (so the
|
|
4465
|
+
// re-seated drawHeaders upload below). Indices live in a separate
|
|
4466
|
+
// buffer and are not compacted here.
|
|
4467
|
+
if (enableCompaction) runCompaction(false);
|
|
4468
|
+
|
|
3912
4469
|
for (const bucket of buckets) {
|
|
3913
4470
|
if (bucket.headerDirtyMax > bucket.headerDirtyMin) {
|
|
3914
4471
|
device.queue.writeBuffer(
|
|
@@ -4112,14 +4669,42 @@ export function buildHeapScene(
|
|
|
4112
4669
|
}
|
|
4113
4670
|
|
|
4114
4671
|
function dispose(): void {
|
|
4115
|
-
|
|
4116
|
-
|
|
4672
|
+
// Stop receiving compaction remaps for the shared store.
|
|
4673
|
+
storageReg.dispose();
|
|
4674
|
+
// Shared storage: release this scene's allocations so the shared pool's
|
|
4675
|
+
// refcounts stay correct (another scene may still hold the same regions),
|
|
4676
|
+
// but don't destroy the buffers — the storage owner does that. Private
|
|
4677
|
+
// storage: the whole arena is torn down wholesale, so per-draw release is
|
|
4678
|
+
// unnecessary.
|
|
4679
|
+
if (!ownsStorage) {
|
|
4680
|
+
for (let drawId = 0; drawId < drawIdToBucket.length; drawId++) {
|
|
4681
|
+
if (drawIdToBucket[drawId] !== undefined) removeDraw(drawId);
|
|
4682
|
+
}
|
|
4683
|
+
}
|
|
4117
4684
|
for (const b of buckets) destroyBucketResources(b);
|
|
4685
|
+
if (ownsStorage) storage.dispose();
|
|
4118
4686
|
}
|
|
4119
4687
|
|
|
4120
4688
|
// Test-only escape hatch for inspecting megacall bucket state. Not
|
|
4121
4689
|
// part of the public API surface — keep cast at use-site.
|
|
4122
4690
|
const _debug = {
|
|
4691
|
+
/** Force a full heap compaction pass (attribute arena + index buffer)
|
|
4692
|
+
* regardless of the waste heuristics (bypasses the floor + 50%-live
|
|
4693
|
+
* gate), re-seat all refs, flush. Returns residual waste afterwards in
|
|
4694
|
+
* bytes (attr bytes + index elements×4; 0 when fully packed). Test-only —
|
|
4695
|
+
* drives relocation deterministically without engineering megabytes of
|
|
4696
|
+
* fragmentation. Callers should `update()` afterward to flush drawTables /
|
|
4697
|
+
* re-dispatch the partition (the render path does this). */
|
|
4698
|
+
forceCompact(): number {
|
|
4699
|
+
runCompaction(true);
|
|
4700
|
+
return arena.attrs.totalWasteBytes();
|
|
4701
|
+
},
|
|
4702
|
+
/** Current arena fragmentation waste (bytes) — covers uniforms,
|
|
4703
|
+
* attributes, and index regions (all one arena now). */
|
|
4704
|
+
attrWasteBytes(): number { return arena.attrs.totalWasteBytes(); },
|
|
4705
|
+
/** Number of arena chunks (pages) currently open. >1 means group
|
|
4706
|
+
* placement rolled past one buffer cap. */
|
|
4707
|
+
pageCount(): number { return arena.attrs.chunkCount; },
|
|
4123
4708
|
bucketsForTest(): readonly {
|
|
4124
4709
|
indirectBuf: GPUBuffer | undefined;
|
|
4125
4710
|
drawTableBuf: GPUBuffer | undefined;
|
|
@@ -4180,14 +4765,14 @@ export function buildHeapScene(
|
|
|
4180
4765
|
// multi-chunk-aware validator needs per-bucket-chunkIdx
|
|
4181
4766
|
// staging which is a bigger refactor; assert for now so
|
|
4182
4767
|
// multi-chunk callers don't get false-passes.
|
|
4183
|
-
if (arena.attrs.chunkCount > 1
|
|
4768
|
+
if (arena.attrs.chunkCount > 1) {
|
|
4184
4769
|
throw new Error(
|
|
4185
4770
|
"heapScene/validateHeap: multi-chunk arenas not yet supported by this validator (chunk-0-only). " +
|
|
4186
4771
|
"Run with the default chunk cap (no second chunk opened) for now.",
|
|
4187
4772
|
);
|
|
4188
4773
|
}
|
|
4189
4774
|
const arenaSize = arena.attrs.chunk(0).buffer.size;
|
|
4190
|
-
const indicesSize = arena
|
|
4775
|
+
const indicesSize = arenaSize; // indices live in the attr arena now
|
|
4191
4776
|
|
|
4192
4777
|
const enc = device.createCommandEncoder({ label: "validateHeap" });
|
|
4193
4778
|
const stage = (src: GPUBuffer, size: number): GPUBuffer => {
|
|
@@ -4200,7 +4785,7 @@ export function buildHeapScene(
|
|
|
4200
4785
|
};
|
|
4201
4786
|
|
|
4202
4787
|
const arenaCopy = stage(arena.attrs.chunk(0).buffer, arenaSize);
|
|
4203
|
-
const indicesCopy =
|
|
4788
|
+
const indicesCopy = arenaCopy; // same buffer; _indexStart is an element offset into it
|
|
4204
4789
|
type DC = {
|
|
4205
4790
|
bucket: typeof buckets[number];
|
|
4206
4791
|
drawHeap: GPUBuffer;
|
|
@@ -4249,7 +4834,7 @@ export function buildHeapScene(
|
|
|
4249
4834
|
// Track unique attribute-alloc refs we've already inspected so we
|
|
4250
4835
|
// don't re-validate the same shared alloc 20K times.
|
|
4251
4836
|
const attrAllocsSeen = new Set<number>();
|
|
4252
|
-
|
|
4837
|
+
const KNOWN_TYPE_IDS = new Set<number>([0, 1, 2, 3]);
|
|
4253
4838
|
|
|
4254
4839
|
let bucketIdx = 0;
|
|
4255
4840
|
for (const dc of dcs) {
|
|
@@ -4260,7 +4845,7 @@ export function buildHeapScene(
|
|
|
4260
4845
|
for (let slot = 0; slot < recordCount; slot++) {
|
|
4261
4846
|
const slotOff = slot * stride;
|
|
4262
4847
|
for (const f of dc.bucket.layout.drawHeaderFields) {
|
|
4263
|
-
if (f.name === "__layoutId") continue;
|
|
4848
|
+
if (f.name === "__layoutId" || INLINE_HEADER_UNIFORMS.has(f.name)) continue;
|
|
4264
4849
|
if (f.kind !== "uniform-ref" && f.kind !== "attribute-ref") continue;
|
|
4265
4850
|
const ref = u32[(slotOff + f.byteOffset) >>> 2];
|
|
4266
4851
|
if (ref === undefined || ref === 0) continue;
|
|
@@ -4294,18 +4879,9 @@ export function buildHeapScene(
|
|
|
4294
4879
|
? `prev=[${arenaU32[refU32 - 4]},${arenaU32[refU32 - 3]},${arenaU32[refU32 - 2]},${arenaU32[refU32 - 1]}] `
|
|
4295
4880
|
: "";
|
|
4296
4881
|
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]}]`;
|
|
4297
|
-
//
|
|
4298
|
-
//
|
|
4299
|
-
|
|
4300
|
-
const shadowChunk = arena.attrs.chunk(0);
|
|
4301
|
-
const shadowU32 = shadowChunk.peekShadowU32(ref, 8);
|
|
4302
|
-
const shadowStr = `shadow=[${shadowU32[0]},${shadowU32[1]},${shadowU32[2]},${shadowU32[3]},${shadowU32[4]},${shadowU32[5]},${shadowU32[6]},${shadowU32[7]}]`;
|
|
4303
|
-
const cpuMatches = shadowU32[0] === arenaU32[refU32]
|
|
4304
|
-
&& shadowU32[1] === arenaU32[refU32 + 1]
|
|
4305
|
-
&& shadowU32[2] === arenaU32[refU32 + 2]
|
|
4306
|
-
&& shadowU32[3] === arenaU32[refU32 + 3];
|
|
4307
|
-
const verdict = cpuMatches ? "GPU=CPU (likely CPU-write bug)" : "GPU≠CPU (likely GPU-write corruption)";
|
|
4308
|
-
push(`bucket#${bucketIdx} slot=${slot} field='${f.name}' alloc@0x${ref.toString(16)} typeId=${typeId} unknown ${verdict} ${before}${here} ${shadowStr}`);
|
|
4882
|
+
// (Mirror-less arena: no CPU shadow to cross-check against —
|
|
4883
|
+
// the GPU read-back is the only copy.)
|
|
4884
|
+
push(`bucket#${bucketIdx} slot=${slot} field='${f.name}' alloc@0x${ref.toString(16)} typeId=${typeId} unknown ${before}${here}`);
|
|
4309
4885
|
attrAllocsBad++;
|
|
4310
4886
|
continue;
|
|
4311
4887
|
}
|
|
@@ -4516,11 +5092,11 @@ export function buildHeapScene(
|
|
|
4516
5092
|
const push = (s: string) => { if (issues.length < 30) issues.push(s); };
|
|
4517
5093
|
|
|
4518
5094
|
// §3 v1: chunk-0-only like validateHeap.
|
|
4519
|
-
if (arena.attrs.chunkCount > 1
|
|
5095
|
+
if (arena.attrs.chunkCount > 1) {
|
|
4520
5096
|
throw new Error("heapScene/simulateDraws: multi-chunk arenas not yet supported (chunk-0-only).");
|
|
4521
5097
|
}
|
|
4522
5098
|
const arenaSize = arena.attrs.chunk(0).buffer.size;
|
|
4523
|
-
const indicesSize = arena
|
|
5099
|
+
const indicesSize = arenaSize; // indices live in the attr arena now
|
|
4524
5100
|
|
|
4525
5101
|
// Stage all buffers we'll need.
|
|
4526
5102
|
const enc = device.createCommandEncoder({ label: "simulateDraws" });
|
|
@@ -4532,7 +5108,7 @@ export function buildHeapScene(
|
|
|
4532
5108
|
return c;
|
|
4533
5109
|
};
|
|
4534
5110
|
const arenaCopy = stage(arena.attrs.chunk(0).buffer, arenaSize);
|
|
4535
|
-
const indicesCopy =
|
|
5111
|
+
const indicesCopy = arenaCopy; // same buffer; _indexStart is an element offset into it
|
|
4536
5112
|
type DC = {
|
|
4537
5113
|
bucket: typeof buckets[number];
|
|
4538
5114
|
drawHeap: GPUBuffer;
|
|
@@ -4592,7 +5168,7 @@ export function buildHeapScene(
|
|
|
4592
5168
|
for (const dc of dcs) {
|
|
4593
5169
|
const fs: Field[] = [];
|
|
4594
5170
|
for (const f of dc.bucket.layout.drawHeaderFields) {
|
|
4595
|
-
if (f.name === "__layoutId") continue;
|
|
5171
|
+
if (f.name === "__layoutId" || INLINE_HEADER_UNIFORMS.has(f.name)) continue;
|
|
4596
5172
|
if (f.kind !== "uniform-ref" && f.kind !== "attribute-ref") continue;
|
|
4597
5173
|
const wt = f.kind === "uniform-ref" ? f.uniformWgslType : f.attributeWgslType;
|
|
4598
5174
|
const eltF =
|
|
@@ -4803,11 +5379,11 @@ export function buildHeapScene(
|
|
|
4803
5379
|
const allocClaims: { ref: number; bytes: number; owner: string }[] = [];
|
|
4804
5380
|
|
|
4805
5381
|
// §3 v1: chunk-0-only (see validateHeap).
|
|
4806
|
-
if (arena.attrs.chunkCount > 1
|
|
5382
|
+
if (arena.attrs.chunkCount > 1) {
|
|
4807
5383
|
throw new Error("heapScene/checkTriangleCoherence: multi-chunk arenas not yet supported (chunk-0-only).");
|
|
4808
5384
|
}
|
|
4809
5385
|
const arenaBuf0 = arena.attrs.chunk(0).buffer;
|
|
4810
|
-
const indicesBuf0 = arena.
|
|
5386
|
+
const indicesBuf0 = arena.attrs.chunk(0).buffer;
|
|
4811
5387
|
// Stage arena + arena.indices once (shared across buckets).
|
|
4812
5388
|
const arenaCopy = device.createBuffer({
|
|
4813
5389
|
size: arenaBuf0.size,
|
|
@@ -5255,7 +5831,7 @@ export function buildHeapScene(
|
|
|
5255
5831
|
{ binding: 0, resource: { buffer: target.slots[0]!.drawTableBuf!.buffer } },
|
|
5256
5832
|
{ binding: 1, resource: { buffer: target.slots[0]!.firstDrawInTileBuf!.buffer } },
|
|
5257
5833
|
{ binding: 2, resource: { buffer: sampleBuf } },
|
|
5258
|
-
{ binding: 3, resource: { buffer: arena.
|
|
5834
|
+
{ binding: 3, resource: { buffer: arena.attrs.chunk(target.chunkIdx).buffer } },
|
|
5259
5835
|
{ binding: 4, resource: { buffer: outBuf } },
|
|
5260
5836
|
{ binding: 5, resource: { buffer: paramBuf } },
|
|
5261
5837
|
],
|
|
@@ -5278,7 +5854,7 @@ export function buildHeapScene(
|
|
|
5278
5854
|
sampleBuf.destroy(); outBuf.destroy(); paramBuf.destroy();
|
|
5279
5855
|
|
|
5280
5856
|
// Read drawTable + indices for CPU expectation.
|
|
5281
|
-
const idxBuf = arena.
|
|
5857
|
+
const idxBuf = arena.attrs.chunk(target.chunkIdx).buffer;
|
|
5282
5858
|
const indicesCopy2 = device.createBuffer({
|
|
5283
5859
|
size: idxBuf.size,
|
|
5284
5860
|
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
|