@aardworx/wombat.rendering 0.19.10 → 0.19.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/renderObject.d.ts +9 -0
- package/dist/core/renderObject.d.ts.map +1 -1
- package/dist/resources/preparedRenderObject.d.ts +11 -1
- package/dist/resources/preparedRenderObject.d.ts.map +1 -1
- package/dist/resources/preparedRenderObject.js +51 -3
- package/dist/resources/preparedRenderObject.js.map +1 -1
- package/dist/runtime/derivedModes/axisEnum.d.ts +21 -0
- package/dist/runtime/derivedModes/axisEnum.d.ts.map +1 -0
- package/dist/runtime/derivedModes/axisEnum.js +64 -0
- package/dist/runtime/derivedModes/axisEnum.js.map +1 -0
- package/dist/runtime/derivedModes/cpuEval.d.ts +11 -0
- package/dist/runtime/derivedModes/cpuEval.d.ts.map +1 -0
- package/dist/runtime/derivedModes/cpuEval.js +98 -0
- package/dist/runtime/derivedModes/cpuEval.js.map +1 -0
- package/dist/runtime/derivedUniforms/codegen.d.ts +11 -0
- package/dist/runtime/derivedUniforms/codegen.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/codegen.js +59 -2
- package/dist/runtime/derivedUniforms/codegen.js.map +1 -1
- package/dist/runtime/derivedUniforms/cpuEval.d.ts +12 -1
- package/dist/runtime/derivedUniforms/cpuEval.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/cpuEval.js +15 -9
- package/dist/runtime/derivedUniforms/cpuEval.js.map +1 -1
- package/dist/runtime/derivedUniforms/dispatch.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/dispatch.js +21 -11
- package/dist/runtime/derivedUniforms/dispatch.js.map +1 -1
- package/dist/runtime/derivedUniforms/sceneIntegration.d.ts +24 -1
- package/dist/runtime/derivedUniforms/sceneIntegration.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/sceneIntegration.js +57 -6
- package/dist/runtime/derivedUniforms/sceneIntegration.js.map +1 -1
- package/dist/runtime/derivedUniforms/slots.d.ts +6 -0
- package/dist/runtime/derivedUniforms/slots.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/slots.js +14 -0
- package/dist/runtime/derivedUniforms/slots.js.map +1 -1
- package/dist/runtime/heapAdapter.d.ts.map +1 -1
- package/dist/runtime/heapAdapter.js +1 -0
- package/dist/runtime/heapAdapter.js.map +1 -1
- package/dist/runtime/heapScene.d.ts +11 -1
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +6 -0
- package/dist/runtime/heapScene.js.map +1 -1
- package/package.json +1 -1
- package/src/core/renderObject.ts +9 -0
- package/src/resources/preparedRenderObject.ts +42 -1
- package/src/runtime/derivedModes/axisEnum.ts +65 -0
- package/src/runtime/derivedModes/cpuEval.ts +104 -0
- package/src/runtime/derivedUniforms/codegen.ts +61 -2
- package/src/runtime/derivedUniforms/cpuEval.ts +25 -3
- package/src/runtime/derivedUniforms/dispatch.ts +20 -10
- package/src/runtime/derivedUniforms/sceneIntegration.ts +75 -6
- package/src/runtime/derivedUniforms/slots.ts +15 -0
- package/src/runtime/heapAdapter.ts +1 -0
- package/src/runtime/heapScene.ts +15 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aardworx/wombat.rendering",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.12",
|
|
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",
|
package/src/core/renderObject.ts
CHANGED
|
@@ -102,4 +102,13 @@ export interface RenderObject {
|
|
|
102
102
|
readonly depthWrite?: import("../runtime/derivedModes/rule.js").DerivedModeRule<"depthWrite">;
|
|
103
103
|
readonly alphaToCoverage?: import("../runtime/derivedModes/rule.js").DerivedModeRule<"alphaToCoverage">;
|
|
104
104
|
};
|
|
105
|
+
/**
|
|
106
|
+
* GPU transform propagation: this RO's `Model` as an ancestor trafo CHAIN
|
|
107
|
+
* (root→leaf order, constant runs folded) instead of a single composed
|
|
108
|
+
* `ModelTrafo` aval. The scene-graph emits this so a shared root trafo over
|
|
109
|
+
* N objects never fans out to N composed CPU avals — the heap composes each
|
|
110
|
+
* RO's Model on the GPU. Heap-path only (the legacy path composes on CPU).
|
|
111
|
+
* See docs/gpu-transform-propagation.md.
|
|
112
|
+
*/
|
|
113
|
+
readonly modelChain?: readonly aval<import("@aardworx/wombat.base").Trafo3d>[];
|
|
105
114
|
}
|
|
@@ -35,6 +35,7 @@ import type { Type } from "@aardworx/wombat.shader/ir";
|
|
|
35
35
|
import { isDerivedRule } from "../runtime/derivedUniforms/rule.js";
|
|
36
36
|
import { STANDARD_DERIVED_RULES, STANDARD_TRAFO_LEAVES } from "../runtime/derivedUniforms/recipes.js";
|
|
37
37
|
import { interpretExpr } from "../runtime/derivedUniforms/cpuEval.js";
|
|
38
|
+
import { evaluateModeRule } from "../runtime/derivedModes/cpuEval.js";
|
|
38
39
|
import { prepareAdaptiveBuffer } from "./adaptiveBuffer.js";
|
|
39
40
|
import { prepareAdaptiveTexture } from "./adaptiveTexture.js";
|
|
40
41
|
import { prepareAdaptiveSampler } from "./adaptiveSampler.js";
|
|
@@ -369,6 +370,8 @@ export class PreparedRenderObject {
|
|
|
369
370
|
pipelineCtx: PipelineBuildContext,
|
|
370
371
|
pipelineState: import("../core/index.js").PipelineState,
|
|
371
372
|
private readonly active: aval<boolean> | undefined = undefined,
|
|
373
|
+
private readonly modeRules: import("../core/renderObject.js").RenderObject["modeRules"] = undefined,
|
|
374
|
+
private readonly modeUniformReader: ((name: string, token: AdaptiveToken) => unknown) | undefined = undefined,
|
|
372
375
|
) {
|
|
373
376
|
this.groups = groups;
|
|
374
377
|
this._pipelineCtx = pipelineCtx;
|
|
@@ -381,6 +384,28 @@ export class PreparedRenderObject {
|
|
|
381
384
|
/** Internal — exposed for tests. The PipelineState the RO is reading. */
|
|
382
385
|
get pipelineState(): import("../core/index.js").PipelineState { return this._pipelineState; }
|
|
383
386
|
|
|
387
|
+
/**
|
|
388
|
+
* Override pipeline-axis values with CPU-evaluated derived-mode rules.
|
|
389
|
+
* Each rule reads per-RO uniforms + the SG-declared default; evaluating
|
|
390
|
+
* under `token` registers the dependency so a uniform/declared change
|
|
391
|
+
* re-keys the pipeline (recompile via the snapshot cache). The heap path
|
|
392
|
+
* does this on a GPU partition kernel — this is the legacy per-RO answer.
|
|
393
|
+
*/
|
|
394
|
+
private applyModeRules(snap: PipelineSnap, token: AdaptiveToken): PipelineSnap {
|
|
395
|
+
const mr = this.modeRules;
|
|
396
|
+
const ru = this.modeUniformReader;
|
|
397
|
+
if (mr === undefined || ru === undefined) return snap;
|
|
398
|
+
const read = (name: string): unknown => ru(name, token);
|
|
399
|
+
const ov: Record<string, unknown> = {};
|
|
400
|
+
if (mr.cull !== undefined) ov.cullMode = evaluateModeRule(mr.cull, read, token);
|
|
401
|
+
if (mr.frontFace !== undefined) ov.frontFace = evaluateModeRule(mr.frontFace, read, token);
|
|
402
|
+
if (mr.topology !== undefined) ov.topology = evaluateModeRule(mr.topology, read, token);
|
|
403
|
+
if (mr.depthWrite !== undefined) ov.depthWrite = evaluateModeRule(mr.depthWrite, read, token);
|
|
404
|
+
if (mr.depthCompare !== undefined) ov.depthCompare = evaluateModeRule(mr.depthCompare, read, token);
|
|
405
|
+
if (mr.alphaToCoverage !== undefined) ov.alphaToCoverage = evaluateModeRule(mr.alphaToCoverage, read, token);
|
|
406
|
+
return { ...snap, ...ov };
|
|
407
|
+
}
|
|
408
|
+
|
|
384
409
|
acquire(): void {
|
|
385
410
|
for (const r of this.vertexBuffers.values()) r.acquire();
|
|
386
411
|
if (this.indexBuffer !== undefined) this.indexBuffer.acquire();
|
|
@@ -399,7 +424,7 @@ export class PreparedRenderObject {
|
|
|
399
424
|
* surrounding sort / record path can read it without forcing.
|
|
400
425
|
*/
|
|
401
426
|
update(token: AdaptiveToken): void {
|
|
402
|
-
const snap = snapshotPipeline(this._pipelineState, token);
|
|
427
|
+
const snap = this.applyModeRules(snapshotPipeline(this._pipelineState, token), token);
|
|
403
428
|
const key = pipelineKeyOf(snap);
|
|
404
429
|
let pipeline = this._pipelineCache.get(key);
|
|
405
430
|
if (pipeline === undefined) {
|
|
@@ -751,6 +776,20 @@ export function prepareRenderObject(
|
|
|
751
776
|
...(opts.effectId !== undefined ? { effectId: opts.effectId } : {}),
|
|
752
777
|
};
|
|
753
778
|
|
|
779
|
+
// Derived-mode rules (cull/frontFace/… as a function of per-RO uniforms)
|
|
780
|
+
// are evaluated CPU-side in the legacy path. Build a uniform reader over
|
|
781
|
+
// the RO's provider (resolving nested derived-uniform rules too).
|
|
782
|
+
let modeUniformReader: ((name: string, token: AdaptiveToken) => unknown) | undefined;
|
|
783
|
+
if (obj.modeRules !== undefined) {
|
|
784
|
+
const uniProv = asUniformProvider(obj.uniforms);
|
|
785
|
+
modeUniformReader = (name: string, token: AdaptiveToken): unknown => {
|
|
786
|
+
const v = uniProv.tryGet(name);
|
|
787
|
+
if (v === undefined) return undefined;
|
|
788
|
+
if (isDerivedRule(v)) return interpretExpr(v.ir, (n) => readLeaf(n, uniProv, effect.avalBindings, token));
|
|
789
|
+
return (v as aval<unknown>).getValue(token);
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
|
|
754
793
|
return new PreparedRenderObject(
|
|
755
794
|
device,
|
|
756
795
|
vertexBindings, vertexBuffers, vertexViews,
|
|
@@ -760,6 +799,8 @@ export function prepareRenderObject(
|
|
|
760
799
|
pipelineCtx,
|
|
761
800
|
obj.pipelineState,
|
|
762
801
|
obj.active,
|
|
802
|
+
obj.modeRules,
|
|
803
|
+
modeUniformReader,
|
|
763
804
|
);
|
|
764
805
|
}
|
|
765
806
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Canonical per-axis enum tables + value mapping for derived modes.
|
|
2
|
+
//
|
|
3
|
+
// Used by the legacy CPU-eval path (derivedModes/cpuEval). heapScene's
|
|
4
|
+
// GPU-routing path keeps a parallel inline copy of this table + helpers
|
|
5
|
+
// (heapScene.ts ~AXIS_ENUM_TABLE / resolveAxisValue / resolveDeclaredU32);
|
|
6
|
+
// they must stay in sync — fold both onto this module when convenient.
|
|
7
|
+
// The index↔value mapping is the WebGPU enum order; a rule body returns a
|
|
8
|
+
// u32 index and the renderer maps it to the concrete mode value here.
|
|
9
|
+
|
|
10
|
+
import type { aval, AdaptiveToken } from "@aardworx/wombat.adaptive";
|
|
11
|
+
import type { DerivedModeRule, ModeAxis } from "./rule.js";
|
|
12
|
+
|
|
13
|
+
/** u32 index → mode value, per axis. `blend` has no canonical table —
|
|
14
|
+
* blend rules return object literals directly (see resolveAxisValue). */
|
|
15
|
+
export const AXIS_ENUM_TABLE: { readonly [A in ModeAxis]: ReadonlyArray<unknown> } = {
|
|
16
|
+
cull: ["none", "front", "back"],
|
|
17
|
+
frontFace: ["ccw", "cw"],
|
|
18
|
+
topology: ["point-list", "line-list", "line-strip", "triangle-list", "triangle-strip"],
|
|
19
|
+
depthCompare: ["never", "less", "equal", "less-equal", "greater", "not-equal", "greater-equal", "always"],
|
|
20
|
+
depthWrite: [false, true],
|
|
21
|
+
alphaToCoverage: [false, true],
|
|
22
|
+
blend: [],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Map a rule's resolved output to its axis-shape value:
|
|
27
|
+
* - object → already the full axis value (e.g. a blend AttachmentBlend).
|
|
28
|
+
* - number → enum-table lookup for enum axes; pass-through otherwise.
|
|
29
|
+
*/
|
|
30
|
+
export function resolveAxisValue<A extends ModeAxis>(rule: DerivedModeRule<A>, value: unknown): unknown {
|
|
31
|
+
if (typeof value === "object" && value !== null) return value;
|
|
32
|
+
if (typeof value === "number") {
|
|
33
|
+
const table = AXIS_ENUM_TABLE[rule.axis];
|
|
34
|
+
if (table.length > 0) {
|
|
35
|
+
const v = table[value];
|
|
36
|
+
if (v !== undefined) return v;
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Resolve a rule's `declared` SG-context value to its u32 index:
|
|
45
|
+
* - omitted → 0 (axis canonical zero).
|
|
46
|
+
* - number → passed through.
|
|
47
|
+
* - aval / string / boolean → looked up in the canonical enum table.
|
|
48
|
+
*/
|
|
49
|
+
export function declaredToU32<A extends ModeAxis>(rule: DerivedModeRule<A>, tok: AdaptiveToken): number {
|
|
50
|
+
const d = rule.declared;
|
|
51
|
+
if (d === undefined) return 0;
|
|
52
|
+
const v = (typeof d === "object" && d !== null && "getValue" in (d as object))
|
|
53
|
+
? (d as aval<unknown>).getValue(tok)
|
|
54
|
+
: d;
|
|
55
|
+
if (typeof v === "number") return v >>> 0;
|
|
56
|
+
const table = AXIS_ENUM_TABLE[rule.axis];
|
|
57
|
+
if (table.length === 0) {
|
|
58
|
+
throw new Error(`derivedModes: axis '${rule.axis}' has no canonical enum table; pass \`declared\` as a u32 index or omit it`);
|
|
59
|
+
}
|
|
60
|
+
const i = table.indexOf(v);
|
|
61
|
+
if (i < 0) {
|
|
62
|
+
throw new Error(`derivedModes: declared value '${String(v)}' for axis '${rule.axis}' not in canonical enum table ${JSON.stringify(table)}`);
|
|
63
|
+
}
|
|
64
|
+
return i;
|
|
65
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// CPU evaluation of a derived-MODE rule for the legacy per-RO path.
|
|
2
|
+
//
|
|
3
|
+
// A `derivedMode(...)` rule is a RuleExpr whose body computes a pipeline-axis
|
|
4
|
+
// value (cull / frontFace / …) from per-RO uniforms (e.g. determinant of the
|
|
5
|
+
// model matrix) and the SG-declared default. The heap path lowers this to a
|
|
6
|
+
// GPU partition kernel; the legacy per-RO path has no kernel, so we evaluate
|
|
7
|
+
// the rule body on the CPU here — the "v1 evaluates rules CPU-side" design in
|
|
8
|
+
// core/renderObject.ts.
|
|
9
|
+
//
|
|
10
|
+
// Reuses the matrix-aware expression interpreter (derivedUniforms/cpuEval) and
|
|
11
|
+
// adds a small Stmt walker (locals + control flow) plus user-function `Call`
|
|
12
|
+
// resolution against the rule's own Module (helpers like `flipCull` are NOT
|
|
13
|
+
// inlined — they're separate Function items in template.values).
|
|
14
|
+
|
|
15
|
+
import type { Expr, Stmt, ValueDef } from "@aardworx/wombat.shader/ir";
|
|
16
|
+
import type { AdaptiveToken } from "@aardworx/wombat.adaptive";
|
|
17
|
+
import { interpretExpr, type EvalValue } from "../derivedUniforms/cpuEval.js";
|
|
18
|
+
import type { DerivedModeRule, ModeAxis, ModeValue } from "./rule.js";
|
|
19
|
+
import { declaredToU32, resolveAxisValue } from "./axisEnum.js";
|
|
20
|
+
|
|
21
|
+
const NO_RETURN = Symbol("no-return");
|
|
22
|
+
type RunResult = EvalValue | typeof NO_RETURN;
|
|
23
|
+
|
|
24
|
+
interface FnDef { readonly params: readonly string[]; readonly body: Stmt; }
|
|
25
|
+
|
|
26
|
+
function truthy(v: EvalValue): boolean {
|
|
27
|
+
return typeof v === "boolean" ? v : typeof v === "number" ? v !== 0 : true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Evaluate a derived-mode rule to its concrete axis value for one RO.
|
|
32
|
+
* `readUniform(name)` returns the RO's current value for a `u.<name>` leaf
|
|
33
|
+
* (Trafo3d / M44f / number / …); reading it under `token` registers the
|
|
34
|
+
* dependency so the pipeline re-keys when it changes. Returns the mapped
|
|
35
|
+
* mode value (e.g. "back", or `true` for depthWrite).
|
|
36
|
+
*/
|
|
37
|
+
export function evaluateModeRule<A extends ModeAxis>(
|
|
38
|
+
rule: DerivedModeRule<A>,
|
|
39
|
+
readUniform: (name: string) => unknown,
|
|
40
|
+
token: AdaptiveToken,
|
|
41
|
+
): ModeValue<A> {
|
|
42
|
+
// Collect helper functions + the entry body from the rule's Module.
|
|
43
|
+
const fns = new Map<string, FnDef>();
|
|
44
|
+
let body: Stmt | undefined;
|
|
45
|
+
for (const v of rule.expr.template.values as readonly ValueDef[]) {
|
|
46
|
+
if (v.kind === "Function") fns.set(v.signature.name, { params: v.signature.parameters.map((p) => p.name), body: v.body });
|
|
47
|
+
else if (v.kind === "Entry") body = v.entry.body;
|
|
48
|
+
}
|
|
49
|
+
if (body === undefined) {
|
|
50
|
+
throw new Error(`derivedModes: rule for axis '${rule.axis}' has no Entry in its template`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const declaredU32 = declaredToU32(rule, token);
|
|
54
|
+
const readLeaf = (name: string): unknown => (name === "declared" ? declaredU32 : readUniform(name));
|
|
55
|
+
|
|
56
|
+
// Mutually-recursive: an expr `Call` runs a function body; a function body
|
|
57
|
+
// evaluates exprs via interpretExpr with `callFn` wired back here.
|
|
58
|
+
const callFn = (name: string, args: EvalValue[]): EvalValue => {
|
|
59
|
+
const fn = fns.get(name);
|
|
60
|
+
if (fn === undefined) throw new Error(`derivedModes: rule calls unknown function '${name}'`);
|
|
61
|
+
const locals = new Map<string, EvalValue>();
|
|
62
|
+
fn.params.forEach((p, i) => { if (args[i] !== undefined) locals.set(p, args[i]!); });
|
|
63
|
+
return evalBody(fn.body, locals);
|
|
64
|
+
};
|
|
65
|
+
const evalExpr = (e: Expr, locals: Map<string, EvalValue>): EvalValue =>
|
|
66
|
+
interpretExpr(e, readLeaf, { locals, callFn });
|
|
67
|
+
|
|
68
|
+
const run = (s: Stmt, locals: Map<string, EvalValue>): RunResult => {
|
|
69
|
+
switch (s.kind) {
|
|
70
|
+
case "Sequential":
|
|
71
|
+
case "Isolated": {
|
|
72
|
+
for (const c of s.body) { const r = run(c, locals); if (r !== NO_RETURN) return r; }
|
|
73
|
+
return NO_RETURN;
|
|
74
|
+
}
|
|
75
|
+
case "Declare": {
|
|
76
|
+
if (s.init !== undefined && s.init.kind === "Expr") locals.set(s.var.name, evalExpr(s.init.value, locals));
|
|
77
|
+
return NO_RETURN;
|
|
78
|
+
}
|
|
79
|
+
case "Write": {
|
|
80
|
+
if (s.target.kind === "LVar") locals.set(s.target.var.name, evalExpr(s.value, locals));
|
|
81
|
+
return NO_RETURN;
|
|
82
|
+
}
|
|
83
|
+
case "If": {
|
|
84
|
+
if (truthy(evalExpr(s.cond, locals))) return run(s.then, locals);
|
|
85
|
+
return s.else !== undefined ? run(s.else, locals) : NO_RETURN;
|
|
86
|
+
}
|
|
87
|
+
case "ReturnValue": return evalExpr(s.value, locals);
|
|
88
|
+
case "Expression":
|
|
89
|
+
case "Nop":
|
|
90
|
+
case "Return":
|
|
91
|
+
return NO_RETURN;
|
|
92
|
+
default:
|
|
93
|
+
throw new Error(`derivedModes: unsupported statement '${s.kind}' in rule body`);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const evalBody = (b: Stmt, locals: Map<string, EvalValue>): EvalValue => {
|
|
97
|
+
const r = run(b, locals);
|
|
98
|
+
if (r === NO_RETURN) throw new Error(`derivedModes: rule body for axis '${rule.axis}' produced no return value`);
|
|
99
|
+
return r;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const raw = evalBody(body, new Map());
|
|
103
|
+
return resolveAxisValue(rule, raw) as ModeValue<A>;
|
|
104
|
+
}
|
|
@@ -23,6 +23,18 @@ export interface UberKernel {
|
|
|
23
23
|
readonly strideU32: number;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Reserved rule id for the built-in variable-length trafo-chain arm (GPU
|
|
28
|
+
* transform propagation). A chain record is laid out as
|
|
29
|
+
* [ CHAIN_RULE_ID, out_slot, count, link0, link1, … ]
|
|
30
|
+
* where each `linkK` is a Constituent handle. The arm folds the df32 product
|
|
31
|
+
* `link0 · link1 · … · link(count-1)` and writes it (collapsed) to `out_slot`.
|
|
32
|
+
* One arm handles every chain length, so distinct SG ancestor paths don't each
|
|
33
|
+
* spawn a kernel arm / recompile. Kept far above the registry's monotonic ids
|
|
34
|
+
* (which start at 0) so it can never collide. See docs/gpu-transform-propagation.md.
|
|
35
|
+
*/
|
|
36
|
+
export const CHAIN_RULE_ID = 0x7fffffff;
|
|
37
|
+
|
|
26
38
|
// ─── Rule-shape classification ────────────────────────────────────────
|
|
27
39
|
|
|
28
40
|
type Shape =
|
|
@@ -136,6 +148,12 @@ fn load_entry_mat4(h: u32, r: u32, c: u32) -> vec2<f32> {
|
|
|
136
148
|
fn write_mat4_entry(out_byte: u32, r: u32, c: u32, v: f32) {
|
|
137
149
|
MainHeap[(out_byte >> 2u) + r * 4u + c] = v;
|
|
138
150
|
}
|
|
151
|
+
// Write one df32 mat4 entry (r,c) into a constituent slot — keeps both hi+lo
|
|
152
|
+
// so a downstream §7 pass reads it at full df32 precision. Used by the chain
|
|
153
|
+
// pass, whose output IS a per-RO Model constituent (not a collapsed uniform).
|
|
154
|
+
fn write_constituent_entry(slot: u32, r: u32, c: u32, e: vec2<f32>) {
|
|
155
|
+
Constituents[slot * 16u + r * 4u + c] = e;
|
|
156
|
+
}
|
|
139
157
|
fn write_mat3_entry(out_byte: u32, r: u32, c: u32, v: f32) {
|
|
140
158
|
// std140 mat3<f32>: 3 rows × 4-float stride; the 4th column per row is left untouched.
|
|
141
159
|
MainHeap[(out_byte >> 2u) + r * 4u + c] = v;
|
|
@@ -144,7 +162,7 @@ fn write_mat3_entry(out_byte: u32, r: u32, c: u32, v: f32) {
|
|
|
144
162
|
|
|
145
163
|
function bindings(strideU32: number): string {
|
|
146
164
|
return /* wgsl */ `
|
|
147
|
-
@group(0) @binding(0) var<storage,
|
|
165
|
+
@group(0) @binding(0) var<storage, read_write> Constituents: array<vec2<f32>>;
|
|
148
166
|
@group(0) @binding(1) var<storage, read_write> MainHeap: array<f32>;
|
|
149
167
|
@group(0) @binding(2) var<storage, read> RecordData: array<u32>;
|
|
150
168
|
struct CountUniform { count: u32 }
|
|
@@ -214,6 +232,43 @@ function emitMatMulChainArm(id: number, n: number): string {
|
|
|
214
232
|
return `\nfn arm_${id}(${params}, out_byte: u32) {\n${body}}\n`;
|
|
215
233
|
}
|
|
216
234
|
|
|
235
|
+
/** Built-in `CHAIN` arm (GPU transform propagation): variable-length df32
|
|
236
|
+
* matmul chain read straight from the record. `RecordData[base+2]` = count;
|
|
237
|
+
* `RecordData[base+3+k]` = link k (Constituent handle). Folds
|
|
238
|
+
* P = L0·L1·…·L(count-1) in df32 and writes it — keeping BOTH hi+lo — into the
|
|
239
|
+
* output CONSTITUENT slot (`out_slot` = the out handle's payload), so a later
|
|
240
|
+
* §7 pass reads it at full df32 precision. The inverse half is produced by a
|
|
241
|
+
* second chain record over the links' inv slots in reverse order (same arm).
|
|
242
|
+
* P starts at identity so count==0 yields identity. */
|
|
243
|
+
const CHAIN_ARM = /* wgsl */ `
|
|
244
|
+
fn arm_chain(base: u32, out_slot: u32) {
|
|
245
|
+
let count = RecordData[base + 2u];
|
|
246
|
+
var P: array<vec2<f32>, 16>;
|
|
247
|
+
for (var i: u32 = 0u; i < 16u; i = i + 1u) { P[i] = vec2<f32>(0.0, 0.0); }
|
|
248
|
+
P[0] = vec2<f32>(1.0, 0.0); P[5] = vec2<f32>(1.0, 0.0);
|
|
249
|
+
P[10] = vec2<f32>(1.0, 0.0); P[15] = vec2<f32>(1.0, 0.0);
|
|
250
|
+
for (var k: u32 = 0u; k < count; k = k + 1u) {
|
|
251
|
+
let h = RecordData[base + 3u + k];
|
|
252
|
+
var Q: array<vec2<f32>, 16>;
|
|
253
|
+
for (var r: u32 = 0u; r < 4u; r = r + 1u) {
|
|
254
|
+
for (var c: u32 = 0u; c < 4u; c = c + 1u) {
|
|
255
|
+
var acc = vec2<f32>(0.0, 0.0);
|
|
256
|
+
for (var t: u32 = 0u; t < 4u; t = t + 1u) {
|
|
257
|
+
acc = df_add(acc, df_mul(P[r * 4u + t], load_entry_mat4(h, t, c)));
|
|
258
|
+
}
|
|
259
|
+
Q[r * 4u + c] = acc;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
P = Q;
|
|
263
|
+
}
|
|
264
|
+
for (var r: u32 = 0u; r < 4u; r = r + 1u) {
|
|
265
|
+
for (var c: u32 = 0u; c < 4u; c = c + 1u) {
|
|
266
|
+
write_constituent_entry(out_slot, r, c, P[r * 4u + c]);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
`;
|
|
271
|
+
|
|
217
272
|
// ─── Generic path: arbitrary IR lowered via the wgsl printer (f32) ────
|
|
218
273
|
//
|
|
219
274
|
// The rule's IR is rewritten so each input leaf — `ReadInput("Uniform", x)` or
|
|
@@ -454,13 +509,16 @@ export function buildUberKernel(registry: DerivedUniformRegistry, strideU32: num
|
|
|
454
509
|
|
|
455
510
|
const arms = entries.map((e, i) => emitArm(e, shapes[i]!)).join("");
|
|
456
511
|
const cases = entries.map((e, i) => emitCase(e, shapes[i]!)).join("\n");
|
|
457
|
-
|
|
512
|
+
// The CHAIN arm is always present (GPU transform propagation) and always
|
|
513
|
+
// needs the df32 library.
|
|
514
|
+
const needsDf32 = true;
|
|
458
515
|
|
|
459
516
|
const wgsl = `${bindings(strideU32)}
|
|
460
517
|
${needsDf32 ? DF32_LIB : ""}
|
|
461
518
|
${HANDLE_HELPERS}
|
|
462
519
|
${loadersStorers}
|
|
463
520
|
${arms}
|
|
521
|
+
${CHAIN_ARM}
|
|
464
522
|
@compute @workgroup_size(64)
|
|
465
523
|
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
466
524
|
if (gid.x >= Count.count) { return; }
|
|
@@ -468,6 +526,7 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
|
468
526
|
let rule_id = RecordData[base];
|
|
469
527
|
let out_byte = RecordData[base + 1u] & SLOT_PAYLOAD_MASK;
|
|
470
528
|
switch rule_id {
|
|
529
|
+
case ${CHAIN_RULE_ID >>> 0}u: { arm_chain(base, out_byte); }
|
|
471
530
|
${cases}
|
|
472
531
|
default: { return; }
|
|
473
532
|
}
|
|
@@ -198,7 +198,19 @@ function intrinsic(name: string, a: EvalValue[]): EvalValue {
|
|
|
198
198
|
* `readLeaf`. Returns a host double value (`M44d` / `V3d` / number / …) that
|
|
199
199
|
* `writeField` narrows to f32 when packing the UBO.
|
|
200
200
|
*/
|
|
201
|
-
|
|
201
|
+
/**
|
|
202
|
+
* Optional evaluation context. `locals` resolves `Var` nodes (function
|
|
203
|
+
* params / `let` bindings); `callFn` resolves a user-function `Call` to a
|
|
204
|
+
* value (its name + already-evaluated args). Derived UNIFORMS pass neither
|
|
205
|
+
* (their rules are leaf-and-op only); derived MODES pass both (rule bodies
|
|
206
|
+
* have locals and call helpers like `flipCull`). See derivedModes/cpuEval.
|
|
207
|
+
*/
|
|
208
|
+
export interface EvalOpts {
|
|
209
|
+
readonly locals?: ReadonlyMap<string, EvalValue>;
|
|
210
|
+
readonly callFn?: (name: string, args: EvalValue[]) => EvalValue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function interpretExpr(ir: Expr, readLeaf: LeafReader, opts: EvalOpts = {}): EvalValue {
|
|
202
214
|
const ev = (e: Expr): EvalValue => {
|
|
203
215
|
switch (e.kind) {
|
|
204
216
|
case "Const": {
|
|
@@ -208,11 +220,21 @@ export function interpretExpr(ir: Expr, readLeaf: LeafReader): EvalValue {
|
|
|
208
220
|
throw new Error("derived eval: null const");
|
|
209
221
|
}
|
|
210
222
|
case "ReadInput": {
|
|
211
|
-
|
|
223
|
+
// Scope-agnostic: the readLeaf decides what a name resolves to
|
|
224
|
+
// (uniforms for derived uniforms; uniforms + `declared` for modes).
|
|
212
225
|
const raw = readLeaf(e.name);
|
|
213
|
-
if (raw === undefined) throw new Error("derived eval: missing
|
|
226
|
+
if (raw === undefined) throw new Error("derived eval: missing leaf '" + e.name + "'");
|
|
214
227
|
return coerce(raw, e.type);
|
|
215
228
|
}
|
|
229
|
+
case "Var": {
|
|
230
|
+
const v = opts.locals?.get(e.var.name);
|
|
231
|
+
if (v === undefined) throw new Error("derived eval: unbound local '" + e.var.name + "'");
|
|
232
|
+
return v;
|
|
233
|
+
}
|
|
234
|
+
case "Call": {
|
|
235
|
+
if (opts.callFn === undefined) throw new Error("derived eval: Call '" + e.fn.signature.name + "' but no callFn provided");
|
|
236
|
+
return opts.callFn(e.fn.signature.name, e.args.map(ev));
|
|
237
|
+
}
|
|
216
238
|
case "Neg": return negate(ev(e.value));
|
|
217
239
|
case "Not": return !(ev(e.value) as boolean);
|
|
218
240
|
case "Add": return elementwise(ev(e.lhs), ev(e.rhs), (x, y) => x + y);
|
|
@@ -43,7 +43,9 @@ export interface DerivedUniformsResources {
|
|
|
43
43
|
|
|
44
44
|
function bglEntries(): GPUBindGroupLayoutEntry[] {
|
|
45
45
|
return [
|
|
46
|
-
|
|
46
|
+
// Constituents: read_write — the chain pass writes per-RO Model constituents
|
|
47
|
+
// here (a separate, earlier dispatch); §7 reads them. read_write permits both.
|
|
48
|
+
{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } },
|
|
47
49
|
{ binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } },
|
|
48
50
|
{ binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
|
|
49
51
|
{ binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } },
|
|
@@ -119,9 +121,12 @@ class RecordsGpu {
|
|
|
119
121
|
class UberPipeline {
|
|
120
122
|
private readonly device: GPUDevice;
|
|
121
123
|
readonly bgl: GPUBindGroupLayout;
|
|
122
|
-
|
|
124
|
+
// Cache one pipeline per record stride (the kernel bakes RECORD_STRIDE). The
|
|
125
|
+
// chain pass and the §7 pass usually have different strides, and both are
|
|
126
|
+
// dispatched every frame, so a single-slot cache would thrash → recompile
|
|
127
|
+
// per frame. Cleared on a registry-version bump (new rule shape).
|
|
128
|
+
private readonly pipes = new Map<number, GPUComputePipeline>();
|
|
123
129
|
private builtVersion = -1;
|
|
124
|
-
private builtStride = -1;
|
|
125
130
|
|
|
126
131
|
constructor(device: GPUDevice) {
|
|
127
132
|
this.device = device;
|
|
@@ -129,20 +134,25 @@ class UberPipeline {
|
|
|
129
134
|
}
|
|
130
135
|
|
|
131
136
|
pipeline(registry: DerivedUniformRegistry, strideU32: number): GPUComputePipeline | undefined {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
137
|
+
// Always buildable: the kernel carries the built-in CHAIN arm even with an
|
|
138
|
+
// empty registry, so a scene using only transform-propagation chains (no
|
|
139
|
+
// §7 rules) still gets a pipeline. encodeChunk's recordCount===0 guard is
|
|
140
|
+
// what skips the dispatch when there's genuinely nothing to do.
|
|
141
|
+
if (registry.version !== this.builtVersion) {
|
|
142
|
+
this.pipes.clear();
|
|
143
|
+
this.builtVersion = registry.version;
|
|
135
144
|
}
|
|
145
|
+
const cached = this.pipes.get(strideU32);
|
|
146
|
+
if (cached !== undefined) return cached;
|
|
136
147
|
const { wgsl } = buildUberKernel(registry, strideU32);
|
|
137
148
|
const module = this.device.createShaderModule({ code: wgsl, label: "derivedUniforms.uber" });
|
|
138
|
-
|
|
149
|
+
const pipe = this.device.createComputePipeline({
|
|
139
150
|
label: "derivedUniforms.uber",
|
|
140
151
|
layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.bgl] }),
|
|
141
152
|
compute: { module, entryPoint: "main" },
|
|
142
153
|
});
|
|
143
|
-
this.
|
|
144
|
-
|
|
145
|
-
return this.pipe;
|
|
154
|
+
this.pipes.set(strideU32, pipe);
|
|
155
|
+
return pipe;
|
|
146
156
|
}
|
|
147
157
|
}
|
|
148
158
|
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
} from "./slots.js";
|
|
26
26
|
import { DerivedUniformRegistry } from "./registry.js";
|
|
27
27
|
import { RecordsBuffer, SlotTag, makeHandle } from "./records.js";
|
|
28
|
+
import { CHAIN_RULE_ID } from "./codegen.js";
|
|
28
29
|
import { DerivedUniformsDispatcher, uploadConstituentsRange } from "./dispatch.js";
|
|
29
30
|
import { flatten, type RuleInput } from "./flatten.js";
|
|
30
31
|
import { ruleFromIR, type DerivedRule } from "./rule.js";
|
|
@@ -41,6 +42,11 @@ export class DerivedUniformsScene {
|
|
|
41
42
|
* scene-wide; records partition by chunk so each dispatch binds
|
|
42
43
|
* the right chunk's main heap. */
|
|
43
44
|
private readonly recordsByChunk = new Map<number, RecordsBuffer>();
|
|
45
|
+
/** Scene-wide CHAIN records (GPU transform propagation). Dispatched BEFORE
|
|
46
|
+
* the §7 per-chunk passes — they write per-RO Model constituents that §7
|
|
47
|
+
* then reads. Scene-wide (not per-chunk) because they target the shared
|
|
48
|
+
* constituents buffer, not any chunk's main heap. */
|
|
49
|
+
readonly chainRecords = new RecordsBuffer();
|
|
44
50
|
readonly dispatcher: DerivedUniformsDispatcher;
|
|
45
51
|
/** Constituents storage GPU buffer (`array<vec2<f32>>` — df32 mat4 halves). */
|
|
46
52
|
constituentsBuf: GPUBuffer;
|
|
@@ -57,7 +63,7 @@ export class DerivedUniformsScene {
|
|
|
57
63
|
this.constituentsBuf = device.createBuffer({
|
|
58
64
|
label: "derivedUniforms.constituents",
|
|
59
65
|
size: initial * DF32_MAT4_BYTES,
|
|
60
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
66
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
|
|
61
67
|
});
|
|
62
68
|
this.dispatcher = new DerivedUniformsDispatcher(device, {
|
|
63
69
|
constituentsBuf: () => this.constituentsBuf,
|
|
@@ -71,6 +77,13 @@ export class DerivedUniformsScene {
|
|
|
71
77
|
this.mainHeapByChunk.set(chunkIdx, getter);
|
|
72
78
|
}
|
|
73
79
|
|
|
80
|
+
/** Any registered main-heap getter (the chain pass needs a MainHeap binding
|
|
81
|
+
* even though its arm never touches it). */
|
|
82
|
+
private firstHeapGetter(): (() => GPUBuffer) | undefined {
|
|
83
|
+
for (const g of this.mainHeapByChunk.values()) return g;
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
74
87
|
/** Get-or-create the records buffer for `chunkIdx`. */
|
|
75
88
|
recordsFor(chunkIdx: number): RecordsBuffer {
|
|
76
89
|
let r = this.recordsByChunk.get(chunkIdx);
|
|
@@ -97,7 +110,7 @@ export class DerivedUniformsScene {
|
|
|
97
110
|
this.constituentsBuf = this.device.createBuffer({
|
|
98
111
|
label: "derivedUniforms.constituents",
|
|
99
112
|
size: cap,
|
|
100
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
113
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
|
|
101
114
|
});
|
|
102
115
|
this.bufferEpoch++;
|
|
103
116
|
// Fresh buffer is zero-initialised; re-upload everything packed so far.
|
|
@@ -134,6 +147,17 @@ export class DerivedUniformsScene {
|
|
|
134
147
|
* main heap buffer. */
|
|
135
148
|
encode(enc: GPUCommandEncoder): boolean {
|
|
136
149
|
let any = false;
|
|
150
|
+
// Phase 1: chain pass — writes per-RO Model constituents (fwd+inv) into the
|
|
151
|
+
// shared constituents buffer. Must run BEFORE the §7 passes that read them.
|
|
152
|
+
// The MainHeap binding is unused by the chain arm; bind any registered heap.
|
|
153
|
+
if (this.chainRecords.recordCount > 0) {
|
|
154
|
+
const anyHeap = this.firstHeapGetter();
|
|
155
|
+
if (anyHeap === undefined) {
|
|
156
|
+
throw new Error("derivedUniforms.encode: chain records present but no chunk main-heap registered to bind.");
|
|
157
|
+
}
|
|
158
|
+
if (this.dispatcher.encodeChunk(enc, this.registry, this.chainRecords, anyHeap)) any = true;
|
|
159
|
+
}
|
|
160
|
+
// Phase 2: §7 per-chunk (reads constituents incl. the chain-written Model).
|
|
137
161
|
for (const [chunkIdx, records] of this.recordsByChunk) {
|
|
138
162
|
if (records.recordCount === 0) continue;
|
|
139
163
|
const heapGetter = this.mainHeapByChunk.get(chunkIdx);
|
|
@@ -159,6 +183,18 @@ export class DerivedUniformsScene {
|
|
|
159
183
|
export interface RoDerivedRequest {
|
|
160
184
|
/** Derived rules to install, keyed by the uniform name each produces. */
|
|
161
185
|
readonly rules: ReadonlyMap<string, DerivedRule>;
|
|
186
|
+
/**
|
|
187
|
+
* GPU transform propagation: the SG ancestor trafo chain for THIS RO's
|
|
188
|
+
* `Model` (root→leaf order, constant runs folded), as `aval<Trafo3d>` links.
|
|
189
|
+
* Links are constituents shared by aval identity, so a root trafo shared by
|
|
190
|
+
* N ROs is ONE slot referenced N times (no CPU fan-out). When present, the
|
|
191
|
+
* chain pass computes a per-RO `Model` constituent (fwd from the links, inv
|
|
192
|
+
* from the reversed inverse links) and the §7 rules' `Model` leaf resolves to
|
|
193
|
+
* it — so ModelTrafo / ModelView / ModelViewInv / NormalMatrix / custom rules
|
|
194
|
+
* all derive from the GPU-composed Model, unchanged. See
|
|
195
|
+
* docs/gpu-transform-propagation.md.
|
|
196
|
+
*/
|
|
197
|
+
readonly modelChain?: readonly aval<Trafo3d>[];
|
|
162
198
|
/** Trafo avals available on this RO, keyed by the uniform name a rule leaf may reference (e.g. "ModelTrafo"). */
|
|
163
199
|
readonly trafoAvals: ReadonlyMap<string, aval<Trafo3d>>;
|
|
164
200
|
/** drawHeader byte offset (relative to `drawHeaderBaseByte`) of a host-supplied uniform on this RO; undefined if not present. */
|
|
@@ -180,6 +216,9 @@ export interface RoRegistration {
|
|
|
180
216
|
readonly ruleHashes: readonly string[];
|
|
181
217
|
/** Chunk this registration was placed into — used by deregister. */
|
|
182
218
|
readonly chunkIdx: number;
|
|
219
|
+
/** Per-RO Model output constituent pair (transform propagation); freed on
|
|
220
|
+
* deregister. Present iff the request carried a `modelChain`. */
|
|
221
|
+
readonly modelPair?: PairedSlots;
|
|
183
222
|
}
|
|
184
223
|
|
|
185
224
|
export function registerRoDerivations(
|
|
@@ -187,7 +226,9 @@ export function registerRoDerivations(
|
|
|
187
226
|
owner: object,
|
|
188
227
|
req: RoDerivedRequest,
|
|
189
228
|
): RoRegistration {
|
|
190
|
-
if (req.rules.size === 0
|
|
229
|
+
if (req.rules.size === 0 && req.modelChain === undefined) {
|
|
230
|
+
return { owner, constituentAvals: [], ruleHashes: [], chunkIdx: req.chunkIdx };
|
|
231
|
+
}
|
|
191
232
|
const records = scene.recordsFor(req.chunkIdx);
|
|
192
233
|
|
|
193
234
|
const acquiredAvals: aval<Trafo3d>[] = [];
|
|
@@ -207,8 +248,9 @@ export function registerRoDerivations(
|
|
|
207
248
|
};
|
|
208
249
|
const resolve = (inp: RuleInput): number => {
|
|
209
250
|
// A matrix-typed leaf bound as an aval is a `Trafo3d` ⇒ a df32 constituent slot
|
|
210
|
-
// (its `Inverse` reads the stored backward half — free).
|
|
211
|
-
|
|
251
|
+
// (its `Inverse` reads the stored backward half — free). `Model` may instead
|
|
252
|
+
// be a GPU-composed chain output already in `pairByName` (modelChain case).
|
|
253
|
+
if (inp.type.kind === "Matrix" && (req.trafoAvals.has(inp.name) || pairByName.has(inp.name))) {
|
|
212
254
|
const p = acquireTrafo(inp.name);
|
|
213
255
|
return makeHandle(SlotTag.Constituent, inp.inverse ? p.inv : p.fwd);
|
|
214
256
|
}
|
|
@@ -225,6 +267,27 @@ export function registerRoDerivations(
|
|
|
225
267
|
);
|
|
226
268
|
};
|
|
227
269
|
|
|
270
|
+
// GPU transform propagation: compute this RO's `Model` as a per-RO constituent
|
|
271
|
+
// from its ancestor chain, and point §7's `Model` leaf at it. The chain pass
|
|
272
|
+
// (a separate, earlier dispatch) writes the fwd half from the links and the
|
|
273
|
+
// inv half from the reversed inverse links.
|
|
274
|
+
let modelPair: PairedSlots | undefined;
|
|
275
|
+
if (req.modelChain !== undefined) {
|
|
276
|
+
modelPair = scene.constituents.allocOutputPair();
|
|
277
|
+
pairByName.set("Model", modelPair);
|
|
278
|
+
const linkPairs = req.modelChain.map((av) => {
|
|
279
|
+
acquiredAvals.push(av);
|
|
280
|
+
return scene.constituents.acquire(av);
|
|
281
|
+
});
|
|
282
|
+
const n = linkPairs.length;
|
|
283
|
+
// Model.fwd = link0·link1·…·link(n-1) (forward order, fwd slots).
|
|
284
|
+
scene.chainRecords.add(owner, CHAIN_RULE_ID, makeHandle(SlotTag.Constituent, modelPair.fwd),
|
|
285
|
+
[n, ...linkPairs.map((p) => makeHandle(SlotTag.Constituent, p.fwd))]);
|
|
286
|
+
// Model.inv = link(n-1)⁻¹·…·link0⁻¹ (reversed order, inv slots).
|
|
287
|
+
scene.chainRecords.add(owner, CHAIN_RULE_ID, makeHandle(SlotTag.Constituent, modelPair.inv),
|
|
288
|
+
[n, ...linkPairs.slice().reverse().map((p) => makeHandle(SlotTag.Constituent, p.inv))]);
|
|
289
|
+
}
|
|
290
|
+
|
|
228
291
|
for (const [outName, rule] of req.rules) {
|
|
229
292
|
// `flatten` returns the same Expr object when nothing was substituted (the common case —
|
|
230
293
|
// the standard recipes reference no derived names), so we can reuse `rule` (it already
|
|
@@ -241,12 +304,18 @@ export function registerRoDerivations(
|
|
|
241
304
|
}
|
|
242
305
|
records.add(owner, id, makeHandle(SlotTag.HostHeap, req.drawHeaderBaseByte + outOff), inSlots);
|
|
243
306
|
}
|
|
307
|
+
|
|
244
308
|
scene.ensureConstituentsCapacity(scene.constituents.slotCount * DF32_MAT4_BYTES);
|
|
245
|
-
return {
|
|
309
|
+
return {
|
|
310
|
+
owner, constituentAvals: acquiredAvals, ruleHashes, chunkIdx: req.chunkIdx,
|
|
311
|
+
...(modelPair !== undefined ? { modelPair } : {}),
|
|
312
|
+
};
|
|
246
313
|
}
|
|
247
314
|
|
|
248
315
|
export function deregisterRoDerivations(scene: DerivedUniformsScene, reg: RoRegistration): void {
|
|
249
316
|
scene.recordsFor(reg.chunkIdx).removeAllForOwner(reg.owner);
|
|
317
|
+
scene.chainRecords.removeAllForOwner(reg.owner);
|
|
250
318
|
for (const h of reg.ruleHashes) scene.registry.release(h);
|
|
251
319
|
for (const av of reg.constituentAvals) scene.constituents.release(av);
|
|
320
|
+
if (reg.modelPair !== undefined) scene.constituents.freeOutputPair(reg.modelPair);
|
|
252
321
|
}
|