@aardworx/wombat.rendering 0.19.23 → 0.19.25
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/elementType.d.ts +2 -0
- package/dist/core/elementType.d.ts.map +1 -1
- package/dist/core/elementType.js +10 -0
- package/dist/core/elementType.js.map +1 -1
- package/dist/core/provider.d.ts.map +1 -1
- package/dist/core/provider.js +65 -32
- package/dist/core/provider.js.map +1 -1
- package/dist/core/renderObject.d.ts +9 -0
- package/dist/core/renderObject.d.ts.map +1 -1
- package/dist/resources/preparedRenderObject.d.ts.map +1 -1
- package/dist/resources/preparedRenderObject.js +19 -2
- package/dist/resources/preparedRenderObject.js.map +1 -1
- package/dist/runtime/derivedModes/modeKeyCpu.d.ts +2 -12
- package/dist/runtime/derivedModes/modeKeyCpu.d.ts.map +1 -1
- package/dist/runtime/derivedModes/modeKeyCpu.js +30 -13
- package/dist/runtime/derivedModes/modeKeyCpu.js.map +1 -1
- package/dist/runtime/derivedModes/partitionDispatcher.d.ts +11 -0
- package/dist/runtime/derivedModes/partitionDispatcher.d.ts.map +1 -1
- package/dist/runtime/derivedModes/partitionDispatcher.js +31 -0
- package/dist/runtime/derivedModes/partitionDispatcher.js.map +1 -1
- package/dist/runtime/derivedUniforms/dispatch.d.ts +5 -1
- package/dist/runtime/derivedUniforms/dispatch.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/dispatch.js +28 -13
- package/dist/runtime/derivedUniforms/dispatch.js.map +1 -1
- package/dist/runtime/derivedUniforms/records.d.ts +15 -0
- package/dist/runtime/derivedUniforms/records.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/records.js +81 -14
- package/dist/runtime/derivedUniforms/records.js.map +1 -1
- package/dist/runtime/derivedUniforms/sceneIntegration.d.ts +6 -0
- package/dist/runtime/derivedUniforms/sceneIntegration.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/sceneIntegration.js +9 -0
- package/dist/runtime/derivedUniforms/sceneIntegration.js.map +1 -1
- package/dist/runtime/flattenTree.d.ts.map +1 -1
- package/dist/runtime/flattenTree.js +46 -2
- package/dist/runtime/flattenTree.js.map +1 -1
- package/dist/runtime/heapAdapter.d.ts +3 -1
- package/dist/runtime/heapAdapter.d.ts.map +1 -1
- package/dist/runtime/heapAdapter.js +42 -4
- package/dist/runtime/heapAdapter.js.map +1 -1
- package/dist/runtime/heapDecoder.d.ts.map +1 -1
- package/dist/runtime/heapDecoder.js +8 -3
- package/dist/runtime/heapDecoder.js.map +1 -1
- package/dist/runtime/heapEffect.d.ts +14 -1
- package/dist/runtime/heapEffect.d.ts.map +1 -1
- package/dist/runtime/heapEffect.js +13 -1
- package/dist/runtime/heapEffect.js.map +1 -1
- package/dist/runtime/heapEffectIR.d.ts.map +1 -1
- package/dist/runtime/heapEffectIR.js +18 -9
- package/dist/runtime/heapEffectIR.js.map +1 -1
- package/dist/runtime/heapEligibility.d.ts.map +1 -1
- package/dist/runtime/heapEligibility.js +64 -12
- package/dist/runtime/heapEligibility.js.map +1 -1
- package/dist/runtime/heapIrBuilders.d.ts +8 -0
- package/dist/runtime/heapIrBuilders.d.ts.map +1 -1
- package/dist/runtime/heapIrBuilders.js +47 -2
- package/dist/runtime/heapIrBuilders.js.map +1 -1
- package/dist/runtime/heapScene/chunkedArena.d.ts +20 -28
- package/dist/runtime/heapScene/chunkedArena.d.ts.map +1 -1
- package/dist/runtime/heapScene/chunkedArena.js +28 -91
- package/dist/runtime/heapScene/chunkedArena.js.map +1 -1
- package/dist/runtime/heapScene/freelist.d.ts +1 -1
- package/dist/runtime/heapScene/freelist.js +3 -3
- package/dist/runtime/heapScene/freelist.js.map +1 -1
- package/dist/runtime/heapScene/pools.d.ts +103 -59
- package/dist/runtime/heapScene/pools.d.ts.map +1 -1
- package/dist/runtime/heapScene/pools.js +335 -194
- package/dist/runtime/heapScene/pools.js.map +1 -1
- package/dist/runtime/heapScene.d.ts +116 -1
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +581 -125
- package/dist/runtime/heapScene.js.map +1 -1
- package/dist/runtime/hybridScene.d.ts.map +1 -1
- package/dist/runtime/hybridScene.js +19 -3
- package/dist/runtime/hybridScene.js.map +1 -1
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/elementType.ts +10 -0
- package/src/core/provider.ts +56 -28
- package/src/core/renderObject.ts +9 -0
- package/src/resources/preparedRenderObject.ts +22 -3
- package/src/runtime/derivedModes/modeKeyCpu.ts +26 -11
- package/src/runtime/derivedModes/partitionDispatcher.ts +30 -0
- package/src/runtime/derivedUniforms/dispatch.ts +26 -10
- package/src/runtime/derivedUniforms/records.ts +70 -15
- package/src/runtime/derivedUniforms/sceneIntegration.ts +10 -0
- package/src/runtime/flattenTree.ts +47 -2
- package/src/runtime/heapAdapter.ts +40 -5
- package/src/runtime/heapDecoder.ts +8 -3
- package/src/runtime/heapEffect.ts +15 -1
- package/src/runtime/heapEffectIR.ts +18 -9
- package/src/runtime/heapEligibility.ts +39 -12
- package/src/runtime/heapIrBuilders.ts +50 -2
- package/src/runtime/heapScene/chunkedArena.ts +29 -94
- package/src/runtime/heapScene/freelist.ts +3 -3
- package/src/runtime/heapScene/pools.ts +340 -218
- package/src/runtime/heapScene.ts +717 -141
- package/src/runtime/hybridScene.ts +20 -3
- package/src/runtime/index.ts +5 -0
|
@@ -48,7 +48,42 @@ const MIN_STRIDE_U32 = 5; // rule_id, out_slot, in0, in1, in2 — covers the 13
|
|
|
48
48
|
export class RecordsBuffer {
|
|
49
49
|
private buf: Uint32Array;
|
|
50
50
|
private owners: (RecordOwner | undefined)[] = [];
|
|
51
|
-
|
|
51
|
+
/** Record indices per owner. A single number for the common one-
|
|
52
|
+
* record-per-owner case, promoted to a small array on the second —
|
|
53
|
+
* saves a whole `Set` per RO at heap scale. */
|
|
54
|
+
private readonly byOwner = new Map<RecordOwner, number | number[]>();
|
|
55
|
+
|
|
56
|
+
private ownerAdd(owner: RecordOwner, idx: number): void {
|
|
57
|
+
const v = this.byOwner.get(owner);
|
|
58
|
+
if (v === undefined) this.byOwner.set(owner, idx);
|
|
59
|
+
else if (typeof v === "number") this.byOwner.set(owner, [v, idx]);
|
|
60
|
+
else v.push(idx);
|
|
61
|
+
}
|
|
62
|
+
private ownerDelete(owner: RecordOwner, idx: number): void {
|
|
63
|
+
const v = this.byOwner.get(owner);
|
|
64
|
+
if (v === undefined) return;
|
|
65
|
+
if (typeof v === "number") {
|
|
66
|
+
if (v === idx) this.byOwner.delete(owner);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const i = v.indexOf(idx);
|
|
70
|
+
if (i >= 0) {
|
|
71
|
+
v[i] = v[v.length - 1]!;
|
|
72
|
+
v.pop();
|
|
73
|
+
if (v.length === 1) this.byOwner.set(owner, v[0]!);
|
|
74
|
+
else if (v.length === 0) this.byOwner.delete(owner);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
private ownerReplace(owner: RecordOwner, oldIdx: number, newIdx: number): void {
|
|
78
|
+
const v = this.byOwner.get(owner);
|
|
79
|
+
if (v === undefined) return;
|
|
80
|
+
if (typeof v === "number") {
|
|
81
|
+
if (v === oldIdx) this.byOwner.set(owner, newIdx);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const i = v.indexOf(oldIdx);
|
|
85
|
+
if (i >= 0) v[i] = newIdx;
|
|
86
|
+
}
|
|
52
87
|
private strideU32 = MIN_STRIDE_U32;
|
|
53
88
|
private count = 0;
|
|
54
89
|
/** Bumps whenever the byte layout changes shape (stride growth) so the dispatcher recompiles. */
|
|
@@ -86,44 +121,64 @@ export class RecordsBuffer {
|
|
|
86
121
|
for (let i = 0; i < inSlots.length; i++) this.buf[base + 2 + i] = inSlots[i]! >>> 0;
|
|
87
122
|
for (let i = base + 2 + inSlots.length; i < base + this.strideU32; i++) this.buf[i] = 0;
|
|
88
123
|
this.owners[idx] = owner;
|
|
89
|
-
|
|
90
|
-
if (set === undefined) {
|
|
91
|
-
set = new Set();
|
|
92
|
-
this.byOwner.set(owner, set);
|
|
93
|
-
}
|
|
94
|
-
set.add(idx);
|
|
124
|
+
this.ownerAdd(owner, idx);
|
|
95
125
|
this.generation++;
|
|
96
126
|
return idx;
|
|
97
127
|
}
|
|
98
128
|
|
|
99
129
|
/** Remove every record owned by `owner` (swap-remove against the tail). */
|
|
100
130
|
removeAllForOwner(owner: RecordOwner): void {
|
|
101
|
-
const
|
|
102
|
-
if (
|
|
103
|
-
this.byOwner.delete(owner);
|
|
131
|
+
const v = this.byOwner.get(owner);
|
|
132
|
+
if (v === undefined) {
|
|
104
133
|
return;
|
|
105
134
|
}
|
|
106
135
|
// Process highest indices first so a swap can never move a not-yet-removed
|
|
107
136
|
// record of this owner that we still hold a stale index for.
|
|
108
|
-
const indices = [...
|
|
137
|
+
const indices = typeof v === "number" ? [v] : [...v].sort((a, b) => b - a);
|
|
109
138
|
for (const idx of indices) this.removeAt(idx);
|
|
110
139
|
this.byOwner.delete(owner);
|
|
111
140
|
this.generation++;
|
|
112
141
|
}
|
|
113
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Re-seat `HostHeap` handles after a main-heap compaction relocated their
|
|
145
|
+
* backing allocations. `remap` maps an OLD handle payload (the data byte
|
|
146
|
+
* offset in the main heap) to its NEW one. `Constituent` handles point into
|
|
147
|
+
* the separate df32 slot buffer (never compacted) and are left untouched.
|
|
148
|
+
* Returns the number of handle words rewritten; bumps `generation` so the
|
|
149
|
+
* dispatcher re-uploads. O(records × stride).
|
|
150
|
+
*/
|
|
151
|
+
remapHostHeap(remap: ReadonlyMap<number, number>): number {
|
|
152
|
+
if (remap.size === 0 || this.count === 0) return 0;
|
|
153
|
+
const s = this.strideU32;
|
|
154
|
+
let changed = 0;
|
|
155
|
+
for (let i = 0; i < this.count; i++) {
|
|
156
|
+
const base = i * s;
|
|
157
|
+
// out_slot at base+1, in_slots at base+2 .. base+s-1.
|
|
158
|
+
for (let w = base + 1; w < base + s; w++) {
|
|
159
|
+
const h = this.buf[w]!;
|
|
160
|
+
if (handleTag(h) !== SlotTag.HostHeap) continue;
|
|
161
|
+
const nn = remap.get(handlePayload(h));
|
|
162
|
+
if (nn === undefined) continue;
|
|
163
|
+
this.buf[w] = makeHandle(SlotTag.HostHeap, nn);
|
|
164
|
+
changed++;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (changed > 0) this.generation++;
|
|
168
|
+
return changed;
|
|
169
|
+
}
|
|
170
|
+
|
|
114
171
|
private removeAt(idx: number): void {
|
|
115
172
|
const last = this.count - 1;
|
|
116
173
|
// The record currently at `idx` is the one being removed.
|
|
117
|
-
this.
|
|
174
|
+
this.ownerDelete(this.owners[idx]!, idx);
|
|
118
175
|
if (idx !== last) {
|
|
119
176
|
// Move the tail record's words into the hole, and re-home it.
|
|
120
177
|
const dst = idx * this.strideU32;
|
|
121
178
|
const src = last * this.strideU32;
|
|
122
179
|
this.buf.copyWithin(dst, src, src + this.strideU32);
|
|
123
180
|
const lastOwner = this.owners[last]!;
|
|
124
|
-
|
|
125
|
-
lset.delete(last);
|
|
126
|
-
lset.add(idx);
|
|
181
|
+
this.ownerReplace(lastOwner, last, idx);
|
|
127
182
|
this.owners[idx] = lastOwner;
|
|
128
183
|
}
|
|
129
184
|
this.owners[last] = undefined;
|
|
@@ -109,6 +109,16 @@ export class DerivedUniformsScene {
|
|
|
109
109
|
return r;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Re-seat the per-chunk derived-uniform records' `HostHeap` handles
|
|
114
|
+
* (derived outputs and host-uniform inputs) after the main heap compacted
|
|
115
|
+
* `chunkIdx`. `remap` maps OLD→NEW data byte offsets. Returns words changed.
|
|
116
|
+
*/
|
|
117
|
+
remapHostHeap(chunkIdx: number, remap: ReadonlyMap<number, number>): number {
|
|
118
|
+
const r = this.recordsByChunk.get(chunkIdx);
|
|
119
|
+
return r === undefined ? 0 : r.remapHostHeap(remap);
|
|
120
|
+
}
|
|
121
|
+
|
|
112
122
|
/** Total record count summed across every chunk's records buffer.
|
|
113
123
|
* Used by the scene's stats / diagnostics. */
|
|
114
124
|
get totalRecordCount(): number {
|
|
@@ -19,6 +19,49 @@ import { AListBridges } from "@aardworx/wombat.adaptive";
|
|
|
19
19
|
import type { RenderObject } from "../core/renderObject.js";
|
|
20
20
|
import type { RenderTree } from "../core/renderTree.js";
|
|
21
21
|
|
|
22
|
+
/** True when `tree` contains no reactive nodes — its RenderObjects can
|
|
23
|
+
* be enumerated as a plain iterable (no inner aset / reader needed). */
|
|
24
|
+
function isStaticTree(tree: RenderTree): boolean {
|
|
25
|
+
switch (tree.kind) {
|
|
26
|
+
case "Empty":
|
|
27
|
+
case "Leaf":
|
|
28
|
+
return true;
|
|
29
|
+
case "Ordered":
|
|
30
|
+
case "Unordered":
|
|
31
|
+
return tree.children.every(isStaticTree);
|
|
32
|
+
default:
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Enumerate a static subtree's RenderObjects (pre: `isStaticTree`). */
|
|
38
|
+
function* staticObjects(tree: RenderTree): Iterable<RenderObject> {
|
|
39
|
+
switch (tree.kind) {
|
|
40
|
+
case "Empty":
|
|
41
|
+
return;
|
|
42
|
+
case "Leaf":
|
|
43
|
+
yield tree.object;
|
|
44
|
+
return;
|
|
45
|
+
case "Ordered":
|
|
46
|
+
case "Unordered":
|
|
47
|
+
for (const c of tree.children) yield* staticObjects(c);
|
|
48
|
+
return;
|
|
49
|
+
default:
|
|
50
|
+
throw new Error(`staticObjects: reactive node '${tree.kind}'`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Flatten a reactive container of subtrees. The static children go
|
|
55
|
+
* through `collectSeq` (plain iterables — ONE reader for the whole
|
|
56
|
+
* set, instead of a ConstantAset + reader + subscription PER LEAF,
|
|
57
|
+
* which at heap scale was megabytes of ballast); reactive children
|
|
58
|
+
* keep the full `collect`. */
|
|
59
|
+
function flattenChildSet(children: aset<RenderTree>): aset<RenderObject> {
|
|
60
|
+
const staticPart = children.filter(isStaticTree).collectSeq(staticObjects);
|
|
61
|
+
const reactivePart = children.filter(t => !isStaticTree(t)).collect(flattenRenderTree);
|
|
62
|
+
return ASet.union(staticPart, reactivePart);
|
|
63
|
+
}
|
|
64
|
+
|
|
22
65
|
export function flattenRenderTree(tree: RenderTree): aset<RenderObject> {
|
|
23
66
|
switch (tree.kind) {
|
|
24
67
|
case "Empty":
|
|
@@ -27,6 +70,8 @@ export function flattenRenderTree(tree: RenderTree): aset<RenderObject> {
|
|
|
27
70
|
return ASet.single(tree.object);
|
|
28
71
|
case "Ordered":
|
|
29
72
|
case "Unordered": {
|
|
73
|
+
// Fully-static subtree → one constant aset over a plain array.
|
|
74
|
+
if (isStaticTree(tree)) return ASet.ofArray([...staticObjects(tree)]);
|
|
30
75
|
// N-ary union via `unionMany(aset<aset<T>>)`. The naïve
|
|
31
76
|
// `children.reduce(ASet.union)` builds a 2N-deep nested-union
|
|
32
77
|
// tree; the reader's traversal recurses through it and blows
|
|
@@ -39,8 +84,8 @@ export function flattenRenderTree(tree: RenderTree): aset<RenderObject> {
|
|
|
39
84
|
case "Adaptive":
|
|
40
85
|
return ASet.bind(flattenRenderTree, tree.tree);
|
|
41
86
|
case "OrderedFromList":
|
|
42
|
-
return
|
|
87
|
+
return flattenChildSet(AListBridges.toASet(tree.children));
|
|
43
88
|
case "UnorderedFromSet":
|
|
44
|
-
return
|
|
89
|
+
return flattenChildSet(tree.children);
|
|
45
90
|
}
|
|
46
91
|
}
|
|
@@ -113,6 +113,8 @@ import type { HeapDrawSpec, HeapTextureSet } from "./heapScene.js";
|
|
|
113
113
|
import { isBufferView } from "./heapScene/pools.js";
|
|
114
114
|
import { compileHeapEffect } from "./heapEffect.js";
|
|
115
115
|
import { AtlasPool } from "./textureAtlas/atlasPool.js";
|
|
116
|
+
import { STANDARD_DERIVED_RULES, STANDARD_TRAFO_LEAVES } from "./derivedUniforms/recipes.js";
|
|
117
|
+
import { inputsOf } from "./derivedUniforms/flatten.js";
|
|
116
118
|
|
|
117
119
|
/**
|
|
118
120
|
* View `HostBufferSource` as `Uint32Array`. Index buffers are u32 in
|
|
@@ -326,6 +328,7 @@ export function renderObjectToHeapSpec(
|
|
|
326
328
|
ro: RenderObject,
|
|
327
329
|
token: AdaptiveToken,
|
|
328
330
|
pool?: AtlasPool,
|
|
331
|
+
opts?: { readonly enableDerivedUniforms?: boolean },
|
|
329
332
|
): HeapDrawSpec {
|
|
330
333
|
// 1. Inputs map: vertex attributes (BufferView) + uniforms, pulled
|
|
331
334
|
// SHADER-DRIVEN from the providers. We compile `ro.effect` (cached
|
|
@@ -356,12 +359,38 @@ export function renderObjectToHeapSpec(
|
|
|
356
359
|
const av = uProv.tryGet(name);
|
|
357
360
|
if (av !== undefined) inputs[name] = av;
|
|
358
361
|
};
|
|
359
|
-
|
|
362
|
+
// §7-covered derived trafos (ModelViewProjTrafo, NormalMatrix, …) are
|
|
363
|
+
// GPU-computed from the M/V/P constituents — addDraw never reads their
|
|
364
|
+
// `spec.inputs` entry when the rule applies. Deferring the pull keeps
|
|
365
|
+
// the provider from materialising a per-RO derived-aval chain (MapVal +
|
|
366
|
+
// nested closure contexts + a never-forced lazy constant) that would sit
|
|
367
|
+
// in `inputs` as pure ballast. Pulled after all as a fallback only when
|
|
368
|
+
// the recipe's base leaves turn out not to be bound (mirrors
|
|
369
|
+
// heapScene's `ruleForUniform` applicability test).
|
|
370
|
+
const deriveOnGpu = opts?.enableDerivedUniforms !== false;
|
|
371
|
+
const deferredDerived: string[] = [];
|
|
372
|
+
for (const u of schema.uniforms) {
|
|
373
|
+
if (deriveOnGpu && STANDARD_DERIVED_RULES.has(u.name)) {
|
|
374
|
+
deferredDerived.push(u.name);
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
pullUniform(u.name);
|
|
378
|
+
}
|
|
360
379
|
// §7 derived-uniforms constituents — cheap (these are the raw
|
|
361
380
|
// `state.model/view/proj` avals, no `compose`/`inverse`) and the
|
|
362
381
|
// compute pre-pass needs them even when the effect itself doesn't
|
|
363
382
|
// declare them. No-ops if the provider doesn't carry them.
|
|
364
383
|
for (const n of ["ModelTrafo", "ViewTrafo", "ProjTrafo"]) pullUniform(n);
|
|
384
|
+
for (const name of deferredDerived) {
|
|
385
|
+
const rule = STANDARD_DERIVED_RULES.get(name)!;
|
|
386
|
+
let leavesBound = true;
|
|
387
|
+
for (const inp of inputsOf(rule.ir)) {
|
|
388
|
+
if (inp.name === "Model" && ro.modelChain !== undefined) continue;
|
|
389
|
+
const specName = STANDARD_TRAFO_LEAVES.get(inp.name) ?? inp.name;
|
|
390
|
+
if (inputs[specName] === undefined) { leavesBound = false; break; }
|
|
391
|
+
}
|
|
392
|
+
if (!leavesBound) pullUniform(name);
|
|
393
|
+
}
|
|
365
394
|
|
|
366
395
|
// 1b. Instance attributes — provider is map-backed (user-supplied via
|
|
367
396
|
// `Sg.instanced({attributes})`), so enumerate its names. Threaded
|
|
@@ -399,9 +428,11 @@ export function renderObjectToHeapSpec(
|
|
|
399
428
|
// WGSL schema's binding shape doesn't matter at scene time — that
|
|
400
429
|
// leaves us with two HashMap entries pointing at the same aval.
|
|
401
430
|
// Dedupe by identity before applying the single-pair rule.
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
431
|
+
// `let` + nulled after use: the spec's long-lived closures share
|
|
432
|
+
// this scope context — a leftover Set here is per-RO ballast.
|
|
433
|
+
let distinctTexAvals: Set<aval<ITexture>> | undefined = new Set<aval<ITexture>>();
|
|
434
|
+
ro.textures.iter((_n, av) => { distinctTexAvals!.add(av as aval<ITexture>); });
|
|
435
|
+
let distinctSamplerAvals: Set<aval<ISampler>> | undefined = new Set<aval<ISampler>>();
|
|
405
436
|
// Shader-defined sampler state (from a `sampler2d { filter …; addressU … }`
|
|
406
437
|
// builder, carried through the IR) overrides the scene's default sampler.
|
|
407
438
|
const stateBinding = schema.samplers.find(b => b.state !== undefined);
|
|
@@ -418,7 +449,7 @@ export function renderObjectToHeapSpec(
|
|
|
418
449
|
AVal.constant(ISampler.fromDescriptor(desc)) as aval<ISampler>,
|
|
419
450
|
);
|
|
420
451
|
} else {
|
|
421
|
-
ro.samplers.iter((_n, av) => { distinctSamplerAvals
|
|
452
|
+
ro.samplers.iter((_n, av) => { distinctSamplerAvals!.add(av as aval<ISampler>); });
|
|
422
453
|
}
|
|
423
454
|
let textures: HeapTextureSet | undefined;
|
|
424
455
|
if (distinctTexAvals.size === 1 && distinctSamplerAvals.size === 1) {
|
|
@@ -531,6 +562,9 @@ export function renderObjectToHeapSpec(
|
|
|
531
562
|
`single-pair only in v1 (classifier should have caught this)`,
|
|
532
563
|
);
|
|
533
564
|
}
|
|
565
|
+
// Drop the dedup scratch — see the `let` note above.
|
|
566
|
+
distinctTexAvals = undefined;
|
|
567
|
+
distinctSamplerAvals = undefined;
|
|
534
568
|
|
|
535
569
|
// 4. DrawCall geometry. Classifier validates fields are heap-
|
|
536
570
|
// compatible (instanceCount≥1, firstInstance=0, non-indexed firstVertex=0).
|
|
@@ -550,6 +584,7 @@ export function renderObjectToHeapSpec(
|
|
|
550
584
|
|
|
551
585
|
return {
|
|
552
586
|
effect: ro.effect,
|
|
587
|
+
...(ro.pickId !== undefined ? { pickId: ro.pickId } : {}),
|
|
553
588
|
pipelineState: ro.pipelineState,
|
|
554
589
|
inputs,
|
|
555
590
|
...(instanceAttributes !== undefined ? { instanceAttributes } : {}),
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
Tu32, Tf32,
|
|
47
47
|
} from "@aardworx/wombat.shader/ir";
|
|
48
48
|
import type { BucketLayout, DrawHeaderField } from "./heapEffect.js";
|
|
49
|
+
import { isInlineHeaderField } from "./heapEffect.js";
|
|
49
50
|
import {
|
|
50
51
|
Tvec2, Tvec3, Tvec4, Tmat4, TarrU32,
|
|
51
52
|
constU32, add, mul, div, item, newVec, readScope,
|
|
@@ -307,9 +308,13 @@ function addUniformOutput(
|
|
|
307
308
|
}
|
|
308
309
|
const irType = wgslTypeToIrType(wgslType);
|
|
309
310
|
const refExpr = loadHeaderRef(drawIdxExpr, f.byteOffset, layout.strideU32);
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
311
|
+
// Inline fields (PickId, __layoutId family): the header word IS the
|
|
312
|
+
// u32 value — no arena indirection, no pool entry per draw.
|
|
313
|
+
const value = isInlineHeaderField(f)
|
|
314
|
+
? refExpr
|
|
315
|
+
: layout.perInstanceUniforms.has(f.name)
|
|
316
|
+
? loadInstanceByRef(refExpr, instIdExpr, wgslType)
|
|
317
|
+
: loadUniformByRef(refExpr, wgslType);
|
|
313
318
|
|
|
314
319
|
outputs.push({
|
|
315
320
|
name: f.name,
|
|
@@ -104,7 +104,21 @@ export interface FragmentOutputLayout {
|
|
|
104
104
|
// and on quota; every failure simply degrades to "recompute".
|
|
105
105
|
|
|
106
106
|
/** Cache-generation stamp for the persistent (localStorage) tier. */
|
|
107
|
-
export const HEAP_PERSIST_VERSION = "
|
|
107
|
+
export const HEAP_PERSIST_VERSION = "h4"; // h4: inline PickId drawHeader read (was h3: packed decode arms)
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Uniforms whose drawHeader word IS the u32 value (no arena
|
|
111
|
+
* indirection, no pool entry, no per-RO aval) — the `__layoutId`
|
|
112
|
+
* mechanism generalised. Values come from first-class RenderObject
|
|
113
|
+
* fields (`ro.pickId`), not the uniform provider. Fields must be
|
|
114
|
+
* u32-typed.
|
|
115
|
+
*/
|
|
116
|
+
export const INLINE_HEADER_UNIFORMS: ReadonlySet<string> = new Set(["PickId"]);
|
|
117
|
+
|
|
118
|
+
/** True for fields read inline from the drawHeader word. */
|
|
119
|
+
export function isInlineHeaderField(f: { kind: string; name: string }): boolean {
|
|
120
|
+
return f.kind === "uniform-ref" && (f.name === "__layoutId" || INLINE_HEADER_UNIFORMS.has(f.name));
|
|
121
|
+
}
|
|
108
122
|
export const HEAP_PERSIST_PREFIX = "wbt.heapfx.";
|
|
109
123
|
|
|
110
124
|
export function persistKey(version: string, kind: string, contentKey: string): string {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
// consumes is identical to the one heapScene.ts already builds.
|
|
19
19
|
|
|
20
20
|
import { compileModule, stage as makeStage, effect as makeEffect } from "@aardworx/wombat.shader";
|
|
21
|
+
import { isInlineHeaderField } from "./heapEffect.js";
|
|
21
22
|
import type { Effect, CompileOptions } from "@aardworx/wombat.shader";
|
|
22
23
|
import { substituteInputsInStage, readInputs, mapExpr, mapStmt, liftReturns, uniformsToInputs } from "@aardworx/wombat.shader/passes";
|
|
23
24
|
import { synthesizeHeapDecoderModule } from "./heapDecoder.js";
|
|
@@ -278,9 +279,12 @@ function buildVertexUniformMap(layout: BucketLayout): Map<string, Expr> {
|
|
|
278
279
|
for (const f of layout.drawHeaderFields) {
|
|
279
280
|
if (f.kind !== "uniform-ref") continue;
|
|
280
281
|
const refExpr = loadHeaderRef(drawIdx, f.byteOffset, stride);
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
282
|
+
// Inline fields: the header word IS the u32 value.
|
|
283
|
+
const value = isInlineHeaderField(f)
|
|
284
|
+
? refExpr
|
|
285
|
+
: layout.perInstanceUniforms.has(f.name)
|
|
286
|
+
? loadInstanceByRef(refExpr, iidx, f.uniformWgslType ?? "")
|
|
287
|
+
: loadUniformByRef(refExpr, f.uniformWgslType ?? "");
|
|
284
288
|
out.set(f.name, value);
|
|
285
289
|
}
|
|
286
290
|
return out;
|
|
@@ -420,9 +424,11 @@ function rewriteFsUniformsDirect(m: Module, layout: BucketLayout): Module {
|
|
|
420
424
|
const f = fieldByName.get(name);
|
|
421
425
|
if (f === undefined || f.kind !== "uniform-ref") continue;
|
|
422
426
|
const refExpr = loadHeaderRef(drawIdxExpr, f.byteOffset, layout.strideU32);
|
|
423
|
-
const value =
|
|
424
|
-
?
|
|
425
|
-
:
|
|
427
|
+
const value = isInlineHeaderField(f)
|
|
428
|
+
? refExpr
|
|
429
|
+
: layout.perInstanceUniforms.has(name)
|
|
430
|
+
? loadInstanceByRef(refExpr, instIdExpr, f.uniformWgslType ?? "")
|
|
431
|
+
: loadUniformByRef(refExpr, f.uniformWgslType ?? "");
|
|
426
432
|
fsSubst.set(name, value);
|
|
427
433
|
}
|
|
428
434
|
return substituteInputsInStage(m, "fragment", "Uniform", n => fsSubst.get(n));
|
|
@@ -541,9 +547,12 @@ function rewriteFsUniforms(m: Module, layout: BucketLayout): Module {
|
|
|
541
547
|
value: { kind: "Expr", value: refExprWriter },
|
|
542
548
|
});
|
|
543
549
|
const refReadFs = readScope("Input", refParamName, Tu32);
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
550
|
+
// Inline fields: the threaded varying already carries the value.
|
|
551
|
+
const value = isInlineHeaderField(f)
|
|
552
|
+
? refReadFs
|
|
553
|
+
: layout.perInstanceUniforms.has(name)
|
|
554
|
+
? loadInstanceByRef(refReadFs, readScope("Input", "_iidx", Tu32), f.uniformWgslType ?? "")
|
|
555
|
+
: loadUniformByRef(refReadFs, f.uniformWgslType ?? "");
|
|
547
556
|
fsSubst.set(name, value);
|
|
548
557
|
}
|
|
549
558
|
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
// grows feature support each becomes a per-buffer eligibility
|
|
35
35
|
// question instead of a blanket "no".
|
|
36
36
|
|
|
37
|
-
import { AVal, type aval } from "@aardworx/wombat.adaptive";
|
|
37
|
+
import { AVal, AdaptiveToken, type aval } from "@aardworx/wombat.adaptive";
|
|
38
38
|
import type { IBuffer } from "../core/buffer.js";
|
|
39
39
|
import type { BufferView } from "../core/bufferView.js";
|
|
40
40
|
import type { ITexture } from "../core/texture.js";
|
|
@@ -174,14 +174,14 @@ export function isHeapEligible(ro: RenderObject): aval<boolean> {
|
|
|
174
174
|
// per-RO path, which at scale (thousands of textured ROs ⇒ thousands
|
|
175
175
|
// of draw calls + bind-group switches) is far slower than the heap
|
|
176
176
|
// megacall sampling one shared atlas page.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
177
|
+
// NOTE identity-dedup WITHOUT a Set: the long-lived AVal.custom
|
|
178
|
+
// closure below shares this function's V8 scope context, so any
|
|
179
|
+
// temporary Set here would be retained per-RO for the scene's
|
|
180
|
+
// lifetime (measured: 1 Set/RO of pure ballast at heap scale).
|
|
181
|
+
if (countDistinct(ro.textures) > 1) {
|
|
180
182
|
return AVal.constant(false);
|
|
181
183
|
}
|
|
182
|
-
|
|
183
|
-
ro.samplers.iter((_n, av) => { distinctSamplerAvals.add(av); });
|
|
184
|
-
if (distinctSamplerAvals.size > 1) {
|
|
184
|
+
if (countDistinct(ro.samplers) > 1) {
|
|
185
185
|
return AVal.constant(false);
|
|
186
186
|
}
|
|
187
187
|
|
|
@@ -202,11 +202,16 @@ export function isHeapEligible(ro: RenderObject): aval<boolean> {
|
|
|
202
202
|
const textures = textureAvals(ro);
|
|
203
203
|
const samplers = samplerAvals(ro);
|
|
204
204
|
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
|
|
205
|
+
// Constant collapse: when every participating aval is constant the
|
|
206
|
+
// answer can never change — evaluate once and return a plain
|
|
207
|
+
// constant. This keeps the hybrid partition from holding a custom
|
|
208
|
+
// aval + subscriptions per static RO (the overwhelmingly common
|
|
209
|
+
// case at heap scale).
|
|
210
|
+
let allConstant = ro.drawCall.isConstant;
|
|
211
|
+
if (allConstant) for (const av of buffers) { if (!av.isConstant) { allConstant = false; break; } }
|
|
212
|
+
if (allConstant) for (const av of textures) { if (!av.isConstant) { allConstant = false; break; } }
|
|
213
|
+
if (allConstant) for (const av of samplers) { if (!av.isConstant) { allConstant = false; break; } }
|
|
214
|
+
const evaluate = (token: AdaptiveToken): boolean => {
|
|
210
215
|
let payloadBytes = 0;
|
|
211
216
|
for (const av of buffers) {
|
|
212
217
|
const b = av.getValue(token);
|
|
@@ -240,5 +245,27 @@ export function isHeapEligible(ro: RenderObject): aval<boolean> {
|
|
|
240
245
|
if (dc.firstVertex !== 0) return false;
|
|
241
246
|
}
|
|
242
247
|
return true;
|
|
248
|
+
};
|
|
249
|
+
if (allConstant) return AVal.constant(evaluate(AdaptiveToken.top));
|
|
250
|
+
// Reactive AND-fold over all participating avals. We also subscribe
|
|
251
|
+
// to drawCall (instanceCount/baseVertex/firstIndex/firstInstance
|
|
252
|
+
// can flip; if they violate heap constraints the RO routes to the
|
|
253
|
+
// legacy path that frame).
|
|
254
|
+
return AVal.custom(evaluate);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
/** Count distinct avals in a name→aval map by identity, allocation-free
|
|
259
|
+
* for the 0/1/2-distinct cases that matter (we only compare > 1). */
|
|
260
|
+
function countDistinct(m: { iter(f: (n: string, av: aval<unknown>) => void): void }): number {
|
|
261
|
+
let first: aval<unknown> | undefined;
|
|
262
|
+
let second: aval<unknown> | undefined;
|
|
263
|
+
let n = 0;
|
|
264
|
+
m.iter((_n, av) => {
|
|
265
|
+
if (av === first || av === second) return;
|
|
266
|
+
if (first === undefined) { first = av; n = 1; }
|
|
267
|
+
else if (second === undefined) { second = av; n = 2; }
|
|
268
|
+
else n++;
|
|
243
269
|
});
|
|
270
|
+
return n;
|
|
244
271
|
}
|
|
@@ -62,6 +62,23 @@ export const shr = (lhs: Expr, rhs: Expr, type: Type): Expr => ({
|
|
|
62
62
|
export const select = (cond: Expr, ifTrue: Expr, ifFalse: Expr, type: Type): Expr => ({
|
|
63
63
|
kind: "Conditional", cond, ifTrue, ifFalse, type,
|
|
64
64
|
});
|
|
65
|
+
export const bitAnd = (lhs: Expr, rhs: Expr, type: Type): Expr => ({
|
|
66
|
+
kind: "BitAnd", lhs, rhs, type,
|
|
67
|
+
});
|
|
68
|
+
export const ltF32 = (lhs: Expr, rhs: Expr): Expr => ({ kind: "Lt", lhs, rhs, type: Tbool });
|
|
69
|
+
export const geF32 = (lhs: Expr, rhs: Expr): Expr => ({ kind: "Ge", lhs, rhs, type: Tbool });
|
|
70
|
+
/** u32 → f32 numeric conversion. */
|
|
71
|
+
export const toF32 = (value: Expr): Expr => ({ kind: "Convert", value, type: Tf32 } as Expr);
|
|
72
|
+
/** Pure WGSL/GLSL intrinsic call (same emit name unless glsl differs). */
|
|
73
|
+
const callIntrinsic = (wgsl: string, glsl: string, ret: Type, args: Expr[]): Expr => ({
|
|
74
|
+
kind: "CallIntrinsic",
|
|
75
|
+
op: { name: wgsl, returnTypeOf: () => ret, pure: true, emit: { wgsl, glsl } },
|
|
76
|
+
args, type: ret,
|
|
77
|
+
} as Expr);
|
|
78
|
+
export const absF32 = (e: Expr): Expr => callIntrinsic("abs", "abs", Tf32, [e]);
|
|
79
|
+
export const normalize3 = (e: Expr): Expr => callIntrinsic("normalize", "normalize", Tvec3, [e]);
|
|
80
|
+
export const unpack4x8unorm = (e: Expr): Expr =>
|
|
81
|
+
callIntrinsic("unpack4x8unorm", "unpackUnorm4x8", Tvec4, [e]);
|
|
65
82
|
export const newVec = (components: Expr[], type: Type): Expr => ({
|
|
66
83
|
kind: "NewVector", components, type,
|
|
67
84
|
});
|
|
@@ -174,14 +191,40 @@ export function loadAttributeByRef(refIdent: Expr, idx: Expr, wgslType: string):
|
|
|
174
191
|
const length = item(heapU32, add(div(refIdent, constU32(4), Tu32), constU32(1), Tu32), Tu32);
|
|
175
192
|
return mul(mod(idx, length, Tu32), constU32(elemSize), Tu32);
|
|
176
193
|
};
|
|
194
|
+
/** Header typeId (word 0 — same 16-byte cache line as the length the
|
|
195
|
+
* broadcast modulo already loads, so the packed-encoding branch costs
|
|
196
|
+
* no extra memory round-trip and is warp-coherent per field). */
|
|
197
|
+
const typeId = item(heapU32, div(refIdent, constU32(4), Tu32), Tu32);
|
|
198
|
+
/** One u32 element at the cyclic index (packed encodings: 4 B/elt). */
|
|
199
|
+
const packedWord = (): Expr => add(dataF32Base, cyclic(1), Tu32);
|
|
177
200
|
switch (wgslType) {
|
|
178
201
|
case "vec3<f32>": {
|
|
179
202
|
const base = add(dataF32Base, cyclic(3), Tu32);
|
|
180
|
-
|
|
203
|
+
const tight = newVec([
|
|
181
204
|
item(heapF32, base, Tf32),
|
|
182
205
|
item(heapF32, add(base, constU32(1), Tu32), Tf32),
|
|
183
206
|
item(heapF32, add(base, constU32(2), Tu32), Tf32),
|
|
184
207
|
], Tvec3);
|
|
208
|
+
// ENC_OCT32 (typeId 2): one u32/element — x = low unorm16, y = high
|
|
209
|
+
// unorm16, both → [-1,1]; octahedral fold when z<0 (matches
|
|
210
|
+
// build_vienna.py's oct_pack and the CPU decoder in renderbench).
|
|
211
|
+
const u = item(heapU32, packedWord(), Tu32);
|
|
212
|
+
const toSnorm = (word: Expr): Expr =>
|
|
213
|
+
add(mul(toF32(word), constF32(2 / 65535), Tf32), constF32(-1.0), Tf32);
|
|
214
|
+
const fx = toSnorm(bitAnd(u, constU32(0xffff), Tu32));
|
|
215
|
+
const fy = toSnorm(shr(u, constU32(16), Tu32));
|
|
216
|
+
const z = sub(sub(constF32(1.0), absF32(fx), Tf32), absF32(fy), Tf32);
|
|
217
|
+
const neg = ltF32(z, constF32(0.0));
|
|
218
|
+
const signOf = (v: Expr): Expr =>
|
|
219
|
+
select(geF32(v, constF32(0.0)), constF32(1.0), constF32(-1.0), Tf32);
|
|
220
|
+
const fold = (a: Expr, b: Expr): Expr => // (1-|b|) * sign(a)
|
|
221
|
+
mul(sub(constF32(1.0), absF32(b), Tf32), signOf(a), Tf32);
|
|
222
|
+
const oct = normalize3(newVec([
|
|
223
|
+
select(neg, fold(fx, fy), fx, Tf32),
|
|
224
|
+
select(neg, fold(fy, fx), fy, Tf32),
|
|
225
|
+
z,
|
|
226
|
+
], Tvec3));
|
|
227
|
+
return select(eqU32(typeId, constU32(2 /* ENC_OCT32 */)), oct, tight, Tvec3);
|
|
185
228
|
}
|
|
186
229
|
case "vec4<f32>": {
|
|
187
230
|
const strideBytes = item(heapU32, add(div(refIdent, constU32(4), Tu32), constU32(2), Tu32), Tu32);
|
|
@@ -190,12 +233,17 @@ export function loadAttributeByRef(refIdent: Expr, idx: Expr, wgslType: string):
|
|
|
190
233
|
const off = mul(cycled, strideF, Tu32);
|
|
191
234
|
const base = add(dataF32Base, off, Tu32);
|
|
192
235
|
const w = select(eqU32(strideF, constU32(4)), item(heapF32, add(base, constU32(3), Tu32), Tf32), constF32(1.0), Tf32);
|
|
193
|
-
|
|
236
|
+
const tight = newVec([
|
|
194
237
|
item(heapF32, base, Tf32),
|
|
195
238
|
item(heapF32, add(base, constU32(1), Tu32), Tf32),
|
|
196
239
|
item(heapF32, add(base, constU32(2), Tu32), Tf32),
|
|
197
240
|
w,
|
|
198
241
|
], Tvec4);
|
|
242
|
+
// ENC_C4B (typeId 3): one u32/element, RGBA8 unorm — a single
|
|
243
|
+
// hardware unpack. (Vienna colours ship as C4b; storing them raw is
|
|
244
|
+
// 4 B/vertex instead of the 12 B tight-RGB expansion.)
|
|
245
|
+
const c4b = unpack4x8unorm(item(heapU32, packedWord(), Tu32));
|
|
246
|
+
return select(eqU32(typeId, constU32(3 /* ENC_C4B */)), c4b, tight, Tvec4);
|
|
199
247
|
}
|
|
200
248
|
case "vec2<f32>": {
|
|
201
249
|
const base = add(dataF32Base, cyclic(2), Tu32);
|