@aardworx/wombat.rendering 0.19.24 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/elementType.d.ts +2 -0
- package/dist/core/elementType.d.ts.map +1 -1
- package/dist/core/elementType.js +10 -0
- package/dist/core/elementType.js.map +1 -1
- package/dist/core/provider.d.ts.map +1 -1
- package/dist/core/provider.js +65 -32
- package/dist/core/provider.js.map +1 -1
- package/dist/core/renderObject.d.ts +18 -0
- package/dist/core/renderObject.d.ts.map +1 -1
- package/dist/resources/preparedRenderObject.d.ts.map +1 -1
- package/dist/resources/preparedRenderObject.js +28 -5
- package/dist/resources/preparedRenderObject.js.map +1 -1
- package/dist/resources/uniformBuffer.d.ts.map +1 -1
- package/dist/resources/uniformBuffer.js +3 -1
- package/dist/resources/uniformBuffer.js.map +1 -1
- package/dist/runtime/derivedModes/modeKeyCpu.d.ts +2 -12
- package/dist/runtime/derivedModes/modeKeyCpu.d.ts.map +1 -1
- package/dist/runtime/derivedModes/modeKeyCpu.js +30 -13
- package/dist/runtime/derivedModes/modeKeyCpu.js.map +1 -1
- package/dist/runtime/derivedModes/partitionDispatcher.d.ts +11 -0
- package/dist/runtime/derivedModes/partitionDispatcher.d.ts.map +1 -1
- package/dist/runtime/derivedModes/partitionDispatcher.js +31 -0
- package/dist/runtime/derivedModes/partitionDispatcher.js.map +1 -1
- package/dist/runtime/derivedUniforms/codegen.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/codegen.js +34 -13
- package/dist/runtime/derivedUniforms/codegen.js.map +1 -1
- package/dist/runtime/derivedUniforms/dispatch.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/dispatch.js +6 -0
- package/dist/runtime/derivedUniforms/dispatch.js.map +1 -1
- package/dist/runtime/derivedUniforms/records.d.ts +15 -0
- package/dist/runtime/derivedUniforms/records.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/records.js +81 -14
- package/dist/runtime/derivedUniforms/records.js.map +1 -1
- package/dist/runtime/derivedUniforms/sceneIntegration.d.ts +6 -0
- package/dist/runtime/derivedUniforms/sceneIntegration.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/sceneIntegration.js +18 -4
- package/dist/runtime/derivedUniforms/sceneIntegration.js.map +1 -1
- package/dist/runtime/flattenTree.d.ts.map +1 -1
- package/dist/runtime/flattenTree.js +46 -2
- package/dist/runtime/flattenTree.js.map +1 -1
- package/dist/runtime/heapAdapter.d.ts +3 -1
- package/dist/runtime/heapAdapter.d.ts.map +1 -1
- package/dist/runtime/heapAdapter.js +45 -4
- package/dist/runtime/heapAdapter.js.map +1 -1
- package/dist/runtime/heapDecoder.d.ts.map +1 -1
- package/dist/runtime/heapDecoder.js +8 -3
- package/dist/runtime/heapDecoder.js.map +1 -1
- package/dist/runtime/heapEffect.d.ts +36 -1
- package/dist/runtime/heapEffect.d.ts.map +1 -1
- package/dist/runtime/heapEffect.js +29 -2
- package/dist/runtime/heapEffect.js.map +1 -1
- package/dist/runtime/heapEffectIR.d.ts.map +1 -1
- package/dist/runtime/heapEffectIR.js +51 -11
- package/dist/runtime/heapEffectIR.js.map +1 -1
- package/dist/runtime/heapEligibility.d.ts.map +1 -1
- package/dist/runtime/heapEligibility.js +64 -12
- package/dist/runtime/heapEligibility.js.map +1 -1
- package/dist/runtime/heapIrBuilders.d.ts +8 -0
- package/dist/runtime/heapIrBuilders.d.ts.map +1 -1
- package/dist/runtime/heapIrBuilders.js +47 -2
- package/dist/runtime/heapIrBuilders.js.map +1 -1
- package/dist/runtime/heapScene/chunkedArena.d.ts +20 -28
- package/dist/runtime/heapScene/chunkedArena.d.ts.map +1 -1
- package/dist/runtime/heapScene/chunkedArena.js +28 -91
- package/dist/runtime/heapScene/chunkedArena.js.map +1 -1
- package/dist/runtime/heapScene/freelist.d.ts +1 -1
- package/dist/runtime/heapScene/freelist.js +3 -3
- package/dist/runtime/heapScene/freelist.js.map +1 -1
- package/dist/runtime/heapScene/pools.d.ts +103 -59
- package/dist/runtime/heapScene/pools.d.ts.map +1 -1
- package/dist/runtime/heapScene/pools.js +335 -194
- package/dist/runtime/heapScene/pools.js.map +1 -1
- package/dist/runtime/heapScene.d.ts +125 -1
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +671 -126
- package/dist/runtime/heapScene.js.map +1 -1
- package/dist/runtime/heapShaderFamily.d.ts.map +1 -1
- package/dist/runtime/heapShaderFamily.js +19 -0
- package/dist/runtime/heapShaderFamily.js.map +1 -1
- package/dist/runtime/hybridScene.d.ts +6 -0
- package/dist/runtime/hybridScene.d.ts.map +1 -1
- package/dist/runtime/hybridScene.js +36 -3
- package/dist/runtime/hybridScene.js.map +1 -1
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/renderTask.d.ts +6 -0
- package/dist/runtime/renderTask.d.ts.map +1 -1
- package/dist/runtime/renderTask.js +3 -0
- package/dist/runtime/renderTask.js.map +1 -1
- package/dist/runtime/runtime.d.ts +15 -0
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +3 -0
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.d.ts +19 -2
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.d.ts.map +1 -1
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.js +37 -6
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.js.map +1 -1
- package/dist/runtime/textureAtlas/atlasPool.d.ts +5 -0
- package/dist/runtime/textureAtlas/atlasPool.d.ts.map +1 -1
- package/dist/runtime/textureAtlas/atlasPool.js +25 -3
- package/dist/runtime/textureAtlas/atlasPool.js.map +1 -1
- package/dist/window/loop.d.ts.map +1 -1
- package/dist/window/loop.js +31 -4
- package/dist/window/loop.js.map +1 -1
- package/package.json +3 -3
- package/src/core/elementType.ts +10 -0
- package/src/core/provider.ts +56 -28
- package/src/core/renderObject.ts +18 -0
- package/src/resources/preparedRenderObject.ts +29 -6
- package/src/resources/uniformBuffer.ts +3 -1
- package/src/runtime/derivedModes/modeKeyCpu.ts +26 -11
- package/src/runtime/derivedModes/partitionDispatcher.ts +30 -0
- package/src/runtime/derivedUniforms/codegen.ts +34 -13
- package/src/runtime/derivedUniforms/dispatch.ts +6 -0
- package/src/runtime/derivedUniforms/records.ts +70 -15
- package/src/runtime/derivedUniforms/sceneIntegration.ts +19 -4
- package/src/runtime/flattenTree.ts +47 -2
- package/src/runtime/heapAdapter.ts +43 -5
- package/src/runtime/heapDecoder.ts +8 -3
- package/src/runtime/heapEffect.ts +44 -2
- package/src/runtime/heapEffectIR.ts +54 -11
- package/src/runtime/heapEligibility.ts +39 -12
- package/src/runtime/heapIrBuilders.ts +50 -2
- package/src/runtime/heapScene/chunkedArena.ts +29 -94
- package/src/runtime/heapScene/freelist.ts +3 -3
- package/src/runtime/heapScene/pools.ts +340 -218
- package/src/runtime/heapScene.ts +826 -142
- package/src/runtime/heapShaderFamily.ts +21 -0
- package/src/runtime/hybridScene.ts +43 -3
- package/src/runtime/index.ts +5 -0
- package/src/runtime/renderTask.ts +9 -0
- package/src/runtime/runtime.ts +18 -0
- package/src/runtime/textureAtlas/atlasMipGutterKernel.ts +52 -3
- package/src/runtime/textureAtlas/atlasPool.ts +28 -2
- package/src/window/loop.ts +32 -4
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
// classes here are tightly interleaved:
|
|
4
4
|
//
|
|
5
5
|
// AttributeArena — byte-bump over a STORAGE GrowBuffer
|
|
6
|
-
//
|
|
6
|
+
// (index arrays are AttributeArena regions too — VS storage-decodes them)
|
|
7
7
|
// DrawHeap — slot-indexed allocator over a STORAGE GrowBuffer
|
|
8
8
|
// (one slot per RO drawHeader)
|
|
9
9
|
// UniformPool — aval-keyed refcounted alloc on top of AttributeArena
|
|
10
|
-
// IndexPool — aval-keyed refcounted alloc on top of
|
|
10
|
+
// IndexPool — aval-keyed refcounted alloc on top of AttributeArena,
|
|
11
11
|
// with value-tuple dedup for constant avals (§5b).
|
|
12
12
|
//
|
|
13
13
|
// Plus the per-allocation header constants used by all of them and
|
|
@@ -17,8 +17,7 @@
|
|
|
17
17
|
import { AVal } from "@aardworx/wombat.adaptive";
|
|
18
18
|
import { GrowBuffer, ALIGN16, DEFAULT_MAX_BUFFER_BYTES } from "./growBuffer.js";
|
|
19
19
|
import { Freelist } from "./freelist.js";
|
|
20
|
-
import {
|
|
21
|
-
import { ChunkedAttributeArena, ChunkedIndexAllocator } from "./chunkedArena.js";
|
|
20
|
+
import { ChunkedAttributeArena } from "./chunkedArena.js";
|
|
22
21
|
// ─── Debug toggles ─────────────────────────────────────────────────────
|
|
23
22
|
/** Opt-in O(live-allocs) overlap validation on every alloc. Set
|
|
24
23
|
* `globalThis.__wombatDebugAllocOverlap = true` BEFORE scene creation
|
|
@@ -33,8 +32,16 @@ const DEBUG_ALLOC_OVERLAP = globalThis
|
|
|
33
32
|
* up for future vec4 reads). */
|
|
34
33
|
export const ALLOC_HEADER_BYTES = 8;
|
|
35
34
|
export const ALLOC_HEADER_PAD_TO = 16;
|
|
36
|
-
/**
|
|
35
|
+
/** Default fragmentation-waste floor (bytes) below which `AttributeArena`
|
|
36
|
+
* declines to compact — relocating a few KB isn't worth a GPU copy +
|
|
37
|
+
* ref re-seat. Matches Aardvark's 4 MiB compaction floor. */
|
|
38
|
+
export const COMPACTION_WASTE_FLOOR_BYTES = 4 * 1024 * 1024;
|
|
39
|
+
/** Encoding-tag enum (low 16 bits of typeId). The generated VS decode
|
|
40
|
+
* branches on this per field (2-arm select; header word 0 shares the
|
|
41
|
+
* cache line with the broadcast length in word 1). */
|
|
37
42
|
export const ENC_V3F_TIGHT = 1; // tightly-packed array of vec3<f32> (12 B/elt)
|
|
43
|
+
export const ENC_OCT32 = 2; // oct-packed unit vectors, 2×unorm16 in one u32 (4 B/elt)
|
|
44
|
+
export const ENC_C4B = 3; // RGBA8-unorm colors, one u32/elt → unpack4x8unorm (4 B/elt)
|
|
38
45
|
/** Semantic-tag enum (high 16 bits of typeId). Optional metadata —
|
|
39
46
|
* the shader doesn't branch on this. */
|
|
40
47
|
export const SEM_POSITIONS = 1;
|
|
@@ -48,37 +55,114 @@ export const SEM_NORMALS = 2;
|
|
|
48
55
|
* an aval still share the underlying allocation.
|
|
49
56
|
*/
|
|
50
57
|
export class UniformPool {
|
|
58
|
+
/** Value is a single `PoolEntry` (the overwhelmingly common case —
|
|
59
|
+
* one aval, one chunk) or a `Map<chunkIdx, PoolEntry>` once the
|
|
60
|
+
* same aval is allocated in a second chunk. The flat form saves a
|
|
61
|
+
* whole inner `Map` per aval, which at heap scale (several avals
|
|
62
|
+
* per draw) is megabytes of JS heap. */
|
|
51
63
|
byAval = new Map();
|
|
64
|
+
getEntry(av, chunkIdx) {
|
|
65
|
+
const v = this.byAval.get(av);
|
|
66
|
+
if (v === undefined)
|
|
67
|
+
return undefined;
|
|
68
|
+
if (v instanceof Map)
|
|
69
|
+
return v.get(chunkIdx);
|
|
70
|
+
return v.chunkIdx === chunkIdx ? v : undefined;
|
|
71
|
+
}
|
|
72
|
+
setEntry(av, e) {
|
|
73
|
+
const v = this.byAval.get(av);
|
|
74
|
+
if (v === undefined) {
|
|
75
|
+
this.byAval.set(av, e);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (v instanceof Map) {
|
|
79
|
+
v.set(e.chunkIdx, e);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const m = new Map();
|
|
83
|
+
m.set(v.chunkIdx, v);
|
|
84
|
+
m.set(e.chunkIdx, e);
|
|
85
|
+
this.byAval.set(av, m);
|
|
86
|
+
}
|
|
87
|
+
deleteEntry(av, chunkIdx) {
|
|
88
|
+
const v = this.byAval.get(av);
|
|
89
|
+
if (v === undefined)
|
|
90
|
+
return;
|
|
91
|
+
if (v instanceof Map) {
|
|
92
|
+
v.delete(chunkIdx);
|
|
93
|
+
if (v.size === 1) {
|
|
94
|
+
this.byAval.set(av, v.values().next().value);
|
|
95
|
+
}
|
|
96
|
+
else if (v.size === 0)
|
|
97
|
+
this.byAval.delete(av);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (v.chunkIdx === chunkIdx)
|
|
101
|
+
this.byAval.delete(av);
|
|
102
|
+
}
|
|
103
|
+
*entriesOf(av) {
|
|
104
|
+
const v = this.byAval.get(av);
|
|
105
|
+
if (v === undefined)
|
|
106
|
+
return;
|
|
107
|
+
if (v instanceof Map)
|
|
108
|
+
yield* v.values();
|
|
109
|
+
else
|
|
110
|
+
yield v;
|
|
111
|
+
}
|
|
52
112
|
has(av, chunkIdx) {
|
|
53
|
-
return this.
|
|
113
|
+
return this.getEntry(av, chunkIdx) !== undefined;
|
|
54
114
|
}
|
|
55
115
|
/** True when the pool holds at least one entry for `av` in any
|
|
56
116
|
* chunk. Used by the scene's `inputChanged` dispatch to decide
|
|
57
117
|
* whether a marking aval is one of our tracked allocations. */
|
|
58
118
|
hasAny(av) {
|
|
59
|
-
|
|
119
|
+
const v = this.byAval.get(av);
|
|
120
|
+
return v !== undefined && (!(v instanceof Map) || v.size > 0);
|
|
60
121
|
}
|
|
61
122
|
entry(av, chunkIdx) {
|
|
62
|
-
return this.
|
|
123
|
+
return this.getEntry(av, chunkIdx);
|
|
63
124
|
}
|
|
64
125
|
/** First chunk this aval is allocated in, if any. Used by `addRO`
|
|
65
126
|
* to prefer co-locating a new RO with its already-allocated
|
|
66
127
|
* shared inputs (avoids unnecessary duplication). */
|
|
67
128
|
firstChunkContaining(av) {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
129
|
+
for (const e of this.entriesOf(av))
|
|
130
|
+
return e.chunkIdx;
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Unkeyed allocation for slots the GPU overwrites every dirty frame
|
|
135
|
+
* (§7 derived-uniform outputs). No aval, no `byAval` entry, no
|
|
136
|
+
* sharing — the caller keeps the ref in its drawHeader cell (which
|
|
137
|
+
* the compaction remap re-seats) and frees it with `releaseRaw`.
|
|
138
|
+
* Avoids a placeholder `AVal.constant` + pool entry per draw.
|
|
139
|
+
*/
|
|
140
|
+
allocRaw(arena, chunkIdx, dataBytes, typeId, length) {
|
|
141
|
+
const r = arena.alloc(dataBytes, chunkIdx);
|
|
142
|
+
if (r.chunkIdx !== chunkIdx) {
|
|
143
|
+
throw new Error(`UniformPool.allocRaw: allocator spilled from chunk ${chunkIdx} to chunk ${r.chunkIdx} ` +
|
|
144
|
+
`(${dataBytes} bytes). Caller's bucket is bound to chunk ${chunkIdx}.`);
|
|
145
|
+
}
|
|
146
|
+
const allocBytes = ALIGN16(ALLOC_HEADER_PAD_TO + dataBytes);
|
|
147
|
+
const buf = new ArrayBuffer(allocBytes);
|
|
148
|
+
const u32 = new Uint32Array(buf);
|
|
149
|
+
u32[0] = typeId;
|
|
150
|
+
u32[1] = length;
|
|
151
|
+
u32[2] = length > 0 ? Math.floor(dataBytes / length) : 0;
|
|
152
|
+
arena.write(chunkIdx, r.off, new Uint8Array(buf));
|
|
153
|
+
return r.off;
|
|
154
|
+
}
|
|
155
|
+
releaseRaw(arena, chunkIdx, ref, dataBytes) {
|
|
156
|
+
arena.release(chunkIdx, ref, dataBytes);
|
|
72
157
|
}
|
|
73
158
|
/**
|
|
74
159
|
* Acquire (or share) an allocation for `aval` in `chunkIdx`.
|
|
75
160
|
* Returns the byte offset within that chunk's GPUBuffer. If a
|
|
76
161
|
* fresh allocation is made, the value is packed + uploaded
|
|
77
|
-
* immediately
|
|
162
|
+
* immediately to the chunk's GPU buffer (mirror-less).
|
|
78
163
|
*/
|
|
79
164
|
acquire(device, arena, av, chunkIdx, value, dataBytes, typeId, length, pack) {
|
|
80
|
-
|
|
81
|
-
const existing = byChunk?.get(chunkIdx);
|
|
165
|
+
const existing = this.getEntry(av, chunkIdx);
|
|
82
166
|
if (existing !== undefined) {
|
|
83
167
|
existing.refcount++;
|
|
84
168
|
return existing.ref;
|
|
@@ -110,18 +194,13 @@ export class UniformPool {
|
|
|
110
194
|
pack(value, f32, ALLOC_HEADER_PAD_TO / 4);
|
|
111
195
|
arena.write(finalChunk, r.off, new Uint8Array(buf));
|
|
112
196
|
void device;
|
|
113
|
-
|
|
114
|
-
byChunk = new Map();
|
|
115
|
-
this.byAval.set(av, byChunk);
|
|
116
|
-
}
|
|
117
|
-
byChunk.set(finalChunk, {
|
|
197
|
+
this.setEntry(av, {
|
|
118
198
|
chunkIdx: finalChunk, ref: r.off, dataBytes, typeId, pack, refcount: 1,
|
|
119
199
|
});
|
|
120
200
|
return r.off;
|
|
121
201
|
}
|
|
122
202
|
release(arena, av, chunkIdx) {
|
|
123
|
-
const
|
|
124
|
-
const e = byChunk?.get(chunkIdx);
|
|
203
|
+
const e = this.getEntry(av, chunkIdx);
|
|
125
204
|
if (e === undefined)
|
|
126
205
|
return;
|
|
127
206
|
e.refcount--;
|
|
@@ -138,20 +217,30 @@ export class UniformPool {
|
|
|
138
217
|
// 16 B inside the previous alloc → overlapping allocations →
|
|
139
218
|
// garbage `typeId`/`length` in the attribute header.
|
|
140
219
|
arena.release(e.chunkIdx, e.ref, e.dataBytes);
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
220
|
+
this.deleteEntry(av, chunkIdx);
|
|
221
|
+
}
|
|
222
|
+
/** Re-seat every entry's `ref` in `chunkIdx` after a waste-triggered
|
|
223
|
+
* arena compaction relocated allocations. `remap` maps OLD→NEW byte
|
|
224
|
+
* offset. Entries whose ref didn't move are left as-is. O(entries). */
|
|
225
|
+
remapRefs(chunkIdx, remap) {
|
|
226
|
+
if (remap.size === 0)
|
|
227
|
+
return;
|
|
228
|
+
for (const v of this.byAval.values()) {
|
|
229
|
+
const e = v instanceof Map ? v.get(chunkIdx) : (v.chunkIdx === chunkIdx ? v : undefined);
|
|
230
|
+
if (e === undefined)
|
|
231
|
+
continue;
|
|
232
|
+
const nn = remap.get(e.ref);
|
|
233
|
+
if (nn !== undefined)
|
|
234
|
+
e.ref = nn;
|
|
235
|
+
}
|
|
144
236
|
}
|
|
145
237
|
/** Re-pack one entry's data region into every chunk that holds
|
|
146
238
|
* this aval. When an aval is shared across N chunks (each chunk
|
|
147
239
|
* has its own duplicate alloc), every duplicate needs the
|
|
148
240
|
* refresh so all the chunks' ROs see the new value. */
|
|
149
241
|
repack(device, arena, av, val) {
|
|
150
|
-
const byChunk = this.byAval.get(av);
|
|
151
|
-
if (byChunk === undefined)
|
|
152
|
-
return;
|
|
153
242
|
let dst;
|
|
154
|
-
for (const e of
|
|
243
|
+
for (const e of this.entriesOf(av)) {
|
|
155
244
|
if (dst === undefined || dst.length !== e.dataBytes / 4) {
|
|
156
245
|
dst = new Float32Array(e.dataBytes / 4);
|
|
157
246
|
}
|
|
@@ -163,18 +252,15 @@ export class UniformPool {
|
|
|
163
252
|
/** Total bytes touched by `repack` for this aval — sum across
|
|
164
253
|
* chunks. Used by the dirty-bytes diagnostics. */
|
|
165
254
|
totalDataBytes(av) {
|
|
166
|
-
const byChunk = this.byAval.get(av);
|
|
167
|
-
if (byChunk === undefined)
|
|
168
|
-
return 0;
|
|
169
255
|
let s = 0;
|
|
170
|
-
for (const e of
|
|
256
|
+
for (const e of this.entriesOf(av))
|
|
171
257
|
s += e.dataBytes;
|
|
172
258
|
return s;
|
|
173
259
|
}
|
|
174
260
|
}
|
|
175
261
|
// ─── IndexPool ─────────────────────────────────────────────────────────
|
|
176
262
|
/**
|
|
177
|
-
* Aval-keyed pool over the
|
|
263
|
+
* Aval-keyed pool over the (attribute) arena. Two draws referencing
|
|
178
264
|
* the same `Uint32Array` (or aval thereof) share an index range —
|
|
179
265
|
* 19K instanced clones of the same mesh share one allocation, one
|
|
180
266
|
* upload.
|
|
@@ -187,8 +273,48 @@ export class UniformPool {
|
|
|
187
273
|
*/
|
|
188
274
|
export class IndexPool {
|
|
189
275
|
/** Per-aval, per-chunk bookkeeping. Same aval acquired in two
|
|
190
|
-
* chunks gets two entries (accepted §3 duplication).
|
|
276
|
+
* chunks gets two entries (accepted §3 duplication). Value is a
|
|
277
|
+
* single binding (common case) or a Map<chunkIdx, binding> once
|
|
278
|
+
* the aval spans chunks — same flat-form saving as UniformPool. */
|
|
191
279
|
byAval = new Map();
|
|
280
|
+
getBinding(av, chunkIdx) {
|
|
281
|
+
const v = this.byAval.get(av);
|
|
282
|
+
if (v === undefined)
|
|
283
|
+
return undefined;
|
|
284
|
+
if (v instanceof Map)
|
|
285
|
+
return v.get(chunkIdx);
|
|
286
|
+
return v.entry.chunkIdx === chunkIdx ? v : undefined;
|
|
287
|
+
}
|
|
288
|
+
setBinding(av, b) {
|
|
289
|
+
const v = this.byAval.get(av);
|
|
290
|
+
if (v === undefined) {
|
|
291
|
+
this.byAval.set(av, b);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (v instanceof Map) {
|
|
295
|
+
v.set(b.entry.chunkIdx, b);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const m = new Map();
|
|
299
|
+
m.set(v.entry.chunkIdx, v);
|
|
300
|
+
m.set(b.entry.chunkIdx, b);
|
|
301
|
+
this.byAval.set(av, m);
|
|
302
|
+
}
|
|
303
|
+
deleteBinding(av, chunkIdx) {
|
|
304
|
+
const v = this.byAval.get(av);
|
|
305
|
+
if (v === undefined)
|
|
306
|
+
return;
|
|
307
|
+
if (v instanceof Map) {
|
|
308
|
+
v.delete(chunkIdx);
|
|
309
|
+
if (v.size === 1)
|
|
310
|
+
this.byAval.set(av, v.values().next().value);
|
|
311
|
+
else if (v.size === 0)
|
|
312
|
+
this.byAval.delete(av);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (v.entry.chunkIdx === chunkIdx)
|
|
316
|
+
this.byAval.delete(av);
|
|
317
|
+
}
|
|
192
318
|
/** Per-chunk constant-aval value-dedup (§5b in-chunk). */
|
|
193
319
|
byValueKey = new Map();
|
|
194
320
|
bufferIds = new WeakMap();
|
|
@@ -202,14 +328,15 @@ export class IndexPool {
|
|
|
202
328
|
return id;
|
|
203
329
|
}
|
|
204
330
|
firstChunkContaining(av) {
|
|
205
|
-
const
|
|
206
|
-
if (
|
|
331
|
+
const v = this.byAval.get(av);
|
|
332
|
+
if (v === undefined)
|
|
207
333
|
return undefined;
|
|
208
|
-
|
|
334
|
+
if (v instanceof Map)
|
|
335
|
+
return v.size > 0 ? v.keys().next().value : undefined;
|
|
336
|
+
return v.entry.chunkIdx;
|
|
209
337
|
}
|
|
210
|
-
acquire(device,
|
|
211
|
-
|
|
212
|
-
const bound = byChunk?.get(chunkIdx);
|
|
338
|
+
acquire(device, arena, av, chunkIdx, arr) {
|
|
339
|
+
const bound = this.getBinding(av, chunkIdx);
|
|
213
340
|
if (bound !== undefined) {
|
|
214
341
|
bound.perAvalCount++;
|
|
215
342
|
bound.entry.totalRefcount++;
|
|
@@ -219,51 +346,77 @@ export class IndexPool {
|
|
|
219
346
|
if (av.isConstant) {
|
|
220
347
|
// Value-dedup is scoped per chunk — two constant avals in the
|
|
221
348
|
// same chunk collapse to one allocation; two constant avals in
|
|
222
|
-
// DIFFERENT chunks each get their own (
|
|
223
|
-
//
|
|
349
|
+
// DIFFERENT chunks each get their own (the index range lives in
|
|
350
|
+
// the chunk's GPUBuffer).
|
|
224
351
|
valueKey = `${chunkIdx}:${this.bufferIdOf(arr.buffer)}:${arr.byteOffset}:${arr.byteLength}`;
|
|
225
352
|
const shared = this.byValueKey.get(valueKey);
|
|
226
353
|
if (shared !== undefined) {
|
|
227
354
|
shared.totalRefcount++;
|
|
228
|
-
|
|
229
|
-
byChunk = new Map();
|
|
230
|
-
this.byAval.set(av, byChunk);
|
|
231
|
-
}
|
|
232
|
-
byChunk.set(chunkIdx, { entry: shared, perAvalCount: 1 });
|
|
355
|
+
this.setBinding(av, { entry: shared, perAvalCount: 1 });
|
|
233
356
|
return { chunkIdx: shared.chunkIdx, firstIndex: shared.firstIndex, count: shared.count };
|
|
234
357
|
}
|
|
235
358
|
}
|
|
236
|
-
|
|
237
|
-
indices
|
|
359
|
+
// Index data lives in the SAME arena as attributes/uniforms (the heap VS
|
|
360
|
+
// storage-decodes indices — `indexStorage[_indexStart + i]` — so they need
|
|
361
|
+
// no separate index buffer). Allocate a normal header+data region; the
|
|
362
|
+
// raw u32 indices sit at `ref + 16`, so the element offset the drawTable
|
|
363
|
+
// stores is `(ref + 16) >> 2`. The header is unread by index gathers but
|
|
364
|
+
// kept for a uniform region layout (and so compaction treats it like any
|
|
365
|
+
// other region).
|
|
366
|
+
const byteLen = arr.byteLength;
|
|
367
|
+
const r = arena.alloc(byteLen, chunkIdx);
|
|
368
|
+
const dataOff = r.off + ALLOC_HEADER_PAD_TO;
|
|
369
|
+
const hdr = new Uint32Array([0, arr.length, 4, 0]); // (typeId, length, strideBytes, pad)
|
|
370
|
+
arena.write(r.chunkIdx, r.off, new Uint8Array(hdr.buffer));
|
|
371
|
+
arena.write(r.chunkIdx, dataOff, new Uint8Array(arr.buffer, arr.byteOffset, byteLen));
|
|
238
372
|
void device;
|
|
373
|
+
const firstIndex = dataOff >>> 2;
|
|
239
374
|
const entry = {
|
|
240
|
-
chunkIdx: r.chunkIdx,
|
|
375
|
+
chunkIdx: r.chunkIdx, ref: r.off, firstIndex, count: arr.length,
|
|
241
376
|
totalRefcount: 1, valueKey,
|
|
242
377
|
};
|
|
243
|
-
|
|
244
|
-
byChunk = new Map();
|
|
245
|
-
this.byAval.set(av, byChunk);
|
|
246
|
-
}
|
|
247
|
-
byChunk.set(r.chunkIdx, { entry, perAvalCount: 1 });
|
|
378
|
+
this.setBinding(av, { entry, perAvalCount: 1 });
|
|
248
379
|
if (valueKey !== undefined)
|
|
249
380
|
this.byValueKey.set(valueKey, entry);
|
|
250
|
-
return { chunkIdx: r.chunkIdx, firstIndex
|
|
381
|
+
return { chunkIdx: r.chunkIdx, firstIndex, count: arr.length };
|
|
382
|
+
}
|
|
383
|
+
/** The allocation's byte header offset (`ref`) of `av`'s entry in `chunkIdx`,
|
|
384
|
+
* or undefined if not allocated there. The heap compaction pass looks the
|
|
385
|
+
* region up in the arena remap by this ref to compute the draw's new
|
|
386
|
+
* `_indexStart`. */
|
|
387
|
+
baseFor(av, chunkIdx) {
|
|
388
|
+
return this.getBinding(av, chunkIdx)?.entry.ref;
|
|
389
|
+
}
|
|
390
|
+
/** Re-seat every entry after a compaction relocated its region. `remap` maps
|
|
391
|
+
* OLD→NEW byte header offset (`ref`); `firstIndex` is recomputed from the
|
|
392
|
+
* new ref. Entries are shared by value-dedup, so each is visited once. */
|
|
393
|
+
remapRefs(chunkIdx, remap) {
|
|
394
|
+
if (remap.size === 0)
|
|
395
|
+
return;
|
|
396
|
+
const seen = new Set();
|
|
397
|
+
for (const v of this.byAval.values()) {
|
|
398
|
+
const bound = v instanceof Map ? v.get(chunkIdx) : (v.entry.chunkIdx === chunkIdx ? v : undefined);
|
|
399
|
+
if (bound === undefined || seen.has(bound.entry))
|
|
400
|
+
continue;
|
|
401
|
+
seen.add(bound.entry);
|
|
402
|
+
const nn = remap.get(bound.entry.ref);
|
|
403
|
+
if (nn !== undefined) {
|
|
404
|
+
bound.entry.ref = nn;
|
|
405
|
+
bound.entry.firstIndex = (nn + ALLOC_HEADER_PAD_TO) >>> 2;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
251
408
|
}
|
|
252
|
-
release(
|
|
253
|
-
const
|
|
254
|
-
const bound = byChunk?.get(chunkIdx);
|
|
409
|
+
release(arena, av, chunkIdx) {
|
|
410
|
+
const bound = this.getBinding(av, chunkIdx);
|
|
255
411
|
if (bound === undefined)
|
|
256
412
|
return;
|
|
257
413
|
bound.perAvalCount--;
|
|
258
414
|
bound.entry.totalRefcount--;
|
|
259
|
-
if (bound.perAvalCount === 0)
|
|
260
|
-
|
|
261
|
-
if (byChunk.size === 0)
|
|
262
|
-
this.byAval.delete(av);
|
|
263
|
-
}
|
|
415
|
+
if (bound.perAvalCount === 0)
|
|
416
|
+
this.deleteBinding(av, chunkIdx);
|
|
264
417
|
if (bound.entry.totalRefcount > 0)
|
|
265
418
|
return;
|
|
266
|
-
|
|
419
|
+
arena.release(bound.entry.chunkIdx, bound.entry.ref, bound.entry.count * 4);
|
|
267
420
|
if (bound.entry.valueKey !== undefined) {
|
|
268
421
|
this.byValueKey.delete(bound.entry.valueKey);
|
|
269
422
|
}
|
|
@@ -320,52 +473,43 @@ export class DrawHeap {
|
|
|
320
473
|
* onto a sorted free list with coalesce on insert.
|
|
321
474
|
*/
|
|
322
475
|
export class AttributeArena {
|
|
476
|
+
device;
|
|
323
477
|
buf;
|
|
324
478
|
cursor = 0;
|
|
479
|
+
/** Sum of `allocBytes` over all live allocations. `cursor - liveBytes`
|
|
480
|
+
* is the fragmentation waste below the high-water mark — what
|
|
481
|
+
* `compact()` reclaims. */
|
|
482
|
+
liveBytes = 0;
|
|
325
483
|
freelist = new Freelist();
|
|
326
484
|
/** DEBUG: tracks every live allocation by start byte → size. Set
|
|
327
485
|
* on alloc, cleared on release. Used by `assertNoOverlap()` to
|
|
328
486
|
* detect allocator returning overlapping ranges. Enabled in
|
|
329
487
|
* development; cheap (one Map insert/delete per alloc). */
|
|
330
488
|
liveAllocs = new Map();
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
489
|
+
// MIRROR-LESS: no CPU shadow of the arena. Writes go straight to
|
|
490
|
+
// `queue.writeBuffer` (queue-ordered with submits, so they serialize
|
|
491
|
+
// correctly against GrowBuffer's grow-copy and the compaction bounce —
|
|
492
|
+
// both of which preserve contents GPU-side). This halves the heap's
|
|
493
|
+
// host-memory footprint: the source-of-truth for values is the user's
|
|
494
|
+
// avals (they must stay alive — changeability is the point), and the
|
|
495
|
+
// GPU buffer is the render copy; a third full-size CPU mirror bought
|
|
496
|
+
// only dirty-range batching + debug readback comparisons.
|
|
497
|
+
constructor(device, buf) {
|
|
498
|
+
this.device = device;
|
|
338
499
|
this.buf = buf;
|
|
339
|
-
this.shadow = new Uint8Array(buf.capacity);
|
|
340
|
-
buf.onResize(() => {
|
|
341
|
-
const grown = new Uint8Array(buf.capacity);
|
|
342
|
-
grown.set(this.shadow);
|
|
343
|
-
this.shadow = grown;
|
|
344
|
-
});
|
|
345
500
|
}
|
|
346
501
|
get buffer() { return this.buf.buffer; }
|
|
347
502
|
get capacity() { return this.buf.capacity; }
|
|
348
503
|
get usedBytes() { return this.cursor; }
|
|
349
|
-
/** Read 4 u32s from the CPU shadow starting at byte `off`. Used by
|
|
350
|
-
* the validator to compare against the GPU's read-back values:
|
|
351
|
-
* if CPU shadow is correct but GPU shows garbage → the corruption
|
|
352
|
-
* came from a GPU-side write (a compute kernel writing to a wrong
|
|
353
|
-
* byte). If both differ from what pool.acquire wrote → CPU-side
|
|
354
|
-
* write path is the culprit. */
|
|
355
|
-
peekShadowU32(off, count) {
|
|
356
|
-
return new Uint32Array(this.shadow.buffer, this.shadow.byteOffset + off, count);
|
|
357
|
-
}
|
|
358
504
|
write(dst, data) {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
if (this.dirty.isEmpty)
|
|
364
|
-
return;
|
|
365
|
-
this.dirty.drain((start, end) => {
|
|
366
|
-
device.queue.writeBuffer(this.buf.buffer, start, this.shadow.buffer, this.shadow.byteOffset + start, end - start);
|
|
367
|
-
});
|
|
505
|
+
// Immediate upload; Chrome's queue staging does the batching. The
|
|
506
|
+
// write targets the CURRENT buffer handle — a later grow copies it
|
|
507
|
+
// forward (queue-ordered), so ordering stays correct.
|
|
508
|
+
this.device.queue.writeBuffer(this.buf.buffer, dst, data);
|
|
368
509
|
}
|
|
510
|
+
/** No-op — kept for call-site compatibility (mirror-less arena uploads
|
|
511
|
+
* in `write`). */
|
|
512
|
+
flush(_device) { }
|
|
369
513
|
/**
|
|
370
514
|
* Allocate space for one attribute. Returns the byte ref (offset
|
|
371
515
|
* to the header — data lives at ref + 16).
|
|
@@ -379,6 +523,7 @@ export class AttributeArena {
|
|
|
379
523
|
const reused = this.freelist.alloc(allocBytes);
|
|
380
524
|
if (reused !== undefined) {
|
|
381
525
|
this.recordAlloc(reused, allocBytes, "freelist");
|
|
526
|
+
this.liveBytes += allocBytes;
|
|
382
527
|
return reused;
|
|
383
528
|
}
|
|
384
529
|
const next = this.cursor + allocBytes;
|
|
@@ -389,6 +534,7 @@ export class AttributeArena {
|
|
|
389
534
|
this.buf.setUsed(next);
|
|
390
535
|
const ref = next - allocBytes;
|
|
391
536
|
this.recordAlloc(ref, allocBytes, "bump");
|
|
537
|
+
this.liveBytes += allocBytes;
|
|
392
538
|
return ref;
|
|
393
539
|
}
|
|
394
540
|
alloc(dataBytes) {
|
|
@@ -412,6 +558,7 @@ export class AttributeArena {
|
|
|
412
558
|
`Caller is releasing the wrong size; this WILL corrupt the freelist via over- or under-shrinking.`);
|
|
413
559
|
}
|
|
414
560
|
this.liveAllocs.delete(ref);
|
|
561
|
+
this.liveBytes -= allocBytes;
|
|
415
562
|
this.freelist.release(ref, allocBytes);
|
|
416
563
|
// §5: shrink the bump cursor back if release exposed a free
|
|
417
564
|
// tail touching it (cascading — `freelist.release` may have
|
|
@@ -441,103 +588,92 @@ export class AttributeArena {
|
|
|
441
588
|
}
|
|
442
589
|
this.liveAllocs.set(off, size);
|
|
443
590
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
/**
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
this.
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
if (this.dirty.isEmpty)
|
|
478
|
-
return;
|
|
479
|
-
this.dirty.drain((start, end) => {
|
|
480
|
-
device.queue.writeBuffer(this.buf.buffer, start, this.shadow.buffer, this.shadow.byteOffset + start, end - start);
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
/** Non-throwing alloc — returns `undefined` when the request
|
|
484
|
-
* would exceed the underlying GrowBuffer's `maxCapacity` cap.
|
|
485
|
-
* Used by `ChunkedIndexAllocator` to probe before spilling. */
|
|
486
|
-
tryAlloc(elements) {
|
|
487
|
-
const reused = this.freelist.alloc(elements);
|
|
488
|
-
if (reused !== undefined) {
|
|
489
|
-
this.recordAlloc(reused, elements, "freelist");
|
|
490
|
-
return reused;
|
|
591
|
+
/** Bytes the bump cursor can still grow into before hitting the chunk cap
|
|
592
|
+
* (ignores freelist holes — a conservative lower bound on free space, used
|
|
593
|
+
* by group placement to decide whether a draw's group fits here). */
|
|
594
|
+
get bumpHeadroom() { return this.buf.maxCapacity - this.cursor; }
|
|
595
|
+
/** Live bytes (sum of all live allocations, header+pad included). */
|
|
596
|
+
get liveByteCount() { return this.liveBytes; }
|
|
597
|
+
/** Fragmentation waste below the high-water mark: free bytes the tail-
|
|
598
|
+
* reclaim couldn't recover because they sit between live allocations. */
|
|
599
|
+
get wasteBytes() { return this.cursor - this.liveBytes; }
|
|
600
|
+
/**
|
|
601
|
+
* Waste-triggered compaction. Relocates live allocations to the front of
|
|
602
|
+
* the buffer, eliminating the high-water ratchet that exact-size freelist
|
|
603
|
+
* reuse alone can't fix (a drifting allocation-size distribution never
|
|
604
|
+
* reuses its holes and grows toward the high-water forever).
|
|
605
|
+
*
|
|
606
|
+
* The byte move happens GPU-side via `copyBufferToBuffer` through a scratch
|
|
607
|
+
* buffer — no CPU→GPU re-upload — and is mirrored into the CPU shadow so it
|
|
608
|
+
* stays authoritative for future partial writes. Same-buffer copies can't
|
|
609
|
+
* overlap, hence the scratch round-trip.
|
|
610
|
+
*
|
|
611
|
+
* Returns a map of OLD→NEW byte offset for every allocation that moved; the
|
|
612
|
+
* caller must re-seat every cached ref (drawHeader cells, uniform-pool
|
|
613
|
+
* entries, derived-uniform handles, partition master refs). Returns an
|
|
614
|
+
* empty map when it declines to compact (waste below `wasteFloorBytes` or
|
|
615
|
+
* less than half the extent wasted).
|
|
616
|
+
*
|
|
617
|
+
* Precondition: the arena's pending shadow writes must already be flushed
|
|
618
|
+
* to the GPU buffer (the GPU copy reads the live buffer). O(live).
|
|
619
|
+
*/
|
|
620
|
+
compact(device, wasteFloorBytes, force = false) {
|
|
621
|
+
const waste = this.cursor - this.liveBytes;
|
|
622
|
+
if (!force && (this.liveBytes * 2 >= this.cursor || waste < wasteFloorBytes)) {
|
|
623
|
+
return new Map();
|
|
491
624
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
`(size=${size}) overlaps live alloc at [${liveOff},${liveOff + liveSize}) ` +
|
|
508
|
-
`(size=${liveSize}). Index allocator is handing out shared memory!`);
|
|
509
|
-
}
|
|
625
|
+
// Live allocations sorted by current offset.
|
|
626
|
+
const live = [...this.liveAllocs.entries()].sort((a, b) => a[0] - b[0]);
|
|
627
|
+
// Assign packed offsets (bump from 0); build the OLD→NEW remap and the
|
|
628
|
+
// source-contiguous runs to copy. A run is a maximal set of live blocks
|
|
629
|
+
// adjacent in the source; under the bump it lands dest-contiguous too, so
|
|
630
|
+
// the compacted prefix [0, newExtent) is fully covered by the runs.
|
|
631
|
+
const remap = new Map();
|
|
632
|
+
const runs = [];
|
|
633
|
+
let w = 0;
|
|
634
|
+
let cur;
|
|
635
|
+
for (const [off, size] of live) {
|
|
636
|
+
if (w !== off)
|
|
637
|
+
remap.set(off, w);
|
|
638
|
+
if (cur !== undefined && off === cur.srcStart + cur.len) {
|
|
639
|
+
cur.len += size;
|
|
510
640
|
}
|
|
641
|
+
else {
|
|
642
|
+
cur = { srcStart: off, dstStart: w, len: size };
|
|
643
|
+
runs.push(cur);
|
|
644
|
+
}
|
|
645
|
+
w += size;
|
|
511
646
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
`liveAllocs — double-free or release for an alloc that never happened. ` +
|
|
526
|
-
`Live alloc count=${this.liveAllocs.size}.`);
|
|
527
|
-
}
|
|
528
|
-
if (tracked !== elements) {
|
|
529
|
-
throw new Error(`IndexAllocator.release: size mismatch at off=${off} — recorded=${tracked} ` +
|
|
530
|
-
`but release elements=${elements}. Will corrupt the freelist.`);
|
|
531
|
-
}
|
|
532
|
-
this.liveAllocs.delete(off);
|
|
533
|
-
this.freelist.release(off, elements);
|
|
534
|
-
while (true) {
|
|
535
|
-
const top = this.freelist.takeBlockEndingAt(this.cursor);
|
|
536
|
-
if (top === undefined)
|
|
537
|
-
break;
|
|
538
|
-
this.cursor = top.off;
|
|
539
|
-
this.buf.setUsed(this.cursor * 4);
|
|
647
|
+
const newExtent = w;
|
|
648
|
+
if (remap.size === 0)
|
|
649
|
+
return remap; // already tight
|
|
650
|
+
// GPU move: arena → scratch (compacted), then scratch → arena[0,newExtent).
|
|
651
|
+
// Both copies are between distinct buffers, so no overlap constraint.
|
|
652
|
+
const scratch = device.createBuffer({
|
|
653
|
+
label: "AttributeArena/compact-scratch",
|
|
654
|
+
size: newExtent,
|
|
655
|
+
usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
|
|
656
|
+
});
|
|
657
|
+
const enc = device.createCommandEncoder({ label: "AttributeArena/compact" });
|
|
658
|
+
for (const r of runs) {
|
|
659
|
+
enc.copyBufferToBuffer(this.buf.buffer, r.srcStart, scratch, r.dstStart, r.len);
|
|
540
660
|
}
|
|
661
|
+
enc.copyBufferToBuffer(scratch, 0, this.buf.buffer, 0, newExtent);
|
|
662
|
+
device.queue.submit([enc.finish()]);
|
|
663
|
+
scratch.destroy();
|
|
664
|
+
// Rebuild allocator bookkeeping: live allocs at their new offsets, no free
|
|
665
|
+
// blocks (everything is now contiguous), cursor at the new high-water.
|
|
666
|
+
// (Mirror-less: the GPU bounce above IS the move — nothing else to sync.)
|
|
667
|
+
const moved = [];
|
|
668
|
+
for (const [off, size] of live)
|
|
669
|
+
moved.push([remap.get(off) ?? off, size]);
|
|
670
|
+
this.liveAllocs.clear();
|
|
671
|
+
for (const [o, s] of moved)
|
|
672
|
+
this.liveAllocs.set(o, s);
|
|
673
|
+
this.freelist.clear();
|
|
674
|
+
this.cursor = newExtent;
|
|
675
|
+
this.buf.setUsed(newExtent);
|
|
676
|
+
return remap;
|
|
541
677
|
}
|
|
542
678
|
onResize(cb) { return this.buf.onResize(cb); }
|
|
543
679
|
destroy() { this.buf.destroy(); }
|
|
@@ -551,12 +687,17 @@ export function buildArenaState(device, attrBytesHint, idxBytesHint, label, idxE
|
|
|
551
687
|
// exercise multi-chunk routing).
|
|
552
688
|
const adapterCap = device.limits.maxStorageBufferBindingSize;
|
|
553
689
|
const cap = maxChunkBytes ?? Math.min(adapterCap, DEFAULT_MAX_BUFFER_BYTES);
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
690
|
+
void idxBytesHint;
|
|
691
|
+
void idxExtraUsage; // legacy params (separate index buffer removed)
|
|
692
|
+
// Clamp the pre-size hint to the per-chunk cap: GrowBuffer RAISES its max
|
|
693
|
+
// to fit a larger initial size, which would mint an oversized page (and
|
|
694
|
+
// §7 derived-record handles cap page offsets at 2^29 B = 512 MB). A
|
|
695
|
+
// multi-page scene simply pre-sizes each page at the cap as it opens.
|
|
696
|
+
const attrs = new ChunkedAttributeArena(device, `${label}/arena`, GPUBufferUsage.STORAGE, Math.min(attrBytesHint, cap), cap);
|
|
697
|
+
return { attrs };
|
|
557
698
|
}
|
|
558
699
|
export function arenaBytes(arena) {
|
|
559
|
-
return arena.attrs.totalUsedBytes()
|
|
700
|
+
return arena.attrs.totalUsedBytes();
|
|
560
701
|
}
|
|
561
702
|
/** Upload a single attribute — header (typeId, length) + data — into the arena at byte offset `ref`. */
|
|
562
703
|
export function writeAttribute(device, buf, ref, typeId, length, data) {
|