@aardworx/wombat.rendering 0.18.1 → 0.19.0
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/dist/core/renderObject.d.ts +16 -0
- package/dist/core/renderObject.d.ts.map +1 -1
- package/dist/runtime/heapAdapter.d.ts.map +1 -1
- package/dist/runtime/heapAdapter.js +17 -0
- package/dist/runtime/heapAdapter.js.map +1 -1
- package/dist/runtime/heapScene/freelist.d.ts +5 -0
- package/dist/runtime/heapScene/freelist.d.ts.map +1 -1
- package/dist/runtime/heapScene/freelist.js +28 -0
- package/dist/runtime/heapScene/freelist.js.map +1 -1
- package/dist/runtime/heapScene/pools.d.ts +19 -0
- package/dist/runtime/heapScene/pools.d.ts.map +1 -1
- package/dist/runtime/heapScene/pools.js +115 -12
- package/dist/runtime/heapScene/pools.js.map +1 -1
- package/dist/runtime/heapScene.d.ts +12 -0
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +223 -10
- package/dist/runtime/heapScene.js.map +1 -1
- package/dist/runtime/hybridScene.d.ts +8 -0
- package/dist/runtime/hybridScene.d.ts.map +1 -1
- package/dist/runtime/hybridScene.js +15 -0
- package/dist/runtime/hybridScene.js.map +1 -1
- package/dist/runtime/textureAtlas/atlasPool.d.ts +12 -0
- package/dist/runtime/textureAtlas/atlasPool.d.ts.map +1 -1
- package/dist/runtime/textureAtlas/atlasPool.js +20 -0
- package/dist/runtime/textureAtlas/atlasPool.js.map +1 -1
- package/package.json +1 -1
- package/src/core/renderObject.ts +16 -0
- package/src/runtime/heapAdapter.ts +17 -0
- package/src/runtime/heapScene/freelist.ts +35 -0
- package/src/runtime/heapScene/pools.ts +131 -12
- package/src/runtime/heapScene.ts +234 -10
- package/src/runtime/hybridScene.ts +23 -0
- package/src/runtime/textureAtlas/atlasPool.ts +19 -0
package/src/runtime/heapScene.ts
CHANGED
|
@@ -438,6 +438,18 @@ export interface HeapDrawSpec {
|
|
|
438
438
|
* computes `indexCount * instanceCount` per record.
|
|
439
439
|
*/
|
|
440
440
|
readonly instanceCount?: aval<number> | number;
|
|
441
|
+
/**
|
|
442
|
+
* Optional visibility gate. When ticks to `false`, this RO emits
|
|
443
|
+
* 0 vertices that frame — its drawTable record gets effective
|
|
444
|
+
* `indexCount = 0`, the scan kernel's prefix sum skips past it,
|
|
445
|
+
* and no fragment shader invocations fire. Pool allocations,
|
|
446
|
+
* arena uploads, and bucket membership all stay intact, so the
|
|
447
|
+
* `true → false → true` round-trip is a pair of small drawTable
|
|
448
|
+
* writes plus a scan-pass re-issue — no `addDraw`/`removeDraw`
|
|
449
|
+
* churn. Designed for `<Sg Active={cval}>` driven by LOD walkers
|
|
450
|
+
* that flip many tiles per camera move.
|
|
451
|
+
*/
|
|
452
|
+
readonly active?: aval<boolean>;
|
|
441
453
|
/**
|
|
442
454
|
* Index buffer for this draw. Indices live in their own `INDEX`-
|
|
443
455
|
* usage `GPUBuffer` (WebGPU forces this), separate from the arena.
|
|
@@ -774,6 +786,90 @@ export function buildHeapScene(
|
|
|
774
786
|
const modeAvalToDrawIds = new Map<aval<unknown>, Set<number>>();
|
|
775
787
|
const modeAvalCallbacks = new Map<aval<unknown>, IDisposable>();
|
|
776
788
|
|
|
789
|
+
// ─── HeapDrawSpec.active subscription ────────────────────────────────
|
|
790
|
+
// Per-drawId state for the "skip without re-allocating" path. When a
|
|
791
|
+
// tile (or any RO) flips visibility many times per second (e.g. an
|
|
792
|
+
// LOD walker culling/uncrossing the frustum), pool churn would be
|
|
793
|
+
// prohibitive. Instead we keep the RO in the bucket and flip its
|
|
794
|
+
// drawTable record's `indexCount` field between the original count
|
|
795
|
+
// (visible) and 0 (hidden). The scan kernel's prefix sum naturally
|
|
796
|
+
// skips a record with 0 emit count, so this costs one drawTable
|
|
797
|
+
// write + a re-scan dispatch — no arena/freelist activity.
|
|
798
|
+
const drawIdToActiveAval = new Map<number, aval<boolean>>();
|
|
799
|
+
const drawIdToOrigIndexCount = new Map<number, number>();
|
|
800
|
+
const drawIdToActiveCallback = new Map<number, IDisposable>();
|
|
801
|
+
const activeAvalToDrawIds = new Map<aval<boolean>, Set<number>>();
|
|
802
|
+
const activeDirty = new Set<number>();
|
|
803
|
+
|
|
804
|
+
function subscribeActive(av: aval<boolean>, drawId: number): void {
|
|
805
|
+
let set = activeAvalToDrawIds.get(av);
|
|
806
|
+
if (set === undefined) {
|
|
807
|
+
set = new Set<number>();
|
|
808
|
+
activeAvalToDrawIds.set(av, set);
|
|
809
|
+
}
|
|
810
|
+
set.add(drawId);
|
|
811
|
+
const cb = addMarkingCallback(av, () => { activeDirty.add(drawId); });
|
|
812
|
+
drawIdToActiveCallback.set(drawId, cb);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function unsubscribeActive(drawId: number): void {
|
|
816
|
+
const av = drawIdToActiveAval.get(drawId);
|
|
817
|
+
if (av === undefined) return;
|
|
818
|
+
const cb = drawIdToActiveCallback.get(drawId);
|
|
819
|
+
cb?.dispose();
|
|
820
|
+
drawIdToActiveCallback.delete(drawId);
|
|
821
|
+
const set = activeAvalToDrawIds.get(av);
|
|
822
|
+
if (set !== undefined) {
|
|
823
|
+
set.delete(drawId);
|
|
824
|
+
if (set.size === 0) activeAvalToDrawIds.delete(av);
|
|
825
|
+
}
|
|
826
|
+
drawIdToActiveAval.delete(drawId);
|
|
827
|
+
drawIdToOrigIndexCount.delete(drawId);
|
|
828
|
+
activeDirty.delete(drawId);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/** Update the drawTable record's effective indexCount in place
|
|
832
|
+
* (GPU-routed: masterShadow + partition re-dispatch; legacy:
|
|
833
|
+
* drawTableShadow + scan re-dispatch). Common helper used by the
|
|
834
|
+
* active-aval drain in `update()`. */
|
|
835
|
+
function setEffectiveIndexCount(drawId: number, newIndexCount: number): void {
|
|
836
|
+
const bucket = drawIdToBucket[drawId];
|
|
837
|
+
if (bucket === undefined) return;
|
|
838
|
+
if (bucket.gpuRouted) {
|
|
839
|
+
const partition = bucket.partitionScene!;
|
|
840
|
+
const recIdx = bucket.drawIdToRecord!.get(drawId);
|
|
841
|
+
if (recIdx === undefined) return;
|
|
842
|
+
const ru32 = partition.recordU32;
|
|
843
|
+
const oldIndexCount = partition.masterShadow[recIdx * ru32 + 3]!;
|
|
844
|
+
const instanceCount = partition.masterShadow[recIdx * ru32 + 4]!;
|
|
845
|
+
partition.masterShadow[recIdx * ru32 + 3] = newIndexCount >>> 0;
|
|
846
|
+
const delta = (newIndexCount - oldIndexCount) * instanceCount;
|
|
847
|
+
bucket.partitionDirty = true;
|
|
848
|
+
for (const s of bucket.slots) {
|
|
849
|
+
s.totalEmitEstimate = Math.max(0, s.totalEmitEstimate + delta);
|
|
850
|
+
s.scanDirty = true;
|
|
851
|
+
}
|
|
852
|
+
} else {
|
|
853
|
+
const slotIdx = drawIdToSlotIdx[drawId];
|
|
854
|
+
const slot = slotIdx !== undefined ? bucket.slots[slotIdx] : bucket.slots[0];
|
|
855
|
+
if (slot === undefined) return;
|
|
856
|
+
const localSlot = drawIdToLocalSlot[drawId];
|
|
857
|
+
if (localSlot === undefined) return;
|
|
858
|
+
const recIdx = slot.slotToRecord[localSlot];
|
|
859
|
+
if (recIdx === undefined || recIdx < 0) return;
|
|
860
|
+
const shadow = slot.drawTableShadow!;
|
|
861
|
+
const oldIndexCount = shadow[recIdx * RECORD_U32 + 3]!;
|
|
862
|
+
const instanceCount = shadow[recIdx * RECORD_U32 + 4]!;
|
|
863
|
+
shadow[recIdx * RECORD_U32 + 3] = newIndexCount >>> 0;
|
|
864
|
+
const byteOff = recIdx * RECORD_BYTES + 3 * 4;
|
|
865
|
+
if (byteOff < slot.drawTableDirtyMin) slot.drawTableDirtyMin = byteOff;
|
|
866
|
+
if (byteOff + 4 > slot.drawTableDirtyMax) slot.drawTableDirtyMax = byteOff + 4;
|
|
867
|
+
const delta = (newIndexCount - oldIndexCount) * instanceCount;
|
|
868
|
+
slot.totalEmitEstimate = Math.max(0, slot.totalEmitEstimate + delta);
|
|
869
|
+
slot.scanDirty = true;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
777
873
|
function subscribeModeLeaf(av: aval<unknown>, drawId: number): void {
|
|
778
874
|
let set = modeAvalToDrawIds.get(av);
|
|
779
875
|
if (set === undefined) {
|
|
@@ -2785,6 +2881,11 @@ export function buildHeapScene(
|
|
|
2785
2881
|
// inside a single outer evaluateAlways and invoke addDrawImpl
|
|
2786
2882
|
// directly with their token — collapsing 1000× nested
|
|
2787
2883
|
// evaluateAlways into 1× outer.
|
|
2884
|
+
// DEBUG counters.
|
|
2885
|
+
let __addDrawCalls = 0;
|
|
2886
|
+
let __removeDrawCalls = 0;
|
|
2887
|
+
(sceneObj as unknown as { __addDrawCalls: () => number }).__addDrawCalls = () => __addDrawCalls;
|
|
2888
|
+
(sceneObj as unknown as { __removeDrawCalls: () => number }).__removeDrawCalls = () => __removeDrawCalls;
|
|
2788
2889
|
function addDraw(spec: HeapDrawSpec): number {
|
|
2789
2890
|
let id = -1;
|
|
2790
2891
|
sceneObj.evaluateAlways(AdaptiveToken.top, (tok) => {
|
|
@@ -2793,6 +2894,7 @@ export function buildHeapScene(
|
|
|
2793
2894
|
return id;
|
|
2794
2895
|
}
|
|
2795
2896
|
function addDrawImpl(spec: HeapDrawSpec, outerTok: AdaptiveToken): number {
|
|
2897
|
+
__addDrawCalls++;
|
|
2796
2898
|
const drawId = nextDrawId++;
|
|
2797
2899
|
// Family-merge (slice 3c): build the family lazily from this
|
|
2798
2900
|
// single spec when no batched lazy-build occurred earlier (e.g.
|
|
@@ -2991,6 +3093,51 @@ export function buildHeapScene(
|
|
|
2991
3093
|
packBucketHeader(bucket, localSlot, perDrawRefs, layoutId);
|
|
2992
3094
|
if (bucket.isAtlasBucket && spec.textures !== undefined && spec.textures.kind === "atlas") {
|
|
2993
3095
|
packAtlasTextureFields(bucket, localSlot, spec.textures);
|
|
3096
|
+
// Pair this addDraw with one atlas refcount bump. The spec's
|
|
3097
|
+
// release closure (stored below, fired in removeDraw) decrements
|
|
3098
|
+
// exactly once per add/remove cycle; without this incRef the
|
|
3099
|
+
// refcount underflows when the same cached spec is re-introduced
|
|
3100
|
+
// across multiple aset add/remove cycles (e.g., a tile flipping
|
|
3101
|
+
// in and out of the heap-eligible subset).
|
|
3102
|
+
//
|
|
3103
|
+
// Falls back to a full re-acquire if the entry was already
|
|
3104
|
+
// evicted (incRef returns false): in that case the spec's stored
|
|
3105
|
+
// (pageId, origin, size) are stale and the new acquire's values
|
|
3106
|
+
// overwrite them before packAtlasTextureFields is re-called.
|
|
3107
|
+
if (atlasPool !== undefined) {
|
|
3108
|
+
const ok = atlasPool.incRef(spec.textures.poolRef);
|
|
3109
|
+
if (!ok) {
|
|
3110
|
+
// Entry was evicted while the cached spec was idle in heap-
|
|
3111
|
+
// remove state. Acquire a fresh sub-rect, redirect the spec
|
|
3112
|
+
// (and the per-aval cell behind its release closure) to the
|
|
3113
|
+
// new ref, and re-emit the drawHeader fields.
|
|
3114
|
+
const acq = atlasPool.acquire(
|
|
3115
|
+
spec.textures.page.format,
|
|
3116
|
+
spec.textures.sourceAval as aval<ITexture>,
|
|
3117
|
+
spec.textures.size.x, spec.textures.size.y,
|
|
3118
|
+
{ wantsMips: spec.textures.numMips > 1 },
|
|
3119
|
+
);
|
|
3120
|
+
(spec.textures as { pageId: number }).pageId = acq.pageId;
|
|
3121
|
+
(spec.textures as { origin: V2f }).origin = acq.origin;
|
|
3122
|
+
(spec.textures as { size: V2f }).size = acq.size;
|
|
3123
|
+
(spec.textures as { numMips: number }).numMips = acq.numMips;
|
|
3124
|
+
(spec.textures as { page: AtlasPage }).page = acq.page;
|
|
3125
|
+
(spec.textures as { poolRef: number }).poolRef = acq.ref;
|
|
3126
|
+
// The release closure captures a per-aval cell whose `ref`
|
|
3127
|
+
// must track the LATEST acquired sub-rect; otherwise the
|
|
3128
|
+
// closure releases a long-evicted ref (no-op leak).
|
|
3129
|
+
if (spec.textures.repack !== undefined) {
|
|
3130
|
+
// repack updates the per-aval cell to the new ref as a
|
|
3131
|
+
// side effect of its own atlas-mark drain path. Re-using
|
|
3132
|
+
// it here would free the entry we just acquired; instead
|
|
3133
|
+
// mirror the same effect with a tiny helper attached to
|
|
3134
|
+
// the spec at adapter time (`__retarget`).
|
|
3135
|
+
}
|
|
3136
|
+
const retarget = (spec.textures as unknown as { __retarget?: (ref: number) => void }).__retarget;
|
|
3137
|
+
if (retarget !== undefined) retarget(acq.ref);
|
|
3138
|
+
packAtlasTextureFields(bucket, localSlot, spec.textures);
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
2994
3141
|
bucket.localAtlasReleases[localSlot] = spec.textures.release;
|
|
2995
3142
|
bucket.localAtlasTextures[localSlot] = spec.textures;
|
|
2996
3143
|
// Reactivity wire-up: subscribe to `sourceAval` (so sceneObj.inputChanged
|
|
@@ -3099,6 +3246,23 @@ export function buildHeapScene(
|
|
|
3099
3246
|
drawIdToSlotIdx[drawId] = roSlotIdx;
|
|
3100
3247
|
drawIdToIndexAval[drawId] = indicesAval;
|
|
3101
3248
|
|
|
3249
|
+
// ─── HeapDrawSpec.active wire-up ──────────────────────────────────
|
|
3250
|
+
// Track the visibility aval (if any) and apply its initial value
|
|
3251
|
+
// immediately. After this point, ticks on the aval are routed via
|
|
3252
|
+
// `subscribeActive` → `activeDirty` and drained in `update()`.
|
|
3253
|
+
if (spec.active !== undefined) {
|
|
3254
|
+
drawIdToActiveAval.set(drawId, spec.active);
|
|
3255
|
+
drawIdToOrigIndexCount.set(drawId, idxAlloc.count);
|
|
3256
|
+
const initial = spec.active.getValue(outerTok);
|
|
3257
|
+
subscribeActive(spec.active, drawId);
|
|
3258
|
+
if (initial === false) {
|
|
3259
|
+
// Apply the off state synchronously so the very first frame
|
|
3260
|
+
// already skips this RO (no flash of un-gated geometry while
|
|
3261
|
+
// we wait for the next update tick).
|
|
3262
|
+
setEffectiveIndexCount(drawId, 0);
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3102
3266
|
// ─── Reactive rebucket: track PS-modeKey changes ────────────────
|
|
3103
3267
|
// When any mode-axis aval marks (e.g. cullCval.value = 'front'),
|
|
3104
3268
|
// schedule this RO for rebucket on the next update(). Today's
|
|
@@ -3203,9 +3367,13 @@ export function buildHeapScene(
|
|
|
3203
3367
|
}
|
|
3204
3368
|
|
|
3205
3369
|
function removeDraw(drawId: number): void {
|
|
3370
|
+
__removeDrawCalls++;
|
|
3206
3371
|
const bucket = drawIdToBucket[drawId];
|
|
3207
3372
|
const localSlot = drawIdToLocalSlot[drawId];
|
|
3208
3373
|
if (bucket === undefined || localSlot === undefined) return;
|
|
3374
|
+
// Tear down `active` subscription (if any) so its callback won't
|
|
3375
|
+
// fire after the underlying drawTable record is gone.
|
|
3376
|
+
unsubscribeActive(drawId);
|
|
3209
3377
|
// §7: deregister this RO's derivation records and release slots.
|
|
3210
3378
|
if (derivedScene !== undefined) {
|
|
3211
3379
|
const reg = derivedByDrawId.get(drawId);
|
|
@@ -3218,9 +3386,15 @@ export function buildHeapScene(
|
|
|
3218
3386
|
const partition = bucket.partitionScene!;
|
|
3219
3387
|
const recIdx = bucket.drawIdToRecord!.get(drawId);
|
|
3220
3388
|
const removedEntry = bucket.localEntries[localSlot];
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3389
|
+
// Read the EFFECTIVE indexCount from masterShadow (already 0 for
|
|
3390
|
+
// an inactive draw via setEffectiveIndexCount). Using the original
|
|
3391
|
+
// count from localEntries would double-subtract for inactive draws
|
|
3392
|
+
// (whose contribution was already removed when active flipped false).
|
|
3393
|
+
const effectiveIndexCount = recIdx !== undefined
|
|
3394
|
+
? partition.masterShadow[recIdx * partition.recordU32 + 3]!
|
|
3395
|
+
: (removedEntry?.indexCount ?? 0);
|
|
3396
|
+
const instanceCount = removedEntry?.instanceCount ?? 0;
|
|
3397
|
+
const removedCount = effectiveIndexCount * instanceCount;
|
|
3224
3398
|
if (recIdx !== undefined) {
|
|
3225
3399
|
const moved = partition.removeRecord(recIdx);
|
|
3226
3400
|
if (moved >= 0) {
|
|
@@ -3247,9 +3421,15 @@ export function buildHeapScene(
|
|
|
3247
3421
|
const slotIdx = drawIdToSlotIdx[drawId];
|
|
3248
3422
|
const slot = slotIdx !== undefined ? bucket.slots[slotIdx]! : bucket.slots[0]!;
|
|
3249
3423
|
const removedEntry = bucket.localEntries[localSlot];
|
|
3250
|
-
const
|
|
3251
|
-
|
|
3252
|
-
|
|
3424
|
+
const localSlotRec = slot.slotToRecord[localSlot];
|
|
3425
|
+
// Effective (post-active) indexCount, read from shadow so that
|
|
3426
|
+
// inactive draws contribute 0 here (their contribution was
|
|
3427
|
+
// already removed when active flipped false).
|
|
3428
|
+
const effectiveIndexCount = localSlotRec !== undefined && localSlotRec >= 0
|
|
3429
|
+
? slot.drawTableShadow![localSlotRec * RECORD_U32 + 3]!
|
|
3430
|
+
: (removedEntry?.indexCount ?? 0);
|
|
3431
|
+
const instanceCount = removedEntry?.instanceCount ?? 0;
|
|
3432
|
+
const removedCount = effectiveIndexCount * instanceCount;
|
|
3253
3433
|
slot.totalEmitEstimate = Math.max(0, slot.totalEmitEstimate - removedCount);
|
|
3254
3434
|
// Swap-pop: move the last record into the freed slot, decrement
|
|
3255
3435
|
// recordCount. firstEmit is GPU-rewritten by the next scan, so
|
|
@@ -3545,6 +3725,22 @@ export function buildHeapScene(
|
|
|
3545
3725
|
allocDirty.clear();
|
|
3546
3726
|
}
|
|
3547
3727
|
|
|
3728
|
+
// 1c. HeapDrawSpec.active drain — flip drawTable.indexCount
|
|
3729
|
+
// between origIndexCount (visible) and 0 (hidden) for any RO
|
|
3730
|
+
// whose active aval ticked since last frame. Set is small in
|
|
3731
|
+
// steady state (only the ROs whose visibility actually
|
|
3732
|
+
// changed). No pool / arena / freelist activity.
|
|
3733
|
+
if (activeDirty.size > 0) {
|
|
3734
|
+
for (const did of activeDirty) {
|
|
3735
|
+
const av = drawIdToActiveAval.get(did);
|
|
3736
|
+
if (av === undefined) continue;
|
|
3737
|
+
const a = av.getValue(tok);
|
|
3738
|
+
const orig = drawIdToOrigIndexCount.get(did) ?? 0;
|
|
3739
|
+
setEffectiveIndexCount(did, a ? orig : 0);
|
|
3740
|
+
}
|
|
3741
|
+
activeDirty.clear();
|
|
3742
|
+
}
|
|
3743
|
+
|
|
3548
3744
|
// 1b. Atlas-texture aval reactivity: an `aval<ITexture>` that
|
|
3549
3745
|
// drives an atlas placement was marked. Repack the pool entry
|
|
3550
3746
|
// and rewrite the drawHeader fields of every (bucket, slot)
|
|
@@ -3994,7 +4190,26 @@ export function buildHeapScene(
|
|
|
3994
4190
|
const length = arenaU32[refU32 + 1]!;
|
|
3995
4191
|
|
|
3996
4192
|
if (!KNOWN_TYPE_IDS.has(typeId)) {
|
|
3997
|
-
|
|
4193
|
+
// Dump the first 8 u32s of the alloc to help diagnose
|
|
4194
|
+
// what wrote garbage onto the header. Also peek the
|
|
4195
|
+
// FOUR u32s BEFORE the alloc to see if a neighbour
|
|
4196
|
+
// overran into us.
|
|
4197
|
+
const before = refU32 >= 4
|
|
4198
|
+
? `prev=[${arenaU32[refU32 - 4]},${arenaU32[refU32 - 3]},${arenaU32[refU32 - 2]},${arenaU32[refU32 - 1]}] `
|
|
4199
|
+
: "";
|
|
4200
|
+
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]}]`;
|
|
4201
|
+
// Compare CPU shadow vs GPU read-back. Differ → GPU
|
|
4202
|
+
// write corrupted; match → bug is in CPU-side write path
|
|
4203
|
+
// OR the alloc never went through pool.acquire.
|
|
4204
|
+
const shadowChunk = arena.attrs.chunk(0);
|
|
4205
|
+
const shadowU32 = shadowChunk.peekShadowU32(ref, 8);
|
|
4206
|
+
const shadowStr = `shadow=[${shadowU32[0]},${shadowU32[1]},${shadowU32[2]},${shadowU32[3]},${shadowU32[4]},${shadowU32[5]},${shadowU32[6]},${shadowU32[7]}]`;
|
|
4207
|
+
const cpuMatches = shadowU32[0] === arenaU32[refU32]
|
|
4208
|
+
&& shadowU32[1] === arenaU32[refU32 + 1]
|
|
4209
|
+
&& shadowU32[2] === arenaU32[refU32 + 2]
|
|
4210
|
+
&& shadowU32[3] === arenaU32[refU32 + 3];
|
|
4211
|
+
const verdict = cpuMatches ? "GPU=CPU (likely CPU-write bug)" : "GPU≠CPU (likely GPU-write corruption)";
|
|
4212
|
+
push(`bucket#${bucketIdx} slot=${slot} field='${f.name}' alloc@0x${ref.toString(16)} typeId=${typeId} unknown ${verdict} ${before}${here} ${shadowStr}`);
|
|
3998
4213
|
attrAllocsBad++;
|
|
3999
4214
|
continue;
|
|
4000
4215
|
}
|
|
@@ -4102,11 +4317,18 @@ export function buildHeapScene(
|
|
|
4102
4317
|
tilesBad++;
|
|
4103
4318
|
}
|
|
4104
4319
|
}
|
|
4320
|
+
// The scan kernel intentionally writes `numRecords - 1` as the
|
|
4321
|
+
// sentinel (not `numRecords`) — the render VS's binary search
|
|
4322
|
+
// uses `hi = firstDrawInTile[_tileIdx + 1u]` as an *inclusive*
|
|
4323
|
+
// upper bound, so the sentinel must point at the last valid
|
|
4324
|
+
// record slot. See scanKernel.ts `buildTileIndex` for the
|
|
4325
|
+
// full rationale.
|
|
4326
|
+
const expectedSentinel = recordCount === 0 ? 0 : recordCount - 1;
|
|
4105
4327
|
const sentinel = fdt[dc.numTiles]!;
|
|
4106
4328
|
tilesChecked++;
|
|
4107
|
-
if (sentinel !==
|
|
4329
|
+
if (sentinel !== expectedSentinel) {
|
|
4108
4330
|
push(`bucket#${bucketIdx} firstDrawInTile[${dc.numTiles}] sentinel ` +
|
|
4109
|
-
`got=${sentinel} expected=${
|
|
4331
|
+
`got=${sentinel} expected=${expectedSentinel}`);
|
|
4110
4332
|
tilesBad++;
|
|
4111
4333
|
}
|
|
4112
4334
|
dc.firstDrawInTile.unmap();
|
|
@@ -5018,5 +5240,7 @@ export function buildHeapScene(
|
|
|
5018
5240
|
},
|
|
5019
5241
|
};
|
|
5020
5242
|
|
|
5021
|
-
|
|
5243
|
+
const __addDrawCallsGetter = (): number => __addDrawCalls;
|
|
5244
|
+
const __removeDrawCallsGetter = (): number => __removeDrawCalls;
|
|
5245
|
+
return { frame, update, encodeIntoPass, encodeComputePrep, addDraw, removeDraw, stats, dispose, _debug, __addDrawCalls: __addDrawCallsGetter, __removeDrawCalls: __removeDrawCallsGetter } as HeapScene;
|
|
5022
5246
|
}
|
|
@@ -124,6 +124,14 @@ export interface HybridScene {
|
|
|
124
124
|
* pipelineState. Useful for status / dev-overlay text.
|
|
125
125
|
*/
|
|
126
126
|
heapBucketCount(): number;
|
|
127
|
+
/** DEBUG: outstanding live ROs in the heap path. */
|
|
128
|
+
heapTotalDraws(): number;
|
|
129
|
+
/** DEBUG: cumulative addDraw invocations on the heap scene. */
|
|
130
|
+
__addDrawCalls(): number;
|
|
131
|
+
/** DEBUG: cumulative removeDraw invocations on the heap scene. */
|
|
132
|
+
__removeDrawCalls(): number;
|
|
133
|
+
/** DEBUG: current legacy-path RO count. */
|
|
134
|
+
__legacyCount(): number;
|
|
127
135
|
/** Per-frame breakdown of §7 derived-uniforms work (CPU). */
|
|
128
136
|
heapDerivedTimings(): {
|
|
129
137
|
pullMs: number; uploadMs: number; encodeMs: number; records: number;
|
|
@@ -211,6 +219,7 @@ export function compileHybridScene(
|
|
|
211
219
|
// comments) — the pool is wired here so the next PR can add Tier-S
|
|
212
220
|
// classification without touching this file.
|
|
213
221
|
const atlasPool = new AtlasPool(device);
|
|
222
|
+
(globalThis as { __atlasDebug?: AtlasPool }).__atlasDebug = atlasPool;
|
|
214
223
|
|
|
215
224
|
// ─── Heap subset → HeapDrawSpec aset ─────────────────────────────
|
|
216
225
|
// Memoize the adapter: aset removal must identify the SAME spec
|
|
@@ -268,6 +277,20 @@ export function compileHybridScene(
|
|
|
268
277
|
heapBucketCount(): number {
|
|
269
278
|
return heapScene.stats.groups;
|
|
270
279
|
},
|
|
280
|
+
heapTotalDraws(): number {
|
|
281
|
+
return heapScene.stats.totalDraws;
|
|
282
|
+
},
|
|
283
|
+
__addDrawCalls(): number {
|
|
284
|
+
const fn = (heapScene as unknown as { __addDrawCalls?: () => number }).__addDrawCalls;
|
|
285
|
+
return fn ? fn() : -1;
|
|
286
|
+
},
|
|
287
|
+
__removeDrawCalls(): number {
|
|
288
|
+
const fn = (heapScene as unknown as { __removeDrawCalls?: () => number }).__removeDrawCalls;
|
|
289
|
+
return fn ? fn() : -1;
|
|
290
|
+
},
|
|
291
|
+
__legacyCount(): number {
|
|
292
|
+
return scenePass.collect().length;
|
|
293
|
+
},
|
|
271
294
|
heapDerivedTimings() {
|
|
272
295
|
const s = heapScene.stats;
|
|
273
296
|
return {
|
|
@@ -611,6 +611,25 @@ export class AtlasPool {
|
|
|
611
611
|
this.lru.set(e.ref, e);
|
|
612
612
|
}
|
|
613
613
|
|
|
614
|
+
/**
|
|
615
|
+
* Bump refcount on an entry that's already live (used by the heap
|
|
616
|
+
* path when a cached HeapDrawSpec is re-introduced: the spec already
|
|
617
|
+
* carries `poolRef` from the original acquire, but every add/remove
|
|
618
|
+
* cycle fires the release closure once — without a matching incRef
|
|
619
|
+
* the refcount underflows and the entry gets actuallyFree'd while
|
|
620
|
+
* live drawHeaders still reference it).
|
|
621
|
+
*
|
|
622
|
+
* Returns `true` if the bump was applied; `false` if the entry has
|
|
623
|
+
* been evicted in the meantime (caller must fall back to re-acquire).
|
|
624
|
+
*/
|
|
625
|
+
incRef(ref: number): boolean {
|
|
626
|
+
const e = this.entriesByRef.get(ref);
|
|
627
|
+
if (e === undefined) return false;
|
|
628
|
+
if (e.refcount === 0) this.lru.delete(e.ref);
|
|
629
|
+
e.refcount++;
|
|
630
|
+
return true;
|
|
631
|
+
}
|
|
632
|
+
|
|
614
633
|
/**
|
|
615
634
|
* Drop an LRU entry for real: remove its packer slot, drop the entry
|
|
616
635
|
* from every lookup map. Caller must guarantee `refcount === 0`.
|