@aardworx/wombat.rendering 0.9.11 → 0.9.13
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 +76 -0
- package/dist/runtime/derivedUniforms/marker.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/marker.js +156 -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 +78 -26
- 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 +169 -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 +77 -28
- 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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// §7 v2 — derived-uniform rule: a content-hashed pure IR fragment.
|
|
2
|
+
//
|
|
3
|
+
// A DerivedRule is a closure-shaped value: an output WGSL type plus an IR
|
|
4
|
+
// expression ("ir") that computes it from `ReadInput("Uniform", name)` leaves.
|
|
5
|
+
// The names are resolved per-RenderObject at registration time (constituent
|
|
6
|
+
// slot / host uniform). See docs/derived-uniforms-extensible.md.
|
|
7
|
+
//
|
|
8
|
+
// This is the pre-plugin surface: rules are hand-built from IR exprs. The
|
|
9
|
+
// inline `derivedUniform(u => …)` marker (lowered by the wombat-shader-vite
|
|
10
|
+
// plugin) lands later and produces exactly this shape.
|
|
11
|
+
|
|
12
|
+
import type { Expr, Type } from "@aardworx/wombat.shader/ir";
|
|
13
|
+
import { hashValue, stableStringify } from "@aardworx/wombat.shader/ir";
|
|
14
|
+
|
|
15
|
+
/** A rule's IR fragment: the result expression, over `ReadInput("Uniform", …)` leaves. */
|
|
16
|
+
export type IRFragment = Expr;
|
|
17
|
+
|
|
18
|
+
export interface DerivedRule<T = unknown> {
|
|
19
|
+
/** Brand — lets `isDerivedRule` recognise a rule among uniform binding values. */
|
|
20
|
+
readonly __derivedRule: true;
|
|
21
|
+
/** Output WGSL type (`=== ir.type`). Drives the storer codegen and the drawHeader byte budget. */
|
|
22
|
+
readonly outputType: Type;
|
|
23
|
+
/** Pure IR computing the output; every leaf input is a `ReadInput("Uniform", name)`. */
|
|
24
|
+
readonly ir: IRFragment;
|
|
25
|
+
/** Stable structural content hash of `ir` — the registry dedup key. */
|
|
26
|
+
readonly hash: string;
|
|
27
|
+
/** Phantom — carries the TS result type through `derivedUniform<T>`. Never read. */
|
|
28
|
+
readonly __t?: T;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** True iff `x` is a derived-uniform rule (vs. an `aval` / constant value). */
|
|
32
|
+
export function isDerivedRule(x: unknown): x is DerivedRule {
|
|
33
|
+
return typeof x === "object" && x !== null && (x as { __derivedRule?: unknown }).__derivedRule === true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** True iff two IR `Type`s are structurally identical. */
|
|
37
|
+
export function sameType(a: Type, b: Type): boolean {
|
|
38
|
+
return a === b || stableStringify(a) === stableStringify(b);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Stable content hash of an IR expression. */
|
|
42
|
+
export function hashIR(ir: Expr): string {
|
|
43
|
+
return hashValue(ir);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Build a `DerivedRule` from a hand-constructed IR expression. */
|
|
47
|
+
export function ruleFromIR<T = unknown>(ir: Expr, outputType: Type = ir.type): DerivedRule<T> {
|
|
48
|
+
if (!sameType(outputType, ir.type)) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`derived rule: declared outputType ${stableStringify(outputType)} != ir type ${stableStringify(ir.type)}`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return { __derivedRule: true, outputType, ir, hash: hashIR(ir) };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A `ReadInput("Uniform", name)` leaf of the given type — the only leaf kind a rule may use. */
|
|
57
|
+
export function uniformRef(name: string, type: Type): Expr {
|
|
58
|
+
return { kind: "ReadInput", scope: "Uniform", name, type };
|
|
59
|
+
}
|
|
@@ -1,84 +1,61 @@
|
|
|
1
|
-
// §7
|
|
1
|
+
// §7 v2 — heap-scene integration.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// One DerivedUniformsScene per heap scene: owns the rule registry, the (single,
|
|
4
|
+
// scene-wide) records buffer, the constituent-slots heap (df32 trafo halves), the
|
|
5
|
+
// constituents GPU buffer, and the dispatcher. Per RO: flatten each rule against the
|
|
6
|
+
// RO's rule set, register it, resolve every input leaf to a tagged slot handle
|
|
7
|
+
// (constituent fwd/bwd / host drawHeader byte), and push one record.
|
|
6
8
|
//
|
|
7
|
-
// Per frame, the heap scene's `
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
// O(changed) bitmask flips.
|
|
16
|
-
// 3. Per bucket: `bucket.dispatcher.encode(enc)`. The kernel
|
|
17
|
-
// early-returns on records whose inputs are all clean.
|
|
18
|
-
//
|
|
19
|
-
// On clean frames pullDirty/uploadDirty are no-ops. The records
|
|
20
|
-
// buffer is static. CPU steady-state cost: zero.
|
|
21
|
-
|
|
22
|
-
import type { aval, IAdaptiveObject } from "@aardworx/wombat.adaptive";
|
|
9
|
+
// Per frame, inside the heap scene's `evaluateAlways(token, …)` scope:
|
|
10
|
+
// const dirty = scene.pullDirty(token); // drains changed trafo avals (re-subscribes)
|
|
11
|
+
// scene.uploadDirty(dirty); // O(changed) value uploads
|
|
12
|
+
// scene.encode(enc); // one compute dispatch
|
|
13
|
+
// Clean frames: zero CPU work, records buffer static.
|
|
14
|
+
// See docs/derived-uniforms-extensible.md.
|
|
15
|
+
|
|
16
|
+
import type { aval, IAdaptiveObject, AdaptiveToken } from "@aardworx/wombat.adaptive";
|
|
23
17
|
import type { Trafo3d } from "@aardworx/wombat.base";
|
|
24
18
|
import {
|
|
25
|
-
ConstituentSlots,
|
|
26
|
-
|
|
27
|
-
type DerivationRecord, type PairedSlots, type SlotIndex,
|
|
28
|
-
type SubscribeFn,
|
|
19
|
+
ConstituentSlots, DF32_MAT4_BYTES,
|
|
20
|
+
type PairedSlots, type SlotIndex,
|
|
29
21
|
} from "./slots.js";
|
|
30
|
-
import {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
} from "./
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Scene-wide §7 state. One per heap scene. Owns the single records
|
|
41
|
-
* buffer + dispatcher — all buckets share constituents, dirty mask,
|
|
42
|
-
* AND main heap (arena.attrs.buffer), so per-bucket records would
|
|
43
|
-
* just multiply dispatch overhead for no reason. */
|
|
22
|
+
import { DerivedUniformRegistry } from "./registry.js";
|
|
23
|
+
import { RecordsBuffer, SlotTag, makeHandle } from "./records.js";
|
|
24
|
+
import { DerivedUniformsDispatcher, uploadConstituentsRange } from "./dispatch.js";
|
|
25
|
+
import { flatten, type RuleInput } from "./flatten.js";
|
|
26
|
+
import { ruleFromIR, type DerivedRule } from "./rule.js";
|
|
27
|
+
|
|
28
|
+
export interface DerivedUniformsSceneOptions {
|
|
29
|
+
readonly initialConstituentSlots?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
44
32
|
export class DerivedUniformsScene {
|
|
45
33
|
readonly device: GPUDevice;
|
|
46
34
|
readonly constituents: ConstituentSlots;
|
|
47
|
-
readonly
|
|
48
|
-
|
|
49
|
-
|
|
35
|
+
readonly registry = new DerivedUniformRegistry();
|
|
36
|
+
/** Single scene-wide records buffer — all buckets share constituents AND the main heap, so per-bucket records would just multiply dispatch overhead. */
|
|
37
|
+
readonly records = new RecordsBuffer();
|
|
38
|
+
readonly dispatcher: DerivedUniformsDispatcher;
|
|
39
|
+
/** Constituents storage GPU buffer (`array<vec2<f32>>` — df32 mat4 halves). */
|
|
50
40
|
constituentsBuf: GPUBuffer;
|
|
51
|
-
|
|
52
|
-
* `rebindMainHeap` on arena resize. */
|
|
53
|
-
private mainHeapRef: { current: GPUBuffer };
|
|
41
|
+
private readonly mainHeapRef: { current: GPUBuffer };
|
|
54
42
|
/** Bumped each time a shared GPU buffer is replaced. */
|
|
55
43
|
bufferEpoch = 0;
|
|
56
44
|
|
|
57
|
-
constructor(
|
|
58
|
-
device: GPUDevice,
|
|
59
|
-
mainHeapBuf: GPUBuffer,
|
|
60
|
-
opts?: { initialConstituentSlots?: number; initialRecordCapacity?: number },
|
|
61
|
-
) {
|
|
45
|
+
constructor(device: GPUDevice, mainHeapBuf: GPUBuffer, opts?: DerivedUniformsSceneOptions) {
|
|
62
46
|
this.device = device;
|
|
63
47
|
const initial = opts?.initialConstituentSlots ?? 64;
|
|
64
|
-
|
|
65
|
-
this.constituents = new ConstituentSlots(subscribe, initial);
|
|
66
|
-
this.pipeline = new DerivedUniformsPipeline(device);
|
|
48
|
+
this.constituents = new ConstituentSlots(() => {}, initial);
|
|
67
49
|
this.constituentsBuf = device.createBuffer({
|
|
68
50
|
label: "derivedUniforms.constituents",
|
|
69
|
-
size:
|
|
51
|
+
size: initial * DF32_MAT4_BYTES,
|
|
70
52
|
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
71
53
|
});
|
|
72
54
|
this.mainHeapRef = { current: mainHeapBuf };
|
|
73
|
-
this.dispatcher = new DerivedUniformsDispatcher(
|
|
74
|
-
|
|
75
|
-
this.
|
|
76
|
-
|
|
77
|
-
constituentsBuf: () => this.constituentsBuf,
|
|
78
|
-
mainHeapBuf: () => this.mainHeapRef.current,
|
|
79
|
-
},
|
|
80
|
-
opts?.initialRecordCapacity ?? 64,
|
|
81
|
-
);
|
|
55
|
+
this.dispatcher = new DerivedUniformsDispatcher(device, {
|
|
56
|
+
constituentsBuf: () => this.constituentsBuf,
|
|
57
|
+
mainHeapBuf: () => this.mainHeapRef.current,
|
|
58
|
+
});
|
|
82
59
|
}
|
|
83
60
|
|
|
84
61
|
rebindMainHeap(mainHeapBuf: GPUBuffer): void {
|
|
@@ -92,40 +69,42 @@ export class DerivedUniformsScene {
|
|
|
92
69
|
this.constituentsBuf.destroy();
|
|
93
70
|
this.constituentsBuf = this.device.createBuffer({
|
|
94
71
|
label: "derivedUniforms.constituents",
|
|
95
|
-
size:
|
|
72
|
+
size: cap,
|
|
96
73
|
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
97
74
|
});
|
|
98
75
|
this.bufferEpoch++;
|
|
99
|
-
//
|
|
100
|
-
// gone. Re-upload everything we've ever packed (slot 0..slotCount).
|
|
101
|
-
// Without this, slots that were last written before the grow read
|
|
102
|
-
// zero on the GPU — every df_mul on them produces NaN.
|
|
76
|
+
// Fresh buffer is zero-initialised; re-upload everything packed so far.
|
|
103
77
|
const slotCount = this.constituents.slotCount;
|
|
104
78
|
if (slotCount > 0) {
|
|
105
79
|
const mirror = this.constituents.cpuMirror;
|
|
106
80
|
this.device.queue.writeBuffer(
|
|
107
|
-
this.constituentsBuf, 0,
|
|
108
|
-
mirror.buffer, mirror.byteOffset,
|
|
109
|
-
slotCount * DF32_MAT4_BYTES,
|
|
81
|
+
this.constituentsBuf, 0, mirror.buffer, mirror.byteOffset, slotCount * DF32_MAT4_BYTES,
|
|
110
82
|
);
|
|
111
83
|
}
|
|
112
84
|
}
|
|
113
85
|
|
|
114
|
-
/** Routed from heap scene's `inputChanged(t, o)`.
|
|
115
|
-
* `o` matched a known constituent aval. */
|
|
86
|
+
/** Routed from the heap scene's `inputChanged(t, o)`. True iff `o` is a tracked trafo aval. */
|
|
116
87
|
routeInputChanged(o: IAdaptiveObject): boolean {
|
|
117
88
|
if (!this.constituents.has(o)) return false;
|
|
118
89
|
this.constituents.markDirty(o);
|
|
119
90
|
return true;
|
|
120
91
|
}
|
|
121
92
|
|
|
122
|
-
/**
|
|
93
|
+
/** Drain changed trafo avals (re-subscribes via `getValue`). Call inside the scene's evaluate scope. */
|
|
94
|
+
pullDirty(token: AdaptiveToken): ReadonlySet<SlotIndex> {
|
|
95
|
+
return this.constituents.pullDirty(token);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Upload the changed constituent value range. O(changed). */
|
|
123
99
|
uploadDirty(dirty: ReadonlySet<SlotIndex>): void {
|
|
124
100
|
if (dirty.size === 0) return;
|
|
125
101
|
this.ensureConstituentsCapacity(this.constituents.slotCount * DF32_MAT4_BYTES);
|
|
126
|
-
uploadConstituentsRange(
|
|
127
|
-
|
|
128
|
-
|
|
102
|
+
uploadConstituentsRange(this.device, this.constituentsBuf, this.constituents.cpuMirror, dirty);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Run the uber-kernel. No-op if there are no records. */
|
|
106
|
+
encode(enc: GPUCommandEncoder): boolean {
|
|
107
|
+
return this.dispatcher.encode(enc, this.registry, this.records);
|
|
129
108
|
}
|
|
130
109
|
|
|
131
110
|
dispose(): void {
|
|
@@ -136,104 +115,91 @@ export class DerivedUniformsScene {
|
|
|
136
115
|
|
|
137
116
|
// ─── Per-RO registration ──────────────────────────────────────────────
|
|
138
117
|
|
|
139
|
-
export interface RoTrafoInputs {
|
|
140
|
-
readonly modelTrafo?: aval<Trafo3d> | undefined;
|
|
141
|
-
readonly viewTrafo?: aval<Trafo3d> | undefined;
|
|
142
|
-
readonly projTrafo?: aval<Trafo3d> | undefined;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
118
|
export interface RoDerivedRequest {
|
|
146
|
-
|
|
147
|
-
readonly
|
|
148
|
-
|
|
119
|
+
/** Derived rules to install, keyed by the uniform name each produces. */
|
|
120
|
+
readonly rules: ReadonlyMap<string, DerivedRule>;
|
|
121
|
+
/** Trafo avals available on this RO, keyed by the uniform name a rule leaf may reference (e.g. "ModelTrafo"). */
|
|
122
|
+
readonly trafoAvals: ReadonlyMap<string, aval<Trafo3d>>;
|
|
123
|
+
/** drawHeader byte offset (relative to `drawHeaderBaseByte`) of a host-supplied uniform on this RO; undefined if not present. */
|
|
124
|
+
readonly hostUniformOffset: (name: string) => number | undefined;
|
|
125
|
+
/** drawHeader byte offset (relative to `drawHeaderBaseByte`) where each derived output is written; undefined if absent. */
|
|
126
|
+
readonly outputOffset: (name: string) => number | undefined;
|
|
127
|
+
/** Absolute byte offset of this RO's drawHeader within the main heap. */
|
|
149
128
|
readonly drawHeaderBaseByte: number;
|
|
150
129
|
}
|
|
151
130
|
|
|
152
131
|
export interface RoRegistration {
|
|
153
|
-
/**
|
|
154
|
-
readonly
|
|
132
|
+
/** The records-buffer owner key (the RenderObject). */
|
|
133
|
+
readonly owner: object;
|
|
155
134
|
readonly constituentAvals: readonly aval<Trafo3d>[];
|
|
135
|
+
/** Flattened-rule hashes — for `registry.release` on teardown. */
|
|
136
|
+
readonly ruleHashes: readonly string[];
|
|
156
137
|
}
|
|
157
138
|
|
|
158
|
-
/** Register all derived-uniform records for one RO. */
|
|
159
139
|
export function registerRoDerivations(
|
|
160
|
-
scene:
|
|
161
|
-
|
|
140
|
+
scene: DerivedUniformsScene,
|
|
141
|
+
owner: object,
|
|
142
|
+
req: RoDerivedRequest,
|
|
162
143
|
): RoRegistration {
|
|
163
|
-
if (req.
|
|
164
|
-
return { recordIds: [], constituentAvals: [] };
|
|
165
|
-
}
|
|
144
|
+
if (req.rules.size === 0) return { owner, constituentAvals: [], ruleHashes: [] };
|
|
166
145
|
|
|
167
146
|
const acquiredAvals: aval<Trafo3d>[] = [];
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const acquirePair = (key: "Model" | "View" | "Proj"): PairedSlots => {
|
|
173
|
-
let p = pairFor.get(key);
|
|
147
|
+
const ruleHashes: string[] = [];
|
|
148
|
+
const pairByName = new Map<string, PairedSlots>();
|
|
149
|
+
const acquireTrafo = (name: string): PairedSlots => {
|
|
150
|
+
let p = pairByName.get(name);
|
|
174
151
|
if (p !== undefined) return p;
|
|
175
|
-
const av = req.
|
|
152
|
+
const av = req.trafoAvals.get(name);
|
|
176
153
|
if (av === undefined) {
|
|
177
|
-
throw new Error(
|
|
178
|
-
`derivedUniforms: RO requires '${key}' trafo but trafos.${TRAFO_FIELD[key]} is undefined`,
|
|
179
|
-
);
|
|
154
|
+
throw new Error(`derivedUniforms: rule references trafo '${name}' not available on this RO`);
|
|
180
155
|
}
|
|
181
156
|
p = scene.constituents.acquire(av);
|
|
182
|
-
|
|
157
|
+
pairByName.set(name, p);
|
|
183
158
|
acquiredAvals.push(av);
|
|
184
159
|
return p;
|
|
185
160
|
};
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
161
|
+
const resolve = (inp: RuleInput): number => {
|
|
162
|
+
// A matrix-typed leaf bound as an aval is a `Trafo3d` ⇒ a df32 constituent slot
|
|
163
|
+
// (its `Inverse` reads the stored backward half — free).
|
|
164
|
+
if (inp.type.kind === "Matrix" && req.trafoAvals.has(inp.name)) {
|
|
165
|
+
const p = acquireTrafo(inp.name);
|
|
166
|
+
return makeHandle(SlotTag.Constituent, inp.inverse ? p.inv : p.fwd);
|
|
167
|
+
}
|
|
168
|
+
// Anything else is a plain host uniform — whatever value reached this RO (the sg
|
|
169
|
+
// already collapsed any subtree overrides).
|
|
170
|
+
if (inp.inverse) {
|
|
171
|
+
throw new Error(`derivedUniforms: Inverse of a non-constituent input '${inp.name}' is not supported`);
|
|
195
172
|
}
|
|
173
|
+
const off = req.hostUniformOffset(inp.name);
|
|
174
|
+
if (off !== undefined) return makeHandle(SlotTag.HostHeap, req.drawHeaderBaseByte + off);
|
|
175
|
+
throw new Error(
|
|
176
|
+
`derivedUniforms: rule input '${inp.name}' cannot be resolved on this RO ` +
|
|
177
|
+
`(not a Trafo3d binding, and non-trafo host-uniform leaves aren't supported yet)`,
|
|
178
|
+
);
|
|
196
179
|
};
|
|
197
180
|
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
181
|
+
for (const [outName, rule] of req.rules) {
|
|
182
|
+
// `flatten` returns the same Expr object when nothing was substituted (the common case —
|
|
183
|
+
// the standard recipes reference no derived names), so we can reuse `rule` (it already
|
|
184
|
+
// carries a precomputed `.hash`) and skip a redundant IR hash walk per RO.
|
|
185
|
+
const flatIr = flatten(outName, rule.ir, req.rules);
|
|
186
|
+
const flat = flatIr === rule.ir ? rule : ruleFromIR(flatIr, rule.outputType);
|
|
187
|
+
const id = scene.registry.register(flat);
|
|
188
|
+
ruleHashes.push(flat.hash);
|
|
189
|
+
const entry = scene.registry.get(id)!;
|
|
190
|
+
const inSlots = entry.inputs.map(resolve);
|
|
191
|
+
const outOff = req.outputOffset(outName);
|
|
192
|
+
if (outOff === undefined) {
|
|
193
|
+
throw new Error(`derivedUniforms: no drawHeader slot for derived uniform '${outName}' on this RO`);
|
|
203
194
|
}
|
|
204
|
-
|
|
205
|
-
if (off === undefined) {
|
|
206
|
-
throw new Error(
|
|
207
|
-
`derivedUniforms: '${name}' has no byte offset in drawHeader for this RO`,
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
const refs = recipeInputs(id);
|
|
211
|
-
const slots = refs.map(resolveConstituent);
|
|
212
|
-
const inCount = recipeInputCount(id);
|
|
213
|
-
const rec: DerivationRecord = {
|
|
214
|
-
recipe: id as number,
|
|
215
|
-
in0: slots[0]!,
|
|
216
|
-
in1: inCount >= 2 ? slots[1]! : (0 as SlotIndex),
|
|
217
|
-
in2: inCount >= 3 ? slots[2]! : (0 as SlotIndex),
|
|
218
|
-
outByte: req.drawHeaderBaseByte + off,
|
|
219
|
-
};
|
|
220
|
-
recordIds.push(scene.dispatcher.records.add(rec));
|
|
195
|
+
scene.records.add(owner, id, makeHandle(SlotTag.HostHeap, req.drawHeaderBaseByte + outOff), inSlots);
|
|
221
196
|
}
|
|
222
|
-
|
|
223
|
-
return {
|
|
197
|
+
scene.ensureConstituentsCapacity(scene.constituents.slotCount * DF32_MAT4_BYTES);
|
|
198
|
+
return { owner, constituentAvals: acquiredAvals, ruleHashes };
|
|
224
199
|
}
|
|
225
200
|
|
|
226
|
-
export function deregisterRoDerivations(
|
|
227
|
-
scene
|
|
228
|
-
reg
|
|
229
|
-
): void {
|
|
230
|
-
for (const id of reg.recordIds) scene.dispatcher.records.remove(id);
|
|
201
|
+
export function deregisterRoDerivations(scene: DerivedUniformsScene, reg: RoRegistration): void {
|
|
202
|
+
scene.records.removeAllForOwner(reg.owner);
|
|
203
|
+
for (const h of reg.ruleHashes) scene.registry.release(h);
|
|
231
204
|
for (const av of reg.constituentAvals) scene.constituents.release(av);
|
|
232
205
|
}
|
|
233
|
-
|
|
234
|
-
export function isDerivedUniformName(name: string): boolean {
|
|
235
|
-
return DERIVED_UNIFORM_NAMES.has(name);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Re-export RecipeId for callers (e.g. heapScene's drawHeader walk).
|
|
239
|
-
export { RecipeId };
|
package/src/runtime/heapScene.ts
CHANGED
|
@@ -72,8 +72,8 @@ import {
|
|
|
72
72
|
import {
|
|
73
73
|
DerivedUniformsScene,
|
|
74
74
|
registerRoDerivations, deregisterRoDerivations,
|
|
75
|
-
|
|
76
|
-
type RoRegistration,
|
|
75
|
+
isDerivedRule, STANDARD_DERIVED_RULES, STANDARD_TRAFO_LEAVES, inputsOf,
|
|
76
|
+
type RoRegistration, type DerivedRule,
|
|
77
77
|
} from "./derivedUniforms/index.js";
|
|
78
78
|
|
|
79
79
|
// ---------------------------------------------------------------------------
|
|
@@ -1652,19 +1652,49 @@ export function buildHeapScene(
|
|
|
1652
1652
|
// ─── §7 derived-uniforms (opt-in) ─────────────────────────────────
|
|
1653
1653
|
// Constructed lazily after sceneObj so the SubscribeFn can reference
|
|
1654
1654
|
// both. `derivedScene` is undefined when the option is off; nothing
|
|
1655
|
-
// else in this file should run when it is.
|
|
1656
|
-
|
|
1655
|
+
// else in this file should run when it is. On by default — pass
|
|
1656
|
+
// `enableDerivedUniforms: false` to opt out.
|
|
1657
|
+
const enableDerivedUniforms = opts.enableDerivedUniforms !== false;
|
|
1657
1658
|
const derivedScene: DerivedUniformsScene | undefined = enableDerivedUniforms
|
|
1658
1659
|
? new DerivedUniformsScene(device, arena.attrs.buffer, {
|
|
1659
1660
|
// Larger initial capacity reduces grow-during-first-frame churn.
|
|
1660
1661
|
initialConstituentSlots: 4096,
|
|
1661
|
-
initialRecordCapacity: 8192,
|
|
1662
1662
|
})
|
|
1663
1663
|
: undefined;
|
|
1664
1664
|
/** Per-RO §7 registration handles, keyed by global drawId.
|
|
1665
1665
|
* Drained on removeDraw to release slots + records. */
|
|
1666
1666
|
const derivedByDrawId = new Map<number, RoRegistration>();
|
|
1667
1667
|
|
|
1668
|
+
/** The derived-uniform rule for a uniform binding on this RO, if any: an explicit
|
|
1669
|
+
* `derivedUniform(...)` value in `spec.inputs`, or a standard trafo recipe by name
|
|
1670
|
+
* (recipes apply only when their base-trafo leaves are actually bound on this RO —
|
|
1671
|
+
* otherwise the field falls back to its `spec.inputs` value, if any).
|
|
1672
|
+
* (A uniform binding is either a value — an aval/constant — or a rule.) */
|
|
1673
|
+
const ruleForUniform = (spec: HeapDrawSpec, name: string): DerivedRule | undefined => {
|
|
1674
|
+
if (derivedScene === undefined) return undefined;
|
|
1675
|
+
const v = spec.inputs[name];
|
|
1676
|
+
const explicit = isDerivedRule(v);
|
|
1677
|
+
const rule = explicit ? v : STANDARD_DERIVED_RULES.get(name);
|
|
1678
|
+
if (rule === undefined) return undefined;
|
|
1679
|
+
// Each input leaf must be a uniform bound on this RO (a value, not another rule): a
|
|
1680
|
+
// matrix-typed leaf resolves to a df32 constituent slot (it's expected to be an
|
|
1681
|
+
// aval<Trafo3d>); anything else reads the packed value from the drawHeader. If a leaf
|
|
1682
|
+
// isn't bound: for an explicit user rule that's a hard error; for a standard recipe it
|
|
1683
|
+
// just means "this RO doesn't supply the base trafos", so the field falls back to its
|
|
1684
|
+
// packed `spec.inputs` value (or errors later if there is none — same as without §7).
|
|
1685
|
+
for (const inp of inputsOf(rule.ir)) {
|
|
1686
|
+
const specName = STANDARD_TRAFO_LEAVES.get(inp.name) ?? inp.name;
|
|
1687
|
+
const lv = spec.inputs[specName];
|
|
1688
|
+
if (lv === undefined || isDerivedRule(lv)) {
|
|
1689
|
+
if (explicit) {
|
|
1690
|
+
throw new Error(`derivedUniform '${name}': leaf '${inp.name}' is not a uniform bound on this RO`);
|
|
1691
|
+
}
|
|
1692
|
+
return undefined; // standard recipe, base trafos not bound here — use the packed value
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
return rule;
|
|
1696
|
+
};
|
|
1697
|
+
|
|
1668
1698
|
// ─── Atlas bindings ───────────────────────────────────────────────
|
|
1669
1699
|
//
|
|
1670
1700
|
// A bucket whose textures live in the atlas binds N consecutive
|
|
@@ -2625,7 +2655,7 @@ export function buildHeapScene(
|
|
|
2625
2655
|
// pre-pass. We still allocate an arena slot here so the
|
|
2626
2656
|
// drawHeader gets a valid ref written by packBucketHeader; §7
|
|
2627
2657
|
// overwrites the arena data each frame the inputs are dirty.
|
|
2628
|
-
if (
|
|
2658
|
+
if (ruleForUniform(spec, f.name) !== undefined) {
|
|
2629
2659
|
const dummyAval = AVal.constant<M44d>(M44d.zero);
|
|
2630
2660
|
const ref = pool.acquire(
|
|
2631
2661
|
device, arena.attrs, dummyAval, M44d.zero,
|
|
@@ -2762,32 +2792,51 @@ export function buildHeapScene(
|
|
|
2762
2792
|
drawIdToIndexAval[drawId] = indicesAval;
|
|
2763
2793
|
|
|
2764
2794
|
// ─── §7 derived-uniforms registration ────────────────────────────
|
|
2765
|
-
//
|
|
2766
|
-
//
|
|
2767
|
-
//
|
|
2768
|
-
//
|
|
2769
|
-
// i.e. this RO's drawHeader region in the bucket's drawHeap.
|
|
2795
|
+
// A uniform binding on this RO is either a value (aval/constant) or a rule —
|
|
2796
|
+
// collect the rules (explicit `derivedUniform(...)` values + standard trafo
|
|
2797
|
+
// recipes) for fields the effect declares. The kernel writes each result into
|
|
2798
|
+
// this RO's drawHeader region in the bucket's drawHeap (its arena slot).
|
|
2770
2799
|
if (derivedScene !== undefined) {
|
|
2771
|
-
const
|
|
2772
|
-
const
|
|
2800
|
+
const ruleSubset = new Map<string, DerivedRule>();
|
|
2801
|
+
const outOffsetByName = new Map<string, number>();
|
|
2773
2802
|
for (const f of bucket.layout.drawHeaderFields) {
|
|
2774
2803
|
if (f.name === "__layoutId") continue;
|
|
2775
2804
|
if (!effectFields.has(f.name)) continue;
|
|
2776
|
-
|
|
2805
|
+
const rule = ruleForUniform(spec, f.name);
|
|
2806
|
+
if (rule === undefined) continue;
|
|
2777
2807
|
const ref = perDrawRefs.get(f.name);
|
|
2778
2808
|
if (ref === undefined) continue;
|
|
2779
|
-
|
|
2780
|
-
|
|
2809
|
+
ruleSubset.set(f.name, rule);
|
|
2810
|
+
outOffsetByName.set(f.name, ref + ALLOC_HEADER_PAD_TO);
|
|
2781
2811
|
}
|
|
2782
|
-
if (
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2812
|
+
if (ruleSubset.size > 0) {
|
|
2813
|
+
// Trafo avals a rule leaf may reference: the standard `Model`/`View`/`Proj`
|
|
2814
|
+
// leaf names (mapped to their `*Trafo` bindings), plus every aval-valued input
|
|
2815
|
+
// by its own name (so a `derivedUniform(u => u.ViewTrafo.mul(u.ModelTrafo))`
|
|
2816
|
+
// resolves `u.ViewTrafo` to that binding). resolveSource only uses these for
|
|
2817
|
+
// matrix-typed leaves; a non-matrix leaf falls through to the (unsupported-in-v0)
|
|
2818
|
+
// host path.
|
|
2819
|
+
const trafoAvals = new Map<string, aval<Trafo3d>>();
|
|
2820
|
+
for (const [leaf, specName] of STANDARD_TRAFO_LEAVES) {
|
|
2821
|
+
const av = spec.inputs[specName];
|
|
2822
|
+
if (av !== undefined && !isDerivedRule(av)) trafoAvals.set(leaf, av as aval<Trafo3d>);
|
|
2823
|
+
}
|
|
2824
|
+
for (const n of Object.keys(spec.inputs)) {
|
|
2825
|
+
const v = spec.inputs[n];
|
|
2826
|
+
if (v !== undefined && !isDerivedRule(v) && !trafoAvals.has(n)) trafoAvals.set(n, v as aval<Trafo3d>);
|
|
2827
|
+
}
|
|
2828
|
+
const reg = registerRoDerivations(derivedScene, {}, {
|
|
2829
|
+
rules: ruleSubset,
|
|
2830
|
+
trafoAvals,
|
|
2831
|
+
// A non-trafo rule leaf reads the uniform's data straight from this RO's
|
|
2832
|
+
// drawHeader region in the arena (= its pool ref + the alloc-header pad).
|
|
2833
|
+
// Only resolvable if the uniform was actually packed (i.e. it's a drawHeader
|
|
2834
|
+
// field of the effect too) — undefined otherwise, which `resolveSource` rejects.
|
|
2835
|
+
hostUniformOffset: (n) => {
|
|
2836
|
+
const r = perDrawRefs.get(n);
|
|
2837
|
+
return r === undefined ? undefined : r + ALLOC_HEADER_PAD_TO;
|
|
2788
2838
|
},
|
|
2789
|
-
|
|
2790
|
-
byteOffsetByName: arenaByteOffsetByName,
|
|
2839
|
+
outputOffset: (n) => outOffsetByName.get(n),
|
|
2791
2840
|
drawHeaderBaseByte: 0,
|
|
2792
2841
|
});
|
|
2793
2842
|
derivedByDrawId.set(drawId, reg);
|
|
@@ -3126,19 +3175,19 @@ export function buildHeapScene(
|
|
|
3126
3175
|
// adds the evaluating object to the aval's outputs). Without
|
|
3127
3176
|
// this scope, view changes never propagate again — the scene
|
|
3128
3177
|
// freezes after one frame.
|
|
3129
|
-
let dirty: ReturnType<typeof derivedScene.
|
|
3178
|
+
let dirty: ReturnType<typeof derivedScene.pullDirty> | undefined;
|
|
3130
3179
|
sceneObj.evaluateAlways(token, (t) => {
|
|
3131
|
-
dirty = derivedScene.
|
|
3180
|
+
dirty = derivedScene.pullDirty(t);
|
|
3132
3181
|
});
|
|
3133
3182
|
const _t1 = performance.now();
|
|
3134
3183
|
derivedScene.uploadDirty(dirty!);
|
|
3135
3184
|
const _t2 = performance.now();
|
|
3136
|
-
if (dirty!.size > 0) derivedScene.
|
|
3185
|
+
if (dirty!.size > 0) derivedScene.encode(enc);
|
|
3137
3186
|
const _t3 = performance.now();
|
|
3138
3187
|
stats.derivedPullMs = _t1 - _t0;
|
|
3139
3188
|
stats.derivedUploadMs = _t2 - _t1;
|
|
3140
3189
|
stats.derivedEncodeMs = _t3 - _t2;
|
|
3141
|
-
stats.derivedRecords = derivedScene.
|
|
3190
|
+
stats.derivedRecords = derivedScene.records.recordCount;
|
|
3142
3191
|
}
|
|
3143
3192
|
let anyDirty = false;
|
|
3144
3193
|
for (const b of buckets) { if (b.scanDirty) { anyDirty = true; break; } }
|