@aardworx/wombat.rendering 0.9.33 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/runtime/derivedModes/kernelCodegen.d.ts +90 -0
- package/dist/runtime/derivedModes/kernelCodegen.d.ts.map +1 -0
- package/dist/runtime/derivedModes/kernelCodegen.js +623 -0
- package/dist/runtime/derivedModes/kernelCodegen.js.map +1 -0
- package/dist/runtime/derivedModes/modeKeyCpu.d.ts.map +1 -1
- package/dist/runtime/derivedModes/modeKeyCpu.js +23 -67
- package/dist/runtime/derivedModes/modeKeyCpu.js.map +1 -1
- package/dist/runtime/derivedModes/partitionDispatcher.d.ts +79 -0
- package/dist/runtime/derivedModes/partitionDispatcher.d.ts.map +1 -0
- package/dist/runtime/derivedModes/partitionDispatcher.js +303 -0
- package/dist/runtime/derivedModes/partitionDispatcher.js.map +1 -0
- package/dist/runtime/derivedModes/partitionKernel.d.ts +12 -0
- package/dist/runtime/derivedModes/partitionKernel.d.ts.map +1 -0
- package/dist/runtime/derivedModes/partitionKernel.js +127 -0
- package/dist/runtime/derivedModes/partitionKernel.js.map +1 -0
- package/dist/runtime/derivedModes/partitionKernelLayout.d.ts +4 -0
- package/dist/runtime/derivedModes/partitionKernelLayout.d.ts.map +1 -0
- package/dist/runtime/derivedModes/partitionKernelLayout.js +24 -0
- package/dist/runtime/derivedModes/partitionKernelLayout.js.map +1 -0
- package/dist/runtime/derivedModes/rule.d.ts +38 -86
- package/dist/runtime/derivedModes/rule.d.ts.map +1 -1
- package/dist/runtime/derivedModes/rule.js +34 -74
- package/dist/runtime/derivedModes/rule.js.map +1 -1
- package/dist/runtime/derivedUniforms/dispatch.d.ts +10 -8
- package/dist/runtime/derivedUniforms/dispatch.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/dispatch.js +31 -18
- package/dist/runtime/derivedUniforms/dispatch.js.map +1 -1
- package/dist/runtime/derivedUniforms/marker.d.ts +90 -11
- package/dist/runtime/derivedUniforms/marker.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/marker.js +161 -15
- package/dist/runtime/derivedUniforms/marker.js.map +1 -1
- package/dist/runtime/derivedUniforms/sceneIntegration.d.ts +25 -6
- package/dist/runtime/derivedUniforms/sceneIntegration.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/sceneIntegration.js +61 -20
- package/dist/runtime/derivedUniforms/sceneIntegration.js.map +1 -1
- package/dist/runtime/heapEligibility.d.ts.map +1 -1
- package/dist/runtime/heapEligibility.js +22 -2
- package/dist/runtime/heapEligibility.js.map +1 -1
- package/dist/runtime/heapScene/chunkedArena.d.ts +76 -0
- package/dist/runtime/heapScene/chunkedArena.d.ts.map +1 -0
- package/dist/runtime/heapScene/chunkedArena.js +223 -0
- package/dist/runtime/heapScene/chunkedArena.js.map +1 -0
- package/dist/runtime/heapScene/freelist.d.ts +50 -0
- package/dist/runtime/heapScene/freelist.d.ts.map +1 -0
- package/dist/runtime/heapScene/freelist.js +160 -0
- package/dist/runtime/heapScene/freelist.js.map +1 -0
- package/dist/runtime/heapScene/growBuffer.d.ts +23 -2
- package/dist/runtime/heapScene/growBuffer.d.ts.map +1 -1
- package/dist/runtime/heapScene/growBuffer.js +35 -3
- package/dist/runtime/heapScene/growBuffer.js.map +1 -1
- package/dist/runtime/heapScene/pools.d.ts +65 -42
- package/dist/runtime/heapScene/pools.d.ts.map +1 -1
- package/dist/runtime/heapScene/pools.js +206 -134
- package/dist/runtime/heapScene/pools.js.map +1 -1
- package/dist/runtime/heapScene.d.ts +12 -0
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +938 -85
- package/dist/runtime/heapScene.js.map +1 -1
- package/dist/runtime/hybridScene.d.ts +7 -0
- package/dist/runtime/hybridScene.d.ts.map +1 -1
- package/dist/runtime/hybridScene.js +1 -0
- package/dist/runtime/hybridScene.js.map +1 -1
- package/dist/runtime/index.d.ts +4 -3
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +4 -3
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/renderTask.d.ts +5 -0
- package/dist/runtime/renderTask.d.ts.map +1 -1
- package/dist/runtime/renderTask.js +1 -0
- package/dist/runtime/renderTask.js.map +1 -1
- package/dist/runtime/runtime.d.ts +6 -0
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +1 -0
- package/dist/runtime/runtime.js.map +1 -1
- package/package.json +1 -1
- package/src/runtime/derivedModes/kernelCodegen.ts +673 -0
- package/src/runtime/derivedModes/modeKeyCpu.ts +22 -60
- package/src/runtime/derivedModes/partitionDispatcher.ts +349 -0
- package/src/runtime/derivedModes/partitionKernelLayout.ts +25 -0
- package/src/runtime/derivedModes/rule.ts +66 -145
- package/src/runtime/derivedUniforms/dispatch.ts +48 -21
- package/src/runtime/derivedUniforms/marker.ts +210 -16
- package/src/runtime/derivedUniforms/sceneIntegration.ts +67 -20
- package/src/runtime/heapEligibility.ts +22 -1
- package/src/runtime/heapScene/chunkedArena.ts +238 -0
- package/src/runtime/heapScene/freelist.ts +161 -0
- package/src/runtime/heapScene/growBuffer.ts +39 -2
- package/src/runtime/heapScene/pools.ts +231 -143
- package/src/runtime/heapScene.ts +1119 -81
- package/src/runtime/hybridScene.ts +8 -0
- package/src/runtime/index.ts +12 -9
- package/src/runtime/renderTask.ts +6 -0
- package/src/runtime/runtime.ts +7 -0
- package/src/runtime/derivedModes/gpuDispatcher.ts +0 -226
- package/src/runtime/derivedModes/gpuKernel.ts +0 -98
|
@@ -19,7 +19,9 @@ import { AVal } from "@aardworx/wombat.adaptive";
|
|
|
19
19
|
import type { aval, IDisposable } from "@aardworx/wombat.adaptive";
|
|
20
20
|
import type { BufferView } from "../../core/bufferView.js";
|
|
21
21
|
import type { HostBufferSource } from "../../core/buffer.js";
|
|
22
|
-
import { GrowBuffer, ALIGN16 } from "./growBuffer.js";
|
|
22
|
+
import { GrowBuffer, ALIGN16, DEFAULT_MAX_BUFFER_BYTES } from "./growBuffer.js";
|
|
23
|
+
import { Freelist } from "./freelist.js";
|
|
24
|
+
import { ChunkedAttributeArena, ChunkedIndexAllocator } from "./chunkedArena.js";
|
|
23
25
|
|
|
24
26
|
// ─── Per-allocation header layout ──────────────────────────────────────
|
|
25
27
|
|
|
@@ -39,100 +41,147 @@ export const SEM_NORMALS = 2;
|
|
|
39
41
|
|
|
40
42
|
// ─── UniformPool ───────────────────────────────────────────────────────
|
|
41
43
|
|
|
44
|
+
/**
|
|
45
|
+
* One pool entry tracks a single (aval, chunkIdx) pair. Refs are
|
|
46
|
+
* byte offsets within the chunk's GPUBuffer; chunkIdx is implicit
|
|
47
|
+
* in which chunk's bind group is active at draw time (shaders
|
|
48
|
+
* never decode it).
|
|
49
|
+
*/
|
|
42
50
|
interface PoolEntry {
|
|
43
|
-
|
|
51
|
+
readonly chunkIdx: number;
|
|
44
52
|
readonly ref: number;
|
|
45
53
|
readonly dataBytes: number;
|
|
46
54
|
readonly typeId: number;
|
|
47
|
-
/** Packer used to refresh the data region on aval marks. */
|
|
48
55
|
readonly pack: (val: unknown, dst: Float32Array, off: number) => void;
|
|
49
56
|
refcount: number;
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
/**
|
|
53
|
-
*
|
|
54
|
-
* aval
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* (
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* Sharing emerges from aval identity — no separate "frequency"
|
|
61
|
-
* declaration needed. A `cval` shared by all draws → 1 alloc.
|
|
62
|
-
* A static positions array shared across instanced draws → 1 alloc.
|
|
63
|
-
* Same code path either way.
|
|
60
|
+
* `(aval, chunkIdx)`-keyed pool of chunked-arena allocations. An
|
|
61
|
+
* aval that's referenced from multiple chunks (e.g. two ROs sharing
|
|
62
|
+
* a uniform but landing in different chunks) gets one entry per
|
|
63
|
+
* chunk — accepted duplication cost of the multi-draw-call
|
|
64
|
+
* per-chunk render path (§3). Within a single chunk, ROs sharing
|
|
65
|
+
* an aval still share the underlying allocation.
|
|
64
66
|
*/
|
|
65
67
|
export class UniformPool {
|
|
66
|
-
|
|
67
|
-
// keys are overwhelmingly reactive `cval`s (per-object trafos,
|
|
68
|
-
// colours, …) and the hot path is `acquire`/`release` ~once per
|
|
69
|
-
// drawHeader field per RO.
|
|
70
|
-
private readonly byAval = new Map<aval<unknown>, PoolEntry>();
|
|
68
|
+
private readonly byAval = new Map<aval<unknown>, Map<number, PoolEntry>>();
|
|
71
69
|
|
|
72
|
-
has(av: aval<unknown
|
|
73
|
-
|
|
70
|
+
has(av: aval<unknown>, chunkIdx: number): boolean {
|
|
71
|
+
return this.byAval.get(av)?.has(chunkIdx) ?? false;
|
|
72
|
+
}
|
|
73
|
+
/** True when the pool holds at least one entry for `av` in any
|
|
74
|
+
* chunk. Used by the scene's `inputChanged` dispatch to decide
|
|
75
|
+
* whether a marking aval is one of our tracked allocations. */
|
|
76
|
+
hasAny(av: aval<unknown>): boolean {
|
|
77
|
+
return (this.byAval.get(av)?.size ?? 0) > 0;
|
|
78
|
+
}
|
|
79
|
+
entry(av: aval<unknown>, chunkIdx: number): PoolEntry | undefined {
|
|
80
|
+
return this.byAval.get(av)?.get(chunkIdx);
|
|
81
|
+
}
|
|
82
|
+
/** First chunk this aval is allocated in, if any. Used by `addRO`
|
|
83
|
+
* to prefer co-locating a new RO with its already-allocated
|
|
84
|
+
* shared inputs (avoids unnecessary duplication). */
|
|
85
|
+
firstChunkContaining(av: aval<unknown>): number | undefined {
|
|
86
|
+
const byChunk = this.byAval.get(av);
|
|
87
|
+
if (byChunk === undefined || byChunk.size === 0) return undefined;
|
|
88
|
+
return byChunk.keys().next().value as number;
|
|
89
|
+
}
|
|
74
90
|
|
|
75
91
|
/**
|
|
76
|
-
* Acquire (or share) an allocation for `aval
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* uploaded immediately.
|
|
92
|
+
* Acquire (or share) an allocation for `aval` in `chunkIdx`.
|
|
93
|
+
* Returns the byte offset within that chunk's GPUBuffer. If a
|
|
94
|
+
* fresh allocation is made, the value is packed + uploaded
|
|
95
|
+
* immediately into the chunk's CPU shadow.
|
|
81
96
|
*/
|
|
82
97
|
acquire(
|
|
83
98
|
device: GPUDevice,
|
|
84
|
-
arena:
|
|
99
|
+
arena: ChunkedAttributeArena,
|
|
85
100
|
av: aval<unknown>,
|
|
101
|
+
chunkIdx: number,
|
|
86
102
|
value: unknown,
|
|
87
103
|
dataBytes: number,
|
|
88
104
|
typeId: number,
|
|
89
105
|
length: number,
|
|
90
106
|
pack: (val: unknown, dst: Float32Array, off: number) => void,
|
|
91
107
|
): number {
|
|
92
|
-
|
|
108
|
+
let byChunk = this.byAval.get(av);
|
|
109
|
+
const existing = byChunk?.get(chunkIdx);
|
|
93
110
|
if (existing !== undefined) {
|
|
94
111
|
existing.refcount++;
|
|
95
112
|
return existing.ref;
|
|
96
113
|
}
|
|
97
|
-
const
|
|
114
|
+
const r = arena.alloc(dataBytes, chunkIdx);
|
|
115
|
+
// The alloc may have spilled into a different chunk if `chunkIdx`
|
|
116
|
+
// was full. Honour wherever it landed — addRO's chunk-routing
|
|
117
|
+
// commits the RO to the spill chunk too.
|
|
118
|
+
const finalChunk = r.chunkIdx;
|
|
98
119
|
const allocBytes = ALIGN16(ALLOC_HEADER_PAD_TO + dataBytes);
|
|
99
120
|
const buf = new ArrayBuffer(allocBytes);
|
|
100
121
|
const u32 = new Uint32Array(buf);
|
|
101
122
|
const f32 = new Float32Array(buf);
|
|
102
123
|
u32[0] = typeId;
|
|
103
124
|
u32[1] = length;
|
|
104
|
-
// stride_bytes (offset 8): bytes per element. Lets the VS decode
|
|
105
|
-
// pick V3- vs V4-tight load expressions for vec4 attributes.
|
|
106
125
|
u32[2] = length > 0 ? Math.floor(dataBytes / length) : 0;
|
|
107
126
|
pack(value, f32, ALLOC_HEADER_PAD_TO / 4);
|
|
108
|
-
arena.write(
|
|
127
|
+
arena.write(finalChunk, r.off, new Uint8Array(buf));
|
|
109
128
|
void device;
|
|
110
|
-
|
|
111
|
-
|
|
129
|
+
if (byChunk === undefined) {
|
|
130
|
+
byChunk = new Map();
|
|
131
|
+
this.byAval.set(av, byChunk);
|
|
132
|
+
}
|
|
133
|
+
byChunk.set(finalChunk, {
|
|
134
|
+
chunkIdx: finalChunk, ref: r.off, dataBytes, typeId, pack, refcount: 1,
|
|
135
|
+
});
|
|
136
|
+
return r.off;
|
|
112
137
|
}
|
|
113
138
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const e =
|
|
139
|
+
release(arena: ChunkedAttributeArena, av: aval<unknown>, chunkIdx: number): void {
|
|
140
|
+
const byChunk = this.byAval.get(av);
|
|
141
|
+
const e = byChunk?.get(chunkIdx);
|
|
117
142
|
if (e === undefined) return;
|
|
118
143
|
e.refcount--;
|
|
119
144
|
if (e.refcount > 0) return;
|
|
120
|
-
arena.release(e.ref, ALIGN16(ALLOC_HEADER_PAD_TO + e.dataBytes));
|
|
121
|
-
|
|
145
|
+
arena.release(e.chunkIdx, e.ref, ALIGN16(ALLOC_HEADER_PAD_TO + e.dataBytes));
|
|
146
|
+
byChunk!.delete(chunkIdx);
|
|
147
|
+
if (byChunk!.size === 0) this.byAval.delete(av);
|
|
122
148
|
}
|
|
123
149
|
|
|
124
|
-
/** Re-pack one entry's data region into
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
arena
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
150
|
+
/** Re-pack one entry's data region into every chunk that holds
|
|
151
|
+
* this aval. When an aval is shared across N chunks (each chunk
|
|
152
|
+
* has its own duplicate alloc), every duplicate needs the
|
|
153
|
+
* refresh so all the chunks' ROs see the new value. */
|
|
154
|
+
repack(
|
|
155
|
+
device: GPUDevice,
|
|
156
|
+
arena: ChunkedAttributeArena,
|
|
157
|
+
av: aval<unknown>,
|
|
158
|
+
val: unknown,
|
|
159
|
+
): void {
|
|
160
|
+
const byChunk = this.byAval.get(av);
|
|
161
|
+
if (byChunk === undefined) return;
|
|
162
|
+
let dst: Float32Array | undefined;
|
|
163
|
+
for (const e of byChunk.values()) {
|
|
164
|
+
if (dst === undefined || dst.length !== e.dataBytes / 4) {
|
|
165
|
+
dst = new Float32Array(e.dataBytes / 4);
|
|
166
|
+
}
|
|
167
|
+
e.pack(val, dst, 0);
|
|
168
|
+
arena.write(
|
|
169
|
+
e.chunkIdx,
|
|
170
|
+
e.ref + ALLOC_HEADER_PAD_TO,
|
|
171
|
+
new Uint8Array(dst.buffer, dst.byteOffset, e.dataBytes),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
134
174
|
void device;
|
|
135
175
|
}
|
|
176
|
+
/** Total bytes touched by `repack` for this aval — sum across
|
|
177
|
+
* chunks. Used by the dirty-bytes diagnostics. */
|
|
178
|
+
totalDataBytes(av: aval<unknown>): number {
|
|
179
|
+
const byChunk = this.byAval.get(av);
|
|
180
|
+
if (byChunk === undefined) return 0;
|
|
181
|
+
let s = 0;
|
|
182
|
+
for (const e of byChunk.values()) s += e.dataBytes;
|
|
183
|
+
return s;
|
|
184
|
+
}
|
|
136
185
|
}
|
|
137
186
|
|
|
138
187
|
// ─── IndexPool ─────────────────────────────────────────────────────────
|
|
@@ -150,10 +199,13 @@ export class UniformPool {
|
|
|
150
199
|
* `Uint32Array` view collapse to one allocation.
|
|
151
200
|
*/
|
|
152
201
|
export class IndexPool {
|
|
202
|
+
/** Per-aval, per-chunk bookkeeping. Same aval acquired in two
|
|
203
|
+
* chunks gets two entries (accepted §3 duplication). */
|
|
153
204
|
private readonly byAval = new Map<
|
|
154
205
|
aval<Uint32Array>,
|
|
155
|
-
{ entry: IndexPoolEntry; perAvalCount: number }
|
|
206
|
+
Map<number, { entry: IndexPoolEntry; perAvalCount: number }>
|
|
156
207
|
>();
|
|
208
|
+
/** Per-chunk constant-aval value-dedup (§5b in-chunk). */
|
|
157
209
|
private readonly byValueKey = new Map<string, IndexPoolEntry>();
|
|
158
210
|
private readonly bufferIds = new WeakMap<ArrayBufferLike, number>();
|
|
159
211
|
private nextBufferId = 1;
|
|
@@ -166,48 +218,75 @@ export class IndexPool {
|
|
|
166
218
|
return id;
|
|
167
219
|
}
|
|
168
220
|
|
|
221
|
+
firstChunkContaining(av: aval<Uint32Array>): number | undefined {
|
|
222
|
+
const byChunk = this.byAval.get(av);
|
|
223
|
+
if (byChunk === undefined || byChunk.size === 0) return undefined;
|
|
224
|
+
return byChunk.keys().next().value as number;
|
|
225
|
+
}
|
|
226
|
+
|
|
169
227
|
acquire(
|
|
170
228
|
device: GPUDevice,
|
|
171
|
-
indices:
|
|
229
|
+
indices: ChunkedIndexAllocator,
|
|
172
230
|
av: aval<Uint32Array>,
|
|
231
|
+
chunkIdx: number,
|
|
173
232
|
arr: Uint32Array,
|
|
174
|
-
): { firstIndex: number; count: number } {
|
|
175
|
-
|
|
233
|
+
): { chunkIdx: number; firstIndex: number; count: number } {
|
|
234
|
+
let byChunk = this.byAval.get(av);
|
|
235
|
+
const bound = byChunk?.get(chunkIdx);
|
|
176
236
|
if (bound !== undefined) {
|
|
177
237
|
bound.perAvalCount++;
|
|
178
238
|
bound.entry.totalRefcount++;
|
|
179
|
-
return { firstIndex: bound.entry.firstIndex, count: bound.entry.count };
|
|
239
|
+
return { chunkIdx: bound.entry.chunkIdx, firstIndex: bound.entry.firstIndex, count: bound.entry.count };
|
|
180
240
|
}
|
|
181
241
|
let valueKey: string | undefined;
|
|
182
242
|
if (av.isConstant) {
|
|
183
|
-
|
|
243
|
+
// Value-dedup is scoped per chunk — two constant avals in the
|
|
244
|
+
// same chunk collapse to one allocation; two constant avals in
|
|
245
|
+
// DIFFERENT chunks each get their own (we can't share across
|
|
246
|
+
// chunks since the index range lives in the chunk's GPUBuffer).
|
|
247
|
+
valueKey = `${chunkIdx}:${this.bufferIdOf(arr.buffer)}:${arr.byteOffset}:${arr.byteLength}`;
|
|
184
248
|
const shared = this.byValueKey.get(valueKey);
|
|
185
249
|
if (shared !== undefined) {
|
|
186
250
|
shared.totalRefcount++;
|
|
187
|
-
|
|
188
|
-
|
|
251
|
+
if (byChunk === undefined) {
|
|
252
|
+
byChunk = new Map();
|
|
253
|
+
this.byAval.set(av, byChunk);
|
|
254
|
+
}
|
|
255
|
+
byChunk.set(chunkIdx, { entry: shared, perAvalCount: 1 });
|
|
256
|
+
return { chunkIdx: shared.chunkIdx, firstIndex: shared.firstIndex, count: shared.count };
|
|
189
257
|
}
|
|
190
258
|
}
|
|
191
|
-
const
|
|
259
|
+
const r = indices.alloc(arr.length, chunkIdx);
|
|
192
260
|
indices.write(
|
|
193
|
-
|
|
261
|
+
r.chunkIdx, r.off * 4,
|
|
194
262
|
new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength),
|
|
195
263
|
);
|
|
196
264
|
void device;
|
|
197
|
-
const entry: IndexPoolEntry = {
|
|
198
|
-
|
|
265
|
+
const entry: IndexPoolEntry = {
|
|
266
|
+
chunkIdx: r.chunkIdx, firstIndex: r.off, count: arr.length,
|
|
267
|
+
totalRefcount: 1, valueKey,
|
|
268
|
+
};
|
|
269
|
+
if (byChunk === undefined) {
|
|
270
|
+
byChunk = new Map();
|
|
271
|
+
this.byAval.set(av, byChunk);
|
|
272
|
+
}
|
|
273
|
+
byChunk.set(r.chunkIdx, { entry, perAvalCount: 1 });
|
|
199
274
|
if (valueKey !== undefined) this.byValueKey.set(valueKey, entry);
|
|
200
|
-
return { firstIndex, count: arr.length };
|
|
275
|
+
return { chunkIdx: r.chunkIdx, firstIndex: r.off, count: arr.length };
|
|
201
276
|
}
|
|
202
277
|
|
|
203
|
-
release(indices:
|
|
204
|
-
const
|
|
278
|
+
release(indices: ChunkedIndexAllocator, av: aval<Uint32Array>, chunkIdx: number): void {
|
|
279
|
+
const byChunk = this.byAval.get(av);
|
|
280
|
+
const bound = byChunk?.get(chunkIdx);
|
|
205
281
|
if (bound === undefined) return;
|
|
206
282
|
bound.perAvalCount--;
|
|
207
283
|
bound.entry.totalRefcount--;
|
|
208
|
-
if (bound.perAvalCount === 0)
|
|
284
|
+
if (bound.perAvalCount === 0) {
|
|
285
|
+
byChunk!.delete(chunkIdx);
|
|
286
|
+
if (byChunk!.size === 0) this.byAval.delete(av);
|
|
287
|
+
}
|
|
209
288
|
if (bound.entry.totalRefcount > 0) return;
|
|
210
|
-
indices.release(bound.entry.firstIndex, bound.entry.count);
|
|
289
|
+
indices.release(bound.entry.chunkIdx, bound.entry.firstIndex, bound.entry.count);
|
|
211
290
|
if (bound.entry.valueKey !== undefined) {
|
|
212
291
|
this.byValueKey.delete(bound.entry.valueKey);
|
|
213
292
|
}
|
|
@@ -215,6 +294,7 @@ export class IndexPool {
|
|
|
215
294
|
}
|
|
216
295
|
|
|
217
296
|
interface IndexPoolEntry {
|
|
297
|
+
chunkIdx: number;
|
|
218
298
|
firstIndex: number;
|
|
219
299
|
count: number;
|
|
220
300
|
totalRefcount: number;
|
|
@@ -259,7 +339,7 @@ export class DrawHeap {
|
|
|
259
339
|
*/
|
|
260
340
|
export class AttributeArena {
|
|
261
341
|
private cursor = 0;
|
|
262
|
-
private
|
|
342
|
+
private readonly freelist = new Freelist();
|
|
263
343
|
/** CPU shadow of the entire GPU buffer; writes go here first then
|
|
264
344
|
* flush() emits one writeBuffer per dirty contiguous range. */
|
|
265
345
|
private shadow: Uint8Array;
|
|
@@ -296,76 +376,58 @@ export class AttributeArena {
|
|
|
296
376
|
* Allocate space for one attribute. Returns the byte ref (offset
|
|
297
377
|
* to the header — data lives at ref + 16).
|
|
298
378
|
*/
|
|
299
|
-
alloc
|
|
379
|
+
/** Non-throwing alloc — returns `undefined` when the request
|
|
380
|
+
* would exceed the underlying GrowBuffer's `maxCapacity` cap.
|
|
381
|
+
* Used by `ChunkedAttributeArena` to probe before spilling to
|
|
382
|
+
* the next chunk. */
|
|
383
|
+
tryAlloc(dataBytes: number): number | undefined {
|
|
300
384
|
const allocBytes = ALIGN16(ALLOC_HEADER_PAD_TO + dataBytes);
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
385
|
+
const reused = this.freelist.alloc(allocBytes);
|
|
386
|
+
if (reused !== undefined) return reused;
|
|
387
|
+
const next = this.cursor + allocBytes;
|
|
388
|
+
if (next > this.buf.maxCapacity) return undefined;
|
|
389
|
+
this.cursor = next;
|
|
390
|
+
this.buf.ensureCapacity(next);
|
|
391
|
+
this.buf.setUsed(next);
|
|
392
|
+
return next - allocBytes;
|
|
393
|
+
}
|
|
394
|
+
alloc(dataBytes: number): number {
|
|
395
|
+
const r = this.tryAlloc(dataBytes);
|
|
396
|
+
if (r === undefined) {
|
|
397
|
+
throw new Error(
|
|
398
|
+
`AttributeArena: allocation of ${dataBytes}B (with header alignment) exceeds chunk cap`,
|
|
399
|
+
);
|
|
310
400
|
}
|
|
311
|
-
|
|
312
|
-
this.cursor += allocBytes;
|
|
313
|
-
this.buf.ensureCapacity(this.cursor);
|
|
314
|
-
this.buf.setUsed(this.cursor);
|
|
315
|
-
return ref;
|
|
401
|
+
return r;
|
|
316
402
|
}
|
|
317
403
|
release(ref: number, dataBytes: number): void {
|
|
318
404
|
const allocBytes = ALIGN16(ALLOC_HEADER_PAD_TO + dataBytes);
|
|
319
|
-
|
|
405
|
+
this.freelist.release(ref, allocBytes);
|
|
406
|
+
// §5: shrink the bump cursor back if release exposed a free
|
|
407
|
+
// tail touching it (cascading — `freelist.release` may have
|
|
408
|
+
// coalesced into a single block ending at `cursor`). Reclaims
|
|
409
|
+
// the high-watermark over time in long-lived high-churn scenes.
|
|
410
|
+
while (true) {
|
|
411
|
+
const top = this.freelist.takeBlockEndingAt(this.cursor);
|
|
412
|
+
if (top === undefined) break;
|
|
413
|
+
this.cursor = top.off;
|
|
414
|
+
this.buf.setUsed(this.cursor);
|
|
415
|
+
}
|
|
320
416
|
}
|
|
321
417
|
onResize(cb: () => void): IDisposable { return this.buf.onResize(cb); }
|
|
322
418
|
destroy(): void { this.buf.destroy(); }
|
|
323
419
|
}
|
|
324
420
|
|
|
325
|
-
/**
|
|
326
|
-
* Insert `{off, size}` into a free-list kept sorted by `off`, then
|
|
327
|
-
* coalesce with the two immediate neighbours.
|
|
328
|
-
*/
|
|
329
|
-
export function insertSortedFreeBlock(
|
|
330
|
-
freeList: { off: number; size: number }[],
|
|
331
|
-
off: number,
|
|
332
|
-
size: number,
|
|
333
|
-
): void {
|
|
334
|
-
let lo = 0, hi = freeList.length;
|
|
335
|
-
while (lo < hi) {
|
|
336
|
-
const mid = (lo + hi) >>> 1;
|
|
337
|
-
if (freeList[mid]!.off <= off) lo = mid + 1;
|
|
338
|
-
else hi = mid;
|
|
339
|
-
}
|
|
340
|
-
const prev = lo > 0 ? freeList[lo - 1] : undefined;
|
|
341
|
-
if (prev !== undefined && prev.off + prev.size === off) {
|
|
342
|
-
prev.size += size;
|
|
343
|
-
const next = freeList[lo];
|
|
344
|
-
if (next !== undefined && prev.off + prev.size === next.off) {
|
|
345
|
-
prev.size += next.size;
|
|
346
|
-
freeList.splice(lo, 1);
|
|
347
|
-
}
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
const next = freeList[lo];
|
|
351
|
-
if (next !== undefined && off + size === next.off) {
|
|
352
|
-
next.off = off;
|
|
353
|
-
next.size += size;
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
freeList.splice(lo, 0, { off, size });
|
|
357
|
-
}
|
|
358
|
-
|
|
359
421
|
// ─── IndexAllocator (element-bump) ─────────────────────────────────────
|
|
360
422
|
|
|
361
423
|
/**
|
|
362
424
|
* Element-bump allocator over an index GrowBuffer (units = u32). Each
|
|
363
425
|
* draw's index range is allocated as one block; on release the block
|
|
364
|
-
* is returned to a
|
|
426
|
+
* is returned to a Freelist and can be reused best-fit.
|
|
365
427
|
*/
|
|
366
428
|
export class IndexAllocator {
|
|
367
429
|
private cursor = 0; // in u32s, not bytes
|
|
368
|
-
private
|
|
430
|
+
private readonly freelist = new Freelist();
|
|
369
431
|
private shadow: Uint8Array;
|
|
370
432
|
private dirtyMin = Infinity;
|
|
371
433
|
private dirtyMax = 0;
|
|
@@ -395,24 +457,37 @@ export class IndexAllocator {
|
|
|
395
457
|
this.dirtyMin = Infinity;
|
|
396
458
|
this.dirtyMax = 0;
|
|
397
459
|
}
|
|
460
|
+
/** Non-throwing alloc — returns `undefined` when the request
|
|
461
|
+
* would exceed the underlying GrowBuffer's `maxCapacity` cap.
|
|
462
|
+
* Used by `ChunkedIndexAllocator` to probe before spilling. */
|
|
463
|
+
tryAlloc(elements: number): number | undefined {
|
|
464
|
+
const reused = this.freelist.alloc(elements);
|
|
465
|
+
if (reused !== undefined) return reused;
|
|
466
|
+
const nextElts = this.cursor + elements;
|
|
467
|
+
if (nextElts * 4 > this.buf.maxCapacity) return undefined;
|
|
468
|
+
this.cursor = nextElts;
|
|
469
|
+
this.buf.ensureCapacity(nextElts * 4);
|
|
470
|
+
this.buf.setUsed(nextElts * 4);
|
|
471
|
+
return nextElts - elements;
|
|
472
|
+
}
|
|
398
473
|
alloc(elements: number): number {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
const off = f.off;
|
|
403
|
-
if (f.size === elements) this.freeList.splice(i, 1);
|
|
404
|
-
else { f.off += elements; f.size -= elements; }
|
|
405
|
-
return off;
|
|
406
|
-
}
|
|
474
|
+
const r = this.tryAlloc(elements);
|
|
475
|
+
if (r === undefined) {
|
|
476
|
+
throw new Error(`IndexAllocator: allocation of ${elements} elements exceeds chunk cap`);
|
|
407
477
|
}
|
|
408
|
-
|
|
409
|
-
this.cursor += elements;
|
|
410
|
-
this.buf.ensureCapacity(this.cursor * 4);
|
|
411
|
-
this.buf.setUsed(this.cursor * 4);
|
|
412
|
-
return off;
|
|
478
|
+
return r;
|
|
413
479
|
}
|
|
414
480
|
release(off: number, elements: number): void {
|
|
415
|
-
|
|
481
|
+
this.freelist.release(off, elements);
|
|
482
|
+
// §5: cursor-shrink mirror of AttributeArena.release. Cursor
|
|
483
|
+
// is in u32 elements here, and setUsed on the GrowBuffer
|
|
484
|
+
// expects bytes — multiply by 4.
|
|
485
|
+
while (true) {
|
|
486
|
+
const top = this.freelist.takeBlockEndingAt(this.cursor);
|
|
487
|
+
if (top === undefined) break;
|
|
488
|
+
this.cursor = top.off;
|
|
489
|
+
this.buf.setUsed(this.cursor * 4);
|
|
490
|
+
}
|
|
416
491
|
}
|
|
417
492
|
onResize(cb: () => void): IDisposable { return this.buf.onResize(cb); }
|
|
418
493
|
destroy(): void { this.buf.destroy(); }
|
|
@@ -423,11 +498,15 @@ export class IndexAllocator {
|
|
|
423
498
|
/**
|
|
424
499
|
* Global arena state: attribute / uniform data lives in `attrs`
|
|
425
500
|
* (multi-typed-view storage); indices live in `indices` (separate
|
|
426
|
-
* INDEX-usage buffer).
|
|
501
|
+
* INDEX-usage buffer). Both are chunked: each chunk owns a separate
|
|
502
|
+
* GPUBuffer + Freelist. Refs that downstream code stores are byte
|
|
503
|
+
* offsets within a chunk; the chunk identity is carried alongside
|
|
504
|
+
* (via bucket.chunkIdx in heapScene) and made implicit at draw
|
|
505
|
+
* time by which chunk's bind group is active.
|
|
427
506
|
*/
|
|
428
507
|
export interface ArenaState {
|
|
429
|
-
readonly attrs:
|
|
430
|
-
readonly indices:
|
|
508
|
+
readonly attrs: ChunkedAttributeArena;
|
|
509
|
+
readonly indices: ChunkedIndexAllocator;
|
|
431
510
|
}
|
|
432
511
|
|
|
433
512
|
export function buildArenaState(
|
|
@@ -436,20 +515,29 @@ export function buildArenaState(
|
|
|
436
515
|
idxBytesHint: number,
|
|
437
516
|
label: string,
|
|
438
517
|
idxExtraUsage: GPUBufferUsageFlags = 0,
|
|
518
|
+
maxChunkBytes: number | undefined = undefined,
|
|
439
519
|
): ArenaState {
|
|
440
|
-
|
|
520
|
+
// §3: cap chunk size at min(adapter's maxStorageBufferBindingSize,
|
|
521
|
+
// GrowBuffer's DEFAULT_MAX_BUFFER_BYTES). Lets chunks grow as far
|
|
522
|
+
// as the hardware lets us (typically 2 GiB+ on desktop, ≥ 256 MB
|
|
523
|
+
// on mobile/integrated) while keeping a sensible internal ceiling.
|
|
524
|
+
// Caller can override (the heap-demo uses a tiny 4 MB cap to
|
|
525
|
+
// exercise multi-chunk routing).
|
|
526
|
+
const adapterCap = device.limits.maxStorageBufferBindingSize;
|
|
527
|
+
const cap = maxChunkBytes ?? Math.min(adapterCap, DEFAULT_MAX_BUFFER_BYTES);
|
|
528
|
+
const attrs = new ChunkedAttributeArena(
|
|
441
529
|
device, `${label}/attrs`, GPUBufferUsage.STORAGE,
|
|
442
|
-
attrBytesHint,
|
|
443
|
-
)
|
|
444
|
-
const indices = new
|
|
530
|
+
attrBytesHint, cap,
|
|
531
|
+
);
|
|
532
|
+
const indices = new ChunkedIndexAllocator(
|
|
445
533
|
device, `${label}/idx`, GPUBufferUsage.INDEX | idxExtraUsage,
|
|
446
|
-
idxBytesHint,
|
|
447
|
-
)
|
|
534
|
+
idxBytesHint, cap,
|
|
535
|
+
);
|
|
448
536
|
return { attrs, indices };
|
|
449
537
|
}
|
|
450
538
|
|
|
451
539
|
export function arenaBytes(arena: ArenaState): number {
|
|
452
|
-
return arena.attrs.
|
|
540
|
+
return arena.attrs.totalUsedBytes() + arena.indices.totalUsedElements() * 4;
|
|
453
541
|
}
|
|
454
542
|
|
|
455
543
|
/** Upload a single attribute — header (typeId, length) + data — into the arena at byte offset `ref`. */
|