@aardworx/wombat.rendering 0.9.32 → 0.18.1

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.
Files changed (95) hide show
  1. package/dist/runtime/derivedModes/kernelCodegen.d.ts +90 -0
  2. package/dist/runtime/derivedModes/kernelCodegen.d.ts.map +1 -0
  3. package/dist/runtime/derivedModes/kernelCodegen.js +623 -0
  4. package/dist/runtime/derivedModes/kernelCodegen.js.map +1 -0
  5. package/dist/runtime/derivedModes/modeKeyCpu.d.ts.map +1 -1
  6. package/dist/runtime/derivedModes/modeKeyCpu.js +23 -67
  7. package/dist/runtime/derivedModes/modeKeyCpu.js.map +1 -1
  8. package/dist/runtime/derivedModes/partitionDispatcher.d.ts +79 -0
  9. package/dist/runtime/derivedModes/partitionDispatcher.d.ts.map +1 -0
  10. package/dist/runtime/derivedModes/partitionDispatcher.js +303 -0
  11. package/dist/runtime/derivedModes/partitionDispatcher.js.map +1 -0
  12. package/dist/runtime/derivedModes/partitionKernel.d.ts +12 -0
  13. package/dist/runtime/derivedModes/partitionKernel.d.ts.map +1 -0
  14. package/dist/runtime/derivedModes/partitionKernel.js +127 -0
  15. package/dist/runtime/derivedModes/partitionKernel.js.map +1 -0
  16. package/dist/runtime/derivedModes/partitionKernelLayout.d.ts +4 -0
  17. package/dist/runtime/derivedModes/partitionKernelLayout.d.ts.map +1 -0
  18. package/dist/runtime/derivedModes/partitionKernelLayout.js +24 -0
  19. package/dist/runtime/derivedModes/partitionKernelLayout.js.map +1 -0
  20. package/dist/runtime/derivedModes/rule.d.ts +38 -86
  21. package/dist/runtime/derivedModes/rule.d.ts.map +1 -1
  22. package/dist/runtime/derivedModes/rule.js +34 -74
  23. package/dist/runtime/derivedModes/rule.js.map +1 -1
  24. package/dist/runtime/derivedUniforms/dispatch.d.ts +10 -8
  25. package/dist/runtime/derivedUniforms/dispatch.d.ts.map +1 -1
  26. package/dist/runtime/derivedUniforms/dispatch.js +31 -18
  27. package/dist/runtime/derivedUniforms/dispatch.js.map +1 -1
  28. package/dist/runtime/derivedUniforms/marker.d.ts +90 -11
  29. package/dist/runtime/derivedUniforms/marker.d.ts.map +1 -1
  30. package/dist/runtime/derivedUniforms/marker.js +161 -15
  31. package/dist/runtime/derivedUniforms/marker.js.map +1 -1
  32. package/dist/runtime/derivedUniforms/sceneIntegration.d.ts +25 -6
  33. package/dist/runtime/derivedUniforms/sceneIntegration.d.ts.map +1 -1
  34. package/dist/runtime/derivedUniforms/sceneIntegration.js +61 -20
  35. package/dist/runtime/derivedUniforms/sceneIntegration.js.map +1 -1
  36. package/dist/runtime/heapEligibility.d.ts.map +1 -1
  37. package/dist/runtime/heapEligibility.js +22 -2
  38. package/dist/runtime/heapEligibility.js.map +1 -1
  39. package/dist/runtime/heapScene/chunkedArena.d.ts +76 -0
  40. package/dist/runtime/heapScene/chunkedArena.d.ts.map +1 -0
  41. package/dist/runtime/heapScene/chunkedArena.js +223 -0
  42. package/dist/runtime/heapScene/chunkedArena.js.map +1 -0
  43. package/dist/runtime/heapScene/freelist.d.ts +50 -0
  44. package/dist/runtime/heapScene/freelist.d.ts.map +1 -0
  45. package/dist/runtime/heapScene/freelist.js +160 -0
  46. package/dist/runtime/heapScene/freelist.js.map +1 -0
  47. package/dist/runtime/heapScene/growBuffer.d.ts +23 -2
  48. package/dist/runtime/heapScene/growBuffer.d.ts.map +1 -1
  49. package/dist/runtime/heapScene/growBuffer.js +35 -3
  50. package/dist/runtime/heapScene/growBuffer.js.map +1 -1
  51. package/dist/runtime/heapScene/pools.d.ts +65 -42
  52. package/dist/runtime/heapScene/pools.d.ts.map +1 -1
  53. package/dist/runtime/heapScene/pools.js +206 -134
  54. package/dist/runtime/heapScene/pools.js.map +1 -1
  55. package/dist/runtime/heapScene.d.ts +12 -0
  56. package/dist/runtime/heapScene.d.ts.map +1 -1
  57. package/dist/runtime/heapScene.js +949 -167
  58. package/dist/runtime/heapScene.js.map +1 -1
  59. package/dist/runtime/hybridScene.d.ts +7 -0
  60. package/dist/runtime/hybridScene.d.ts.map +1 -1
  61. package/dist/runtime/hybridScene.js +1 -0
  62. package/dist/runtime/hybridScene.js.map +1 -1
  63. package/dist/runtime/index.d.ts +4 -3
  64. package/dist/runtime/index.d.ts.map +1 -1
  65. package/dist/runtime/index.js +4 -3
  66. package/dist/runtime/index.js.map +1 -1
  67. package/dist/runtime/renderTask.d.ts +5 -0
  68. package/dist/runtime/renderTask.d.ts.map +1 -1
  69. package/dist/runtime/renderTask.js +1 -0
  70. package/dist/runtime/renderTask.js.map +1 -1
  71. package/dist/runtime/runtime.d.ts +6 -0
  72. package/dist/runtime/runtime.d.ts.map +1 -1
  73. package/dist/runtime/runtime.js +1 -0
  74. package/dist/runtime/runtime.js.map +1 -1
  75. package/package.json +1 -1
  76. package/src/runtime/derivedModes/kernelCodegen.ts +673 -0
  77. package/src/runtime/derivedModes/modeKeyCpu.ts +22 -60
  78. package/src/runtime/derivedModes/partitionDispatcher.ts +349 -0
  79. package/src/runtime/derivedModes/partitionKernelLayout.ts +25 -0
  80. package/src/runtime/derivedModes/rule.ts +66 -145
  81. package/src/runtime/derivedUniforms/dispatch.ts +48 -21
  82. package/src/runtime/derivedUniforms/marker.ts +210 -16
  83. package/src/runtime/derivedUniforms/sceneIntegration.ts +67 -20
  84. package/src/runtime/heapEligibility.ts +22 -1
  85. package/src/runtime/heapScene/chunkedArena.ts +238 -0
  86. package/src/runtime/heapScene/freelist.ts +161 -0
  87. package/src/runtime/heapScene/growBuffer.ts +39 -2
  88. package/src/runtime/heapScene/pools.ts +231 -143
  89. package/src/runtime/heapScene.ts +1134 -161
  90. package/src/runtime/hybridScene.ts +8 -0
  91. package/src/runtime/index.ts +12 -9
  92. package/src/runtime/renderTask.ts +6 -0
  93. package/src/runtime/runtime.ts +7 -0
  94. package/src/runtime/derivedModes/gpuDispatcher.ts +0 -226
  95. package/src/runtime/derivedModes/gpuKernel.ts +0 -98
@@ -1,35 +1,44 @@
1
- // `derivedMode(axis, (u, declared) => modeValue)` author-facing
2
- // marker for per-RO pipeline-state values that are functions of the
3
- // RO's uniforms.
1
+ // Derived-mode rule branded combination of a shader-IR rule
2
+ // expression (from `rule(...)` in wombat.shader) + an axis + the
3
+ // SG-declared aval. The IR is what gets compiled into the partition
4
+ // kernel; the declared aval drives per-frame re-specialisation.
4
5
  //
5
- // v1 is CPU-evaluated: the user's closure runs in JS each time the
6
- // rule's inputs mark. That's a good fit for the typical authoring
7
- // shapes (a uniform-driven enum flip, a determinant-sign cull flip,
8
- // etc.) and pairs naturally with the bucket-level rebuild fast path
9
- // shipped in 0.9.16 — only ROs whose inputs actually changed get
10
- // re-evaluated, and same-key buckets re-bind their pipeline once
11
- // rather than per-RO.
6
+ // Authoring flow (with `@boperators/plugin-vite` + the shader-vite
7
+ // plugin both installed):
12
8
  //
13
- // v2 (deferred) lowers rules to a GPU compute kernel via §7's IR
14
- // machinery needed only when bulk-animated per-RO inputs become a
15
- // CPU-side bottleneck. See docs/derived-mode-rules-plan.md.
9
+ // declare const u: { ModelTrafo: M44f };
10
+ // declare const declared: number;
16
11
  //
17
- // What can be derived (the small-enum axes — cull/blend/depth-compare/
18
- // topology/etc.) and what stays static (depth bias, blend constant,
19
- // stencil ref) is covered in docs/derived-modes.md.
12
+ // const cullExpr = rule(() => {
13
+ // const m: M33f = mat3(u.ModelTrafo);
14
+ // const det = determinant(m);
15
+ // return det < 0 ? flipCull(declared) : declared;
16
+ // });
17
+ //
18
+ // const cullRule = derivedMode("cull", cullExpr, { declared: cullModeC });
19
+ //
20
+ // At runtime, heapScene reads `cullRule.expr.template`, extracts the
21
+ // entry's body Stmt, calls `analyseOutputSet(body)` to obtain the
22
+ // symbolic set, and for each declared-aval value resolves the set
23
+ // via `evaluateSet({"declared": current}, intrinsics)` to size the
24
+ // bucket's slots + codegen the partition kernel.
20
25
 
21
- import type { CullMode, FrontFace, Topology } from "../pipelineCache/index.js";
26
+ import type { RuleExpr } from "@aardworx/wombat.shader";
27
+ import type { aval } from "@aardworx/wombat.adaptive";
28
+ import type { CullMode, FrontFace, Topology, AttachmentBlend } from "../pipelineCache/index.js";
22
29
 
23
- /** Discrete pipeline-state axes a `derivedMode` rule can target. v1
24
- * supports cull / frontFace / topology / depthCompare / depthWrite /
25
- * blendEnabled. Stencil + per-attachment blend factor/operation are
26
- * deferred — same carve-outs as `derived-modes.md`. */
27
30
  export type ModeAxis =
28
31
  | "cull" | "frontFace" | "topology"
29
32
  | "depthCompare" | "depthWrite"
30
- | "alphaToCoverage";
33
+ | "alphaToCoverage"
34
+ /** Per-attachment blend state. The rule's u32 output indexes into
35
+ * the `values` array on `DerivedModeOptions` (or the `resolve`
36
+ * callback); each entry is a full {@link AttachmentBlend} struct
37
+ * with srcFactor / dstFactor / operation / writeMask / enabled
38
+ * fields — letting blend rules specify the entire mode object,
39
+ * not just an enum index. */
40
+ | "blend";
31
41
 
32
- /** Output value type per axis. */
33
42
  export type ModeValue<A extends ModeAxis> =
34
43
  A extends "cull" ? CullMode :
35
44
  A extends "frontFace" ? FrontFace :
@@ -37,147 +46,59 @@ export type ModeValue<A extends ModeAxis> =
37
46
  A extends "depthCompare" ? GPUCompareFunction :
38
47
  A extends "depthWrite" ? boolean :
39
48
  A extends "alphaToCoverage" ? boolean :
49
+ A extends "blend" ? AttachmentBlend :
40
50
  never;
41
51
 
42
- /**
43
- * The closure signature a `derivedMode` rule body takes:
44
- * - `u`: a proxy whose properties resolve to the RO's uniform values
45
- * at evaluation time. The proxy is type-erased in v1 — declare the
46
- * uniforms you'll read via `UniformScope` augmentation on the
47
- * wombat.shader side for compile-time safety, or annotate locally.
48
- * - `declared`: the SG-declared value for this axis at this leaf.
49
- * Identity rules `(u, d) => d` are valid and useful for axes that
50
- * want to pass declared through unchanged.
51
- */
52
- export type DerivedModeBuilder<A extends ModeAxis> = (
53
- u: Record<string, unknown>,
54
- declared: ModeValue<A>,
55
- ) => ModeValue<A>;
56
-
57
- /**
58
- * Opt-in GPU evaluation marker. When present on a rule, the heap
59
- * scene runs a hard-coded WGSL kernel (see `./gpuKernel.ts`) instead
60
- * of the CPU closure. The closure is retained for fallback (e.g.
61
- * MockGPU tests, devices without compute, or before frame 0).
62
- *
63
- * v1 supports one kernel: `"flipCullByDeterminant"`. v2 generalizes
64
- * via a traced IR builder.
65
- */
66
- export interface GpuRuleSpec<A extends ModeAxis = ModeAxis> {
67
- readonly kernel: "flipCullByDeterminant";
68
- /** Name of the input uniform the kernel reads (e.g. "ModelTrafo"). */
69
- readonly inputUniform: string;
70
- /**
71
- * Declared (SG-scope) value the kernel mutates by. Either a plain
72
- * mode value (baked at scene-build) or an `aval` for reactive
73
- * declared — the dispatcher re-uploads the kernel uniform on each
74
- * dispatch from the aval's current value, so flipping it via a UI
75
- * cval re-runs the rule with the new declared.
76
- */
77
- readonly declared:
78
- | ModeValue<A>
79
- | import("@aardworx/wombat.adaptive").aval<ModeValue<A>>;
80
- }
81
-
82
52
  export interface DerivedModeRule<A extends ModeAxis = ModeAxis> {
83
53
  readonly __derivedModeRule: true;
84
- readonly axis: A;
85
- readonly evaluate: DerivedModeBuilder<A>;
86
- /** Opt-in GPU evaluation. Absent CPU closure path. */
87
- readonly gpu?: GpuRuleSpec<A>;
54
+ readonly axis: A;
55
+ /** Shader-IR rule body — the entry's return Expr is the rule. For
56
+ * axes whose `ModeValue<A>` is a struct (e.g. blend's
57
+ * `AttachmentBlend`) the body returns object literals directly;
58
+ * the renderer fingerprints distinct returns and auto-assigns
59
+ * slot indices. For enum axes (cull etc.) the body returns u32
60
+ * enum indices that the renderer maps via the canonical table. */
61
+ readonly expr: RuleExpr<ModeValue<A> | number>;
88
62
  /**
89
- * Conservative over-approximation of the rule's output set. Used by
90
- * the build-time pre-warm in v2 to enumerate the pipelines a scene
91
- * could realize. v1 doesn't use it at runtime, but recording it
92
- * here keeps the API forward-compatible.
63
+ * SG-context value for this axis. Filled in by `<Sg CullMode>` /
64
+ * `<Sg DepthTest>` / etc. at traversal time users never write
65
+ * it. The rule body's `declared` ambient leaf resolves to this;
66
+ * when the underlying aval marks, heapScene re-specialises +
67
+ * swaps the cached kernel + pipeline set.
68
+ *
69
+ * `undefined` is the initial state right after `derivedMode(...)`;
70
+ * the SG fills it in before the rule reaches the renderer.
93
71
  */
94
- readonly domain?: ReadonlyArray<ModeValue<A>>;
95
- }
96
-
97
- /**
98
- * Author a derived mode rule.
99
- *
100
- * derivedMode("cull", (u, declared) =>
101
- * u.WindingFlipped ? flipCull(declared) : declared);
102
- *
103
- * derivedMode("blend", (u) => u.Premultiplied ? "premul" : "straight");
104
- *
105
- * // Determinant-flip-cull (the motivating case):
106
- * derivedMode("cull", (u, declared) => {
107
- * const m = u.ModelTrafo as M44d;
108
- * const det =
109
- * m.M00 * (m.M11 * m.M22 - m.M12 * m.M21) -
110
- * m.M01 * (m.M10 * m.M22 - m.M12 * m.M20) +
111
- * m.M02 * (m.M10 * m.M21 - m.M11 * m.M20);
112
- * return det < 0 ? flipCull(declared) : declared;
113
- * });
114
- */
115
- export function derivedMode<A extends ModeAxis>(
116
- axis: A,
117
- evaluate: DerivedModeBuilder<A>,
118
- options: { readonly domain?: ReadonlyArray<ModeValue<A>> } = {},
119
- ): DerivedModeRule<A> {
120
- const out: DerivedModeRule<A> = options.domain !== undefined
121
- ? { __derivedModeRule: true, axis, evaluate, domain: options.domain }
122
- : { __derivedModeRule: true, axis, evaluate };
123
- return out;
72
+ readonly declared: aval<ModeValue<A>> | ModeValue<A> | undefined;
124
73
  }
125
74
 
126
- /** Brand check — distinguishes a DerivedModeRule from an aval/raw value
127
- * at runtime so PipelineState consumers can route accordingly. */
128
75
  export function isDerivedModeRule(x: unknown): x is DerivedModeRule {
129
76
  return typeof x === "object" && x !== null
130
77
  && (x as { __derivedModeRule?: unknown }).__derivedModeRule === true;
131
78
  }
132
79
 
133
- /** Convenience: flip 'back' ↔ 'front' (and pass through 'none').
134
- * Useful in `(u, declared) => mirrored ? flipCull(declared) : declared`. */
135
- export function flipCull(c: CullMode): CullMode {
136
- return c === "back" ? "front" : c === "front" ? "back" : "none";
137
- }
138
80
 
139
81
  /**
140
- * Construct a derived-mode rule that flips cullMode based on the sign
141
- * of `det(upperLeft3x3(u.<inputUniform>))`. Runs on the GPU when
142
- * heapScene's compute path is available; the same logic is mirrored
143
- * in the CPU closure for tests and fallback.
82
+ * Brand a `RuleExpr` (the shader-IR artefact emitted by the
83
+ * `rule(...)` build-time marker) as a derived-mode rule for `axis`.
144
84
  *
145
- * const rule = gpuFlipCullByDeterminant("ModelTrafo", "back");
146
- * spec.modeRules = { cull: rule };
85
+ * The `declared` option carries the SG-context value the rule reads
86
+ * via `declare const declared: number` in its body. heapScene
87
+ * substitutes it (as a Const) before codegen — when `declared` is a
88
+ * cval, every mark triggers a re-specialise + cached-kernel swap.
147
89
  *
148
- * Useful for mirrored geometry: scenes that animate a per-RO trafo
149
- * with potentially-negative scale don't pay CPU per-RO determinant
150
- * cost the kernel walks the arena once per frame for ALL ROs that
151
- * carry this rule.
90
+ * `domain` is NOT a parameter: the renderer infers the per-declared
91
+ * set of possible outputs from the rule's IR via
92
+ * `analyseOutputSet` + `evaluateSet`.
152
93
  */
153
- export function gpuFlipCullByDeterminant(
154
- inputUniform: string,
155
- declared:
156
- | CullMode
157
- | import("@aardworx/wombat.adaptive").aval<CullMode>
158
- = "back",
159
- ): DerivedModeRule<"cull"> {
94
+ export function derivedMode<A extends ModeAxis>(
95
+ axis: A,
96
+ expr: RuleExpr<ModeValue<A> | number>,
97
+ ): DerivedModeRule<A> {
160
98
  return {
161
99
  __derivedModeRule: true,
162
- axis: "cull",
163
- domain: ["back", "front", "none"],
164
- gpu: { kernel: "flipCullByDeterminant", inputUniform, declared },
165
- evaluate: (u, declaredFromCtx) => {
166
- // Accept Trafo3d ({forward: M44d}) — the wombat.base shape used
167
- // by SG ModelTrafo — or any object with M00..M22 accessors.
168
- const raw = u[inputUniform];
169
- type M = { M00: number; M01: number; M02: number;
170
- M10: number; M11: number; M12: number;
171
- M20: number; M21: number; M22: number };
172
- const m: M | undefined =
173
- (raw as { forward?: M }).forward
174
- ?? (raw as M);
175
- if (m === undefined || typeof m.M00 !== "number") return declaredFromCtx;
176
- const det =
177
- m.M00 * (m.M11 * m.M22 - m.M12 * m.M21)
178
- - m.M01 * (m.M10 * m.M22 - m.M12 * m.M20)
179
- + m.M02 * (m.M10 * m.M21 - m.M11 * m.M20);
180
- return det < 0 ? flipCull(declaredFromCtx) : declaredFromCtx;
181
- },
100
+ axis,
101
+ expr,
102
+ declared: undefined, // SG fills this in at traversal time
182
103
  };
183
104
  }
@@ -32,12 +32,13 @@ export function uploadConstituentsRange(
32
32
  device.queue.writeBuffer(buf, startByte, mirror.buffer, mirror.byteOffset + startByte, endByte - startByte);
33
33
  }
34
34
 
35
- /** Live getters for the GPU buffers the kernel binds (scene may grow/replace them). */
35
+ /** Live getters for the scene-wide GPU buffers the kernel binds. The
36
+ * main-heap binding is per-dispatch and supplied to `encodeChunk`,
37
+ * since one DerivedUniformsScene now serves multiple arena chunks
38
+ * (§3) and each chunk has its own main-heap GPUBuffer. */
36
39
  export interface DerivedUniformsResources {
37
40
  /** `array<vec2<f32>>` — df32 trafo halves (the constituents heap). */
38
41
  readonly constituentsBuf: () => GPUBuffer;
39
- /** `array<f32>` — the main heap (drawHeaders); written by the kernel. */
40
- readonly mainHeapBuf: () => GPUBuffer;
41
42
  }
42
43
 
43
44
  function bglEntries(): GPUBindGroupLayoutEntry[] {
@@ -145,57 +146,83 @@ class UberPipeline {
145
146
  }
146
147
  }
147
148
 
149
+ /** Per-chunk dispatch state: GPU mirror of that chunk's records +
150
+ * a cached bind group keyed on (consts, heap, recs). */
151
+ interface ChunkState {
152
+ readonly gpu: RecordsGpu;
153
+ cachedBg: GPUBindGroup | undefined;
154
+ bgKey: [GPUBuffer | undefined, GPUBuffer | undefined, GPUBuffer | undefined];
155
+ }
156
+
148
157
  export class DerivedUniformsDispatcher {
149
158
  private readonly device: GPUDevice;
150
159
  private readonly resources: DerivedUniformsResources;
151
- private readonly gpu: RecordsGpu;
152
160
  private readonly pipe: UberPipeline;
153
- private cachedBg: GPUBindGroup | undefined;
154
- private bgKey: [GPUBuffer | undefined, GPUBuffer | undefined, GPUBuffer | undefined] = [undefined, undefined, undefined];
161
+ private readonly byChunk = new Map<RecordsBuffer, ChunkState>();
155
162
 
156
163
  constructor(device: GPUDevice, resources: DerivedUniformsResources) {
157
164
  this.device = device;
158
165
  this.resources = resources;
159
- this.gpu = new RecordsGpu(device);
160
166
  this.pipe = new UberPipeline(device);
161
167
  }
162
168
 
163
- /** Open a compute pass and run the uber-kernel. No-ops if there are no records / no rules. */
164
- encode(enc: GPUCommandEncoder, registry: DerivedUniformRegistry, records: RecordsBuffer): boolean {
169
+ /** Run the uber-kernel for ONE chunk's records, binding
170
+ * `mainHeapGetter()` as the heap target. No-op if the chunk's
171
+ * records buffer is empty or the registry has no rules. */
172
+ encodeChunk(
173
+ enc: GPUCommandEncoder,
174
+ registry: DerivedUniformRegistry,
175
+ records: RecordsBuffer,
176
+ mainHeapGetter: () => GPUBuffer,
177
+ ): boolean {
165
178
  if (records.recordCount === 0) return false;
166
179
  const pipeline = this.pipe.pipeline(registry, records.strideWords);
167
180
  if (pipeline === undefined) return false;
168
- this.gpu.sync(records);
181
+ const state = this.stateFor(records);
182
+ state.gpu.sync(records);
169
183
  const pass = enc.beginComputePass({ label: "derivedUniforms.uber" });
170
184
  pass.setPipeline(pipeline);
171
- pass.setBindGroup(0, this.bindGroup());
185
+ pass.setBindGroup(0, this.bindGroup(state, mainHeapGetter()));
172
186
  pass.dispatchWorkgroups(Math.ceil(records.recordCount / 64));
173
187
  pass.end();
174
188
  return true;
175
189
  }
176
190
 
177
191
  dispose(): void {
178
- this.gpu.dispose();
192
+ for (const s of this.byChunk.values()) s.gpu.dispose();
193
+ this.byChunk.clear();
194
+ }
195
+
196
+ private stateFor(records: RecordsBuffer): ChunkState {
197
+ let s = this.byChunk.get(records);
198
+ if (s === undefined) {
199
+ s = {
200
+ gpu: new RecordsGpu(this.device),
201
+ cachedBg: undefined,
202
+ bgKey: [undefined, undefined, undefined],
203
+ };
204
+ this.byChunk.set(records, s);
205
+ }
206
+ return s;
179
207
  }
180
208
 
181
- private bindGroup(): GPUBindGroup {
209
+ private bindGroup(state: ChunkState, heap: GPUBuffer): GPUBindGroup {
182
210
  const consts = this.resources.constituentsBuf();
183
- const heap = this.resources.mainHeapBuf();
184
- const recs = this.gpu.buffer();
185
- if (this.cachedBg !== undefined && this.bgKey[0] === consts && this.bgKey[1] === heap && this.bgKey[2] === recs) {
186
- return this.cachedBg;
211
+ const recs = state.gpu.buffer();
212
+ if (state.cachedBg !== undefined && state.bgKey[0] === consts && state.bgKey[1] === heap && state.bgKey[2] === recs) {
213
+ return state.cachedBg;
187
214
  }
188
- this.cachedBg = this.device.createBindGroup({
215
+ state.cachedBg = this.device.createBindGroup({
189
216
  label: "derivedUniforms.uber",
190
217
  layout: this.pipe.bgl,
191
218
  entries: [
192
219
  { binding: 0, resource: { buffer: consts } },
193
220
  { binding: 1, resource: { buffer: heap } },
194
221
  { binding: 2, resource: { buffer: recs } },
195
- { binding: 3, resource: { buffer: this.gpu.countBuf } },
222
+ { binding: 3, resource: { buffer: state.gpu.countBuf } },
196
223
  ],
197
224
  });
198
- this.bgKey = [consts, heap, recs];
199
- return this.cachedBg;
225
+ state.bgKey = [consts, heap, recs];
226
+ return state.cachedBg;
200
227
  }
201
228
  }
@@ -49,27 +49,79 @@ function intrinsicExpr(name: string, type: Type, args: readonly Expr[]): Expr {
49
49
  };
50
50
  }
51
51
 
52
- /** A traced expression in a `derivedUniform` builder. */
52
+ /** Convertible-to-DerivedExpr operand. `aardvark-operators` rewrites
53
+ * e.g. `det < 0` to `det.lt(0)` — so every binary method accepts a
54
+ * raw number and lifts it to a Const matching the receiver's
55
+ * numeric type (f32 / u32 / i32). */
56
+ export type DerivedOperand = DerivedExpr | number;
57
+
58
+ /** True iff `t` is a numeric scalar IR type (Float / Int). */
59
+ function isNumericScalar(t: Type): boolean {
60
+ return t.kind === "Float" || t.kind === "Int";
61
+ }
62
+ /** Element type for scalar arithmetic: scalar types pass through,
63
+ * vector/matrix types unwrap to their element type, anything else
64
+ * defaults to f32 (the natural numeric on the GPU side). */
65
+ function scalarLiftType(t: Type): Type {
66
+ if (isNumericScalar(t)) return t;
67
+ if (t.kind === "Vector" || t.kind === "Matrix") return t.element;
68
+ return Tf32;
69
+ }
70
+ function liftConst(value: number, target: Type): Expr {
71
+ if (target.kind === "Int") {
72
+ if (target.signed) {
73
+ return { kind: "Const", type: target, value: { kind: "Int", value: value | 0, signed: true } } as Expr;
74
+ }
75
+ return { kind: "Const", type: target, value: { kind: "Int", value: value >>> 0, signed: false } } as Expr;
76
+ }
77
+ // Default: f32 const.
78
+ return { kind: "Const", type: Tf32, value: { kind: "Float", value } } as Expr;
79
+ }
80
+ /** Coerce `DerivedOperand` to a DerivedExpr, lifting raw numbers to a
81
+ * Const expression whose type matches the receiver's scalar type. */
82
+ function lift(other: DerivedOperand, receiverType: Type): DerivedExpr {
83
+ if (other instanceof DerivedExpr) return other;
84
+ return new DerivedExpr(liftConst(other, scalarLiftType(receiverType)));
85
+ }
86
+
87
+ /**
88
+ * A traced expression in a `derivedUniform` / `derivedMode` builder.
89
+ *
90
+ * Plays nicely with `boperators` (https://npmjs.com/package/boperators):
91
+ * the named-static operator methods (`static ["+"]`, `static ["*"]`,
92
+ * `static ["<"]`, …) make `a + b`, `a * b`, `a < b`, etc. on
93
+ * DerivedExprs rewrite to those statics at build time. Without the
94
+ * plugin, the same logic is reachable via the instance methods
95
+ * (`.add`, `.mul`, `.lt`, …).
96
+ *
97
+ * Ternary `a < b ? x : y` isn't rewritten — call `.select` on the
98
+ * resulting bool DerivedExpr:
99
+ *
100
+ * (det < 0).select(flipped, declared)
101
+ */
53
102
  export class DerivedExpr {
54
103
  constructor(readonly ir: Expr) {}
55
104
  get type(): Type { return this.ir.type; }
56
105
 
57
106
  /** Matrix product `this · other` (also matrix·vector / vector·matrix by operand types). */
58
- mul(other: DerivedExpr): DerivedExpr {
59
- const a = this.ir.type, b = other.ir.type;
107
+ mul(other: DerivedOperand): DerivedExpr {
108
+ const o = lift(other, this.ir.type);
109
+ const a = this.ir.type, b = o.ir.type;
60
110
  if (a.kind === "Matrix" && b.kind === "Matrix") {
61
- return new DerivedExpr({ kind: "MulMatMat", lhs: this.ir, rhs: other.ir, type: { kind: "Matrix", element: a.element, rows: a.rows, cols: b.cols } });
111
+ return new DerivedExpr({ kind: "MulMatMat", lhs: this.ir, rhs: o.ir, type: { kind: "Matrix", element: a.element, rows: a.rows, cols: b.cols } });
62
112
  }
63
113
  if (a.kind === "Matrix" && b.kind === "Vector") {
64
- return new DerivedExpr({ kind: "MulMatVec", lhs: this.ir, rhs: other.ir, type: { kind: "Vector", element: b.element, dim: a.rows } });
114
+ return new DerivedExpr({ kind: "MulMatVec", lhs: this.ir, rhs: o.ir, type: { kind: "Vector", element: b.element, dim: a.rows } });
65
115
  }
66
116
  if (a.kind === "Vector" && b.kind === "Matrix") {
67
- return new DerivedExpr({ kind: "MulVecMat", lhs: this.ir, rhs: other.ir, type: { kind: "Vector", element: a.element, dim: b.cols } });
117
+ return new DerivedExpr({ kind: "MulVecMat", lhs: this.ir, rhs: o.ir, type: { kind: "Vector", element: a.element, dim: b.cols } });
68
118
  }
69
- return new DerivedExpr({ kind: "Mul", lhs: this.ir, rhs: other.ir, type: a.kind === "Float" ? b : a });
119
+ return new DerivedExpr({ kind: "Mul", lhs: this.ir, rhs: o.ir, type: a.kind === "Float" || a.kind === "Int" ? b : a });
70
120
  }
71
- add(other: DerivedExpr): DerivedExpr { return new DerivedExpr({ kind: "Add", lhs: this.ir, rhs: other.ir, type: this.ir.type }); }
72
- sub(other: DerivedExpr): DerivedExpr { return new DerivedExpr({ kind: "Sub", lhs: this.ir, rhs: other.ir, type: this.ir.type }); }
121
+ add(other: DerivedOperand): DerivedExpr { const o = lift(other, this.ir.type); return new DerivedExpr({ kind: "Add", lhs: this.ir, rhs: o.ir, type: this.ir.type }); }
122
+ sub(other: DerivedOperand): DerivedExpr { const o = lift(other, this.ir.type); return new DerivedExpr({ kind: "Sub", lhs: this.ir, rhs: o.ir, type: this.ir.type }); }
123
+ div(other: DerivedOperand): DerivedExpr { const o = lift(other, this.ir.type); return new DerivedExpr({ kind: "Div", lhs: this.ir, rhs: o.ir, type: this.ir.type }); }
124
+ mod(other: DerivedOperand): DerivedExpr { const o = lift(other, this.ir.type); return new DerivedExpr({ kind: "Mod", lhs: this.ir, rhs: o.ir, type: this.ir.type }); }
73
125
  neg(): DerivedExpr { return new DerivedExpr({ kind: "Neg", value: this.ir, type: this.ir.type }); }
74
126
 
75
127
  /** Inverse (of a constituent trafo this reads its stored backward half; otherwise unsupported in codegen). */
@@ -130,18 +182,160 @@ export class DerivedExpr {
130
182
  log2(): DerivedExpr { return this.u1("log2"); }
131
183
  sign(): DerivedExpr { return this.u1("sign"); }
132
184
  normalize(): DerivedExpr { return this.u1("normalize"); }
133
- min(o: DerivedExpr): DerivedExpr { return this.bin("min", o); }
134
- max(o: DerivedExpr): DerivedExpr { return this.bin("max", o); }
135
- pow(o: DerivedExpr): DerivedExpr { return this.bin("pow", o); }
136
- atan2(o: DerivedExpr): DerivedExpr { return this.bin("atan2", o); }
137
- step(edge: DerivedExpr): DerivedExpr { return new DerivedExpr(intrinsicExpr("step", this.ir.type, [edge.ir, this.ir])); }
138
- clamp(lo: DerivedExpr, hi: DerivedExpr): DerivedExpr { return new DerivedExpr(intrinsicExpr("clamp", this.ir.type, [this.ir, lo.ir, hi.ir])); }
139
- mix(o: DerivedExpr, t: DerivedExpr): DerivedExpr { return new DerivedExpr(intrinsicExpr("mix", this.ir.type, [this.ir, o.ir, t.ir])); }
185
+ min(o: DerivedOperand): DerivedExpr { return this.bin("min", lift(o, this.ir.type)); }
186
+ max(o: DerivedOperand): DerivedExpr { return this.bin("max", lift(o, this.ir.type)); }
187
+ pow(o: DerivedOperand): DerivedExpr { return this.bin("pow", lift(o, this.ir.type)); }
188
+ atan2(o: DerivedOperand): DerivedExpr { return this.bin("atan2", lift(o, this.ir.type)); }
189
+ step(edge: DerivedOperand): DerivedExpr { const e = lift(edge, this.ir.type); return new DerivedExpr(intrinsicExpr("step", this.ir.type, [e.ir, this.ir])); }
190
+ clamp(lo: DerivedOperand, hi: DerivedOperand): DerivedExpr { const l = lift(lo, this.ir.type); const h = lift(hi, this.ir.type); return new DerivedExpr(intrinsicExpr("clamp", this.ir.type, [this.ir, l.ir, h.ir])); }
191
+ mix(o: DerivedOperand, t: DerivedOperand): DerivedExpr { const oo = lift(o, this.ir.type); const tt = lift(t, this.ir.type); return new DerivedExpr(intrinsicExpr("mix", this.ir.type, [this.ir, oo.ir, tt.ir])); }
140
192
  dot(o: DerivedExpr): DerivedExpr { return new DerivedExpr(intrinsicExpr("dot", elemOf(this.ir.type), [this.ir, o.ir])); }
141
193
  cross(o: DerivedExpr): DerivedExpr { return new DerivedExpr(intrinsicExpr("cross", this.ir.type, [this.ir, o.ir])); }
142
194
  length(): DerivedExpr { return new DerivedExpr(intrinsicExpr("length", elemOf(this.ir.type), [this.ir])); }
143
195
  distance(o: DerivedExpr): DerivedExpr { return new DerivedExpr(intrinsicExpr("distance", elemOf(this.ir.type), [this.ir, o.ir])); }
144
196
  reflect(n: DerivedExpr): DerivedExpr { return new DerivedExpr(intrinsicExpr("reflect", this.ir.type, [this.ir, n.ir])); }
197
+
198
+ // ── Comparisons (produce a bool DerivedExpr). Receivers + arg can be
199
+ // raw numbers — `aardvark-operators` lifts `a < 2` to `a.lt(2)`,
200
+ // and we lift `2` to a const matching the receiver's scalar type. ──
201
+ private cmp(kind: "Eq" | "Neq" | "Lt" | "Le" | "Gt" | "Ge", other: DerivedOperand): DerivedExpr {
202
+ const o = lift(other, this.ir.type);
203
+ const ir = { kind, lhs: this.ir, rhs: o.ir, type: { kind: "Bool" } as Type } as Expr;
204
+ return new DerivedExpr(ir);
205
+ }
206
+ eq(o: DerivedOperand): DerivedExpr { return this.cmp("Eq", o); }
207
+ ne(o: DerivedOperand): DerivedExpr { return this.cmp("Neq", o); }
208
+ lt(o: DerivedOperand): DerivedExpr { return this.cmp("Lt", o); }
209
+ le(o: DerivedOperand): DerivedExpr { return this.cmp("Le", o); }
210
+ gt(o: DerivedOperand): DerivedExpr { return this.cmp("Gt", o); }
211
+ ge(o: DerivedOperand): DerivedExpr { return this.cmp("Ge", o); }
212
+
213
+ /**
214
+ * Ternary select. The RECEIVER must be a bool — typically the
215
+ * result of a comparison — and the args are the then / else
216
+ * branches:
217
+ *
218
+ * (det < 0).select(flipped, declared) // det<0 ? flipped : declared
219
+ *
220
+ * The result type follows `then` (which must be widening-
221
+ * compatible with `else`). Raw numbers are lifted using `then`'s
222
+ * type when `then` is a DerivedExpr, else f32.
223
+ */
224
+ select(thenE: DerivedOperand, elseE: DerivedOperand): DerivedExpr {
225
+ if (this.ir.type.kind !== "Bool") {
226
+ throw new Error("DerivedExpr.select(then, else): receiver must be a bool (typically a comparison)");
227
+ }
228
+ const t = thenE instanceof DerivedExpr ? thenE : lift(thenE, elseE instanceof DerivedExpr ? elseE.ir.type : Tf32);
229
+ const e = elseE instanceof DerivedExpr ? elseE : lift(elseE, t.ir.type);
230
+ const ir = { kind: "Conditional", cond: this.ir, ifTrue: t.ir, ifFalse: e.ir, type: t.ir.type } as Expr;
231
+ return new DerivedExpr(ir);
232
+ }
233
+
234
+ /** Determinant of a square matrix expression — yields a scalar f32. */
235
+ determinant(): DerivedExpr {
236
+ if (this.ir.type.kind !== "Matrix") {
237
+ throw new Error("DerivedExpr.determinant(): receiver must be a matrix expression");
238
+ }
239
+ return new DerivedExpr({ kind: "Determinant", value: this.ir, type: Tf32 } as Expr);
240
+ }
241
+
242
+ // ── Static literal builders. Mode rules in particular need u32 constants for enum
243
+ // indices (slot lookup). Floats for thresholds (e.g. determinant against zero).
244
+ static f32(value: number): DerivedExpr {
245
+ return new DerivedExpr({ kind: "Const", type: Tf32, value: { kind: "Float", value } } as Expr);
246
+ }
247
+ static u32(value: number): DerivedExpr {
248
+ return new DerivedExpr({ kind: "Const", type: Tu32, value: { kind: "Int", value: value >>> 0, signed: false } } as Expr);
249
+ }
250
+ static i32(value: number): DerivedExpr {
251
+ return new DerivedExpr({ kind: "Const", type: Ti32, value: { kind: "Int", value: value | 0, signed: true } } as Expr);
252
+ }
253
+
254
+ // ─── boperators static dispatch ──────────────────────────────────────
255
+ // `a OP b` (where at least one operand is a DerivedExpr) is rewritten
256
+ // by the boperators plugin to `DerivedExpr["OP"](a, b)`. Either-order
257
+ // overloads (`number OP expr`, `expr OP number`) are handled by lifting
258
+ // the raw number to a Const matching the other operand's scalar type.
259
+
260
+ static "+"(a: DerivedExpr, b: DerivedExpr): DerivedExpr;
261
+ static "+"(a: DerivedExpr, b: number): DerivedExpr;
262
+ static "+"(a: number, b: DerivedExpr): DerivedExpr;
263
+ static "+"(a: DerivedOperand, b: DerivedOperand): DerivedExpr {
264
+ const x = a instanceof DerivedExpr ? a : lift(a, (b as DerivedExpr).ir.type);
265
+ return x.add(b);
266
+ }
267
+ static "-"(a: DerivedExpr, b: DerivedExpr): DerivedExpr;
268
+ static "-"(a: DerivedExpr, b: number): DerivedExpr;
269
+ static "-"(a: number, b: DerivedExpr): DerivedExpr;
270
+ static "-"(a: DerivedExpr): DerivedExpr; // unary minus
271
+ static "-"(a: DerivedOperand, b?: DerivedOperand): DerivedExpr {
272
+ if (b === undefined) return (a as DerivedExpr).neg();
273
+ const x = a instanceof DerivedExpr ? a : lift(a, (b as DerivedExpr).ir.type);
274
+ return x.sub(b);
275
+ }
276
+ static "*"(a: DerivedExpr, b: DerivedExpr): DerivedExpr;
277
+ static "*"(a: DerivedExpr, b: number): DerivedExpr;
278
+ static "*"(a: number, b: DerivedExpr): DerivedExpr;
279
+ static "*"(a: DerivedOperand, b: DerivedOperand): DerivedExpr {
280
+ const x = a instanceof DerivedExpr ? a : lift(a, (b as DerivedExpr).ir.type);
281
+ return x.mul(b);
282
+ }
283
+ static "/"(a: DerivedExpr, b: DerivedExpr): DerivedExpr;
284
+ static "/"(a: DerivedExpr, b: number): DerivedExpr;
285
+ static "/"(a: number, b: DerivedExpr): DerivedExpr;
286
+ static "/"(a: DerivedOperand, b: DerivedOperand): DerivedExpr {
287
+ const x = a instanceof DerivedExpr ? a : lift(a, (b as DerivedExpr).ir.type);
288
+ return x.div(b);
289
+ }
290
+ static "%"(a: DerivedExpr, b: DerivedExpr): DerivedExpr;
291
+ static "%"(a: DerivedExpr, b: number): DerivedExpr;
292
+ static "%"(a: number, b: DerivedExpr): DerivedExpr;
293
+ static "%"(a: DerivedOperand, b: DerivedOperand): DerivedExpr {
294
+ const x = a instanceof DerivedExpr ? a : lift(a, (b as DerivedExpr).ir.type);
295
+ return x.mod(b);
296
+ }
297
+ static "<"(a: DerivedExpr, b: DerivedExpr): DerivedExpr;
298
+ static "<"(a: DerivedExpr, b: number): DerivedExpr;
299
+ static "<"(a: number, b: DerivedExpr): DerivedExpr;
300
+ static "<"(a: DerivedOperand, b: DerivedOperand): DerivedExpr {
301
+ const x = a instanceof DerivedExpr ? a : lift(a, (b as DerivedExpr).ir.type);
302
+ return x.lt(b);
303
+ }
304
+ static "<="(a: DerivedExpr, b: DerivedExpr): DerivedExpr;
305
+ static "<="(a: DerivedExpr, b: number): DerivedExpr;
306
+ static "<="(a: number, b: DerivedExpr): DerivedExpr;
307
+ static "<="(a: DerivedOperand, b: DerivedOperand): DerivedExpr {
308
+ const x = a instanceof DerivedExpr ? a : lift(a, (b as DerivedExpr).ir.type);
309
+ return x.le(b);
310
+ }
311
+ static ">"(a: DerivedExpr, b: DerivedExpr): DerivedExpr;
312
+ static ">"(a: DerivedExpr, b: number): DerivedExpr;
313
+ static ">"(a: number, b: DerivedExpr): DerivedExpr;
314
+ static ">"(a: DerivedOperand, b: DerivedOperand): DerivedExpr {
315
+ const x = a instanceof DerivedExpr ? a : lift(a, (b as DerivedExpr).ir.type);
316
+ return x.gt(b);
317
+ }
318
+ static ">="(a: DerivedExpr, b: DerivedExpr): DerivedExpr;
319
+ static ">="(a: DerivedExpr, b: number): DerivedExpr;
320
+ static ">="(a: number, b: DerivedExpr): DerivedExpr;
321
+ static ">="(a: DerivedOperand, b: DerivedOperand): DerivedExpr {
322
+ const x = a instanceof DerivedExpr ? a : lift(a, (b as DerivedExpr).ir.type);
323
+ return x.ge(b);
324
+ }
325
+ static "=="(a: DerivedExpr, b: DerivedExpr): DerivedExpr;
326
+ static "=="(a: DerivedExpr, b: number): DerivedExpr;
327
+ static "=="(a: number, b: DerivedExpr): DerivedExpr;
328
+ static "=="(a: DerivedOperand, b: DerivedOperand): DerivedExpr {
329
+ const x = a instanceof DerivedExpr ? a : lift(a, (b as DerivedExpr).ir.type);
330
+ return x.eq(b);
331
+ }
332
+ static "!="(a: DerivedExpr, b: DerivedExpr): DerivedExpr;
333
+ static "!="(a: DerivedExpr, b: number): DerivedExpr;
334
+ static "!="(a: number, b: DerivedExpr): DerivedExpr;
335
+ static "!="(a: DerivedOperand, b: DerivedOperand): DerivedExpr {
336
+ const x = a instanceof DerivedExpr ? a : lift(a, (b as DerivedExpr).ir.type);
337
+ return x.ne(b);
338
+ }
145
339
  }
146
340
 
147
341
  /** The `u` passed to a `derivedUniform` builder. `u.<Name>` is a uniform leaf — a mat4