@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.
@@ -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)