@aardworx/wombat.rendering 0.9.27 → 0.9.29
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/runtime/heapScene.d.ts +5 -0
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +326 -306
- package/dist/runtime/heapScene.js.map +1 -1
- package/package.json +1 -1
- package/src/runtime/heapScene.ts +352 -323
|
@@ -88,6 +88,10 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
88
88
|
// ─── Per-draw global bookkeeping (sparse, indexed by drawId) ──────
|
|
89
89
|
const drawIdToBucket = [];
|
|
90
90
|
const drawIdToLocalSlot = [];
|
|
91
|
+
/** Per-draw slot index within its bucket. Phase 5c.2: each bucket
|
|
92
|
+
* holds multiple BucketSlots (one per realized modeKey); this map
|
|
93
|
+
* tells each removeDraw / record write which slot to touch. */
|
|
94
|
+
const drawIdToSlotIdx = [];
|
|
91
95
|
/** Per-draw index aval — for `indexPool.release` on removeDraw. */
|
|
92
96
|
const drawIdToIndexAval = [];
|
|
93
97
|
/**
|
|
@@ -519,19 +523,20 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
519
523
|
compute: { module: scanModule, entryPoint: "buildTileIndex" },
|
|
520
524
|
});
|
|
521
525
|
}
|
|
522
|
-
function buildScanBindGroup(bucket) {
|
|
526
|
+
function buildScanBindGroup(bucket, slotIdx = 0) {
|
|
523
527
|
if (scanBgl === undefined)
|
|
524
528
|
throw new Error("heapScene: scan BGL missing");
|
|
529
|
+
const s = bucket.slots[slotIdx];
|
|
525
530
|
return device.createBindGroup({
|
|
526
|
-
label: `heapScene/${bucket.label}/scanBg`,
|
|
531
|
+
label: `heapScene/${bucket.label}/slot${slotIdx}/scanBg`,
|
|
527
532
|
layout: scanBgl,
|
|
528
533
|
entries: [
|
|
529
|
-
{ binding: 0, resource: { buffer:
|
|
530
|
-
{ binding: 1, resource: { buffer:
|
|
531
|
-
{ binding: 2, resource: { buffer:
|
|
532
|
-
{ binding: 3, resource: { buffer:
|
|
533
|
-
{ binding: 4, resource: { buffer:
|
|
534
|
-
{ binding: 5, resource: { buffer:
|
|
534
|
+
{ binding: 0, resource: { buffer: s.drawTableBuf.buffer } },
|
|
535
|
+
{ binding: 1, resource: { buffer: s.blockSumsBuf.buffer } },
|
|
536
|
+
{ binding: 2, resource: { buffer: s.blockOffsetsBuf.buffer } },
|
|
537
|
+
{ binding: 3, resource: { buffer: s.indirectBuf } },
|
|
538
|
+
{ binding: 4, resource: { buffer: s.paramsBuf } },
|
|
539
|
+
{ binding: 5, resource: { buffer: s.firstDrawInTileBuf.buffer } },
|
|
535
540
|
],
|
|
536
541
|
});
|
|
537
542
|
}
|
|
@@ -797,7 +802,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
797
802
|
// pipelines via noTexBgl/texBgl; the bind-group itself binds this
|
|
798
803
|
// bucket's drawHeap (binding 1) + its globals UBO (binding 0) +
|
|
799
804
|
// the global arena's heapF32 view (binding 2) + textures if any.
|
|
800
|
-
function buildBucketBindGroup(bucket) {
|
|
805
|
+
function buildBucketBindGroup(bucket, slotIdx = 0) {
|
|
801
806
|
// heapU32 / heapF32 / heapV4f are different typed views of the
|
|
802
807
|
// SAME global arena GPUBuffer (emscripten-style aliasing). The
|
|
803
808
|
// WGSL prelude declares one binding per view; the shader picks
|
|
@@ -809,7 +814,8 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
809
814
|
{ binding: 3, resource: { buffer: arena.attrs.buffer } }, // heapV4f
|
|
810
815
|
];
|
|
811
816
|
{
|
|
812
|
-
|
|
817
|
+
const s = bucket.slots[slotIdx];
|
|
818
|
+
if (s.drawTableBuf === undefined) {
|
|
813
819
|
throw new Error("heapScene: megacall bucket without drawTableBuf");
|
|
814
820
|
}
|
|
815
821
|
// Bind drawTable with size = recordCount * RECORD_BYTES so the VS
|
|
@@ -817,9 +823,9 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
817
823
|
// the live record count — keeps stale tail entries out of the
|
|
818
824
|
// binary search. Minimum one zero-record to satisfy WebGPU non-
|
|
819
825
|
// zero size constraint when the bucket is empty.
|
|
820
|
-
const dtBytes = Math.max(RECORD_BYTES,
|
|
821
|
-
entries.push({ binding: 4, resource: { buffer:
|
|
822
|
-
|
|
826
|
+
const dtBytes = Math.max(RECORD_BYTES, s.recordCount * RECORD_BYTES);
|
|
827
|
+
entries.push({ binding: 4, resource: { buffer: s.drawTableBuf.buffer, offset: 0, size: dtBytes } }, { binding: 5, resource: { buffer: arena.indices.buffer } }, { binding: 6, resource: { buffer: s.firstDrawInTileBuf.buffer } });
|
|
828
|
+
s.renderBoundRecordCount = s.recordCount;
|
|
823
829
|
}
|
|
824
830
|
// Schema-driven texture + sampler entries. v1 user surface still
|
|
825
831
|
// accepts a single (texture, sampler) pair via HeapTextureSet —
|
|
@@ -902,36 +908,29 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
902
908
|
// `atlasPool.pagesFor(format)` when building bind groups. Buckets
|
|
903
909
|
// carry strong refcount handles (`localAtlasReleases`) that drive
|
|
904
910
|
// `pool.release` on removeDraw.
|
|
905
|
-
|
|
906
|
-
|
|
911
|
+
/** Rebuild every slot's render bind group across every bucket.
|
|
912
|
+
* Used when a global resource (arena.attrs, arena.indices, atlas
|
|
913
|
+
* page set) reallocates and invalidates every bind group at once. */
|
|
914
|
+
function rebuildAllBindGroups(filterAtlasOnly = false) {
|
|
915
|
+
for (const b of buckets) {
|
|
916
|
+
if (filterAtlasOnly && !b.isAtlasBucket)
|
|
917
|
+
continue;
|
|
918
|
+
for (let i = 0; i < b.slots.length; i++) {
|
|
919
|
+
b.slots[i].bindGroup = buildBucketBindGroup(b, i);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
907
923
|
arena.attrs.onResize(() => {
|
|
908
|
-
|
|
909
|
-
b.bindGroup = buildBucketBindGroup(b);
|
|
910
|
-
// §7 dispatcher targets arena.attrs.buffer; rebind on grow.
|
|
924
|
+
rebuildAllBindGroups();
|
|
911
925
|
if (derivedScene !== undefined)
|
|
912
926
|
derivedScene.rebindMainHeap(arena.attrs.buffer);
|
|
913
927
|
});
|
|
914
|
-
// Same when the atlas pool allocates a fresh page — its slot in the
|
|
915
|
-
// BGL ladder transitions from placeholder to the real GPUTexture.
|
|
916
|
-
// Every atlas bucket rebuilds (cheap; just N+N+1 entries each).
|
|
917
928
|
if (atlasPool !== undefined) {
|
|
918
929
|
for (const f of ATLAS_PAGE_FORMATS) {
|
|
919
|
-
atlasPool.onPageAdded(f, () =>
|
|
920
|
-
for (const b of buckets) {
|
|
921
|
-
if (b.isAtlasBucket)
|
|
922
|
-
b.bindGroup = buildBucketBindGroup(b);
|
|
923
|
-
}
|
|
924
|
-
});
|
|
930
|
+
atlasPool.onPageAdded(f, () => rebuildAllBindGroups(true));
|
|
925
931
|
}
|
|
926
932
|
}
|
|
927
|
-
|
|
928
|
-
// also invalidates every bucket's bind group.
|
|
929
|
-
{
|
|
930
|
-
arena.indices.onResize(() => {
|
|
931
|
-
for (const b of buckets)
|
|
932
|
-
b.bindGroup = buildBucketBindGroup(b);
|
|
933
|
-
});
|
|
934
|
-
}
|
|
933
|
+
arena.indices.onResize(() => rebuildAllBindGroups());
|
|
935
934
|
// ─── findOrCreateBucket ───────────────────────────────────────────
|
|
936
935
|
// Slice 3c: the bucket key collapses to (familyId, pipelineState).
|
|
937
936
|
// Every member effect in the family shares one bucket per
|
|
@@ -939,6 +938,75 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
939
938
|
// right per-effect helper at draw time. Atlas-binding shape follows
|
|
940
939
|
// from `family.drawHeaderUnion.atlasTextureBindings.size > 0`.
|
|
941
940
|
void texIdOf; // retained for future per-bucket diagnostics
|
|
941
|
+
/**
|
|
942
|
+
* Allocate a brand-new BucketSlot inside `bucket` covering
|
|
943
|
+
* `descriptor` (modeKey = encode(descriptor)). Builds the per-slot
|
|
944
|
+
* GPU resources: pipeline (via cache), drawTable + scan buffers +
|
|
945
|
+
* indirect + paramsBuf, and the slot's render/scan bind groups
|
|
946
|
+
* (the latter rebuilt on resize). The bucket's drawHeap +
|
|
947
|
+
* texture bindings are shared across slots.
|
|
948
|
+
*/
|
|
949
|
+
function createSlot(bucket, descriptor) {
|
|
950
|
+
const fam = familyForBucket.get(bucket);
|
|
951
|
+
const pipeline = getOrCreatePipeline(fam, bucket.layout, bucket.isAtlasBucket, descriptor);
|
|
952
|
+
const modeKey = encodeModeKey(descriptor);
|
|
953
|
+
const slotIdx = bucket.slots.length;
|
|
954
|
+
const slotLabel = `${bucket.label}/slot${slotIdx}`;
|
|
955
|
+
const dtBuf = new GrowBuffer(device, `heapScene/${slotLabel}/drawTable`, GPUBufferUsage.STORAGE, 1024);
|
|
956
|
+
const blockSumsBuf = new GrowBuffer(device, `heapScene/${slotLabel}/blockSums`, GPUBufferUsage.STORAGE, 4 * Math.max(1, Math.ceil((dtBuf.capacity / RECORD_BYTES) / SCAN_TILE_SIZE)));
|
|
957
|
+
const blockOffsetsBuf = new GrowBuffer(device, `heapScene/${slotLabel}/blockOffsets`, GPUBufferUsage.STORAGE, 4 * Math.max(1, Math.ceil((dtBuf.capacity / RECORD_BYTES) / SCAN_TILE_SIZE)));
|
|
958
|
+
const firstDrawInTileBuf = new GrowBuffer(device, `heapScene/${slotLabel}/firstDrawInTile`, GPUBufferUsage.STORAGE, 128);
|
|
959
|
+
const indirectBuf = device.createBuffer({
|
|
960
|
+
label: `heapScene/${slotLabel}/indirect`, size: 16,
|
|
961
|
+
usage: GPUBufferUsage.INDIRECT | GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
|
|
962
|
+
});
|
|
963
|
+
device.queue.writeBuffer(indirectBuf, 0, new Uint32Array([0, 1, 0, 0]));
|
|
964
|
+
const paramsBuf = device.createBuffer({
|
|
965
|
+
label: `heapScene/${slotLabel}/params`, size: 16,
|
|
966
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
967
|
+
});
|
|
968
|
+
const slot = {
|
|
969
|
+
pipeline, modeKey,
|
|
970
|
+
drawTableDirtyMin: Infinity, drawTableDirtyMax: 0,
|
|
971
|
+
recordCount: 0, slotToRecord: [], recordToSlot: [],
|
|
972
|
+
totalEmitEstimate: 0, scanDirty: false,
|
|
973
|
+
drawTableBuf: dtBuf,
|
|
974
|
+
drawTableShadow: new Uint32Array(dtBuf.capacity / 4),
|
|
975
|
+
blockSumsBuf, blockOffsetsBuf, firstDrawInTileBuf,
|
|
976
|
+
indirectBuf, paramsBuf,
|
|
977
|
+
};
|
|
978
|
+
bucket.slots.push(slot);
|
|
979
|
+
const thisSlotIdx = bucket.slots.length - 1;
|
|
980
|
+
const ensureScanBuffers = () => {
|
|
981
|
+
const needBlocks = Math.max(1, Math.ceil(slot.recordCount / SCAN_TILE_SIZE));
|
|
982
|
+
blockSumsBuf.ensureCapacity(needBlocks * 4);
|
|
983
|
+
blockOffsetsBuf.ensureCapacity(needBlocks * 4);
|
|
984
|
+
};
|
|
985
|
+
const rebuildScanBg = () => {
|
|
986
|
+
slot.scanBindGroup = buildScanBindGroup(bucket, thisSlotIdx);
|
|
987
|
+
};
|
|
988
|
+
dtBuf.onResize(() => {
|
|
989
|
+
const grown = new Uint32Array(dtBuf.capacity / 4);
|
|
990
|
+
grown.set(slot.drawTableShadow);
|
|
991
|
+
slot.drawTableShadow = grown;
|
|
992
|
+
ensureScanBuffers();
|
|
993
|
+
slot.bindGroup = buildBucketBindGroup(bucket, thisSlotIdx);
|
|
994
|
+
rebuildScanBg();
|
|
995
|
+
slot.drawTableDirtyMin = 0;
|
|
996
|
+
slot.drawTableDirtyMax = slot.recordCount * RECORD_BYTES;
|
|
997
|
+
});
|
|
998
|
+
blockSumsBuf.onResize(rebuildScanBg);
|
|
999
|
+
blockOffsetsBuf.onResize(rebuildScanBg);
|
|
1000
|
+
firstDrawInTileBuf.onResize(() => {
|
|
1001
|
+
rebuildScanBg();
|
|
1002
|
+
slot.bindGroup = buildBucketBindGroup(bucket, thisSlotIdx);
|
|
1003
|
+
});
|
|
1004
|
+
slot.scanBindGroup = buildScanBindGroup(bucket, thisSlotIdx);
|
|
1005
|
+
slot.bindGroup = buildBucketBindGroup(bucket, thisSlotIdx);
|
|
1006
|
+
return slot;
|
|
1007
|
+
}
|
|
1008
|
+
/** Side map from Bucket → family. Populated by findOrCreateBucket. */
|
|
1009
|
+
const familyForBucket = new WeakMap();
|
|
942
1010
|
function findOrCreateBucket(effect, _textures, pipelineState,
|
|
943
1011
|
/**
|
|
944
1012
|
* Optional precomputed descriptor. addDrawImpl passes this when
|
|
@@ -951,62 +1019,23 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
951
1019
|
throw new Error("heapScene: findOrCreateBucket called before family build");
|
|
952
1020
|
}
|
|
953
1021
|
const fam = familyFor(effect);
|
|
954
|
-
//
|
|
955
|
-
//
|
|
956
|
-
//
|
|
957
|
-
//
|
|
958
|
-
//
|
|
959
|
-
|
|
960
|
-
// cvals collapse to ONE bucket. See docs/derived-modes.md and
|
|
961
|
-
// tests/heap-multi-pipeline-bucket.test.ts.
|
|
962
|
-
const psDescriptor = precomputedDescriptor ?? snapshotDescriptor(pipelineState, sig);
|
|
963
|
-
const psModeKey = encodeModeKey(psDescriptor);
|
|
964
|
-
const bk = `family#${fam.schema.id}|mk#${psModeKey.toString(16)}`;
|
|
1022
|
+
// Phase 5c.2: bucket key drops modeKey. All ROs with the same
|
|
1023
|
+
// (effect, textureSet) share one bucket; per-modeKey slots live
|
|
1024
|
+
// inside the bucket and route records via `ensureSlot`. Pipeline
|
|
1025
|
+
// pre-warm + the scene-level pipelineByModeKey cache mean every
|
|
1026
|
+
// realized slot lookup hits the cache.
|
|
1027
|
+
const bk = `family#${fam.schema.id}`;
|
|
965
1028
|
const existing = bucketByKey.get(bk);
|
|
966
1029
|
if (existing !== undefined)
|
|
967
1030
|
return existing;
|
|
968
|
-
// If a precomputed descriptor is supplied (rule-evaluated path),
|
|
969
|
-
// use its post-rule axis values so the pipeline reflects what the
|
|
970
|
-
// RO actually wants. Otherwise fall through to PS-aval forcing.
|
|
971
|
-
const ps = precomputedDescriptor !== undefined
|
|
972
|
-
? {
|
|
973
|
-
topology: precomputedDescriptor.topology,
|
|
974
|
-
cullMode: precomputedDescriptor.cullMode,
|
|
975
|
-
frontFace: precomputedDescriptor.frontFace,
|
|
976
|
-
...(precomputedDescriptor.depth !== undefined
|
|
977
|
-
? { depth: { write: precomputedDescriptor.depth.write, compare: precomputedDescriptor.depth.compare } }
|
|
978
|
-
: {}),
|
|
979
|
-
}
|
|
980
|
-
: resolvePipelineState(pipelineState);
|
|
981
1031
|
const layout = fam.schema.drawHeaderUnion;
|
|
982
1032
|
const isAtlasBucket = layout.atlasTextureBindings.size > 0;
|
|
983
|
-
// Pipeline lookup-or-create through the scene-level cache so
|
|
984
|
-
// subsequent buckets / fast-path renames with the same descriptor
|
|
985
|
-
// get the same GPU pipeline object (no duplicate
|
|
986
|
-
// createRenderPipeline calls).
|
|
987
|
-
const descForPipeline = precomputedDescriptor ?? psDescriptor;
|
|
988
|
-
const pipeline = getOrCreatePipeline(fam, layout, isAtlasBucket, descForPipeline);
|
|
989
|
-
// Per-bucket DrawHeader buffer (every uniform / attribute is a u32
|
|
990
|
-
// ref into the global arena; the per-bucket buffer just holds the
|
|
991
|
-
// refs).
|
|
992
1033
|
const drawHeapBuf = new GrowBuffer(device, `heapScene/${bk}/drawHeap`, GPUBufferUsage.STORAGE, Math.max(layout.drawHeaderBytes, 64));
|
|
993
1034
|
const drawHeap = new DrawHeap(drawHeapBuf, layout.drawHeaderBytes);
|
|
994
|
-
const slot0 = {
|
|
995
|
-
pipeline,
|
|
996
|
-
drawTableDirtyMin: Infinity, drawTableDirtyMax: 0,
|
|
997
|
-
recordCount: 0, slotToRecord: [], recordToSlot: [],
|
|
998
|
-
totalEmitEstimate: 0,
|
|
999
|
-
scanDirty: false,
|
|
1000
|
-
};
|
|
1001
1035
|
const bucket = {
|
|
1002
|
-
// §6 family-merge: family buckets aren't keyed on a specific
|
|
1003
|
-
// texture set — atlas placements are addressed per-RO via
|
|
1004
|
-
// drawHeader fields (`pageRef` + `formatBits` + `origin` +
|
|
1005
|
-
// `size`), and the bucket's atlas-binding ladder is driven by
|
|
1006
|
-
// `atlasPool.pagesFor(format)`. Leave `textures` undefined.
|
|
1007
1036
|
label: bk, textures: undefined, layout,
|
|
1008
|
-
slots: [
|
|
1009
|
-
bindGroup: null,
|
|
1037
|
+
slots: [], // populated lazily by ensureSlot per modeKey
|
|
1038
|
+
bindGroup: null, // legacy field; slot.bindGroup is the real one
|
|
1010
1039
|
drawHeap,
|
|
1011
1040
|
drawHeaderStaging: new Float32Array(drawHeapBuf.capacity / 4),
|
|
1012
1041
|
headerDirtyMin: Infinity, headerDirtyMax: 0,
|
|
@@ -1019,76 +1048,47 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1019
1048
|
localAtlasTextures: [],
|
|
1020
1049
|
localAtlasArrIdx: [],
|
|
1021
1050
|
};
|
|
1022
|
-
|
|
1023
|
-
const dtBuf = new GrowBuffer(device, `heapScene/${bk}/drawTable`, GPUBufferUsage.STORAGE, 1024);
|
|
1024
|
-
const blockSumsBuf = new GrowBuffer(device, `heapScene/${bk}/blockSums`, GPUBufferUsage.STORAGE, 4 * Math.max(1, Math.ceil((dtBuf.capacity / RECORD_BYTES) / SCAN_TILE_SIZE)));
|
|
1025
|
-
const blockOffsetsBuf = new GrowBuffer(device, `heapScene/${bk}/blockOffsets`, GPUBufferUsage.STORAGE, 4 * Math.max(1, Math.ceil((dtBuf.capacity / RECORD_BYTES) / SCAN_TILE_SIZE)));
|
|
1026
|
-
// 32 u32 = 128 bytes is the floor; pow2-grown by ensureCapacity
|
|
1027
|
-
// as totalEmitEstimate grows.
|
|
1028
|
-
const firstDrawInTileBuf = new GrowBuffer(device, `heapScene/${bk}/firstDrawInTile`, GPUBufferUsage.STORAGE, 128);
|
|
1029
|
-
const indirectBuf = device.createBuffer({
|
|
1030
|
-
label: `heapScene/${bk}/indirect`, size: 16,
|
|
1031
|
-
usage: GPUBufferUsage.INDIRECT | GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
|
|
1032
|
-
});
|
|
1033
|
-
// Initialize to (0, 1, 0, 0) so an unscanned/empty bucket draws
|
|
1034
|
-
// nothing safely.
|
|
1035
|
-
device.queue.writeBuffer(indirectBuf, 0, new Uint32Array([0, 1, 0, 0]));
|
|
1036
|
-
const paramsBuf = device.createBuffer({
|
|
1037
|
-
label: `heapScene/${bk}/params`, size: 16,
|
|
1038
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1039
|
-
});
|
|
1040
|
-
bucket.slots[0].drawTableBuf = dtBuf;
|
|
1041
|
-
bucket.slots[0].drawTableShadow = new Uint32Array(dtBuf.capacity / 4);
|
|
1042
|
-
bucket.slots[0].blockSumsBuf = blockSumsBuf;
|
|
1043
|
-
bucket.slots[0].blockOffsetsBuf = blockOffsetsBuf;
|
|
1044
|
-
bucket.slots[0].firstDrawInTileBuf = firstDrawInTileBuf;
|
|
1045
|
-
bucket.slots[0].indirectBuf = indirectBuf;
|
|
1046
|
-
bucket.slots[0].paramsBuf = paramsBuf;
|
|
1047
|
-
const ensureScanBuffers = () => {
|
|
1048
|
-
const needBlocks = Math.max(1, Math.ceil(bucket.slots[0].recordCount / SCAN_TILE_SIZE));
|
|
1049
|
-
blockSumsBuf.ensureCapacity(needBlocks * 4);
|
|
1050
|
-
blockOffsetsBuf.ensureCapacity(needBlocks * 4);
|
|
1051
|
-
};
|
|
1052
|
-
const rebuildScanBg = () => {
|
|
1053
|
-
bucket.slots[0].scanBindGroup = buildScanBindGroup(bucket);
|
|
1054
|
-
};
|
|
1055
|
-
dtBuf.onResize(() => {
|
|
1056
|
-
const grown = new Uint32Array(dtBuf.capacity / 4);
|
|
1057
|
-
grown.set(bucket.slots[0].drawTableShadow);
|
|
1058
|
-
bucket.slots[0].drawTableShadow = grown;
|
|
1059
|
-
ensureScanBuffers();
|
|
1060
|
-
bucket.bindGroup = buildBucketBindGroup(bucket);
|
|
1061
|
-
rebuildScanBg();
|
|
1062
|
-
bucket.slots[0].drawTableDirtyMin = 0;
|
|
1063
|
-
bucket.slots[0].drawTableDirtyMax = bucket.slots[0].recordCount * RECORD_BYTES;
|
|
1064
|
-
});
|
|
1065
|
-
blockSumsBuf.onResize(rebuildScanBg);
|
|
1066
|
-
blockOffsetsBuf.onResize(rebuildScanBg);
|
|
1067
|
-
firstDrawInTileBuf.onResize(() => {
|
|
1068
|
-
rebuildScanBg();
|
|
1069
|
-
bucket.bindGroup = buildBucketBindGroup(bucket);
|
|
1070
|
-
});
|
|
1071
|
-
bucket.slots[0].scanBindGroup = buildScanBindGroup(bucket);
|
|
1072
|
-
}
|
|
1073
|
-
bucket.bindGroup = buildBucketBindGroup(bucket);
|
|
1051
|
+
familyForBucket.set(bucket, fam);
|
|
1074
1052
|
drawHeap.onResize(() => {
|
|
1075
1053
|
bucket.drawHeaderStaging = new Float32Array(drawHeapBuf.capacity / 4);
|
|
1076
|
-
// GPU-side data was copied by the GrowBuffer's resize, but our
|
|
1077
|
-
// CPU mirror is fresh — mark all live local slots dirty so
|
|
1078
|
-
// their headers get re-packed and re-uploaded next frame.
|
|
1079
1054
|
for (const s of bucket.drawSlots)
|
|
1080
1055
|
bucket.dirty.add(s);
|
|
1081
|
-
|
|
1082
|
-
//
|
|
1083
|
-
|
|
1084
|
-
|
|
1056
|
+
// Rebuild every slot's render bind group (binding 1 = drawHeap
|
|
1057
|
+
// points at the new buffer).
|
|
1058
|
+
for (let i = 0; i < bucket.slots.length; i++) {
|
|
1059
|
+
bucket.slots[i].bindGroup = buildBucketBindGroup(bucket, i);
|
|
1060
|
+
}
|
|
1085
1061
|
});
|
|
1086
|
-
//
|
|
1087
|
-
//
|
|
1062
|
+
// Ensure the bucket has a slot for the supplied descriptor — at
|
|
1063
|
+
// bucket-creation time we eagerly seed slot 0 from the caller's
|
|
1064
|
+
// descriptor so the first addDraw doesn't pay a slot-create cost.
|
|
1065
|
+
const seedDescriptor = precomputedDescriptor ?? snapshotDescriptor(pipelineState, sig);
|
|
1066
|
+
createSlot(bucket, seedDescriptor);
|
|
1088
1067
|
buckets.push(bucket);
|
|
1089
1068
|
bucketByKey.set(bk, bucket);
|
|
1090
1069
|
return bucket;
|
|
1091
1070
|
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Return the existing BucketSlot for `descriptor` inside `bucket`,
|
|
1073
|
+
* or create one on miss. The mode-flip hot path calls this when an
|
|
1074
|
+
* RO's tracker.modeKey lands on a slot that doesn't exist yet.
|
|
1075
|
+
*/
|
|
1076
|
+
function ensureSlot(bucket, descriptor) {
|
|
1077
|
+
const wanted = encodeModeKey(descriptor);
|
|
1078
|
+
for (const s of bucket.slots) {
|
|
1079
|
+
if (s.modeKey === wanted)
|
|
1080
|
+
return s;
|
|
1081
|
+
}
|
|
1082
|
+
return createSlot(bucket, descriptor);
|
|
1083
|
+
}
|
|
1084
|
+
/** Find an RO's slot by drawId; null if the drawId is dead. */
|
|
1085
|
+
function slotForDrawId(drawId) {
|
|
1086
|
+
const idx = drawIdToSlotIdx[drawId];
|
|
1087
|
+
const bucket = drawIdToBucket[drawId];
|
|
1088
|
+
if (idx === undefined || bucket === undefined)
|
|
1089
|
+
return null;
|
|
1090
|
+
return bucket.slots[idx] ?? null;
|
|
1091
|
+
}
|
|
1092
1092
|
// ─── Per-bucket header writer (refs only, post step 3) ────────────
|
|
1093
1093
|
// The DrawHeader is now a flat list of u32 refs into the arena —
|
|
1094
1094
|
// values themselves live in arena allocations the pool manages.
|
|
@@ -1201,6 +1201,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1201
1201
|
// ─── Stats (declared early so addDraw/removeDraw can mutate it) ───
|
|
1202
1202
|
const stats = {
|
|
1203
1203
|
groups: 0,
|
|
1204
|
+
slotCount: 0,
|
|
1204
1205
|
totalDraws: 0,
|
|
1205
1206
|
drawBytes: 0,
|
|
1206
1207
|
geometryBytes: 0,
|
|
@@ -1210,6 +1211,15 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1210
1211
|
derivedRecords: 0,
|
|
1211
1212
|
};
|
|
1212
1213
|
Object.defineProperty(stats, "groups", { get: () => buckets.length, configurable: true });
|
|
1214
|
+
Object.defineProperty(stats, "slotCount", {
|
|
1215
|
+
get: () => {
|
|
1216
|
+
let n = 0;
|
|
1217
|
+
for (const b of buckets)
|
|
1218
|
+
n += b.slots.length;
|
|
1219
|
+
return n;
|
|
1220
|
+
},
|
|
1221
|
+
configurable: true,
|
|
1222
|
+
});
|
|
1213
1223
|
// ─── addDraw / removeDraw ─────────────────────────────────────────
|
|
1214
1224
|
// Public addDraw wrapper. Establishes a sceneObj.evaluateAlways
|
|
1215
1225
|
// scope so external callers (no batched outer eval) still register
|
|
@@ -1258,6 +1268,15 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1258
1268
|
});
|
|
1259
1269
|
}
|
|
1260
1270
|
const bucket = findOrCreateBucket(spec.effect, spec.textures, spec.pipelineState, precomputedDescriptor);
|
|
1271
|
+
// Phase 5c.2: route this RO to the bucket's slot covering its
|
|
1272
|
+
// current descriptor. ensureSlot creates a new slot on miss; the
|
|
1273
|
+
// pipeline lookup hits the scene-level cache.
|
|
1274
|
+
const roDescriptor = precomputedDescriptor ?? snapshotDescriptor(spec.pipelineState, sig);
|
|
1275
|
+
const roSlot = ensureSlot(bucket, roDescriptor);
|
|
1276
|
+
const roSlotIdx = bucket.slots.indexOf(roSlot);
|
|
1277
|
+
if (spec.modeRules !== undefined && drawId < 8) {
|
|
1278
|
+
console.debug(`[heapScene] addDraw drawId=${drawId} bucket=${bucket.label} slotIdx=${roSlotIdx} cullMode=${roDescriptor.cullMode}`);
|
|
1279
|
+
}
|
|
1261
1280
|
const fam = familyFor(spec.effect);
|
|
1262
1281
|
const effectFields = fam.fieldsForEffect.get(spec.effect.id);
|
|
1263
1282
|
// Indices live in their own INDEX-usage buffer (WebGPU constraint).
|
|
@@ -1384,8 +1403,8 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1384
1403
|
if (end > bucket.headerDirtyMax)
|
|
1385
1404
|
bucket.headerDirtyMax = end;
|
|
1386
1405
|
{
|
|
1387
|
-
const dtBuf =
|
|
1388
|
-
const recIdx =
|
|
1406
|
+
const dtBuf = roSlot.drawTableBuf;
|
|
1407
|
+
const recIdx = roSlot.recordCount;
|
|
1389
1408
|
if (recIdx >= SCAN_MAX_RECORDS) {
|
|
1390
1409
|
throw new Error(`heapScene: bucket exceeds SCAN_MAX_RECORDS (${SCAN_MAX_RECORDS}); ` +
|
|
1391
1410
|
`extend the scan to multi-level if you need more`);
|
|
@@ -1395,29 +1414,30 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1395
1414
|
dtBuf.setUsed(Math.max(dtBuf.usedBytes, byteOff + RECORD_BYTES));
|
|
1396
1415
|
// Grow scan-side buffers if recordCount crosses a tile boundary.
|
|
1397
1416
|
const needBlocks = Math.max(1, Math.ceil((recIdx + 1) / SCAN_TILE_SIZE));
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
const shadow =
|
|
1417
|
+
roSlot.blockSumsBuf.ensureCapacity(needBlocks * 4);
|
|
1418
|
+
roSlot.blockOffsetsBuf.ensureCapacity(needBlocks * 4);
|
|
1419
|
+
const shadow = roSlot.drawTableShadow;
|
|
1401
1420
|
// firstEmit is GPU-overwritten by the prefix-sum pass; 0 is fine.
|
|
1402
1421
|
shadow[recIdx * RECORD_U32 + 0] = 0;
|
|
1403
1422
|
shadow[recIdx * RECORD_U32 + 1] = localSlot;
|
|
1404
1423
|
shadow[recIdx * RECORD_U32 + 2] = idxAlloc.firstIndex;
|
|
1405
1424
|
shadow[recIdx * RECORD_U32 + 3] = idxAlloc.count;
|
|
1406
1425
|
shadow[recIdx * RECORD_U32 + 4] = instanceCount;
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
if (byteOff <
|
|
1411
|
-
|
|
1412
|
-
if (byteOff + RECORD_BYTES >
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
const newNumTiles = Math.max(1, Math.ceil(
|
|
1416
|
-
|
|
1417
|
-
|
|
1426
|
+
roSlot.recordCount = recIdx + 1;
|
|
1427
|
+
roSlot.slotToRecord[localSlot] = recIdx;
|
|
1428
|
+
roSlot.recordToSlot[recIdx] = localSlot;
|
|
1429
|
+
if (byteOff < roSlot.drawTableDirtyMin)
|
|
1430
|
+
roSlot.drawTableDirtyMin = byteOff;
|
|
1431
|
+
if (byteOff + RECORD_BYTES > roSlot.drawTableDirtyMax)
|
|
1432
|
+
roSlot.drawTableDirtyMax = byteOff + RECORD_BYTES;
|
|
1433
|
+
roSlot.totalEmitEstimate += idxAlloc.count * instanceCount;
|
|
1434
|
+
const newNumTiles = Math.max(1, Math.ceil(roSlot.totalEmitEstimate / TILE_K));
|
|
1435
|
+
roSlot.firstDrawInTileBuf.ensureCapacity((newNumTiles + 1) * 4);
|
|
1436
|
+
roSlot.scanDirty = true;
|
|
1418
1437
|
}
|
|
1419
1438
|
drawIdToBucket[drawId] = bucket;
|
|
1420
1439
|
drawIdToLocalSlot[drawId] = localSlot;
|
|
1440
|
+
drawIdToSlotIdx[drawId] = roSlotIdx;
|
|
1421
1441
|
drawIdToIndexAval[drawId] = indicesAval;
|
|
1422
1442
|
// ─── Reactive rebucket: track PS-modeKey changes ────────────────
|
|
1423
1443
|
// When any mode-axis aval marks (e.g. cullCval.value = 'front'),
|
|
@@ -1565,17 +1585,19 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1565
1585
|
}
|
|
1566
1586
|
}
|
|
1567
1587
|
{
|
|
1588
|
+
const slotIdx = drawIdToSlotIdx[drawId];
|
|
1589
|
+
const slot = slotIdx !== undefined ? bucket.slots[slotIdx] : bucket.slots[0];
|
|
1568
1590
|
const removedEntry = bucket.localEntries[localSlot];
|
|
1569
1591
|
const removedCount = removedEntry !== undefined
|
|
1570
1592
|
? removedEntry.indexCount * removedEntry.instanceCount
|
|
1571
1593
|
: 0;
|
|
1572
|
-
|
|
1594
|
+
slot.totalEmitEstimate = Math.max(0, slot.totalEmitEstimate - removedCount);
|
|
1573
1595
|
// Swap-pop: move the last record into the freed slot, decrement
|
|
1574
1596
|
// recordCount. firstEmit is GPU-rewritten by the next scan, so
|
|
1575
1597
|
// we only fix (drawIdx, indexStart, indexCount, instanceCount).
|
|
1576
|
-
const recIdx =
|
|
1577
|
-
const lastRecIdx =
|
|
1578
|
-
const shadow =
|
|
1598
|
+
const recIdx = slot.slotToRecord[localSlot];
|
|
1599
|
+
const lastRecIdx = slot.recordCount - 1;
|
|
1600
|
+
const shadow = slot.drawTableShadow;
|
|
1579
1601
|
if (recIdx !== lastRecIdx) {
|
|
1580
1602
|
const dst = recIdx * RECORD_U32;
|
|
1581
1603
|
const src = lastRecIdx * RECORD_U32;
|
|
@@ -1584,19 +1606,19 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1584
1606
|
shadow[dst + 2] = shadow[src + 2];
|
|
1585
1607
|
shadow[dst + 3] = shadow[src + 3];
|
|
1586
1608
|
shadow[dst + 4] = shadow[src + 4];
|
|
1587
|
-
const movedSlot =
|
|
1588
|
-
|
|
1589
|
-
|
|
1609
|
+
const movedSlot = slot.recordToSlot[lastRecIdx];
|
|
1610
|
+
slot.slotToRecord[movedSlot] = recIdx;
|
|
1611
|
+
slot.recordToSlot[recIdx] = movedSlot;
|
|
1590
1612
|
const byteOff = recIdx * RECORD_BYTES;
|
|
1591
|
-
if (byteOff <
|
|
1592
|
-
|
|
1593
|
-
if (byteOff + RECORD_BYTES >
|
|
1594
|
-
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1613
|
+
if (byteOff < slot.drawTableDirtyMin)
|
|
1614
|
+
slot.drawTableDirtyMin = byteOff;
|
|
1615
|
+
if (byteOff + RECORD_BYTES > slot.drawTableDirtyMax)
|
|
1616
|
+
slot.drawTableDirtyMax = byteOff + RECORD_BYTES;
|
|
1617
|
+
}
|
|
1618
|
+
slot.slotToRecord[localSlot] = -1;
|
|
1619
|
+
slot.recordToSlot[lastRecIdx] = -1;
|
|
1620
|
+
slot.recordCount = lastRecIdx;
|
|
1621
|
+
slot.scanDirty = true;
|
|
1600
1622
|
}
|
|
1601
1623
|
// Release pool entries — refcount drops; if zero, allocation freed.
|
|
1602
1624
|
const avals = bucket.localPerDrawAvals[localSlot];
|
|
@@ -1649,6 +1671,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1649
1671
|
bucket.drawHeap.release(localSlot);
|
|
1650
1672
|
drawIdToBucket[drawId] = undefined;
|
|
1651
1673
|
drawIdToLocalSlot[drawId] = undefined;
|
|
1674
|
+
drawIdToSlotIdx[drawId] = undefined;
|
|
1652
1675
|
drawIdToIndexAval[drawId] = undefined;
|
|
1653
1676
|
drawIdToSpec[drawId] = undefined;
|
|
1654
1677
|
const oldTracker = drawIdToModeTracker[drawId];
|
|
@@ -1768,104 +1791,91 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1768
1791
|
if (dirtyModeKeyDrawIds.size > 0) {
|
|
1769
1792
|
const dirty = [...dirtyModeKeyDrawIds];
|
|
1770
1793
|
dirtyModeKeyDrawIds.clear();
|
|
1771
|
-
//
|
|
1772
|
-
//
|
|
1773
|
-
//
|
|
1774
|
-
//
|
|
1775
|
-
//
|
|
1776
|
-
//
|
|
1777
|
-
|
|
1794
|
+
// Phase 5c.2 reslot pass. For each dirty drawId whose
|
|
1795
|
+
// tracker.modeKey changed:
|
|
1796
|
+
// 1. Find its bucket + current slot.
|
|
1797
|
+
// 2. ensureSlot for the new modeKey (within the same bucket).
|
|
1798
|
+
// 3. Move the record from oldSlot.drawTable to
|
|
1799
|
+
// newSlot.drawTable (swap-pop + push). Bucket-shared
|
|
1800
|
+
// drawHeap + pool entries stay put.
|
|
1801
|
+
// 4. Update drawIdToSlotIdx.
|
|
1802
|
+
// No bucket rename, no createRenderPipeline (pre-warm + cache
|
|
1803
|
+
// mean ensureSlot is a hit). No removeDraw+addDraw arena
|
|
1804
|
+
// churn.
|
|
1778
1805
|
for (const drawId of dirty) {
|
|
1779
|
-
const
|
|
1780
|
-
if (
|
|
1781
|
-
continue; // RO removed since mark
|
|
1782
|
-
let arr = byBucket.get(b);
|
|
1783
|
-
if (arr === undefined) {
|
|
1784
|
-
arr = [];
|
|
1785
|
-
byBucket.set(b, arr);
|
|
1786
|
-
}
|
|
1787
|
-
arr.push(drawId);
|
|
1788
|
-
}
|
|
1789
|
-
const renames = [];
|
|
1790
|
-
const slowChanged = []; // ROs going through per-RO
|
|
1791
|
-
for (const [bucket, ids] of byBucket) {
|
|
1792
|
-
const changed = [];
|
|
1793
|
-
for (const drawId of ids) {
|
|
1794
|
-
const tracker = drawIdToModeTracker[drawId];
|
|
1795
|
-
if (tracker === undefined)
|
|
1796
|
-
continue;
|
|
1797
|
-
const oldKey = tracker.modeKey;
|
|
1798
|
-
if (!tracker.recompute())
|
|
1799
|
-
continue;
|
|
1800
|
-
if (tracker.modeKey === oldKey)
|
|
1801
|
-
continue;
|
|
1802
|
-
changed.push(drawId);
|
|
1803
|
-
}
|
|
1804
|
-
if (changed.length === 0)
|
|
1806
|
+
const tracker = drawIdToModeTracker[drawId];
|
|
1807
|
+
if (tracker === undefined)
|
|
1805
1808
|
continue;
|
|
1806
|
-
const
|
|
1807
|
-
if (
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
for (const [k, v] of bucketByKey) {
|
|
1816
|
-
if (v === bucket) {
|
|
1817
|
-
oldBk = k;
|
|
1818
|
-
break;
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
if (oldBk !== undefined && oldBk !== newBk) {
|
|
1822
|
-
renames.push({ bucket, repId, newKey, newBk, oldBk });
|
|
1823
|
-
continue;
|
|
1824
|
-
}
|
|
1825
|
-
if (oldBk === newBk)
|
|
1826
|
-
continue; // shouldn't happen but safe
|
|
1827
|
-
}
|
|
1828
|
-
// Partial transition: per-RO slow path.
|
|
1829
|
-
for (const c of changed)
|
|
1830
|
-
slowChanged.push(c);
|
|
1831
|
-
}
|
|
1832
|
-
if (renames.length > 0) {
|
|
1833
|
-
// Phase 1: remove all old keys. After this, the renames' new
|
|
1834
|
-
// keys are free unless they collide with a NON-rename
|
|
1835
|
-
// bucket's current key.
|
|
1836
|
-
for (const r of renames)
|
|
1837
|
-
bucketByKey.delete(r.oldBk);
|
|
1838
|
-
// Phase 2: assign new keys, building new pipelines. If a
|
|
1839
|
-
// collision still exists after phase 1 (against a stale
|
|
1840
|
-
// bucket that's NOT being renamed), restore old key + fall
|
|
1841
|
-
// back to per-RO for that bucket's records.
|
|
1842
|
-
for (const r of renames) {
|
|
1843
|
-
if (bucketByKey.has(r.newBk)) {
|
|
1844
|
-
// Collision with a stale bucket. Restore + flag for slow.
|
|
1845
|
-
bucketByKey.set(r.oldBk, r.bucket);
|
|
1846
|
-
for (const id of r.bucket.drawSlots)
|
|
1847
|
-
slowChanged.push(id);
|
|
1848
|
-
continue;
|
|
1849
|
-
}
|
|
1850
|
-
bucketByKey.set(r.newBk, r.bucket);
|
|
1851
|
-
const tracker = drawIdToModeTracker[r.repId];
|
|
1852
|
-
const sample = drawIdToSpec[r.repId];
|
|
1853
|
-
const desc = tracker.descriptor;
|
|
1854
|
-
// Cache hit: reuse an already-built pipeline. Cache miss
|
|
1855
|
-
// builds + stashes; future flips back to this descriptor
|
|
1856
|
-
// hit the cache.
|
|
1857
|
-
const newPipeline = getOrCreatePipeline(familyFor(sample.effect), r.bucket.layout, r.bucket.isAtlasBucket, desc);
|
|
1858
|
-
r.bucket.slots[0].pipeline = newPipeline;
|
|
1859
|
-
}
|
|
1860
|
-
}
|
|
1861
|
-
// Slow per-RO rebucket for the leftover cases.
|
|
1862
|
-
for (const drawId of slowChanged) {
|
|
1863
|
-
const spec = drawIdToSpec[drawId];
|
|
1864
|
-
if (spec === undefined)
|
|
1809
|
+
const oldKey = tracker.modeKey;
|
|
1810
|
+
if (!tracker.recompute())
|
|
1811
|
+
continue;
|
|
1812
|
+
if (tracker.modeKey === oldKey)
|
|
1813
|
+
continue;
|
|
1814
|
+
const bucket = drawIdToBucket[drawId];
|
|
1815
|
+
const localSlot = drawIdToLocalSlot[drawId];
|
|
1816
|
+
const oldSlotIdx = drawIdToSlotIdx[drawId];
|
|
1817
|
+
if (bucket === undefined || localSlot === undefined || oldSlotIdx === undefined)
|
|
1865
1818
|
continue;
|
|
1866
|
-
|
|
1867
|
-
const
|
|
1868
|
-
|
|
1819
|
+
const newSlot = ensureSlot(bucket, tracker.descriptor);
|
|
1820
|
+
const newSlotIdx = bucket.slots.indexOf(newSlot);
|
|
1821
|
+
if (newSlotIdx === oldSlotIdx)
|
|
1822
|
+
continue;
|
|
1823
|
+
// Swap-pop from old slot's drawTable.
|
|
1824
|
+
const oldSlot = bucket.slots[oldSlotIdx];
|
|
1825
|
+
const entry = bucket.localEntries[localSlot];
|
|
1826
|
+
const emit = entry !== undefined ? entry.indexCount * entry.instanceCount : 0;
|
|
1827
|
+
const oldRecIdx = oldSlot.slotToRecord[localSlot];
|
|
1828
|
+
const lastRecIdx = oldSlot.recordCount - 1;
|
|
1829
|
+
const oldShadow = oldSlot.drawTableShadow;
|
|
1830
|
+
if (oldRecIdx !== lastRecIdx) {
|
|
1831
|
+
const dst = oldRecIdx * RECORD_U32;
|
|
1832
|
+
const src = lastRecIdx * RECORD_U32;
|
|
1833
|
+
oldShadow[dst + 0] = 0;
|
|
1834
|
+
oldShadow[dst + 1] = oldShadow[src + 1];
|
|
1835
|
+
oldShadow[dst + 2] = oldShadow[src + 2];
|
|
1836
|
+
oldShadow[dst + 3] = oldShadow[src + 3];
|
|
1837
|
+
oldShadow[dst + 4] = oldShadow[src + 4];
|
|
1838
|
+
const movedLocal = oldSlot.recordToSlot[lastRecIdx];
|
|
1839
|
+
oldSlot.slotToRecord[movedLocal] = oldRecIdx;
|
|
1840
|
+
oldSlot.recordToSlot[oldRecIdx] = movedLocal;
|
|
1841
|
+
const off = oldRecIdx * RECORD_BYTES;
|
|
1842
|
+
if (off < oldSlot.drawTableDirtyMin)
|
|
1843
|
+
oldSlot.drawTableDirtyMin = off;
|
|
1844
|
+
if (off + RECORD_BYTES > oldSlot.drawTableDirtyMax)
|
|
1845
|
+
oldSlot.drawTableDirtyMax = off + RECORD_BYTES;
|
|
1846
|
+
}
|
|
1847
|
+
oldSlot.slotToRecord[localSlot] = -1;
|
|
1848
|
+
oldSlot.recordToSlot[lastRecIdx] = -1;
|
|
1849
|
+
oldSlot.recordCount = lastRecIdx;
|
|
1850
|
+
oldSlot.totalEmitEstimate = Math.max(0, oldSlot.totalEmitEstimate - emit);
|
|
1851
|
+
oldSlot.scanDirty = true;
|
|
1852
|
+
// Push into new slot's drawTable.
|
|
1853
|
+
const newRecIdx = newSlot.recordCount;
|
|
1854
|
+
const dtBuf = newSlot.drawTableBuf;
|
|
1855
|
+
const byteOff = newRecIdx * RECORD_BYTES;
|
|
1856
|
+
dtBuf.ensureCapacity(byteOff + RECORD_BYTES);
|
|
1857
|
+
dtBuf.setUsed(Math.max(dtBuf.usedBytes, byteOff + RECORD_BYTES));
|
|
1858
|
+
const needBlocks = Math.max(1, Math.ceil((newRecIdx + 1) / SCAN_TILE_SIZE));
|
|
1859
|
+
newSlot.blockSumsBuf.ensureCapacity(needBlocks * 4);
|
|
1860
|
+
newSlot.blockOffsetsBuf.ensureCapacity(needBlocks * 4);
|
|
1861
|
+
const newShadow = newSlot.drawTableShadow;
|
|
1862
|
+
newShadow[newRecIdx * RECORD_U32 + 0] = 0;
|
|
1863
|
+
newShadow[newRecIdx * RECORD_U32 + 1] = localSlot;
|
|
1864
|
+
newShadow[newRecIdx * RECORD_U32 + 2] = entry !== undefined ? entry.firstIndex : 0;
|
|
1865
|
+
newShadow[newRecIdx * RECORD_U32 + 3] = entry !== undefined ? entry.indexCount : 0;
|
|
1866
|
+
newShadow[newRecIdx * RECORD_U32 + 4] = entry !== undefined ? entry.instanceCount : 0;
|
|
1867
|
+
newSlot.recordCount = newRecIdx + 1;
|
|
1868
|
+
newSlot.slotToRecord[localSlot] = newRecIdx;
|
|
1869
|
+
newSlot.recordToSlot[newRecIdx] = localSlot;
|
|
1870
|
+
if (byteOff < newSlot.drawTableDirtyMin)
|
|
1871
|
+
newSlot.drawTableDirtyMin = byteOff;
|
|
1872
|
+
if (byteOff + RECORD_BYTES > newSlot.drawTableDirtyMax)
|
|
1873
|
+
newSlot.drawTableDirtyMax = byteOff + RECORD_BYTES;
|
|
1874
|
+
newSlot.totalEmitEstimate += emit;
|
|
1875
|
+
const newNumTiles = Math.max(1, Math.ceil(newSlot.totalEmitEstimate / TILE_K));
|
|
1876
|
+
newSlot.firstDrawInTileBuf.ensureCapacity((newNumTiles + 1) * 4);
|
|
1877
|
+
newSlot.scanDirty = true;
|
|
1878
|
+
drawIdToSlotIdx[drawId] = newSlotIdx;
|
|
1869
1879
|
}
|
|
1870
1880
|
}
|
|
1871
1881
|
// 1. Pool: re-pack any aval whose value changed since last frame.
|
|
@@ -1980,13 +1990,21 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1980
1990
|
function encodeIntoPass(pass) {
|
|
1981
1991
|
let curBg = null;
|
|
1982
1992
|
for (const b of buckets) {
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1993
|
+
// Phase 5c.2: iterate the bucket's slots, drawIndirect per
|
|
1994
|
+
// non-empty slot. Empty slots draw zero (skip — saves the
|
|
1995
|
+
// setPipeline/setBindGroup overhead).
|
|
1996
|
+
for (const slot of b.slots) {
|
|
1997
|
+
if (slot.recordCount === 0)
|
|
1998
|
+
continue;
|
|
1999
|
+
const bg = slot.bindGroup;
|
|
2000
|
+
if (bg !== undefined && bg !== curBg) {
|
|
2001
|
+
pass.setBindGroup(0, bg);
|
|
2002
|
+
curBg = bg;
|
|
2003
|
+
}
|
|
2004
|
+
pass.setPipeline(slot.pipeline);
|
|
2005
|
+
if (slot.indirectBuf !== undefined)
|
|
2006
|
+
pass.drawIndirect(slot.indirectBuf, 0);
|
|
1986
2007
|
}
|
|
1987
|
-
pass.setPipeline(b.slots[0].pipeline);
|
|
1988
|
-
if (b.slots[0].recordCount > 0)
|
|
1989
|
-
pass.drawIndirect(b.slots[0].indirectBuf, 0);
|
|
1990
2008
|
}
|
|
1991
2009
|
}
|
|
1992
2010
|
/**
|
|
@@ -2061,42 +2079,44 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2061
2079
|
stats.derivedRecords = derivedScene.records.recordCount;
|
|
2062
2080
|
}
|
|
2063
2081
|
let anyDirty = false;
|
|
2064
|
-
for (const b of buckets) {
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2082
|
+
outer: for (const b of buckets) {
|
|
2083
|
+
for (const s of b.slots) {
|
|
2084
|
+
if (s.scanDirty) {
|
|
2085
|
+
anyDirty = true;
|
|
2086
|
+
break outer;
|
|
2087
|
+
}
|
|
2068
2088
|
}
|
|
2069
2089
|
}
|
|
2070
2090
|
if (!anyDirty)
|
|
2071
2091
|
return;
|
|
2072
2092
|
const pass = enc.beginComputePass({ label: "heapScene/scan" });
|
|
2073
2093
|
for (const b of buckets) {
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2094
|
+
for (let i = 0; i < b.slots.length; i++) {
|
|
2095
|
+
const slot = b.slots[i];
|
|
2096
|
+
if (!slot.scanDirty)
|
|
2097
|
+
continue;
|
|
2098
|
+
// Rebuild render bind group if recordCount changed — drawTable
|
|
2099
|
+
// binding is sized to recordCount * RECORD_BYTES.
|
|
2100
|
+
if (slot.renderBoundRecordCount !== slot.recordCount) {
|
|
2101
|
+
slot.bindGroup = buildBucketBindGroup(b, i);
|
|
2102
|
+
}
|
|
2103
|
+
const numRecords = slot.recordCount;
|
|
2104
|
+
const numBlocks = Math.max(1, Math.ceil(numRecords / SCAN_TILE_SIZE));
|
|
2105
|
+
device.queue.writeBuffer(slot.paramsBuf, 0, new Uint32Array([numRecords, numBlocks, 0, 0]));
|
|
2106
|
+
pass.setBindGroup(0, slot.scanBindGroup);
|
|
2107
|
+
pass.setPipeline(scanPipeTile);
|
|
2108
|
+
pass.dispatchWorkgroups(numBlocks, 1, 1);
|
|
2109
|
+
pass.setPipeline(scanPipeBlocks);
|
|
2110
|
+
pass.dispatchWorkgroups(1, 1, 1);
|
|
2111
|
+
pass.setPipeline(scanPipeAdd);
|
|
2112
|
+
pass.dispatchWorkgroups(numBlocks, 1, 1);
|
|
2113
|
+
const SCAN_WG_SIZE = 256;
|
|
2114
|
+
const numTilesCap = Math.max(1, Math.ceil(slot.totalEmitEstimate / TILE_K));
|
|
2115
|
+
const tileWgs = Math.max(1, Math.ceil((numTilesCap + 1) / SCAN_WG_SIZE));
|
|
2116
|
+
pass.setPipeline(scanPipeBuildTileIndex);
|
|
2117
|
+
pass.dispatchWorkgroups(tileWgs, 1, 1);
|
|
2118
|
+
slot.scanDirty = false;
|
|
2119
|
+
}
|
|
2100
2120
|
}
|
|
2101
2121
|
pass.end();
|
|
2102
2122
|
}
|