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