@aardworx/wombat.rendering 0.8.1 → 0.9.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.
- package/dist/core/command.d.ts +7 -9
- package/dist/core/command.d.ts.map +1 -1
- package/dist/core/renderTask.d.ts +64 -1
- package/dist/core/renderTask.d.ts.map +1 -1
- package/dist/core/renderTask.js +3 -1
- package/dist/core/renderTask.js.map +1 -1
- package/dist/resources/preparedRenderObject.js +3 -3
- package/dist/resources/preparedRenderObject.js.map +1 -1
- package/dist/resources/uniformBuffer.d.ts.map +1 -1
- package/dist/resources/uniformBuffer.js +17 -0
- package/dist/resources/uniformBuffer.js.map +1 -1
- package/dist/runtime/derivedUniforms/dispatch.d.ts +105 -0
- package/dist/runtime/derivedUniforms/dispatch.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/dispatch.js +355 -0
- package/dist/runtime/derivedUniforms/dispatch.js.map +1 -0
- package/dist/runtime/derivedUniforms/index.d.ts +6 -0
- package/dist/runtime/derivedUniforms/index.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/index.js +26 -0
- package/dist/runtime/derivedUniforms/index.js.map +1 -0
- package/dist/runtime/derivedUniforms/recipes.d.ts +42 -0
- package/dist/runtime/derivedUniforms/recipes.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/recipes.js +134 -0
- package/dist/runtime/derivedUniforms/recipes.js.map +1 -0
- package/dist/runtime/derivedUniforms/sceneIntegration.d.ts +56 -0
- package/dist/runtime/derivedUniforms/sceneIntegration.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/sceneIntegration.js +171 -0
- package/dist/runtime/derivedUniforms/sceneIntegration.js.map +1 -0
- package/dist/runtime/derivedUniforms/slotId.d.ts +15 -0
- package/dist/runtime/derivedUniforms/slotId.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/slotId.js +30 -0
- package/dist/runtime/derivedUniforms/slotId.js.map +1 -0
- package/dist/runtime/derivedUniforms/slots.d.ts +49 -0
- package/dist/runtime/derivedUniforms/slots.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/slots.js +123 -0
- package/dist/runtime/derivedUniforms/slots.js.map +1 -0
- package/dist/runtime/derivedUniforms/uberKernel.wgsl.d.ts +13 -0
- package/dist/runtime/derivedUniforms/uberKernel.wgsl.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/uberKernel.wgsl.js +218 -0
- package/dist/runtime/derivedUniforms/uberKernel.wgsl.js.map +1 -0
- package/dist/runtime/flattenTree.d.ts +5 -0
- package/dist/runtime/flattenTree.d.ts.map +1 -0
- package/dist/runtime/flattenTree.js +45 -0
- package/dist/runtime/flattenTree.js.map +1 -0
- package/dist/runtime/heapAdapter.d.ts +16 -0
- package/dist/runtime/heapAdapter.d.ts.map +1 -0
- package/dist/runtime/heapAdapter.js +263 -0
- package/dist/runtime/heapAdapter.js.map +1 -0
- package/dist/runtime/heapEffect.d.ts +235 -0
- package/dist/runtime/heapEffect.d.ts.map +1 -0
- package/dist/runtime/heapEffect.js +529 -0
- package/dist/runtime/heapEffect.js.map +1 -0
- package/dist/runtime/heapEffectIR.d.ts +34 -0
- package/dist/runtime/heapEffectIR.d.ts.map +1 -0
- package/dist/runtime/heapEffectIR.js +1161 -0
- package/dist/runtime/heapEffectIR.js.map +1 -0
- package/dist/runtime/heapEligibility.d.ts +9 -0
- package/dist/runtime/heapEligibility.d.ts.map +1 -0
- package/dist/runtime/heapEligibility.js +195 -0
- package/dist/runtime/heapEligibility.js.map +1 -0
- package/dist/runtime/heapScene.d.ts +240 -31
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +3334 -265
- package/dist/runtime/heapScene.js.map +1 -1
- package/dist/runtime/heapShaderFamily.d.ts +132 -0
- package/dist/runtime/heapShaderFamily.d.ts.map +1 -0
- package/dist/runtime/heapShaderFamily.js +866 -0
- package/dist/runtime/heapShaderFamily.js.map +1 -0
- package/dist/runtime/hybridScene.d.ts +133 -0
- package/dist/runtime/hybridScene.d.ts.map +1 -0
- package/dist/runtime/hybridScene.js +159 -0
- package/dist/runtime/hybridScene.js.map +1 -0
- package/dist/runtime/index.d.ts +8 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +9 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/renderTask.d.ts +21 -4
- package/dist/runtime/renderTask.d.ts.map +1 -1
- package/dist/runtime/renderTask.js +127 -32
- package/dist/runtime/renderTask.js.map +1 -1
- package/dist/runtime/runtime.d.ts +33 -3
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +12 -3
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/scenePass.d.ts +28 -3
- package/dist/runtime/scenePass.d.ts.map +1 -1
- package/dist/runtime/scenePass.js +37 -4
- package/dist/runtime/scenePass.js.map +1 -1
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.d.ts +35 -0
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.d.ts.map +1 -0
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.js +247 -0
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.js.map +1 -0
- package/dist/runtime/textureAtlas/atlasPool.d.ts +240 -0
- package/dist/runtime/textureAtlas/atlasPool.d.ts.map +1 -0
- package/dist/runtime/textureAtlas/atlasPool.js +798 -0
- package/dist/runtime/textureAtlas/atlasPool.js.map +1 -0
- package/dist/runtime/textureAtlas/bvhTree2d.d.ts +33 -0
- package/dist/runtime/textureAtlas/bvhTree2d.d.ts.map +1 -0
- package/dist/runtime/textureAtlas/bvhTree2d.js +501 -0
- package/dist/runtime/textureAtlas/bvhTree2d.js.map +1 -0
- package/dist/runtime/textureAtlas/packer.d.ts +50 -0
- package/dist/runtime/textureAtlas/packer.d.ts.map +1 -0
- package/dist/runtime/textureAtlas/packer.js +390 -0
- package/dist/runtime/textureAtlas/packer.js.map +1 -0
- package/dist/window/loop.d.ts +14 -0
- package/dist/window/loop.d.ts.map +1 -1
- package/dist/window/loop.js +18 -5
- package/dist/window/loop.js.map +1 -1
- package/package.json +6 -5
- package/src/core/command.ts +9 -9
- package/src/core/renderTask.ts +55 -2
- package/src/resources/preparedRenderObject.ts +3 -3
- package/src/resources/uniformBuffer.ts +15 -0
- package/src/runtime/derivedUniforms/dispatch.ts +407 -0
- package/src/runtime/derivedUniforms/index.ts +52 -0
- package/src/runtime/derivedUniforms/recipes.ts +159 -0
- package/src/runtime/derivedUniforms/sceneIntegration.ts +239 -0
- package/src/runtime/derivedUniforms/slotId.ts +35 -0
- package/src/runtime/derivedUniforms/slots.ts +155 -0
- package/src/runtime/derivedUniforms/uberKernel.wgsl.ts +220 -0
- package/src/runtime/flattenTree.ts +46 -0
- package/src/runtime/heapAdapter.ts +314 -0
- package/src/runtime/heapEffect.ts +728 -0
- package/src/runtime/heapEffectIR.ts +1211 -0
- package/src/runtime/heapEligibility.ts +189 -0
- package/src/runtime/heapScene.ts +3981 -349
- package/src/runtime/heapShaderFamily.ts +1102 -0
- package/src/runtime/hybridScene.ts +310 -0
- package/src/runtime/index.ts +45 -1
- package/src/runtime/renderTask.ts +167 -32
- package/src/runtime/runtime.ts +37 -4
- package/src/runtime/scenePass.ts +39 -4
- package/src/runtime/textureAtlas/atlasMipGutterKernel.ts +291 -0
- package/src/runtime/textureAtlas/atlasPool.ts +991 -0
- package/src/runtime/textureAtlas/bvhTree2d.ts +530 -0
- package/src/runtime/textureAtlas/packer.ts +429 -0
- package/src/window/loop.ts +32 -6
- package/dist/runtime/heapShader.d.ts +0 -12
- package/dist/runtime/heapShader.d.ts.map +0 -1
- package/dist/runtime/heapShader.js +0 -157
- package/dist/runtime/heapShader.js.map +0 -1
- package/src/runtime/heapShader.ts +0 -185
package/src/core/renderTask.ts
CHANGED
|
@@ -1,16 +1,69 @@
|
|
|
1
1
|
// RenderTask — the compiled, runnable form of an alist<Command>.
|
|
2
2
|
// The runtime returns one of these from `compile`; the user calls
|
|
3
|
-
// `run(token)` to encode + submit a frame
|
|
3
|
+
// `run(framebuffer, token)` to encode + submit a frame against the
|
|
4
|
+
// supplied framebuffer instance. The framebuffer's signature must
|
|
5
|
+
// match the one the task was compiled against.
|
|
4
6
|
|
|
5
7
|
import type { AdaptiveToken } from "@aardworx/wombat.adaptive";
|
|
8
|
+
import type { FramebufferSignature } from "./framebufferSignature.js";
|
|
9
|
+
import type { IFramebuffer } from "./framebuffer.js";
|
|
6
10
|
|
|
7
11
|
export interface IRenderTask {
|
|
12
|
+
/**
|
|
13
|
+
* The signature this task was compiled against. All pipelines
|
|
14
|
+
* baked in were specialised against this signature; framebuffers
|
|
15
|
+
* passed to `run`/`encode` must match.
|
|
16
|
+
*/
|
|
17
|
+
readonly signature: FramebufferSignature;
|
|
18
|
+
|
|
8
19
|
/**
|
|
9
20
|
* Encode all commands into the GPU queue using the current
|
|
10
21
|
* adaptive state read via `token`. Returns once submission has
|
|
11
22
|
* been queued (does not wait for GPU completion).
|
|
12
23
|
*/
|
|
13
|
-
run(token: AdaptiveToken): void;
|
|
24
|
+
run(framebuffer: IFramebuffer, token: AdaptiveToken): void;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Total GPU buckets the heap path emits across this task's
|
|
28
|
+
* compiled scenes. With §6 family-merge in v1 this collapses to
|
|
29
|
+
* one per pipelineState per Render command. Useful for status
|
|
30
|
+
* text / dev overlays.
|
|
31
|
+
*/
|
|
32
|
+
heapBucketCount(): number;
|
|
33
|
+
|
|
34
|
+
/** Per-frame breakdown of §7 derived-uniforms work (CPU, last frame). */
|
|
35
|
+
heapDerivedTimings(): {
|
|
36
|
+
pullMs: number; uploadMs: number; encodeMs: number; records: number;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Diagnostic: walks heap drawHeaders, drawTables, prefix sums,
|
|
40
|
+
* attribute alloc headers; checks refs/indices/instance counts and
|
|
41
|
+
* data finiteness. Useful for hunting OOB-corruption bugs. */
|
|
42
|
+
validateHeap(): Promise<{
|
|
43
|
+
arenaBytes: number; issues: string[];
|
|
44
|
+
okRefs: number; badRefs: number;
|
|
45
|
+
drawTableRows: number; drawTableErrs: number; prefixSumErrs: number;
|
|
46
|
+
attrAllocsChecked: number; attrAllocsBad: number;
|
|
47
|
+
tilesChecked: number; tilesBad: number;
|
|
48
|
+
vidChecks: number; vidBad: number;
|
|
49
|
+
indicesHash: string;
|
|
50
|
+
}>;
|
|
51
|
+
/** CPU draw simulator. Walks N sampled emits, runs the binary-search
|
|
52
|
+
* + index/instance recovery, and verifies every storage read lands
|
|
53
|
+
* inside the bound buffer. */
|
|
54
|
+
simulateDraws(samples?: number): Promise<{
|
|
55
|
+
emitsChecked: number; oob: number; issues: string[];
|
|
56
|
+
}>;
|
|
57
|
+
/** GPU-side binary-search probe — runs the same search the render VS
|
|
58
|
+
* does and compares its result with the CPU's. */
|
|
59
|
+
probeBinarySearch(samples?: number): Promise<{
|
|
60
|
+
emitsChecked: number; gpuMismatches: number; issues: string[];
|
|
61
|
+
}>;
|
|
62
|
+
/** Triangle-level coherence — verifies each triangle's 3 emits land
|
|
63
|
+
* in the same slot (no cross-RO triangles). */
|
|
64
|
+
checkTriangleCoherence(samples?: number): Promise<{
|
|
65
|
+
trianglesChecked: number; crossSlot: number; issues: string[];
|
|
66
|
+
}>;
|
|
14
67
|
|
|
15
68
|
/** Tear down resources owned by the task. Idempotent. */
|
|
16
69
|
dispose(): void;
|
|
@@ -548,9 +548,9 @@ export function prepareRenderObject(
|
|
|
548
548
|
// tight one-element buffer and asks every vertex/instance to read
|
|
549
549
|
// it. WebGPU reads the same element when arrayStride is 0
|
|
550
550
|
// (regardless of stepMode), so override the default tight stride
|
|
551
|
-
// here. Without this the second vertex would read past the end
|
|
552
|
-
// the 16-byte buffer and pick up zeros — colorAval would tint
|
|
553
|
-
// only the first vertex of
|
|
551
|
+
// here. Without this the second vertex would read past the end
|
|
552
|
+
// of the 16-byte buffer and pick up zeros — colorAval would tint
|
|
553
|
+
// only the first vertex of each draw.
|
|
554
554
|
const isBroadcast = view.singleValue !== undefined;
|
|
555
555
|
const stride = isBroadcast ? 0 : BufferView.strideOf(view);
|
|
556
556
|
const offset = BufferView.offsetOf(view);
|
|
@@ -135,6 +135,16 @@ export function writeField(
|
|
|
135
135
|
view.f32.set(value, offset >> 2);
|
|
136
136
|
return;
|
|
137
137
|
}
|
|
138
|
+
if (value instanceof Float64Array) {
|
|
139
|
+
// Downcast to f32 in-place. Aardvark's V/M types in the F#-on-JS
|
|
140
|
+
// path use Float64Array internally (V4d / M44d / Trafo3d) but
|
|
141
|
+
// GPU uniforms are always f32 — converting here saves every
|
|
142
|
+
// F# user from sprinkling V4f / V3f conversions across their
|
|
143
|
+
// Sg.Uniform values.
|
|
144
|
+
const dst = view.f32.subarray(offset >> 2, (offset >> 2) + value.length);
|
|
145
|
+
for (let i = 0; i < value.length; i++) dst[i] = value[i]!;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
138
148
|
if (value instanceof Int32Array) {
|
|
139
149
|
view.i32.set(value, offset >> 2);
|
|
140
150
|
return;
|
|
@@ -146,6 +156,11 @@ export function writeField(
|
|
|
146
156
|
if (value !== null && typeof value === "object" && "_data" in (value as object)) {
|
|
147
157
|
const data = (value as { _data: unknown })._data;
|
|
148
158
|
if (data instanceof Float32Array) { view.f32.set(data, offset >> 2); return; }
|
|
159
|
+
if (data instanceof Float64Array) {
|
|
160
|
+
const dst = view.f32.subarray(offset >> 2, (offset >> 2) + data.length);
|
|
161
|
+
for (let i = 0; i < data.length; i++) dst[i] = data[i]!;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
149
164
|
if (data instanceof Int32Array) { view.i32.set(data, offset >> 2); return; }
|
|
150
165
|
if (data instanceof Uint32Array) { view.u32.set(data, offset >> 2); return; }
|
|
151
166
|
}
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
// §7 single-dispatch leaf dispatcher.
|
|
2
|
+
//
|
|
3
|
+
// Each bucket holds a static-ish records buffer (only edited on RO
|
|
4
|
+
// add/remove). Per frame the caller:
|
|
5
|
+
// 1. Drains dirty constituent avals and uploads the changed byte
|
|
6
|
+
// range to the shared constituents buffer.
|
|
7
|
+
// 2. Builds a dirty bitmask (one bit per constituent slot) and
|
|
8
|
+
// uploads it to the shared dirty buffer.
|
|
9
|
+
// 3. For each bucket, calls `dispatcher.encode(enc)` — one
|
|
10
|
+
// compute pass with one dispatch per bucket. The kernel
|
|
11
|
+
// early-returns on records whose inputs are all clean.
|
|
12
|
+
//
|
|
13
|
+
// No worklist building, no atomicAdd compaction, no per-frame record
|
|
14
|
+
// encoding. The records buffer changes only when ROs come and go.
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
ConstituentSlots, DF32_MAT4_BYTES,
|
|
18
|
+
type DerivationRecord, type SlotIndex,
|
|
19
|
+
} from "./slots.js";
|
|
20
|
+
import {
|
|
21
|
+
RECORD_STRIDE_BYTES, RECORD_STRIDE_U32, RECORD_FIELD,
|
|
22
|
+
DERIVED_UBER_KERNEL_WGSL,
|
|
23
|
+
} from "./uberKernel.wgsl.js";
|
|
24
|
+
|
|
25
|
+
/** Live getters for the GPU resources the dispatcher needs. The scene
|
|
26
|
+
* may replace these buffers in-place when capacity grows; the
|
|
27
|
+
* dispatcher rebinds each frame. */
|
|
28
|
+
export interface DerivedUniformsResources {
|
|
29
|
+
readonly constituentsBuf: () => GPUBuffer;
|
|
30
|
+
readonly mainHeapBuf: () => GPUBuffer;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Per-bucket records buffer. Records are added/removed when ROs
|
|
34
|
+
* enter/leave the bucket. Uses swap-remove to keep the buffer dense.
|
|
35
|
+
* CPU mirror is kept up-to-date; the dirty range is uploaded once
|
|
36
|
+
* per `flush()`. */
|
|
37
|
+
export class RecordsBuffer {
|
|
38
|
+
private readonly device: GPUDevice;
|
|
39
|
+
private cpu: Uint32Array;
|
|
40
|
+
private cap = 0; // record slots (capacity, in records)
|
|
41
|
+
private count = 0; // live records
|
|
42
|
+
private nextId = 0;
|
|
43
|
+
private readonly slotById = new Map<number, number>();
|
|
44
|
+
private readonly idBySlot: number[] = [];
|
|
45
|
+
|
|
46
|
+
/** Dirty CPU-mirror byte range that must be uploaded next flush. */
|
|
47
|
+
private dirtyMinByte = Infinity;
|
|
48
|
+
private dirtyMaxByte = -1;
|
|
49
|
+
|
|
50
|
+
private gpu: GPUBuffer | undefined;
|
|
51
|
+
private gpuCap = 0; // record slots in `gpu`
|
|
52
|
+
|
|
53
|
+
constructor(device: GPUDevice, initialCapacity = 64) {
|
|
54
|
+
this.device = device;
|
|
55
|
+
this.cap = initialCapacity;
|
|
56
|
+
this.cpu = new Uint32Array(this.cap * RECORD_STRIDE_U32);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Add a record. Returns a stable handle for later removal. */
|
|
60
|
+
add(rec: DerivationRecord): number {
|
|
61
|
+
if (this.count === this.cap) this.growCpu();
|
|
62
|
+
const slot = this.count++;
|
|
63
|
+
const id = this.nextId++;
|
|
64
|
+
this.slotById.set(id, slot);
|
|
65
|
+
this.idBySlot[slot] = id;
|
|
66
|
+
this.writeAt(slot, rec);
|
|
67
|
+
return id;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Swap-remove. The last record moves into the freed slot. */
|
|
71
|
+
remove(id: number): void {
|
|
72
|
+
const slot = this.slotById.get(id);
|
|
73
|
+
if (slot === undefined) {
|
|
74
|
+
throw new Error("RecordsBuffer.remove: unknown id");
|
|
75
|
+
}
|
|
76
|
+
const last = this.count - 1;
|
|
77
|
+
if (slot !== last) {
|
|
78
|
+
// Move last → slot.
|
|
79
|
+
const srcOff = last * RECORD_STRIDE_U32;
|
|
80
|
+
const dstOff = slot * RECORD_STRIDE_U32;
|
|
81
|
+
for (let i = 0; i < RECORD_STRIDE_U32; i++) {
|
|
82
|
+
this.cpu[dstOff + i] = this.cpu[srcOff + i]!;
|
|
83
|
+
}
|
|
84
|
+
const movedId = this.idBySlot[last]!;
|
|
85
|
+
this.slotById.set(movedId, slot);
|
|
86
|
+
this.idBySlot[slot] = movedId;
|
|
87
|
+
this.markDirty(slot, slot + 1);
|
|
88
|
+
}
|
|
89
|
+
this.slotById.delete(id);
|
|
90
|
+
this.idBySlot.length = last;
|
|
91
|
+
this.count = last;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Live record count. */
|
|
95
|
+
get recordCount(): number { return this.count; }
|
|
96
|
+
|
|
97
|
+
/** Ensure GPU buffer matches cap and upload dirty CPU range. Call
|
|
98
|
+
* before `encode`. */
|
|
99
|
+
flush(): void {
|
|
100
|
+
if (this.gpu === undefined || this.gpuCap < this.cap) {
|
|
101
|
+
if (this.gpu !== undefined) this.gpu.destroy();
|
|
102
|
+
this.gpu = this.device.createBuffer({
|
|
103
|
+
label: "derivedUniforms.records",
|
|
104
|
+
size: this.cap * RECORD_STRIDE_BYTES,
|
|
105
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
106
|
+
});
|
|
107
|
+
this.gpuCap = this.cap;
|
|
108
|
+
// Whole CPU mirror needs re-upload.
|
|
109
|
+
this.dirtyMinByte = 0;
|
|
110
|
+
this.dirtyMaxByte = this.count * RECORD_STRIDE_BYTES;
|
|
111
|
+
}
|
|
112
|
+
if (this.dirtyMaxByte > this.dirtyMinByte) {
|
|
113
|
+
const start = this.dirtyMinByte;
|
|
114
|
+
const end = Math.min(this.dirtyMaxByte, this.count * RECORD_STRIDE_BYTES);
|
|
115
|
+
if (end > start) {
|
|
116
|
+
this.device.queue.writeBuffer(
|
|
117
|
+
this.gpu!, start,
|
|
118
|
+
this.cpu.buffer,
|
|
119
|
+
this.cpu.byteOffset + start,
|
|
120
|
+
end - start,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
this.dirtyMinByte = Infinity;
|
|
124
|
+
this.dirtyMaxByte = -1;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
buffer(): GPUBuffer {
|
|
129
|
+
if (this.gpu === undefined) {
|
|
130
|
+
// Empty bucket — make a tiny stub so binding always has something.
|
|
131
|
+
this.gpu = this.device.createBuffer({
|
|
132
|
+
label: "derivedUniforms.records.stub",
|
|
133
|
+
size: RECORD_STRIDE_BYTES,
|
|
134
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
135
|
+
});
|
|
136
|
+
this.gpuCap = 1;
|
|
137
|
+
}
|
|
138
|
+
return this.gpu;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
dispose(): void {
|
|
142
|
+
if (this.gpu !== undefined) {
|
|
143
|
+
this.gpu.destroy();
|
|
144
|
+
this.gpu = undefined;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── Internals ─────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
private writeAt(slot: number, rec: DerivationRecord): void {
|
|
151
|
+
const off = slot * RECORD_STRIDE_U32;
|
|
152
|
+
this.cpu[off + RECORD_FIELD.recipe_id] = rec.recipe;
|
|
153
|
+
this.cpu[off + RECORD_FIELD.in0] = rec.in0;
|
|
154
|
+
this.cpu[off + RECORD_FIELD.in1] = rec.in1;
|
|
155
|
+
this.cpu[off + RECORD_FIELD.in2] = rec.in2;
|
|
156
|
+
this.cpu[off + RECORD_FIELD.out_byte] = rec.outByte;
|
|
157
|
+
this.markDirty(slot, slot + 1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private markDirty(startSlot: number, endSlot: number): void {
|
|
161
|
+
const startByte = startSlot * RECORD_STRIDE_BYTES;
|
|
162
|
+
const endByte = endSlot * RECORD_STRIDE_BYTES;
|
|
163
|
+
if (startByte < this.dirtyMinByte) this.dirtyMinByte = startByte;
|
|
164
|
+
if (endByte > this.dirtyMaxByte) this.dirtyMaxByte = endByte;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private growCpu(): void {
|
|
168
|
+
const newCap = this.cap * 2;
|
|
169
|
+
const grown = new Uint32Array(newCap * RECORD_STRIDE_U32);
|
|
170
|
+
grown.set(this.cpu);
|
|
171
|
+
this.cpu = grown;
|
|
172
|
+
this.cap = newCap;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** One compute pipeline shared across buckets — created once per scene. */
|
|
177
|
+
export class DerivedUniformsPipeline {
|
|
178
|
+
readonly pipeline: GPUComputePipeline;
|
|
179
|
+
readonly bgl: GPUBindGroupLayout;
|
|
180
|
+
|
|
181
|
+
constructor(device: GPUDevice) {
|
|
182
|
+
const module = device.createShaderModule({ code: DERIVED_UBER_KERNEL_WGSL });
|
|
183
|
+
this.bgl = device.createBindGroupLayout({
|
|
184
|
+
entries: [
|
|
185
|
+
{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
|
|
186
|
+
{ binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } },
|
|
187
|
+
{ binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
|
|
188
|
+
{ binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } },
|
|
189
|
+
],
|
|
190
|
+
});
|
|
191
|
+
this.pipeline = device.createComputePipeline({
|
|
192
|
+
layout: device.createPipelineLayout({ bindGroupLayouts: [this.bgl] }),
|
|
193
|
+
compute: { module, entryPoint: "main" },
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Per-bucket dispatcher. One bind group cached, rebuilt only when a
|
|
199
|
+
* bound buffer reference changes (constituents or main heap grew, or
|
|
200
|
+
* records buffer was re-created). */
|
|
201
|
+
export class DerivedUniformsDispatcher {
|
|
202
|
+
private readonly device: GPUDevice;
|
|
203
|
+
private readonly pipe: DerivedUniformsPipeline;
|
|
204
|
+
private readonly resources: DerivedUniformsResources;
|
|
205
|
+
readonly records: RecordsBuffer;
|
|
206
|
+
|
|
207
|
+
private cachedBg: GPUBindGroup | undefined;
|
|
208
|
+
private bgKeyConsts: GPUBuffer | undefined;
|
|
209
|
+
private bgKeyHeap: GPUBuffer | undefined;
|
|
210
|
+
private bgKeyRecs: GPUBuffer | undefined;
|
|
211
|
+
|
|
212
|
+
/** Tiny uniform holding the live record count — gates threads past
|
|
213
|
+
* the live range from reading stale swap-removed records. */
|
|
214
|
+
private readonly countBuf: GPUBuffer;
|
|
215
|
+
private readonly countCpu = new Uint32Array(1);
|
|
216
|
+
private lastCountUploaded = -1;
|
|
217
|
+
|
|
218
|
+
constructor(
|
|
219
|
+
device: GPUDevice,
|
|
220
|
+
pipe: DerivedUniformsPipeline,
|
|
221
|
+
resources: DerivedUniformsResources,
|
|
222
|
+
initialRecordCapacity = 64,
|
|
223
|
+
) {
|
|
224
|
+
this.device = device;
|
|
225
|
+
this.pipe = pipe;
|
|
226
|
+
this.resources = resources;
|
|
227
|
+
this.records = new RecordsBuffer(device, initialRecordCapacity);
|
|
228
|
+
this.countBuf = device.createBuffer({
|
|
229
|
+
label: "derivedUniforms.recordCount",
|
|
230
|
+
size: 16, // uniform min size
|
|
231
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Dispatch into an open compute pass. Caller is responsible for
|
|
236
|
+
* beginning/ending the pass and setting the pipeline once across
|
|
237
|
+
* all dispatchers (the pipeline is shared). */
|
|
238
|
+
dispatchInto(pass: GPUComputePassEncoder): boolean {
|
|
239
|
+
const count = this.records.recordCount;
|
|
240
|
+
if (count === 0) return false;
|
|
241
|
+
this.records.flush();
|
|
242
|
+
if (count !== this.lastCountUploaded) {
|
|
243
|
+
this.countCpu[0] = count;
|
|
244
|
+
this.device.queue.writeBuffer(
|
|
245
|
+
this.countBuf, 0, this.countCpu.buffer, this.countCpu.byteOffset, 4,
|
|
246
|
+
);
|
|
247
|
+
this.lastCountUploaded = count;
|
|
248
|
+
}
|
|
249
|
+
pass.setBindGroup(0, this.bindGroup());
|
|
250
|
+
pass.dispatchWorkgroups(Math.ceil(count / 64));
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Convenience: open/close a pass and dispatch. Use only when this
|
|
255
|
+
* is the sole dispatcher in the pass; otherwise use `dispatchInto`
|
|
256
|
+
* inside a caller-managed pass. */
|
|
257
|
+
encode(enc: GPUCommandEncoder): boolean {
|
|
258
|
+
if (this.records.recordCount === 0) return false;
|
|
259
|
+
const pass = enc.beginComputePass({ label: "derivedUniforms.uber" });
|
|
260
|
+
pass.setPipeline(this.pipe.pipeline);
|
|
261
|
+
const ran = this.dispatchInto(pass);
|
|
262
|
+
pass.end();
|
|
263
|
+
return ran;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
dispose(): void {
|
|
267
|
+
this.records.dispose();
|
|
268
|
+
this.countBuf.destroy();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private bindGroup(): GPUBindGroup {
|
|
272
|
+
const consts = this.resources.constituentsBuf();
|
|
273
|
+
const heap = this.resources.mainHeapBuf();
|
|
274
|
+
const recs = this.records.buffer();
|
|
275
|
+
if (
|
|
276
|
+
this.cachedBg !== undefined &&
|
|
277
|
+
this.bgKeyConsts === consts && this.bgKeyHeap === heap &&
|
|
278
|
+
this.bgKeyRecs === recs
|
|
279
|
+
) {
|
|
280
|
+
return this.cachedBg;
|
|
281
|
+
}
|
|
282
|
+
this.cachedBg = this.device.createBindGroup({
|
|
283
|
+
layout: this.pipe.bgl,
|
|
284
|
+
entries: [
|
|
285
|
+
{ binding: 0, resource: { buffer: consts } },
|
|
286
|
+
{ binding: 1, resource: { buffer: heap } },
|
|
287
|
+
{ binding: 2, resource: { buffer: recs } },
|
|
288
|
+
{ binding: 3, resource: { buffer: this.countBuf } },
|
|
289
|
+
],
|
|
290
|
+
});
|
|
291
|
+
this.bgKeyConsts = consts; this.bgKeyHeap = heap;
|
|
292
|
+
this.bgKeyRecs = recs;
|
|
293
|
+
return this.cachedBg;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Scene-side helper: O(changed) maintenance of a GPU dirty bitmask.
|
|
298
|
+
*
|
|
299
|
+
* Steady state (clean frame): zero work. The CPU mirror reflects the
|
|
300
|
+
* GPU buffer, so we only ever touch bits that actually flip.
|
|
301
|
+
*
|
|
302
|
+
* Each `upload(dirty)` call:
|
|
303
|
+
* 1. Clears the bits set last frame (O(prev_dirty)).
|
|
304
|
+
* 2. Sets the bits dirty this frame (O(dirty)).
|
|
305
|
+
* 3. Writes only the affected u32 range to the GPU (one writeBuffer).
|
|
306
|
+
* If both sets are empty, returns immediately. */
|
|
307
|
+
export class DirtyMaskUploader {
|
|
308
|
+
private readonly device: GPUDevice;
|
|
309
|
+
private cpu: Uint32Array;
|
|
310
|
+
private gpu: GPUBuffer | undefined;
|
|
311
|
+
private gpuCapU32 = 0;
|
|
312
|
+
/** Bits set on the previous upload — must be cleared next time. */
|
|
313
|
+
private prevDirty: SlotIndex[] = [];
|
|
314
|
+
|
|
315
|
+
constructor(device: GPUDevice, initialU32 = 8) {
|
|
316
|
+
this.device = device;
|
|
317
|
+
this.cpu = new Uint32Array(Math.max(1, initialU32));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Resize CPU + GPU mirrors so they cover `slotCount` bits. */
|
|
321
|
+
ensureCapacity(slotCount: number): void {
|
|
322
|
+
const need = Math.max(1, Math.ceil(slotCount / 32));
|
|
323
|
+
if (need > this.cpu.length) {
|
|
324
|
+
let cap = Math.max(this.cpu.length, 1);
|
|
325
|
+
while (cap < need) cap *= 2;
|
|
326
|
+
const grown = new Uint32Array(cap);
|
|
327
|
+
grown.set(this.cpu);
|
|
328
|
+
this.cpu = grown;
|
|
329
|
+
}
|
|
330
|
+
if (this.gpu === undefined || this.gpuCapU32 < need) {
|
|
331
|
+
if (this.gpu !== undefined) this.gpu.destroy();
|
|
332
|
+
const cap = this.cpu.length;
|
|
333
|
+
this.gpu = this.device.createBuffer({
|
|
334
|
+
label: "derivedUniforms.dirtyMask",
|
|
335
|
+
size: cap * 4,
|
|
336
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
337
|
+
});
|
|
338
|
+
this.gpuCapU32 = cap;
|
|
339
|
+
// GPU buffer is fresh-zero; CPU mirror should match. Wipe any
|
|
340
|
+
// prevDirty (those bits live in the destroyed buffer).
|
|
341
|
+
this.prevDirty.length = 0;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Apply the new dirty set. O(prev_dirty + dirty), no O(slotCount). */
|
|
346
|
+
upload(dirty: ReadonlySet<SlotIndex>): void {
|
|
347
|
+
let minU32 = Infinity;
|
|
348
|
+
let maxU32 = -Infinity;
|
|
349
|
+
for (const s of this.prevDirty) {
|
|
350
|
+
const i = s >>> 5; const b = s & 31;
|
|
351
|
+
this.cpu[i] = (this.cpu[i]! & ~(1 << b)) >>> 0;
|
|
352
|
+
if (i < minU32) minU32 = i;
|
|
353
|
+
if (i > maxU32) maxU32 = i;
|
|
354
|
+
}
|
|
355
|
+
this.prevDirty.length = 0;
|
|
356
|
+
for (const s of dirty) {
|
|
357
|
+
const i = s >>> 5; const b = s & 31;
|
|
358
|
+
this.cpu[i] = (this.cpu[i]! | (1 << b)) >>> 0;
|
|
359
|
+
if (i < minU32) minU32 = i;
|
|
360
|
+
if (i > maxU32) maxU32 = i;
|
|
361
|
+
this.prevDirty.push(s);
|
|
362
|
+
}
|
|
363
|
+
if (maxU32 < 0 || maxU32 === -Infinity) return;
|
|
364
|
+
const startByte = minU32 * 4;
|
|
365
|
+
const endByte = (maxU32 + 1) * 4;
|
|
366
|
+
this.device.queue.writeBuffer(
|
|
367
|
+
this.gpu!, startByte,
|
|
368
|
+
this.cpu.buffer, this.cpu.byteOffset + startByte,
|
|
369
|
+
endByte - startByte,
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
buffer(): GPUBuffer {
|
|
374
|
+
if (this.gpu === undefined) this.ensureCapacity(1);
|
|
375
|
+
return this.gpu!;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
dispose(): void {
|
|
379
|
+
if (this.gpu !== undefined) {
|
|
380
|
+
this.gpu.destroy();
|
|
381
|
+
this.gpu = undefined;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** Upload the dirty constituent value range to the constituents GPU
|
|
387
|
+
* buffer. Helper used by the scene each frame after draining. */
|
|
388
|
+
export function uploadConstituentsRange(
|
|
389
|
+
device: GPUDevice,
|
|
390
|
+
buf: GPUBuffer,
|
|
391
|
+
mirror: Float32Array,
|
|
392
|
+
dirty: ReadonlySet<SlotIndex>,
|
|
393
|
+
): void {
|
|
394
|
+
let minSlot = Infinity, maxSlot = -1;
|
|
395
|
+
for (const s of dirty) {
|
|
396
|
+
if (s < minSlot) minSlot = s;
|
|
397
|
+
if (s > maxSlot) maxSlot = s;
|
|
398
|
+
}
|
|
399
|
+
if (maxSlot < 0) return;
|
|
400
|
+
const startByte = minSlot * DF32_MAT4_BYTES;
|
|
401
|
+
const endByte = (maxSlot + 1) * DF32_MAT4_BYTES;
|
|
402
|
+
device.queue.writeBuffer(
|
|
403
|
+
buf, startByte,
|
|
404
|
+
mirror.buffer, mirror.byteOffset + startByte,
|
|
405
|
+
endByte - startByte,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// §7 derived-uniforms module — public surface.
|
|
2
|
+
//
|
|
3
|
+
// Caller responsibilities (typically heapScene.ts):
|
|
4
|
+
//
|
|
5
|
+
// 1. Construct one DerivedUniformsScene per heap scene, one
|
|
6
|
+
// DerivedUniformsBucket per render bucket. The scene owns the
|
|
7
|
+
// shared constituents storage buffer + dirty bitmask + pipeline.
|
|
8
|
+
// 2. In the parent scene-object's `inputChanged(t, o)` router, call
|
|
9
|
+
// `scene.routeInputChanged(o)`. Returns true iff o matched.
|
|
10
|
+
// 3. In each RO's add path: `registerRoDerivations(scene, bucket, req)`
|
|
11
|
+
// allocates constituent slots and pushes records into the bucket's
|
|
12
|
+
// records buffer. In remove path: `deregisterRoDerivations(...)`.
|
|
13
|
+
// 4. In the per-frame compute prep, inside the parent sceneObj's
|
|
14
|
+
// `evaluateAlways(token, ...)` scope:
|
|
15
|
+
// const dirty = scene.constituents.pullDirty(token);
|
|
16
|
+
// scene.uploadDirty(dirty);
|
|
17
|
+
// for each bucket: bucket.dispatcher.encode(enc);
|
|
18
|
+
// All steps are O(changed); on clean frames they no-op.
|
|
19
|
+
//
|
|
20
|
+
// Identifying which uniform names are §7-owned: DERIVED_UNIFORM_NAMES.
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
RecipeId,
|
|
24
|
+
ALL_RECIPES, DERIVED_UNIFORM_NAMES,
|
|
25
|
+
recipeIdByName, recipeName, recipeInputs, recipeInputCount, recipeOutput,
|
|
26
|
+
type ConstituentRef, type RecipeOutput,
|
|
27
|
+
} from "./recipes.js";
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
ConstituentSlots, DF32_MAT4_BYTES,
|
|
31
|
+
type SlotIndex, type PairedSlots, type DerivationRecord, type SubscribeFn,
|
|
32
|
+
} from "./slots.js";
|
|
33
|
+
|
|
34
|
+
export {
|
|
35
|
+
DerivedUniformsDispatcher, DerivedUniformsPipeline,
|
|
36
|
+
RecordsBuffer, uploadConstituentsRange,
|
|
37
|
+
type DerivedUniformsResources,
|
|
38
|
+
} from "./dispatch.js";
|
|
39
|
+
|
|
40
|
+
export {
|
|
41
|
+
DerivedUniformsScene,
|
|
42
|
+
registerRoDerivations,
|
|
43
|
+
deregisterRoDerivations,
|
|
44
|
+
isDerivedUniformName,
|
|
45
|
+
type RoTrafoInputs,
|
|
46
|
+
type RoDerivedRequest,
|
|
47
|
+
type RoRegistration,
|
|
48
|
+
} from "./sceneIntegration.js";
|
|
49
|
+
|
|
50
|
+
export {
|
|
51
|
+
DERIVED_UBER_KERNEL_WGSL,
|
|
52
|
+
} from "./uberKernel.wgsl.js";
|