@aardworx/wombat.rendering 0.19.9 → 0.19.11

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.
@@ -0,0 +1,271 @@
1
+ // CPU interpreter for derived-uniform rule IR (a wombat.shader `Expr`).
2
+ //
3
+ // Evaluates a `derivedUniform(u => …)` rule in DOUBLE precision (M44d / V3d /
4
+ // …) — no df32 dual-float trick, no GPU constituent slots. The heap path
5
+ // compiles rules into a §7 GPU compute pass (f32, df32 for the inverse trick);
6
+ // here we just interpret the IR with f64 host math (`M44d.inverse()` is exact
7
+ // enough that the backward-storage trick is unnecessary), and `writeField`
8
+ // narrows the result to f32 at pack time.
9
+ //
10
+ // Used by the legacy per-RO uniform path so derived uniforms work on ROs that
11
+ // aren't heap-eligible (a storage buffer, 2+ textures, …). The evaluator is a
12
+ // standalone `Expr → value` function so other CPU rule paths can reuse it.
13
+
14
+ import type { Expr, Type } from "@aardworx/wombat.shader/ir";
15
+ import { V2d, V3d, V4d, M22d, M33d, M44d } from "@aardworx/wombat.base";
16
+
17
+ /** A value produced by the interpreter (host doubles). */
18
+ export type EvalValue = number | boolean | V2d | V3d | V4d | M22d | M33d | M44d;
19
+
20
+ /** Resolve a `u.<name>` leaf to its current host value (Trafo3d | M44f | M44d |
21
+ * V*f | number | …) — or undefined if unbound. */
22
+ export type LeafReader = (name: string) => unknown;
23
+
24
+ // ── runtime classification ───────────────────────────────────────────────
25
+ function isVec(v: unknown): v is V2d | V3d | V4d {
26
+ return v instanceof V2d || v instanceof V3d || v instanceof V4d;
27
+ }
28
+ function isMat(v: unknown): v is M22d | M33d | M44d {
29
+ return v instanceof M22d || v instanceof M33d || v instanceof M44d;
30
+ }
31
+ function data(v: EvalValue): Float64Array { return (v as unknown as { _data: Float64Array })._data; }
32
+ function num(v: EvalValue): number {
33
+ if (typeof v === "number") return v;
34
+ if (typeof v === "boolean") return v ? 1 : 0;
35
+ throw new Error("derived eval: expected scalar, got " + describe(v));
36
+ }
37
+ function describe(v: unknown): string {
38
+ if (isVec(v)) return "vec" + data(v as EvalValue).length;
39
+ if (isMat(v)) return "mat";
40
+ return typeof v;
41
+ }
42
+ function vecOf(d: ArrayLike<number>): V2d | V3d | V4d {
43
+ switch (d.length) {
44
+ case 2: return new V2d(d[0]!, d[1]!);
45
+ case 3: return new V3d(d[0]!, d[1]!, d[2]!);
46
+ case 4: return new V4d(d[0]!, d[1]!, d[2]!, d[3]!);
47
+ default: throw new Error("derived eval: bad vector arity " + d.length);
48
+ }
49
+ }
50
+
51
+ // ── leaf coercion (by the IR leaf type) ──────────────────────────────────
52
+ function toM44(raw: unknown): M44d {
53
+ if (raw instanceof M44d) return raw;
54
+ const o = raw as { forward?: unknown; _data?: ArrayLike<number> };
55
+ if (o && o.forward !== undefined) return toM44(o.forward); // Trafo3d → forward
56
+ if (o && o._data !== undefined && o._data.length >= 16) return M44d.fromArray(o._data); // M44f
57
+ throw new Error("derived eval: leaf is not a mat4: " + describe(raw));
58
+ }
59
+ function upperLeft3(m: M44d): M33d {
60
+ const d = data(m); // row-major r*4+c
61
+ return M33d.fromArray([d[0]!, d[1]!, d[2]!, d[4]!, d[5]!, d[6]!, d[8]!, d[9]!, d[10]!]);
62
+ }
63
+ function toM33(raw: unknown): M33d {
64
+ if (raw instanceof M33d) return raw;
65
+ const o = raw as { forward?: unknown; _data?: ArrayLike<number> };
66
+ if (raw instanceof M44d) return upperLeft3(raw);
67
+ if (o && o.forward !== undefined) return upperLeft3(toM44(o.forward));
68
+ if (o && o._data !== undefined && o._data.length === 9) return M33d.fromArray(o._data);
69
+ if (o && o._data !== undefined && o._data.length >= 16) return upperLeft3(toM44(raw));
70
+ throw new Error("derived eval: leaf is not a mat3: " + describe(raw));
71
+ }
72
+ function toM22(raw: unknown): M22d {
73
+ if (raw instanceof M22d) return raw;
74
+ const o = raw as { _data?: ArrayLike<number> };
75
+ if (o && o._data !== undefined && o._data.length === 4) return M22d.fromArray(o._data);
76
+ throw new Error("derived eval: leaf is not a mat2: " + describe(raw));
77
+ }
78
+ function toVec(raw: unknown, n: 2 | 3 | 4): V2d | V3d | V4d {
79
+ if (isVec(raw)) return raw;
80
+ const o = raw as { _data?: ArrayLike<number> };
81
+ if (o && o._data !== undefined) {
82
+ const d = o._data;
83
+ return vecOf(n === 2 ? [d[0]!, d[1]!] : n === 3 ? [d[0]!, d[1]!, d[2]!] : [d[0]!, d[1]!, d[2]!, d[3]!]);
84
+ }
85
+ throw new Error("derived eval: leaf is not a vec" + n + ": " + describe(raw));
86
+ }
87
+ function coerce(raw: unknown, t: Type): EvalValue {
88
+ switch (t.kind) {
89
+ case "Matrix": return t.rows === 4 ? toM44(raw) : t.rows === 3 ? toM33(raw) : toM22(raw);
90
+ case "Vector": return toVec(raw, t.dim);
91
+ case "Float": return Number(raw);
92
+ case "Int": return Math.trunc(Number(raw));
93
+ case "Bool": return Boolean(raw);
94
+ default: throw new Error("derived eval: unsupported leaf type " + t.kind);
95
+ }
96
+ }
97
+
98
+ // ── arithmetic over scalar / vector / matrix ─────────────────────────────
99
+ function negate(x: EvalValue): EvalValue {
100
+ if (typeof x === "number") return -x;
101
+ if (isVec(x) || isMat(x)) return remap(x, (a) => -a);
102
+ throw new Error("derived eval: cannot negate " + describe(x));
103
+ }
104
+ /** Map a vector/matrix's components through `f`, returning the same shape. */
105
+ function remap(v: V2d | V3d | V4d | M22d | M33d | M44d, f: (x: number) => number): EvalValue {
106
+ const d = data(v); const out = new Array<number>(d.length);
107
+ for (let i = 0; i < d.length; i++) out[i] = f(d[i]!);
108
+ return isMat(v) ? matOf(out) : vecOf(out);
109
+ }
110
+ function matOf(d: ArrayLike<number>): M22d | M33d | M44d {
111
+ switch (d.length) {
112
+ case 4: return M22d.fromArray(d);
113
+ case 9: return M33d.fromArray(d);
114
+ case 16: return M44d.fromArray(d);
115
+ default: throw new Error("derived eval: bad matrix arity " + d.length);
116
+ }
117
+ }
118
+ /** Element-wise binary op, broadcasting a scalar against a vector/matrix. */
119
+ function elementwise(a: EvalValue, b: EvalValue, f: (x: number, y: number) => number): EvalValue {
120
+ if (typeof a === "number" && typeof b === "number") return f(a, b);
121
+ if (isVec(a) || isMat(a)) {
122
+ const da = data(a);
123
+ const out = new Array<number>(da.length);
124
+ if (typeof b === "number") { for (let i = 0; i < da.length; i++) out[i] = f(da[i]!, b); }
125
+ else { const db = data(b as EvalValue); for (let i = 0; i < da.length; i++) out[i] = f(da[i]!, db[i]!); }
126
+ return isMat(a) ? matOf(out) : vecOf(out);
127
+ }
128
+ if (typeof a === "number" && (isVec(b) || isMat(b))) {
129
+ const db = data(b); const out = new Array<number>(db.length);
130
+ for (let i = 0; i < db.length; i++) out[i] = f(a, db[i]!);
131
+ return isMat(b) ? matOf(out) : vecOf(out);
132
+ }
133
+ throw new Error(`derived eval: cannot combine ${describe(a)} and ${describe(b)}`);
134
+ }
135
+ /** `Mul` node — scalar·scalar, scalar·(vec|mat) broadcast, or component-wise
136
+ * vec·vec (mat·mat / mat·vec / vec·mat have their own node kinds). */
137
+ function mulNode(a: EvalValue, b: EvalValue): EvalValue {
138
+ if (typeof a === "number" && isMat(b)) return (b as M44d).mul(a) as EvalValue;
139
+ if (typeof b === "number" && isMat(a)) return (a as M44d).mul(b) as EvalValue;
140
+ return elementwise(a, b, (x, y) => x * y);
141
+ }
142
+ function vlen(v: EvalValue): number {
143
+ if (typeof v === "number") return Math.abs(v);
144
+ const d = data(v); let s = 0; for (let i = 0; i < d.length; i++) s += d[i]! * d[i]!;
145
+ return Math.sqrt(s);
146
+ }
147
+ function dot(a: EvalValue, b: EvalValue): number {
148
+ const da = data(a), db = data(b); let s = 0;
149
+ for (let i = 0; i < da.length; i++) s += da[i]! * db[i]!;
150
+ return s;
151
+ }
152
+ function normalize(v: EvalValue): EvalValue {
153
+ const len = vlen(v); return len === 0 ? v : remap(v as V3d, (x) => x / len);
154
+ }
155
+ function swizzle(v: EvalValue, comps: readonly string[]): EvalValue {
156
+ const d = data(v); const idx = (c: string): number => ({ x: 0, y: 1, z: 2, w: 3 }[c] ?? 0);
157
+ if (comps.length === 1) return d[idx(comps[0]!)]!;
158
+ return vecOf(comps.map((c) => d[idx(c)]!));
159
+ }
160
+ function convertMatrix(v: EvalValue, t: Type): EvalValue {
161
+ if (t.kind === "Matrix" && t.rows === 3 && v instanceof M44d) return upperLeft3(v);
162
+ if (isMat(v)) return v; // identity-ish convert
163
+ throw new Error("derived eval: ConvertMatrix on " + describe(v));
164
+ }
165
+
166
+ // ── intrinsics (vec-componentwise where natural) ─────────────────────────
167
+ const UNARY: Record<string, (x: number) => number> = {
168
+ sin: Math.sin, cos: Math.cos, tan: Math.tan, abs: Math.abs, floor: Math.floor,
169
+ ceil: Math.ceil, sqrt: Math.sqrt, exp: Math.exp, log: Math.log,
170
+ exp2: (x) => 2 ** x, log2: Math.log2, sign: Math.sign, fract: (x) => x - Math.floor(x),
171
+ };
172
+ const BINARY: Record<string, (x: number, y: number) => number> = {
173
+ min: Math.min, max: Math.max, pow: (x, y) => x ** y, atan2: Math.atan2,
174
+ };
175
+ function intrinsic(name: string, a: EvalValue[]): EvalValue {
176
+ if (UNARY[name]) return typeof a[0] === "number" ? UNARY[name]!(a[0]) : remap(a[0] as V3d, UNARY[name]!);
177
+ if (BINARY[name]) return elementwise(a[0]!, a[1]!, BINARY[name]!);
178
+ switch (name) {
179
+ case "normalize": return normalize(a[0]!);
180
+ case "length": return vlen(a[0]!);
181
+ case "dot": return dot(a[0]!, a[1]!);
182
+ case "cross": return (a[0] as V3d).cross(a[1] as V3d);
183
+ case "distance": return vlen(elementwise(a[0]!, a[1]!, (x, y) => x - y));
184
+ case "step": return elementwise(a[1]!, a[0]!, (x, edge) => (x < edge ? 0 : 1));
185
+ case "clamp": return elementwise(elementwise(a[0]!, a[1]!, Math.max), a[2]!, Math.min);
186
+ case "mix": { // a + (b - a) * t
187
+ const diff = elementwise(a[1]!, a[0]!, (y, x) => y - x);
188
+ const scaled = elementwise(diff, a[2]!, (d, t) => d * t);
189
+ return elementwise(a[0]!, scaled, (x, s) => x + s);
190
+ }
191
+ case "reflect": { const d = dot(a[0]!, a[1]!); return elementwise(a[0]!, elementwise(a[1]!, 2 * d, (n, k) => n * k), (i, s) => i - s); }
192
+ default: throw new Error("derived eval: unsupported intrinsic '" + name + "'");
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Interpret a derived-uniform rule IR, reading `u.<name>` leaves via
198
+ * `readLeaf`. Returns a host double value (`M44d` / `V3d` / number / …) that
199
+ * `writeField` narrows to f32 when packing the UBO.
200
+ */
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 {
214
+ const ev = (e: Expr): EvalValue => {
215
+ switch (e.kind) {
216
+ case "Const": {
217
+ const v = e.value;
218
+ if (v.kind === "Bool") return v.value;
219
+ if (v.kind === "Int" || v.kind === "Float") return v.value;
220
+ throw new Error("derived eval: null const");
221
+ }
222
+ case "ReadInput": {
223
+ // Scope-agnostic: the readLeaf decides what a name resolves to
224
+ // (uniforms for derived uniforms; uniforms + `declared` for modes).
225
+ const raw = readLeaf(e.name);
226
+ if (raw === undefined) throw new Error("derived eval: missing leaf '" + e.name + "'");
227
+ return coerce(raw, e.type);
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
+ }
238
+ case "Neg": return negate(ev(e.value));
239
+ case "Not": return !(ev(e.value) as boolean);
240
+ case "Add": return elementwise(ev(e.lhs), ev(e.rhs), (x, y) => x + y);
241
+ case "Sub": return elementwise(ev(e.lhs), ev(e.rhs), (x, y) => x - y);
242
+ case "Div": return elementwise(ev(e.lhs), ev(e.rhs), (x, y) => x / y);
243
+ case "Mod": return elementwise(ev(e.lhs), ev(e.rhs), (x, y) => x % y);
244
+ case "Mul": return mulNode(ev(e.lhs), ev(e.rhs));
245
+ case "MulMatMat": return (ev(e.lhs) as M44d).mul(ev(e.rhs) as M44d) as EvalValue;
246
+ case "MulMatVec": return (ev(e.lhs) as M44d).mul(ev(e.rhs) as V4d) as EvalValue;
247
+ case "MulVecMat": return (ev(e.rhs) as M44d).transpose().mul(ev(e.lhs) as V4d) as EvalValue;
248
+ case "Transpose": return (ev(e.value) as M44d).transpose();
249
+ case "Inverse": return (ev(e.value) as M44d).inverse();
250
+ case "Determinant": return (ev(e.value) as M44d).determinant();
251
+ case "Dot": return dot(ev(e.lhs), ev(e.rhs));
252
+ case "Cross": return (ev(e.lhs) as V3d).cross(ev(e.rhs) as V3d);
253
+ case "Length": return vlen(ev(e.value));
254
+ case "ConvertMatrix": return convertMatrix(ev(e.value), e.type);
255
+ case "VecSwizzle": return swizzle(ev(e.value), e.comps);
256
+ case "NewVector": return vecOf(e.components.map((c) => num(ev(c))));
257
+ case "And": return (ev(e.lhs) as boolean) && (ev(e.rhs) as boolean);
258
+ case "Or": return (ev(e.lhs) as boolean) || (ev(e.rhs) as boolean);
259
+ case "Eq": return num(ev(e.lhs)) === num(ev(e.rhs));
260
+ case "Neq": return num(ev(e.lhs)) !== num(ev(e.rhs));
261
+ case "Lt": return num(ev(e.lhs)) < num(ev(e.rhs));
262
+ case "Le": return num(ev(e.lhs)) <= num(ev(e.rhs));
263
+ case "Gt": return num(ev(e.lhs)) > num(ev(e.rhs));
264
+ case "Ge": return num(ev(e.lhs)) >= num(ev(e.rhs));
265
+ case "Conditional": return (ev(e.cond) as boolean) ? ev(e.ifTrue) : ev(e.ifFalse);
266
+ case "CallIntrinsic": return intrinsic(e.op.name, e.args.map(ev));
267
+ default: throw new Error("derived eval: unsupported IR node '" + e.kind + "'");
268
+ }
269
+ };
270
+ return ev(ir);
271
+ }