@aardworx/wombat.rendering 0.21.5 → 0.21.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/runtime/flattenTree.ts +91 -10
- package/src/runtime/hybridScene.ts +41 -37
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aardworx/wombat.rendering",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.6",
|
|
4
4
|
"description": "WebGPU rendering layer for the Wombat TypeScript stack — RenderObject/RenderTask/runtime + window glue, port of Aardvark.Rendering's lower layers on top of WebGPU and wombat.shader.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "krauthaufen",
|
|
@@ -7,14 +7,27 @@
|
|
|
7
7
|
// (separate Render passes, in the Command list's declared order).
|
|
8
8
|
//
|
|
9
9
|
// Reactive variants (`Adaptive`, `OrderedFromList`, `UnorderedFromSet`)
|
|
10
|
-
// stay reactive: marks propagate through
|
|
10
|
+
// stay reactive: marks propagate through the fused reader, and the
|
|
11
11
|
// resulting aset reflects current tree state without a manual rebuild.
|
|
12
12
|
//
|
|
13
13
|
// `OrderedFromList`'s `alist<RenderTree>` is converted via
|
|
14
14
|
// `AListBridges.toASet` — this drops index/order info, which we
|
|
15
15
|
// wouldn't honor anyway.
|
|
16
|
+
//
|
|
17
|
+
// FUSION: a reactive child-set used to lower through FIVE aset stages
|
|
18
|
+
// (`filter(static).collectSeq(...)` ∪ `filter(!static).collect(...)`),
|
|
19
|
+
// each with its own History + full per-element state — at collection
|
|
20
|
+
// scale that is several complete N-element states of pure plumbing.
|
|
21
|
+
// `FlattenChildSetReader` fuses them into ONE reader: static subtrees
|
|
22
|
+
// (the overwhelmingly common case) enumerate their RenderObjects
|
|
23
|
+
// directly; reactive subtrees get an inner reader exactly like
|
|
24
|
+
// `ASet.collect`'s. One History, one state, two caches.
|
|
16
25
|
|
|
17
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
ASet, AbstractDirtyReader, Cache, HashSetDelta, SetOperation,
|
|
28
|
+
hashSetDeltaMonoid,
|
|
29
|
+
type AbstractReader, type AdaptiveToken, type IHashSetReader, type aset,
|
|
30
|
+
} from "@aardworx/wombat.adaptive";
|
|
18
31
|
import { AListBridges } from "@aardworx/wombat.adaptive";
|
|
19
32
|
import type { RenderObject } from "../core/renderObject.js";
|
|
20
33
|
import type { RenderTree } from "../core/renderTree.js";
|
|
@@ -51,15 +64,83 @@ function* staticObjects(tree: RenderTree): Iterable<RenderObject> {
|
|
|
51
64
|
}
|
|
52
65
|
}
|
|
53
66
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
*
|
|
67
|
+
const INNER_TAG = "FlattenInnerReader";
|
|
68
|
+
const _tagIsInnerTag = (tag: unknown): boolean => tag === INNER_TAG;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Fused flattener for a reactive container of subtrees. Per child
|
|
72
|
+
* delta: a STATIC subtree contributes its objects directly (cached as
|
|
73
|
+
* an array so removal emits exactly what addition emitted); a
|
|
74
|
+
* reactive subtree contributes through an inner reader with
|
|
75
|
+
* `ASet.collect`'s dirty-tracking protocol.
|
|
76
|
+
*/
|
|
77
|
+
class FlattenChildSetReader extends AbstractDirtyReader<
|
|
78
|
+
IHashSetReader<RenderObject>,
|
|
79
|
+
HashSetDelta<RenderObject>
|
|
80
|
+
> {
|
|
81
|
+
private readonly _reader: IHashSetReader<RenderTree>;
|
|
82
|
+
private readonly _static: Cache<RenderTree, RenderObject[]>;
|
|
83
|
+
private readonly _inner: Cache<RenderTree, IHashSetReader<RenderObject>>;
|
|
84
|
+
|
|
85
|
+
constructor(children: aset<RenderTree>) {
|
|
86
|
+
super(hashSetDeltaMonoid<RenderObject>(), _tagIsInnerTag);
|
|
87
|
+
this._reader = children.getReader();
|
|
88
|
+
this._static = new Cache<RenderTree, RenderObject[]>(
|
|
89
|
+
(t) => [...staticObjects(t)],
|
|
90
|
+
);
|
|
91
|
+
this._inner = new Cache<RenderTree, IHashSetReader<RenderObject>>(
|
|
92
|
+
(t) => {
|
|
93
|
+
const r = flattenRenderTree(t).getReader();
|
|
94
|
+
r.tag = INNER_TAG;
|
|
95
|
+
return r;
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
override compute(
|
|
101
|
+
tok: AdaptiveToken,
|
|
102
|
+
dirty: Set<IHashSetReader<RenderObject>>,
|
|
103
|
+
): HashSetDelta<RenderObject> {
|
|
104
|
+
let deltas = this._reader.getChanges(tok).collect((d) => {
|
|
105
|
+
const t = d.value;
|
|
106
|
+
if (d.count === 1) {
|
|
107
|
+
if (isStaticTree(t)) {
|
|
108
|
+
let out = HashSetDelta.empty<RenderObject>();
|
|
109
|
+
for (const ro of this._static.invoke(t)) out = out.add(SetOperation.add(ro));
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
const r = this._inner.invoke(t);
|
|
113
|
+
dirty.delete(r);
|
|
114
|
+
return r.getChanges(tok);
|
|
115
|
+
}
|
|
116
|
+
if (d.count === -1) {
|
|
117
|
+
if (isStaticTree(t)) {
|
|
118
|
+
let out = HashSetDelta.empty<RenderObject>();
|
|
119
|
+
for (const ro of this._static.revokeUnsafe(t)) out = out.add(SetOperation.rem(ro));
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
const r = this._inner.tryRevokeAndGetDeleted(t);
|
|
123
|
+
if (r === undefined) return HashSetDelta.empty<RenderObject>();
|
|
124
|
+
dirty.delete(r.value);
|
|
125
|
+
if (r.deleted) {
|
|
126
|
+
r.value.outputs.remove(this);
|
|
127
|
+
return r.value.state.removeAll();
|
|
128
|
+
}
|
|
129
|
+
return r.value.getChanges(tok);
|
|
130
|
+
}
|
|
131
|
+
throw new Error("flattenTree: unexpected delta count");
|
|
132
|
+
});
|
|
133
|
+
for (const d of dirty) {
|
|
134
|
+
deltas = deltas.combine(d.getChanges(tok));
|
|
135
|
+
}
|
|
136
|
+
return deltas;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
59
140
|
function flattenChildSet(children: aset<RenderTree>): aset<RenderObject> {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
141
|
+
return ASet.ofReader(
|
|
142
|
+
() => new FlattenChildSetReader(children) as unknown as AbstractReader<HashSetDelta<RenderObject>>,
|
|
143
|
+
);
|
|
63
144
|
}
|
|
64
145
|
|
|
65
146
|
export function flattenRenderTree(tree: RenderTree): aset<RenderObject> {
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
// hybridScene — composes the heap-bucket fast path with the legacy
|
|
2
2
|
// per-RO path into a single render-pass renderer.
|
|
3
3
|
//
|
|
4
|
-
// Pipeline:
|
|
4
|
+
// Pipeline (fused — one aset stage per backend):
|
|
5
5
|
// 1. `flattenRenderTree(tree)` → `aset<RenderObject>` (drops intra-
|
|
6
6
|
// pass ordering by contract).
|
|
7
|
-
// 2. `
|
|
7
|
+
// 2. `flat.chooseA(ro => elig(ro).map(e => e ? spec : undefined))`
|
|
8
|
+
// → `aset<HeapDrawSpec>` fed to `buildHeapScene`; the adapter is
|
|
9
|
+
// memoized per-RO so the same RO always maps to the same spec
|
|
10
|
+
// object (delta-driven removal identifies the right entry).
|
|
8
11
|
// Reactive: an RO migrates when its eligibility predicate marks
|
|
9
12
|
// (e.g. an `aval<IBuffer>` flips host↔gpu).
|
|
10
|
-
// 3. `
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// 4. `legacyAset` wrapped as `RenderTree.unorderedFromSet(map(leaf))`
|
|
15
|
-
// and fed to `ScenePass`. Same NodeWalker machinery that the
|
|
16
|
-
// legacy `RenderTask` uses.
|
|
13
|
+
// 3. `flat.chooseA(ro => elig(ro).map(e => e ? undefined : leaf))`
|
|
14
|
+
// wrapped as `RenderTree.unorderedFromSet` and fed to
|
|
15
|
+
// `ScenePass`. Same NodeWalker machinery as the legacy
|
|
16
|
+
// `RenderTask`.
|
|
17
17
|
//
|
|
18
18
|
// Per-frame:
|
|
19
19
|
// - `update(token)` runs both backends' update phases (CPU work
|
|
@@ -196,21 +196,25 @@ export function compileHybridScene(
|
|
|
196
196
|
e.compile({ target: "wgsl", fragmentOutputLayout }));
|
|
197
197
|
|
|
198
198
|
// ─── Partition ───────────────────────────────────────────────────
|
|
199
|
-
// Memoize the eligibility predicate per-RO so
|
|
200
|
-
// share one underlying observation.
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
//
|
|
199
|
+
// Memoize the eligibility predicate per-RO so both partition sides
|
|
200
|
+
// share one underlying observation. The per-RO eligibility is ANDed
|
|
201
|
+
// with the global `heapEnabled` toggle — flipping that off forces
|
|
202
|
+
// every RO to legacy.
|
|
203
|
+
//
|
|
204
|
+
// FUSION: each side is ONE `chooseA` straight off `flat` — the old
|
|
205
|
+
// `filterA(elig)` + `choose(spec)` (heap) and `filterA(notElig)` +
|
|
206
|
+
// `map(leaf)` (legacy) pipelines carried an intermediate aset each,
|
|
207
|
+
// i.e. a full History + per-element state of pure plumbing, plus a
|
|
208
|
+
// negation aval per RO. `chooseA`'s per-element wrapper rides
|
|
209
|
+
// `elig(ro).map(...)`, which for a CONSTANT eligibility (asserted
|
|
210
|
+
// rows under a constant `heapEnabled`, or fully-static ROs)
|
|
211
|
+
// collapses to a lazy constant — no adaptive node, no edges.
|
|
204
212
|
const heapEnabled = opts.heapEnabled ?? AVal.constant(true);
|
|
205
|
-
// Producer-asserted ROs (`heapAsserted: true`) skip the per-RO
|
|
206
|
-
// predicate entirely — they all share these TWO scene-level avals
|
|
207
|
-
// instead of two live avals each (see RenderObject.heapAsserted).
|
|
208
|
-
const notHeapEnabled = heapEnabled.isConstant
|
|
209
|
-
? AVal.constant(!heapEnabled.force(/* allow-force */))
|
|
210
|
-
: heapEnabled.map(b => !b);
|
|
211
213
|
const eligCache = new WeakMap<RenderObject, aval<boolean>>();
|
|
212
|
-
const notEligCache = new WeakMap<RenderObject, aval<boolean>>();
|
|
213
214
|
const elig = (ro: RenderObject): aval<boolean> => {
|
|
215
|
+
// Producer-asserted ROs (`heapAsserted: true`) skip the per-RO
|
|
216
|
+
// predicate entirely — they all share the scene-level toggle aval
|
|
217
|
+
// (see RenderObject.heapAsserted).
|
|
214
218
|
if (ro.heapAsserted === true) return heapEnabled;
|
|
215
219
|
let av = eligCache.get(ro);
|
|
216
220
|
if (av === undefined) {
|
|
@@ -224,20 +228,8 @@ export function compileHybridScene(
|
|
|
224
228
|
}
|
|
225
229
|
return av;
|
|
226
230
|
};
|
|
227
|
-
const notElig = (ro: RenderObject): aval<boolean> => {
|
|
228
|
-
if (ro.heapAsserted === true) return notHeapEnabled;
|
|
229
|
-
let av = notEligCache.get(ro);
|
|
230
|
-
if (av === undefined) {
|
|
231
|
-
const e = elig(ro);
|
|
232
|
-
av = e.isConstant ? AVal.constant(!e.force(/* allow-force */)) : e.map(b => !b);
|
|
233
|
-
notEligCache.set(ro, av);
|
|
234
|
-
}
|
|
235
|
-
return av;
|
|
236
|
-
};
|
|
237
231
|
|
|
238
232
|
const flat = flattenRenderTree(tree);
|
|
239
|
-
const heapAset = flat.filterA(ro => elig(ro));
|
|
240
|
-
const legacyAset = flat.filterA(ro => notElig(ro));
|
|
241
233
|
|
|
242
234
|
// ─── Atlas pool ──────────────────────────────────────────────────
|
|
243
235
|
// Per-scene Tier-S atlas pool. Owned by this hybrid scene; disposed
|
|
@@ -262,7 +254,7 @@ export function compileHybridScene(
|
|
|
262
254
|
// usually have freed atlas space). It costs a missing tile for a frame or two;
|
|
263
255
|
// the alternative was an exception in the frame callback, which killed the
|
|
264
256
|
// render loop outright.
|
|
265
|
-
const
|
|
257
|
+
const specOf = (ro: RenderObject): HeapDrawSpec | undefined => {
|
|
266
258
|
let spec = specCache.get(ro);
|
|
267
259
|
if (spec === undefined) {
|
|
268
260
|
const built = renderObjectToHeapSpec(ro, AdaptiveToken.top, atlasPool, {
|
|
@@ -273,7 +265,10 @@ export function compileHybridScene(
|
|
|
273
265
|
specCache.set(ro, spec);
|
|
274
266
|
}
|
|
275
267
|
return spec;
|
|
276
|
-
}
|
|
268
|
+
};
|
|
269
|
+
// ONE fused stage: eligibility routes straight into the spec.
|
|
270
|
+
const heapSpecAset = flat.chooseA((ro: RenderObject) =>
|
|
271
|
+
elig(ro).map((e) => (e ? specOf(ro) : undefined)));
|
|
277
272
|
|
|
278
273
|
const heapScene: HeapScene = buildHeapScene(device, signature, heapSpecAset, {
|
|
279
274
|
fragmentOutputLayout,
|
|
@@ -291,8 +286,17 @@ export function compileHybridScene(
|
|
|
291
286
|
// path inherits the same "no order inside a pass" contract. Re-using
|
|
292
287
|
// the existing NodeWalker machinery means RO preparation, caching,
|
|
293
288
|
// and resource ref-counting all behave exactly as the master path.
|
|
294
|
-
|
|
295
|
-
|
|
289
|
+
// ONE fused stage (ineligible → leaf); the leaf wrapper is memoized
|
|
290
|
+
// so eligibility flips re-emit the identical tree object.
|
|
291
|
+
const leafCache = new WeakMap<RenderObject, RenderTree>();
|
|
292
|
+
const leafOf = (ro: RenderObject): RenderTree => {
|
|
293
|
+
let l = leafCache.get(ro);
|
|
294
|
+
if (l === undefined) { l = RenderTree.leaf(ro); leafCache.set(ro, l); }
|
|
295
|
+
return l;
|
|
296
|
+
};
|
|
297
|
+
const legacyTree: RenderTree = RenderTree.unorderedFromSet(
|
|
298
|
+
flat.chooseA((ro: RenderObject) =>
|
|
299
|
+
elig(ro).map((e) => (e ? undefined : leafOf(ro)))));
|
|
296
300
|
const scenePass = new ScenePass(device, signature, legacyTree, compileEffect);
|
|
297
301
|
// DEBUG registry: per-hybrid-scene live draw counts (heap vs legacy) so
|
|
298
302
|
// multi-task pipelines (OIT) can be inspected from the console.
|