@aardworx/wombat.rendering 0.9.14 → 0.9.16
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/modeKeyCpu.d.ts +62 -0
- package/dist/runtime/derivedModes/modeKeyCpu.d.ts.map +1 -0
- package/dist/runtime/derivedModes/modeKeyCpu.js +225 -0
- package/dist/runtime/derivedModes/modeKeyCpu.js.map +1 -0
- package/dist/runtime/derivedModes/partition.d.ts +36 -0
- package/dist/runtime/derivedModes/partition.d.ts.map +1 -0
- package/dist/runtime/derivedModes/partition.js +170 -0
- package/dist/runtime/derivedModes/partition.js.map +1 -0
- package/dist/runtime/derivedModes/slotTable.d.ts +70 -0
- package/dist/runtime/derivedModes/slotTable.d.ts.map +1 -0
- package/dist/runtime/derivedModes/slotTable.js +130 -0
- package/dist/runtime/derivedModes/slotTable.js.map +1 -0
- package/dist/runtime/heapScene/growBuffer.d.ts +33 -0
- package/dist/runtime/heapScene/growBuffer.d.ts.map +1 -0
- package/dist/runtime/heapScene/growBuffer.js +76 -0
- package/dist/runtime/heapScene/growBuffer.js.map +1 -0
- package/dist/runtime/heapScene/packers.d.ts +25 -0
- package/dist/runtime/heapScene/packers.d.ts.map +1 -0
- package/dist/runtime/heapScene/packers.js +111 -0
- package/dist/runtime/heapScene/packers.js.map +1 -0
- package/dist/runtime/heapScene/pools.d.ts +181 -0
- package/dist/runtime/heapScene/pools.d.ts.map +1 -0
- package/dist/runtime/heapScene/pools.js +417 -0
- package/dist/runtime/heapScene/pools.js.map +1 -0
- package/dist/runtime/heapScene/scanKernel.d.ts +11 -0
- package/dist/runtime/heapScene/scanKernel.d.ts.map +1 -0
- package/dist/runtime/heapScene/scanKernel.js +181 -0
- package/dist/runtime/heapScene/scanKernel.js.map +1 -0
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +346 -937
- package/dist/runtime/heapScene.js.map +1 -1
- package/dist/runtime/index.d.ts +4 -0
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +5 -0
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/pipelineCache/bitfield.d.ts +5 -0
- package/dist/runtime/pipelineCache/bitfield.d.ts.map +1 -0
- package/dist/runtime/pipelineCache/bitfield.js +201 -0
- package/dist/runtime/pipelineCache/bitfield.js.map +1 -0
- package/dist/runtime/pipelineCache/cache.d.ts +36 -0
- package/dist/runtime/pipelineCache/cache.d.ts.map +1 -0
- package/dist/runtime/pipelineCache/cache.js +108 -0
- package/dist/runtime/pipelineCache/cache.js.map +1 -0
- package/dist/runtime/pipelineCache/descriptor.d.ts +58 -0
- package/dist/runtime/pipelineCache/descriptor.d.ts.map +1 -0
- package/dist/runtime/pipelineCache/descriptor.js +100 -0
- package/dist/runtime/pipelineCache/descriptor.js.map +1 -0
- package/dist/runtime/pipelineCache/index.d.ts +5 -0
- package/dist/runtime/pipelineCache/index.d.ts.map +1 -0
- package/dist/runtime/pipelineCache/index.js +12 -0
- package/dist/runtime/pipelineCache/index.js.map +1 -0
- package/package.json +1 -1
- package/src/runtime/derivedModes/modeKeyCpu.ts +251 -0
- package/src/runtime/derivedModes/partition.ts +206 -0
- package/src/runtime/derivedModes/slotTable.ts +153 -0
- package/src/runtime/heapScene/growBuffer.ts +75 -0
- package/src/runtime/heapScene/packers.ts +127 -0
- package/src/runtime/heapScene/pools.ts +492 -0
- package/src/runtime/heapScene/scanKernel.ts +184 -0
- package/src/runtime/heapScene.ts +397 -1064
- package/src/runtime/index.ts +40 -0
- package/src/runtime/pipelineCache/bitfield.ts +225 -0
- package/src/runtime/pipelineCache/cache.ts +129 -0
- package/src/runtime/pipelineCache/descriptor.ts +150 -0
- package/src/runtime/pipelineCache/index.ts +37 -0
package/src/runtime/heapScene.ts
CHANGED
|
@@ -75,161 +75,29 @@ import {
|
|
|
75
75
|
isDerivedRule, STANDARD_DERIVED_RULES, STANDARD_TRAFO_LEAVES, inputsOf,
|
|
76
76
|
type RoRegistration, type DerivedRule,
|
|
77
77
|
} from "./derivedUniforms/index.js";
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
// Zero-alloc flat copy (row-major) straight into the f32 staging
|
|
102
|
-
// buffer — `copyTo` does `dst.set(m._data, off)` which narrows f64→f32
|
|
103
|
-
// on store, no throwaway `number[]` per call.
|
|
104
|
-
m.copyTo(dst, off);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// ─── Layout-driven value packing ────────────────────────────────────
|
|
108
|
-
//
|
|
109
|
-
// Maps a schema uniform name + its value source to bytes in a staging
|
|
110
|
-
// buffer. The bridge between the spec's named JS-side fields (e.g.
|
|
111
|
-
// `spec.modelTrafo: Trafo3d`) and the schema's typed uniforms
|
|
112
|
-
// ("ModelTrafo" mat4, "ModelTrafoInv" mat4, …). Step 5 generalises
|
|
113
|
-
// this to a `spec.uniforms: { [name]: aval }` map; until then we
|
|
114
|
-
// hardcode the shape here.
|
|
115
|
-
|
|
116
|
-
// ─── Generic packer registry, keyed on WGSL type ────────────────────
|
|
117
|
-
//
|
|
118
|
-
// Each per-draw uniform comes from the spec as an aval whose JS value
|
|
119
|
-
// type is determined by what the user passes. The packer for a given
|
|
120
|
-
// WGSL type knows how to turn that JS value into bytes for the arena.
|
|
121
|
-
// Step 5: this replaces the per-name `perDrawBinding` switch — the
|
|
122
|
-
// spec just supplies `uniforms: { [name]: aval<...> }` and the
|
|
123
|
-
// runtime asks the registry "how do I pack a `mat4x4<f32>`".
|
|
124
|
-
|
|
125
|
-
/** A packer for one WGSL storage-buffer type. */
|
|
126
|
-
interface WgslPacker {
|
|
127
|
-
/** Tightly-packed size in bytes of one value (mat4 = 64, vec3 = 12, …). */
|
|
128
|
-
readonly dataBytes: number;
|
|
129
|
-
readonly typeId: number;
|
|
130
|
-
/**
|
|
131
|
-
* Pack `val` (the aval's `.getValue(tok)` result) into `dst` at
|
|
132
|
-
* float offset `off`. The packer is responsible for handling the
|
|
133
|
-
* value type — Trafo3d, M44d, V4f, V3d, V3f, number — coercing as
|
|
134
|
-
* needed. Throws on unsupported value shapes.
|
|
135
|
-
*/
|
|
136
|
-
readonly pack: (val: unknown, dst: Float32Array, off: number) => void;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const PACKER_MAT4: WgslPacker = {
|
|
140
|
-
dataBytes: 64, typeId: 0,
|
|
141
|
-
pack: (val, dst, off) => {
|
|
142
|
-
// Accept Trafo3d (uses .forward) or M44d directly.
|
|
143
|
-
const m = (val as { forward?: M44d }).forward !== undefined
|
|
144
|
-
? (val as { forward: M44d }).forward
|
|
145
|
-
: (val as M44d);
|
|
146
|
-
packMat44(m, dst, off);
|
|
147
|
-
},
|
|
148
|
-
};
|
|
149
|
-
const PACKER_VEC4: WgslPacker = {
|
|
150
|
-
dataBytes: 16, typeId: 0,
|
|
151
|
-
pack: (val, dst, off) => {
|
|
152
|
-
const v = val as V4f;
|
|
153
|
-
dst[off + 0] = v.x; dst[off + 1] = v.y;
|
|
154
|
-
dst[off + 2] = v.z; dst[off + 3] = v.w;
|
|
155
|
-
},
|
|
156
|
-
};
|
|
157
|
-
const PACKER_VEC3: WgslPacker = {
|
|
158
|
-
dataBytes: 12, typeId: 0,
|
|
159
|
-
pack: (val, dst, off) => {
|
|
160
|
-
// V3f or V3d both expose .x/.y/.z; cast through a common shape.
|
|
161
|
-
const v = val as { x: number; y: number; z: number };
|
|
162
|
-
dst[off + 0] = v.x; dst[off + 1] = v.y; dst[off + 2] = v.z;
|
|
163
|
-
},
|
|
164
|
-
};
|
|
165
|
-
const PACKER_VEC2: WgslPacker = {
|
|
166
|
-
dataBytes: 8, typeId: 0,
|
|
167
|
-
pack: (val, dst, off) => {
|
|
168
|
-
const v = val as { x: number; y: number };
|
|
169
|
-
dst[off + 0] = v.x; dst[off + 1] = v.y;
|
|
170
|
-
},
|
|
171
|
-
};
|
|
172
|
-
const PACKER_F32: WgslPacker = {
|
|
173
|
-
dataBytes: 4, typeId: 0,
|
|
174
|
-
pack: (val, dst, off) => { dst[off] = val as number; },
|
|
175
|
-
};
|
|
176
|
-
// Integer scalars / vectors. The arena is fronted by a `Float32Array`,
|
|
177
|
-
// so writing raw bits has to go through a same-buffer Uint32/Int32 view
|
|
178
|
-
// to avoid the lossy `i32 → f32` coercion you'd get from a direct
|
|
179
|
-
// `dst[off] = ...` assignment.
|
|
180
|
-
function makeIntPacker(
|
|
181
|
-
ctor: typeof Uint32Array | typeof Int32Array,
|
|
182
|
-
dim: 1 | 2 | 3 | 4,
|
|
183
|
-
): WgslPacker {
|
|
184
|
-
const bytes = dim * 4;
|
|
185
|
-
if (dim === 1) {
|
|
186
|
-
return {
|
|
187
|
-
dataBytes: bytes, typeId: 0,
|
|
188
|
-
pack: (val, dst, off) => {
|
|
189
|
-
new ctor(dst.buffer as ArrayBuffer, dst.byteOffset + off * 4, 1)[0] = val as number;
|
|
190
|
-
},
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
// Vector: accept {x,y,z,w} components.
|
|
194
|
-
return {
|
|
195
|
-
dataBytes: bytes, typeId: 0,
|
|
196
|
-
pack: (val, dst, off) => {
|
|
197
|
-
const view = new ctor(dst.buffer as ArrayBuffer, dst.byteOffset + off * 4, dim);
|
|
198
|
-
const v = val as { x: number; y: number; z?: number; w?: number };
|
|
199
|
-
view[0] = v.x; view[1] = v.y;
|
|
200
|
-
if (dim >= 3) view[2] = v.z!;
|
|
201
|
-
if (dim >= 4) view[3] = v.w!;
|
|
202
|
-
},
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
const PACKER_U32 = makeIntPacker(Uint32Array, 1);
|
|
206
|
-
const PACKER_UVEC2 = makeIntPacker(Uint32Array, 2);
|
|
207
|
-
const PACKER_UVEC3 = makeIntPacker(Uint32Array, 3);
|
|
208
|
-
const PACKER_UVEC4 = makeIntPacker(Uint32Array, 4);
|
|
209
|
-
const PACKER_I32 = makeIntPacker(Int32Array, 1);
|
|
210
|
-
const PACKER_IVEC2 = makeIntPacker(Int32Array, 2);
|
|
211
|
-
const PACKER_IVEC3 = makeIntPacker(Int32Array, 3);
|
|
212
|
-
const PACKER_IVEC4 = makeIntPacker(Int32Array, 4);
|
|
213
|
-
|
|
214
|
-
function packerForWgslType(wgslType: string): WgslPacker {
|
|
215
|
-
switch (wgslType) {
|
|
216
|
-
case "mat4x4<f32>": return PACKER_MAT4;
|
|
217
|
-
case "vec4<f32>": return PACKER_VEC4;
|
|
218
|
-
case "vec3<f32>": return PACKER_VEC3;
|
|
219
|
-
case "vec2<f32>": return PACKER_VEC2;
|
|
220
|
-
case "f32": return PACKER_F32;
|
|
221
|
-
case "u32": return PACKER_U32;
|
|
222
|
-
case "vec2<u32>": return PACKER_UVEC2;
|
|
223
|
-
case "vec3<u32>": return PACKER_UVEC3;
|
|
224
|
-
case "vec4<u32>": return PACKER_UVEC4;
|
|
225
|
-
case "i32": return PACKER_I32;
|
|
226
|
-
case "vec2<i32>": return PACKER_IVEC2;
|
|
227
|
-
case "vec3<i32>": return PACKER_IVEC3;
|
|
228
|
-
case "vec4<i32>": return PACKER_IVEC4;
|
|
229
|
-
default:
|
|
230
|
-
throw new Error(`heapScene: no JS-side packer for WGSL type '${wgslType}'`);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
78
|
+
import { packerForWgslType, PACKER_MAT4, type WgslPacker } from "./heapScene/packers.js";
|
|
79
|
+
import {
|
|
80
|
+
HEAP_SCAN_WGSL, SCAN_TILE_SIZE, SCAN_WG_SIZE, SCAN_MAX_RECORDS,
|
|
81
|
+
TILE_K, RECORD_U32, RECORD_BYTES,
|
|
82
|
+
} from "./heapScene/scanKernel.js";
|
|
83
|
+
import {
|
|
84
|
+
GrowBuffer, MIN_BUFFER_BYTES, POW2, ALIGN16,
|
|
85
|
+
} from "./heapScene/growBuffer.js";
|
|
86
|
+
import {
|
|
87
|
+
UniformPool, IndexPool, DrawHeap,
|
|
88
|
+
AttributeArena, IndexAllocator, insertSortedFreeBlock,
|
|
89
|
+
buildArenaState, arenaBytes, writeAttribute,
|
|
90
|
+
asAval, isBufferView, asFloat32,
|
|
91
|
+
ALLOC_HEADER_BYTES, ALLOC_HEADER_PAD_TO,
|
|
92
|
+
ENC_V3F_TIGHT, SEM_POSITIONS, SEM_NORMALS,
|
|
93
|
+
type ArenaState,
|
|
94
|
+
} from "./heapScene/pools.js";
|
|
95
|
+
import { encodeModeKey } from "./pipelineCache/index.js";
|
|
96
|
+
import { snapshotDescriptor, ModeKeyTracker } from "./derivedModes/modeKeyCpu.js";
|
|
97
|
+
import { addMarkingCallback } from "@aardworx/wombat.adaptive";
|
|
98
|
+
|
|
99
|
+
// GrowBuffer + ALIGN16 + MIN_BUFFER_BYTES + POW2 live in ./heapScene/growBuffer.
|
|
100
|
+
// Packers (WGSL-type → JS-value → arena bytes) live in ./heapScene/packers.
|
|
233
101
|
|
|
234
102
|
// ---------------------------------------------------------------------------
|
|
235
103
|
// Shared WGSL prelude (struct + bindings + VS)
|
|
@@ -268,765 +136,65 @@ export interface HeapGeometry {
|
|
|
268
136
|
}
|
|
269
137
|
|
|
270
138
|
|
|
271
|
-
// ---------------------------------------------------------------------------
|
|
272
|
-
// Resizable buffer (pow2 grow + GPU-side copy on resize)
|
|
273
|
-
// ---------------------------------------------------------------------------
|
|
274
|
-
|
|
275
|
-
const MIN_BUFFER_BYTES = 64 * 1024;
|
|
276
|
-
const POW2 = (n: number): number => {
|
|
277
|
-
let p = 1; while (p < n) p <<= 1; return p;
|
|
278
|
-
};
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* A GPUBuffer that can grow to next power-of-two on demand. On grow,
|
|
282
|
-
* a fresh buffer is created at the new size, the live tail copied
|
|
283
|
-
* over via copyBufferToBuffer, and dependents (bind groups, mostly)
|
|
284
|
-
* are notified to rebuild via the `onResize` callback.
|
|
285
|
-
*
|
|
286
|
-
* `usedBytes` is the high-water mark — the runtime advances this as
|
|
287
|
-
* it allocates, and `ensureCapacity` grows when required. This
|
|
288
|
-
* separates allocation policy from grow policy.
|
|
289
|
-
*/
|
|
290
|
-
class GrowBuffer {
|
|
291
|
-
private buf: GPUBuffer;
|
|
292
|
-
private cap: number;
|
|
293
|
-
private used = 0;
|
|
294
|
-
private readonly listeners = new Set<() => void>();
|
|
295
|
-
constructor(
|
|
296
|
-
private readonly device: GPUDevice,
|
|
297
|
-
private readonly label: string,
|
|
298
|
-
private readonly usage: GPUBufferUsageFlags,
|
|
299
|
-
initialBytes: number,
|
|
300
|
-
) {
|
|
301
|
-
this.cap = Math.max(MIN_BUFFER_BYTES, POW2(initialBytes));
|
|
302
|
-
this.buf = device.createBuffer({ size: this.cap, usage: usage | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC, label });
|
|
303
|
-
}
|
|
304
|
-
get buffer(): GPUBuffer { return this.buf; }
|
|
305
|
-
get capacity(): number { return this.cap; }
|
|
306
|
-
get usedBytes(): number { return this.used; }
|
|
307
|
-
setUsed(n: number): void { this.used = n; }
|
|
308
|
-
onResize(cb: () => void): IDisposable {
|
|
309
|
-
this.listeners.add(cb);
|
|
310
|
-
return { dispose: () => { this.listeners.delete(cb); } };
|
|
311
|
-
}
|
|
312
|
-
/** Ensure the buffer is at least `bytes` capacity. Grows by pow2 + copies live tail. */
|
|
313
|
-
ensureCapacity(bytes: number): void {
|
|
314
|
-
if (bytes <= this.cap) return;
|
|
315
|
-
const newCap = POW2(bytes);
|
|
316
|
-
const newBuf = this.device.createBuffer({
|
|
317
|
-
size: newCap, usage: this.usage | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC, label: this.label,
|
|
318
|
-
});
|
|
319
|
-
if (this.used > 0) {
|
|
320
|
-
const enc = this.device.createCommandEncoder({ label: `${this.label}: grow-copy` });
|
|
321
|
-
enc.copyBufferToBuffer(this.buf, 0, newBuf, 0, ALIGN16(this.used));
|
|
322
|
-
this.device.queue.submit([enc.finish()]);
|
|
323
|
-
}
|
|
324
|
-
this.buf.destroy();
|
|
325
|
-
this.buf = newBuf;
|
|
326
|
-
this.cap = newCap;
|
|
327
|
-
for (const cb of this.listeners) cb();
|
|
328
|
-
}
|
|
329
|
-
destroy(): void { this.buf.destroy(); }
|
|
330
|
-
}
|
|
331
139
|
|
|
332
140
|
// ---------------------------------------------------------------------------
|
|
333
|
-
//
|
|
334
|
-
// ---------------------------------------------------------------------------
|
|
335
|
-
|
|
336
|
-
interface PoolEntry {
|
|
337
|
-
/** Byte offset into the arena. Data starts at ref + ALLOC_HEADER_PAD_TO. */
|
|
338
|
-
readonly ref: number;
|
|
339
|
-
readonly dataBytes: number;
|
|
340
|
-
readonly typeId: number;
|
|
341
|
-
/** Packer used to refresh the data region on aval marks. */
|
|
342
|
-
readonly pack: (val: unknown, dst: Float32Array, off: number) => void;
|
|
343
|
-
refcount: number;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* Aval-keyed pool of arena allocations. One allocation per unique
|
|
348
|
-
* aval (object identity). Two draws referencing the same aval share
|
|
349
|
-
* the allocation; their DrawHeaders carry the same u32 ref. Holds
|
|
350
|
-
* uniforms (fixed-size scalars/vectors/matrices) AND attribute arrays
|
|
351
|
-
* (variable-size). The caller decides `dataBytes` + `length` per
|
|
352
|
-
* acquisition — the pool just keys on aval identity and refcounts.
|
|
353
|
-
*
|
|
354
|
-
* Sharing emerges from aval identity — no separate "frequency"
|
|
355
|
-
* declaration needed. A `cval` shared by all draws → 1 alloc.
|
|
356
|
-
* A static positions array shared across instanced draws → 1 alloc.
|
|
357
|
-
* Same code path either way.
|
|
358
|
-
*/
|
|
359
|
-
class UniformPool {
|
|
360
|
-
// Keyed by `aval<unknown>` *by reference* (a plain JS `Map`). These
|
|
361
|
-
// keys are overwhelmingly reactive `cval`s (per-object trafos,
|
|
362
|
-
// colours, …) and the hot path is `acquire`/`release` ~once per
|
|
363
|
-
// drawHeader field per RO. A content-keyed `HashTable` would buy
|
|
364
|
-
// nothing here — reactive avals never compare content-equal — and
|
|
365
|
-
// would cost: reactive avals have no `equals`/`getHashCode`, so a
|
|
366
|
-
// `HashTable` falls back to a WeakMap-counter identity hash per
|
|
367
|
-
// lookup, measurably slower than `Map`'s native hashing. (Constant-
|
|
368
|
-
// aval dedup matters where keys are *texture* avals — there the
|
|
369
|
-
// `AtlasPool` is content-keyed; constant avals there carry a cached
|
|
370
|
-
// hash and a fast `equals`.)
|
|
371
|
-
private readonly byAval = new Map<aval<unknown>, PoolEntry>();
|
|
372
|
-
|
|
373
|
-
has(aval: aval<unknown>): boolean { return this.byAval.has(aval); }
|
|
374
|
-
entry(aval: aval<unknown>): PoolEntry | undefined { return this.byAval.get(aval); }
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* Acquire (or share) an allocation for `aval`. Caller passes the
|
|
378
|
-
* pre-read `value` (so the pool doesn't need a token) plus the
|
|
379
|
-
* (`dataBytes`, `typeId`, `length`, `pack`) describing how to lay
|
|
380
|
-
* it out. If a new allocation is made, the value is packed and
|
|
381
|
-
* uploaded immediately.
|
|
382
|
-
*/
|
|
383
|
-
acquire(
|
|
384
|
-
device: GPUDevice,
|
|
385
|
-
arena: AttributeArena,
|
|
386
|
-
aval: aval<unknown>,
|
|
387
|
-
value: unknown,
|
|
388
|
-
dataBytes: number,
|
|
389
|
-
typeId: number,
|
|
390
|
-
length: number,
|
|
391
|
-
pack: (val: unknown, dst: Float32Array, off: number) => void,
|
|
392
|
-
): number {
|
|
393
|
-
const existing = this.byAval.get(aval);
|
|
394
|
-
if (existing !== undefined) {
|
|
395
|
-
existing.refcount++;
|
|
396
|
-
return existing.ref;
|
|
397
|
-
}
|
|
398
|
-
const ref = arena.alloc(dataBytes);
|
|
399
|
-
const allocBytes = ALIGN16(ALLOC_HEADER_PAD_TO + dataBytes);
|
|
400
|
-
const buf = new ArrayBuffer(allocBytes);
|
|
401
|
-
const u32 = new Uint32Array(buf);
|
|
402
|
-
const f32 = new Float32Array(buf);
|
|
403
|
-
u32[0] = typeId;
|
|
404
|
-
u32[1] = length;
|
|
405
|
-
// stride_bytes (offset 8): bytes per element. Lets the VS decode
|
|
406
|
-
// pick V3- vs V4-tight load expressions for vec4 attributes
|
|
407
|
-
// (and is informative for everything else).
|
|
408
|
-
u32[2] = length > 0 ? Math.floor(dataBytes / length) : 0;
|
|
409
|
-
pack(value, f32, ALLOC_HEADER_PAD_TO / 4);
|
|
410
|
-
arena.write(ref, new Uint8Array(buf));
|
|
411
|
-
void device;
|
|
412
|
-
this.byAval.set(aval, { ref, dataBytes, typeId, pack, refcount: 1 });
|
|
413
|
-
return ref;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
/** Decrement refcount; if zero, free the arena allocation. */
|
|
417
|
-
release(arena: AttributeArena, aval: aval<unknown>): void {
|
|
418
|
-
const e = this.byAval.get(aval);
|
|
419
|
-
if (e === undefined) return;
|
|
420
|
-
e.refcount--;
|
|
421
|
-
if (e.refcount > 0) return;
|
|
422
|
-
arena.release(e.ref, ALIGN16(ALLOC_HEADER_PAD_TO + e.dataBytes));
|
|
423
|
-
this.byAval.delete(aval);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
/** Re-pack one entry's data region into the arena's CPU shadow. */
|
|
427
|
-
repack(device: GPUDevice, arena: AttributeArena, aval: aval<unknown>, val: unknown): void {
|
|
428
|
-
const e = this.byAval.get(aval);
|
|
429
|
-
if (e === undefined) return;
|
|
430
|
-
const dst = new Float32Array(e.dataBytes / 4);
|
|
431
|
-
e.pack(val, dst, 0);
|
|
432
|
-
arena.write(
|
|
433
|
-
e.ref + ALLOC_HEADER_PAD_TO,
|
|
434
|
-
new Uint8Array(dst.buffer, dst.byteOffset, e.dataBytes),
|
|
435
|
-
);
|
|
436
|
-
void device;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
/**
|
|
441
|
-
* Aval-keyed pool over the `IndexAllocator`. Two draws referencing
|
|
442
|
-
* the same `Uint32Array` (or aval thereof) share an index range —
|
|
443
|
-
* 19K instanced clones of the same mesh share one allocation, one
|
|
444
|
-
* upload. Index data is treated as immutable for the aval's
|
|
445
|
-
* lifetime: an aval mark won't repack (we'd have to free + re-alloc
|
|
446
|
-
* since size changes are likely). Use a fresh aval to swap meshes.
|
|
447
|
-
*
|
|
448
|
-
* **Value-equality dedup for constant avals (§5b):** when an
|
|
449
|
-
* incoming aval has `isConstant === true`, the pool also keys by
|
|
450
|
-
* the underlying `ArrayBuffer` tuple `(buffer, byteOffset,
|
|
451
|
-
* byteLength)`. Two distinct constant avals wrapping the same
|
|
452
|
-
* `Uint32Array` view (or two views over the same backing buffer
|
|
453
|
-
* with matching offsets) collapse to one allocation. Hashing
|
|
454
|
-
* kilobytes of indices on every acquire would be wasteful; the
|
|
455
|
-
* tuple key catches the realistic "one ArrayBuffer shared across
|
|
456
|
-
* many aval wrappers" pattern, which is the only one that matters
|
|
457
|
-
* for the heap path. Reactive (non-constant) avals fall through
|
|
458
|
-
* to identity-only — their content can change and the pool can't
|
|
459
|
-
* silently merge them.
|
|
460
|
-
*/
|
|
461
|
-
class IndexPool {
|
|
462
|
-
// Per-aval binding. `perAvalCount` tracks acquire/release balance
|
|
463
|
-
// for THIS aval; `entry` is the shared allocation (one entry can be
|
|
464
|
-
// referenced by many aliasing avals via §5b dedup).
|
|
465
|
-
private readonly byAval = new Map<
|
|
466
|
-
aval<Uint32Array>,
|
|
467
|
-
{ entry: IndexPoolEntry; perAvalCount: number }
|
|
468
|
-
>();
|
|
469
|
-
private readonly byValueKey = new Map<string, IndexPoolEntry>();
|
|
470
|
-
// Stable per-ArrayBuffer numeric id for value-key composition.
|
|
471
|
-
// WeakMap-backed so buffers GC'd elsewhere drop their entry too.
|
|
472
|
-
private readonly bufferIds = new WeakMap<ArrayBufferLike, number>();
|
|
473
|
-
private nextBufferId = 1;
|
|
474
|
-
private bufferIdOf(buf: ArrayBufferLike): number {
|
|
475
|
-
let id = this.bufferIds.get(buf);
|
|
476
|
-
if (id === undefined) {
|
|
477
|
-
id = this.nextBufferId++;
|
|
478
|
-
this.bufferIds.set(buf, id);
|
|
479
|
-
}
|
|
480
|
-
return id;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
acquire(
|
|
484
|
-
device: GPUDevice,
|
|
485
|
-
indices: IndexAllocator,
|
|
486
|
-
aval: aval<Uint32Array>,
|
|
487
|
-
arr: Uint32Array,
|
|
488
|
-
): { firstIndex: number; count: number } {
|
|
489
|
-
const bound = this.byAval.get(aval);
|
|
490
|
-
if (bound !== undefined) {
|
|
491
|
-
bound.perAvalCount++;
|
|
492
|
-
bound.entry.totalRefcount++;
|
|
493
|
-
return { firstIndex: bound.entry.firstIndex, count: bound.entry.count };
|
|
494
|
-
}
|
|
495
|
-
let valueKey: string | undefined;
|
|
496
|
-
if (aval.isConstant) {
|
|
497
|
-
valueKey = `${this.bufferIdOf(arr.buffer)}:${arr.byteOffset}:${arr.byteLength}`;
|
|
498
|
-
const shared = this.byValueKey.get(valueKey);
|
|
499
|
-
if (shared !== undefined) {
|
|
500
|
-
shared.totalRefcount++;
|
|
501
|
-
this.byAval.set(aval, { entry: shared, perAvalCount: 1 });
|
|
502
|
-
return { firstIndex: shared.firstIndex, count: shared.count };
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
const firstIndex = indices.alloc(arr.length);
|
|
506
|
-
indices.write(
|
|
507
|
-
firstIndex * 4,
|
|
508
|
-
new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength),
|
|
509
|
-
);
|
|
510
|
-
void device;
|
|
511
|
-
const entry: IndexPoolEntry = { firstIndex, count: arr.length, totalRefcount: 1, valueKey };
|
|
512
|
-
this.byAval.set(aval, { entry, perAvalCount: 1 });
|
|
513
|
-
if (valueKey !== undefined) this.byValueKey.set(valueKey, entry);
|
|
514
|
-
return { firstIndex, count: arr.length };
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
release(indices: IndexAllocator, aval: aval<Uint32Array>): void {
|
|
518
|
-
const bound = this.byAval.get(aval);
|
|
519
|
-
if (bound === undefined) return;
|
|
520
|
-
bound.perAvalCount--;
|
|
521
|
-
bound.entry.totalRefcount--;
|
|
522
|
-
if (bound.perAvalCount === 0) this.byAval.delete(aval);
|
|
523
|
-
if (bound.entry.totalRefcount > 0) return;
|
|
524
|
-
indices.release(bound.entry.firstIndex, bound.entry.count);
|
|
525
|
-
if (bound.entry.valueKey !== undefined) {
|
|
526
|
-
this.byValueKey.delete(bound.entry.valueKey);
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
interface IndexPoolEntry {
|
|
532
|
-
firstIndex: number;
|
|
533
|
-
count: number;
|
|
534
|
-
totalRefcount: number;
|
|
535
|
-
valueKey: string | undefined;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
// ---------------------------------------------------------------------------
|
|
539
|
-
// DrawHeap (slot-indexed) and AttributeArena (byte-bump) allocators
|
|
141
|
+
// Internal state
|
|
540
142
|
// ---------------------------------------------------------------------------
|
|
541
143
|
|
|
542
144
|
/**
|
|
543
|
-
*
|
|
544
|
-
*
|
|
545
|
-
*
|
|
546
|
-
*
|
|
547
|
-
*/
|
|
548
|
-
class DrawHeap {
|
|
549
|
-
private free: number[] = [];
|
|
550
|
-
private nextSlot = 0;
|
|
551
|
-
constructor(private readonly buf: GrowBuffer, private readonly slotBytes: number) {}
|
|
552
|
-
get buffer(): GPUBuffer { return this.buf.buffer; }
|
|
553
|
-
/** Bytes per slot — caller multiplies by slot index for byte offsets. */
|
|
554
|
-
get bytesPerSlot(): number { return this.slotBytes; }
|
|
555
|
-
/** High-water mark in bytes (used to size bind-group entry on rebuild). */
|
|
556
|
-
get usedBytes(): number { return this.nextSlot * this.slotBytes; }
|
|
557
|
-
alloc(): number {
|
|
558
|
-
const slot = this.free.length > 0 ? this.free.pop()! : this.nextSlot++;
|
|
559
|
-
this.buf.ensureCapacity((slot + 1) * this.slotBytes);
|
|
560
|
-
this.buf.setUsed(Math.max(this.buf.usedBytes, (slot + 1) * this.slotBytes));
|
|
561
|
-
return slot;
|
|
562
|
-
}
|
|
563
|
-
release(slot: number): void { this.free.push(slot); }
|
|
564
|
-
onResize(cb: () => void): IDisposable { return this.buf.onResize(cb); }
|
|
565
|
-
destroy(): void { this.buf.destroy(); }
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
/**
|
|
569
|
-
* Byte-bump allocator over a GrowBuffer for variable-size attribute
|
|
570
|
-
* allocations. Each allocation gets a 16-byte aligned start (8-byte
|
|
571
|
-
* (typeId, length) header at the start, data 16 bytes in). Frees go
|
|
572
|
-
* onto a list keyed by size for simple first-fit reuse later — for
|
|
573
|
-
* now `release` just records the gap and the bump cursor never
|
|
574
|
-
* shrinks.
|
|
575
|
-
*/
|
|
576
|
-
class AttributeArena {
|
|
577
|
-
private cursor = 0;
|
|
578
|
-
// (offset, size) free entries; first-fit reuse not yet implemented.
|
|
579
|
-
private freeList: { off: number; size: number }[] = [];
|
|
580
|
-
// CPU shadow of the entire GPU buffer. Writes go here first; a
|
|
581
|
-
// single `device.queue.writeBuffer` per dirty contiguous range
|
|
582
|
-
// lifts them to the GPU at flush time. At the cost of doubling
|
|
583
|
-
// host memory we collapse N small writeBuffer calls (10K+ at
|
|
584
|
-
// initial population) to 1 per frame.
|
|
585
|
-
private shadow: Uint8Array;
|
|
586
|
-
private dirtyMin = Infinity;
|
|
587
|
-
private dirtyMax = 0;
|
|
588
|
-
constructor(private readonly buf: GrowBuffer) {
|
|
589
|
-
this.shadow = new Uint8Array(buf.capacity);
|
|
590
|
-
buf.onResize(() => {
|
|
591
|
-
const grown = new Uint8Array(buf.capacity);
|
|
592
|
-
grown.set(this.shadow);
|
|
593
|
-
this.shadow = grown;
|
|
594
|
-
});
|
|
595
|
-
}
|
|
596
|
-
get buffer(): GPUBuffer { return this.buf.buffer; }
|
|
597
|
-
get capacity(): number { return this.buf.capacity; }
|
|
598
|
-
get usedBytes(): number { return this.cursor; }
|
|
599
|
-
/**
|
|
600
|
-
* Stage `data` to the shadow at byte offset `dst`. Tracks the
|
|
601
|
-
* dirty range so `flush(device)` can emit a single writeBuffer
|
|
602
|
-
* covering everything dirty since the last flush.
|
|
603
|
-
*/
|
|
604
|
-
write(dst: number, data: Uint8Array): void {
|
|
605
|
-
this.shadow.set(data, dst);
|
|
606
|
-
if (dst < this.dirtyMin) this.dirtyMin = dst;
|
|
607
|
-
const end = dst + data.byteLength;
|
|
608
|
-
if (end > this.dirtyMax) this.dirtyMax = end;
|
|
609
|
-
}
|
|
610
|
-
flush(device: GPUDevice): void {
|
|
611
|
-
if (this.dirtyMax <= this.dirtyMin) return;
|
|
612
|
-
device.queue.writeBuffer(
|
|
613
|
-
this.buf.buffer, this.dirtyMin,
|
|
614
|
-
this.shadow.buffer, this.shadow.byteOffset + this.dirtyMin,
|
|
615
|
-
this.dirtyMax - this.dirtyMin,
|
|
616
|
-
);
|
|
617
|
-
this.dirtyMin = Infinity;
|
|
618
|
-
this.dirtyMax = 0;
|
|
619
|
-
}
|
|
620
|
-
/**
|
|
621
|
-
* Allocate space for one attribute. Returns the byte ref (offset
|
|
622
|
-
* to the header — data lives at ref + 16).
|
|
623
|
-
*/
|
|
624
|
-
alloc(dataBytes: number): number {
|
|
625
|
-
const allocBytes = ALIGN16(ALLOC_HEADER_PAD_TO + dataBytes);
|
|
626
|
-
// First-fit reuse from free list.
|
|
627
|
-
for (let i = 0; i < this.freeList.length; i++) {
|
|
628
|
-
const f = this.freeList[i]!;
|
|
629
|
-
if (f.size >= allocBytes) {
|
|
630
|
-
const ref = f.off;
|
|
631
|
-
if (f.size === allocBytes) this.freeList.splice(i, 1);
|
|
632
|
-
else { f.off += allocBytes; f.size -= allocBytes; }
|
|
633
|
-
return ref;
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
const ref = this.cursor;
|
|
637
|
-
this.cursor += allocBytes;
|
|
638
|
-
this.buf.ensureCapacity(this.cursor);
|
|
639
|
-
this.buf.setUsed(this.cursor);
|
|
640
|
-
return ref;
|
|
641
|
-
}
|
|
642
|
-
release(ref: number, dataBytes: number): void {
|
|
643
|
-
const allocBytes = ALIGN16(ALLOC_HEADER_PAD_TO + dataBytes);
|
|
644
|
-
insertSortedFreeBlock(this.freeList, ref, allocBytes);
|
|
645
|
-
}
|
|
646
|
-
onResize(cb: () => void): IDisposable { return this.buf.onResize(cb); }
|
|
647
|
-
destroy(): void { this.buf.destroy(); }
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
/**
|
|
651
|
-
* Insert `{off, size}` into a free-list kept sorted by `off`, then
|
|
652
|
-
* coalesce with the two immediate neighbours.
|
|
653
|
-
*
|
|
654
|
-
* The list invariant — sorted, non-overlapping, never-adjacent — is
|
|
655
|
-
* preserved across allocs (which take from the front or split a
|
|
656
|
-
* block) and releases (this function). The previous implementation
|
|
657
|
-
* did `push + Array.prototype.sort + linear coalesce scan`, which is
|
|
658
|
-
* O(N log N) per release. Under a 500-RO bulk-remove the sort
|
|
659
|
-
* dominated `removeDraw` (~41 ms of self-time in the heap-demo-sg
|
|
660
|
-
* toggle profile). Binary-search insert + 2-neighbour merge collapses
|
|
661
|
-
* that to O(log N + N-shift), and is principled — the sort never
|
|
662
|
-
* actually mattered since we already had the sorted prefix as an
|
|
663
|
-
* invariant.
|
|
664
|
-
*/
|
|
665
|
-
function insertSortedFreeBlock(
|
|
666
|
-
freeList: { off: number; size: number }[],
|
|
667
|
-
off: number,
|
|
668
|
-
size: number,
|
|
669
|
-
): void {
|
|
670
|
-
// Binary-search for the insertion index (first entry whose off > new).
|
|
671
|
-
let lo = 0, hi = freeList.length;
|
|
672
|
-
while (lo < hi) {
|
|
673
|
-
const mid = (lo + hi) >>> 1;
|
|
674
|
-
if (freeList[mid]!.off <= off) lo = mid + 1;
|
|
675
|
-
else hi = mid;
|
|
676
|
-
}
|
|
677
|
-
// lo is the index where the new entry would be inserted.
|
|
678
|
-
// Try merging with the predecessor first; if successful, the merged
|
|
679
|
-
// block may now be adjacent to its (former) successor too.
|
|
680
|
-
const prev = lo > 0 ? freeList[lo - 1] : undefined;
|
|
681
|
-
if (prev !== undefined && prev.off + prev.size === off) {
|
|
682
|
-
prev.size += size;
|
|
683
|
-
// Check forward-merge with what was freeList[lo].
|
|
684
|
-
const next = freeList[lo];
|
|
685
|
-
if (next !== undefined && prev.off + prev.size === next.off) {
|
|
686
|
-
prev.size += next.size;
|
|
687
|
-
freeList.splice(lo, 1);
|
|
688
|
-
}
|
|
689
|
-
return;
|
|
690
|
-
}
|
|
691
|
-
const next = freeList[lo];
|
|
692
|
-
if (next !== undefined && off + size === next.off) {
|
|
693
|
-
next.off = off;
|
|
694
|
-
next.size += size;
|
|
695
|
-
return;
|
|
696
|
-
}
|
|
697
|
-
freeList.splice(lo, 0, { off, size });
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
/**
|
|
701
|
-
* Element-bump allocator over an index GrowBuffer (units = u32). Each
|
|
702
|
-
* draw's index range is allocated as one block; on release the block
|
|
703
|
-
* is returned to a free list and can be reused first-fit.
|
|
145
|
+
* One pipeline-state bucket. Holds the pipeline + a list of global
|
|
146
|
+
* draw-slot indices to emit with it. The bind group it draws against
|
|
147
|
+
* is referenced by `bindGroup` (typically the shared no-textures one
|
|
148
|
+
* or one of the per-texture-set ones).
|
|
704
149
|
*/
|
|
705
|
-
class IndexAllocator {
|
|
706
|
-
private cursor = 0; // in u32s, not bytes
|
|
707
|
-
private freeList: { off: number; size: number }[] = [];
|
|
708
|
-
// CPU shadow + dirty range, same shape as AttributeArena. Index
|
|
709
|
-
// uploads (one per drawn mesh's index buffer) get coalesced to a
|
|
710
|
-
// single writeBuffer per dirty range at flush time.
|
|
711
|
-
private shadow: Uint8Array;
|
|
712
|
-
private dirtyMin = Infinity;
|
|
713
|
-
private dirtyMax = 0;
|
|
714
|
-
constructor(private readonly buf: GrowBuffer) {
|
|
715
|
-
this.shadow = new Uint8Array(buf.capacity);
|
|
716
|
-
buf.onResize(() => {
|
|
717
|
-
const grown = new Uint8Array(buf.capacity);
|
|
718
|
-
grown.set(this.shadow);
|
|
719
|
-
this.shadow = grown;
|
|
720
|
-
});
|
|
721
|
-
}
|
|
722
|
-
get buffer(): GPUBuffer { return this.buf.buffer; }
|
|
723
|
-
get usedElements(): number { return this.cursor; }
|
|
724
|
-
/** Stage `data` (bytes) at the given byte offset; tracks dirty range. */
|
|
725
|
-
write(dstByteOffset: number, data: Uint8Array): void {
|
|
726
|
-
this.shadow.set(data, dstByteOffset);
|
|
727
|
-
if (dstByteOffset < this.dirtyMin) this.dirtyMin = dstByteOffset;
|
|
728
|
-
const end = dstByteOffset + data.byteLength;
|
|
729
|
-
if (end > this.dirtyMax) this.dirtyMax = end;
|
|
730
|
-
}
|
|
731
|
-
flush(device: GPUDevice): void {
|
|
732
|
-
if (this.dirtyMax <= this.dirtyMin) return;
|
|
733
|
-
device.queue.writeBuffer(
|
|
734
|
-
this.buf.buffer, this.dirtyMin,
|
|
735
|
-
this.shadow.buffer, this.shadow.byteOffset + this.dirtyMin,
|
|
736
|
-
this.dirtyMax - this.dirtyMin,
|
|
737
|
-
);
|
|
738
|
-
this.dirtyMin = Infinity;
|
|
739
|
-
this.dirtyMax = 0;
|
|
740
|
-
}
|
|
741
|
-
alloc(elements: number): number {
|
|
742
|
-
for (let i = 0; i < this.freeList.length; i++) {
|
|
743
|
-
const f = this.freeList[i]!;
|
|
744
|
-
if (f.size >= elements) {
|
|
745
|
-
const off = f.off;
|
|
746
|
-
if (f.size === elements) this.freeList.splice(i, 1);
|
|
747
|
-
else { f.off += elements; f.size -= elements; }
|
|
748
|
-
return off;
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
const off = this.cursor;
|
|
752
|
-
this.cursor += elements;
|
|
753
|
-
this.buf.ensureCapacity(this.cursor * 4);
|
|
754
|
-
this.buf.setUsed(this.cursor * 4);
|
|
755
|
-
return off;
|
|
756
|
-
}
|
|
757
|
-
release(off: number, elements: number): void {
|
|
758
|
-
insertSortedFreeBlock(this.freeList, off, elements);
|
|
759
|
-
}
|
|
760
|
-
onResize(cb: () => void): IDisposable { return this.buf.onResize(cb); }
|
|
761
|
-
destroy(): void { this.buf.destroy(); }
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
// ---------------------------------------------------------------------------
|
|
765
|
-
// Static initial pack (uses the new allocators)
|
|
766
|
-
// ---------------------------------------------------------------------------
|
|
767
|
-
|
|
768
150
|
/**
|
|
769
|
-
*
|
|
770
|
-
*
|
|
771
|
-
*
|
|
772
|
-
*
|
|
773
|
-
*
|
|
151
|
+
* Per-slot state inside a bucket. Today every bucket has exactly ONE
|
|
152
|
+
* slot — `bucket.slots.length === 1`. Multi-pipeline buckets (the
|
|
153
|
+
* derived-modes work, see docs/derived-modes.md) grow this to one
|
|
154
|
+
* slot per distinct pipeline state used by the bucket's ROs. Each
|
|
155
|
+
* slot owns its own drawTable + scan buffers + indirect + pipeline.
|
|
774
156
|
*/
|
|
775
|
-
interface
|
|
776
|
-
|
|
777
|
-
readonly
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
function buildArenaState(
|
|
781
|
-
device: GPUDevice,
|
|
782
|
-
attrBytesHint: number,
|
|
783
|
-
idxBytesHint: number,
|
|
784
|
-
label: string,
|
|
785
|
-
idxExtraUsage: GPUBufferUsageFlags = 0,
|
|
786
|
-
): ArenaState {
|
|
787
|
-
const attrs = new AttributeArena(new GrowBuffer(
|
|
788
|
-
device, `${label}/attrs`, GPUBufferUsage.STORAGE,
|
|
789
|
-
attrBytesHint,
|
|
790
|
-
));
|
|
791
|
-
const indices = new IndexAllocator(new GrowBuffer(
|
|
792
|
-
device, `${label}/idx`, GPUBufferUsage.INDEX | idxExtraUsage,
|
|
793
|
-
idxBytesHint,
|
|
794
|
-
));
|
|
795
|
-
return { attrs, indices };
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
function arenaBytes(arena: ArenaState): number {
|
|
799
|
-
return arena.attrs.usedBytes + arena.indices.usedElements * 4;
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
/** Upload a single attribute — header (typeId, length) + data — into the arena at byte offset `ref`. */
|
|
803
|
-
function writeAttribute(
|
|
804
|
-
device: GPUDevice, buf: GPUBuffer, ref: number, typeId: number, length: number, data: Float32Array,
|
|
805
|
-
): void {
|
|
806
|
-
const allocBytes = ALIGN16(ALLOC_HEADER_PAD_TO + data.byteLength);
|
|
807
|
-
const staging = new ArrayBuffer(allocBytes);
|
|
808
|
-
const u32 = new Uint32Array(staging);
|
|
809
|
-
const f32 = new Float32Array(staging);
|
|
810
|
-
u32[0] = typeId;
|
|
811
|
-
u32[1] = length;
|
|
812
|
-
f32.set(data, ALLOC_HEADER_PAD_TO / 4);
|
|
813
|
-
device.queue.writeBuffer(buf, ref, staging, 0, allocBytes);
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
function asAval<T>(v: aval<T> | T): aval<T> {
|
|
817
|
-
return (typeof v === "object" && v !== null && typeof (v as { getValue?: unknown }).getValue === "function")
|
|
818
|
-
? (v as aval<T>)
|
|
819
|
-
: AVal.constant(v as T);
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
/** Heuristic predicate — BufferView has `buffer: aval<IBuffer>` + elementType. */
|
|
823
|
-
function isBufferView(v: unknown): v is BufferView {
|
|
824
|
-
if (typeof v !== "object" || v === null) return false;
|
|
825
|
-
const o = v as { buffer?: unknown; elementType?: unknown };
|
|
826
|
-
return typeof o.buffer === "object" && o.buffer !== null
|
|
827
|
-
&& typeof (o.buffer as { getValue?: unknown }).getValue === "function"
|
|
828
|
-
&& typeof o.elementType === "object" && o.elementType !== null;
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
/**
|
|
832
|
-
* Float32 view over a host-side buffer source. Used by the BufferView
|
|
833
|
-
* packer to hand the pool a typed array it can `set()` from.
|
|
834
|
-
*/
|
|
835
|
-
function asFloat32(data: HostBufferSource): Float32Array {
|
|
836
|
-
if (data instanceof Float32Array) return data;
|
|
837
|
-
if (ArrayBuffer.isView(data)) {
|
|
838
|
-
return new Float32Array(data.buffer, data.byteOffset, data.byteLength / 4);
|
|
839
|
-
}
|
|
840
|
-
return new Float32Array(data); // ArrayBuffer
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
// ---------------------------------------------------------------------------
|
|
844
|
-
// Megacall GPU prefix-sum compute shader
|
|
845
|
-
// ---------------------------------------------------------------------------
|
|
846
|
-
|
|
847
|
-
const SCAN_TILE_SIZE = 512;
|
|
848
|
-
const SCAN_WG_SIZE = 256;
|
|
849
|
-
const SCAN_MAX_RECORDS = SCAN_TILE_SIZE * SCAN_TILE_SIZE; // numBlocks ≤ TILE_SIZE
|
|
850
|
-
|
|
851
|
-
const TILE_K = 64;
|
|
852
|
-
|
|
853
|
-
/** drawTable record width: (firstEmit, drawIdx, indexStart, indexCount, instanceCount). */
|
|
854
|
-
const RECORD_U32 = 5;
|
|
855
|
-
const RECORD_BYTES = RECORD_U32 * 4;
|
|
856
|
-
|
|
857
|
-
const HEAP_SCAN_WGSL = `
|
|
858
|
-
struct Params {
|
|
859
|
-
numRecords: u32,
|
|
860
|
-
numBlocks: u32,
|
|
861
|
-
_pad0: u32,
|
|
862
|
-
_pad1: u32,
|
|
863
|
-
};
|
|
864
|
-
|
|
865
|
-
struct Record {
|
|
866
|
-
firstEmit: u32,
|
|
867
|
-
drawIdx: u32,
|
|
868
|
-
indexStart: u32,
|
|
869
|
-
indexCount: u32,
|
|
870
|
-
instanceCount: u32,
|
|
871
|
-
};
|
|
872
|
-
|
|
873
|
-
@group(0) @binding(0) var<storage, read_write> drawTable: array<Record>;
|
|
874
|
-
@group(0) @binding(1) var<storage, read_write> blockSums: array<u32>;
|
|
875
|
-
@group(0) @binding(2) var<storage, read_write> blockOffsets: array<u32>;
|
|
876
|
-
@group(0) @binding(3) var<storage, read_write> indirect: array<u32>;
|
|
877
|
-
@group(0) @binding(4) var<uniform> params: Params;
|
|
878
|
-
@group(0) @binding(5) var<storage, read_write> firstDrawInTile: array<u32>;
|
|
879
|
-
|
|
880
|
-
const TILE_SIZE: u32 = 512u;
|
|
881
|
-
const WG_SIZE: u32 = 256u;
|
|
882
|
-
const TILE_K: u32 = 64u;
|
|
883
|
-
|
|
884
|
-
var<workgroup> sdata: array<u32, 512>;
|
|
885
|
-
|
|
886
|
-
fn blellochScan(tid: u32) {
|
|
887
|
-
var offset: u32 = 1u;
|
|
888
|
-
for (var d: u32 = TILE_SIZE >> 1u; d > 0u; d = d >> 1u) {
|
|
889
|
-
workgroupBarrier();
|
|
890
|
-
if (tid < d) {
|
|
891
|
-
let ai = offset * (2u * tid + 1u) - 1u;
|
|
892
|
-
let bi = offset * (2u * tid + 2u) - 1u;
|
|
893
|
-
sdata[bi] = sdata[bi] + sdata[ai];
|
|
894
|
-
}
|
|
895
|
-
offset = offset * 2u;
|
|
896
|
-
}
|
|
897
|
-
if (tid == 0u) { sdata[TILE_SIZE - 1u] = 0u; }
|
|
898
|
-
for (var d: u32 = 1u; d < TILE_SIZE; d = d * 2u) {
|
|
899
|
-
offset = offset >> 1u;
|
|
900
|
-
workgroupBarrier();
|
|
901
|
-
if (tid < d) {
|
|
902
|
-
let ai = offset * (2u * tid + 1u) - 1u;
|
|
903
|
-
let bi = offset * (2u * tid + 2u) - 1u;
|
|
904
|
-
let t = sdata[ai];
|
|
905
|
-
sdata[ai] = sdata[bi];
|
|
906
|
-
sdata[bi] = sdata[bi] + t;
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
workgroupBarrier();
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
@compute @workgroup_size(WG_SIZE)
|
|
913
|
-
fn scanTile(@builtin(local_invocation_id) lid: vec3<u32>, @builtin(workgroup_id) wgid: vec3<u32>) {
|
|
914
|
-
let tid = lid.x;
|
|
915
|
-
let blockOff = wgid.x * TILE_SIZE;
|
|
916
|
-
let n = params.numRecords;
|
|
917
|
-
let i0 = blockOff + tid;
|
|
918
|
-
let i1 = blockOff + tid + WG_SIZE;
|
|
919
|
-
var v0: u32 = 0u;
|
|
920
|
-
var v1: u32 = 0u;
|
|
921
|
-
if (i0 < n) { v0 = drawTable[i0].indexCount * drawTable[i0].instanceCount; }
|
|
922
|
-
if (i1 < n) { v1 = drawTable[i1].indexCount * drawTable[i1].instanceCount; }
|
|
923
|
-
sdata[tid] = v0;
|
|
924
|
-
sdata[tid + WG_SIZE] = v1;
|
|
925
|
-
workgroupBarrier();
|
|
926
|
-
blellochScan(tid);
|
|
927
|
-
if (i0 < n) { drawTable[i0].firstEmit = sdata[tid]; }
|
|
928
|
-
if (i1 < n) { drawTable[i1].firstEmit = sdata[tid + WG_SIZE]; }
|
|
929
|
-
if (tid == WG_SIZE - 1u) {
|
|
930
|
-
blockSums[wgid.x] = sdata[tid + WG_SIZE] + v1;
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
@compute @workgroup_size(WG_SIZE)
|
|
935
|
-
fn scanBlocks(@builtin(local_invocation_id) lid: vec3<u32>) {
|
|
936
|
-
let tid = lid.x;
|
|
937
|
-
let n = params.numBlocks;
|
|
938
|
-
let i0 = tid;
|
|
939
|
-
let i1 = tid + WG_SIZE;
|
|
940
|
-
var v0: u32 = 0u;
|
|
941
|
-
var v1: u32 = 0u;
|
|
942
|
-
if (i0 < n) { v0 = blockSums[i0]; }
|
|
943
|
-
if (i1 < n) { v1 = blockSums[i1]; }
|
|
944
|
-
sdata[tid] = v0;
|
|
945
|
-
sdata[tid + WG_SIZE] = v1;
|
|
946
|
-
workgroupBarrier();
|
|
947
|
-
blellochScan(tid);
|
|
948
|
-
if (i0 < n) { blockOffsets[i0] = sdata[tid]; }
|
|
949
|
-
if (i1 < n) { blockOffsets[i1] = sdata[tid + WG_SIZE]; }
|
|
950
|
-
workgroupBarrier();
|
|
951
|
-
if (tid == 0u) {
|
|
952
|
-
if (n > 0u) {
|
|
953
|
-
let lastIdx = n - 1u;
|
|
954
|
-
let total = blockOffsets[lastIdx] + blockSums[lastIdx];
|
|
955
|
-
indirect[0] = total;
|
|
956
|
-
} else {
|
|
957
|
-
indirect[0] = 0u;
|
|
958
|
-
}
|
|
959
|
-
indirect[1] = 1u;
|
|
960
|
-
indirect[2] = 0u;
|
|
961
|
-
indirect[3] = 0u;
|
|
962
|
-
}
|
|
963
|
-
}
|
|
157
|
+
interface BucketSlot {
|
|
158
|
+
/** The render pipeline this slot draws with. */
|
|
159
|
+
readonly pipeline: GPURenderPipeline;
|
|
964
160
|
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
161
|
+
// ─── drawTable / megacall state ────────────────────────────────────
|
|
162
|
+
drawTableBuf?: GrowBuffer;
|
|
163
|
+
drawTableShadow?: Uint32Array;
|
|
164
|
+
drawTableDirtyMin: number;
|
|
165
|
+
drawTableDirtyMax: number;
|
|
166
|
+
/** Number of live records routed to this slot (= drawTable length).
|
|
167
|
+
* GPU owns firstEmit / total. */
|
|
168
|
+
recordCount: number;
|
|
169
|
+
/** localSlot → recordIdx (or -1). */
|
|
170
|
+
slotToRecord: number[];
|
|
171
|
+
/** recordIdx → localSlot. */
|
|
172
|
+
recordToSlot: number[];
|
|
976
173
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
if (tileIdx == numTiles) {
|
|
990
|
-
// Sentinel for the open upper bound — the LAST VALID SLOT, not
|
|
991
|
-
// numRecords. The render VS uses
|
|
992
|
-
// hi = firstDrawInTile[_tileIdx + 1u]
|
|
993
|
-
// and the binary search treats hi as INCLUSIVE. If the sentinel
|
|
994
|
-
// were numRecords (one past last), the search would drag lo into
|
|
995
|
-
// the OOB slot for emits in the last tile, since drawTable reads
|
|
996
|
-
// past recordCount return 0 (binding size clamping) and 0 ≤ emit
|
|
997
|
-
// is always true. Visible symptom: the LAST few emits in the
|
|
998
|
-
// bucket land on slot=numRecords (drawIdx=0, indexCount=0 → /-by-
|
|
999
|
-
// zero) → degenerate / cross-RO triangle stitched to slot 0.
|
|
1000
|
-
firstDrawInTile[tileIdx] = params.numRecords - 1u;
|
|
1001
|
-
return;
|
|
1002
|
-
}
|
|
1003
|
-
let tileStart = tileIdx * TILE_K;
|
|
1004
|
-
var lo: u32 = 0u;
|
|
1005
|
-
var hi: u32 = params.numRecords - 1u;
|
|
1006
|
-
loop {
|
|
1007
|
-
if (lo >= hi) { break; }
|
|
1008
|
-
let mid = (lo + hi + 1u) >> 1u;
|
|
1009
|
-
if (drawTable[mid].firstEmit <= tileStart) { lo = mid; } else { hi = mid - 1u; }
|
|
1010
|
-
}
|
|
1011
|
-
firstDrawInTile[tileIdx] = lo;
|
|
174
|
+
// ─── Per-slot scan / indirect buffers ──────────────────────────────
|
|
175
|
+
blockSumsBuf?: GrowBuffer;
|
|
176
|
+
blockOffsetsBuf?: GrowBuffer;
|
|
177
|
+
firstDrawInTileBuf?: GrowBuffer;
|
|
178
|
+
/** CPU sum of indexCounts across live records — drives firstDrawInTileBuf sizing only. */
|
|
179
|
+
totalEmitEstimate: number;
|
|
180
|
+
indirectBuf?: GPUBuffer;
|
|
181
|
+
paramsBuf?: GPUBuffer;
|
|
182
|
+
scanBindGroup?: GPUBindGroup;
|
|
183
|
+
/** numRecords used to size the current render bindGroup; rebuild when it changes. */
|
|
184
|
+
renderBoundRecordCount?: number;
|
|
185
|
+
scanDirty: boolean;
|
|
1012
186
|
}
|
|
1013
|
-
`;
|
|
1014
187
|
|
|
1015
|
-
// ---------------------------------------------------------------------------
|
|
1016
|
-
// Internal state
|
|
1017
|
-
// ---------------------------------------------------------------------------
|
|
1018
|
-
|
|
1019
|
-
/**
|
|
1020
|
-
* One pipeline-state bucket. Holds the pipeline + a list of global
|
|
1021
|
-
* draw-slot indices to emit with it. The bind group it draws against
|
|
1022
|
-
* is referenced by `bindGroup` (typically the shared no-textures one
|
|
1023
|
-
* or one of the per-texture-set ones).
|
|
1024
|
-
*/
|
|
1025
188
|
interface Bucket {
|
|
1026
189
|
readonly label: string;
|
|
1027
190
|
readonly textures: HeapTextureSet | undefined;
|
|
1028
191
|
readonly layout: BucketLayout;
|
|
1029
|
-
|
|
192
|
+
/**
|
|
193
|
+
* One entry per distinct pipeline state. Phase 5a: always length 1
|
|
194
|
+
* (per-RO pipeline-state still keyed at the bucket level).
|
|
195
|
+
* Later phases of the derived-modes work let `slots.length > 1`.
|
|
196
|
+
*/
|
|
197
|
+
readonly slots: BucketSlot[];
|
|
1030
198
|
/** Repointed by `rebuildBindGroups` whenever any backing GrowBuffer reallocates. */
|
|
1031
199
|
bindGroup: GPUBindGroup;
|
|
1032
200
|
|
|
@@ -1082,30 +250,6 @@ interface Bucket {
|
|
|
1082
250
|
/** Local slots whose DrawHeader needs re-pack + writeBuffer next frame. */
|
|
1083
251
|
readonly dirty: Set<number>;
|
|
1084
252
|
|
|
1085
|
-
// ─── Megacall state ────────────────────────────────────────────────
|
|
1086
|
-
drawTableBuf?: GrowBuffer;
|
|
1087
|
-
drawTableShadow?: Uint32Array;
|
|
1088
|
-
drawTableDirtyMin: number;
|
|
1089
|
-
drawTableDirtyMax: number;
|
|
1090
|
-
/** Number of live records (= drawTable length). GPU owns firstEmit / total. */
|
|
1091
|
-
recordCount: number;
|
|
1092
|
-
/** localSlot → recordIdx (or -1). */
|
|
1093
|
-
slotToRecord: number[];
|
|
1094
|
-
/** recordIdx → localSlot. */
|
|
1095
|
-
recordToSlot: number[];
|
|
1096
|
-
/** Per-bucket buffers for the GPU prefix-sum pipeline. */
|
|
1097
|
-
blockSumsBuf?: GrowBuffer;
|
|
1098
|
-
blockOffsetsBuf?: GrowBuffer;
|
|
1099
|
-
firstDrawInTileBuf?: GrowBuffer;
|
|
1100
|
-
/** CPU sum of indexCounts across live records — drives firstDrawInTileBuf sizing only. */
|
|
1101
|
-
totalEmitEstimate: number;
|
|
1102
|
-
indirectBuf?: GPUBuffer;
|
|
1103
|
-
paramsBuf?: GPUBuffer;
|
|
1104
|
-
scanBindGroup?: GPUBindGroup;
|
|
1105
|
-
/** numRecords used to size the current render bindGroup; rebuild when it changes. */
|
|
1106
|
-
renderBoundRecordCount?: number;
|
|
1107
|
-
scanDirty: boolean;
|
|
1108
|
-
|
|
1109
253
|
// ─── Atlas-binding state (atlas-variant buckets only) ─────────────
|
|
1110
254
|
/**
|
|
1111
255
|
* True when this bucket holds at least one atlas-variant RO.
|
|
@@ -1428,6 +572,52 @@ export function buildHeapScene(
|
|
|
1428
572
|
const drawIdToLocalSlot: (number | undefined)[] = [];
|
|
1429
573
|
/** Per-draw index aval — for `indexPool.release` on removeDraw. */
|
|
1430
574
|
const drawIdToIndexAval: (aval<Uint32Array> | undefined)[] = [];
|
|
575
|
+
/**
|
|
576
|
+
* Original spec retained per drawId so we can re-add the RO into a
|
|
577
|
+
* new bucket when its PipelineState modeKey changes (reactive
|
|
578
|
+
* cullmode flip, etc.). Cheap to retain — specs are mostly aval
|
|
579
|
+
* references.
|
|
580
|
+
*/
|
|
581
|
+
const drawIdToSpec: (HeapDrawSpec | undefined)[] = [];
|
|
582
|
+
/** Per-draw ModeKeyTracker — recomputes the modeKey on demand. Does
|
|
583
|
+
* NOT install per-instance marking callbacks; subscriptions are
|
|
584
|
+
* scene-level (deduped by aval identity below). */
|
|
585
|
+
const drawIdToModeTracker: (ModeKeyTracker | undefined)[] = [];
|
|
586
|
+
/** drawIds whose PS marked since last update; drained by `update`. */
|
|
587
|
+
const dirtyModeKeyDrawIds = new Set<number>();
|
|
588
|
+
/**
|
|
589
|
+
* Scene-level mode-aval subscription dedupe. One IDisposable per
|
|
590
|
+
* unique leaf aval, plus a Set of dependent drawIds. When the aval
|
|
591
|
+
* marks, all drawIds in the set become dirty in O(1) per RO instead
|
|
592
|
+
* of installing N separate addMarkingCallbacks. The big win for the
|
|
593
|
+
* shared-cullCval case (20k ROs subscribing to one cval → 1 callback
|
|
594
|
+
* instead of 20k).
|
|
595
|
+
*/
|
|
596
|
+
const modeAvalToDrawIds = new Map<aval<unknown>, Set<number>>();
|
|
597
|
+
const modeAvalCallbacks = new Map<aval<unknown>, IDisposable>();
|
|
598
|
+
|
|
599
|
+
function subscribeModeLeaf(av: aval<unknown>, drawId: number): void {
|
|
600
|
+
let set = modeAvalToDrawIds.get(av);
|
|
601
|
+
if (set === undefined) {
|
|
602
|
+
set = new Set<number>();
|
|
603
|
+
modeAvalToDrawIds.set(av, set);
|
|
604
|
+
modeAvalCallbacks.set(av, addMarkingCallback(av, () => {
|
|
605
|
+
for (const id of set!) dirtyModeKeyDrawIds.add(id);
|
|
606
|
+
}));
|
|
607
|
+
}
|
|
608
|
+
set.add(drawId);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function unsubscribeModeLeaf(av: aval<unknown>, drawId: number): void {
|
|
612
|
+
const set = modeAvalToDrawIds.get(av);
|
|
613
|
+
if (set === undefined) return;
|
|
614
|
+
set.delete(drawId);
|
|
615
|
+
if (set.size === 0) {
|
|
616
|
+
modeAvalToDrawIds.delete(av);
|
|
617
|
+
const cb = modeAvalCallbacks.get(av);
|
|
618
|
+
if (cb !== undefined) { cb.dispose(); modeAvalCallbacks.delete(av); }
|
|
619
|
+
}
|
|
620
|
+
}
|
|
1431
621
|
let nextDrawId = 0;
|
|
1432
622
|
|
|
1433
623
|
/**
|
|
@@ -1853,12 +1043,12 @@ export function buildHeapScene(
|
|
|
1853
1043
|
label: `heapScene/${bucket.label}/scanBg`,
|
|
1854
1044
|
layout: scanBgl,
|
|
1855
1045
|
entries: [
|
|
1856
|
-
{ binding: 0, resource: { buffer: bucket.drawTableBuf!.buffer } },
|
|
1857
|
-
{ binding: 1, resource: { buffer: bucket.blockSumsBuf!.buffer } },
|
|
1858
|
-
{ binding: 2, resource: { buffer: bucket.blockOffsetsBuf!.buffer } },
|
|
1859
|
-
{ binding: 3, resource: { buffer: bucket.indirectBuf! } },
|
|
1860
|
-
{ binding: 4, resource: { buffer: bucket.paramsBuf! } },
|
|
1861
|
-
{ binding: 5, resource: { buffer: bucket.firstDrawInTileBuf!.buffer } },
|
|
1046
|
+
{ binding: 0, resource: { buffer: bucket.slots[0]!.drawTableBuf!.buffer } },
|
|
1047
|
+
{ binding: 1, resource: { buffer: bucket.slots[0]!.blockSumsBuf!.buffer } },
|
|
1048
|
+
{ binding: 2, resource: { buffer: bucket.slots[0]!.blockOffsetsBuf!.buffer } },
|
|
1049
|
+
{ binding: 3, resource: { buffer: bucket.slots[0]!.indirectBuf! } },
|
|
1050
|
+
{ binding: 4, resource: { buffer: bucket.slots[0]!.paramsBuf! } },
|
|
1051
|
+
{ binding: 5, resource: { buffer: bucket.slots[0]!.firstDrawInTileBuf!.buffer } },
|
|
1862
1052
|
],
|
|
1863
1053
|
});
|
|
1864
1054
|
}
|
|
@@ -2085,29 +1275,10 @@ export function buildHeapScene(
|
|
|
2085
1275
|
avalIds.set(av, vid);
|
|
2086
1276
|
return `c${vid}`;
|
|
2087
1277
|
};
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
if (cached !== undefined) return cached;
|
|
2093
|
-
const r = ps.rasterizer;
|
|
2094
|
-
const parts: string[] = [
|
|
2095
|
-
avalIdOf(r.topology),
|
|
2096
|
-
avalIdOf(r.cullMode),
|
|
2097
|
-
avalIdOf(r.frontFace),
|
|
2098
|
-
avalIdOf(r.depthBias),
|
|
2099
|
-
ps.depth !== undefined
|
|
2100
|
-
? `d:${avalIdOf(ps.depth.write)}:${avalIdOf(ps.depth.compare)}:${avalIdOf(ps.depth.clamp)}`
|
|
2101
|
-
: "d:_",
|
|
2102
|
-
ps.stencil !== undefined ? "s:1" : "s:_",
|
|
2103
|
-
avalIdOf(ps.blends),
|
|
2104
|
-
avalIdOf(ps.alphaToCoverage),
|
|
2105
|
-
avalIdOf(ps.blendConstant),
|
|
2106
|
-
];
|
|
2107
|
-
const key = `ps#${parts.join("|")}`;
|
|
2108
|
-
psContentIds.set(ps, key);
|
|
2109
|
-
return key;
|
|
2110
|
-
};
|
|
1278
|
+
// (psIdOf removed in Phase 5b — replaced by snapshotDescriptor +
|
|
1279
|
+
// encodeModeKey at the findOrCreateBucket call site, which keys on
|
|
1280
|
+
// the VALUE of the pipeline state rather than aval identity. The
|
|
1281
|
+
// 20k-cvals-with-same-value pathology now collapses correctly.)
|
|
2111
1282
|
|
|
2112
1283
|
/** Resolved (forced) snapshot of the user's PipelineState. */
|
|
2113
1284
|
interface ResolvedPipelineState {
|
|
@@ -2155,7 +1326,7 @@ export function buildHeapScene(
|
|
|
2155
1326
|
{ binding: 3, resource: { buffer: arena.attrs.buffer } }, // heapV4f
|
|
2156
1327
|
];
|
|
2157
1328
|
{
|
|
2158
|
-
if (bucket.drawTableBuf === undefined) {
|
|
1329
|
+
if (bucket.slots[0]!.drawTableBuf === undefined) {
|
|
2159
1330
|
throw new Error("heapScene: megacall bucket without drawTableBuf");
|
|
2160
1331
|
}
|
|
2161
1332
|
// Bind drawTable with size = recordCount * RECORD_BYTES so the VS
|
|
@@ -2163,13 +1334,13 @@ export function buildHeapScene(
|
|
|
2163
1334
|
// the live record count — keeps stale tail entries out of the
|
|
2164
1335
|
// binary search. Minimum one zero-record to satisfy WebGPU non-
|
|
2165
1336
|
// zero size constraint when the bucket is empty.
|
|
2166
|
-
const dtBytes = Math.max(RECORD_BYTES, bucket.recordCount * RECORD_BYTES);
|
|
1337
|
+
const dtBytes = Math.max(RECORD_BYTES, bucket.slots[0]!.recordCount * RECORD_BYTES);
|
|
2167
1338
|
entries.push(
|
|
2168
|
-
{ binding: 4, resource: { buffer: bucket.drawTableBuf.buffer, offset: 0, size: dtBytes } },
|
|
1339
|
+
{ binding: 4, resource: { buffer: bucket.slots[0]!.drawTableBuf.buffer, offset: 0, size: dtBytes } },
|
|
2169
1340
|
{ binding: 5, resource: { buffer: arena.indices.buffer } },
|
|
2170
|
-
{ binding: 6, resource: { buffer: bucket.firstDrawInTileBuf!.buffer } },
|
|
1341
|
+
{ binding: 6, resource: { buffer: bucket.slots[0]!.firstDrawInTileBuf!.buffer } },
|
|
2171
1342
|
);
|
|
2172
|
-
bucket.renderBoundRecordCount = bucket.recordCount;
|
|
1343
|
+
bucket.slots[0]!.renderBoundRecordCount = bucket.slots[0]!.recordCount;
|
|
2173
1344
|
}
|
|
2174
1345
|
// Schema-driven texture + sampler entries. v1 user surface still
|
|
2175
1346
|
// accepts a single (texture, sampler) pair via HeapTextureSet —
|
|
@@ -2307,8 +1478,17 @@ export function buildHeapScene(
|
|
|
2307
1478
|
throw new Error("heapScene: findOrCreateBucket called before family build");
|
|
2308
1479
|
}
|
|
2309
1480
|
const fam = familyFor(effect);
|
|
2310
|
-
|
|
2311
|
-
|
|
1481
|
+
// ─── PS keying — VALUE based, not identity based ───────────────
|
|
1482
|
+
// psIdOf used to hash by aval identity. That meant 20k ROs each
|
|
1483
|
+
// constructed with `cval("back")` for cullMode hashed to 20k
|
|
1484
|
+
// distinct keys → 20k buckets, all rendering with bitwise-
|
|
1485
|
+
// identical state. Now the key is the bitfield-encoded modeKey
|
|
1486
|
+
// (the actual value-set in the descriptor), so identical-value
|
|
1487
|
+
// cvals collapse to ONE bucket. See docs/derived-modes.md and
|
|
1488
|
+
// tests/heap-multi-pipeline-bucket.test.ts.
|
|
1489
|
+
const psDescriptor = snapshotDescriptor(pipelineState, sig);
|
|
1490
|
+
const psModeKey = encodeModeKey(psDescriptor);
|
|
1491
|
+
const bk = `family#${fam.schema.id}|mk#${psModeKey.toString(16)}`;
|
|
2312
1492
|
const existing = bucketByKey.get(bk);
|
|
2313
1493
|
if (existing !== undefined) return existing;
|
|
2314
1494
|
const ps = resolvePipelineState(pipelineState);
|
|
@@ -2343,13 +1523,21 @@ export function buildHeapScene(
|
|
|
2343
1523
|
);
|
|
2344
1524
|
const drawHeap = new DrawHeap(drawHeapBuf, layout.drawHeaderBytes);
|
|
2345
1525
|
|
|
1526
|
+
const slot0: BucketSlot = {
|
|
1527
|
+
pipeline,
|
|
1528
|
+
drawTableDirtyMin: Infinity, drawTableDirtyMax: 0,
|
|
1529
|
+
recordCount: 0, slotToRecord: [], recordToSlot: [],
|
|
1530
|
+
totalEmitEstimate: 0,
|
|
1531
|
+
scanDirty: false,
|
|
1532
|
+
};
|
|
2346
1533
|
const bucket: Bucket = {
|
|
2347
1534
|
// §6 family-merge: family buckets aren't keyed on a specific
|
|
2348
1535
|
// texture set — atlas placements are addressed per-RO via
|
|
2349
1536
|
// drawHeader fields (`pageRef` + `formatBits` + `origin` +
|
|
2350
1537
|
// `size`), and the bucket's atlas-binding ladder is driven by
|
|
2351
1538
|
// `atlasPool.pagesFor(format)`. Leave `textures` undefined.
|
|
2352
|
-
label: bk, textures: undefined, layout,
|
|
1539
|
+
label: bk, textures: undefined, layout,
|
|
1540
|
+
slots: [slot0],
|
|
2353
1541
|
bindGroup: null as unknown as GPUBindGroup,
|
|
2354
1542
|
drawHeap,
|
|
2355
1543
|
drawHeaderStaging: new Float32Array(drawHeapBuf.capacity / 4),
|
|
@@ -2358,10 +1546,6 @@ export function buildHeapScene(
|
|
|
2358
1546
|
localEntries: [], localToDrawId: [],
|
|
2359
1547
|
localPerDrawAvals: [], localPerDrawRefs: [], localLayoutIds: [],
|
|
2360
1548
|
drawSlots: new Set<number>(), dirty: new Set<number>(),
|
|
2361
|
-
drawTableDirtyMin: Infinity, drawTableDirtyMax: 0,
|
|
2362
|
-
recordCount: 0, slotToRecord: [], recordToSlot: [],
|
|
2363
|
-
totalEmitEstimate: 0,
|
|
2364
|
-
scanDirty: false,
|
|
2365
1549
|
isAtlasBucket,
|
|
2366
1550
|
localAtlasReleases: [],
|
|
2367
1551
|
localAtlasTextures: [],
|
|
@@ -2401,30 +1585,30 @@ export function buildHeapScene(
|
|
|
2401
1585
|
label: `heapScene/${bk}/params`, size: 16,
|
|
2402
1586
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
2403
1587
|
});
|
|
2404
|
-
bucket.drawTableBuf = dtBuf;
|
|
2405
|
-
bucket.drawTableShadow = new Uint32Array(dtBuf.capacity / 4);
|
|
2406
|
-
bucket.blockSumsBuf = blockSumsBuf;
|
|
2407
|
-
bucket.blockOffsetsBuf = blockOffsetsBuf;
|
|
2408
|
-
bucket.firstDrawInTileBuf = firstDrawInTileBuf;
|
|
2409
|
-
bucket.indirectBuf = indirectBuf;
|
|
2410
|
-
bucket.paramsBuf = paramsBuf;
|
|
1588
|
+
bucket.slots[0]!.drawTableBuf = dtBuf;
|
|
1589
|
+
bucket.slots[0]!.drawTableShadow = new Uint32Array(dtBuf.capacity / 4);
|
|
1590
|
+
bucket.slots[0]!.blockSumsBuf = blockSumsBuf;
|
|
1591
|
+
bucket.slots[0]!.blockOffsetsBuf = blockOffsetsBuf;
|
|
1592
|
+
bucket.slots[0]!.firstDrawInTileBuf = firstDrawInTileBuf;
|
|
1593
|
+
bucket.slots[0]!.indirectBuf = indirectBuf;
|
|
1594
|
+
bucket.slots[0]!.paramsBuf = paramsBuf;
|
|
2411
1595
|
const ensureScanBuffers = (): void => {
|
|
2412
|
-
const needBlocks = Math.max(1, Math.ceil(bucket.recordCount / SCAN_TILE_SIZE));
|
|
1596
|
+
const needBlocks = Math.max(1, Math.ceil(bucket.slots[0]!.recordCount / SCAN_TILE_SIZE));
|
|
2413
1597
|
blockSumsBuf.ensureCapacity(needBlocks * 4);
|
|
2414
1598
|
blockOffsetsBuf.ensureCapacity(needBlocks * 4);
|
|
2415
1599
|
};
|
|
2416
1600
|
const rebuildScanBg = (): void => {
|
|
2417
|
-
bucket.scanBindGroup = buildScanBindGroup(bucket);
|
|
1601
|
+
bucket.slots[0]!.scanBindGroup = buildScanBindGroup(bucket);
|
|
2418
1602
|
};
|
|
2419
1603
|
dtBuf.onResize(() => {
|
|
2420
1604
|
const grown = new Uint32Array(dtBuf.capacity / 4);
|
|
2421
|
-
grown.set(bucket.drawTableShadow!);
|
|
2422
|
-
bucket.drawTableShadow = grown;
|
|
1605
|
+
grown.set(bucket.slots[0]!.drawTableShadow!);
|
|
1606
|
+
bucket.slots[0]!.drawTableShadow = grown;
|
|
2423
1607
|
ensureScanBuffers();
|
|
2424
1608
|
bucket.bindGroup = buildBucketBindGroup(bucket);
|
|
2425
1609
|
rebuildScanBg();
|
|
2426
|
-
bucket.drawTableDirtyMin = 0;
|
|
2427
|
-
bucket.drawTableDirtyMax = bucket.recordCount * RECORD_BYTES;
|
|
1610
|
+
bucket.slots[0]!.drawTableDirtyMin = 0;
|
|
1611
|
+
bucket.slots[0]!.drawTableDirtyMax = bucket.slots[0]!.recordCount * RECORD_BYTES;
|
|
2428
1612
|
});
|
|
2429
1613
|
blockSumsBuf.onResize(rebuildScanBg);
|
|
2430
1614
|
blockOffsetsBuf.onResize(rebuildScanBg);
|
|
@@ -2432,7 +1616,7 @@ export function buildHeapScene(
|
|
|
2432
1616
|
rebuildScanBg();
|
|
2433
1617
|
bucket.bindGroup = buildBucketBindGroup(bucket);
|
|
2434
1618
|
});
|
|
2435
|
-
bucket.scanBindGroup = buildScanBindGroup(bucket);
|
|
1619
|
+
bucket.slots[0]!.scanBindGroup = buildScanBindGroup(bucket);
|
|
2436
1620
|
}
|
|
2437
1621
|
bucket.bindGroup = buildBucketBindGroup(bucket);
|
|
2438
1622
|
drawHeap.onResize(() => {
|
|
@@ -2754,8 +1938,8 @@ export function buildHeapScene(
|
|
|
2754
1938
|
if (end > bucket.headerDirtyMax) bucket.headerDirtyMax = end;
|
|
2755
1939
|
|
|
2756
1940
|
{
|
|
2757
|
-
const dtBuf = bucket.drawTableBuf!;
|
|
2758
|
-
const recIdx = bucket.recordCount;
|
|
1941
|
+
const dtBuf = bucket.slots[0]!.drawTableBuf!;
|
|
1942
|
+
const recIdx = bucket.slots[0]!.recordCount;
|
|
2759
1943
|
if (recIdx >= SCAN_MAX_RECORDS) {
|
|
2760
1944
|
throw new Error(
|
|
2761
1945
|
`heapScene: bucket exceeds SCAN_MAX_RECORDS (${SCAN_MAX_RECORDS}); ` +
|
|
@@ -2767,30 +1951,48 @@ export function buildHeapScene(
|
|
|
2767
1951
|
dtBuf.setUsed(Math.max(dtBuf.usedBytes, byteOff + RECORD_BYTES));
|
|
2768
1952
|
// Grow scan-side buffers if recordCount crosses a tile boundary.
|
|
2769
1953
|
const needBlocks = Math.max(1, Math.ceil((recIdx + 1) / SCAN_TILE_SIZE));
|
|
2770
|
-
bucket.blockSumsBuf!.ensureCapacity(needBlocks * 4);
|
|
2771
|
-
bucket.blockOffsetsBuf!.ensureCapacity(needBlocks * 4);
|
|
2772
|
-
const shadow = bucket.drawTableShadow!;
|
|
1954
|
+
bucket.slots[0]!.blockSumsBuf!.ensureCapacity(needBlocks * 4);
|
|
1955
|
+
bucket.slots[0]!.blockOffsetsBuf!.ensureCapacity(needBlocks * 4);
|
|
1956
|
+
const shadow = bucket.slots[0]!.drawTableShadow!;
|
|
2773
1957
|
// firstEmit is GPU-overwritten by the prefix-sum pass; 0 is fine.
|
|
2774
1958
|
shadow[recIdx * RECORD_U32 + 0] = 0;
|
|
2775
1959
|
shadow[recIdx * RECORD_U32 + 1] = localSlot;
|
|
2776
1960
|
shadow[recIdx * RECORD_U32 + 2] = idxAlloc.firstIndex;
|
|
2777
1961
|
shadow[recIdx * RECORD_U32 + 3] = idxAlloc.count;
|
|
2778
1962
|
shadow[recIdx * RECORD_U32 + 4] = instanceCount;
|
|
2779
|
-
bucket.recordCount = recIdx + 1;
|
|
2780
|
-
bucket.slotToRecord[localSlot] = recIdx;
|
|
2781
|
-
bucket.recordToSlot[recIdx] = localSlot;
|
|
2782
|
-
if (byteOff < bucket.drawTableDirtyMin) bucket.drawTableDirtyMin = byteOff;
|
|
2783
|
-
if (byteOff + RECORD_BYTES > bucket.drawTableDirtyMax) bucket.drawTableDirtyMax = byteOff + RECORD_BYTES;
|
|
2784
|
-
bucket.totalEmitEstimate += idxAlloc.count * instanceCount;
|
|
2785
|
-
const newNumTiles = Math.max(1, Math.ceil(bucket.totalEmitEstimate / TILE_K));
|
|
2786
|
-
bucket.firstDrawInTileBuf!.ensureCapacity((newNumTiles + 1) * 4);
|
|
2787
|
-
bucket.scanDirty = true;
|
|
1963
|
+
bucket.slots[0]!.recordCount = recIdx + 1;
|
|
1964
|
+
bucket.slots[0]!.slotToRecord[localSlot] = recIdx;
|
|
1965
|
+
bucket.slots[0]!.recordToSlot[recIdx] = localSlot;
|
|
1966
|
+
if (byteOff < bucket.slots[0]!.drawTableDirtyMin) bucket.slots[0]!.drawTableDirtyMin = byteOff;
|
|
1967
|
+
if (byteOff + RECORD_BYTES > bucket.slots[0]!.drawTableDirtyMax) bucket.slots[0]!.drawTableDirtyMax = byteOff + RECORD_BYTES;
|
|
1968
|
+
bucket.slots[0]!.totalEmitEstimate += idxAlloc.count * instanceCount;
|
|
1969
|
+
const newNumTiles = Math.max(1, Math.ceil(bucket.slots[0]!.totalEmitEstimate / TILE_K));
|
|
1970
|
+
bucket.slots[0]!.firstDrawInTileBuf!.ensureCapacity((newNumTiles + 1) * 4);
|
|
1971
|
+
bucket.slots[0]!.scanDirty = true;
|
|
2788
1972
|
}
|
|
2789
1973
|
|
|
2790
1974
|
drawIdToBucket[drawId] = bucket;
|
|
2791
1975
|
drawIdToLocalSlot[drawId] = localSlot;
|
|
2792
1976
|
drawIdToIndexAval[drawId] = indicesAval;
|
|
2793
1977
|
|
|
1978
|
+
// ─── Reactive rebucket: track PS-modeKey changes ────────────────
|
|
1979
|
+
// When any mode-axis aval marks (e.g. cullCval.value = 'front'),
|
|
1980
|
+
// schedule this RO for rebucket on the next update(). Today's
|
|
1981
|
+
// bucket key is the modeKey VALUE (Phase 5b), so changing the
|
|
1982
|
+
// value must move the RO to a different bucket. Without this
|
|
1983
|
+
// tracker the cval flip would be silently ignored.
|
|
1984
|
+
drawIdToSpec[drawId] = spec;
|
|
1985
|
+
// skipSubscribe: this tracker doesn't install its own callbacks.
|
|
1986
|
+
// The scene-level `modeAvalToDrawIds` does it once per unique aval
|
|
1987
|
+
// (the 20k-ROs-share-one-cullCval optimization).
|
|
1988
|
+
const tracker = new ModeKeyTracker(
|
|
1989
|
+
spec.pipelineState, sig,
|
|
1990
|
+
() => { dirtyModeKeyDrawIds.add(drawId); },
|
|
1991
|
+
{ skipSubscribe: true },
|
|
1992
|
+
);
|
|
1993
|
+
drawIdToModeTracker[drawId] = tracker;
|
|
1994
|
+
tracker.forEachLeaf((av) => subscribeModeLeaf(av, drawId));
|
|
1995
|
+
|
|
2794
1996
|
// ─── §7 derived-uniforms registration ────────────────────────────
|
|
2795
1997
|
// A uniform binding on this RO is either a value (aval/constant) or a rule —
|
|
2796
1998
|
// collect the rules (explicit `derivedUniform(...)` values + standard trafo
|
|
@@ -2865,13 +2067,13 @@ export function buildHeapScene(
|
|
|
2865
2067
|
const removedCount = removedEntry !== undefined
|
|
2866
2068
|
? removedEntry.indexCount * removedEntry.instanceCount
|
|
2867
2069
|
: 0;
|
|
2868
|
-
bucket.totalEmitEstimate = Math.max(0, bucket.totalEmitEstimate - removedCount);
|
|
2070
|
+
bucket.slots[0]!.totalEmitEstimate = Math.max(0, bucket.slots[0]!.totalEmitEstimate - removedCount);
|
|
2869
2071
|
// Swap-pop: move the last record into the freed slot, decrement
|
|
2870
2072
|
// recordCount. firstEmit is GPU-rewritten by the next scan, so
|
|
2871
2073
|
// we only fix (drawIdx, indexStart, indexCount, instanceCount).
|
|
2872
|
-
const recIdx = bucket.slotToRecord[localSlot]!;
|
|
2873
|
-
const lastRecIdx = bucket.recordCount - 1;
|
|
2874
|
-
const shadow = bucket.drawTableShadow!;
|
|
2074
|
+
const recIdx = bucket.slots[0]!.slotToRecord[localSlot]!;
|
|
2075
|
+
const lastRecIdx = bucket.slots[0]!.recordCount - 1;
|
|
2076
|
+
const shadow = bucket.slots[0]!.drawTableShadow!;
|
|
2875
2077
|
if (recIdx !== lastRecIdx) {
|
|
2876
2078
|
const dst = recIdx * RECORD_U32;
|
|
2877
2079
|
const src = lastRecIdx * RECORD_U32;
|
|
@@ -2880,17 +2082,17 @@ export function buildHeapScene(
|
|
|
2880
2082
|
shadow[dst + 2] = shadow[src + 2]!;
|
|
2881
2083
|
shadow[dst + 3] = shadow[src + 3]!;
|
|
2882
2084
|
shadow[dst + 4] = shadow[src + 4]!;
|
|
2883
|
-
const movedSlot = bucket.recordToSlot[lastRecIdx]!;
|
|
2884
|
-
bucket.slotToRecord[movedSlot] = recIdx;
|
|
2885
|
-
bucket.recordToSlot[recIdx] = movedSlot;
|
|
2085
|
+
const movedSlot = bucket.slots[0]!.recordToSlot[lastRecIdx]!;
|
|
2086
|
+
bucket.slots[0]!.slotToRecord[movedSlot] = recIdx;
|
|
2087
|
+
bucket.slots[0]!.recordToSlot[recIdx] = movedSlot;
|
|
2886
2088
|
const byteOff = recIdx * RECORD_BYTES;
|
|
2887
|
-
if (byteOff < bucket.drawTableDirtyMin) bucket.drawTableDirtyMin = byteOff;
|
|
2888
|
-
if (byteOff + RECORD_BYTES > bucket.drawTableDirtyMax) bucket.drawTableDirtyMax = byteOff + RECORD_BYTES;
|
|
2089
|
+
if (byteOff < bucket.slots[0]!.drawTableDirtyMin) bucket.slots[0]!.drawTableDirtyMin = byteOff;
|
|
2090
|
+
if (byteOff + RECORD_BYTES > bucket.slots[0]!.drawTableDirtyMax) bucket.slots[0]!.drawTableDirtyMax = byteOff + RECORD_BYTES;
|
|
2889
2091
|
}
|
|
2890
|
-
bucket.slotToRecord[localSlot] = -1;
|
|
2891
|
-
bucket.recordToSlot[lastRecIdx] = -1;
|
|
2892
|
-
bucket.recordCount = lastRecIdx;
|
|
2893
|
-
bucket.scanDirty = true;
|
|
2092
|
+
bucket.slots[0]!.slotToRecord[localSlot] = -1;
|
|
2093
|
+
bucket.slots[0]!.recordToSlot[lastRecIdx] = -1;
|
|
2094
|
+
bucket.slots[0]!.recordCount = lastRecIdx;
|
|
2095
|
+
bucket.slots[0]!.scanDirty = true;
|
|
2894
2096
|
}
|
|
2895
2097
|
|
|
2896
2098
|
// Release pool entries — refcount drops; if zero, allocation freed.
|
|
@@ -2943,9 +2145,21 @@ export function buildHeapScene(
|
|
|
2943
2145
|
drawIdToBucket[drawId] = undefined;
|
|
2944
2146
|
drawIdToLocalSlot[drawId] = undefined;
|
|
2945
2147
|
drawIdToIndexAval[drawId] = undefined;
|
|
2148
|
+
drawIdToSpec[drawId] = undefined;
|
|
2149
|
+
const oldTracker = drawIdToModeTracker[drawId];
|
|
2150
|
+
if (oldTracker !== undefined) {
|
|
2151
|
+
oldTracker.forEachLeaf((av) => unsubscribeModeLeaf(av, drawId));
|
|
2152
|
+
oldTracker.dispose();
|
|
2153
|
+
}
|
|
2154
|
+
drawIdToModeTracker[drawId] = undefined;
|
|
2155
|
+
dirtyModeKeyDrawIds.delete(drawId);
|
|
2946
2156
|
|
|
2947
2157
|
stats.totalDraws--;
|
|
2948
2158
|
stats.geometryBytes = arenaBytes(arena);
|
|
2159
|
+
|
|
2160
|
+
// GC the bucket if it just emptied (common after reactive
|
|
2161
|
+
// PS rebucket leaves the source bucket with zero live ROs).
|
|
2162
|
+
gcBucket(bucket);
|
|
2949
2163
|
}
|
|
2950
2164
|
|
|
2951
2165
|
// ─── Aset reader (pull-driven on each frame) ──────────────────────
|
|
@@ -3030,6 +2244,105 @@ export function buildHeapScene(
|
|
|
3030
2244
|
sceneObj.evaluateAlways(token, (tok) => {
|
|
3031
2245
|
drainAsetWith(tok);
|
|
3032
2246
|
|
|
2247
|
+
// 0. Reactive rebucket: any RO whose PipelineState modeKey
|
|
2248
|
+
// changed since last frame moves to its new bucket.
|
|
2249
|
+
// `dirtyModeKeyDrawIds` was populated by ModeKeyTracker's
|
|
2250
|
+
// onDirty callbacks (one per leaf aval per RO). Calling
|
|
2251
|
+
// `recompute()` here re-snapshots the descriptor and tells
|
|
2252
|
+
// us whether the modeKey actually changed (vs the aval just
|
|
2253
|
+
// being marked-but-equal). If the key changed, we remove +
|
|
2254
|
+
// re-add the RO via the retained spec; addDraw routes it to
|
|
2255
|
+
// the correct (new) bucket via the value-keyed lookup.
|
|
2256
|
+
if (dirtyModeKeyDrawIds.size > 0) {
|
|
2257
|
+
const dirty = [...dirtyModeKeyDrawIds];
|
|
2258
|
+
dirtyModeKeyDrawIds.clear();
|
|
2259
|
+
|
|
2260
|
+
// Bucket-level fast path: group dirty drawIds by bucket. If
|
|
2261
|
+
// ALL of a bucket's ROs are dirty (typical SG case where one
|
|
2262
|
+
// shared aval drives every leaf — e.g. <Sg CullMode={cullC}>
|
|
2263
|
+
// wrapping 20k leaves), the whole bucket transitions together.
|
|
2264
|
+
// Rebuild the bucket's pipeline + rename in bucketByKey,
|
|
2265
|
+
// O(1) work instead of N remove+add cycles.
|
|
2266
|
+
const byBucket = new Map<Bucket, number[]>();
|
|
2267
|
+
for (const drawId of dirty) {
|
|
2268
|
+
const b = drawIdToBucket[drawId];
|
|
2269
|
+
if (b === undefined) continue; // RO removed since mark
|
|
2270
|
+
let arr = byBucket.get(b);
|
|
2271
|
+
if (arr === undefined) { arr = []; byBucket.set(b, arr); }
|
|
2272
|
+
arr.push(drawId);
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
for (const [bucket, ids] of byBucket) {
|
|
2276
|
+
// First recompute every dirty tracker so .modeKey is fresh.
|
|
2277
|
+
// Filter out ones whose value didn't actually change.
|
|
2278
|
+
const changed: number[] = [];
|
|
2279
|
+
for (const drawId of ids) {
|
|
2280
|
+
const tracker = drawIdToModeTracker[drawId];
|
|
2281
|
+
if (tracker === undefined) continue;
|
|
2282
|
+
const oldKey = tracker.modeKey;
|
|
2283
|
+
if (!tracker.recompute()) continue;
|
|
2284
|
+
if (tracker.modeKey === oldKey) continue;
|
|
2285
|
+
changed.push(drawId);
|
|
2286
|
+
}
|
|
2287
|
+
if (changed.length === 0) continue;
|
|
2288
|
+
|
|
2289
|
+
// Try the all-together fast path.
|
|
2290
|
+
const wholeBucket = changed.length === bucket.drawSlots.size;
|
|
2291
|
+
if (wholeBucket) {
|
|
2292
|
+
const repId = changed[0]!;
|
|
2293
|
+
const repTracker = drawIdToModeTracker[repId]!;
|
|
2294
|
+
const newKey = repTracker.modeKey;
|
|
2295
|
+
// Compute the new bucketByKey string; we need the family.
|
|
2296
|
+
const sample = drawIdToSpec[repId]!;
|
|
2297
|
+
const fam = familyFor(sample.effect);
|
|
2298
|
+
const newBk = `family#${fam.schema.id}|mk#${newKey.toString(16)}`;
|
|
2299
|
+
const existing = bucketByKey.get(newBk);
|
|
2300
|
+
if (existing === undefined) {
|
|
2301
|
+
// No conflict — rename + rebuild pipeline in place.
|
|
2302
|
+
// Find this bucket's current key by linear scan
|
|
2303
|
+
// (buckets ≤ ~100 typically, so this is cheap).
|
|
2304
|
+
let oldBk: string | undefined;
|
|
2305
|
+
for (const [k, v] of bucketByKey) {
|
|
2306
|
+
if (v === bucket) { oldBk = k; break; }
|
|
2307
|
+
}
|
|
2308
|
+
if (oldBk !== undefined) bucketByKey.delete(oldBk);
|
|
2309
|
+
bucketByKey.set(newBk, bucket);
|
|
2310
|
+
// Build the new pipeline with the snapshotted PS.
|
|
2311
|
+
const desc = repTracker.descriptor;
|
|
2312
|
+
const { pipelineLayout } = getBgl(bucket.layout, bucket.isAtlasBucket);
|
|
2313
|
+
const newPipeline = device.createRenderPipeline({
|
|
2314
|
+
label: `heapScene/${newBk}/pipeline`,
|
|
2315
|
+
layout: pipelineLayout,
|
|
2316
|
+
vertex: { module: familyFor(sample.effect).vsModule, entryPoint: familyFor(sample.effect).vsEntryName, buffers: [] },
|
|
2317
|
+
fragment: { module: familyFor(sample.effect).fsModule, entryPoint: familyFor(sample.effect).fsEntryName, targets: colorTargets },
|
|
2318
|
+
primitive: { topology: desc.topology, cullMode: desc.cullMode, frontFace: desc.frontFace },
|
|
2319
|
+
...(depthFormat !== undefined && desc.depth !== undefined
|
|
2320
|
+
? { depthStencil: { format: depthFormat, depthWriteEnabled: desc.depth.write, depthCompare: desc.depth.compare } }
|
|
2321
|
+
: depthFormat !== undefined
|
|
2322
|
+
? { depthStencil: { format: depthFormat, depthWriteEnabled: false, depthCompare: "always" as GPUCompareFunction } }
|
|
2323
|
+
: {}),
|
|
2324
|
+
});
|
|
2325
|
+
// Swap pipeline reference on the bucket's single slot.
|
|
2326
|
+
// (Multi-slot Phase 5c would update one slot at a time.)
|
|
2327
|
+
(bucket.slots[0] as { pipeline: GPURenderPipeline }).pipeline = newPipeline;
|
|
2328
|
+
continue; // done with this bucket
|
|
2329
|
+
}
|
|
2330
|
+
// Conflict: another bucket already owns newBk. Fall through
|
|
2331
|
+
// to per-RO rebucket below, which will merge into `existing`.
|
|
2332
|
+
}
|
|
2333
|
+
// Slow path — per-RO rebucket (used when partial transition
|
|
2334
|
+
// OR when a merge with an existing destination bucket is
|
|
2335
|
+
// required).
|
|
2336
|
+
for (const drawId of changed) {
|
|
2337
|
+
const spec = drawIdToSpec[drawId];
|
|
2338
|
+
if (spec === undefined) continue;
|
|
2339
|
+
removeDraw(drawId);
|
|
2340
|
+
const newId = addDrawImpl(spec, tok);
|
|
2341
|
+
void newId;
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
|
|
3033
2346
|
// 1. Pool: re-pack any aval whose value changed since last frame.
|
|
3034
2347
|
// One writeBuffer per dirty aval, regardless of how many draws
|
|
3035
2348
|
// reference it — sharing pays off here.
|
|
@@ -3125,16 +2438,16 @@ export function buildHeapScene(
|
|
|
3125
2438
|
bucket.headerDirtyMin = Infinity;
|
|
3126
2439
|
bucket.headerDirtyMax = 0;
|
|
3127
2440
|
}
|
|
3128
|
-
if (bucket.drawTableDirtyMax > bucket.drawTableDirtyMin) {
|
|
3129
|
-
const shadow = bucket.drawTableShadow!;
|
|
2441
|
+
if (bucket.slots[0]!.drawTableDirtyMax > bucket.slots[0]!.drawTableDirtyMin) {
|
|
2442
|
+
const shadow = bucket.slots[0]!.drawTableShadow!;
|
|
3130
2443
|
device.queue.writeBuffer(
|
|
3131
|
-
bucket.drawTableBuf!.buffer, bucket.drawTableDirtyMin,
|
|
2444
|
+
bucket.slots[0]!.drawTableBuf!.buffer, bucket.slots[0]!.drawTableDirtyMin,
|
|
3132
2445
|
shadow.buffer,
|
|
3133
|
-
shadow.byteOffset + bucket.drawTableDirtyMin,
|
|
3134
|
-
bucket.drawTableDirtyMax - bucket.drawTableDirtyMin,
|
|
2446
|
+
shadow.byteOffset + bucket.slots[0]!.drawTableDirtyMin,
|
|
2447
|
+
bucket.slots[0]!.drawTableDirtyMax - bucket.slots[0]!.drawTableDirtyMin,
|
|
3135
2448
|
);
|
|
3136
|
-
bucket.drawTableDirtyMin = Infinity;
|
|
3137
|
-
bucket.drawTableDirtyMax = 0;
|
|
2449
|
+
bucket.slots[0]!.drawTableDirtyMin = Infinity;
|
|
2450
|
+
bucket.slots[0]!.drawTableDirtyMax = 0;
|
|
3138
2451
|
}
|
|
3139
2452
|
}
|
|
3140
2453
|
}
|
|
@@ -3148,8 +2461,8 @@ export function buildHeapScene(
|
|
|
3148
2461
|
let curBg: GPUBindGroup | null = null;
|
|
3149
2462
|
for (const b of buckets) {
|
|
3150
2463
|
if (b.bindGroup !== curBg) { pass.setBindGroup(0, b.bindGroup); curBg = b.bindGroup; }
|
|
3151
|
-
pass.setPipeline(b.pipeline);
|
|
3152
|
-
if (b.recordCount > 0) pass.drawIndirect(b.indirectBuf!, 0);
|
|
2464
|
+
pass.setPipeline(b.slots[0]!.pipeline);
|
|
2465
|
+
if (b.slots[0]!.recordCount > 0) pass.drawIndirect(b.slots[0]!.indirectBuf!, 0);
|
|
3153
2466
|
}
|
|
3154
2467
|
}
|
|
3155
2468
|
|
|
@@ -3190,23 +2503,23 @@ export function buildHeapScene(
|
|
|
3190
2503
|
stats.derivedRecords = derivedScene.records.recordCount;
|
|
3191
2504
|
}
|
|
3192
2505
|
let anyDirty = false;
|
|
3193
|
-
for (const b of buckets) { if (b.scanDirty) { anyDirty = true; break; } }
|
|
2506
|
+
for (const b of buckets) { if (b.slots[0]!.scanDirty) { anyDirty = true; break; } }
|
|
3194
2507
|
if (!anyDirty) return;
|
|
3195
2508
|
const pass = enc.beginComputePass({ label: "heapScene/scan" });
|
|
3196
2509
|
for (const b of buckets) {
|
|
3197
|
-
if (!b.scanDirty) continue;
|
|
2510
|
+
if (!b.slots[0]!.scanDirty) continue;
|
|
3198
2511
|
// Rebuild render bind group if recordCount changed — its
|
|
3199
2512
|
// drawTable binding is sized to recordCount * 16.
|
|
3200
|
-
if (b.renderBoundRecordCount !== b.recordCount) {
|
|
2513
|
+
if (b.slots[0]!.renderBoundRecordCount !== b.slots[0]!.recordCount) {
|
|
3201
2514
|
b.bindGroup = buildBucketBindGroup(b);
|
|
3202
2515
|
}
|
|
3203
|
-
const numRecords = b.recordCount;
|
|
2516
|
+
const numRecords = b.slots[0]!.recordCount;
|
|
3204
2517
|
const numBlocks = Math.max(1, Math.ceil(numRecords / SCAN_TILE_SIZE));
|
|
3205
2518
|
device.queue.writeBuffer(
|
|
3206
|
-
b.paramsBuf!, 0,
|
|
2519
|
+
b.slots[0]!.paramsBuf!, 0,
|
|
3207
2520
|
new Uint32Array([numRecords, numBlocks, 0, 0]),
|
|
3208
2521
|
);
|
|
3209
|
-
pass.setBindGroup(0, b.scanBindGroup!);
|
|
2522
|
+
pass.setBindGroup(0, b.slots[0]!.scanBindGroup!);
|
|
3210
2523
|
pass.setPipeline(scanPipeTile!);
|
|
3211
2524
|
pass.dispatchWorkgroups(numBlocks, 1, 1);
|
|
3212
2525
|
pass.setPipeline(scanPipeBlocks!);
|
|
@@ -3217,11 +2530,11 @@ export function buildHeapScene(
|
|
|
3217
2530
|
// must cover the worst-case totalEmit. Each WG handles WG_SIZE
|
|
3218
2531
|
// tiles; +1 for the sentinel slot.
|
|
3219
2532
|
const SCAN_WG_SIZE = 256;
|
|
3220
|
-
const numTilesCap = Math.max(1, Math.ceil(b.totalEmitEstimate / TILE_K));
|
|
2533
|
+
const numTilesCap = Math.max(1, Math.ceil(b.slots[0]!.totalEmitEstimate / TILE_K));
|
|
3221
2534
|
const tileWgs = Math.max(1, Math.ceil((numTilesCap + 1) / SCAN_WG_SIZE));
|
|
3222
2535
|
pass.setPipeline(scanPipeBuildTileIndex!);
|
|
3223
2536
|
pass.dispatchWorkgroups(tileWgs, 1, 1);
|
|
3224
|
-
b.scanDirty = false;
|
|
2537
|
+
b.slots[0]!.scanDirty = false;
|
|
3225
2538
|
}
|
|
3226
2539
|
pass.end();
|
|
3227
2540
|
}
|
|
@@ -3259,18 +2572,38 @@ export function buildHeapScene(
|
|
|
3259
2572
|
device.queue.submit([enc.finish()]);
|
|
3260
2573
|
}
|
|
3261
2574
|
|
|
2575
|
+
function destroyBucketResources(b: Bucket): void {
|
|
2576
|
+
b.drawHeap.destroy();
|
|
2577
|
+
for (const slot of b.slots) {
|
|
2578
|
+
slot.drawTableBuf?.destroy();
|
|
2579
|
+
slot.blockSumsBuf?.destroy();
|
|
2580
|
+
slot.blockOffsetsBuf?.destroy();
|
|
2581
|
+
slot.firstDrawInTileBuf?.destroy();
|
|
2582
|
+
slot.indirectBuf?.destroy();
|
|
2583
|
+
slot.paramsBuf?.destroy();
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
/**
|
|
2588
|
+
* Remove an empty bucket from the scene's bucket list + key index +
|
|
2589
|
+
* destroy its GPU resources. Called from removeDraw when an RO's
|
|
2590
|
+
* removal leaves the bucket with zero live slots — common after
|
|
2591
|
+
* reactive PS rebucket leaves the source bucket empty.
|
|
2592
|
+
*/
|
|
2593
|
+
function gcBucket(b: Bucket): void {
|
|
2594
|
+
if (b.drawSlots.size !== 0) return;
|
|
2595
|
+
const idx = buckets.indexOf(b);
|
|
2596
|
+
if (idx >= 0) buckets.splice(idx, 1);
|
|
2597
|
+
for (const [k, v] of bucketByKey) {
|
|
2598
|
+
if (v === b) { bucketByKey.delete(k); break; }
|
|
2599
|
+
}
|
|
2600
|
+
destroyBucketResources(b);
|
|
2601
|
+
}
|
|
2602
|
+
|
|
3262
2603
|
function dispose(): void {
|
|
3263
2604
|
arena.attrs.destroy();
|
|
3264
2605
|
arena.indices.destroy();
|
|
3265
|
-
for (const b of buckets)
|
|
3266
|
-
b.drawHeap.destroy();
|
|
3267
|
-
b.drawTableBuf?.destroy();
|
|
3268
|
-
b.blockSumsBuf?.destroy();
|
|
3269
|
-
b.blockOffsetsBuf?.destroy();
|
|
3270
|
-
b.firstDrawInTileBuf?.destroy();
|
|
3271
|
-
b.indirectBuf?.destroy();
|
|
3272
|
-
b.paramsBuf?.destroy();
|
|
3273
|
-
}
|
|
2606
|
+
for (const b of buckets) destroyBucketResources(b);
|
|
3274
2607
|
}
|
|
3275
2608
|
|
|
3276
2609
|
// Test-only escape hatch for inspecting megacall bucket state. Not
|
|
@@ -3285,11 +2618,11 @@ export function buildHeapScene(
|
|
|
3285
2618
|
layout: BucketLayout;
|
|
3286
2619
|
}[] {
|
|
3287
2620
|
return buckets.map(b => ({
|
|
3288
|
-
indirectBuf: b.indirectBuf,
|
|
3289
|
-
drawTableBuf: b.drawTableBuf?.buffer,
|
|
3290
|
-
firstDrawInTileBuf: b.firstDrawInTileBuf?.buffer,
|
|
3291
|
-
totalEmitEstimate: b.totalEmitEstimate,
|
|
3292
|
-
recordCount: b.recordCount,
|
|
2621
|
+
indirectBuf: b.slots[0]!.indirectBuf,
|
|
2622
|
+
drawTableBuf: b.slots[0]!.drawTableBuf?.buffer,
|
|
2623
|
+
firstDrawInTileBuf: b.slots[0]!.firstDrawInTileBuf?.buffer,
|
|
2624
|
+
totalEmitEstimate: b.slots[0]!.totalEmitEstimate,
|
|
2625
|
+
recordCount: b.slots[0]!.recordCount,
|
|
3293
2626
|
layout: b.layout,
|
|
3294
2627
|
}));
|
|
3295
2628
|
},
|
|
@@ -3358,18 +2691,18 @@ export function buildHeapScene(
|
|
|
3358
2691
|
const TILE_K_LOCAL = 64;
|
|
3359
2692
|
const dcs: DC[] = [];
|
|
3360
2693
|
for (const b of buckets) {
|
|
3361
|
-
const dhSize = Math.min(b.drawHeap.buffer.size, b.recordCount * b.layout.drawHeaderBytes);
|
|
2694
|
+
const dhSize = Math.min(b.drawHeap.buffer.size, b.slots[0]!.recordCount * b.layout.drawHeaderBytes);
|
|
3362
2695
|
if (dhSize === 0) continue;
|
|
3363
2696
|
const dh = stage(b.drawHeap.buffer, dhSize);
|
|
3364
|
-
const dt = b.drawTableBuf !== undefined && b.recordCount > 0
|
|
3365
|
-
? stage(b.drawTableBuf.buffer, b.recordCount * RECORD_BYTES)
|
|
2697
|
+
const dt = b.slots[0]!.drawTableBuf !== undefined && b.slots[0]!.recordCount > 0
|
|
2698
|
+
? stage(b.slots[0]!.drawTableBuf.buffer, b.slots[0]!.recordCount * RECORD_BYTES)
|
|
3366
2699
|
: undefined;
|
|
3367
|
-
const totalEmit = b.totalEmitEstimate;
|
|
2700
|
+
const totalEmit = b.slots[0]!.totalEmitEstimate;
|
|
3368
2701
|
const numTiles = totalEmit > 0 ? Math.ceil(totalEmit / TILE_K_LOCAL) : 0;
|
|
3369
|
-
const fdt = b.firstDrawInTileBuf !== undefined && numTiles > 0
|
|
3370
|
-
? stage(b.firstDrawInTileBuf.buffer, Math.min(b.firstDrawInTileBuf.buffer.size, (numTiles + 1) * 4))
|
|
2702
|
+
const fdt = b.slots[0]!.firstDrawInTileBuf !== undefined && numTiles > 0
|
|
2703
|
+
? stage(b.slots[0]!.firstDrawInTileBuf.buffer, Math.min(b.slots[0]!.firstDrawInTileBuf.buffer.size, (numTiles + 1) * 4))
|
|
3371
2704
|
: undefined;
|
|
3372
|
-
const ind = b.indirectBuf !== undefined ? stage(b.indirectBuf, 16) : undefined;
|
|
2705
|
+
const ind = b.slots[0]!.indirectBuf !== undefined ? stage(b.slots[0]!.indirectBuf, 16) : undefined;
|
|
3373
2706
|
dcs.push({
|
|
3374
2707
|
bucket: b, drawHeap: dh, numTiles,
|
|
3375
2708
|
...(dt !== undefined ? { drawTable: dt } : {}),
|
|
@@ -3401,7 +2734,7 @@ export function buildHeapScene(
|
|
|
3401
2734
|
for (const dc of dcs) {
|
|
3402
2735
|
const u32 = new Uint32Array(dc.drawHeap.getMappedRange());
|
|
3403
2736
|
const stride = dc.bucket.layout.drawHeaderBytes;
|
|
3404
|
-
const recordCount = dc.bucket.recordCount;
|
|
2737
|
+
const recordCount = dc.bucket.slots[0]!.recordCount;
|
|
3405
2738
|
|
|
3406
2739
|
for (let slot = 0; slot < recordCount; slot++) {
|
|
3407
2740
|
const slotOff = slot * stride;
|
|
@@ -3557,7 +2890,7 @@ export function buildHeapScene(
|
|
|
3557
2890
|
// ── 6. indirect[0] (totalEmit) must match CPU prefix sum.
|
|
3558
2891
|
if (dc.indirect !== undefined) {
|
|
3559
2892
|
const ind = new Uint32Array(dc.indirect.getMappedRange());
|
|
3560
|
-
const expectedTotal = dc.bucket.totalEmitEstimate;
|
|
2893
|
+
const expectedTotal = dc.bucket.slots[0]!.totalEmitEstimate;
|
|
3561
2894
|
const got = ind[0]!;
|
|
3562
2895
|
if (got !== expectedTotal) {
|
|
3563
2896
|
push(`bucket#${bucketIdx} indirect[0]=${got} ≠ expected totalEmit=${expectedTotal}`);
|
|
@@ -3658,10 +2991,10 @@ export function buildHeapScene(
|
|
|
3658
2991
|
};
|
|
3659
2992
|
const dcs: DC[] = [];
|
|
3660
2993
|
for (const b of buckets) {
|
|
3661
|
-
if (b.recordCount === 0 || b.totalEmitEstimate === 0) continue;
|
|
3662
|
-
const dh = stage(b.drawHeap.buffer, Math.min(b.drawHeap.buffer.size, b.recordCount * b.layout.drawHeaderBytes));
|
|
3663
|
-
const dt = stage(b.drawTableBuf!.buffer, b.recordCount * RECORD_BYTES);
|
|
3664
|
-
dcs.push({ bucket: b, drawHeap: dh, drawTable: dt, firstEmit: [], totalEmit: b.totalEmitEstimate });
|
|
2994
|
+
if (b.slots[0]!.recordCount === 0 || b.slots[0]!.totalEmitEstimate === 0) continue;
|
|
2995
|
+
const dh = stage(b.drawHeap.buffer, Math.min(b.drawHeap.buffer.size, b.slots[0]!.recordCount * b.layout.drawHeaderBytes));
|
|
2996
|
+
const dt = stage(b.slots[0]!.drawTableBuf!.buffer, b.slots[0]!.recordCount * RECORD_BYTES);
|
|
2997
|
+
dcs.push({ bucket: b, drawHeap: dh, drawTable: dt, firstEmit: [], totalEmit: b.slots[0]!.totalEmitEstimate });
|
|
3665
2998
|
}
|
|
3666
2999
|
device.queue.submit([enc.finish()]);
|
|
3667
3000
|
|
|
@@ -3687,7 +3020,7 @@ export function buildHeapScene(
|
|
|
3687
3020
|
const dt = new Uint32Array(dc.drawTable.getMappedRange());
|
|
3688
3021
|
bucketDt.push(dt);
|
|
3689
3022
|
bucketHdr.push(new Uint32Array(dc.drawHeap.getMappedRange()));
|
|
3690
|
-
for (let r = 0; r < dc.bucket.recordCount; r++) {
|
|
3023
|
+
for (let r = 0; r < dc.bucket.slots[0]!.recordCount; r++) {
|
|
3691
3024
|
dc.firstEmit.push(dt[r * RECORD_U32 + 0]!);
|
|
3692
3025
|
}
|
|
3693
3026
|
bucketCumEmit.push(totalEmitGlobal);
|
|
@@ -3745,7 +3078,7 @@ export function buildHeapScene(
|
|
|
3745
3078
|
emitsChecked++;
|
|
3746
3079
|
|
|
3747
3080
|
// Binary search for slot.
|
|
3748
|
-
const recCount = dc.bucket.recordCount;
|
|
3081
|
+
const recCount = dc.bucket.slots[0]!.recordCount;
|
|
3749
3082
|
let lo = 0, hi = recCount - 1;
|
|
3750
3083
|
while (lo < hi) {
|
|
3751
3084
|
const mid = (lo + hi + 1) >>> 1;
|
|
@@ -3942,9 +3275,9 @@ export function buildHeapScene(
|
|
|
3942
3275
|
: new Uint32Array(0);
|
|
3943
3276
|
|
|
3944
3277
|
for (const target of buckets) {
|
|
3945
|
-
if (target.recordCount === 0 || target.totalEmitEstimate === 0) { bucketIdx++; continue; }
|
|
3946
|
-
const recordCount = target.recordCount;
|
|
3947
|
-
const totalEmit = target.totalEmitEstimate;
|
|
3278
|
+
if (target.slots[0]!.recordCount === 0 || target.slots[0]!.totalEmitEstimate === 0) { bucketIdx++; continue; }
|
|
3279
|
+
const recordCount = target.slots[0]!.recordCount;
|
|
3280
|
+
const totalEmit = target.slots[0]!.totalEmitEstimate;
|
|
3948
3281
|
const totalTris = Math.floor(totalEmit / 3);
|
|
3949
3282
|
const remainder = totalEmit % 3;
|
|
3950
3283
|
if (remainder !== 0) {
|
|
@@ -3962,7 +3295,7 @@ export function buildHeapScene(
|
|
|
3962
3295
|
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
|
|
3963
3296
|
});
|
|
3964
3297
|
const enc0 = device.createCommandEncoder({ label: "checkTriangleCoherence.dt" });
|
|
3965
|
-
enc0.copyBufferToBuffer(target.drawTableBuf!.buffer, 0, dtCopy, 0, recordCount * RECORD_BYTES);
|
|
3298
|
+
enc0.copyBufferToBuffer(target.slots[0]!.drawTableBuf!.buffer, 0, dtCopy, 0, recordCount * RECORD_BYTES);
|
|
3966
3299
|
enc0.copyBufferToBuffer(target.drawHeap.buffer, 0, dhCopy, 0, recordCount * target.layout.drawHeaderBytes);
|
|
3967
3300
|
device.queue.submit([enc0.finish()]);
|
|
3968
3301
|
await dtCopy.mapAsync(GPUMapMode.READ);
|
|
@@ -4011,7 +3344,7 @@ export function buildHeapScene(
|
|
|
4011
3344
|
});
|
|
4012
3345
|
{
|
|
4013
3346
|
const enc1 = device.createCommandEncoder();
|
|
4014
|
-
enc1.copyBufferToBuffer(target.firstDrawInTileBuf!.buffer, 0, fdtCopy, 0, fdtSize);
|
|
3347
|
+
enc1.copyBufferToBuffer(target.slots[0]!.firstDrawInTileBuf!.buffer, 0, fdtCopy, 0, fdtSize);
|
|
4015
3348
|
device.queue.submit([enc1.finish()]);
|
|
4016
3349
|
await fdtCopy.mapAsync(GPUMapMode.READ);
|
|
4017
3350
|
}
|
|
@@ -4166,9 +3499,9 @@ export function buildHeapScene(
|
|
|
4166
3499
|
// own bytes).
|
|
4167
3500
|
bucketIdx = 0;
|
|
4168
3501
|
for (const target of buckets) {
|
|
4169
|
-
if (target.recordCount === 0) { bucketIdx++; continue; }
|
|
3502
|
+
if (target.slots[0]!.recordCount === 0) { bucketIdx++; continue; }
|
|
4170
3503
|
// Re-stage drawHeap for this bucket.
|
|
4171
|
-
const dhBytes = target.recordCount * target.layout.drawHeaderBytes;
|
|
3504
|
+
const dhBytes = target.slots[0]!.recordCount * target.layout.drawHeaderBytes;
|
|
4172
3505
|
const dhCopy = device.createBuffer({
|
|
4173
3506
|
size: dhBytes, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
|
|
4174
3507
|
});
|
|
@@ -4178,7 +3511,7 @@ export function buildHeapScene(
|
|
|
4178
3511
|
await dhCopy.mapAsync(GPUMapMode.READ);
|
|
4179
3512
|
const dhU32 = new Uint32Array(dhCopy.getMappedRange());
|
|
4180
3513
|
const stride2 = target.layout.drawHeaderBytes;
|
|
4181
|
-
for (let slot = 0; slot < target.recordCount; slot++) {
|
|
3514
|
+
for (let slot = 0; slot < target.slots[0]!.recordCount; slot++) {
|
|
4182
3515
|
for (const f of target.layout.drawHeaderFields) {
|
|
4183
3516
|
if (f.kind !== "uniform-ref" && f.kind !== "attribute-ref") continue;
|
|
4184
3517
|
const ref = dhU32[(slot * stride2 + f.byteOffset) >>> 2];
|
|
@@ -4238,11 +3571,11 @@ export function buildHeapScene(
|
|
|
4238
3571
|
let gpuMismatches = 0;
|
|
4239
3572
|
const push = (s: string) => { if (issues.length < 30) issues.push(s); };
|
|
4240
3573
|
|
|
4241
|
-
const target = buckets.find(b => b.recordCount > 0 && b.totalEmitEstimate > 0);
|
|
3574
|
+
const target = buckets.find(b => b.slots[0]!.recordCount > 0 && b.slots[0]!.totalEmitEstimate > 0);
|
|
4242
3575
|
if (target === undefined) return { emitsChecked: 0, gpuMismatches: 0, issues };
|
|
4243
3576
|
|
|
4244
|
-
const recordCount = target.recordCount;
|
|
4245
|
-
const totalEmit = target.totalEmitEstimate;
|
|
3577
|
+
const recordCount = target.slots[0]!.recordCount;
|
|
3578
|
+
const totalEmit = target.slots[0]!.totalEmitEstimate;
|
|
4246
3579
|
const sampleCount = Math.min(samples, totalEmit);
|
|
4247
3580
|
const stride = Math.max(1, Math.floor(totalEmit / sampleCount));
|
|
4248
3581
|
|
|
@@ -4256,7 +3589,7 @@ export function buildHeapScene(
|
|
|
4256
3589
|
});
|
|
4257
3590
|
{
|
|
4258
3591
|
const enc0 = device.createCommandEncoder({ label: "probeBinarySearch.dtCopy" });
|
|
4259
|
-
enc0.copyBufferToBuffer(target.drawTableBuf!.buffer, 0, dtCopy, 0, recordCount * RECORD_BYTES);
|
|
3592
|
+
enc0.copyBufferToBuffer(target.slots[0]!.drawTableBuf!.buffer, 0, dtCopy, 0, recordCount * RECORD_BYTES);
|
|
4260
3593
|
device.queue.submit([enc0.finish()]);
|
|
4261
3594
|
await dtCopy.mapAsync(GPUMapMode.READ);
|
|
4262
3595
|
}
|
|
@@ -4360,8 +3693,8 @@ export function buildHeapScene(
|
|
|
4360
3693
|
const bg = device.createBindGroup({
|
|
4361
3694
|
layout: bgl,
|
|
4362
3695
|
entries: [
|
|
4363
|
-
{ binding: 0, resource: { buffer: target.drawTableBuf!.buffer } },
|
|
4364
|
-
{ binding: 1, resource: { buffer: target.firstDrawInTileBuf!.buffer } },
|
|
3696
|
+
{ binding: 0, resource: { buffer: target.slots[0]!.drawTableBuf!.buffer } },
|
|
3697
|
+
{ binding: 1, resource: { buffer: target.slots[0]!.firstDrawInTileBuf!.buffer } },
|
|
4365
3698
|
{ binding: 2, resource: { buffer: sampleBuf } },
|
|
4366
3699
|
{ binding: 3, resource: { buffer: arena.indices.buffer } },
|
|
4367
3700
|
{ binding: 4, resource: { buffer: outBuf } },
|