@aardworx/wombat.rendering 0.21.12 → 0.21.14
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/heapAdapter.d.ts.map +1 -1
- package/dist/runtime/heapAdapter.js +15 -5
- package/dist/runtime/heapAdapter.js.map +1 -1
- package/dist/runtime/heapScene/pools.d.ts +30 -6
- package/dist/runtime/heapScene/pools.d.ts.map +1 -1
- package/dist/runtime/heapScene/pools.js +38 -1
- package/dist/runtime/heapScene/pools.js.map +1 -1
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +193 -3
- package/dist/runtime/heapScene.js.map +1 -1
- package/package.json +1 -1
- package/src/runtime/heapAdapter.ts +16 -5
- package/src/runtime/heapScene/pools.ts +70 -5
- package/src/runtime/heapScene.ts +186 -2
|
@@ -262,6 +262,8 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
262
262
|
const activeAvalToDrawIds = new Map();
|
|
263
263
|
const activeDirty = new Set();
|
|
264
264
|
function subscribeActive(av, drawId) {
|
|
265
|
+
if (av.isConstant)
|
|
266
|
+
return; // fixed visibility — nothing will ever tick
|
|
265
267
|
let set = activeAvalToDrawIds.get(av);
|
|
266
268
|
if (set === undefined) {
|
|
267
269
|
set = new Set();
|
|
@@ -337,7 +339,92 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
337
339
|
slot.scanDirty = true;
|
|
338
340
|
}
|
|
339
341
|
}
|
|
342
|
+
// ─── HeapDrawSpec.instanceCount as an aval ────────────────────────
|
|
343
|
+
// The geometry-morphing / collection-count path (epoch switches,
|
|
344
|
+
// point add/remove): a count tick is ONE drawTable write (record
|
|
345
|
+
// word 4) + emit re-estimate + re-scan — no add/remove churn, no
|
|
346
|
+
// arena activity. Instance ATTRIBUTE payloads ride the sized-repack
|
|
347
|
+
// path independently; per-instance UNIFORM arrays (packed per
|
|
348
|
+
// count) are rejected with an adaptive count at addDraw.
|
|
349
|
+
const drawIdToCountAval = new Map();
|
|
350
|
+
const drawIdToCountCallback = new Map();
|
|
351
|
+
const countDirty = new Set();
|
|
352
|
+
function subscribeCount(av, drawId) {
|
|
353
|
+
if (av.isConstant)
|
|
354
|
+
return; // fixed count — nothing will ever tick
|
|
355
|
+
drawIdToCountAval.set(drawId, av);
|
|
356
|
+
drawIdToCountCallback.set(drawId, addMarkingCallback(av, () => { countDirty.add(drawId); }));
|
|
357
|
+
}
|
|
358
|
+
function unsubscribeCount(drawId) {
|
|
359
|
+
const cb = drawIdToCountCallback.get(drawId);
|
|
360
|
+
if (cb === undefined)
|
|
361
|
+
return;
|
|
362
|
+
cb.dispose();
|
|
363
|
+
drawIdToCountCallback.delete(drawId);
|
|
364
|
+
drawIdToCountAval.delete(drawId);
|
|
365
|
+
countDirty.delete(drawId);
|
|
366
|
+
}
|
|
367
|
+
/** Update the drawTable record's instanceCount in place — the
|
|
368
|
+
* word-4 sibling of `setEffectiveIndexCount`. */
|
|
369
|
+
function setEffectiveInstanceCount(drawId, newCount) {
|
|
370
|
+
const bucket = drawIdToBucket[drawId];
|
|
371
|
+
if (bucket === undefined)
|
|
372
|
+
return;
|
|
373
|
+
if (bucket.gpuRouted) {
|
|
374
|
+
const partition = bucket.partitionScene;
|
|
375
|
+
const recIdx = bucket.drawIdToRecord.get(drawId);
|
|
376
|
+
if (recIdx === undefined)
|
|
377
|
+
return;
|
|
378
|
+
const ru32 = partition.recordU32;
|
|
379
|
+
const indexCount = partition.masterShadow[recIdx * ru32 + 3];
|
|
380
|
+
const oldCount = partition.masterShadow[recIdx * ru32 + 4];
|
|
381
|
+
partition.masterShadow[recIdx * ru32 + 4] = newCount >>> 0;
|
|
382
|
+
const delta = indexCount * (newCount - oldCount);
|
|
383
|
+
bucket.partitionDirty = true;
|
|
384
|
+
for (const sl of bucket.slots) {
|
|
385
|
+
sl.totalEmitEstimate = Math.max(0, sl.totalEmitEstimate + delta);
|
|
386
|
+
sl.scanDirty = true;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
const slotIdx = drawIdToSlotIdx[drawId];
|
|
391
|
+
const slot = slotIdx !== undefined ? bucket.slots[slotIdx] : bucket.slots[0];
|
|
392
|
+
if (slot === undefined)
|
|
393
|
+
return;
|
|
394
|
+
const localSlot = drawIdToLocalSlot[drawId];
|
|
395
|
+
if (localSlot === undefined)
|
|
396
|
+
return;
|
|
397
|
+
const recIdx = slot.slotToRecord[localSlot];
|
|
398
|
+
if (recIdx === undefined || recIdx < 0)
|
|
399
|
+
return;
|
|
400
|
+
const shadow = slot.drawTableShadow;
|
|
401
|
+
const indexCount = shadow[recIdx * RECORD_U32 + 3];
|
|
402
|
+
const oldCount = shadow[recIdx * RECORD_U32 + 4];
|
|
403
|
+
shadow[recIdx * RECORD_U32 + 4] = newCount >>> 0;
|
|
404
|
+
const byteOff = recIdx * RECORD_BYTES + 4 * 4;
|
|
405
|
+
if (byteOff < slot.drawTableDirtyMin)
|
|
406
|
+
slot.drawTableDirtyMin = byteOff;
|
|
407
|
+
if (byteOff + 4 > slot.drawTableDirtyMax)
|
|
408
|
+
slot.drawTableDirtyMax = byteOff + 4;
|
|
409
|
+
const delta = indexCount * (newCount - oldCount);
|
|
410
|
+
slot.totalEmitEstimate = Math.max(0, slot.totalEmitEstimate + delta);
|
|
411
|
+
slot.scanDirty = true;
|
|
412
|
+
}
|
|
413
|
+
const localSlot = drawIdToLocalSlot[drawId];
|
|
414
|
+
if (localSlot !== undefined) {
|
|
415
|
+
const e = bucket.localEntries[localSlot];
|
|
416
|
+
if (e !== undefined)
|
|
417
|
+
e.instanceCount = newCount;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
340
420
|
function subscribeModeLeaf(av, drawId) {
|
|
421
|
+
// A CONSTANT leaf can never mark — subscribing would allocate a
|
|
422
|
+
// MultiCallbackObject + callback Map + disposable + a draw-id Set
|
|
423
|
+
// PER AVAL for nothing. At collection scale the per-row uniform
|
|
424
|
+
// wrappers are overwhelmingly constants; this guard was worth
|
|
425
|
+
// ~4-5 KB per row-pass (measured, WBOIT 2k).
|
|
426
|
+
if (av.isConstant)
|
|
427
|
+
return;
|
|
341
428
|
let set = modeAvalToDrawIds.get(av);
|
|
342
429
|
if (set === undefined) {
|
|
343
430
|
set = new Set();
|
|
@@ -2095,7 +2182,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2095
2182
|
headerDirtyMin: Infinity, headerDirtyMax: 0,
|
|
2096
2183
|
localPosRefs: [], localNorRefs: [],
|
|
2097
2184
|
localEntries: [], localToDrawId: [],
|
|
2098
|
-
localPerDrawAvals: [], localPerDrawRawFieldIdx: [], localPerDrawRefs: [], localLayoutIds: [],
|
|
2185
|
+
localPerDrawAvals: [], localPerDrawAvalFieldIdx: [], localPerDrawRawFieldIdx: [], localPerDrawRefs: [], localLayoutIds: [],
|
|
2099
2186
|
fieldIdx,
|
|
2100
2187
|
posFieldIdx: fieldIdx.get("Positions") ?? -1,
|
|
2101
2188
|
norFieldIdx: fieldIdx.get("Normals") ?? -1,
|
|
@@ -2355,8 +2442,21 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2355
2442
|
const hasIndices = spec.indices !== undefined;
|
|
2356
2443
|
const indicesAval = hasIndices ? asAval(spec.indices) : undefined;
|
|
2357
2444
|
const indexArr = hasIndices ? readPlain(spec.indices) : undefined;
|
|
2445
|
+
const icAval = spec.instanceCount !== undefined && typeof spec.instanceCount === "object"
|
|
2446
|
+
&& typeof spec.instanceCount.getValue === "function"
|
|
2447
|
+
? spec.instanceCount
|
|
2448
|
+
: undefined;
|
|
2358
2449
|
const instanceCount = spec.instanceCount !== undefined
|
|
2359
2450
|
? readPlain(spec.instanceCount) : 1;
|
|
2451
|
+
if (icAval !== undefined && perInstanceUniforms.size > 0) {
|
|
2452
|
+
// Per-instance UNIFORM arrays are packed `instanceCount` deep at
|
|
2453
|
+
// addDraw — a changing count would need a repack protocol those
|
|
2454
|
+
// packers don't have. Attribute-backed instancing (BufferViews /
|
|
2455
|
+
// raw arrays) is unaffected: the payload rides the sized-repack
|
|
2456
|
+
// path and the count merely selects a prefix.
|
|
2457
|
+
throw new Error("heapScene: adaptive instanceCount is not supported together with per-instance UNIFORM values " +
|
|
2458
|
+
`(fields: ${[...perInstanceUniforms].join(", ")}) — use instance attributes instead`);
|
|
2459
|
+
}
|
|
2360
2460
|
let estBytes = indexArr !== undefined ? ALIGN16(ALLOC_HEADER_PAD_TO + indexArr.byteLength) : 0;
|
|
2361
2461
|
for (const f of layout.drawHeaderFields) {
|
|
2362
2462
|
if (f.kind === "texture-ref" || f.name === "__layoutId" || INLINE_HEADER_UNIFORMS.has(f.name) || !effectFields.has(f.name))
|
|
@@ -2480,6 +2580,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2480
2580
|
// packs as a single value. Both go through the same pool — sharing
|
|
2481
2581
|
// emerges from aval identity either way.
|
|
2482
2582
|
const perDrawAvals = [];
|
|
2583
|
+
const perDrawAvalFieldIdx = [];
|
|
2483
2584
|
let perDrawRawFieldIdx;
|
|
2484
2585
|
const perDrawRefs = new Int32Array(bucket.layout.drawHeaderFields.length).fill(-1);
|
|
2485
2586
|
const setRef = (name, ref) => {
|
|
@@ -2571,9 +2672,13 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2571
2672
|
let value;
|
|
2572
2673
|
let placement;
|
|
2573
2674
|
let releasable;
|
|
2675
|
+
let replanFn;
|
|
2574
2676
|
if (f.kind === "attribute-ref" && !isPerInstanceUniformField && isBufferView(provided)) {
|
|
2575
2677
|
const bv = provided;
|
|
2576
2678
|
placement = bufferViewPlacement(f, bv);
|
|
2679
|
+
// Size-variable payload: geometry morphing re-derives the
|
|
2680
|
+
// placement from the view's CURRENT buffer value.
|
|
2681
|
+
replanFn = () => bufferViewPlacement(f, bv);
|
|
2577
2682
|
av = bv.buffer;
|
|
2578
2683
|
value = bv.buffer.getValue(tok);
|
|
2579
2684
|
}
|
|
@@ -2607,12 +2712,16 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2607
2712
|
placement = isPerInstanceUniformField
|
|
2608
2713
|
? perInstancePlacementFor(f, value, instanceCount)
|
|
2609
2714
|
: poolPlacementFor(f, value);
|
|
2715
|
+
if (f.kind === "attribute-ref" && !isPerInstanceUniformField) {
|
|
2716
|
+
replanFn = (v) => poolPlacementFor(f, v);
|
|
2717
|
+
}
|
|
2610
2718
|
}
|
|
2611
|
-
const ref = pool.acquire(device, arena.attrs, av, bucket.chunkIdx, value, placement.dataBytes, placement.typeId, placement.length, placement.pack);
|
|
2719
|
+
const ref = pool.acquire(device, arena.attrs, av, bucket.chunkIdx, value, placement.dataBytes, placement.typeId, placement.length, placement.pack, replanFn);
|
|
2612
2720
|
if (releasable !== undefined)
|
|
2613
2721
|
releasable._v = null; // staged — drop the CPU copy
|
|
2614
2722
|
setRef(f.name, ref);
|
|
2615
2723
|
perDrawAvals.push(av);
|
|
2724
|
+
perDrawAvalFieldIdx.push(bucket.fieldIdx.get(f.name));
|
|
2616
2725
|
}
|
|
2617
2726
|
}
|
|
2618
2727
|
bucket.localPosRefs[localSlot] = getRef("Positions");
|
|
@@ -2623,6 +2732,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2623
2732
|
bucket.localToDrawId[localSlot] = drawId;
|
|
2624
2733
|
bucket.drawSlots.add(localSlot);
|
|
2625
2734
|
bucket.localPerDrawAvals[localSlot] = perDrawAvals;
|
|
2735
|
+
bucket.localPerDrawAvalFieldIdx[localSlot] = perDrawAvalFieldIdx;
|
|
2626
2736
|
bucket.localPerDrawRawFieldIdx[localSlot] = perDrawRawFieldIdx;
|
|
2627
2737
|
bucket.localPerDrawRefs[localSlot] = perDrawRefs;
|
|
2628
2738
|
const layoutId = fam.schema.layoutIdOf.get(spec.effect);
|
|
@@ -2815,6 +2925,9 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2815
2925
|
setEffectiveIndexCount(drawId, 0);
|
|
2816
2926
|
}
|
|
2817
2927
|
}
|
|
2928
|
+
if (icAval !== undefined) {
|
|
2929
|
+
subscribeCount(icAval, drawId);
|
|
2930
|
+
}
|
|
2818
2931
|
// ─── Reactive rebucket: track PS-modeKey changes ────────────────
|
|
2819
2932
|
// When any mode-axis aval marks (e.g. cullCval.value = 'front'),
|
|
2820
2933
|
// schedule this RO for rebucket on the next update(). Today's
|
|
@@ -2931,6 +3044,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2931
3044
|
// Tear down `active` subscription (if any) so its callback won't
|
|
2932
3045
|
// fire after the underlying drawTable record is gone.
|
|
2933
3046
|
unsubscribeActive(drawId);
|
|
3047
|
+
unsubscribeCount(drawId);
|
|
2934
3048
|
// §7: deregister this RO's derivation records and release slots.
|
|
2935
3049
|
if (derivedScene !== undefined) {
|
|
2936
3050
|
const reg = derivedByDrawId.get(drawId);
|
|
@@ -3072,6 +3186,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3072
3186
|
bucket.localAtlasTextures[localSlot] = undefined;
|
|
3073
3187
|
bucket.localAtlasArrIdx[localSlot] = undefined;
|
|
3074
3188
|
bucket.localPerDrawAvals[localSlot] = undefined;
|
|
3189
|
+
bucket.localPerDrawAvalFieldIdx[localSlot] = undefined;
|
|
3075
3190
|
bucket.localPerDrawRefs[localSlot] = undefined;
|
|
3076
3191
|
bucket.localLayoutIds[localSlot] = undefined;
|
|
3077
3192
|
bucket.localPosRefs[localSlot] = undefined;
|
|
@@ -3471,11 +3586,74 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3471
3586
|
// One writeBuffer per dirty aval, regardless of how many draws
|
|
3472
3587
|
// reference it — sharing pays off here.
|
|
3473
3588
|
if (allocDirty.size > 0) {
|
|
3589
|
+
// aval → (chunkIdx → newRef) for allocations the sized repack
|
|
3590
|
+
// moved (payload grew/shrank → realloc'd within the chunk).
|
|
3591
|
+
let movedByAval;
|
|
3474
3592
|
for (const av of allocDirty) {
|
|
3475
|
-
pool.repack(device, arena.attrs, av, av.getValue(tok));
|
|
3593
|
+
const moved = pool.repack(device, arena.attrs, av, av.getValue(tok));
|
|
3476
3594
|
totalDirtyBytes += pool.totalDataBytes(av);
|
|
3595
|
+
if (moved !== undefined) {
|
|
3596
|
+
for (const m of moved) {
|
|
3597
|
+
let byChunk = (movedByAval ??= new Map()).get(m.av);
|
|
3598
|
+
if (byChunk === undefined) {
|
|
3599
|
+
byChunk = new Map();
|
|
3600
|
+
movedByAval.set(m.av, byChunk);
|
|
3601
|
+
}
|
|
3602
|
+
byChunk.set(m.chunkIdx, m.newRef);
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3477
3605
|
}
|
|
3478
3606
|
allocDirty.clear();
|
|
3607
|
+
if (movedByAval !== undefined) {
|
|
3608
|
+
// Re-seat every drawHeader field whose aval's allocation
|
|
3609
|
+
// moved. Matched by AVAL IDENTITY via the parallel
|
|
3610
|
+
// `localPerDrawAvalFieldIdx` — never by numeric offset (a
|
|
3611
|
+
// released offset can be re-issued to a DIFFERENT aval in
|
|
3612
|
+
// this same drain). Mirrors the compaction remap loop.
|
|
3613
|
+
for (const bucket of buckets) {
|
|
3614
|
+
for (const localSlot of bucket.drawSlots) {
|
|
3615
|
+
const avals = bucket.localPerDrawAvals[localSlot];
|
|
3616
|
+
const fidx = bucket.localPerDrawAvalFieldIdx[localSlot];
|
|
3617
|
+
const refs = bucket.localPerDrawRefs[localSlot];
|
|
3618
|
+
if (avals === undefined || fidx === undefined || refs === undefined)
|
|
3619
|
+
continue;
|
|
3620
|
+
let changed = false;
|
|
3621
|
+
for (let i = 0; i < avals.length; i++) {
|
|
3622
|
+
const byChunk = movedByAval.get(avals[i]);
|
|
3623
|
+
if (byChunk === undefined)
|
|
3624
|
+
continue;
|
|
3625
|
+
const nn = byChunk.get(bucket.chunkIdx);
|
|
3626
|
+
if (nn === undefined)
|
|
3627
|
+
continue;
|
|
3628
|
+
if (bucket.gpuRouted) {
|
|
3629
|
+
throw new Error("heapScene: payload realloc under a GPU-routed (derived-modes) bucket is not supported yet");
|
|
3630
|
+
}
|
|
3631
|
+
if (refs[fidx[i]] !== nn) {
|
|
3632
|
+
refs[fidx[i]] = nn;
|
|
3633
|
+
changed = true;
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
if (!changed)
|
|
3637
|
+
continue;
|
|
3638
|
+
const pr = bucket.posFieldIdx >= 0 ? refs[bucket.posFieldIdx] : -1;
|
|
3639
|
+
const nr = bucket.norFieldIdx >= 0 ? refs[bucket.norFieldIdx] : -1;
|
|
3640
|
+
bucket.localPosRefs[localSlot] = pr < 0 ? undefined : pr;
|
|
3641
|
+
bucket.localNorRefs[localSlot] = nr < 0 ? undefined : nr;
|
|
3642
|
+
packBucketHeader(bucket, localSlot, refs, bucket.localLayoutIds[localSlot] ?? 0);
|
|
3643
|
+
if (bucket.isAtlasBucket) {
|
|
3644
|
+
const ts = bucket.localAtlasTextures[localSlot];
|
|
3645
|
+
if (ts !== undefined)
|
|
3646
|
+
packAtlasTextureFields(bucket, localSlot, ts);
|
|
3647
|
+
}
|
|
3648
|
+
const byteOff = localSlot * bucket.layout.drawHeaderBytes;
|
|
3649
|
+
if (byteOff < bucket.headerDirtyMin)
|
|
3650
|
+
bucket.headerDirtyMin = byteOff;
|
|
3651
|
+
const end = byteOff + bucket.layout.drawHeaderBytes;
|
|
3652
|
+
if (end > bucket.headerDirtyMax)
|
|
3653
|
+
bucket.headerDirtyMax = end;
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
}
|
|
3479
3657
|
}
|
|
3480
3658
|
// 1c. HeapDrawSpec.active drain — flip drawTable.indexCount
|
|
3481
3659
|
// between origIndexCount (visible) and 0 (hidden) for any RO
|
|
@@ -3493,6 +3671,18 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3493
3671
|
}
|
|
3494
3672
|
activeDirty.clear();
|
|
3495
3673
|
}
|
|
3674
|
+
// 1d. HeapDrawSpec.instanceCount aval drain — write the new
|
|
3675
|
+
// count into drawTable word 4 (emit = indexCount × count;
|
|
3676
|
+
// 0 draws nothing). One record write + re-scan per tick.
|
|
3677
|
+
if (countDirty.size > 0) {
|
|
3678
|
+
for (const did of countDirty) {
|
|
3679
|
+
const av = drawIdToCountAval.get(did);
|
|
3680
|
+
if (av === undefined)
|
|
3681
|
+
continue;
|
|
3682
|
+
setEffectiveInstanceCount(did, Math.max(0, av.getValue(tok) | 0));
|
|
3683
|
+
}
|
|
3684
|
+
countDirty.clear();
|
|
3685
|
+
}
|
|
3496
3686
|
// 1b. Atlas-texture aval reactivity: an `aval<ITexture>` that
|
|
3497
3687
|
// drives an atlas placement was marked. Repack the pool entry
|
|
3498
3688
|
// and rewrite the drawHeader fields of every (bucket, slot)
|