@aardworx/wombat.rendering 0.19.1 → 0.19.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aardworx/wombat.rendering",
3
- "version": "0.19.1",
3
+ "version": "0.19.3",
4
4
  "description": "WebGPU rendering layer for the Wombat TypeScript stack — RenderObject/RenderTask/runtime + window glue, port of Aardvark.Rendering's lower layers on top of WebGPU and wombat.shader.",
5
5
  "license": "MIT",
6
6
  "author": "krauthaufen",
@@ -56,7 +56,7 @@
56
56
  ],
57
57
  "dependencies": {
58
58
  "@aardworx/wombat.adaptive": "^0.2.4",
59
- "@aardworx/wombat.base": "^0.4.3",
59
+ "@aardworx/wombat.base": "^0.4.4",
60
60
  "@aardworx/wombat.shader": "^0.5.9"
61
61
  },
62
62
  "devDependencies": {
@@ -357,6 +357,7 @@ export class PreparedRenderObject {
357
357
  private readonly drawCall: aval<import("../core/index.js").DrawCall>,
358
358
  pipelineCtx: PipelineBuildContext,
359
359
  pipelineState: import("../core/index.js").PipelineState,
360
+ private readonly active: aval<boolean> | undefined = undefined,
360
361
  ) {
361
362
  this.groups = groups;
362
363
  this._pipelineCtx = pipelineCtx;
@@ -398,6 +399,12 @@ export class PreparedRenderObject {
398
399
  }
399
400
 
400
401
  record(pass: GPURenderPassEncoder, token: AdaptiveToken): void {
402
+ // Active gate: a reactive `RenderObject.active` flipping to false
403
+ // skips the draw entirely (legacy/standalone path's equivalent of
404
+ // the heap path's indexCount→0). Subscribed via getValue so the
405
+ // task re-records when it flips back. Without this, `<Sg Active=…>`
406
+ // is a no-op on the non-heap path and gated subtrees keep drawing.
407
+ if (this.active !== undefined && !this.active.getValue(token)) return;
401
408
  // Resolve current pipeline. If `update` was already called by the
402
409
  // walker this is a no-op cache hit; otherwise we still pick the
403
410
  // right pipeline for the token-evaluated values.
@@ -741,6 +748,7 @@ export function prepareRenderObject(
741
748
  obj.drawCall,
742
749
  pipelineCtx,
743
750
  obj.pipelineState,
751
+ obj.active,
744
752
  );
745
753
  }
746
754
 
@@ -234,6 +234,25 @@ export function isHeapEligible(ro: RenderObject): aval<boolean> {
234
234
  if (dc.baseVertex !== 0) return false;
235
235
  if (dc.firstIndex !== 0) return false;
236
236
  if (dc.firstInstance !== 0) return false;
237
+ // Whole-buffer wedge: the heap ingests the ENTIRE index + vertex
238
+ // buffers for an RO (IndexPool/AttributeArena copy `arr.length`,
239
+ // not the drawCall slice). An RO that consumes only a PREFIX of a
240
+ // shared index buffer (firstIndex=0 but indexCount < bufferCount —
241
+ // e.g. one glyph in a multi-glyph text run whose glyphs share a
242
+ // single atlas index buffer) would therefore drag the rest of the
243
+ // buffer into its draw: the trailing indices reference vertices
244
+ // past this RO's range (including the next sub-mesh's / this
245
+ // glyph's band vertices), painting stray triangles. Until the heap
246
+ // honours the drawCall slice, only ROs that own their whole index
247
+ // buffer are eligible. Single-mesh ROs (indexCount === bufferCount)
248
+ // pass; shared-atlas slices fall to the legacy ScenePass.
249
+ if (ro.indices !== undefined) {
250
+ const ib = ro.indices.buffer.getValue(token);
251
+ if (ib.kind === "host") {
252
+ const idxBufCount = ib.sizeBytes >>> 2; // u32 indices
253
+ if (dc.indexCount !== idxBufCount) return false;
254
+ }
255
+ }
237
256
  return true;
238
257
  });
239
258
  }
@@ -2398,12 +2398,24 @@ export function buildHeapScene(
2398
2398
  `has no captured descriptor — bucket promotion cannot synthesise a migration combo.`,
2399
2399
  );
2400
2400
  }
2401
+ // Read EFFECTIVE indexCount from the CPU slot's drawTable shadow
2402
+ // — for an active=false draw this is 0, so the new GPU master
2403
+ // record must carry the 0, not the original entry.indexCount.
2404
+ // Otherwise the CPU→GPU bucket promotion silently resurrects
2405
+ // every hidden draw.
2406
+ const cpuRecIdx = slot !== undefined ? slot.slotToRecord[ls] : undefined;
2407
+ const effIc = (cpuRecIdx !== undefined && cpuRecIdx >= 0 && slot !== undefined)
2408
+ ? slot.drawTableShadow![cpuRecIdx * RECORD_U32 + 3]!
2409
+ : e.indexCount;
2410
+ const effInst = (cpuRecIdx !== undefined && cpuRecIdx >= 0 && slot !== undefined)
2411
+ ? slot.drawTableShadow![cpuRecIdx * RECORD_U32 + 4]!
2412
+ : e.instanceCount;
2401
2413
  cpuMigration.push({
2402
2414
  localSlot: ls,
2403
2415
  drawId,
2404
2416
  firstIndex: e.firstIndex,
2405
- indexCount: e.indexCount,
2406
- instanceCount: e.instanceCount,
2417
+ indexCount: effIc,
2418
+ instanceCount: effInst,
2407
2419
  perDrawRefs: bucket.localPerDrawRefs[ls]!,
2408
2420
  descriptor,
2409
2421
  targetComboId: 0,
@@ -3682,10 +3694,17 @@ export function buildHeapScene(
3682
3694
  // Swap-pop from old slot's drawTable.
3683
3695
  const oldSlot = bucket.slots[oldSlotIdx]!;
3684
3696
  const entry = bucket.localEntries[localSlot];
3685
- const emit = entry !== undefined ? entry.indexCount * entry.instanceCount : 0;
3686
3697
  const oldRecIdx = oldSlot.slotToRecord[localSlot]!;
3687
3698
  const lastRecIdx = oldSlot.recordCount - 1;
3688
3699
  const oldShadow = oldSlot.drawTableShadow!;
3700
+ // Read the EFFECTIVE indexCount from the old slot's drawTable
3701
+ // shadow — for an active=false draw this is already 0, so the
3702
+ // emit accounting on the OLD slot must NOT subtract the
3703
+ // original (entry.indexCount) which would double-decrement and
3704
+ // also pin the carried-over count below.
3705
+ const effectiveIndexCount = oldShadow[oldRecIdx * RECORD_U32 + 3]!;
3706
+ const effectiveInstanceCount = oldShadow[oldRecIdx * RECORD_U32 + 4]!;
3707
+ const emit = effectiveIndexCount * effectiveInstanceCount;
3689
3708
  if (oldRecIdx !== lastRecIdx) {
3690
3709
  const dst = oldRecIdx * RECORD_U32;
3691
3710
  const src = lastRecIdx * RECORD_U32;
@@ -3720,8 +3739,13 @@ export function buildHeapScene(
3720
3739
  newShadow[newRecIdx * RECORD_U32 + 0] = 0;
3721
3740
  newShadow[newRecIdx * RECORD_U32 + 1] = localSlot;
3722
3741
  newShadow[newRecIdx * RECORD_U32 + 2] = entry !== undefined ? entry.firstIndex : 0;
3723
- newShadow[newRecIdx * RECORD_U32 + 3] = entry !== undefined ? entry.indexCount : 0;
3724
- newShadow[newRecIdx * RECORD_U32 + 4] = entry !== undefined ? entry.instanceCount : 0;
3742
+ // Carry over the EFFECTIVE indexCount (read from oldShadow
3743
+ // above) so an active=false draw remains hidden after the
3744
+ // reslot. Using entry.indexCount here was the bug: it always
3745
+ // wrote the ORIGINAL count, resurrecting hidden draws every
3746
+ // time a mode-key flip moved them between slots.
3747
+ newShadow[newRecIdx * RECORD_U32 + 3] = effectiveIndexCount;
3748
+ newShadow[newRecIdx * RECORD_U32 + 4] = effectiveInstanceCount;
3725
3749
  newSlot.recordCount = newRecIdx + 1;
3726
3750
  newSlot.slotToRecord[localSlot] = newRecIdx;
3727
3751
  newSlot.recordToSlot[newRecIdx] = localSlot;