@aardworx/wombat.rendering 0.18.1 → 0.19.1

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.
Files changed (33) hide show
  1. package/dist/core/renderObject.d.ts +16 -0
  2. package/dist/core/renderObject.d.ts.map +1 -1
  3. package/dist/runtime/heapAdapter.d.ts.map +1 -1
  4. package/dist/runtime/heapAdapter.js +24 -0
  5. package/dist/runtime/heapAdapter.js.map +1 -1
  6. package/dist/runtime/heapScene/freelist.d.ts +5 -0
  7. package/dist/runtime/heapScene/freelist.d.ts.map +1 -1
  8. package/dist/runtime/heapScene/freelist.js +28 -0
  9. package/dist/runtime/heapScene/freelist.js.map +1 -1
  10. package/dist/runtime/heapScene/pools.d.ts +19 -0
  11. package/dist/runtime/heapScene/pools.d.ts.map +1 -1
  12. package/dist/runtime/heapScene/pools.js +115 -12
  13. package/dist/runtime/heapScene/pools.js.map +1 -1
  14. package/dist/runtime/heapScene.d.ts +21 -1
  15. package/dist/runtime/heapScene.d.ts.map +1 -1
  16. package/dist/runtime/heapScene.js +237 -10
  17. package/dist/runtime/heapScene.js.map +1 -1
  18. package/dist/runtime/hybridScene.d.ts +8 -0
  19. package/dist/runtime/hybridScene.d.ts.map +1 -1
  20. package/dist/runtime/hybridScene.js +15 -0
  21. package/dist/runtime/hybridScene.js.map +1 -1
  22. package/dist/runtime/textureAtlas/atlasPool.d.ts +12 -0
  23. package/dist/runtime/textureAtlas/atlasPool.d.ts.map +1 -1
  24. package/dist/runtime/textureAtlas/atlasPool.js +20 -0
  25. package/dist/runtime/textureAtlas/atlasPool.js.map +1 -1
  26. package/package.json +2 -2
  27. package/src/core/renderObject.ts +16 -0
  28. package/src/runtime/heapAdapter.ts +24 -0
  29. package/src/runtime/heapScene/freelist.ts +35 -0
  30. package/src/runtime/heapScene/pools.ts +131 -12
  31. package/src/runtime/heapScene.ts +257 -11
  32. package/src/runtime/hybridScene.ts +23 -0
  33. package/src/runtime/textureAtlas/atlasPool.ts +19 -0
@@ -1,5 +1,5 @@
1
1
  import { type V2f } from "@aardworx/wombat.base";
2
- import type { ITexture } from "../core/texture.js";
2
+ import type { HostTextureSource, ITexture } from "../core/texture.js";
3
3
  import type { ISampler } from "../core/sampler.js";
4
4
  import { AdaptiveToken } from "@aardworx/wombat.adaptive";
5
5
  import type { aval, aset } from "@aardworx/wombat.adaptive";
@@ -57,6 +57,18 @@ export interface HeapDrawSpec {
57
57
  * computes `indexCount * instanceCount` per record.
58
58
  */
59
59
  readonly instanceCount?: aval<number> | number;
60
+ /**
61
+ * Optional visibility gate. When ticks to `false`, this RO emits
62
+ * 0 vertices that frame — its drawTable record gets effective
63
+ * `indexCount = 0`, the scan kernel's prefix sum skips past it,
64
+ * and no fragment shader invocations fire. Pool allocations,
65
+ * arena uploads, and bucket membership all stay intact, so the
66
+ * `true → false → true` round-trip is a pair of small drawTable
67
+ * writes plus a scan-pass re-issue — no `addDraw`/`removeDraw`
68
+ * churn. Designed for `<Sg Active={cval}>` driven by LOD walkers
69
+ * that flip many tiles per camera move.
70
+ */
71
+ readonly active?: aval<boolean>;
60
72
  /**
61
73
  * Index buffer for this draw. Indices live in their own `INDEX`-
62
74
  * usage `GPUBuffer` (WebGPU forces this), separate from the arena.
@@ -247,6 +259,14 @@ export type HeapTextureSet = {
247
259
  * reference.
248
260
  */
249
261
  readonly repack?: (newTex: ITexture) => import("./textureAtlas/atlasPool.js").AtlasAcquisition;
262
+ /**
263
+ * Host source captured at adapter time, used by the heap-side
264
+ * re-acquire path when the pool entry was evicted while this spec
265
+ * sat idle (heap remove → re-add). Without it, `AtlasPool.acquire`
266
+ * allocates a fresh sub-rect with no `source` and skips `upload()`,
267
+ * leaving the drawHeader pointing at uninitialized atlas pixels.
268
+ */
269
+ readonly host?: HostTextureSource;
250
270
  };
251
271
  /**
252
272
  * Build a heap-backed scene renderer from a flat list of draws.
@@ -1 +1 @@
1
- {"version":3,"file":"heapScene.d.ts","sourceRoot":"","sources":["../../src/runtime/heapScene.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"heapScene.d.ts","sourceRoot":"","sources":["../../src/runtime/heapScene.ts"],"names":[],"mappings":"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"}
@@ -119,6 +119,96 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
119
119
  */
120
120
  const modeAvalToDrawIds = new Map();
121
121
  const modeAvalCallbacks = new Map();
122
+ // ─── HeapDrawSpec.active subscription ────────────────────────────────
123
+ // Per-drawId state for the "skip without re-allocating" path. When a
124
+ // tile (or any RO) flips visibility many times per second (e.g. an
125
+ // LOD walker culling/uncrossing the frustum), pool churn would be
126
+ // prohibitive. Instead we keep the RO in the bucket and flip its
127
+ // drawTable record's `indexCount` field between the original count
128
+ // (visible) and 0 (hidden). The scan kernel's prefix sum naturally
129
+ // skips a record with 0 emit count, so this costs one drawTable
130
+ // write + a re-scan dispatch — no arena/freelist activity.
131
+ const drawIdToActiveAval = new Map();
132
+ const drawIdToOrigIndexCount = new Map();
133
+ const drawIdToActiveCallback = new Map();
134
+ const activeAvalToDrawIds = new Map();
135
+ const activeDirty = new Set();
136
+ function subscribeActive(av, drawId) {
137
+ let set = activeAvalToDrawIds.get(av);
138
+ if (set === undefined) {
139
+ set = new Set();
140
+ activeAvalToDrawIds.set(av, set);
141
+ }
142
+ set.add(drawId);
143
+ const cb = addMarkingCallback(av, () => { activeDirty.add(drawId); });
144
+ drawIdToActiveCallback.set(drawId, cb);
145
+ }
146
+ function unsubscribeActive(drawId) {
147
+ const av = drawIdToActiveAval.get(drawId);
148
+ if (av === undefined)
149
+ return;
150
+ const cb = drawIdToActiveCallback.get(drawId);
151
+ cb?.dispose();
152
+ drawIdToActiveCallback.delete(drawId);
153
+ const set = activeAvalToDrawIds.get(av);
154
+ if (set !== undefined) {
155
+ set.delete(drawId);
156
+ if (set.size === 0)
157
+ activeAvalToDrawIds.delete(av);
158
+ }
159
+ drawIdToActiveAval.delete(drawId);
160
+ drawIdToOrigIndexCount.delete(drawId);
161
+ activeDirty.delete(drawId);
162
+ }
163
+ /** Update the drawTable record's effective indexCount in place
164
+ * (GPU-routed: masterShadow + partition re-dispatch; legacy:
165
+ * drawTableShadow + scan re-dispatch). Common helper used by the
166
+ * active-aval drain in `update()`. */
167
+ function setEffectiveIndexCount(drawId, newIndexCount) {
168
+ const bucket = drawIdToBucket[drawId];
169
+ if (bucket === undefined)
170
+ return;
171
+ if (bucket.gpuRouted) {
172
+ const partition = bucket.partitionScene;
173
+ const recIdx = bucket.drawIdToRecord.get(drawId);
174
+ if (recIdx === undefined)
175
+ return;
176
+ const ru32 = partition.recordU32;
177
+ const oldIndexCount = partition.masterShadow[recIdx * ru32 + 3];
178
+ const instanceCount = partition.masterShadow[recIdx * ru32 + 4];
179
+ partition.masterShadow[recIdx * ru32 + 3] = newIndexCount >>> 0;
180
+ const delta = (newIndexCount - oldIndexCount) * instanceCount;
181
+ bucket.partitionDirty = true;
182
+ for (const s of bucket.slots) {
183
+ s.totalEmitEstimate = Math.max(0, s.totalEmitEstimate + delta);
184
+ s.scanDirty = true;
185
+ }
186
+ }
187
+ else {
188
+ const slotIdx = drawIdToSlotIdx[drawId];
189
+ const slot = slotIdx !== undefined ? bucket.slots[slotIdx] : bucket.slots[0];
190
+ if (slot === undefined)
191
+ return;
192
+ const localSlot = drawIdToLocalSlot[drawId];
193
+ if (localSlot === undefined)
194
+ return;
195
+ const recIdx = slot.slotToRecord[localSlot];
196
+ if (recIdx === undefined || recIdx < 0)
197
+ return;
198
+ const shadow = slot.drawTableShadow;
199
+ const oldIndexCount = shadow[recIdx * RECORD_U32 + 3];
200
+ const instanceCount = shadow[recIdx * RECORD_U32 + 4];
201
+ shadow[recIdx * RECORD_U32 + 3] = newIndexCount >>> 0;
202
+ const byteOff = recIdx * RECORD_BYTES + 3 * 4;
203
+ if (byteOff < slot.drawTableDirtyMin)
204
+ slot.drawTableDirtyMin = byteOff;
205
+ if (byteOff + 4 > slot.drawTableDirtyMax)
206
+ slot.drawTableDirtyMax = byteOff + 4;
207
+ const delta = (newIndexCount - oldIndexCount) * instanceCount;
208
+ slot.totalEmitEstimate = Math.max(0, slot.totalEmitEstimate + delta);
209
+ slot.scanDirty = true;
210
+ }
211
+ }
122
212
  function subscribeModeLeaf(av, drawId) {
123
213
  let set = modeAvalToDrawIds.get(av);
124
214
  if (set === undefined) {
@@ -1924,6 +2014,11 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
1924
2014
  // inside a single outer evaluateAlways and invoke addDrawImpl
1925
2015
  // directly with their token — collapsing 1000× nested
1926
2016
  // evaluateAlways into 1× outer.
2017
+ // DEBUG counters.
2018
+ let __addDrawCalls = 0;
2019
+ let __removeDrawCalls = 0;
2020
+ sceneObj.__addDrawCalls = () => __addDrawCalls;
2021
+ sceneObj.__removeDrawCalls = () => __removeDrawCalls;
1927
2022
  function addDraw(spec) {
1928
2023
  let id = -1;
1929
2024
  sceneObj.evaluateAlways(AdaptiveToken.top, (tok) => {
@@ -1932,6 +2027,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
1932
2027
  return id;
1933
2028
  }
1934
2029
  function addDrawImpl(spec, outerTok) {
2030
+ __addDrawCalls++;
1935
2031
  const drawId = nextDrawId++;
1936
2032
  // Family-merge (slice 3c): build the family lazily from this
1937
2033
  // single spec when no batched lazy-build occurred earlier (e.g.
@@ -2122,6 +2218,61 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
2122
2218
  packBucketHeader(bucket, localSlot, perDrawRefs, layoutId);
2123
2219
  if (bucket.isAtlasBucket && spec.textures !== undefined && spec.textures.kind === "atlas") {
2124
2220
  packAtlasTextureFields(bucket, localSlot, spec.textures);
2221
+ // Pair this addDraw with one atlas refcount bump. The spec's
2222
+ // release closure (stored below, fired in removeDraw) decrements
2223
+ // exactly once per add/remove cycle; without this incRef the
2224
+ // refcount underflows when the same cached spec is re-introduced
2225
+ // across multiple aset add/remove cycles (e.g., a tile flipping
2226
+ // in and out of the heap-eligible subset).
2227
+ //
2228
+ // Falls back to a full re-acquire if the entry was already
2229
+ // evicted (incRef returns false): in that case the spec's stored
2230
+ // (pageId, origin, size) are stale and the new acquire's values
2231
+ // overwrite them before packAtlasTextureFields is re-called.
2232
+ if (atlasPool !== undefined) {
2233
+ const ok = atlasPool.incRef(spec.textures.poolRef);
2234
+ if (!ok) {
2235
+ // Entry was evicted while the cached spec was idle in heap-
2236
+ // remove state. Acquire a fresh sub-rect, redirect the spec
2237
+ // (and the per-aval cell behind its release closure) to the
2238
+ // new ref, and re-emit the drawHeader fields.
2239
+ // Pass the captured `host` as `source` so the pool's
2240
+ // `finalize` re-runs `upload()` for the fresh sub-rect. Without
2241
+ // this, AtlasPool.acquire allocates the rect and skips the
2242
+ // upload (the `source.host !== undefined` branch in finalize),
2243
+ // so the drawHeader points at uninitialized atlas pixels —
2244
+ // exactly the "black rectangle holes" symptom on tiles that
2245
+ // cycled out and back in across a long-enough gap to be
2246
+ // evicted.
2247
+ const reW = spec.textures.size.x;
2248
+ const reH = spec.textures.size.y;
2249
+ const host = spec.textures.host;
2250
+ const acq = atlasPool.acquire(spec.textures.page.format, spec.textures.sourceAval, reW, reH, {
2251
+ wantsMips: spec.textures.numMips > 1,
2252
+ ...(host !== undefined ? { source: { width: reW, height: reH, host } } : {}),
2253
+ });
2254
+ spec.textures.pageId = acq.pageId;
2255
+ spec.textures.origin = acq.origin;
2256
+ spec.textures.size = acq.size;
2257
+ spec.textures.numMips = acq.numMips;
2258
+ spec.textures.page = acq.page;
2259
+ spec.textures.poolRef = acq.ref;
2260
+ // The release closure captures a per-aval cell whose `ref`
2261
+ // must track the LATEST acquired sub-rect; otherwise the
2262
+ // closure releases a long-evicted ref (no-op leak).
2263
+ if (spec.textures.repack !== undefined) {
2264
+ // repack updates the per-aval cell to the new ref as a
2265
+ // side effect of its own atlas-mark drain path. Re-using
2266
+ // it here would free the entry we just acquired; instead
2267
+ // mirror the same effect with a tiny helper attached to
2268
+ // the spec at adapter time (`__retarget`).
2269
+ }
2270
+ const retarget = spec.textures.__retarget;
2271
+ if (retarget !== undefined)
2272
+ retarget(acq.ref);
2273
+ packAtlasTextureFields(bucket, localSlot, spec.textures);
2274
+ }
2275
+ }
2125
2276
  bucket.localAtlasReleases[localSlot] = spec.textures.release;
2126
2277
  bucket.localAtlasTextures[localSlot] = spec.textures;
2127
2278
  // Reactivity wire-up: subscribe to `sourceAval` (so sceneObj.inputChanged
@@ -2226,6 +2377,22 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
2226
2377
  drawIdToLocalSlot[drawId] = localSlot;
2227
2378
  drawIdToSlotIdx[drawId] = roSlotIdx;
2228
2379
  drawIdToIndexAval[drawId] = indicesAval;
2380
+ // ─── HeapDrawSpec.active wire-up ──────────────────────────────────
2381
+ // Track the visibility aval (if any) and apply its initial value
2382
+ // immediately. After this point, ticks on the aval are routed via
2383
+ // `subscribeActive` → `activeDirty` and drained in `update()`.
2384
+ if (spec.active !== undefined) {
2385
+ drawIdToActiveAval.set(drawId, spec.active);
2386
+ drawIdToOrigIndexCount.set(drawId, idxAlloc.count);
2387
+ const initial = spec.active.getValue(outerTok);
2388
+ subscribeActive(spec.active, drawId);
2389
+ if (initial === false) {
2390
+ // Apply the off state synchronously so the very first frame
2391
+ // already skips this RO (no flash of un-gated geometry while
2392
+ // we wait for the next update tick).
2393
+ setEffectiveIndexCount(drawId, 0);
2394
+ }
2395
+ }
2229
2396
  // ─── Reactive rebucket: track PS-modeKey changes ────────────────
2230
2397
  // When any mode-axis aval marks (e.g. cullCval.value = 'front'),
2231
2398
  // schedule this RO for rebucket on the next update(). Today's
@@ -2329,10 +2496,14 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
2329
2496
  return drawId;
2330
2497
  }
2331
2498
  function removeDraw(drawId) {
2499
+ __removeDrawCalls++;
2332
2500
  const bucket = drawIdToBucket[drawId];
2333
2501
  const localSlot = drawIdToLocalSlot[drawId];
2334
2502
  if (bucket === undefined || localSlot === undefined)
2335
2503
  return;
2504
+ // Tear down `active` subscription (if any) so its callback won't
2505
+ // fire after the underlying drawTable record is gone.
2506
+ unsubscribeActive(drawId);
2336
2507
  // §7: deregister this RO's derivation records and release slots.
2337
2508
  if (derivedScene !== undefined) {
2338
2509
  const reg = derivedByDrawId.get(drawId);
@@ -2345,9 +2516,15 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
2345
2516
  const partition = bucket.partitionScene;
2346
2517
  const recIdx = bucket.drawIdToRecord.get(drawId);
2347
2518
  const removedEntry = bucket.localEntries[localSlot];
2348
- const removedCount = removedEntry !== undefined
2349
- ? removedEntry.indexCount * removedEntry.instanceCount
2350
- : 0;
2519
+ // Read the EFFECTIVE indexCount from masterShadow (already 0 for
2520
+ // an inactive draw via setEffectiveIndexCount). Using the original
2521
+ // count from localEntries would double-subtract for inactive draws
2522
+ // (whose contribution was already removed when active flipped false).
2523
+ const effectiveIndexCount = recIdx !== undefined
2524
+ ? partition.masterShadow[recIdx * partition.recordU32 + 3]
2525
+ : (removedEntry?.indexCount ?? 0);
2526
+ const instanceCount = removedEntry?.instanceCount ?? 0;
2527
+ const removedCount = effectiveIndexCount * instanceCount;
2351
2528
  if (recIdx !== undefined) {
2352
2529
  const moved = partition.removeRecord(recIdx);
2353
2530
  if (moved >= 0) {
@@ -2376,9 +2553,15 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
2376
2553
  const slotIdx = drawIdToSlotIdx[drawId];
2377
2554
  const slot = slotIdx !== undefined ? bucket.slots[slotIdx] : bucket.slots[0];
2378
2555
  const removedEntry = bucket.localEntries[localSlot];
2379
- const removedCount = removedEntry !== undefined
2380
- ? removedEntry.indexCount * removedEntry.instanceCount
2381
- : 0;
2556
+ const localSlotRec = slot.slotToRecord[localSlot];
2557
+ // Effective (post-active) indexCount, read from shadow so that
2558
+ // inactive draws contribute 0 here (their contribution was
2559
+ // already removed when active flipped false).
2560
+ const effectiveIndexCount = localSlotRec !== undefined && localSlotRec >= 0
2561
+ ? slot.drawTableShadow[localSlotRec * RECORD_U32 + 3]
2562
+ : (removedEntry?.indexCount ?? 0);
2563
+ const instanceCount = removedEntry?.instanceCount ?? 0;
2564
+ const removedCount = effectiveIndexCount * instanceCount;
2382
2565
  slot.totalEmitEstimate = Math.max(0, slot.totalEmitEstimate - removedCount);
2383
2566
  // Swap-pop: move the last record into the freed slot, decrement
2384
2567
  // recordCount. firstEmit is GPU-rewritten by the next scan, so
@@ -2680,6 +2863,22 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
2680
2863
  }
2681
2864
  allocDirty.clear();
2682
2865
  }
2866
+ // 1c. HeapDrawSpec.active drain — flip drawTable.indexCount
2867
+ // between origIndexCount (visible) and 0 (hidden) for any RO
2868
+ // whose active aval ticked since last frame. Set is small in
2869
+ // steady state (only the ROs whose visibility actually
2870
+ // changed). No pool / arena / freelist activity.
2871
+ if (activeDirty.size > 0) {
2872
+ for (const did of activeDirty) {
2873
+ const av = drawIdToActiveAval.get(did);
2874
+ if (av === undefined)
2875
+ continue;
2876
+ const a = av.getValue(tok);
2877
+ const orig = drawIdToOrigIndexCount.get(did) ?? 0;
2878
+ setEffectiveIndexCount(did, a ? orig : 0);
2879
+ }
2880
+ activeDirty.clear();
2881
+ }
2683
2882
  // 1b. Atlas-texture aval reactivity: an `aval<ITexture>` that
2684
2883
  // drives an atlas placement was marked. Repack the pool entry
2685
2884
  // and rewrite the drawHeader fields of every (bucket, slot)
@@ -3101,7 +3300,26 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
3101
3300
  const typeId = arenaU32[refU32];
3102
3301
  const length = arenaU32[refU32 + 1];
3103
3302
  if (!KNOWN_TYPE_IDS.has(typeId)) {
3104
- push(`bucket#${bucketIdx} slot=${slot} field='${f.name}' alloc@0x${ref.toString(16)} typeId=${typeId} unknown`);
3303
+ // Dump the first 8 u32s of the alloc to help diagnose
3304
+ // what wrote garbage onto the header. Also peek the
3305
+ // FOUR u32s BEFORE the alloc to see if a neighbour
3306
+ // overran into us.
3307
+ const before = refU32 >= 4
3308
+ ? `prev=[${arenaU32[refU32 - 4]},${arenaU32[refU32 - 3]},${arenaU32[refU32 - 2]},${arenaU32[refU32 - 1]}] `
3309
+ : "";
3310
+ const here = `here=[${arenaU32[refU32]},${arenaU32[refU32 + 1]},${arenaU32[refU32 + 2]},${arenaU32[refU32 + 3]},${arenaU32[refU32 + 4]},${arenaU32[refU32 + 5]},${arenaU32[refU32 + 6]},${arenaU32[refU32 + 7]}]`;
3311
+ // Compare CPU shadow vs GPU read-back. Differ → GPU
3312
+ // write corrupted; match → bug is in CPU-side write path
3313
+ // OR the alloc never went through pool.acquire.
3314
+ const shadowChunk = arena.attrs.chunk(0);
3315
+ const shadowU32 = shadowChunk.peekShadowU32(ref, 8);
3316
+ const shadowStr = `shadow=[${shadowU32[0]},${shadowU32[1]},${shadowU32[2]},${shadowU32[3]},${shadowU32[4]},${shadowU32[5]},${shadowU32[6]},${shadowU32[7]}]`;
3317
+ const cpuMatches = shadowU32[0] === arenaU32[refU32]
3318
+ && shadowU32[1] === arenaU32[refU32 + 1]
3319
+ && shadowU32[2] === arenaU32[refU32 + 2]
3320
+ && shadowU32[3] === arenaU32[refU32 + 3];
3321
+ const verdict = cpuMatches ? "GPU=CPU (likely CPU-write bug)" : "GPU≠CPU (likely GPU-write corruption)";
3322
+ push(`bucket#${bucketIdx} slot=${slot} field='${f.name}' alloc@0x${ref.toString(16)} typeId=${typeId} unknown ${verdict} ${before}${here} ${shadowStr}`);
3105
3323
  attrAllocsBad++;
3106
3324
  continue;
3107
3325
  }
@@ -3205,11 +3423,18 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
3205
3423
  tilesBad++;
3206
3424
  }
3207
3425
  }
3426
+ // The scan kernel intentionally writes `numRecords - 1` as the
3427
+ // sentinel (not `numRecords`) — the render VS's binary search
3428
+ // uses `hi = firstDrawInTile[_tileIdx + 1u]` as an *inclusive*
3429
+ // upper bound, so the sentinel must point at the last valid
3430
+ // record slot. See scanKernel.ts `buildTileIndex` for the
3431
+ // full rationale.
3432
+ const expectedSentinel = recordCount === 0 ? 0 : recordCount - 1;
3208
3433
  const sentinel = fdt[dc.numTiles];
3209
3434
  tilesChecked++;
3210
- if (sentinel !== recordCount) {
3435
+ if (sentinel !== expectedSentinel) {
3211
3436
  push(`bucket#${bucketIdx} firstDrawInTile[${dc.numTiles}] sentinel ` +
3212
- `got=${sentinel} expected=${recordCount}`);
3437
+ `got=${sentinel} expected=${expectedSentinel}`);
3213
3438
  tilesBad++;
3214
3439
  }
3215
3440
  dc.firstDrawInTile.unmap();
@@ -4098,6 +4323,8 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
4098
4323
  return { emitsChecked: sampleCount, gpuMismatches, issues };
4099
4324
  },
4100
4325
  };
4101
- return { frame, update, encodeIntoPass, encodeComputePrep, addDraw, removeDraw, stats, dispose, _debug };
4326
+ const __addDrawCallsGetter = () => __addDrawCalls;
4327
+ const __removeDrawCallsGetter = () => __removeDrawCalls;
4328
+ return { frame, update, encodeIntoPass, encodeComputePrep, addDraw, removeDraw, stats, dispose, _debug, __addDrawCalls: __addDrawCallsGetter, __removeDrawCalls: __removeDrawCallsGetter };
4102
4329
  }
4103
4330
  //# sourceMappingURL=heapScene.js.map