@aardworx/wombat.rendering 0.9.14 → 0.9.15
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 +45 -0
- package/dist/runtime/derivedModes/modeKeyCpu.d.ts.map +1 -0
- package/dist/runtime/derivedModes/modeKeyCpu.js +179 -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 +228 -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 +213 -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 +283 -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
|
@@ -49,797 +49,12 @@ import { buildShaderFamily, compileShaderFamily, } from "./heapShaderFamily.js";
|
|
|
49
49
|
import { ATLAS_PAGE_FORMATS, atlasFormatIndex, } from "./textureAtlas/atlasPool.js";
|
|
50
50
|
import { ATLAS_ARRAY_SIZE, ATLAS_LINEAR_BINDING_BASE, ATLAS_SRGB_BINDING_BASE, ATLAS_SAMPLER_BINDING, } from "./heapEffect.js";
|
|
51
51
|
import { DerivedUniformsScene, registerRoDerivations, deregisterRoDerivations, isDerivedRule, STANDARD_DERIVED_RULES, STANDARD_TRAFO_LEAVES, inputsOf, } from "./derivedUniforms/index.js";
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
// future vec4 reads).
|
|
59
|
-
const ALLOC_HEADER_BYTES = 8;
|
|
60
|
-
const ALLOC_HEADER_PAD_TO = 16; // data starts header_offset + 16
|
|
61
|
-
// Encoding-tag enum (low 16 bits of typeId).
|
|
62
|
-
const ENC_V3F_TIGHT = 1; // tightly-packed array of vec3<f32> (12 B/elt)
|
|
63
|
-
// Semantic-tag enum (high 16 bits of typeId). Optional metadata —
|
|
64
|
-
// the shader doesn't branch on this.
|
|
65
|
-
const SEM_POSITIONS = 1;
|
|
66
|
-
const SEM_NORMALS = 2;
|
|
67
|
-
const ALIGN16 = (n) => (n + 15) & ~15;
|
|
68
|
-
function packMat44(m, dst, off) {
|
|
69
|
-
// Zero-alloc flat copy (row-major) straight into the f32 staging
|
|
70
|
-
// buffer — `copyTo` does `dst.set(m._data, off)` which narrows f64→f32
|
|
71
|
-
// on store, no throwaway `number[]` per call.
|
|
72
|
-
m.copyTo(dst, off);
|
|
73
|
-
}
|
|
74
|
-
const PACKER_MAT4 = {
|
|
75
|
-
dataBytes: 64, typeId: 0,
|
|
76
|
-
pack: (val, dst, off) => {
|
|
77
|
-
// Accept Trafo3d (uses .forward) or M44d directly.
|
|
78
|
-
const m = val.forward !== undefined
|
|
79
|
-
? val.forward
|
|
80
|
-
: val;
|
|
81
|
-
packMat44(m, dst, off);
|
|
82
|
-
},
|
|
83
|
-
};
|
|
84
|
-
const PACKER_VEC4 = {
|
|
85
|
-
dataBytes: 16, typeId: 0,
|
|
86
|
-
pack: (val, dst, off) => {
|
|
87
|
-
const v = val;
|
|
88
|
-
dst[off + 0] = v.x;
|
|
89
|
-
dst[off + 1] = v.y;
|
|
90
|
-
dst[off + 2] = v.z;
|
|
91
|
-
dst[off + 3] = v.w;
|
|
92
|
-
},
|
|
93
|
-
};
|
|
94
|
-
const PACKER_VEC3 = {
|
|
95
|
-
dataBytes: 12, typeId: 0,
|
|
96
|
-
pack: (val, dst, off) => {
|
|
97
|
-
// V3f or V3d both expose .x/.y/.z; cast through a common shape.
|
|
98
|
-
const v = val;
|
|
99
|
-
dst[off + 0] = v.x;
|
|
100
|
-
dst[off + 1] = v.y;
|
|
101
|
-
dst[off + 2] = v.z;
|
|
102
|
-
},
|
|
103
|
-
};
|
|
104
|
-
const PACKER_VEC2 = {
|
|
105
|
-
dataBytes: 8, typeId: 0,
|
|
106
|
-
pack: (val, dst, off) => {
|
|
107
|
-
const v = val;
|
|
108
|
-
dst[off + 0] = v.x;
|
|
109
|
-
dst[off + 1] = v.y;
|
|
110
|
-
},
|
|
111
|
-
};
|
|
112
|
-
const PACKER_F32 = {
|
|
113
|
-
dataBytes: 4, typeId: 0,
|
|
114
|
-
pack: (val, dst, off) => { dst[off] = val; },
|
|
115
|
-
};
|
|
116
|
-
// Integer scalars / vectors. The arena is fronted by a `Float32Array`,
|
|
117
|
-
// so writing raw bits has to go through a same-buffer Uint32/Int32 view
|
|
118
|
-
// to avoid the lossy `i32 → f32` coercion you'd get from a direct
|
|
119
|
-
// `dst[off] = ...` assignment.
|
|
120
|
-
function makeIntPacker(ctor, dim) {
|
|
121
|
-
const bytes = dim * 4;
|
|
122
|
-
if (dim === 1) {
|
|
123
|
-
return {
|
|
124
|
-
dataBytes: bytes, typeId: 0,
|
|
125
|
-
pack: (val, dst, off) => {
|
|
126
|
-
new ctor(dst.buffer, dst.byteOffset + off * 4, 1)[0] = val;
|
|
127
|
-
},
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
// Vector: accept {x,y,z,w} components.
|
|
131
|
-
return {
|
|
132
|
-
dataBytes: bytes, typeId: 0,
|
|
133
|
-
pack: (val, dst, off) => {
|
|
134
|
-
const view = new ctor(dst.buffer, dst.byteOffset + off * 4, dim);
|
|
135
|
-
const v = val;
|
|
136
|
-
view[0] = v.x;
|
|
137
|
-
view[1] = v.y;
|
|
138
|
-
if (dim >= 3)
|
|
139
|
-
view[2] = v.z;
|
|
140
|
-
if (dim >= 4)
|
|
141
|
-
view[3] = v.w;
|
|
142
|
-
},
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
const PACKER_U32 = makeIntPacker(Uint32Array, 1);
|
|
146
|
-
const PACKER_UVEC2 = makeIntPacker(Uint32Array, 2);
|
|
147
|
-
const PACKER_UVEC3 = makeIntPacker(Uint32Array, 3);
|
|
148
|
-
const PACKER_UVEC4 = makeIntPacker(Uint32Array, 4);
|
|
149
|
-
const PACKER_I32 = makeIntPacker(Int32Array, 1);
|
|
150
|
-
const PACKER_IVEC2 = makeIntPacker(Int32Array, 2);
|
|
151
|
-
const PACKER_IVEC3 = makeIntPacker(Int32Array, 3);
|
|
152
|
-
const PACKER_IVEC4 = makeIntPacker(Int32Array, 4);
|
|
153
|
-
function packerForWgslType(wgslType) {
|
|
154
|
-
switch (wgslType) {
|
|
155
|
-
case "mat4x4<f32>": return PACKER_MAT4;
|
|
156
|
-
case "vec4<f32>": return PACKER_VEC4;
|
|
157
|
-
case "vec3<f32>": return PACKER_VEC3;
|
|
158
|
-
case "vec2<f32>": return PACKER_VEC2;
|
|
159
|
-
case "f32": return PACKER_F32;
|
|
160
|
-
case "u32": return PACKER_U32;
|
|
161
|
-
case "vec2<u32>": return PACKER_UVEC2;
|
|
162
|
-
case "vec3<u32>": return PACKER_UVEC3;
|
|
163
|
-
case "vec4<u32>": return PACKER_UVEC4;
|
|
164
|
-
case "i32": return PACKER_I32;
|
|
165
|
-
case "vec2<i32>": return PACKER_IVEC2;
|
|
166
|
-
case "vec3<i32>": return PACKER_IVEC3;
|
|
167
|
-
case "vec4<i32>": return PACKER_IVEC4;
|
|
168
|
-
default:
|
|
169
|
-
throw new Error(`heapScene: no JS-side packer for WGSL type '${wgslType}'`);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
// ---------------------------------------------------------------------------
|
|
173
|
-
// Resizable buffer (pow2 grow + GPU-side copy on resize)
|
|
174
|
-
// ---------------------------------------------------------------------------
|
|
175
|
-
const MIN_BUFFER_BYTES = 64 * 1024;
|
|
176
|
-
const POW2 = (n) => {
|
|
177
|
-
let p = 1;
|
|
178
|
-
while (p < n)
|
|
179
|
-
p <<= 1;
|
|
180
|
-
return p;
|
|
181
|
-
};
|
|
182
|
-
/**
|
|
183
|
-
* A GPUBuffer that can grow to next power-of-two on demand. On grow,
|
|
184
|
-
* a fresh buffer is created at the new size, the live tail copied
|
|
185
|
-
* over via copyBufferToBuffer, and dependents (bind groups, mostly)
|
|
186
|
-
* are notified to rebuild via the `onResize` callback.
|
|
187
|
-
*
|
|
188
|
-
* `usedBytes` is the high-water mark — the runtime advances this as
|
|
189
|
-
* it allocates, and `ensureCapacity` grows when required. This
|
|
190
|
-
* separates allocation policy from grow policy.
|
|
191
|
-
*/
|
|
192
|
-
class GrowBuffer {
|
|
193
|
-
device;
|
|
194
|
-
label;
|
|
195
|
-
usage;
|
|
196
|
-
buf;
|
|
197
|
-
cap;
|
|
198
|
-
used = 0;
|
|
199
|
-
listeners = new Set();
|
|
200
|
-
constructor(device, label, usage, initialBytes) {
|
|
201
|
-
this.device = device;
|
|
202
|
-
this.label = label;
|
|
203
|
-
this.usage = usage;
|
|
204
|
-
this.cap = Math.max(MIN_BUFFER_BYTES, POW2(initialBytes));
|
|
205
|
-
this.buf = device.createBuffer({ size: this.cap, usage: usage | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC, label });
|
|
206
|
-
}
|
|
207
|
-
get buffer() { return this.buf; }
|
|
208
|
-
get capacity() { return this.cap; }
|
|
209
|
-
get usedBytes() { return this.used; }
|
|
210
|
-
setUsed(n) { this.used = n; }
|
|
211
|
-
onResize(cb) {
|
|
212
|
-
this.listeners.add(cb);
|
|
213
|
-
return { dispose: () => { this.listeners.delete(cb); } };
|
|
214
|
-
}
|
|
215
|
-
/** Ensure the buffer is at least `bytes` capacity. Grows by pow2 + copies live tail. */
|
|
216
|
-
ensureCapacity(bytes) {
|
|
217
|
-
if (bytes <= this.cap)
|
|
218
|
-
return;
|
|
219
|
-
const newCap = POW2(bytes);
|
|
220
|
-
const newBuf = this.device.createBuffer({
|
|
221
|
-
size: newCap, usage: this.usage | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC, label: this.label,
|
|
222
|
-
});
|
|
223
|
-
if (this.used > 0) {
|
|
224
|
-
const enc = this.device.createCommandEncoder({ label: `${this.label}: grow-copy` });
|
|
225
|
-
enc.copyBufferToBuffer(this.buf, 0, newBuf, 0, ALIGN16(this.used));
|
|
226
|
-
this.device.queue.submit([enc.finish()]);
|
|
227
|
-
}
|
|
228
|
-
this.buf.destroy();
|
|
229
|
-
this.buf = newBuf;
|
|
230
|
-
this.cap = newCap;
|
|
231
|
-
for (const cb of this.listeners)
|
|
232
|
-
cb();
|
|
233
|
-
}
|
|
234
|
-
destroy() { this.buf.destroy(); }
|
|
235
|
-
}
|
|
236
|
-
/**
|
|
237
|
-
* Aval-keyed pool of arena allocations. One allocation per unique
|
|
238
|
-
* aval (object identity). Two draws referencing the same aval share
|
|
239
|
-
* the allocation; their DrawHeaders carry the same u32 ref. Holds
|
|
240
|
-
* uniforms (fixed-size scalars/vectors/matrices) AND attribute arrays
|
|
241
|
-
* (variable-size). The caller decides `dataBytes` + `length` per
|
|
242
|
-
* acquisition — the pool just keys on aval identity and refcounts.
|
|
243
|
-
*
|
|
244
|
-
* Sharing emerges from aval identity — no separate "frequency"
|
|
245
|
-
* declaration needed. A `cval` shared by all draws → 1 alloc.
|
|
246
|
-
* A static positions array shared across instanced draws → 1 alloc.
|
|
247
|
-
* Same code path either way.
|
|
248
|
-
*/
|
|
249
|
-
class UniformPool {
|
|
250
|
-
// Keyed by `aval<unknown>` *by reference* (a plain JS `Map`). These
|
|
251
|
-
// keys are overwhelmingly reactive `cval`s (per-object trafos,
|
|
252
|
-
// colours, …) and the hot path is `acquire`/`release` ~once per
|
|
253
|
-
// drawHeader field per RO. A content-keyed `HashTable` would buy
|
|
254
|
-
// nothing here — reactive avals never compare content-equal — and
|
|
255
|
-
// would cost: reactive avals have no `equals`/`getHashCode`, so a
|
|
256
|
-
// `HashTable` falls back to a WeakMap-counter identity hash per
|
|
257
|
-
// lookup, measurably slower than `Map`'s native hashing. (Constant-
|
|
258
|
-
// aval dedup matters where keys are *texture* avals — there the
|
|
259
|
-
// `AtlasPool` is content-keyed; constant avals there carry a cached
|
|
260
|
-
// hash and a fast `equals`.)
|
|
261
|
-
byAval = new Map();
|
|
262
|
-
has(aval) { return this.byAval.has(aval); }
|
|
263
|
-
entry(aval) { return this.byAval.get(aval); }
|
|
264
|
-
/**
|
|
265
|
-
* Acquire (or share) an allocation for `aval`. Caller passes the
|
|
266
|
-
* pre-read `value` (so the pool doesn't need a token) plus the
|
|
267
|
-
* (`dataBytes`, `typeId`, `length`, `pack`) describing how to lay
|
|
268
|
-
* it out. If a new allocation is made, the value is packed and
|
|
269
|
-
* uploaded immediately.
|
|
270
|
-
*/
|
|
271
|
-
acquire(device, arena, aval, value, dataBytes, typeId, length, pack) {
|
|
272
|
-
const existing = this.byAval.get(aval);
|
|
273
|
-
if (existing !== undefined) {
|
|
274
|
-
existing.refcount++;
|
|
275
|
-
return existing.ref;
|
|
276
|
-
}
|
|
277
|
-
const ref = arena.alloc(dataBytes);
|
|
278
|
-
const allocBytes = ALIGN16(ALLOC_HEADER_PAD_TO + dataBytes);
|
|
279
|
-
const buf = new ArrayBuffer(allocBytes);
|
|
280
|
-
const u32 = new Uint32Array(buf);
|
|
281
|
-
const f32 = new Float32Array(buf);
|
|
282
|
-
u32[0] = typeId;
|
|
283
|
-
u32[1] = length;
|
|
284
|
-
// stride_bytes (offset 8): bytes per element. Lets the VS decode
|
|
285
|
-
// pick V3- vs V4-tight load expressions for vec4 attributes
|
|
286
|
-
// (and is informative for everything else).
|
|
287
|
-
u32[2] = length > 0 ? Math.floor(dataBytes / length) : 0;
|
|
288
|
-
pack(value, f32, ALLOC_HEADER_PAD_TO / 4);
|
|
289
|
-
arena.write(ref, new Uint8Array(buf));
|
|
290
|
-
void device;
|
|
291
|
-
this.byAval.set(aval, { ref, dataBytes, typeId, pack, refcount: 1 });
|
|
292
|
-
return ref;
|
|
293
|
-
}
|
|
294
|
-
/** Decrement refcount; if zero, free the arena allocation. */
|
|
295
|
-
release(arena, aval) {
|
|
296
|
-
const e = this.byAval.get(aval);
|
|
297
|
-
if (e === undefined)
|
|
298
|
-
return;
|
|
299
|
-
e.refcount--;
|
|
300
|
-
if (e.refcount > 0)
|
|
301
|
-
return;
|
|
302
|
-
arena.release(e.ref, ALIGN16(ALLOC_HEADER_PAD_TO + e.dataBytes));
|
|
303
|
-
this.byAval.delete(aval);
|
|
304
|
-
}
|
|
305
|
-
/** Re-pack one entry's data region into the arena's CPU shadow. */
|
|
306
|
-
repack(device, arena, aval, val) {
|
|
307
|
-
const e = this.byAval.get(aval);
|
|
308
|
-
if (e === undefined)
|
|
309
|
-
return;
|
|
310
|
-
const dst = new Float32Array(e.dataBytes / 4);
|
|
311
|
-
e.pack(val, dst, 0);
|
|
312
|
-
arena.write(e.ref + ALLOC_HEADER_PAD_TO, new Uint8Array(dst.buffer, dst.byteOffset, e.dataBytes));
|
|
313
|
-
void device;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
/**
|
|
317
|
-
* Aval-keyed pool over the `IndexAllocator`. Two draws referencing
|
|
318
|
-
* the same `Uint32Array` (or aval thereof) share an index range —
|
|
319
|
-
* 19K instanced clones of the same mesh share one allocation, one
|
|
320
|
-
* upload. Index data is treated as immutable for the aval's
|
|
321
|
-
* lifetime: an aval mark won't repack (we'd have to free + re-alloc
|
|
322
|
-
* since size changes are likely). Use a fresh aval to swap meshes.
|
|
323
|
-
*
|
|
324
|
-
* **Value-equality dedup for constant avals (§5b):** when an
|
|
325
|
-
* incoming aval has `isConstant === true`, the pool also keys by
|
|
326
|
-
* the underlying `ArrayBuffer` tuple `(buffer, byteOffset,
|
|
327
|
-
* byteLength)`. Two distinct constant avals wrapping the same
|
|
328
|
-
* `Uint32Array` view (or two views over the same backing buffer
|
|
329
|
-
* with matching offsets) collapse to one allocation. Hashing
|
|
330
|
-
* kilobytes of indices on every acquire would be wasteful; the
|
|
331
|
-
* tuple key catches the realistic "one ArrayBuffer shared across
|
|
332
|
-
* many aval wrappers" pattern, which is the only one that matters
|
|
333
|
-
* for the heap path. Reactive (non-constant) avals fall through
|
|
334
|
-
* to identity-only — their content can change and the pool can't
|
|
335
|
-
* silently merge them.
|
|
336
|
-
*/
|
|
337
|
-
class IndexPool {
|
|
338
|
-
// Per-aval binding. `perAvalCount` tracks acquire/release balance
|
|
339
|
-
// for THIS aval; `entry` is the shared allocation (one entry can be
|
|
340
|
-
// referenced by many aliasing avals via §5b dedup).
|
|
341
|
-
byAval = new Map();
|
|
342
|
-
byValueKey = new Map();
|
|
343
|
-
// Stable per-ArrayBuffer numeric id for value-key composition.
|
|
344
|
-
// WeakMap-backed so buffers GC'd elsewhere drop their entry too.
|
|
345
|
-
bufferIds = new WeakMap();
|
|
346
|
-
nextBufferId = 1;
|
|
347
|
-
bufferIdOf(buf) {
|
|
348
|
-
let id = this.bufferIds.get(buf);
|
|
349
|
-
if (id === undefined) {
|
|
350
|
-
id = this.nextBufferId++;
|
|
351
|
-
this.bufferIds.set(buf, id);
|
|
352
|
-
}
|
|
353
|
-
return id;
|
|
354
|
-
}
|
|
355
|
-
acquire(device, indices, aval, arr) {
|
|
356
|
-
const bound = this.byAval.get(aval);
|
|
357
|
-
if (bound !== undefined) {
|
|
358
|
-
bound.perAvalCount++;
|
|
359
|
-
bound.entry.totalRefcount++;
|
|
360
|
-
return { firstIndex: bound.entry.firstIndex, count: bound.entry.count };
|
|
361
|
-
}
|
|
362
|
-
let valueKey;
|
|
363
|
-
if (aval.isConstant) {
|
|
364
|
-
valueKey = `${this.bufferIdOf(arr.buffer)}:${arr.byteOffset}:${arr.byteLength}`;
|
|
365
|
-
const shared = this.byValueKey.get(valueKey);
|
|
366
|
-
if (shared !== undefined) {
|
|
367
|
-
shared.totalRefcount++;
|
|
368
|
-
this.byAval.set(aval, { entry: shared, perAvalCount: 1 });
|
|
369
|
-
return { firstIndex: shared.firstIndex, count: shared.count };
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
const firstIndex = indices.alloc(arr.length);
|
|
373
|
-
indices.write(firstIndex * 4, new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength));
|
|
374
|
-
void device;
|
|
375
|
-
const entry = { firstIndex, count: arr.length, totalRefcount: 1, valueKey };
|
|
376
|
-
this.byAval.set(aval, { entry, perAvalCount: 1 });
|
|
377
|
-
if (valueKey !== undefined)
|
|
378
|
-
this.byValueKey.set(valueKey, entry);
|
|
379
|
-
return { firstIndex, count: arr.length };
|
|
380
|
-
}
|
|
381
|
-
release(indices, aval) {
|
|
382
|
-
const bound = this.byAval.get(aval);
|
|
383
|
-
if (bound === undefined)
|
|
384
|
-
return;
|
|
385
|
-
bound.perAvalCount--;
|
|
386
|
-
bound.entry.totalRefcount--;
|
|
387
|
-
if (bound.perAvalCount === 0)
|
|
388
|
-
this.byAval.delete(aval);
|
|
389
|
-
if (bound.entry.totalRefcount > 0)
|
|
390
|
-
return;
|
|
391
|
-
indices.release(bound.entry.firstIndex, bound.entry.count);
|
|
392
|
-
if (bound.entry.valueKey !== undefined) {
|
|
393
|
-
this.byValueKey.delete(bound.entry.valueKey);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
// ---------------------------------------------------------------------------
|
|
398
|
-
// DrawHeap (slot-indexed) and AttributeArena (byte-bump) allocators
|
|
399
|
-
// ---------------------------------------------------------------------------
|
|
400
|
-
/**
|
|
401
|
-
* Slot-indexed allocator over a GrowBuffer. `slotBytes` is set per-
|
|
402
|
-
* instance — each bucket sizes its DrawHeader from its effect's
|
|
403
|
-
* schema, so a bucket whose layout is e.g. 96 B / slot uses a
|
|
404
|
-
* DrawHeap with `slotBytes=96`.
|
|
405
|
-
*/
|
|
406
|
-
class DrawHeap {
|
|
407
|
-
buf;
|
|
408
|
-
slotBytes;
|
|
409
|
-
free = [];
|
|
410
|
-
nextSlot = 0;
|
|
411
|
-
constructor(buf, slotBytes) {
|
|
412
|
-
this.buf = buf;
|
|
413
|
-
this.slotBytes = slotBytes;
|
|
414
|
-
}
|
|
415
|
-
get buffer() { return this.buf.buffer; }
|
|
416
|
-
/** Bytes per slot — caller multiplies by slot index for byte offsets. */
|
|
417
|
-
get bytesPerSlot() { return this.slotBytes; }
|
|
418
|
-
/** High-water mark in bytes (used to size bind-group entry on rebuild). */
|
|
419
|
-
get usedBytes() { return this.nextSlot * this.slotBytes; }
|
|
420
|
-
alloc() {
|
|
421
|
-
const slot = this.free.length > 0 ? this.free.pop() : this.nextSlot++;
|
|
422
|
-
this.buf.ensureCapacity((slot + 1) * this.slotBytes);
|
|
423
|
-
this.buf.setUsed(Math.max(this.buf.usedBytes, (slot + 1) * this.slotBytes));
|
|
424
|
-
return slot;
|
|
425
|
-
}
|
|
426
|
-
release(slot) { this.free.push(slot); }
|
|
427
|
-
onResize(cb) { return this.buf.onResize(cb); }
|
|
428
|
-
destroy() { this.buf.destroy(); }
|
|
429
|
-
}
|
|
430
|
-
/**
|
|
431
|
-
* Byte-bump allocator over a GrowBuffer for variable-size attribute
|
|
432
|
-
* allocations. Each allocation gets a 16-byte aligned start (8-byte
|
|
433
|
-
* (typeId, length) header at the start, data 16 bytes in). Frees go
|
|
434
|
-
* onto a list keyed by size for simple first-fit reuse later — for
|
|
435
|
-
* now `release` just records the gap and the bump cursor never
|
|
436
|
-
* shrinks.
|
|
437
|
-
*/
|
|
438
|
-
class AttributeArena {
|
|
439
|
-
buf;
|
|
440
|
-
cursor = 0;
|
|
441
|
-
// (offset, size) free entries; first-fit reuse not yet implemented.
|
|
442
|
-
freeList = [];
|
|
443
|
-
// CPU shadow of the entire GPU buffer. Writes go here first; a
|
|
444
|
-
// single `device.queue.writeBuffer` per dirty contiguous range
|
|
445
|
-
// lifts them to the GPU at flush time. At the cost of doubling
|
|
446
|
-
// host memory we collapse N small writeBuffer calls (10K+ at
|
|
447
|
-
// initial population) to 1 per frame.
|
|
448
|
-
shadow;
|
|
449
|
-
dirtyMin = Infinity;
|
|
450
|
-
dirtyMax = 0;
|
|
451
|
-
constructor(buf) {
|
|
452
|
-
this.buf = buf;
|
|
453
|
-
this.shadow = new Uint8Array(buf.capacity);
|
|
454
|
-
buf.onResize(() => {
|
|
455
|
-
const grown = new Uint8Array(buf.capacity);
|
|
456
|
-
grown.set(this.shadow);
|
|
457
|
-
this.shadow = grown;
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
get buffer() { return this.buf.buffer; }
|
|
461
|
-
get capacity() { return this.buf.capacity; }
|
|
462
|
-
get usedBytes() { return this.cursor; }
|
|
463
|
-
/**
|
|
464
|
-
* Stage `data` to the shadow at byte offset `dst`. Tracks the
|
|
465
|
-
* dirty range so `flush(device)` can emit a single writeBuffer
|
|
466
|
-
* covering everything dirty since the last flush.
|
|
467
|
-
*/
|
|
468
|
-
write(dst, data) {
|
|
469
|
-
this.shadow.set(data, dst);
|
|
470
|
-
if (dst < this.dirtyMin)
|
|
471
|
-
this.dirtyMin = dst;
|
|
472
|
-
const end = dst + data.byteLength;
|
|
473
|
-
if (end > this.dirtyMax)
|
|
474
|
-
this.dirtyMax = end;
|
|
475
|
-
}
|
|
476
|
-
flush(device) {
|
|
477
|
-
if (this.dirtyMax <= this.dirtyMin)
|
|
478
|
-
return;
|
|
479
|
-
device.queue.writeBuffer(this.buf.buffer, this.dirtyMin, this.shadow.buffer, this.shadow.byteOffset + this.dirtyMin, this.dirtyMax - this.dirtyMin);
|
|
480
|
-
this.dirtyMin = Infinity;
|
|
481
|
-
this.dirtyMax = 0;
|
|
482
|
-
}
|
|
483
|
-
/**
|
|
484
|
-
* Allocate space for one attribute. Returns the byte ref (offset
|
|
485
|
-
* to the header — data lives at ref + 16).
|
|
486
|
-
*/
|
|
487
|
-
alloc(dataBytes) {
|
|
488
|
-
const allocBytes = ALIGN16(ALLOC_HEADER_PAD_TO + dataBytes);
|
|
489
|
-
// First-fit reuse from free list.
|
|
490
|
-
for (let i = 0; i < this.freeList.length; i++) {
|
|
491
|
-
const f = this.freeList[i];
|
|
492
|
-
if (f.size >= allocBytes) {
|
|
493
|
-
const ref = f.off;
|
|
494
|
-
if (f.size === allocBytes)
|
|
495
|
-
this.freeList.splice(i, 1);
|
|
496
|
-
else {
|
|
497
|
-
f.off += allocBytes;
|
|
498
|
-
f.size -= allocBytes;
|
|
499
|
-
}
|
|
500
|
-
return ref;
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
const ref = this.cursor;
|
|
504
|
-
this.cursor += allocBytes;
|
|
505
|
-
this.buf.ensureCapacity(this.cursor);
|
|
506
|
-
this.buf.setUsed(this.cursor);
|
|
507
|
-
return ref;
|
|
508
|
-
}
|
|
509
|
-
release(ref, dataBytes) {
|
|
510
|
-
const allocBytes = ALIGN16(ALLOC_HEADER_PAD_TO + dataBytes);
|
|
511
|
-
insertSortedFreeBlock(this.freeList, ref, allocBytes);
|
|
512
|
-
}
|
|
513
|
-
onResize(cb) { return this.buf.onResize(cb); }
|
|
514
|
-
destroy() { this.buf.destroy(); }
|
|
515
|
-
}
|
|
516
|
-
/**
|
|
517
|
-
* Insert `{off, size}` into a free-list kept sorted by `off`, then
|
|
518
|
-
* coalesce with the two immediate neighbours.
|
|
519
|
-
*
|
|
520
|
-
* The list invariant — sorted, non-overlapping, never-adjacent — is
|
|
521
|
-
* preserved across allocs (which take from the front or split a
|
|
522
|
-
* block) and releases (this function). The previous implementation
|
|
523
|
-
* did `push + Array.prototype.sort + linear coalesce scan`, which is
|
|
524
|
-
* O(N log N) per release. Under a 500-RO bulk-remove the sort
|
|
525
|
-
* dominated `removeDraw` (~41 ms of self-time in the heap-demo-sg
|
|
526
|
-
* toggle profile). Binary-search insert + 2-neighbour merge collapses
|
|
527
|
-
* that to O(log N + N-shift), and is principled — the sort never
|
|
528
|
-
* actually mattered since we already had the sorted prefix as an
|
|
529
|
-
* invariant.
|
|
530
|
-
*/
|
|
531
|
-
function insertSortedFreeBlock(freeList, off, size) {
|
|
532
|
-
// Binary-search for the insertion index (first entry whose off > new).
|
|
533
|
-
let lo = 0, hi = freeList.length;
|
|
534
|
-
while (lo < hi) {
|
|
535
|
-
const mid = (lo + hi) >>> 1;
|
|
536
|
-
if (freeList[mid].off <= off)
|
|
537
|
-
lo = mid + 1;
|
|
538
|
-
else
|
|
539
|
-
hi = mid;
|
|
540
|
-
}
|
|
541
|
-
// lo is the index where the new entry would be inserted.
|
|
542
|
-
// Try merging with the predecessor first; if successful, the merged
|
|
543
|
-
// block may now be adjacent to its (former) successor too.
|
|
544
|
-
const prev = lo > 0 ? freeList[lo - 1] : undefined;
|
|
545
|
-
if (prev !== undefined && prev.off + prev.size === off) {
|
|
546
|
-
prev.size += size;
|
|
547
|
-
// Check forward-merge with what was freeList[lo].
|
|
548
|
-
const next = freeList[lo];
|
|
549
|
-
if (next !== undefined && prev.off + prev.size === next.off) {
|
|
550
|
-
prev.size += next.size;
|
|
551
|
-
freeList.splice(lo, 1);
|
|
552
|
-
}
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
const next = freeList[lo];
|
|
556
|
-
if (next !== undefined && off + size === next.off) {
|
|
557
|
-
next.off = off;
|
|
558
|
-
next.size += size;
|
|
559
|
-
return;
|
|
560
|
-
}
|
|
561
|
-
freeList.splice(lo, 0, { off, size });
|
|
562
|
-
}
|
|
563
|
-
/**
|
|
564
|
-
* Element-bump allocator over an index GrowBuffer (units = u32). Each
|
|
565
|
-
* draw's index range is allocated as one block; on release the block
|
|
566
|
-
* is returned to a free list and can be reused first-fit.
|
|
567
|
-
*/
|
|
568
|
-
class IndexAllocator {
|
|
569
|
-
buf;
|
|
570
|
-
cursor = 0; // in u32s, not bytes
|
|
571
|
-
freeList = [];
|
|
572
|
-
// CPU shadow + dirty range, same shape as AttributeArena. Index
|
|
573
|
-
// uploads (one per drawn mesh's index buffer) get coalesced to a
|
|
574
|
-
// single writeBuffer per dirty range at flush time.
|
|
575
|
-
shadow;
|
|
576
|
-
dirtyMin = Infinity;
|
|
577
|
-
dirtyMax = 0;
|
|
578
|
-
constructor(buf) {
|
|
579
|
-
this.buf = buf;
|
|
580
|
-
this.shadow = new Uint8Array(buf.capacity);
|
|
581
|
-
buf.onResize(() => {
|
|
582
|
-
const grown = new Uint8Array(buf.capacity);
|
|
583
|
-
grown.set(this.shadow);
|
|
584
|
-
this.shadow = grown;
|
|
585
|
-
});
|
|
586
|
-
}
|
|
587
|
-
get buffer() { return this.buf.buffer; }
|
|
588
|
-
get usedElements() { return this.cursor; }
|
|
589
|
-
/** Stage `data` (bytes) at the given byte offset; tracks dirty range. */
|
|
590
|
-
write(dstByteOffset, data) {
|
|
591
|
-
this.shadow.set(data, dstByteOffset);
|
|
592
|
-
if (dstByteOffset < this.dirtyMin)
|
|
593
|
-
this.dirtyMin = dstByteOffset;
|
|
594
|
-
const end = dstByteOffset + data.byteLength;
|
|
595
|
-
if (end > this.dirtyMax)
|
|
596
|
-
this.dirtyMax = end;
|
|
597
|
-
}
|
|
598
|
-
flush(device) {
|
|
599
|
-
if (this.dirtyMax <= this.dirtyMin)
|
|
600
|
-
return;
|
|
601
|
-
device.queue.writeBuffer(this.buf.buffer, this.dirtyMin, this.shadow.buffer, this.shadow.byteOffset + this.dirtyMin, this.dirtyMax - this.dirtyMin);
|
|
602
|
-
this.dirtyMin = Infinity;
|
|
603
|
-
this.dirtyMax = 0;
|
|
604
|
-
}
|
|
605
|
-
alloc(elements) {
|
|
606
|
-
for (let i = 0; i < this.freeList.length; i++) {
|
|
607
|
-
const f = this.freeList[i];
|
|
608
|
-
if (f.size >= elements) {
|
|
609
|
-
const off = f.off;
|
|
610
|
-
if (f.size === elements)
|
|
611
|
-
this.freeList.splice(i, 1);
|
|
612
|
-
else {
|
|
613
|
-
f.off += elements;
|
|
614
|
-
f.size -= elements;
|
|
615
|
-
}
|
|
616
|
-
return off;
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
const off = this.cursor;
|
|
620
|
-
this.cursor += elements;
|
|
621
|
-
this.buf.ensureCapacity(this.cursor * 4);
|
|
622
|
-
this.buf.setUsed(this.cursor * 4);
|
|
623
|
-
return off;
|
|
624
|
-
}
|
|
625
|
-
release(off, elements) {
|
|
626
|
-
insertSortedFreeBlock(this.freeList, off, elements);
|
|
627
|
-
}
|
|
628
|
-
onResize(cb) { return this.buf.onResize(cb); }
|
|
629
|
-
destroy() { this.buf.destroy(); }
|
|
630
|
-
}
|
|
631
|
-
function buildArenaState(device, attrBytesHint, idxBytesHint, label, idxExtraUsage = 0) {
|
|
632
|
-
const attrs = new AttributeArena(new GrowBuffer(device, `${label}/attrs`, GPUBufferUsage.STORAGE, attrBytesHint));
|
|
633
|
-
const indices = new IndexAllocator(new GrowBuffer(device, `${label}/idx`, GPUBufferUsage.INDEX | idxExtraUsage, idxBytesHint));
|
|
634
|
-
return { attrs, indices };
|
|
635
|
-
}
|
|
636
|
-
function arenaBytes(arena) {
|
|
637
|
-
return arena.attrs.usedBytes + arena.indices.usedElements * 4;
|
|
638
|
-
}
|
|
639
|
-
/** Upload a single attribute — header (typeId, length) + data — into the arena at byte offset `ref`. */
|
|
640
|
-
function writeAttribute(device, buf, ref, typeId, length, data) {
|
|
641
|
-
const allocBytes = ALIGN16(ALLOC_HEADER_PAD_TO + data.byteLength);
|
|
642
|
-
const staging = new ArrayBuffer(allocBytes);
|
|
643
|
-
const u32 = new Uint32Array(staging);
|
|
644
|
-
const f32 = new Float32Array(staging);
|
|
645
|
-
u32[0] = typeId;
|
|
646
|
-
u32[1] = length;
|
|
647
|
-
f32.set(data, ALLOC_HEADER_PAD_TO / 4);
|
|
648
|
-
device.queue.writeBuffer(buf, ref, staging, 0, allocBytes);
|
|
649
|
-
}
|
|
650
|
-
function asAval(v) {
|
|
651
|
-
return (typeof v === "object" && v !== null && typeof v.getValue === "function")
|
|
652
|
-
? v
|
|
653
|
-
: AVal.constant(v);
|
|
654
|
-
}
|
|
655
|
-
/** Heuristic predicate — BufferView has `buffer: aval<IBuffer>` + elementType. */
|
|
656
|
-
function isBufferView(v) {
|
|
657
|
-
if (typeof v !== "object" || v === null)
|
|
658
|
-
return false;
|
|
659
|
-
const o = v;
|
|
660
|
-
return typeof o.buffer === "object" && o.buffer !== null
|
|
661
|
-
&& typeof o.buffer.getValue === "function"
|
|
662
|
-
&& typeof o.elementType === "object" && o.elementType !== null;
|
|
663
|
-
}
|
|
664
|
-
/**
|
|
665
|
-
* Float32 view over a host-side buffer source. Used by the BufferView
|
|
666
|
-
* packer to hand the pool a typed array it can `set()` from.
|
|
667
|
-
*/
|
|
668
|
-
function asFloat32(data) {
|
|
669
|
-
if (data instanceof Float32Array)
|
|
670
|
-
return data;
|
|
671
|
-
if (ArrayBuffer.isView(data)) {
|
|
672
|
-
return new Float32Array(data.buffer, data.byteOffset, data.byteLength / 4);
|
|
673
|
-
}
|
|
674
|
-
return new Float32Array(data); // ArrayBuffer
|
|
675
|
-
}
|
|
676
|
-
// ---------------------------------------------------------------------------
|
|
677
|
-
// Megacall GPU prefix-sum compute shader
|
|
678
|
-
// ---------------------------------------------------------------------------
|
|
679
|
-
const SCAN_TILE_SIZE = 512;
|
|
680
|
-
const SCAN_WG_SIZE = 256;
|
|
681
|
-
const SCAN_MAX_RECORDS = SCAN_TILE_SIZE * SCAN_TILE_SIZE; // numBlocks ≤ TILE_SIZE
|
|
682
|
-
const TILE_K = 64;
|
|
683
|
-
/** drawTable record width: (firstEmit, drawIdx, indexStart, indexCount, instanceCount). */
|
|
684
|
-
const RECORD_U32 = 5;
|
|
685
|
-
const RECORD_BYTES = RECORD_U32 * 4;
|
|
686
|
-
const HEAP_SCAN_WGSL = `
|
|
687
|
-
struct Params {
|
|
688
|
-
numRecords: u32,
|
|
689
|
-
numBlocks: u32,
|
|
690
|
-
_pad0: u32,
|
|
691
|
-
_pad1: u32,
|
|
692
|
-
};
|
|
693
|
-
|
|
694
|
-
struct Record {
|
|
695
|
-
firstEmit: u32,
|
|
696
|
-
drawIdx: u32,
|
|
697
|
-
indexStart: u32,
|
|
698
|
-
indexCount: u32,
|
|
699
|
-
instanceCount: u32,
|
|
700
|
-
};
|
|
701
|
-
|
|
702
|
-
@group(0) @binding(0) var<storage, read_write> drawTable: array<Record>;
|
|
703
|
-
@group(0) @binding(1) var<storage, read_write> blockSums: array<u32>;
|
|
704
|
-
@group(0) @binding(2) var<storage, read_write> blockOffsets: array<u32>;
|
|
705
|
-
@group(0) @binding(3) var<storage, read_write> indirect: array<u32>;
|
|
706
|
-
@group(0) @binding(4) var<uniform> params: Params;
|
|
707
|
-
@group(0) @binding(5) var<storage, read_write> firstDrawInTile: array<u32>;
|
|
708
|
-
|
|
709
|
-
const TILE_SIZE: u32 = 512u;
|
|
710
|
-
const WG_SIZE: u32 = 256u;
|
|
711
|
-
const TILE_K: u32 = 64u;
|
|
712
|
-
|
|
713
|
-
var<workgroup> sdata: array<u32, 512>;
|
|
714
|
-
|
|
715
|
-
fn blellochScan(tid: u32) {
|
|
716
|
-
var offset: u32 = 1u;
|
|
717
|
-
for (var d: u32 = TILE_SIZE >> 1u; d > 0u; d = d >> 1u) {
|
|
718
|
-
workgroupBarrier();
|
|
719
|
-
if (tid < d) {
|
|
720
|
-
let ai = offset * (2u * tid + 1u) - 1u;
|
|
721
|
-
let bi = offset * (2u * tid + 2u) - 1u;
|
|
722
|
-
sdata[bi] = sdata[bi] + sdata[ai];
|
|
723
|
-
}
|
|
724
|
-
offset = offset * 2u;
|
|
725
|
-
}
|
|
726
|
-
if (tid == 0u) { sdata[TILE_SIZE - 1u] = 0u; }
|
|
727
|
-
for (var d: u32 = 1u; d < TILE_SIZE; d = d * 2u) {
|
|
728
|
-
offset = offset >> 1u;
|
|
729
|
-
workgroupBarrier();
|
|
730
|
-
if (tid < d) {
|
|
731
|
-
let ai = offset * (2u * tid + 1u) - 1u;
|
|
732
|
-
let bi = offset * (2u * tid + 2u) - 1u;
|
|
733
|
-
let t = sdata[ai];
|
|
734
|
-
sdata[ai] = sdata[bi];
|
|
735
|
-
sdata[bi] = sdata[bi] + t;
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
workgroupBarrier();
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
@compute @workgroup_size(WG_SIZE)
|
|
742
|
-
fn scanTile(@builtin(local_invocation_id) lid: vec3<u32>, @builtin(workgroup_id) wgid: vec3<u32>) {
|
|
743
|
-
let tid = lid.x;
|
|
744
|
-
let blockOff = wgid.x * TILE_SIZE;
|
|
745
|
-
let n = params.numRecords;
|
|
746
|
-
let i0 = blockOff + tid;
|
|
747
|
-
let i1 = blockOff + tid + WG_SIZE;
|
|
748
|
-
var v0: u32 = 0u;
|
|
749
|
-
var v1: u32 = 0u;
|
|
750
|
-
if (i0 < n) { v0 = drawTable[i0].indexCount * drawTable[i0].instanceCount; }
|
|
751
|
-
if (i1 < n) { v1 = drawTable[i1].indexCount * drawTable[i1].instanceCount; }
|
|
752
|
-
sdata[tid] = v0;
|
|
753
|
-
sdata[tid + WG_SIZE] = v1;
|
|
754
|
-
workgroupBarrier();
|
|
755
|
-
blellochScan(tid);
|
|
756
|
-
if (i0 < n) { drawTable[i0].firstEmit = sdata[tid]; }
|
|
757
|
-
if (i1 < n) { drawTable[i1].firstEmit = sdata[tid + WG_SIZE]; }
|
|
758
|
-
if (tid == WG_SIZE - 1u) {
|
|
759
|
-
blockSums[wgid.x] = sdata[tid + WG_SIZE] + v1;
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
@compute @workgroup_size(WG_SIZE)
|
|
764
|
-
fn scanBlocks(@builtin(local_invocation_id) lid: vec3<u32>) {
|
|
765
|
-
let tid = lid.x;
|
|
766
|
-
let n = params.numBlocks;
|
|
767
|
-
let i0 = tid;
|
|
768
|
-
let i1 = tid + WG_SIZE;
|
|
769
|
-
var v0: u32 = 0u;
|
|
770
|
-
var v1: u32 = 0u;
|
|
771
|
-
if (i0 < n) { v0 = blockSums[i0]; }
|
|
772
|
-
if (i1 < n) { v1 = blockSums[i1]; }
|
|
773
|
-
sdata[tid] = v0;
|
|
774
|
-
sdata[tid + WG_SIZE] = v1;
|
|
775
|
-
workgroupBarrier();
|
|
776
|
-
blellochScan(tid);
|
|
777
|
-
if (i0 < n) { blockOffsets[i0] = sdata[tid]; }
|
|
778
|
-
if (i1 < n) { blockOffsets[i1] = sdata[tid + WG_SIZE]; }
|
|
779
|
-
workgroupBarrier();
|
|
780
|
-
if (tid == 0u) {
|
|
781
|
-
if (n > 0u) {
|
|
782
|
-
let lastIdx = n - 1u;
|
|
783
|
-
let total = blockOffsets[lastIdx] + blockSums[lastIdx];
|
|
784
|
-
indirect[0] = total;
|
|
785
|
-
} else {
|
|
786
|
-
indirect[0] = 0u;
|
|
787
|
-
}
|
|
788
|
-
indirect[1] = 1u;
|
|
789
|
-
indirect[2] = 0u;
|
|
790
|
-
indirect[3] = 0u;
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
@compute @workgroup_size(WG_SIZE)
|
|
795
|
-
fn addOffsets(@builtin(local_invocation_id) lid: vec3<u32>, @builtin(workgroup_id) wgid: vec3<u32>) {
|
|
796
|
-
let tid = lid.x;
|
|
797
|
-
let blockOff = wgid.x * TILE_SIZE;
|
|
798
|
-
let n = params.numRecords;
|
|
799
|
-
let off = blockOffsets[wgid.x];
|
|
800
|
-
let i0 = blockOff + tid;
|
|
801
|
-
let i1 = blockOff + tid + WG_SIZE;
|
|
802
|
-
if (i0 < n) { drawTable[i0].firstEmit = drawTable[i0].firstEmit + off; }
|
|
803
|
-
if (i1 < n) { drawTable[i1].firstEmit = drawTable[i1].firstEmit + off; }
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
@compute @workgroup_size(WG_SIZE)
|
|
807
|
-
fn buildTileIndex(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
808
|
-
let tileIdx = gid.x;
|
|
809
|
-
// totalEmit is computed by scanBlocks into indirect[0]; reading it
|
|
810
|
-
// from indirect avoids a separate uniform/storage round-trip.
|
|
811
|
-
let totalEmit = indirect[0];
|
|
812
|
-
let numTiles = (totalEmit + TILE_K - 1u) / TILE_K;
|
|
813
|
-
if (tileIdx > numTiles) { return; }
|
|
814
|
-
if (params.numRecords == 0u) {
|
|
815
|
-
if (tileIdx == 0u) { firstDrawInTile[0] = 0u; }
|
|
816
|
-
return;
|
|
817
|
-
}
|
|
818
|
-
if (tileIdx == numTiles) {
|
|
819
|
-
// Sentinel for the open upper bound — the LAST VALID SLOT, not
|
|
820
|
-
// numRecords. The render VS uses
|
|
821
|
-
// hi = firstDrawInTile[_tileIdx + 1u]
|
|
822
|
-
// and the binary search treats hi as INCLUSIVE. If the sentinel
|
|
823
|
-
// were numRecords (one past last), the search would drag lo into
|
|
824
|
-
// the OOB slot for emits in the last tile, since drawTable reads
|
|
825
|
-
// past recordCount return 0 (binding size clamping) and 0 ≤ emit
|
|
826
|
-
// is always true. Visible symptom: the LAST few emits in the
|
|
827
|
-
// bucket land on slot=numRecords (drawIdx=0, indexCount=0 → /-by-
|
|
828
|
-
// zero) → degenerate / cross-RO triangle stitched to slot 0.
|
|
829
|
-
firstDrawInTile[tileIdx] = params.numRecords - 1u;
|
|
830
|
-
return;
|
|
831
|
-
}
|
|
832
|
-
let tileStart = tileIdx * TILE_K;
|
|
833
|
-
var lo: u32 = 0u;
|
|
834
|
-
var hi: u32 = params.numRecords - 1u;
|
|
835
|
-
loop {
|
|
836
|
-
if (lo >= hi) { break; }
|
|
837
|
-
let mid = (lo + hi + 1u) >> 1u;
|
|
838
|
-
if (drawTable[mid].firstEmit <= tileStart) { lo = mid; } else { hi = mid - 1u; }
|
|
839
|
-
}
|
|
840
|
-
firstDrawInTile[tileIdx] = lo;
|
|
841
|
-
}
|
|
842
|
-
`;
|
|
52
|
+
import { packerForWgslType, PACKER_MAT4 } from "./heapScene/packers.js";
|
|
53
|
+
import { HEAP_SCAN_WGSL, SCAN_TILE_SIZE, SCAN_WG_SIZE, SCAN_MAX_RECORDS, TILE_K, RECORD_U32, RECORD_BYTES, } from "./heapScene/scanKernel.js";
|
|
54
|
+
import { GrowBuffer, MIN_BUFFER_BYTES, POW2, ALIGN16, } from "./heapScene/growBuffer.js";
|
|
55
|
+
import { UniformPool, IndexPool, DrawHeap, AttributeArena, IndexAllocator, insertSortedFreeBlock, buildArenaState, arenaBytes, writeAttribute, asAval, isBufferView, asFloat32, ALLOC_HEADER_BYTES, ALLOC_HEADER_PAD_TO, ENC_V3F_TIGHT, SEM_POSITIONS, SEM_NORMALS, } from "./heapScene/pools.js";
|
|
56
|
+
import { encodeModeKey } from "./pipelineCache/index.js";
|
|
57
|
+
import { snapshotDescriptor, ModeKeyTracker } from "./derivedModes/modeKeyCpu.js";
|
|
843
58
|
export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
844
59
|
const atlasPool = opts.atlasPool;
|
|
845
60
|
const colorAttachmentName = sig.colorNames[0];
|
|
@@ -872,6 +87,18 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
872
87
|
const drawIdToLocalSlot = [];
|
|
873
88
|
/** Per-draw index aval — for `indexPool.release` on removeDraw. */
|
|
874
89
|
const drawIdToIndexAval = [];
|
|
90
|
+
/**
|
|
91
|
+
* Original spec retained per drawId so we can re-add the RO into a
|
|
92
|
+
* new bucket when its PipelineState modeKey changes (reactive
|
|
93
|
+
* cullmode flip, etc.). Cheap to retain — specs are mostly aval
|
|
94
|
+
* references.
|
|
95
|
+
*/
|
|
96
|
+
const drawIdToSpec = [];
|
|
97
|
+
/** Per-draw ModeKeyTracker — subscribes to PS aval marks, recomputes
|
|
98
|
+
* the modeKey, schedules a rebucket if it changed. */
|
|
99
|
+
const drawIdToModeTracker = [];
|
|
100
|
+
/** drawIds whose PS marked since last update; drained by `update`. */
|
|
101
|
+
const dirtyModeKeyDrawIds = new Set();
|
|
875
102
|
let nextDrawId = 0;
|
|
876
103
|
/**
|
|
877
104
|
* Unwrap an aval to its inner value, or pass through a plain value.
|
|
@@ -1246,12 +473,12 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1246
473
|
label: `heapScene/${bucket.label}/scanBg`,
|
|
1247
474
|
layout: scanBgl,
|
|
1248
475
|
entries: [
|
|
1249
|
-
{ binding: 0, resource: { buffer: bucket.drawTableBuf.buffer } },
|
|
1250
|
-
{ binding: 1, resource: { buffer: bucket.blockSumsBuf.buffer } },
|
|
1251
|
-
{ binding: 2, resource: { buffer: bucket.blockOffsetsBuf.buffer } },
|
|
1252
|
-
{ binding: 3, resource: { buffer: bucket.indirectBuf } },
|
|
1253
|
-
{ binding: 4, resource: { buffer: bucket.paramsBuf } },
|
|
1254
|
-
{ binding: 5, resource: { buffer: bucket.firstDrawInTileBuf.buffer } },
|
|
476
|
+
{ binding: 0, resource: { buffer: bucket.slots[0].drawTableBuf.buffer } },
|
|
477
|
+
{ binding: 1, resource: { buffer: bucket.slots[0].blockSumsBuf.buffer } },
|
|
478
|
+
{ binding: 2, resource: { buffer: bucket.slots[0].blockOffsetsBuf.buffer } },
|
|
479
|
+
{ binding: 3, resource: { buffer: bucket.slots[0].indirectBuf } },
|
|
480
|
+
{ binding: 4, resource: { buffer: bucket.slots[0].paramsBuf } },
|
|
481
|
+
{ binding: 5, resource: { buffer: bucket.slots[0].firstDrawInTileBuf.buffer } },
|
|
1255
482
|
],
|
|
1256
483
|
});
|
|
1257
484
|
}
|
|
@@ -1458,31 +685,6 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1458
685
|
avalIds.set(av, vid);
|
|
1459
686
|
return `c${vid}`;
|
|
1460
687
|
};
|
|
1461
|
-
const psContentIds = new WeakMap();
|
|
1462
|
-
const psIdOf = (ps) => {
|
|
1463
|
-
if (ps === undefined)
|
|
1464
|
-
return "ps#default";
|
|
1465
|
-
const cached = psContentIds.get(ps);
|
|
1466
|
-
if (cached !== undefined)
|
|
1467
|
-
return cached;
|
|
1468
|
-
const r = ps.rasterizer;
|
|
1469
|
-
const parts = [
|
|
1470
|
-
avalIdOf(r.topology),
|
|
1471
|
-
avalIdOf(r.cullMode),
|
|
1472
|
-
avalIdOf(r.frontFace),
|
|
1473
|
-
avalIdOf(r.depthBias),
|
|
1474
|
-
ps.depth !== undefined
|
|
1475
|
-
? `d:${avalIdOf(ps.depth.write)}:${avalIdOf(ps.depth.compare)}:${avalIdOf(ps.depth.clamp)}`
|
|
1476
|
-
: "d:_",
|
|
1477
|
-
ps.stencil !== undefined ? "s:1" : "s:_",
|
|
1478
|
-
avalIdOf(ps.blends),
|
|
1479
|
-
avalIdOf(ps.alphaToCoverage),
|
|
1480
|
-
avalIdOf(ps.blendConstant),
|
|
1481
|
-
];
|
|
1482
|
-
const key = `ps#${parts.join("|")}`;
|
|
1483
|
-
psContentIds.set(ps, key);
|
|
1484
|
-
return key;
|
|
1485
|
-
};
|
|
1486
688
|
function resolvePipelineState(ps) {
|
|
1487
689
|
if (ps === undefined) {
|
|
1488
690
|
return {
|
|
@@ -1521,7 +723,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1521
723
|
{ binding: 3, resource: { buffer: arena.attrs.buffer } }, // heapV4f
|
|
1522
724
|
];
|
|
1523
725
|
{
|
|
1524
|
-
if (bucket.drawTableBuf === undefined) {
|
|
726
|
+
if (bucket.slots[0].drawTableBuf === undefined) {
|
|
1525
727
|
throw new Error("heapScene: megacall bucket without drawTableBuf");
|
|
1526
728
|
}
|
|
1527
729
|
// Bind drawTable with size = recordCount * RECORD_BYTES so the VS
|
|
@@ -1529,9 +731,9 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1529
731
|
// the live record count — keeps stale tail entries out of the
|
|
1530
732
|
// binary search. Minimum one zero-record to satisfy WebGPU non-
|
|
1531
733
|
// zero size constraint when the bucket is empty.
|
|
1532
|
-
const dtBytes = Math.max(RECORD_BYTES, bucket.recordCount * RECORD_BYTES);
|
|
1533
|
-
entries.push({ binding: 4, resource: { buffer: bucket.drawTableBuf.buffer, offset: 0, size: dtBytes } }, { binding: 5, resource: { buffer: arena.indices.buffer } }, { binding: 6, resource: { buffer: bucket.firstDrawInTileBuf.buffer } });
|
|
1534
|
-
bucket.renderBoundRecordCount = bucket.recordCount;
|
|
734
|
+
const dtBytes = Math.max(RECORD_BYTES, bucket.slots[0].recordCount * RECORD_BYTES);
|
|
735
|
+
entries.push({ binding: 4, resource: { buffer: bucket.slots[0].drawTableBuf.buffer, offset: 0, size: dtBytes } }, { binding: 5, resource: { buffer: arena.indices.buffer } }, { binding: 6, resource: { buffer: bucket.slots[0].firstDrawInTileBuf.buffer } });
|
|
736
|
+
bucket.slots[0].renderBoundRecordCount = bucket.slots[0].recordCount;
|
|
1535
737
|
}
|
|
1536
738
|
// Schema-driven texture + sampler entries. v1 user surface still
|
|
1537
739
|
// accepts a single (texture, sampler) pair via HeapTextureSet —
|
|
@@ -1656,8 +858,17 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1656
858
|
throw new Error("heapScene: findOrCreateBucket called before family build");
|
|
1657
859
|
}
|
|
1658
860
|
const fam = familyFor(effect);
|
|
1659
|
-
|
|
1660
|
-
|
|
861
|
+
// ─── PS keying — VALUE based, not identity based ───────────────
|
|
862
|
+
// psIdOf used to hash by aval identity. That meant 20k ROs each
|
|
863
|
+
// constructed with `cval("back")` for cullMode hashed to 20k
|
|
864
|
+
// distinct keys → 20k buckets, all rendering with bitwise-
|
|
865
|
+
// identical state. Now the key is the bitfield-encoded modeKey
|
|
866
|
+
// (the actual value-set in the descriptor), so identical-value
|
|
867
|
+
// cvals collapse to ONE bucket. See docs/derived-modes.md and
|
|
868
|
+
// tests/heap-multi-pipeline-bucket.test.ts.
|
|
869
|
+
const psDescriptor = snapshotDescriptor(pipelineState, sig);
|
|
870
|
+
const psModeKey = encodeModeKey(psDescriptor);
|
|
871
|
+
const bk = `family#${fam.schema.id}|mk#${psModeKey.toString(16)}`;
|
|
1661
872
|
const existing = bucketByKey.get(bk);
|
|
1662
873
|
if (existing !== undefined)
|
|
1663
874
|
return existing;
|
|
@@ -1686,13 +897,21 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1686
897
|
// refs).
|
|
1687
898
|
const drawHeapBuf = new GrowBuffer(device, `heapScene/${bk}/drawHeap`, GPUBufferUsage.STORAGE, Math.max(layout.drawHeaderBytes, 64));
|
|
1688
899
|
const drawHeap = new DrawHeap(drawHeapBuf, layout.drawHeaderBytes);
|
|
900
|
+
const slot0 = {
|
|
901
|
+
pipeline,
|
|
902
|
+
drawTableDirtyMin: Infinity, drawTableDirtyMax: 0,
|
|
903
|
+
recordCount: 0, slotToRecord: [], recordToSlot: [],
|
|
904
|
+
totalEmitEstimate: 0,
|
|
905
|
+
scanDirty: false,
|
|
906
|
+
};
|
|
1689
907
|
const bucket = {
|
|
1690
908
|
// §6 family-merge: family buckets aren't keyed on a specific
|
|
1691
909
|
// texture set — atlas placements are addressed per-RO via
|
|
1692
910
|
// drawHeader fields (`pageRef` + `formatBits` + `origin` +
|
|
1693
911
|
// `size`), and the bucket's atlas-binding ladder is driven by
|
|
1694
912
|
// `atlasPool.pagesFor(format)`. Leave `textures` undefined.
|
|
1695
|
-
label: bk, textures: undefined, layout,
|
|
913
|
+
label: bk, textures: undefined, layout,
|
|
914
|
+
slots: [slot0],
|
|
1696
915
|
bindGroup: null,
|
|
1697
916
|
drawHeap,
|
|
1698
917
|
drawHeaderStaging: new Float32Array(drawHeapBuf.capacity / 4),
|
|
@@ -1701,10 +920,6 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1701
920
|
localEntries: [], localToDrawId: [],
|
|
1702
921
|
localPerDrawAvals: [], localPerDrawRefs: [], localLayoutIds: [],
|
|
1703
922
|
drawSlots: new Set(), dirty: new Set(),
|
|
1704
|
-
drawTableDirtyMin: Infinity, drawTableDirtyMax: 0,
|
|
1705
|
-
recordCount: 0, slotToRecord: [], recordToSlot: [],
|
|
1706
|
-
totalEmitEstimate: 0,
|
|
1707
|
-
scanDirty: false,
|
|
1708
923
|
isAtlasBucket,
|
|
1709
924
|
localAtlasReleases: [],
|
|
1710
925
|
localAtlasTextures: [],
|
|
@@ -1728,30 +943,30 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1728
943
|
label: `heapScene/${bk}/params`, size: 16,
|
|
1729
944
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1730
945
|
});
|
|
1731
|
-
bucket.drawTableBuf = dtBuf;
|
|
1732
|
-
bucket.drawTableShadow = new Uint32Array(dtBuf.capacity / 4);
|
|
1733
|
-
bucket.blockSumsBuf = blockSumsBuf;
|
|
1734
|
-
bucket.blockOffsetsBuf = blockOffsetsBuf;
|
|
1735
|
-
bucket.firstDrawInTileBuf = firstDrawInTileBuf;
|
|
1736
|
-
bucket.indirectBuf = indirectBuf;
|
|
1737
|
-
bucket.paramsBuf = paramsBuf;
|
|
946
|
+
bucket.slots[0].drawTableBuf = dtBuf;
|
|
947
|
+
bucket.slots[0].drawTableShadow = new Uint32Array(dtBuf.capacity / 4);
|
|
948
|
+
bucket.slots[0].blockSumsBuf = blockSumsBuf;
|
|
949
|
+
bucket.slots[0].blockOffsetsBuf = blockOffsetsBuf;
|
|
950
|
+
bucket.slots[0].firstDrawInTileBuf = firstDrawInTileBuf;
|
|
951
|
+
bucket.slots[0].indirectBuf = indirectBuf;
|
|
952
|
+
bucket.slots[0].paramsBuf = paramsBuf;
|
|
1738
953
|
const ensureScanBuffers = () => {
|
|
1739
|
-
const needBlocks = Math.max(1, Math.ceil(bucket.recordCount / SCAN_TILE_SIZE));
|
|
954
|
+
const needBlocks = Math.max(1, Math.ceil(bucket.slots[0].recordCount / SCAN_TILE_SIZE));
|
|
1740
955
|
blockSumsBuf.ensureCapacity(needBlocks * 4);
|
|
1741
956
|
blockOffsetsBuf.ensureCapacity(needBlocks * 4);
|
|
1742
957
|
};
|
|
1743
958
|
const rebuildScanBg = () => {
|
|
1744
|
-
bucket.scanBindGroup = buildScanBindGroup(bucket);
|
|
959
|
+
bucket.slots[0].scanBindGroup = buildScanBindGroup(bucket);
|
|
1745
960
|
};
|
|
1746
961
|
dtBuf.onResize(() => {
|
|
1747
962
|
const grown = new Uint32Array(dtBuf.capacity / 4);
|
|
1748
|
-
grown.set(bucket.drawTableShadow);
|
|
1749
|
-
bucket.drawTableShadow = grown;
|
|
963
|
+
grown.set(bucket.slots[0].drawTableShadow);
|
|
964
|
+
bucket.slots[0].drawTableShadow = grown;
|
|
1750
965
|
ensureScanBuffers();
|
|
1751
966
|
bucket.bindGroup = buildBucketBindGroup(bucket);
|
|
1752
967
|
rebuildScanBg();
|
|
1753
|
-
bucket.drawTableDirtyMin = 0;
|
|
1754
|
-
bucket.drawTableDirtyMax = bucket.recordCount * RECORD_BYTES;
|
|
968
|
+
bucket.slots[0].drawTableDirtyMin = 0;
|
|
969
|
+
bucket.slots[0].drawTableDirtyMax = bucket.slots[0].recordCount * RECORD_BYTES;
|
|
1755
970
|
});
|
|
1756
971
|
blockSumsBuf.onResize(rebuildScanBg);
|
|
1757
972
|
blockOffsetsBuf.onResize(rebuildScanBg);
|
|
@@ -1759,7 +974,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1759
974
|
rebuildScanBg();
|
|
1760
975
|
bucket.bindGroup = buildBucketBindGroup(bucket);
|
|
1761
976
|
});
|
|
1762
|
-
bucket.scanBindGroup = buildScanBindGroup(bucket);
|
|
977
|
+
bucket.slots[0].scanBindGroup = buildScanBindGroup(bucket);
|
|
1763
978
|
}
|
|
1764
979
|
bucket.bindGroup = buildBucketBindGroup(bucket);
|
|
1765
980
|
drawHeap.onResize(() => {
|
|
@@ -2059,8 +1274,8 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2059
1274
|
if (end > bucket.headerDirtyMax)
|
|
2060
1275
|
bucket.headerDirtyMax = end;
|
|
2061
1276
|
{
|
|
2062
|
-
const dtBuf = bucket.drawTableBuf;
|
|
2063
|
-
const recIdx = bucket.recordCount;
|
|
1277
|
+
const dtBuf = bucket.slots[0].drawTableBuf;
|
|
1278
|
+
const recIdx = bucket.slots[0].recordCount;
|
|
2064
1279
|
if (recIdx >= SCAN_MAX_RECORDS) {
|
|
2065
1280
|
throw new Error(`heapScene: bucket exceeds SCAN_MAX_RECORDS (${SCAN_MAX_RECORDS}); ` +
|
|
2066
1281
|
`extend the scan to multi-level if you need more`);
|
|
@@ -2070,30 +1285,41 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2070
1285
|
dtBuf.setUsed(Math.max(dtBuf.usedBytes, byteOff + RECORD_BYTES));
|
|
2071
1286
|
// Grow scan-side buffers if recordCount crosses a tile boundary.
|
|
2072
1287
|
const needBlocks = Math.max(1, Math.ceil((recIdx + 1) / SCAN_TILE_SIZE));
|
|
2073
|
-
bucket.blockSumsBuf.ensureCapacity(needBlocks * 4);
|
|
2074
|
-
bucket.blockOffsetsBuf.ensureCapacity(needBlocks * 4);
|
|
2075
|
-
const shadow = bucket.drawTableShadow;
|
|
1288
|
+
bucket.slots[0].blockSumsBuf.ensureCapacity(needBlocks * 4);
|
|
1289
|
+
bucket.slots[0].blockOffsetsBuf.ensureCapacity(needBlocks * 4);
|
|
1290
|
+
const shadow = bucket.slots[0].drawTableShadow;
|
|
2076
1291
|
// firstEmit is GPU-overwritten by the prefix-sum pass; 0 is fine.
|
|
2077
1292
|
shadow[recIdx * RECORD_U32 + 0] = 0;
|
|
2078
1293
|
shadow[recIdx * RECORD_U32 + 1] = localSlot;
|
|
2079
1294
|
shadow[recIdx * RECORD_U32 + 2] = idxAlloc.firstIndex;
|
|
2080
1295
|
shadow[recIdx * RECORD_U32 + 3] = idxAlloc.count;
|
|
2081
1296
|
shadow[recIdx * RECORD_U32 + 4] = instanceCount;
|
|
2082
|
-
bucket.recordCount = recIdx + 1;
|
|
2083
|
-
bucket.slotToRecord[localSlot] = recIdx;
|
|
2084
|
-
bucket.recordToSlot[recIdx] = localSlot;
|
|
2085
|
-
if (byteOff < bucket.drawTableDirtyMin)
|
|
2086
|
-
bucket.drawTableDirtyMin = byteOff;
|
|
2087
|
-
if (byteOff + RECORD_BYTES > bucket.drawTableDirtyMax)
|
|
2088
|
-
bucket.drawTableDirtyMax = byteOff + RECORD_BYTES;
|
|
2089
|
-
bucket.totalEmitEstimate += idxAlloc.count * instanceCount;
|
|
2090
|
-
const newNumTiles = Math.max(1, Math.ceil(bucket.totalEmitEstimate / TILE_K));
|
|
2091
|
-
bucket.firstDrawInTileBuf.ensureCapacity((newNumTiles + 1) * 4);
|
|
2092
|
-
bucket.scanDirty = true;
|
|
1297
|
+
bucket.slots[0].recordCount = recIdx + 1;
|
|
1298
|
+
bucket.slots[0].slotToRecord[localSlot] = recIdx;
|
|
1299
|
+
bucket.slots[0].recordToSlot[recIdx] = localSlot;
|
|
1300
|
+
if (byteOff < bucket.slots[0].drawTableDirtyMin)
|
|
1301
|
+
bucket.slots[0].drawTableDirtyMin = byteOff;
|
|
1302
|
+
if (byteOff + RECORD_BYTES > bucket.slots[0].drawTableDirtyMax)
|
|
1303
|
+
bucket.slots[0].drawTableDirtyMax = byteOff + RECORD_BYTES;
|
|
1304
|
+
bucket.slots[0].totalEmitEstimate += idxAlloc.count * instanceCount;
|
|
1305
|
+
const newNumTiles = Math.max(1, Math.ceil(bucket.slots[0].totalEmitEstimate / TILE_K));
|
|
1306
|
+
bucket.slots[0].firstDrawInTileBuf.ensureCapacity((newNumTiles + 1) * 4);
|
|
1307
|
+
bucket.slots[0].scanDirty = true;
|
|
2093
1308
|
}
|
|
2094
1309
|
drawIdToBucket[drawId] = bucket;
|
|
2095
1310
|
drawIdToLocalSlot[drawId] = localSlot;
|
|
2096
1311
|
drawIdToIndexAval[drawId] = indicesAval;
|
|
1312
|
+
// ─── Reactive rebucket: track PS-modeKey changes ────────────────
|
|
1313
|
+
// When any mode-axis aval marks (e.g. cullCval.value = 'front'),
|
|
1314
|
+
// schedule this RO for rebucket on the next update(). Today's
|
|
1315
|
+
// bucket key is the modeKey VALUE (Phase 5b), so changing the
|
|
1316
|
+
// value must move the RO to a different bucket. Without this
|
|
1317
|
+
// tracker the cval flip would be silently ignored.
|
|
1318
|
+
drawIdToSpec[drawId] = spec;
|
|
1319
|
+
const tracker = new ModeKeyTracker(spec.pipelineState, sig, () => {
|
|
1320
|
+
dirtyModeKeyDrawIds.add(drawId);
|
|
1321
|
+
});
|
|
1322
|
+
drawIdToModeTracker[drawId] = tracker;
|
|
2097
1323
|
// ─── §7 derived-uniforms registration ────────────────────────────
|
|
2098
1324
|
// A uniform binding on this RO is either a value (aval/constant) or a rule —
|
|
2099
1325
|
// collect the rules (explicit `derivedUniform(...)` values + standard trafo
|
|
@@ -2173,13 +1399,13 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2173
1399
|
const removedCount = removedEntry !== undefined
|
|
2174
1400
|
? removedEntry.indexCount * removedEntry.instanceCount
|
|
2175
1401
|
: 0;
|
|
2176
|
-
bucket.totalEmitEstimate = Math.max(0, bucket.totalEmitEstimate - removedCount);
|
|
1402
|
+
bucket.slots[0].totalEmitEstimate = Math.max(0, bucket.slots[0].totalEmitEstimate - removedCount);
|
|
2177
1403
|
// Swap-pop: move the last record into the freed slot, decrement
|
|
2178
1404
|
// recordCount. firstEmit is GPU-rewritten by the next scan, so
|
|
2179
1405
|
// we only fix (drawIdx, indexStart, indexCount, instanceCount).
|
|
2180
|
-
const recIdx = bucket.slotToRecord[localSlot];
|
|
2181
|
-
const lastRecIdx = bucket.recordCount - 1;
|
|
2182
|
-
const shadow = bucket.drawTableShadow;
|
|
1406
|
+
const recIdx = bucket.slots[0].slotToRecord[localSlot];
|
|
1407
|
+
const lastRecIdx = bucket.slots[0].recordCount - 1;
|
|
1408
|
+
const shadow = bucket.slots[0].drawTableShadow;
|
|
2183
1409
|
if (recIdx !== lastRecIdx) {
|
|
2184
1410
|
const dst = recIdx * RECORD_U32;
|
|
2185
1411
|
const src = lastRecIdx * RECORD_U32;
|
|
@@ -2188,19 +1414,19 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2188
1414
|
shadow[dst + 2] = shadow[src + 2];
|
|
2189
1415
|
shadow[dst + 3] = shadow[src + 3];
|
|
2190
1416
|
shadow[dst + 4] = shadow[src + 4];
|
|
2191
|
-
const movedSlot = bucket.recordToSlot[lastRecIdx];
|
|
2192
|
-
bucket.slotToRecord[movedSlot] = recIdx;
|
|
2193
|
-
bucket.recordToSlot[recIdx] = movedSlot;
|
|
1417
|
+
const movedSlot = bucket.slots[0].recordToSlot[lastRecIdx];
|
|
1418
|
+
bucket.slots[0].slotToRecord[movedSlot] = recIdx;
|
|
1419
|
+
bucket.slots[0].recordToSlot[recIdx] = movedSlot;
|
|
2194
1420
|
const byteOff = recIdx * RECORD_BYTES;
|
|
2195
|
-
if (byteOff < bucket.drawTableDirtyMin)
|
|
2196
|
-
bucket.drawTableDirtyMin = byteOff;
|
|
2197
|
-
if (byteOff + RECORD_BYTES > bucket.drawTableDirtyMax)
|
|
2198
|
-
bucket.drawTableDirtyMax = byteOff + RECORD_BYTES;
|
|
1421
|
+
if (byteOff < bucket.slots[0].drawTableDirtyMin)
|
|
1422
|
+
bucket.slots[0].drawTableDirtyMin = byteOff;
|
|
1423
|
+
if (byteOff + RECORD_BYTES > bucket.slots[0].drawTableDirtyMax)
|
|
1424
|
+
bucket.slots[0].drawTableDirtyMax = byteOff + RECORD_BYTES;
|
|
2199
1425
|
}
|
|
2200
|
-
bucket.slotToRecord[localSlot] = -1;
|
|
2201
|
-
bucket.recordToSlot[lastRecIdx] = -1;
|
|
2202
|
-
bucket.recordCount = lastRecIdx;
|
|
2203
|
-
bucket.scanDirty = true;
|
|
1426
|
+
bucket.slots[0].slotToRecord[localSlot] = -1;
|
|
1427
|
+
bucket.slots[0].recordToSlot[lastRecIdx] = -1;
|
|
1428
|
+
bucket.slots[0].recordCount = lastRecIdx;
|
|
1429
|
+
bucket.slots[0].scanDirty = true;
|
|
2204
1430
|
}
|
|
2205
1431
|
// Release pool entries — refcount drops; if zero, allocation freed.
|
|
2206
1432
|
const avals = bucket.localPerDrawAvals[localSlot];
|
|
@@ -2254,8 +1480,17 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2254
1480
|
drawIdToBucket[drawId] = undefined;
|
|
2255
1481
|
drawIdToLocalSlot[drawId] = undefined;
|
|
2256
1482
|
drawIdToIndexAval[drawId] = undefined;
|
|
1483
|
+
drawIdToSpec[drawId] = undefined;
|
|
1484
|
+
const oldTracker = drawIdToModeTracker[drawId];
|
|
1485
|
+
if (oldTracker !== undefined)
|
|
1486
|
+
oldTracker.dispose();
|
|
1487
|
+
drawIdToModeTracker[drawId] = undefined;
|
|
1488
|
+
dirtyModeKeyDrawIds.delete(drawId);
|
|
2257
1489
|
stats.totalDraws--;
|
|
2258
1490
|
stats.geometryBytes = arenaBytes(arena);
|
|
1491
|
+
// GC the bucket if it just emptied (common after reactive
|
|
1492
|
+
// PS rebucket leaves the source bucket with zero live ROs).
|
|
1493
|
+
gcBucket(bucket);
|
|
2259
1494
|
}
|
|
2260
1495
|
// ─── Aset reader (pull-driven on each frame) ──────────────────────
|
|
2261
1496
|
let asetReader;
|
|
@@ -2345,6 +1580,38 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2345
1580
|
let totalDirtyBytes = 0;
|
|
2346
1581
|
sceneObj.evaluateAlways(token, (tok) => {
|
|
2347
1582
|
drainAsetWith(tok);
|
|
1583
|
+
// 0. Reactive rebucket: any RO whose PipelineState modeKey
|
|
1584
|
+
// changed since last frame moves to its new bucket.
|
|
1585
|
+
// `dirtyModeKeyDrawIds` was populated by ModeKeyTracker's
|
|
1586
|
+
// onDirty callbacks (one per leaf aval per RO). Calling
|
|
1587
|
+
// `recompute()` here re-snapshots the descriptor and tells
|
|
1588
|
+
// us whether the modeKey actually changed (vs the aval just
|
|
1589
|
+
// being marked-but-equal). If the key changed, we remove +
|
|
1590
|
+
// re-add the RO via the retained spec; addDraw routes it to
|
|
1591
|
+
// the correct (new) bucket via the value-keyed lookup.
|
|
1592
|
+
if (dirtyModeKeyDrawIds.size > 0) {
|
|
1593
|
+
const dirty = [...dirtyModeKeyDrawIds];
|
|
1594
|
+
dirtyModeKeyDrawIds.clear();
|
|
1595
|
+
for (const drawId of dirty) {
|
|
1596
|
+
const tracker = drawIdToModeTracker[drawId];
|
|
1597
|
+
if (tracker === undefined)
|
|
1598
|
+
continue; // RO removed since mark
|
|
1599
|
+
const oldKey = tracker.modeKey;
|
|
1600
|
+
if (!tracker.recompute())
|
|
1601
|
+
continue; // value unchanged
|
|
1602
|
+
if (tracker.modeKey === oldKey)
|
|
1603
|
+
continue; // belt + braces
|
|
1604
|
+
const spec = drawIdToSpec[drawId];
|
|
1605
|
+
if (spec === undefined)
|
|
1606
|
+
continue;
|
|
1607
|
+
removeDraw(drawId);
|
|
1608
|
+
const newId = addDrawImpl(spec, tok);
|
|
1609
|
+
// The drawId returned by addDraw is a fresh slot; we don't
|
|
1610
|
+
// attempt to preserve the caller's id mapping in v1. v2
|
|
1611
|
+
// could thread the original id through if needed.
|
|
1612
|
+
void newId;
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
2348
1615
|
// 1. Pool: re-pack any aval whose value changed since last frame.
|
|
2349
1616
|
// One writeBuffer per dirty aval, regardless of how many draws
|
|
2350
1617
|
// reference it — sharing pays off here.
|
|
@@ -2441,11 +1708,11 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2441
1708
|
bucket.headerDirtyMin = Infinity;
|
|
2442
1709
|
bucket.headerDirtyMax = 0;
|
|
2443
1710
|
}
|
|
2444
|
-
if (bucket.drawTableDirtyMax > bucket.drawTableDirtyMin) {
|
|
2445
|
-
const shadow = bucket.drawTableShadow;
|
|
2446
|
-
device.queue.writeBuffer(bucket.drawTableBuf.buffer, bucket.drawTableDirtyMin, shadow.buffer, shadow.byteOffset + bucket.drawTableDirtyMin, bucket.drawTableDirtyMax - bucket.drawTableDirtyMin);
|
|
2447
|
-
bucket.drawTableDirtyMin = Infinity;
|
|
2448
|
-
bucket.drawTableDirtyMax = 0;
|
|
1711
|
+
if (bucket.slots[0].drawTableDirtyMax > bucket.slots[0].drawTableDirtyMin) {
|
|
1712
|
+
const shadow = bucket.slots[0].drawTableShadow;
|
|
1713
|
+
device.queue.writeBuffer(bucket.slots[0].drawTableBuf.buffer, bucket.slots[0].drawTableDirtyMin, shadow.buffer, shadow.byteOffset + bucket.slots[0].drawTableDirtyMin, bucket.slots[0].drawTableDirtyMax - bucket.slots[0].drawTableDirtyMin);
|
|
1714
|
+
bucket.slots[0].drawTableDirtyMin = Infinity;
|
|
1715
|
+
bucket.slots[0].drawTableDirtyMax = 0;
|
|
2449
1716
|
}
|
|
2450
1717
|
}
|
|
2451
1718
|
}
|
|
@@ -2461,9 +1728,9 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2461
1728
|
pass.setBindGroup(0, b.bindGroup);
|
|
2462
1729
|
curBg = b.bindGroup;
|
|
2463
1730
|
}
|
|
2464
|
-
pass.setPipeline(b.pipeline);
|
|
2465
|
-
if (b.recordCount > 0)
|
|
2466
|
-
pass.drawIndirect(b.indirectBuf, 0);
|
|
1731
|
+
pass.setPipeline(b.slots[0].pipeline);
|
|
1732
|
+
if (b.slots[0].recordCount > 0)
|
|
1733
|
+
pass.drawIndirect(b.slots[0].indirectBuf, 0);
|
|
2467
1734
|
}
|
|
2468
1735
|
}
|
|
2469
1736
|
/**
|
|
@@ -2505,7 +1772,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2505
1772
|
}
|
|
2506
1773
|
let anyDirty = false;
|
|
2507
1774
|
for (const b of buckets) {
|
|
2508
|
-
if (b.scanDirty) {
|
|
1775
|
+
if (b.slots[0].scanDirty) {
|
|
2509
1776
|
anyDirty = true;
|
|
2510
1777
|
break;
|
|
2511
1778
|
}
|
|
@@ -2514,17 +1781,17 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2514
1781
|
return;
|
|
2515
1782
|
const pass = enc.beginComputePass({ label: "heapScene/scan" });
|
|
2516
1783
|
for (const b of buckets) {
|
|
2517
|
-
if (!b.scanDirty)
|
|
1784
|
+
if (!b.slots[0].scanDirty)
|
|
2518
1785
|
continue;
|
|
2519
1786
|
// Rebuild render bind group if recordCount changed — its
|
|
2520
1787
|
// drawTable binding is sized to recordCount * 16.
|
|
2521
|
-
if (b.renderBoundRecordCount !== b.recordCount) {
|
|
1788
|
+
if (b.slots[0].renderBoundRecordCount !== b.slots[0].recordCount) {
|
|
2522
1789
|
b.bindGroup = buildBucketBindGroup(b);
|
|
2523
1790
|
}
|
|
2524
|
-
const numRecords = b.recordCount;
|
|
1791
|
+
const numRecords = b.slots[0].recordCount;
|
|
2525
1792
|
const numBlocks = Math.max(1, Math.ceil(numRecords / SCAN_TILE_SIZE));
|
|
2526
|
-
device.queue.writeBuffer(b.paramsBuf, 0, new Uint32Array([numRecords, numBlocks, 0, 0]));
|
|
2527
|
-
pass.setBindGroup(0, b.scanBindGroup);
|
|
1793
|
+
device.queue.writeBuffer(b.slots[0].paramsBuf, 0, new Uint32Array([numRecords, numBlocks, 0, 0]));
|
|
1794
|
+
pass.setBindGroup(0, b.slots[0].scanBindGroup);
|
|
2528
1795
|
pass.setPipeline(scanPipeTile);
|
|
2529
1796
|
pass.dispatchWorkgroups(numBlocks, 1, 1);
|
|
2530
1797
|
pass.setPipeline(scanPipeBlocks);
|
|
@@ -2535,11 +1802,11 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2535
1802
|
// must cover the worst-case totalEmit. Each WG handles WG_SIZE
|
|
2536
1803
|
// tiles; +1 for the sentinel slot.
|
|
2537
1804
|
const SCAN_WG_SIZE = 256;
|
|
2538
|
-
const numTilesCap = Math.max(1, Math.ceil(b.totalEmitEstimate / TILE_K));
|
|
1805
|
+
const numTilesCap = Math.max(1, Math.ceil(b.slots[0].totalEmitEstimate / TILE_K));
|
|
2539
1806
|
const tileWgs = Math.max(1, Math.ceil((numTilesCap + 1) / SCAN_WG_SIZE));
|
|
2540
1807
|
pass.setPipeline(scanPipeBuildTileIndex);
|
|
2541
1808
|
pass.dispatchWorkgroups(tileWgs, 1, 1);
|
|
2542
|
-
b.scanDirty = false;
|
|
1809
|
+
b.slots[0].scanDirty = false;
|
|
2543
1810
|
}
|
|
2544
1811
|
pass.end();
|
|
2545
1812
|
}
|
|
@@ -2572,29 +1839,53 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2572
1839
|
pass.end();
|
|
2573
1840
|
device.queue.submit([enc.finish()]);
|
|
2574
1841
|
}
|
|
1842
|
+
function destroyBucketResources(b) {
|
|
1843
|
+
b.drawHeap.destroy();
|
|
1844
|
+
for (const slot of b.slots) {
|
|
1845
|
+
slot.drawTableBuf?.destroy();
|
|
1846
|
+
slot.blockSumsBuf?.destroy();
|
|
1847
|
+
slot.blockOffsetsBuf?.destroy();
|
|
1848
|
+
slot.firstDrawInTileBuf?.destroy();
|
|
1849
|
+
slot.indirectBuf?.destroy();
|
|
1850
|
+
slot.paramsBuf?.destroy();
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
/**
|
|
1854
|
+
* Remove an empty bucket from the scene's bucket list + key index +
|
|
1855
|
+
* destroy its GPU resources. Called from removeDraw when an RO's
|
|
1856
|
+
* removal leaves the bucket with zero live slots — common after
|
|
1857
|
+
* reactive PS rebucket leaves the source bucket empty.
|
|
1858
|
+
*/
|
|
1859
|
+
function gcBucket(b) {
|
|
1860
|
+
if (b.drawSlots.size !== 0)
|
|
1861
|
+
return;
|
|
1862
|
+
const idx = buckets.indexOf(b);
|
|
1863
|
+
if (idx >= 0)
|
|
1864
|
+
buckets.splice(idx, 1);
|
|
1865
|
+
for (const [k, v] of bucketByKey) {
|
|
1866
|
+
if (v === b) {
|
|
1867
|
+
bucketByKey.delete(k);
|
|
1868
|
+
break;
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
destroyBucketResources(b);
|
|
1872
|
+
}
|
|
2575
1873
|
function dispose() {
|
|
2576
1874
|
arena.attrs.destroy();
|
|
2577
1875
|
arena.indices.destroy();
|
|
2578
|
-
for (const b of buckets)
|
|
2579
|
-
b
|
|
2580
|
-
b.drawTableBuf?.destroy();
|
|
2581
|
-
b.blockSumsBuf?.destroy();
|
|
2582
|
-
b.blockOffsetsBuf?.destroy();
|
|
2583
|
-
b.firstDrawInTileBuf?.destroy();
|
|
2584
|
-
b.indirectBuf?.destroy();
|
|
2585
|
-
b.paramsBuf?.destroy();
|
|
2586
|
-
}
|
|
1876
|
+
for (const b of buckets)
|
|
1877
|
+
destroyBucketResources(b);
|
|
2587
1878
|
}
|
|
2588
1879
|
// Test-only escape hatch for inspecting megacall bucket state. Not
|
|
2589
1880
|
// part of the public API surface — keep cast at use-site.
|
|
2590
1881
|
const _debug = {
|
|
2591
1882
|
bucketsForTest() {
|
|
2592
1883
|
return buckets.map(b => ({
|
|
2593
|
-
indirectBuf: b.indirectBuf,
|
|
2594
|
-
drawTableBuf: b.drawTableBuf?.buffer,
|
|
2595
|
-
firstDrawInTileBuf: b.firstDrawInTileBuf?.buffer,
|
|
2596
|
-
totalEmitEstimate: b.totalEmitEstimate,
|
|
2597
|
-
recordCount: b.recordCount,
|
|
1884
|
+
indirectBuf: b.slots[0].indirectBuf,
|
|
1885
|
+
drawTableBuf: b.slots[0].drawTableBuf?.buffer,
|
|
1886
|
+
firstDrawInTileBuf: b.slots[0].firstDrawInTileBuf?.buffer,
|
|
1887
|
+
totalEmitEstimate: b.slots[0].totalEmitEstimate,
|
|
1888
|
+
recordCount: b.slots[0].recordCount,
|
|
2598
1889
|
layout: b.layout,
|
|
2599
1890
|
}));
|
|
2600
1891
|
},
|
|
@@ -2639,19 +1930,19 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2639
1930
|
const TILE_K_LOCAL = 64;
|
|
2640
1931
|
const dcs = [];
|
|
2641
1932
|
for (const b of buckets) {
|
|
2642
|
-
const dhSize = Math.min(b.drawHeap.buffer.size, b.recordCount * b.layout.drawHeaderBytes);
|
|
1933
|
+
const dhSize = Math.min(b.drawHeap.buffer.size, b.slots[0].recordCount * b.layout.drawHeaderBytes);
|
|
2643
1934
|
if (dhSize === 0)
|
|
2644
1935
|
continue;
|
|
2645
1936
|
const dh = stage(b.drawHeap.buffer, dhSize);
|
|
2646
|
-
const dt = b.drawTableBuf !== undefined && b.recordCount > 0
|
|
2647
|
-
? stage(b.drawTableBuf.buffer, b.recordCount * RECORD_BYTES)
|
|
1937
|
+
const dt = b.slots[0].drawTableBuf !== undefined && b.slots[0].recordCount > 0
|
|
1938
|
+
? stage(b.slots[0].drawTableBuf.buffer, b.slots[0].recordCount * RECORD_BYTES)
|
|
2648
1939
|
: undefined;
|
|
2649
|
-
const totalEmit = b.totalEmitEstimate;
|
|
1940
|
+
const totalEmit = b.slots[0].totalEmitEstimate;
|
|
2650
1941
|
const numTiles = totalEmit > 0 ? Math.ceil(totalEmit / TILE_K_LOCAL) : 0;
|
|
2651
|
-
const fdt = b.firstDrawInTileBuf !== undefined && numTiles > 0
|
|
2652
|
-
? stage(b.firstDrawInTileBuf.buffer, Math.min(b.firstDrawInTileBuf.buffer.size, (numTiles + 1) * 4))
|
|
1942
|
+
const fdt = b.slots[0].firstDrawInTileBuf !== undefined && numTiles > 0
|
|
1943
|
+
? stage(b.slots[0].firstDrawInTileBuf.buffer, Math.min(b.slots[0].firstDrawInTileBuf.buffer.size, (numTiles + 1) * 4))
|
|
2653
1944
|
: undefined;
|
|
2654
|
-
const ind = b.indirectBuf !== undefined ? stage(b.indirectBuf, 16) : undefined;
|
|
1945
|
+
const ind = b.slots[0].indirectBuf !== undefined ? stage(b.slots[0].indirectBuf, 16) : undefined;
|
|
2655
1946
|
dcs.push({
|
|
2656
1947
|
bucket: b, drawHeap: dh, numTiles,
|
|
2657
1948
|
...(dt !== undefined ? { drawTable: dt } : {}),
|
|
@@ -2684,7 +1975,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2684
1975
|
for (const dc of dcs) {
|
|
2685
1976
|
const u32 = new Uint32Array(dc.drawHeap.getMappedRange());
|
|
2686
1977
|
const stride = dc.bucket.layout.drawHeaderBytes;
|
|
2687
|
-
const recordCount = dc.bucket.recordCount;
|
|
1978
|
+
const recordCount = dc.bucket.slots[0].recordCount;
|
|
2688
1979
|
for (let slot = 0; slot < recordCount; slot++) {
|
|
2689
1980
|
const slotOff = slot * stride;
|
|
2690
1981
|
for (const f of dc.bucket.layout.drawHeaderFields) {
|
|
@@ -2836,7 +2127,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2836
2127
|
// ── 6. indirect[0] (totalEmit) must match CPU prefix sum.
|
|
2837
2128
|
if (dc.indirect !== undefined) {
|
|
2838
2129
|
const ind = new Uint32Array(dc.indirect.getMappedRange());
|
|
2839
|
-
const expectedTotal = dc.bucket.totalEmitEstimate;
|
|
2130
|
+
const expectedTotal = dc.bucket.slots[0].totalEmitEstimate;
|
|
2840
2131
|
const got = ind[0];
|
|
2841
2132
|
if (got !== expectedTotal) {
|
|
2842
2133
|
push(`bucket#${bucketIdx} indirect[0]=${got} ≠ expected totalEmit=${expectedTotal}`);
|
|
@@ -2926,11 +2217,11 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2926
2217
|
const indicesCopy = indicesSize > 0 ? stage(arena.indices.buffer, indicesSize) : undefined;
|
|
2927
2218
|
const dcs = [];
|
|
2928
2219
|
for (const b of buckets) {
|
|
2929
|
-
if (b.recordCount === 0 || b.totalEmitEstimate === 0)
|
|
2220
|
+
if (b.slots[0].recordCount === 0 || b.slots[0].totalEmitEstimate === 0)
|
|
2930
2221
|
continue;
|
|
2931
|
-
const dh = stage(b.drawHeap.buffer, Math.min(b.drawHeap.buffer.size, b.recordCount * b.layout.drawHeaderBytes));
|
|
2932
|
-
const dt = stage(b.drawTableBuf.buffer, b.recordCount * RECORD_BYTES);
|
|
2933
|
-
dcs.push({ bucket: b, drawHeap: dh, drawTable: dt, firstEmit: [], totalEmit: b.totalEmitEstimate });
|
|
2222
|
+
const dh = stage(b.drawHeap.buffer, Math.min(b.drawHeap.buffer.size, b.slots[0].recordCount * b.layout.drawHeaderBytes));
|
|
2223
|
+
const dt = stage(b.slots[0].drawTableBuf.buffer, b.slots[0].recordCount * RECORD_BYTES);
|
|
2224
|
+
dcs.push({ bucket: b, drawHeap: dh, drawTable: dt, firstEmit: [], totalEmit: b.slots[0].totalEmitEstimate });
|
|
2934
2225
|
}
|
|
2935
2226
|
device.queue.submit([enc.finish()]);
|
|
2936
2227
|
await arenaCopy.mapAsync(GPUMapMode.READ);
|
|
@@ -2954,7 +2245,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2954
2245
|
const dt = new Uint32Array(dc.drawTable.getMappedRange());
|
|
2955
2246
|
bucketDt.push(dt);
|
|
2956
2247
|
bucketHdr.push(new Uint32Array(dc.drawHeap.getMappedRange()));
|
|
2957
|
-
for (let r = 0; r < dc.bucket.recordCount; r++) {
|
|
2248
|
+
for (let r = 0; r < dc.bucket.slots[0].recordCount; r++) {
|
|
2958
2249
|
dc.firstEmit.push(dt[r * RECORD_U32 + 0]);
|
|
2959
2250
|
}
|
|
2960
2251
|
bucketCumEmit.push(totalEmitGlobal);
|
|
@@ -3003,7 +2294,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3003
2294
|
}
|
|
3004
2295
|
emitsChecked++;
|
|
3005
2296
|
// Binary search for slot.
|
|
3006
|
-
const recCount = dc.bucket.recordCount;
|
|
2297
|
+
const recCount = dc.bucket.slots[0].recordCount;
|
|
3007
2298
|
let lo = 0, hi = recCount - 1;
|
|
3008
2299
|
while (lo < hi) {
|
|
3009
2300
|
const mid = (lo + hi + 1) >>> 1;
|
|
@@ -3199,12 +2490,12 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3199
2490
|
? new Uint32Array(indicesCopy.getMappedRange())
|
|
3200
2491
|
: new Uint32Array(0);
|
|
3201
2492
|
for (const target of buckets) {
|
|
3202
|
-
if (target.recordCount === 0 || target.totalEmitEstimate === 0) {
|
|
2493
|
+
if (target.slots[0].recordCount === 0 || target.slots[0].totalEmitEstimate === 0) {
|
|
3203
2494
|
bucketIdx++;
|
|
3204
2495
|
continue;
|
|
3205
2496
|
}
|
|
3206
|
-
const recordCount = target.recordCount;
|
|
3207
|
-
const totalEmit = target.totalEmitEstimate;
|
|
2497
|
+
const recordCount = target.slots[0].recordCount;
|
|
2498
|
+
const totalEmit = target.slots[0].totalEmitEstimate;
|
|
3208
2499
|
const totalTris = Math.floor(totalEmit / 3);
|
|
3209
2500
|
const remainder = totalEmit % 3;
|
|
3210
2501
|
if (remainder !== 0) {
|
|
@@ -3221,7 +2512,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3221
2512
|
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
|
|
3222
2513
|
});
|
|
3223
2514
|
const enc0 = device.createCommandEncoder({ label: "checkTriangleCoherence.dt" });
|
|
3224
|
-
enc0.copyBufferToBuffer(target.drawTableBuf.buffer, 0, dtCopy, 0, recordCount * RECORD_BYTES);
|
|
2515
|
+
enc0.copyBufferToBuffer(target.slots[0].drawTableBuf.buffer, 0, dtCopy, 0, recordCount * RECORD_BYTES);
|
|
3225
2516
|
enc0.copyBufferToBuffer(target.drawHeap.buffer, 0, dhCopy, 0, recordCount * target.layout.drawHeaderBytes);
|
|
3226
2517
|
device.queue.submit([enc0.finish()]);
|
|
3227
2518
|
await dtCopy.mapAsync(GPUMapMode.READ);
|
|
@@ -3270,7 +2561,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3270
2561
|
});
|
|
3271
2562
|
{
|
|
3272
2563
|
const enc1 = device.createCommandEncoder();
|
|
3273
|
-
enc1.copyBufferToBuffer(target.firstDrawInTileBuf.buffer, 0, fdtCopy, 0, fdtSize);
|
|
2564
|
+
enc1.copyBufferToBuffer(target.slots[0].firstDrawInTileBuf.buffer, 0, fdtCopy, 0, fdtSize);
|
|
3274
2565
|
device.queue.submit([enc1.finish()]);
|
|
3275
2566
|
await fdtCopy.mapAsync(GPUMapMode.READ);
|
|
3276
2567
|
}
|
|
@@ -3425,12 +2716,12 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3425
2716
|
// own bytes).
|
|
3426
2717
|
bucketIdx = 0;
|
|
3427
2718
|
for (const target of buckets) {
|
|
3428
|
-
if (target.recordCount === 0) {
|
|
2719
|
+
if (target.slots[0].recordCount === 0) {
|
|
3429
2720
|
bucketIdx++;
|
|
3430
2721
|
continue;
|
|
3431
2722
|
}
|
|
3432
2723
|
// Re-stage drawHeap for this bucket.
|
|
3433
|
-
const dhBytes = target.recordCount * target.layout.drawHeaderBytes;
|
|
2724
|
+
const dhBytes = target.slots[0].recordCount * target.layout.drawHeaderBytes;
|
|
3434
2725
|
const dhCopy = device.createBuffer({
|
|
3435
2726
|
size: dhBytes, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
|
|
3436
2727
|
});
|
|
@@ -3440,7 +2731,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3440
2731
|
await dhCopy.mapAsync(GPUMapMode.READ);
|
|
3441
2732
|
const dhU32 = new Uint32Array(dhCopy.getMappedRange());
|
|
3442
2733
|
const stride2 = target.layout.drawHeaderBytes;
|
|
3443
|
-
for (let slot = 0; slot < target.recordCount; slot++) {
|
|
2734
|
+
for (let slot = 0; slot < target.slots[0].recordCount; slot++) {
|
|
3444
2735
|
for (const f of target.layout.drawHeaderFields) {
|
|
3445
2736
|
if (f.kind !== "uniform-ref" && f.kind !== "attribute-ref")
|
|
3446
2737
|
continue;
|
|
@@ -3500,11 +2791,11 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3500
2791
|
let gpuMismatches = 0;
|
|
3501
2792
|
const push = (s) => { if (issues.length < 30)
|
|
3502
2793
|
issues.push(s); };
|
|
3503
|
-
const target = buckets.find(b => b.recordCount > 0 && b.totalEmitEstimate > 0);
|
|
2794
|
+
const target = buckets.find(b => b.slots[0].recordCount > 0 && b.slots[0].totalEmitEstimate > 0);
|
|
3504
2795
|
if (target === undefined)
|
|
3505
2796
|
return { emitsChecked: 0, gpuMismatches: 0, issues };
|
|
3506
|
-
const recordCount = target.recordCount;
|
|
3507
|
-
const totalEmit = target.totalEmitEstimate;
|
|
2797
|
+
const recordCount = target.slots[0].recordCount;
|
|
2798
|
+
const totalEmit = target.slots[0].totalEmitEstimate;
|
|
3508
2799
|
const sampleCount = Math.min(samples, totalEmit);
|
|
3509
2800
|
const stride = Math.max(1, Math.floor(totalEmit / sampleCount));
|
|
3510
2801
|
// CPU's firstEmit is GPU-computed — must read it back from the
|
|
@@ -3517,7 +2808,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3517
2808
|
});
|
|
3518
2809
|
{
|
|
3519
2810
|
const enc0 = device.createCommandEncoder({ label: "probeBinarySearch.dtCopy" });
|
|
3520
|
-
enc0.copyBufferToBuffer(target.drawTableBuf.buffer, 0, dtCopy, 0, recordCount * RECORD_BYTES);
|
|
2811
|
+
enc0.copyBufferToBuffer(target.slots[0].drawTableBuf.buffer, 0, dtCopy, 0, recordCount * RECORD_BYTES);
|
|
3521
2812
|
device.queue.submit([enc0.finish()]);
|
|
3522
2813
|
await dtCopy.mapAsync(GPUMapMode.READ);
|
|
3523
2814
|
}
|
|
@@ -3621,8 +2912,8 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3621
2912
|
const bg = device.createBindGroup({
|
|
3622
2913
|
layout: bgl,
|
|
3623
2914
|
entries: [
|
|
3624
|
-
{ binding: 0, resource: { buffer: target.drawTableBuf.buffer } },
|
|
3625
|
-
{ binding: 1, resource: { buffer: target.firstDrawInTileBuf.buffer } },
|
|
2915
|
+
{ binding: 0, resource: { buffer: target.slots[0].drawTableBuf.buffer } },
|
|
2916
|
+
{ binding: 1, resource: { buffer: target.slots[0].firstDrawInTileBuf.buffer } },
|
|
3626
2917
|
{ binding: 2, resource: { buffer: sampleBuf } },
|
|
3627
2918
|
{ binding: 3, resource: { buffer: arena.indices.buffer } },
|
|
3628
2919
|
{ binding: 4, resource: { buffer: outBuf } },
|