@aardworx/wombat.rendering 0.21.12 → 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.
@@ -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) {
@@ -2095,7 +2171,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
2095
2171
  headerDirtyMin: Infinity, headerDirtyMax: 0,
2096
2172
  localPosRefs: [], localNorRefs: [],
2097
2173
  localEntries: [], localToDrawId: [],
2098
- localPerDrawAvals: [], localPerDrawRawFieldIdx: [], localPerDrawRefs: [], localLayoutIds: [],
2174
+ localPerDrawAvals: [], localPerDrawAvalFieldIdx: [], localPerDrawRawFieldIdx: [], localPerDrawRefs: [], localLayoutIds: [],
2099
2175
  fieldIdx,
2100
2176
  posFieldIdx: fieldIdx.get("Positions") ?? -1,
2101
2177
  norFieldIdx: fieldIdx.get("Normals") ?? -1,
@@ -2355,8 +2431,21 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
2355
2431
  const hasIndices = spec.indices !== undefined;
2356
2432
  const indicesAval = hasIndices ? asAval(spec.indices) : undefined;
2357
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;
2358
2438
  const instanceCount = spec.instanceCount !== undefined
2359
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
+ }
2360
2449
  let estBytes = indexArr !== undefined ? ALIGN16(ALLOC_HEADER_PAD_TO + indexArr.byteLength) : 0;
2361
2450
  for (const f of layout.drawHeaderFields) {
2362
2451
  if (f.kind === "texture-ref" || f.name === "__layoutId" || INLINE_HEADER_UNIFORMS.has(f.name) || !effectFields.has(f.name))
@@ -2480,6 +2569,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
2480
2569
  // packs as a single value. Both go through the same pool — sharing
2481
2570
  // emerges from aval identity either way.
2482
2571
  const perDrawAvals = [];
2572
+ const perDrawAvalFieldIdx = [];
2483
2573
  let perDrawRawFieldIdx;
2484
2574
  const perDrawRefs = new Int32Array(bucket.layout.drawHeaderFields.length).fill(-1);
2485
2575
  const setRef = (name, ref) => {
@@ -2571,9 +2661,13 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
2571
2661
  let value;
2572
2662
  let placement;
2573
2663
  let releasable;
2664
+ let replanFn;
2574
2665
  if (f.kind === "attribute-ref" && !isPerInstanceUniformField && isBufferView(provided)) {
2575
2666
  const bv = provided;
2576
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);
2577
2671
  av = bv.buffer;
2578
2672
  value = bv.buffer.getValue(tok);
2579
2673
  }
@@ -2607,12 +2701,16 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
2607
2701
  placement = isPerInstanceUniformField
2608
2702
  ? perInstancePlacementFor(f, value, instanceCount)
2609
2703
  : poolPlacementFor(f, value);
2704
+ if (f.kind === "attribute-ref" && !isPerInstanceUniformField) {
2705
+ replanFn = (v) => poolPlacementFor(f, v);
2706
+ }
2610
2707
  }
2611
- 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);
2612
2709
  if (releasable !== undefined)
2613
2710
  releasable._v = null; // staged — drop the CPU copy
2614
2711
  setRef(f.name, ref);
2615
2712
  perDrawAvals.push(av);
2713
+ perDrawAvalFieldIdx.push(bucket.fieldIdx.get(f.name));
2616
2714
  }
2617
2715
  }
2618
2716
  bucket.localPosRefs[localSlot] = getRef("Positions");
@@ -2623,6 +2721,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
2623
2721
  bucket.localToDrawId[localSlot] = drawId;
2624
2722
  bucket.drawSlots.add(localSlot);
2625
2723
  bucket.localPerDrawAvals[localSlot] = perDrawAvals;
2724
+ bucket.localPerDrawAvalFieldIdx[localSlot] = perDrawAvalFieldIdx;
2626
2725
  bucket.localPerDrawRawFieldIdx[localSlot] = perDrawRawFieldIdx;
2627
2726
  bucket.localPerDrawRefs[localSlot] = perDrawRefs;
2628
2727
  const layoutId = fam.schema.layoutIdOf.get(spec.effect);
@@ -2815,6 +2914,9 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
2815
2914
  setEffectiveIndexCount(drawId, 0);
2816
2915
  }
2817
2916
  }
2917
+ if (icAval !== undefined) {
2918
+ subscribeCount(icAval, drawId);
2919
+ }
2818
2920
  // ─── Reactive rebucket: track PS-modeKey changes ────────────────
2819
2921
  // When any mode-axis aval marks (e.g. cullCval.value = 'front'),
2820
2922
  // schedule this RO for rebucket on the next update(). Today's
@@ -2931,6 +3033,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
2931
3033
  // Tear down `active` subscription (if any) so its callback won't
2932
3034
  // fire after the underlying drawTable record is gone.
2933
3035
  unsubscribeActive(drawId);
3036
+ unsubscribeCount(drawId);
2934
3037
  // §7: deregister this RO's derivation records and release slots.
2935
3038
  if (derivedScene !== undefined) {
2936
3039
  const reg = derivedByDrawId.get(drawId);
@@ -3072,6 +3175,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
3072
3175
  bucket.localAtlasTextures[localSlot] = undefined;
3073
3176
  bucket.localAtlasArrIdx[localSlot] = undefined;
3074
3177
  bucket.localPerDrawAvals[localSlot] = undefined;
3178
+ bucket.localPerDrawAvalFieldIdx[localSlot] = undefined;
3075
3179
  bucket.localPerDrawRefs[localSlot] = undefined;
3076
3180
  bucket.localLayoutIds[localSlot] = undefined;
3077
3181
  bucket.localPosRefs[localSlot] = undefined;
@@ -3471,11 +3575,74 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
3471
3575
  // One writeBuffer per dirty aval, regardless of how many draws
3472
3576
  // reference it — sharing pays off here.
3473
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;
3474
3581
  for (const av of allocDirty) {
3475
- pool.repack(device, arena.attrs, av, av.getValue(tok));
3582
+ const moved = pool.repack(device, arena.attrs, av, av.getValue(tok));
3476
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
+ }
3477
3594
  }
3478
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
+ }
3479
3646
  }
3480
3647
  // 1c. HeapDrawSpec.active drain — flip drawTable.indexCount
3481
3648
  // between origIndexCount (visible) and 0 (hidden) for any RO
@@ -3493,6 +3660,18 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
3493
3660
  }
3494
3661
  activeDirty.clear();
3495
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
+ }
3496
3675
  // 1b. Atlas-texture aval reactivity: an `aval<ITexture>` that
3497
3676
  // drives an atlas placement was marked. Repack the pool entry
3498
3677
  // and rewrite the drawHeader fields of every (bucket, slot)