@aardworx/wombat.rendering 0.9.15 → 0.9.17
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/derivedModes/modeKeyCpu.d.ts +52 -3
- package/dist/runtime/derivedModes/modeKeyCpu.d.ts.map +1 -1
- package/dist/runtime/derivedModes/modeKeyCpu.js +146 -7
- package/dist/runtime/derivedModes/modeKeyCpu.js.map +1 -1
- package/dist/runtime/derivedModes/rule.d.ts +59 -0
- package/dist/runtime/derivedModes/rule.d.ts.map +1 -0
- package/dist/runtime/derivedModes/rule.js +55 -0
- package/dist/runtime/derivedModes/rule.js.map +1 -0
- package/dist/runtime/heapScene.d.ts +32 -0
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +192 -24
- package/dist/runtime/heapScene.js.map +1 -1
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/index.js.map +1 -1
- package/package.json +1 -1
- package/src/runtime/derivedModes/modeKeyCpu.ts +162 -5
- package/src/runtime/derivedModes/rule.ts +110 -0
- package/src/runtime/heapScene.ts +220 -23
- package/src/runtime/index.ts +10 -0
|
@@ -55,6 +55,7 @@ import { GrowBuffer, MIN_BUFFER_BYTES, POW2, ALIGN16, } from "./heapScene/growBu
|
|
|
55
55
|
import { UniformPool, IndexPool, DrawHeap, AttributeArena, IndexAllocator, insertSortedFreeBlock, buildArenaState, arenaBytes, writeAttribute, asAval, isBufferView, asFloat32, ALLOC_HEADER_BYTES, ALLOC_HEADER_PAD_TO, ENC_V3F_TIGHT, SEM_POSITIONS, SEM_NORMALS, } from "./heapScene/pools.js";
|
|
56
56
|
import { encodeModeKey } from "./pipelineCache/index.js";
|
|
57
57
|
import { snapshotDescriptor, ModeKeyTracker } from "./derivedModes/modeKeyCpu.js";
|
|
58
|
+
import { addMarkingCallback } from "@aardworx/wombat.adaptive";
|
|
58
59
|
export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
59
60
|
const atlasPool = opts.atlasPool;
|
|
60
61
|
const colorAttachmentName = sig.colorNames[0];
|
|
@@ -94,11 +95,48 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
94
95
|
* references.
|
|
95
96
|
*/
|
|
96
97
|
const drawIdToSpec = [];
|
|
97
|
-
/** Per-draw ModeKeyTracker —
|
|
98
|
-
*
|
|
98
|
+
/** Per-draw ModeKeyTracker — recomputes the modeKey on demand. Does
|
|
99
|
+
* NOT install per-instance marking callbacks; subscriptions are
|
|
100
|
+
* scene-level (deduped by aval identity below). */
|
|
99
101
|
const drawIdToModeTracker = [];
|
|
100
102
|
/** drawIds whose PS marked since last update; drained by `update`. */
|
|
101
103
|
const dirtyModeKeyDrawIds = new Set();
|
|
104
|
+
/**
|
|
105
|
+
* Scene-level mode-aval subscription dedupe. One IDisposable per
|
|
106
|
+
* unique leaf aval, plus a Set of dependent drawIds. When the aval
|
|
107
|
+
* marks, all drawIds in the set become dirty in O(1) per RO instead
|
|
108
|
+
* of installing N separate addMarkingCallbacks. The big win for the
|
|
109
|
+
* shared-cullCval case (20k ROs subscribing to one cval → 1 callback
|
|
110
|
+
* instead of 20k).
|
|
111
|
+
*/
|
|
112
|
+
const modeAvalToDrawIds = new Map();
|
|
113
|
+
const modeAvalCallbacks = new Map();
|
|
114
|
+
function subscribeModeLeaf(av, drawId) {
|
|
115
|
+
let set = modeAvalToDrawIds.get(av);
|
|
116
|
+
if (set === undefined) {
|
|
117
|
+
set = new Set();
|
|
118
|
+
modeAvalToDrawIds.set(av, set);
|
|
119
|
+
modeAvalCallbacks.set(av, addMarkingCallback(av, () => {
|
|
120
|
+
for (const id of set)
|
|
121
|
+
dirtyModeKeyDrawIds.add(id);
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
set.add(drawId);
|
|
125
|
+
}
|
|
126
|
+
function unsubscribeModeLeaf(av, drawId) {
|
|
127
|
+
const set = modeAvalToDrawIds.get(av);
|
|
128
|
+
if (set === undefined)
|
|
129
|
+
return;
|
|
130
|
+
set.delete(drawId);
|
|
131
|
+
if (set.size === 0) {
|
|
132
|
+
modeAvalToDrawIds.delete(av);
|
|
133
|
+
const cb = modeAvalCallbacks.get(av);
|
|
134
|
+
if (cb !== undefined) {
|
|
135
|
+
cb.dispose();
|
|
136
|
+
modeAvalCallbacks.delete(av);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
102
140
|
let nextDrawId = 0;
|
|
103
141
|
/**
|
|
104
142
|
* Unwrap an aval to its inner value, or pass through a plain value.
|
|
@@ -853,7 +891,14 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
853
891
|
// right per-effect helper at draw time. Atlas-binding shape follows
|
|
854
892
|
// from `family.drawHeaderUnion.atlasTextureBindings.size > 0`.
|
|
855
893
|
void texIdOf; // retained for future per-bucket diagnostics
|
|
856
|
-
function findOrCreateBucket(effect, _textures, pipelineState
|
|
894
|
+
function findOrCreateBucket(effect, _textures, pipelineState,
|
|
895
|
+
/**
|
|
896
|
+
* Optional precomputed descriptor. addDrawImpl passes this when
|
|
897
|
+
* the spec carries `modeRules` so the bucket key reflects the
|
|
898
|
+
* rule-evaluated value (not the raw PS aval values). When omitted
|
|
899
|
+
* the descriptor is snapshotted from PS as before.
|
|
900
|
+
*/
|
|
901
|
+
precomputedDescriptor) {
|
|
857
902
|
if (!familyBuilt) {
|
|
858
903
|
throw new Error("heapScene: findOrCreateBucket called before family build");
|
|
859
904
|
}
|
|
@@ -866,13 +911,25 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
866
911
|
// (the actual value-set in the descriptor), so identical-value
|
|
867
912
|
// cvals collapse to ONE bucket. See docs/derived-modes.md and
|
|
868
913
|
// tests/heap-multi-pipeline-bucket.test.ts.
|
|
869
|
-
const psDescriptor = snapshotDescriptor(pipelineState, sig);
|
|
914
|
+
const psDescriptor = precomputedDescriptor ?? snapshotDescriptor(pipelineState, sig);
|
|
870
915
|
const psModeKey = encodeModeKey(psDescriptor);
|
|
871
916
|
const bk = `family#${fam.schema.id}|mk#${psModeKey.toString(16)}`;
|
|
872
917
|
const existing = bucketByKey.get(bk);
|
|
873
918
|
if (existing !== undefined)
|
|
874
919
|
return existing;
|
|
875
|
-
|
|
920
|
+
// If a precomputed descriptor is supplied (rule-evaluated path),
|
|
921
|
+
// use its post-rule axis values so the pipeline reflects what the
|
|
922
|
+
// RO actually wants. Otherwise fall through to PS-aval forcing.
|
|
923
|
+
const ps = precomputedDescriptor !== undefined
|
|
924
|
+
? {
|
|
925
|
+
topology: precomputedDescriptor.topology,
|
|
926
|
+
cullMode: precomputedDescriptor.cullMode,
|
|
927
|
+
frontFace: precomputedDescriptor.frontFace,
|
|
928
|
+
...(precomputedDescriptor.depth !== undefined
|
|
929
|
+
? { depth: { write: precomputedDescriptor.depth.write, compare: precomputedDescriptor.depth.compare } }
|
|
930
|
+
: {}),
|
|
931
|
+
}
|
|
932
|
+
: resolvePipelineState(pipelineState);
|
|
876
933
|
const layout = fam.schema.drawHeaderUnion;
|
|
877
934
|
const isAtlasBucket = layout.atlasTextureBindings.size > 0;
|
|
878
935
|
const vsModule = fam.vsModule;
|
|
@@ -1147,7 +1204,23 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1147
1204
|
const perInstanceAttributes = spec.instanceAttributes !== undefined
|
|
1148
1205
|
? new Set(Object.keys(spec.instanceAttributes))
|
|
1149
1206
|
: new Set();
|
|
1150
|
-
|
|
1207
|
+
// If the spec has modeRules, pre-snapshot the rule-evaluated
|
|
1208
|
+
// descriptor so the bucket key + pipeline reflect post-rule
|
|
1209
|
+
// values. Without this, two ROs with identical PS but different
|
|
1210
|
+
// rule outputs would collide on the same bucket.
|
|
1211
|
+
let precomputedDescriptor;
|
|
1212
|
+
if (spec.modeRules !== undefined) {
|
|
1213
|
+
const uAvals = new Map();
|
|
1214
|
+
for (const [name, val] of Object.entries(spec.inputs)) {
|
|
1215
|
+
uAvals.set(name, asAval(val));
|
|
1216
|
+
}
|
|
1217
|
+
precomputedDescriptor = snapshotDescriptor(spec.pipelineState, sig, {
|
|
1218
|
+
modeRules: spec.modeRules,
|
|
1219
|
+
uniformAvals: uAvals,
|
|
1220
|
+
token: outerTok,
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
const bucket = findOrCreateBucket(spec.effect, spec.textures, spec.pipelineState, precomputedDescriptor);
|
|
1151
1224
|
const fam = familyFor(spec.effect);
|
|
1152
1225
|
const effectFields = fam.fieldsForEffect.get(spec.effect.id);
|
|
1153
1226
|
// Indices live in their own INDEX-usage buffer (WebGPU constraint).
|
|
@@ -1316,10 +1389,27 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1316
1389
|
// value must move the RO to a different bucket. Without this
|
|
1317
1390
|
// tracker the cval flip would be silently ignored.
|
|
1318
1391
|
drawIdToSpec[drawId] = spec;
|
|
1319
|
-
|
|
1320
|
-
|
|
1392
|
+
// skipSubscribe: this tracker doesn't install its own callbacks.
|
|
1393
|
+
// The scene-level `modeAvalToDrawIds` does it once per unique aval
|
|
1394
|
+
// (the 20k-ROs-share-one-cullCval optimization).
|
|
1395
|
+
// If the spec has modeRules, pre-build a uniforms-aval map the
|
|
1396
|
+
// rule closures will read against; the tracker auto-discovers
|
|
1397
|
+
// which avals each rule reads and surfaces them via forEachLeaf.
|
|
1398
|
+
let uniformAvals;
|
|
1399
|
+
if (spec.modeRules !== undefined) {
|
|
1400
|
+
const m = new Map();
|
|
1401
|
+
for (const [name, val] of Object.entries(spec.inputs)) {
|
|
1402
|
+
m.set(name, asAval(val));
|
|
1403
|
+
}
|
|
1404
|
+
uniformAvals = m;
|
|
1405
|
+
}
|
|
1406
|
+
const tracker = new ModeKeyTracker(spec.pipelineState, sig, () => { dirtyModeKeyDrawIds.add(drawId); }, {
|
|
1407
|
+
skipSubscribe: true,
|
|
1408
|
+
...(spec.modeRules !== undefined ? { modeRules: spec.modeRules } : {}),
|
|
1409
|
+
...(uniformAvals !== undefined ? { uniformAvals } : {}),
|
|
1321
1410
|
});
|
|
1322
1411
|
drawIdToModeTracker[drawId] = tracker;
|
|
1412
|
+
tracker.forEachLeaf((av) => subscribeModeLeaf(av, drawId));
|
|
1323
1413
|
// ─── §7 derived-uniforms registration ────────────────────────────
|
|
1324
1414
|
// A uniform binding on this RO is either a value (aval/constant) or a rule —
|
|
1325
1415
|
// collect the rules (explicit `derivedUniform(...)` values + standard trafo
|
|
@@ -1482,8 +1572,10 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1482
1572
|
drawIdToIndexAval[drawId] = undefined;
|
|
1483
1573
|
drawIdToSpec[drawId] = undefined;
|
|
1484
1574
|
const oldTracker = drawIdToModeTracker[drawId];
|
|
1485
|
-
if (oldTracker !== undefined)
|
|
1575
|
+
if (oldTracker !== undefined) {
|
|
1576
|
+
oldTracker.forEachLeaf((av) => unsubscribeModeLeaf(av, drawId));
|
|
1486
1577
|
oldTracker.dispose();
|
|
1578
|
+
}
|
|
1487
1579
|
drawIdToModeTracker[drawId] = undefined;
|
|
1488
1580
|
dirtyModeKeyDrawIds.delete(drawId);
|
|
1489
1581
|
stats.totalDraws--;
|
|
@@ -1592,24 +1684,100 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1592
1684
|
if (dirtyModeKeyDrawIds.size > 0) {
|
|
1593
1685
|
const dirty = [...dirtyModeKeyDrawIds];
|
|
1594
1686
|
dirtyModeKeyDrawIds.clear();
|
|
1687
|
+
// Bucket-level fast path: group dirty drawIds by bucket. If
|
|
1688
|
+
// ALL of a bucket's ROs are dirty (typical SG case where one
|
|
1689
|
+
// shared aval drives every leaf — e.g. <Sg CullMode={cullC}>
|
|
1690
|
+
// wrapping 20k leaves), the whole bucket transitions together.
|
|
1691
|
+
// Rebuild the bucket's pipeline + rename in bucketByKey,
|
|
1692
|
+
// O(1) work instead of N remove+add cycles.
|
|
1693
|
+
const byBucket = new Map();
|
|
1595
1694
|
for (const drawId of dirty) {
|
|
1596
|
-
const
|
|
1597
|
-
if (
|
|
1695
|
+
const b = drawIdToBucket[drawId];
|
|
1696
|
+
if (b === undefined)
|
|
1598
1697
|
continue; // RO removed since mark
|
|
1599
|
-
|
|
1600
|
-
if (
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1698
|
+
let arr = byBucket.get(b);
|
|
1699
|
+
if (arr === undefined) {
|
|
1700
|
+
arr = [];
|
|
1701
|
+
byBucket.set(b, arr);
|
|
1702
|
+
}
|
|
1703
|
+
arr.push(drawId);
|
|
1704
|
+
}
|
|
1705
|
+
for (const [bucket, ids] of byBucket) {
|
|
1706
|
+
// First recompute every dirty tracker so .modeKey is fresh.
|
|
1707
|
+
// Filter out ones whose value didn't actually change.
|
|
1708
|
+
const changed = [];
|
|
1709
|
+
for (const drawId of ids) {
|
|
1710
|
+
const tracker = drawIdToModeTracker[drawId];
|
|
1711
|
+
if (tracker === undefined)
|
|
1712
|
+
continue;
|
|
1713
|
+
const oldKey = tracker.modeKey;
|
|
1714
|
+
if (!tracker.recompute())
|
|
1715
|
+
continue;
|
|
1716
|
+
if (tracker.modeKey === oldKey)
|
|
1717
|
+
continue;
|
|
1718
|
+
changed.push(drawId);
|
|
1719
|
+
}
|
|
1720
|
+
if (changed.length === 0)
|
|
1606
1721
|
continue;
|
|
1607
|
-
|
|
1608
|
-
const
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1722
|
+
// Try the all-together fast path.
|
|
1723
|
+
const wholeBucket = changed.length === bucket.drawSlots.size;
|
|
1724
|
+
if (wholeBucket) {
|
|
1725
|
+
const repId = changed[0];
|
|
1726
|
+
const repTracker = drawIdToModeTracker[repId];
|
|
1727
|
+
const newKey = repTracker.modeKey;
|
|
1728
|
+
// Compute the new bucketByKey string; we need the family.
|
|
1729
|
+
const sample = drawIdToSpec[repId];
|
|
1730
|
+
const fam = familyFor(sample.effect);
|
|
1731
|
+
const newBk = `family#${fam.schema.id}|mk#${newKey.toString(16)}`;
|
|
1732
|
+
const existing = bucketByKey.get(newBk);
|
|
1733
|
+
if (existing === undefined) {
|
|
1734
|
+
// No conflict — rename + rebuild pipeline in place.
|
|
1735
|
+
// Find this bucket's current key by linear scan
|
|
1736
|
+
// (buckets ≤ ~100 typically, so this is cheap).
|
|
1737
|
+
let oldBk;
|
|
1738
|
+
for (const [k, v] of bucketByKey) {
|
|
1739
|
+
if (v === bucket) {
|
|
1740
|
+
oldBk = k;
|
|
1741
|
+
break;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
if (oldBk !== undefined)
|
|
1745
|
+
bucketByKey.delete(oldBk);
|
|
1746
|
+
bucketByKey.set(newBk, bucket);
|
|
1747
|
+
// Build the new pipeline with the snapshotted PS.
|
|
1748
|
+
const desc = repTracker.descriptor;
|
|
1749
|
+
const { pipelineLayout } = getBgl(bucket.layout, bucket.isAtlasBucket);
|
|
1750
|
+
const newPipeline = device.createRenderPipeline({
|
|
1751
|
+
label: `heapScene/${newBk}/pipeline`,
|
|
1752
|
+
layout: pipelineLayout,
|
|
1753
|
+
vertex: { module: familyFor(sample.effect).vsModule, entryPoint: familyFor(sample.effect).vsEntryName, buffers: [] },
|
|
1754
|
+
fragment: { module: familyFor(sample.effect).fsModule, entryPoint: familyFor(sample.effect).fsEntryName, targets: colorTargets },
|
|
1755
|
+
primitive: { topology: desc.topology, cullMode: desc.cullMode, frontFace: desc.frontFace },
|
|
1756
|
+
...(depthFormat !== undefined && desc.depth !== undefined
|
|
1757
|
+
? { depthStencil: { format: depthFormat, depthWriteEnabled: desc.depth.write, depthCompare: desc.depth.compare } }
|
|
1758
|
+
: depthFormat !== undefined
|
|
1759
|
+
? { depthStencil: { format: depthFormat, depthWriteEnabled: false, depthCompare: "always" } }
|
|
1760
|
+
: {}),
|
|
1761
|
+
});
|
|
1762
|
+
// Swap pipeline reference on the bucket's single slot.
|
|
1763
|
+
// (Multi-slot Phase 5c would update one slot at a time.)
|
|
1764
|
+
bucket.slots[0].pipeline = newPipeline;
|
|
1765
|
+
continue; // done with this bucket
|
|
1766
|
+
}
|
|
1767
|
+
// Conflict: another bucket already owns newBk. Fall through
|
|
1768
|
+
// to per-RO rebucket below, which will merge into `existing`.
|
|
1769
|
+
}
|
|
1770
|
+
// Slow path — per-RO rebucket (used when partial transition
|
|
1771
|
+
// OR when a merge with an existing destination bucket is
|
|
1772
|
+
// required).
|
|
1773
|
+
for (const drawId of changed) {
|
|
1774
|
+
const spec = drawIdToSpec[drawId];
|
|
1775
|
+
if (spec === undefined)
|
|
1776
|
+
continue;
|
|
1777
|
+
removeDraw(drawId);
|
|
1778
|
+
const newId = addDrawImpl(spec, tok);
|
|
1779
|
+
void newId;
|
|
1780
|
+
}
|
|
1613
1781
|
}
|
|
1614
1782
|
}
|
|
1615
1783
|
// 1. Pool: re-pack any aval whose value changed since last frame.
|