@aardworx/wombat.rendering 0.9.11 → 0.9.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/runtime/derivedUniforms/codegen.d.ts +8 -0
- package/dist/runtime/derivedUniforms/codegen.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/codegen.js +449 -0
- package/dist/runtime/derivedUniforms/codegen.js.map +1 -0
- package/dist/runtime/derivedUniforms/dispatch.d.ts +14 -93
- package/dist/runtime/derivedUniforms/dispatch.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/dispatch.js +123 -299
- package/dist/runtime/derivedUniforms/dispatch.js.map +1 -1
- package/dist/runtime/derivedUniforms/flatten.d.ts +25 -0
- package/dist/runtime/derivedUniforms/flatten.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/flatten.js +71 -0
- package/dist/runtime/derivedUniforms/flatten.js.map +1 -0
- package/dist/runtime/derivedUniforms/index.d.ts +10 -5
- package/dist/runtime/derivedUniforms/index.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/index.js +24 -19
- package/dist/runtime/derivedUniforms/index.js.map +1 -1
- package/dist/runtime/derivedUniforms/marker.d.ts +32 -0
- package/dist/runtime/derivedUniforms/marker.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/marker.js +84 -0
- package/dist/runtime/derivedUniforms/marker.js.map +1 -0
- package/dist/runtime/derivedUniforms/recipes.d.ts +6 -41
- package/dist/runtime/derivedUniforms/recipes.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/recipes.js +53 -129
- package/dist/runtime/derivedUniforms/recipes.js.map +1 -1
- package/dist/runtime/derivedUniforms/records.d.ts +40 -0
- package/dist/runtime/derivedUniforms/records.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/records.js +151 -0
- package/dist/runtime/derivedUniforms/records.js.map +1 -0
- package/dist/runtime/derivedUniforms/registry.d.ts +33 -0
- package/dist/runtime/derivedUniforms/registry.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/registry.js +58 -0
- package/dist/runtime/derivedUniforms/registry.js.map +1 -0
- package/dist/runtime/derivedUniforms/rule.d.ts +26 -0
- package/dist/runtime/derivedUniforms/rule.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/rule.js +35 -0
- package/dist/runtime/derivedUniforms/rule.js.map +1 -0
- package/dist/runtime/derivedUniforms/sceneIntegration.d.ts +34 -33
- package/dist/runtime/derivedUniforms/sceneIntegration.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/sceneIntegration.js +80 -90
- package/dist/runtime/derivedUniforms/sceneIntegration.js.map +1 -1
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +72 -27
- package/dist/runtime/heapScene.js.map +1 -1
- package/dist/runtime/hybridScene.js +1 -1
- package/dist/runtime/hybridScene.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 +2 -0
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/renderTask.js +1 -1
- package/dist/runtime/renderTask.js.map +1 -1
- package/dist/runtime/runtime.js +1 -1
- package/dist/runtime/runtime.js.map +1 -1
- package/package.json +1 -1
- package/src/runtime/derivedUniforms/codegen.ts +477 -0
- package/src/runtime/derivedUniforms/dispatch.ts +135 -341
- package/src/runtime/derivedUniforms/flatten.ts +89 -0
- package/src/runtime/derivedUniforms/index.ts +50 -34
- package/src/runtime/derivedUniforms/marker.ts +91 -0
- package/src/runtime/derivedUniforms/recipes.ts +63 -155
- package/src/runtime/derivedUniforms/records.ts +158 -0
- package/src/runtime/derivedUniforms/registry.ts +77 -0
- package/src/runtime/derivedUniforms/rule.ts +59 -0
- package/src/runtime/derivedUniforms/sceneIntegration.ts +115 -149
- package/src/runtime/heapScene.ts +73 -29
- package/src/runtime/hybridScene.ts +1 -1
- package/src/runtime/index.ts +8 -0
- package/src/runtime/renderTask.ts +1 -1
- package/src/runtime/runtime.ts +1 -1
- package/dist/runtime/derivedUniforms/slotId.d.ts +0 -15
- package/dist/runtime/derivedUniforms/slotId.d.ts.map +0 -1
- package/dist/runtime/derivedUniforms/slotId.js +0 -30
- package/dist/runtime/derivedUniforms/slotId.js.map +0 -1
- package/dist/runtime/derivedUniforms/uberKernel.wgsl.d.ts +0 -13
- package/dist/runtime/derivedUniforms/uberKernel.wgsl.d.ts.map +0 -1
- package/dist/runtime/derivedUniforms/uberKernel.wgsl.js +0 -218
- package/dist/runtime/derivedUniforms/uberKernel.wgsl.js.map +0 -1
- package/src/runtime/derivedUniforms/slotId.ts +0 -35
- package/src/runtime/derivedUniforms/uberKernel.wgsl.ts +0 -220
|
@@ -1,52 +1,68 @@
|
|
|
1
|
-
// §7 derived-uniforms module — public surface
|
|
1
|
+
// §7 derived-uniforms module — public surface (v2: content-keyed rule registry,
|
|
2
|
+
// chain flattening, codegen-emitted uber-kernel, one dispatch).
|
|
2
3
|
//
|
|
3
|
-
// Caller responsibilities (
|
|
4
|
+
// Caller responsibilities (heapScene.ts):
|
|
4
5
|
//
|
|
5
|
-
// 1. Construct one DerivedUniformsScene per heap scene,
|
|
6
|
-
//
|
|
7
|
-
// shared constituents storage buffer + dirty bitmask + pipeline.
|
|
6
|
+
// 1. Construct one DerivedUniformsScene per heap scene (owns the rule registry,
|
|
7
|
+
// the records buffer, the constituents heap + GPU buffer, the dispatcher).
|
|
8
8
|
// 2. In the parent scene-object's `inputChanged(t, o)` router, call
|
|
9
|
-
// `scene.routeInputChanged(o)
|
|
10
|
-
// 3.
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// 4.
|
|
14
|
-
//
|
|
15
|
-
// const dirty = scene.constituents.pullDirty(token);
|
|
9
|
+
// `scene.routeInputChanged(o)` — returns true iff `o` is a tracked trafo aval.
|
|
10
|
+
// 3. Per RO add: `registerRoDerivations(scene, owner, req)` — flattens + registers
|
|
11
|
+
// each rule, resolves its input leaves to constituent / host slots, pushes one
|
|
12
|
+
// record per derived uniform. Per RO remove: `deregisterRoDerivations(scene, reg)`.
|
|
13
|
+
// 4. Per frame, inside the scene's `evaluateAlways(token, …)` scope:
|
|
14
|
+
// const dirty = scene.pullDirty(token); // re-subscribes on changed trafo avals
|
|
16
15
|
// scene.uploadDirty(dirty);
|
|
17
|
-
//
|
|
18
|
-
// All
|
|
16
|
+
// scene.encode(enc); // one compute dispatch
|
|
17
|
+
// All O(changed); clean frames no-op.
|
|
19
18
|
//
|
|
20
|
-
//
|
|
19
|
+
// The standard trafo recipes (ModelViewProjTrafo etc.) live in ./recipes as
|
|
20
|
+
// `DerivedRule` constants — `STANDARD_DERIVED_RULES` / `isStandardDerivedName`.
|
|
21
21
|
|
|
22
22
|
export {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
type ConstituentRef, type RecipeOutput,
|
|
27
|
-
} from "./recipes.js";
|
|
23
|
+
type DerivedRule, type IRFragment,
|
|
24
|
+
ruleFromIR, uniformRef, sameType, hashIR, isDerivedRule,
|
|
25
|
+
} from "./rule.js";
|
|
28
26
|
|
|
29
27
|
export {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
derivedUniform, DerivedExpr, type DerivedScope,
|
|
29
|
+
} from "./marker.js";
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
type RuleInput,
|
|
33
|
+
flatten, inputsOf,
|
|
34
|
+
} from "./flatten.js";
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
DerivedUniformRegistry, type RuleEntry,
|
|
38
|
+
} from "./registry.js";
|
|
39
|
+
|
|
40
|
+
export {
|
|
41
|
+
RecordsBuffer, SlotTag,
|
|
42
|
+
makeHandle, handleTag, handlePayload,
|
|
43
|
+
type RecordOwner,
|
|
44
|
+
} from "./records.js";
|
|
45
|
+
|
|
46
|
+
export {
|
|
47
|
+
buildUberKernel, type UberKernel,
|
|
48
|
+
} from "./codegen.js";
|
|
33
49
|
|
|
34
50
|
export {
|
|
35
|
-
DerivedUniformsDispatcher,
|
|
36
|
-
RecordsBuffer, uploadConstituentsRange,
|
|
51
|
+
DerivedUniformsDispatcher, uploadConstituentsRange,
|
|
37
52
|
type DerivedUniformsResources,
|
|
38
53
|
} from "./dispatch.js";
|
|
39
54
|
|
|
40
55
|
export {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
type
|
|
47
|
-
|
|
56
|
+
ConstituentSlots, DF32_MAT4_BYTES,
|
|
57
|
+
type SlotIndex, type PairedSlots, type SubscribeFn,
|
|
58
|
+
} from "./slots.js";
|
|
59
|
+
|
|
60
|
+
export {
|
|
61
|
+
DerivedUniformsScene, type DerivedUniformsSceneOptions,
|
|
62
|
+
registerRoDerivations, deregisterRoDerivations,
|
|
63
|
+
type RoDerivedRequest, type RoRegistration,
|
|
48
64
|
} from "./sceneIntegration.js";
|
|
49
65
|
|
|
50
66
|
export {
|
|
51
|
-
|
|
52
|
-
} from "./
|
|
67
|
+
STANDARD_DERIVED_RULES, STANDARD_TRAFO_LEAVES, isStandardDerivedName,
|
|
68
|
+
} from "./recipes.js";
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// §7 v2 — `derivedUniform((u) => …)` author-facing marker.
|
|
2
|
+
//
|
|
3
|
+
// A uniform binding is either a *value* (an `aval` / constant) or a *rule*
|
|
4
|
+
// (`derivedUniform((u) => u.ViewTrafo.mul(u.ModelTrafo))`). A rule reads other
|
|
5
|
+
// uniforms — `u.<Name>` is a leaf, and is resolved per RenderObject at registration
|
|
6
|
+
// time (a `Trafo3d` aval ⇒ a df32 constituent slot; anything else ⇒ a host uniform).
|
|
7
|
+
//
|
|
8
|
+
// This builds the rule's IR by tracing: `u.<Name>` is a mat4 leaf (the trafo case —
|
|
9
|
+
// the vast majority of derived uniforms), and the `DerivedExpr` methods compose them
|
|
10
|
+
// (the result type follows from the ops, so `.upperLeft3x3()` yields a mat3 rule, etc.).
|
|
11
|
+
// For non-mat4-leaf rules use `ruleFromIR` with hand-built IR, or — once it lands — the
|
|
12
|
+
// build-time `derivedUniform(...)` vite marker, which reads leaf types from the program.
|
|
13
|
+
|
|
14
|
+
import type { Expr, Type } from "@aardworx/wombat.shader/ir";
|
|
15
|
+
import { ruleFromIR, uniformRef, type DerivedRule } from "./rule.js";
|
|
16
|
+
|
|
17
|
+
const Tf32: Type = { kind: "Float", width: 32 };
|
|
18
|
+
const mat = (n: 3 | 4): Type => ({ kind: "Matrix", element: Tf32, rows: n, cols: n });
|
|
19
|
+
const vec = (n: 2 | 3 | 4): Type => ({ kind: "Vector", element: Tf32, dim: n });
|
|
20
|
+
|
|
21
|
+
function transposed(t: Type): Type {
|
|
22
|
+
return t.kind === "Matrix" ? { kind: "Matrix", element: t.element, rows: t.cols, cols: t.rows } : t;
|
|
23
|
+
}
|
|
24
|
+
function constVec4(x: number, y: number, z: number, w: number): Expr {
|
|
25
|
+
const c = (v: number): Expr => ({ kind: "Const", type: Tf32, value: { kind: "Float", value: v } });
|
|
26
|
+
return { kind: "NewVector", components: [c(x), c(y), c(z), c(w)], type: vec(4) };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A traced expression in a `derivedUniform` builder. */
|
|
30
|
+
export class DerivedExpr {
|
|
31
|
+
constructor(readonly ir: Expr) {}
|
|
32
|
+
get type(): Type { return this.ir.type; }
|
|
33
|
+
|
|
34
|
+
/** Matrix product `this · other` (also matrix·vector / vector·matrix by operand types). */
|
|
35
|
+
mul(other: DerivedExpr): DerivedExpr {
|
|
36
|
+
const a = this.ir.type, b = other.ir.type;
|
|
37
|
+
if (a.kind === "Matrix" && b.kind === "Matrix") {
|
|
38
|
+
return new DerivedExpr({ kind: "MulMatMat", lhs: this.ir, rhs: other.ir, type: { kind: "Matrix", element: a.element, rows: a.rows, cols: b.cols } });
|
|
39
|
+
}
|
|
40
|
+
if (a.kind === "Matrix" && b.kind === "Vector") {
|
|
41
|
+
return new DerivedExpr({ kind: "MulMatVec", lhs: this.ir, rhs: other.ir, type: { kind: "Vector", element: b.element, dim: a.rows } });
|
|
42
|
+
}
|
|
43
|
+
if (a.kind === "Vector" && b.kind === "Matrix") {
|
|
44
|
+
return new DerivedExpr({ kind: "MulVecMat", lhs: this.ir, rhs: other.ir, type: { kind: "Vector", element: a.element, dim: b.cols } });
|
|
45
|
+
}
|
|
46
|
+
return new DerivedExpr({ kind: "Mul", lhs: this.ir, rhs: other.ir, type: a.kind === "Float" ? b : a });
|
|
47
|
+
}
|
|
48
|
+
add(other: DerivedExpr): DerivedExpr { return new DerivedExpr({ kind: "Add", lhs: this.ir, rhs: other.ir, type: this.ir.type }); }
|
|
49
|
+
sub(other: DerivedExpr): DerivedExpr { return new DerivedExpr({ kind: "Sub", lhs: this.ir, rhs: other.ir, type: this.ir.type }); }
|
|
50
|
+
neg(): DerivedExpr { return new DerivedExpr({ kind: "Neg", value: this.ir, type: this.ir.type }); }
|
|
51
|
+
|
|
52
|
+
/** Inverse (of a constituent trafo this reads its stored backward half; otherwise unsupported in codegen). */
|
|
53
|
+
inverse(): DerivedExpr { return new DerivedExpr({ kind: "Inverse", value: this.ir, type: this.ir.type }); }
|
|
54
|
+
transpose(): DerivedExpr { return new DerivedExpr({ kind: "Transpose", value: this.ir, type: transposed(this.ir.type) }); }
|
|
55
|
+
/** Upper-left 3×3 (mat → mat3). */
|
|
56
|
+
upperLeft3x3(): DerivedExpr { return new DerivedExpr({ kind: "ConvertMatrix", value: this.ir, type: mat(3) }); }
|
|
57
|
+
/** This mat4 applied to the origin `(0,0,0,1)`, returning the resulting position (xyz). */
|
|
58
|
+
transformOrigin(): DerivedExpr {
|
|
59
|
+
const v4: Expr = { kind: "MulMatVec", lhs: this.ir, rhs: constVec4(0, 0, 0, 1), type: vec(4) };
|
|
60
|
+
return new DerivedExpr({ kind: "VecSwizzle", value: v4, comps: ["x", "y", "z"], type: vec(3) });
|
|
61
|
+
}
|
|
62
|
+
/** vecN swizzle, e.g. `.swizzle("xyz")`. */
|
|
63
|
+
swizzle(comps: string): DerivedExpr {
|
|
64
|
+
const c = comps.split("") as ("x" | "y" | "z" | "w")[];
|
|
65
|
+
return new DerivedExpr({ kind: "VecSwizzle", value: this.ir, comps: c, type: c.length === 1 ? Tf32 : vec(c.length as 2 | 3 | 4) });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The `u` passed to a `derivedUniform` builder. `u.<Name>` is a mat4 trafo leaf. */
|
|
70
|
+
export type DerivedScope = { readonly [name: string]: DerivedExpr };
|
|
71
|
+
|
|
72
|
+
function makeScope(): DerivedScope {
|
|
73
|
+
return new Proxy({} as DerivedScope, {
|
|
74
|
+
get(_t, key): DerivedExpr {
|
|
75
|
+
if (typeof key !== "string") throw new Error("derivedUniform: leaf names must be strings");
|
|
76
|
+
return new DerivedExpr(uniformRef(key, mat(4)));
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Define a derived uniform. `derivedUniform(u => u.ViewTrafo.mul(u.ModelTrafo))` makes a
|
|
83
|
+
* rule that slots in wherever a uniform value goes — see this module's header.
|
|
84
|
+
*/
|
|
85
|
+
export function derivedUniform<T = unknown>(build: (u: DerivedScope) => DerivedExpr): DerivedRule<T> {
|
|
86
|
+
const result = build(makeScope());
|
|
87
|
+
if (!(result instanceof DerivedExpr)) {
|
|
88
|
+
throw new Error("derivedUniform: the builder must return a derived expression, e.g. u.ViewTrafo.mul(u.ModelTrafo)");
|
|
89
|
+
}
|
|
90
|
+
return ruleFromIR<T>(result.ir, result.ir.type);
|
|
91
|
+
}
|
|
@@ -1,159 +1,67 @@
|
|
|
1
|
-
// §7
|
|
1
|
+
// §7 v2 — the standard trafo recipes, as DerivedRule constants.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// These replace the hardcoded RecipeId table. Each rule reads BASE-trafo leaves
|
|
4
|
+
// (`Model` / `View` / `Proj` — the raw `aval<Trafo3d>`s the RO binds as
|
|
5
|
+
// `ModelTrafo` / `ViewTrafo` / `ProjTrafo` uniforms) and produces the derived
|
|
6
|
+
// uniform the shader reads (`ModelViewProjTrafo`, `NormalMatrix`, …). A leaf
|
|
7
|
+
// wrapped in `Inverse(...)` selects that trafo's stored backward half (free).
|
|
7
8
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
9
|
+
// `Inverse`-of-a-product (e.g. `ModelViewProjTrafoInv`) is written as the product
|
|
10
|
+
// of the inverted factors in reverse order — `(P·V·M)⁻¹ = M⁻¹·V⁻¹·P⁻¹` — so it
|
|
11
|
+
// stays a constituent matmul chain the codegen recognizes.
|
|
11
12
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
ProjTrafo
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
[
|
|
50
|
-
[
|
|
51
|
-
[
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
[
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
[
|
|
60
|
-
[
|
|
61
|
-
[
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
export function
|
|
65
|
-
return
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Output shape — drives drawHeader byte allocation. */
|
|
69
|
-
export type RecipeOutput = "mat4" | "mat3";
|
|
70
|
-
|
|
71
|
-
const OUTPUT: Record<RecipeId, RecipeOutput> = {
|
|
72
|
-
[RecipeId.ModelTrafo]: "mat4",
|
|
73
|
-
[RecipeId.ModelTrafoInv]: "mat4",
|
|
74
|
-
[RecipeId.NormalMatrix]: "mat3",
|
|
75
|
-
[RecipeId.ModelViewTrafo]: "mat4",
|
|
76
|
-
[RecipeId.ModelViewTrafoInv]: "mat4",
|
|
77
|
-
[RecipeId.ModelViewProjTrafo]: "mat4",
|
|
78
|
-
[RecipeId.ModelViewProjTrafoInv]: "mat4",
|
|
79
|
-
[RecipeId.ViewTrafo]: "mat4",
|
|
80
|
-
[RecipeId.ViewTrafoInv]: "mat4",
|
|
81
|
-
[RecipeId.ViewProjTrafo]: "mat4",
|
|
82
|
-
[RecipeId.ViewProjTrafoInv]: "mat4",
|
|
83
|
-
[RecipeId.ProjTrafo]: "mat4",
|
|
84
|
-
[RecipeId.ProjTrafoInv]: "mat4",
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
export function recipeOutput(id: RecipeId): RecipeOutput {
|
|
88
|
-
return OUTPUT[id];
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Which side (forward/backward) of which Trafo3d each input slot draws from. */
|
|
92
|
-
export type ConstituentRef =
|
|
93
|
-
| "Model.fwd" | "Model.bwd"
|
|
94
|
-
| "View.fwd" | "View.bwd"
|
|
95
|
-
| "Proj.fwd" | "Proj.bwd";
|
|
96
|
-
|
|
97
|
-
const INPUTS: Record<RecipeId, readonly ConstituentRef[]> = {
|
|
98
|
-
[RecipeId.ModelTrafo]: ["Model.fwd"],
|
|
99
|
-
[RecipeId.ModelTrafoInv]: ["Model.bwd"],
|
|
100
|
-
[RecipeId.NormalMatrix]: ["Model.bwd"],
|
|
101
|
-
// df_mul args are ordered (outer, inner) so the math product
|
|
102
|
-
// matches Aardvark's `View.mul(Model).forward = View.fwd · Model.fwd`.
|
|
103
|
-
[RecipeId.ModelViewTrafo]: ["View.fwd", "Model.fwd"],
|
|
104
|
-
[RecipeId.ModelViewTrafoInv]: ["Model.bwd", "View.bwd"],
|
|
105
|
-
[RecipeId.ModelViewProjTrafo]: ["Proj.fwd", "View.fwd", "Model.fwd"],
|
|
106
|
-
[RecipeId.ModelViewProjTrafoInv]: ["Model.bwd", "View.bwd", "Proj.bwd"],
|
|
107
|
-
[RecipeId.ViewTrafo]: ["View.fwd"],
|
|
108
|
-
[RecipeId.ViewTrafoInv]: ["View.bwd"],
|
|
109
|
-
[RecipeId.ViewProjTrafo]: ["Proj.fwd", "View.fwd"],
|
|
110
|
-
[RecipeId.ViewProjTrafoInv]: ["View.bwd", "Proj.bwd"],
|
|
111
|
-
[RecipeId.ProjTrafo]: ["Proj.fwd"],
|
|
112
|
-
[RecipeId.ProjTrafoInv]: ["Proj.bwd"],
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
export function recipeInputs(id: RecipeId): readonly ConstituentRef[] {
|
|
116
|
-
return INPUTS[id];
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** Public uniform name (one per recipe). */
|
|
120
|
-
const NAME: Record<RecipeId, string> = {
|
|
121
|
-
[RecipeId.ModelTrafo]: "ModelTrafo",
|
|
122
|
-
[RecipeId.ModelTrafoInv]: "ModelTrafoInv",
|
|
123
|
-
[RecipeId.NormalMatrix]: "NormalMatrix",
|
|
124
|
-
[RecipeId.ModelViewTrafo]: "ModelViewTrafo",
|
|
125
|
-
[RecipeId.ModelViewTrafoInv]: "ModelViewTrafoInv",
|
|
126
|
-
[RecipeId.ModelViewProjTrafo]: "ModelViewProjTrafo",
|
|
127
|
-
[RecipeId.ModelViewProjTrafoInv]: "ModelViewProjTrafoInv",
|
|
128
|
-
[RecipeId.ViewTrafo]: "ViewTrafo",
|
|
129
|
-
[RecipeId.ViewTrafoInv]: "ViewTrafoInv",
|
|
130
|
-
[RecipeId.ViewProjTrafo]: "ViewProjTrafo",
|
|
131
|
-
[RecipeId.ViewProjTrafoInv]: "ViewProjTrafoInv",
|
|
132
|
-
[RecipeId.ProjTrafo]: "ProjTrafo",
|
|
133
|
-
[RecipeId.ProjTrafoInv]: "ProjTrafoInv",
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
/** All recipe IDs in switch order. */
|
|
137
|
-
export const ALL_RECIPES: readonly RecipeId[] = [
|
|
138
|
-
RecipeId.ModelTrafo, RecipeId.ModelTrafoInv, RecipeId.NormalMatrix,
|
|
139
|
-
RecipeId.ModelViewTrafo, RecipeId.ModelViewTrafoInv,
|
|
140
|
-
RecipeId.ModelViewProjTrafo, RecipeId.ModelViewProjTrafoInv,
|
|
141
|
-
RecipeId.ViewTrafo, RecipeId.ViewTrafoInv,
|
|
142
|
-
RecipeId.ViewProjTrafo, RecipeId.ViewProjTrafoInv,
|
|
143
|
-
RecipeId.ProjTrafo, RecipeId.ProjTrafoInv,
|
|
144
|
-
];
|
|
145
|
-
|
|
146
|
-
const BY_NAME: ReadonlyMap<string, RecipeId> = new Map(
|
|
147
|
-
ALL_RECIPES.map(id => [NAME[id], id] as const),
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
export const DERIVED_UNIFORM_NAMES: ReadonlySet<string> =
|
|
151
|
-
new Set(ALL_RECIPES.map(id => NAME[id]));
|
|
152
|
-
|
|
153
|
-
export function recipeIdByName(name: string): RecipeId | undefined {
|
|
154
|
-
return BY_NAME.get(name);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export function recipeName(id: RecipeId): string {
|
|
158
|
-
return NAME[id];
|
|
13
|
+
// `NormalMatrix` is `(Model⁻¹)ᵀ` truncated to its upper-3×3 — declared as a mat3
|
|
14
|
+
// output; the codegen's normal-matrix arm does the transpose + truncation.
|
|
15
|
+
|
|
16
|
+
import type { Expr, Type } from "@aardworx/wombat.shader/ir";
|
|
17
|
+
import { ruleFromIR, uniformRef, type DerivedRule } from "./rule.js";
|
|
18
|
+
|
|
19
|
+
const Tf32: Type = { kind: "Float", width: 32 };
|
|
20
|
+
const Tmat4: Type = { kind: "Matrix", element: Tf32, rows: 4, cols: 4 };
|
|
21
|
+
const Tmat3: Type = { kind: "Matrix", element: Tf32, rows: 3, cols: 3 };
|
|
22
|
+
|
|
23
|
+
const mat4 = (name: string): Expr => uniformRef(name, Tmat4);
|
|
24
|
+
const inv = (v: Expr): Expr => ({ kind: "Inverse", value: v, type: Tmat4 });
|
|
25
|
+
const mul = (lhs: Expr, rhs: Expr): Expr => ({ kind: "MulMatMat", lhs, rhs, type: Tmat4 });
|
|
26
|
+
/** Left-fold `A·B·C·…` (multiplication order). */
|
|
27
|
+
const chain = (...fs: Expr[]): Expr => fs.reduce((acc, f) => mul(acc, f));
|
|
28
|
+
/** mat4 → mat3 truncation (upper-left 3×3). */
|
|
29
|
+
const upper3 = (v: Expr): Expr => ({ kind: "ConvertMatrix", value: v, type: Tmat3 });
|
|
30
|
+
const transpose = (v: Expr): Expr => ({ kind: "Transpose", value: v, type: Tmat4 });
|
|
31
|
+
|
|
32
|
+
const Model = mat4("Model");
|
|
33
|
+
const View = mat4("View");
|
|
34
|
+
const Proj = mat4("Proj");
|
|
35
|
+
|
|
36
|
+
/** Standard derived rules, keyed by the drawHeader uniform name each produces. */
|
|
37
|
+
export const STANDARD_DERIVED_RULES: ReadonlyMap<string, DerivedRule> = new Map<string, DerivedRule>([
|
|
38
|
+
// Trafo passthroughs (collapse the df32 fwd/bwd half to an f32 mat4).
|
|
39
|
+
["ModelTrafo", ruleFromIR(Model)],
|
|
40
|
+
["ModelTrafoInv", ruleFromIR(inv(Model))],
|
|
41
|
+
["ViewTrafo", ruleFromIR(View)],
|
|
42
|
+
["ViewTrafoInv", ruleFromIR(inv(View))],
|
|
43
|
+
["ProjTrafo", ruleFromIR(Proj)],
|
|
44
|
+
["ProjTrafoInv", ruleFromIR(inv(Proj))],
|
|
45
|
+
|
|
46
|
+
// Composites. `mul(a,b)` ⇒ matrix product `a · b`.
|
|
47
|
+
["ModelViewTrafo", ruleFromIR(chain(View, Model))],
|
|
48
|
+
["ModelViewTrafoInv", ruleFromIR(chain(inv(Model), inv(View)))],
|
|
49
|
+
["ModelViewProjTrafo", ruleFromIR(chain(Proj, View, Model))],
|
|
50
|
+
["ModelViewProjTrafoInv", ruleFromIR(chain(inv(Model), inv(View), inv(Proj)))],
|
|
51
|
+
["ViewProjTrafo", ruleFromIR(chain(Proj, View))],
|
|
52
|
+
["ViewProjTrafoInv", ruleFromIR(chain(inv(View), inv(Proj)))],
|
|
53
|
+
|
|
54
|
+
// NormalMatrix = (Model⁻¹)ᵀ, upper-3×3.
|
|
55
|
+
["NormalMatrix", ruleFromIR(upper3(transpose(inv(Model))), Tmat3)],
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
/** Leaf name → the RO uniform (`spec.inputs[…]`) that supplies its `aval<Trafo3d>`. */
|
|
59
|
+
export const STANDARD_TRAFO_LEAVES: ReadonlyMap<string, string> = new Map([
|
|
60
|
+
["Model", "ModelTrafo"],
|
|
61
|
+
["View", "ViewTrafo"],
|
|
62
|
+
["Proj", "ProjTrafo"],
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
export function isStandardDerivedName(name: string): boolean {
|
|
66
|
+
return STANDARD_DERIVED_RULES.has(name);
|
|
159
67
|
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// §7 v2 — derived-uniform records buffer (CPU-side authoring; uploaded to a GPU storage buffer).
|
|
2
|
+
//
|
|
3
|
+
// One record per derived-uniform instance (one per (RenderObject, derived name)):
|
|
4
|
+
// [ rule_id, out_slot, in_slot[0], in_slot[1], … ]
|
|
5
|
+
// `out_slot` / each `in_slot` is a tagged 32-bit handle (see resolveSource):
|
|
6
|
+
// top 3 bits = source binding, low 29 = payload (constituent slot index / drawHeader
|
|
7
|
+
// byte offset).
|
|
8
|
+
//
|
|
9
|
+
// Records are flattened (chains substituted away) ⇒ every record is independent ⇒ the
|
|
10
|
+
// uber-kernel runs one thread per record, no levels. Layout is fixed-stride: STRIDE_U32 =
|
|
11
|
+
// 2 + maxArity. If a higher-arity rule shows up the stride grows and `data` is re-packed
|
|
12
|
+
// (rare; the kernel recompiles on that registry-version bump anyway). Fixed stride ⇒ no
|
|
13
|
+
// separate offsets array, trivial swap-remove. See docs/derived-uniforms-extensible.md.
|
|
14
|
+
|
|
15
|
+
/** Tagged-handle source bindings (top 3 bits of an in_slot / out_slot word). */
|
|
16
|
+
export const enum SlotTag {
|
|
17
|
+
/** Constituent slot — a Trafo3d half in df32 storage. Payload = slot index. */
|
|
18
|
+
Constituent = 0,
|
|
19
|
+
/** A host uniform on the same RO — payload = its data byte offset in the main heap. Also used for the output.
|
|
20
|
+
* There is no separate "global" tag: whether a uniform is global is a per-RO fact (the sg can override it
|
|
21
|
+
* anywhere), so anything that isn't a constituent trafo resolves to this; a uniform that's the same on every
|
|
22
|
+
* RO is the same aval ⇒ the UniformPool interns it to one heap slot anyway. */
|
|
23
|
+
HostHeap = 1,
|
|
24
|
+
// 2..7 reserved (per-instance attribute arena, indirect df32 pair, …)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const TAG_SHIFT = 29;
|
|
28
|
+
const PAYLOAD_MASK = (1 << TAG_SHIFT) - 1; // 0x1FFFFFFF
|
|
29
|
+
|
|
30
|
+
export function makeHandle(tag: SlotTag, payload: number): number {
|
|
31
|
+
if (payload < 0 || payload > PAYLOAD_MASK) {
|
|
32
|
+
throw new Error(`derived records: slot payload ${payload} out of range (max ${PAYLOAD_MASK})`);
|
|
33
|
+
}
|
|
34
|
+
return ((tag << TAG_SHIFT) | payload) >>> 0;
|
|
35
|
+
}
|
|
36
|
+
export function handleTag(h: number): SlotTag {
|
|
37
|
+
return (h >>> TAG_SHIFT) as SlotTag;
|
|
38
|
+
}
|
|
39
|
+
export function handlePayload(h: number): number {
|
|
40
|
+
return h & PAYLOAD_MASK;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Owner key — typically the RenderObject (or any stable object). */
|
|
44
|
+
export type RecordOwner = object;
|
|
45
|
+
|
|
46
|
+
const MIN_STRIDE_U32 = 5; // rule_id, out_slot, in0, in1, in2 — covers the 13 standard recipes
|
|
47
|
+
|
|
48
|
+
export class RecordsBuffer {
|
|
49
|
+
private buf: Uint32Array;
|
|
50
|
+
private owners: (RecordOwner | undefined)[] = [];
|
|
51
|
+
private readonly byOwner = new Map<RecordOwner, Set<number>>();
|
|
52
|
+
private strideU32 = MIN_STRIDE_U32;
|
|
53
|
+
private count = 0;
|
|
54
|
+
/** Bumps whenever the byte layout changes shape (stride growth) so the dispatcher recompiles. */
|
|
55
|
+
layoutVersion = 0;
|
|
56
|
+
/** Bumps on every mutation (add / remove / stride growth) so the dispatcher knows to re-upload. */
|
|
57
|
+
generation = 0;
|
|
58
|
+
|
|
59
|
+
constructor(initialCapacityRecords = 256) {
|
|
60
|
+
this.buf = new Uint32Array(Math.max(1, initialCapacityRecords) * this.strideU32);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get recordCount(): number {
|
|
64
|
+
return this.count;
|
|
65
|
+
}
|
|
66
|
+
get strideWords(): number {
|
|
67
|
+
return this.strideU32;
|
|
68
|
+
}
|
|
69
|
+
get strideBytes(): number {
|
|
70
|
+
return this.strideU32 * 4;
|
|
71
|
+
}
|
|
72
|
+
/** The packed record data, exactly `recordCount * strideWords` long. Upload this. */
|
|
73
|
+
get data(): Uint32Array {
|
|
74
|
+
return this.buf.subarray(0, this.count * this.strideU32);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Append a record. Returns its index. `inSlots` length may exceed the current stride (triggers growth). */
|
|
78
|
+
add(owner: RecordOwner, ruleId: number, outSlot: number, inSlots: readonly number[]): number {
|
|
79
|
+
const need = 2 + inSlots.length;
|
|
80
|
+
if (need > this.strideU32) this.growStride(need);
|
|
81
|
+
this.ensureCapacity(this.count + 1);
|
|
82
|
+
const idx = this.count++;
|
|
83
|
+
const base = idx * this.strideU32;
|
|
84
|
+
this.buf[base] = ruleId >>> 0;
|
|
85
|
+
this.buf[base + 1] = outSlot >>> 0;
|
|
86
|
+
for (let i = 0; i < inSlots.length; i++) this.buf[base + 2 + i] = inSlots[i]! >>> 0;
|
|
87
|
+
for (let i = base + 2 + inSlots.length; i < base + this.strideU32; i++) this.buf[i] = 0;
|
|
88
|
+
this.owners[idx] = owner;
|
|
89
|
+
let set = this.byOwner.get(owner);
|
|
90
|
+
if (set === undefined) {
|
|
91
|
+
set = new Set();
|
|
92
|
+
this.byOwner.set(owner, set);
|
|
93
|
+
}
|
|
94
|
+
set.add(idx);
|
|
95
|
+
this.generation++;
|
|
96
|
+
return idx;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Remove every record owned by `owner` (swap-remove against the tail). */
|
|
100
|
+
removeAllForOwner(owner: RecordOwner): void {
|
|
101
|
+
const set = this.byOwner.get(owner);
|
|
102
|
+
if (set === undefined || set.size === 0) {
|
|
103
|
+
this.byOwner.delete(owner);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
// Process highest indices first so a swap can never move a not-yet-removed
|
|
107
|
+
// record of this owner that we still hold a stale index for.
|
|
108
|
+
const indices = [...set].sort((a, b) => b - a);
|
|
109
|
+
for (const idx of indices) this.removeAt(idx);
|
|
110
|
+
this.byOwner.delete(owner);
|
|
111
|
+
this.generation++;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private removeAt(idx: number): void {
|
|
115
|
+
const last = this.count - 1;
|
|
116
|
+
// The record currently at `idx` is the one being removed.
|
|
117
|
+
this.byOwner.get(this.owners[idx]!)?.delete(idx);
|
|
118
|
+
if (idx !== last) {
|
|
119
|
+
// Move the tail record's words into the hole, and re-home it.
|
|
120
|
+
const dst = idx * this.strideU32;
|
|
121
|
+
const src = last * this.strideU32;
|
|
122
|
+
this.buf.copyWithin(dst, src, src + this.strideU32);
|
|
123
|
+
const lastOwner = this.owners[last]!;
|
|
124
|
+
const lset = this.byOwner.get(lastOwner)!;
|
|
125
|
+
lset.delete(last);
|
|
126
|
+
lset.add(idx);
|
|
127
|
+
this.owners[idx] = lastOwner;
|
|
128
|
+
}
|
|
129
|
+
this.owners[last] = undefined;
|
|
130
|
+
this.owners.length = last;
|
|
131
|
+
this.count = last;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private ensureCapacity(records: number): void {
|
|
135
|
+
const needWords = records * this.strideU32;
|
|
136
|
+
if (needWords <= this.buf.length) return;
|
|
137
|
+
let cap = Math.max(1, this.buf.length);
|
|
138
|
+
while (cap < needWords) cap *= 2;
|
|
139
|
+
const next = new Uint32Array(cap);
|
|
140
|
+
next.set(this.buf);
|
|
141
|
+
this.buf = next;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private growStride(newStride: number): void {
|
|
145
|
+
const old = this.strideU32;
|
|
146
|
+
const next = new Uint32Array(Math.max(this.buf.length, this.count * newStride));
|
|
147
|
+
for (let i = 0; i < this.count; i++) {
|
|
148
|
+
const o = i * old;
|
|
149
|
+
const n = i * newStride;
|
|
150
|
+
next.set(this.buf.subarray(o, o + old), n);
|
|
151
|
+
// remaining words already zero
|
|
152
|
+
}
|
|
153
|
+
this.buf = next;
|
|
154
|
+
this.strideU32 = newStride;
|
|
155
|
+
this.layoutVersion++;
|
|
156
|
+
this.generation++;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// §7 v2 — derived-uniform rule registry.
|
|
2
|
+
//
|
|
3
|
+
// Content-hash–keyed table of FLATTENED rules. `register` dedups on the
|
|
4
|
+
// flattened IR's hash, assigns a monotonic id (never reused), and bumps
|
|
5
|
+
// `version` whenever a new unique rule appears so the dispatcher knows to
|
|
6
|
+
// recompile its uber-kernel. v0 never sweeps dead entries (a stale rule's
|
|
7
|
+
// switch arm just goes unused — bytes per rule). See
|
|
8
|
+
// docs/derived-uniforms-extensible.md § "Registry" / "Lifecycle".
|
|
9
|
+
|
|
10
|
+
import type { Expr, Type } from "@aardworx/wombat.shader/ir";
|
|
11
|
+
import type { DerivedRule } from "./rule.js";
|
|
12
|
+
import { inputsOf, type RuleInput } from "./flatten.js";
|
|
13
|
+
|
|
14
|
+
export interface RuleEntry {
|
|
15
|
+
/** Monotonic id — the value the uber-kernel `switch`es on. Never reused. */
|
|
16
|
+
readonly id: number;
|
|
17
|
+
/** Flattened IR (reads only non-derived inputs). */
|
|
18
|
+
readonly ir: Expr;
|
|
19
|
+
readonly outputType: Type;
|
|
20
|
+
/** Distinct inputs of `ir`, sorted by name — defines the switch-arm argument order. */
|
|
21
|
+
readonly inputs: readonly RuleInput[];
|
|
22
|
+
refcount: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class DerivedUniformRegistry {
|
|
26
|
+
private readonly byHash = new Map<string, RuleEntry>();
|
|
27
|
+
private readonly byId = new Map<number, RuleEntry>();
|
|
28
|
+
private nextId = 0;
|
|
29
|
+
/** Bumps on every `register` that finds a NEW hash. */
|
|
30
|
+
version = 0;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Register a flattened rule. Returns its stable id. Idempotent per hash
|
|
34
|
+
* (just bumps the refcount); a new hash allocates an id and bumps `version`.
|
|
35
|
+
*/
|
|
36
|
+
register(rule: DerivedRule): number {
|
|
37
|
+
const cached = this.byHash.get(rule.hash);
|
|
38
|
+
if (cached !== undefined) {
|
|
39
|
+
cached.refcount++;
|
|
40
|
+
return cached.id;
|
|
41
|
+
}
|
|
42
|
+
const entry: RuleEntry = {
|
|
43
|
+
id: this.nextId++,
|
|
44
|
+
ir: rule.ir,
|
|
45
|
+
outputType: rule.outputType,
|
|
46
|
+
inputs: inputsOf(rule.ir),
|
|
47
|
+
refcount: 1,
|
|
48
|
+
};
|
|
49
|
+
this.byHash.set(rule.hash, entry);
|
|
50
|
+
this.byId.set(entry.id, entry);
|
|
51
|
+
this.version++;
|
|
52
|
+
return entry.id;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Drop one reference. v0: never sweeps — kept for forward-compat with v1 GC. */
|
|
56
|
+
release(hash: string): void {
|
|
57
|
+
const entry = this.byHash.get(hash);
|
|
58
|
+
if (entry !== undefined && entry.refcount > 0) entry.refcount--;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get(id: number): RuleEntry | undefined {
|
|
62
|
+
return this.byId.get(id);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
getByHash(hash: string): RuleEntry | undefined {
|
|
66
|
+
return this.byHash.get(hash);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** All entries in id order — the codegen's switch-arm list. */
|
|
70
|
+
entries(): RuleEntry[] {
|
|
71
|
+
return [...this.byId.values()].sort((a, b) => a.id - b.id);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get size(): number {
|
|
75
|
+
return this.byId.size;
|
|
76
|
+
}
|
|
77
|
+
}
|