@aardworx/wombat.rendering 0.21.11 → 0.21.13
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 +195 -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 +189 -2
|
@@ -337,6 +337,82 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
337
337
|
slot.scanDirty = true;
|
|
338
338
|
}
|
|
339
339
|
}
|
|
340
|
+
// ─── HeapDrawSpec.instanceCount as an aval ────────────────────────
|
|
341
|
+
// The geometry-morphing / collection-count path (epoch switches,
|
|
342
|
+
// point add/remove): a count tick is ONE drawTable write (record
|
|
343
|
+
// word 4) + emit re-estimate + re-scan — no add/remove churn, no
|
|
344
|
+
// arena activity. Instance ATTRIBUTE payloads ride the sized-repack
|
|
345
|
+
// path independently; per-instance UNIFORM arrays (packed per
|
|
346
|
+
// count) are rejected with an adaptive count at addDraw.
|
|
347
|
+
const drawIdToCountAval = new Map();
|
|
348
|
+
const drawIdToCountCallback = new Map();
|
|
349
|
+
const countDirty = new Set();
|
|
350
|
+
function subscribeCount(av, drawId) {
|
|
351
|
+
drawIdToCountAval.set(drawId, av);
|
|
352
|
+
drawIdToCountCallback.set(drawId, addMarkingCallback(av, () => { countDirty.add(drawId); }));
|
|
353
|
+
}
|
|
354
|
+
function unsubscribeCount(drawId) {
|
|
355
|
+
const cb = drawIdToCountCallback.get(drawId);
|
|
356
|
+
if (cb === undefined)
|
|
357
|
+
return;
|
|
358
|
+
cb.dispose();
|
|
359
|
+
drawIdToCountCallback.delete(drawId);
|
|
360
|
+
drawIdToCountAval.delete(drawId);
|
|
361
|
+
countDirty.delete(drawId);
|
|
362
|
+
}
|
|
363
|
+
/** Update the drawTable record's instanceCount in place — the
|
|
364
|
+
* word-4 sibling of `setEffectiveIndexCount`. */
|
|
365
|
+
function setEffectiveInstanceCount(drawId, newCount) {
|
|
366
|
+
const bucket = drawIdToBucket[drawId];
|
|
367
|
+
if (bucket === undefined)
|
|
368
|
+
return;
|
|
369
|
+
if (bucket.gpuRouted) {
|
|
370
|
+
const partition = bucket.partitionScene;
|
|
371
|
+
const recIdx = bucket.drawIdToRecord.get(drawId);
|
|
372
|
+
if (recIdx === undefined)
|
|
373
|
+
return;
|
|
374
|
+
const ru32 = partition.recordU32;
|
|
375
|
+
const indexCount = partition.masterShadow[recIdx * ru32 + 3];
|
|
376
|
+
const oldCount = partition.masterShadow[recIdx * ru32 + 4];
|
|
377
|
+
partition.masterShadow[recIdx * ru32 + 4] = newCount >>> 0;
|
|
378
|
+
const delta = indexCount * (newCount - oldCount);
|
|
379
|
+
bucket.partitionDirty = true;
|
|
380
|
+
for (const sl of bucket.slots) {
|
|
381
|
+
sl.totalEmitEstimate = Math.max(0, sl.totalEmitEstimate + delta);
|
|
382
|
+
sl.scanDirty = true;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
const slotIdx = drawIdToSlotIdx[drawId];
|
|
387
|
+
const slot = slotIdx !== undefined ? bucket.slots[slotIdx] : bucket.slots[0];
|
|
388
|
+
if (slot === undefined)
|
|
389
|
+
return;
|
|
390
|
+
const localSlot = drawIdToLocalSlot[drawId];
|
|
391
|
+
if (localSlot === undefined)
|
|
392
|
+
return;
|
|
393
|
+
const recIdx = slot.slotToRecord[localSlot];
|
|
394
|
+
if (recIdx === undefined || recIdx < 0)
|
|
395
|
+
return;
|
|
396
|
+
const shadow = slot.drawTableShadow;
|
|
397
|
+
const indexCount = shadow[recIdx * RECORD_U32 + 3];
|
|
398
|
+
const oldCount = shadow[recIdx * RECORD_U32 + 4];
|
|
399
|
+
shadow[recIdx * RECORD_U32 + 4] = newCount >>> 0;
|
|
400
|
+
const byteOff = recIdx * RECORD_BYTES + 4 * 4;
|
|
401
|
+
if (byteOff < slot.drawTableDirtyMin)
|
|
402
|
+
slot.drawTableDirtyMin = byteOff;
|
|
403
|
+
if (byteOff + 4 > slot.drawTableDirtyMax)
|
|
404
|
+
slot.drawTableDirtyMax = byteOff + 4;
|
|
405
|
+
const delta = indexCount * (newCount - oldCount);
|
|
406
|
+
slot.totalEmitEstimate = Math.max(0, slot.totalEmitEstimate + delta);
|
|
407
|
+
slot.scanDirty = true;
|
|
408
|
+
}
|
|
409
|
+
const localSlot = drawIdToLocalSlot[drawId];
|
|
410
|
+
if (localSlot !== undefined) {
|
|
411
|
+
const e = bucket.localEntries[localSlot];
|
|
412
|
+
if (e !== undefined)
|
|
413
|
+
e.instanceCount = newCount;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
340
416
|
function subscribeModeLeaf(av, drawId) {
|
|
341
417
|
let set = modeAvalToDrawIds.get(av);
|
|
342
418
|
if (set === undefined) {
|
|
@@ -859,7 +935,20 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
859
935
|
// `colorTargets` here silently dropped blends/write-masks from every
|
|
860
936
|
// heap pipeline: transparent heap leaves painted opaque, and the OIT
|
|
861
937
|
// pick passes' write-mask-0 "Colors" leaked color onto the composite.
|
|
938
|
+
// Locations the family's fragment shader actually writes — a
|
|
939
|
+
// signature target the shader has NO output for must set
|
|
940
|
+
// writeMask 0 (WebGPU: non-zero writeMask without a fragment
|
|
941
|
+
// output is a validation error; e.g. a non-picking leaf in a
|
|
942
|
+
// Colors+pickId pass writes only Colors).
|
|
943
|
+
const writtenLocations = new Set();
|
|
944
|
+
for (const e of fam.schema.effects) {
|
|
945
|
+
const sc = fam.schema.perEffectSchema.get(e);
|
|
946
|
+
for (const fo of sc.fragmentOutputs)
|
|
947
|
+
writtenLocations.add(fo.location);
|
|
948
|
+
}
|
|
862
949
|
const targets = colorTargets.map((t, i) => {
|
|
950
|
+
if (!writtenLocations.has(i))
|
|
951
|
+
return { format: t.format, writeMask: 0 };
|
|
863
952
|
const a = desc.attachments[i];
|
|
864
953
|
if (a === undefined)
|
|
865
954
|
return { format: t.format };
|
|
@@ -2082,7 +2171,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2082
2171
|
headerDirtyMin: Infinity, headerDirtyMax: 0,
|
|
2083
2172
|
localPosRefs: [], localNorRefs: [],
|
|
2084
2173
|
localEntries: [], localToDrawId: [],
|
|
2085
|
-
localPerDrawAvals: [], localPerDrawRawFieldIdx: [], localPerDrawRefs: [], localLayoutIds: [],
|
|
2174
|
+
localPerDrawAvals: [], localPerDrawAvalFieldIdx: [], localPerDrawRawFieldIdx: [], localPerDrawRefs: [], localLayoutIds: [],
|
|
2086
2175
|
fieldIdx,
|
|
2087
2176
|
posFieldIdx: fieldIdx.get("Positions") ?? -1,
|
|
2088
2177
|
norFieldIdx: fieldIdx.get("Normals") ?? -1,
|
|
@@ -2342,8 +2431,21 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2342
2431
|
const hasIndices = spec.indices !== undefined;
|
|
2343
2432
|
const indicesAval = hasIndices ? asAval(spec.indices) : undefined;
|
|
2344
2433
|
const indexArr = hasIndices ? readPlain(spec.indices) : undefined;
|
|
2434
|
+
const icAval = spec.instanceCount !== undefined && typeof spec.instanceCount === "object"
|
|
2435
|
+
&& typeof spec.instanceCount.getValue === "function"
|
|
2436
|
+
? spec.instanceCount
|
|
2437
|
+
: undefined;
|
|
2345
2438
|
const instanceCount = spec.instanceCount !== undefined
|
|
2346
2439
|
? readPlain(spec.instanceCount) : 1;
|
|
2440
|
+
if (icAval !== undefined && perInstanceUniforms.size > 0) {
|
|
2441
|
+
// Per-instance UNIFORM arrays are packed `instanceCount` deep at
|
|
2442
|
+
// addDraw — a changing count would need a repack protocol those
|
|
2443
|
+
// packers don't have. Attribute-backed instancing (BufferViews /
|
|
2444
|
+
// raw arrays) is unaffected: the payload rides the sized-repack
|
|
2445
|
+
// path and the count merely selects a prefix.
|
|
2446
|
+
throw new Error("heapScene: adaptive instanceCount is not supported together with per-instance UNIFORM values " +
|
|
2447
|
+
`(fields: ${[...perInstanceUniforms].join(", ")}) — use instance attributes instead`);
|
|
2448
|
+
}
|
|
2347
2449
|
let estBytes = indexArr !== undefined ? ALIGN16(ALLOC_HEADER_PAD_TO + indexArr.byteLength) : 0;
|
|
2348
2450
|
for (const f of layout.drawHeaderFields) {
|
|
2349
2451
|
if (f.kind === "texture-ref" || f.name === "__layoutId" || INLINE_HEADER_UNIFORMS.has(f.name) || !effectFields.has(f.name))
|
|
@@ -2467,6 +2569,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2467
2569
|
// packs as a single value. Both go through the same pool — sharing
|
|
2468
2570
|
// emerges from aval identity either way.
|
|
2469
2571
|
const perDrawAvals = [];
|
|
2572
|
+
const perDrawAvalFieldIdx = [];
|
|
2470
2573
|
let perDrawRawFieldIdx;
|
|
2471
2574
|
const perDrawRefs = new Int32Array(bucket.layout.drawHeaderFields.length).fill(-1);
|
|
2472
2575
|
const setRef = (name, ref) => {
|
|
@@ -2558,9 +2661,13 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2558
2661
|
let value;
|
|
2559
2662
|
let placement;
|
|
2560
2663
|
let releasable;
|
|
2664
|
+
let replanFn;
|
|
2561
2665
|
if (f.kind === "attribute-ref" && !isPerInstanceUniformField && isBufferView(provided)) {
|
|
2562
2666
|
const bv = provided;
|
|
2563
2667
|
placement = bufferViewPlacement(f, bv);
|
|
2668
|
+
// Size-variable payload: geometry morphing re-derives the
|
|
2669
|
+
// placement from the view's CURRENT buffer value.
|
|
2670
|
+
replanFn = () => bufferViewPlacement(f, bv);
|
|
2564
2671
|
av = bv.buffer;
|
|
2565
2672
|
value = bv.buffer.getValue(tok);
|
|
2566
2673
|
}
|
|
@@ -2594,12 +2701,16 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2594
2701
|
placement = isPerInstanceUniformField
|
|
2595
2702
|
? perInstancePlacementFor(f, value, instanceCount)
|
|
2596
2703
|
: poolPlacementFor(f, value);
|
|
2704
|
+
if (f.kind === "attribute-ref" && !isPerInstanceUniformField) {
|
|
2705
|
+
replanFn = (v) => poolPlacementFor(f, v);
|
|
2706
|
+
}
|
|
2597
2707
|
}
|
|
2598
|
-
const ref = pool.acquire(device, arena.attrs, av, bucket.chunkIdx, value, placement.dataBytes, placement.typeId, placement.length, placement.pack);
|
|
2708
|
+
const ref = pool.acquire(device, arena.attrs, av, bucket.chunkIdx, value, placement.dataBytes, placement.typeId, placement.length, placement.pack, replanFn);
|
|
2599
2709
|
if (releasable !== undefined)
|
|
2600
2710
|
releasable._v = null; // staged — drop the CPU copy
|
|
2601
2711
|
setRef(f.name, ref);
|
|
2602
2712
|
perDrawAvals.push(av);
|
|
2713
|
+
perDrawAvalFieldIdx.push(bucket.fieldIdx.get(f.name));
|
|
2603
2714
|
}
|
|
2604
2715
|
}
|
|
2605
2716
|
bucket.localPosRefs[localSlot] = getRef("Positions");
|
|
@@ -2610,6 +2721,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2610
2721
|
bucket.localToDrawId[localSlot] = drawId;
|
|
2611
2722
|
bucket.drawSlots.add(localSlot);
|
|
2612
2723
|
bucket.localPerDrawAvals[localSlot] = perDrawAvals;
|
|
2724
|
+
bucket.localPerDrawAvalFieldIdx[localSlot] = perDrawAvalFieldIdx;
|
|
2613
2725
|
bucket.localPerDrawRawFieldIdx[localSlot] = perDrawRawFieldIdx;
|
|
2614
2726
|
bucket.localPerDrawRefs[localSlot] = perDrawRefs;
|
|
2615
2727
|
const layoutId = fam.schema.layoutIdOf.get(spec.effect);
|
|
@@ -2802,6 +2914,9 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2802
2914
|
setEffectiveIndexCount(drawId, 0);
|
|
2803
2915
|
}
|
|
2804
2916
|
}
|
|
2917
|
+
if (icAval !== undefined) {
|
|
2918
|
+
subscribeCount(icAval, drawId);
|
|
2919
|
+
}
|
|
2805
2920
|
// ─── Reactive rebucket: track PS-modeKey changes ────────────────
|
|
2806
2921
|
// When any mode-axis aval marks (e.g. cullCval.value = 'front'),
|
|
2807
2922
|
// schedule this RO for rebucket on the next update(). Today's
|
|
@@ -2918,6 +3033,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2918
3033
|
// Tear down `active` subscription (if any) so its callback won't
|
|
2919
3034
|
// fire after the underlying drawTable record is gone.
|
|
2920
3035
|
unsubscribeActive(drawId);
|
|
3036
|
+
unsubscribeCount(drawId);
|
|
2921
3037
|
// §7: deregister this RO's derivation records and release slots.
|
|
2922
3038
|
if (derivedScene !== undefined) {
|
|
2923
3039
|
const reg = derivedByDrawId.get(drawId);
|
|
@@ -3059,6 +3175,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3059
3175
|
bucket.localAtlasTextures[localSlot] = undefined;
|
|
3060
3176
|
bucket.localAtlasArrIdx[localSlot] = undefined;
|
|
3061
3177
|
bucket.localPerDrawAvals[localSlot] = undefined;
|
|
3178
|
+
bucket.localPerDrawAvalFieldIdx[localSlot] = undefined;
|
|
3062
3179
|
bucket.localPerDrawRefs[localSlot] = undefined;
|
|
3063
3180
|
bucket.localLayoutIds[localSlot] = undefined;
|
|
3064
3181
|
bucket.localPosRefs[localSlot] = undefined;
|
|
@@ -3458,11 +3575,74 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3458
3575
|
// One writeBuffer per dirty aval, regardless of how many draws
|
|
3459
3576
|
// reference it — sharing pays off here.
|
|
3460
3577
|
if (allocDirty.size > 0) {
|
|
3578
|
+
// aval → (chunkIdx → newRef) for allocations the sized repack
|
|
3579
|
+
// moved (payload grew/shrank → realloc'd within the chunk).
|
|
3580
|
+
let movedByAval;
|
|
3461
3581
|
for (const av of allocDirty) {
|
|
3462
|
-
pool.repack(device, arena.attrs, av, av.getValue(tok));
|
|
3582
|
+
const moved = pool.repack(device, arena.attrs, av, av.getValue(tok));
|
|
3463
3583
|
totalDirtyBytes += pool.totalDataBytes(av);
|
|
3584
|
+
if (moved !== undefined) {
|
|
3585
|
+
for (const m of moved) {
|
|
3586
|
+
let byChunk = (movedByAval ??= new Map()).get(m.av);
|
|
3587
|
+
if (byChunk === undefined) {
|
|
3588
|
+
byChunk = new Map();
|
|
3589
|
+
movedByAval.set(m.av, byChunk);
|
|
3590
|
+
}
|
|
3591
|
+
byChunk.set(m.chunkIdx, m.newRef);
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3464
3594
|
}
|
|
3465
3595
|
allocDirty.clear();
|
|
3596
|
+
if (movedByAval !== undefined) {
|
|
3597
|
+
// Re-seat every drawHeader field whose aval's allocation
|
|
3598
|
+
// moved. Matched by AVAL IDENTITY via the parallel
|
|
3599
|
+
// `localPerDrawAvalFieldIdx` — never by numeric offset (a
|
|
3600
|
+
// released offset can be re-issued to a DIFFERENT aval in
|
|
3601
|
+
// this same drain). Mirrors the compaction remap loop.
|
|
3602
|
+
for (const bucket of buckets) {
|
|
3603
|
+
for (const localSlot of bucket.drawSlots) {
|
|
3604
|
+
const avals = bucket.localPerDrawAvals[localSlot];
|
|
3605
|
+
const fidx = bucket.localPerDrawAvalFieldIdx[localSlot];
|
|
3606
|
+
const refs = bucket.localPerDrawRefs[localSlot];
|
|
3607
|
+
if (avals === undefined || fidx === undefined || refs === undefined)
|
|
3608
|
+
continue;
|
|
3609
|
+
let changed = false;
|
|
3610
|
+
for (let i = 0; i < avals.length; i++) {
|
|
3611
|
+
const byChunk = movedByAval.get(avals[i]);
|
|
3612
|
+
if (byChunk === undefined)
|
|
3613
|
+
continue;
|
|
3614
|
+
const nn = byChunk.get(bucket.chunkIdx);
|
|
3615
|
+
if (nn === undefined)
|
|
3616
|
+
continue;
|
|
3617
|
+
if (bucket.gpuRouted) {
|
|
3618
|
+
throw new Error("heapScene: payload realloc under a GPU-routed (derived-modes) bucket is not supported yet");
|
|
3619
|
+
}
|
|
3620
|
+
if (refs[fidx[i]] !== nn) {
|
|
3621
|
+
refs[fidx[i]] = nn;
|
|
3622
|
+
changed = true;
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
if (!changed)
|
|
3626
|
+
continue;
|
|
3627
|
+
const pr = bucket.posFieldIdx >= 0 ? refs[bucket.posFieldIdx] : -1;
|
|
3628
|
+
const nr = bucket.norFieldIdx >= 0 ? refs[bucket.norFieldIdx] : -1;
|
|
3629
|
+
bucket.localPosRefs[localSlot] = pr < 0 ? undefined : pr;
|
|
3630
|
+
bucket.localNorRefs[localSlot] = nr < 0 ? undefined : nr;
|
|
3631
|
+
packBucketHeader(bucket, localSlot, refs, bucket.localLayoutIds[localSlot] ?? 0);
|
|
3632
|
+
if (bucket.isAtlasBucket) {
|
|
3633
|
+
const ts = bucket.localAtlasTextures[localSlot];
|
|
3634
|
+
if (ts !== undefined)
|
|
3635
|
+
packAtlasTextureFields(bucket, localSlot, ts);
|
|
3636
|
+
}
|
|
3637
|
+
const byteOff = localSlot * bucket.layout.drawHeaderBytes;
|
|
3638
|
+
if (byteOff < bucket.headerDirtyMin)
|
|
3639
|
+
bucket.headerDirtyMin = byteOff;
|
|
3640
|
+
const end = byteOff + bucket.layout.drawHeaderBytes;
|
|
3641
|
+
if (end > bucket.headerDirtyMax)
|
|
3642
|
+
bucket.headerDirtyMax = end;
|
|
3643
|
+
}
|
|
3644
|
+
}
|
|
3645
|
+
}
|
|
3466
3646
|
}
|
|
3467
3647
|
// 1c. HeapDrawSpec.active drain — flip drawTable.indexCount
|
|
3468
3648
|
// between origIndexCount (visible) and 0 (hidden) for any RO
|
|
@@ -3480,6 +3660,18 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
3480
3660
|
}
|
|
3481
3661
|
activeDirty.clear();
|
|
3482
3662
|
}
|
|
3663
|
+
// 1d. HeapDrawSpec.instanceCount aval drain — write the new
|
|
3664
|
+
// count into drawTable word 4 (emit = indexCount × count;
|
|
3665
|
+
// 0 draws nothing). One record write + re-scan per tick.
|
|
3666
|
+
if (countDirty.size > 0) {
|
|
3667
|
+
for (const did of countDirty) {
|
|
3668
|
+
const av = drawIdToCountAval.get(did);
|
|
3669
|
+
if (av === undefined)
|
|
3670
|
+
continue;
|
|
3671
|
+
setEffectiveInstanceCount(did, Math.max(0, av.getValue(tok) | 0));
|
|
3672
|
+
}
|
|
3673
|
+
countDirty.clear();
|
|
3674
|
+
}
|
|
3483
3675
|
// 1b. Atlas-texture aval reactivity: an `aval<ITexture>` that
|
|
3484
3676
|
// drives an atlas placement was marked. Repack the pool entry
|
|
3485
3677
|
// and rewrite the drawHeader fields of every (bucket, slot)
|