@aardworx/wombat.rendering 0.9.10 → 0.9.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/runtime/derivedUniforms/codegen.d.ts +8 -0
- package/dist/runtime/derivedUniforms/codegen.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/codegen.js +449 -0
- package/dist/runtime/derivedUniforms/codegen.js.map +1 -0
- package/dist/runtime/derivedUniforms/dispatch.d.ts +14 -93
- package/dist/runtime/derivedUniforms/dispatch.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/dispatch.js +123 -299
- package/dist/runtime/derivedUniforms/dispatch.js.map +1 -1
- package/dist/runtime/derivedUniforms/flatten.d.ts +25 -0
- package/dist/runtime/derivedUniforms/flatten.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/flatten.js +71 -0
- package/dist/runtime/derivedUniforms/flatten.js.map +1 -0
- package/dist/runtime/derivedUniforms/index.d.ts +10 -5
- package/dist/runtime/derivedUniforms/index.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/index.js +24 -19
- package/dist/runtime/derivedUniforms/index.js.map +1 -1
- package/dist/runtime/derivedUniforms/marker.d.ts +32 -0
- package/dist/runtime/derivedUniforms/marker.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/marker.js +84 -0
- package/dist/runtime/derivedUniforms/marker.js.map +1 -0
- package/dist/runtime/derivedUniforms/recipes.d.ts +6 -41
- package/dist/runtime/derivedUniforms/recipes.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/recipes.js +53 -129
- package/dist/runtime/derivedUniforms/recipes.js.map +1 -1
- package/dist/runtime/derivedUniforms/records.d.ts +40 -0
- package/dist/runtime/derivedUniforms/records.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/records.js +151 -0
- package/dist/runtime/derivedUniforms/records.js.map +1 -0
- package/dist/runtime/derivedUniforms/registry.d.ts +33 -0
- package/dist/runtime/derivedUniforms/registry.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/registry.js +58 -0
- package/dist/runtime/derivedUniforms/registry.js.map +1 -0
- package/dist/runtime/derivedUniforms/rule.d.ts +26 -0
- package/dist/runtime/derivedUniforms/rule.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/rule.js +35 -0
- package/dist/runtime/derivedUniforms/rule.js.map +1 -0
- package/dist/runtime/derivedUniforms/sceneIntegration.d.ts +34 -33
- package/dist/runtime/derivedUniforms/sceneIntegration.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/sceneIntegration.js +80 -90
- package/dist/runtime/derivedUniforms/sceneIntegration.js.map +1 -1
- package/dist/runtime/heapAdapter.d.ts.map +1 -1
- package/dist/runtime/heapAdapter.js +9 -6
- package/dist/runtime/heapAdapter.js.map +1 -1
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +72 -27
- package/dist/runtime/heapScene.js.map +1 -1
- package/dist/runtime/hybridScene.js +1 -1
- package/dist/runtime/hybridScene.js.map +1 -1
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/renderTask.js +1 -1
- package/dist/runtime/renderTask.js.map +1 -1
- package/dist/runtime/runtime.js +1 -1
- package/dist/runtime/runtime.js.map +1 -1
- package/package.json +1 -1
- package/src/runtime/derivedUniforms/codegen.ts +477 -0
- package/src/runtime/derivedUniforms/dispatch.ts +135 -341
- package/src/runtime/derivedUniforms/flatten.ts +89 -0
- package/src/runtime/derivedUniforms/index.ts +50 -34
- package/src/runtime/derivedUniforms/marker.ts +91 -0
- package/src/runtime/derivedUniforms/recipes.ts +63 -155
- package/src/runtime/derivedUniforms/records.ts +158 -0
- package/src/runtime/derivedUniforms/registry.ts +77 -0
- package/src/runtime/derivedUniforms/rule.ts +59 -0
- package/src/runtime/derivedUniforms/sceneIntegration.ts +115 -149
- package/src/runtime/heapAdapter.ts +9 -4
- package/src/runtime/heapScene.ts +73 -29
- package/src/runtime/hybridScene.ts +1 -1
- package/src/runtime/index.ts +8 -0
- package/src/runtime/renderTask.ts +1 -1
- package/src/runtime/runtime.ts +1 -1
- package/dist/runtime/derivedUniforms/slotId.d.ts +0 -15
- package/dist/runtime/derivedUniforms/slotId.d.ts.map +0 -1
- package/dist/runtime/derivedUniforms/slotId.js +0 -30
- package/dist/runtime/derivedUniforms/slotId.js.map +0 -1
- package/dist/runtime/derivedUniforms/uberKernel.wgsl.d.ts +0 -13
- package/dist/runtime/derivedUniforms/uberKernel.wgsl.d.ts.map +0 -1
- package/dist/runtime/derivedUniforms/uberKernel.wgsl.js +0 -218
- package/dist/runtime/derivedUniforms/uberKernel.wgsl.js.map +0 -1
- package/src/runtime/derivedUniforms/slotId.ts +0 -35
- package/src/runtime/derivedUniforms/uberKernel.wgsl.ts +0 -220
|
@@ -1,407 +1,201 @@
|
|
|
1
|
-
// §7
|
|
1
|
+
// §7 v2 — uber-kernel dispatcher (GPU plumbing).
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
3
|
+
// Owns: the GPU mirror of the CPU records buffer, a tiny `Count` uniform, and a
|
|
4
|
+
// recompilable compute pipeline. The kernel WGSL is regenerated whenever the rule
|
|
5
|
+
// registry gains a new rule (`registry.version`) or the record stride grows
|
|
6
|
+
// (`records.layoutVersion`). The records GPU buffer is re-uploaded whenever the CPU
|
|
7
|
+
// records buffer mutates (`records.generation`) — records change only on RO add/remove,
|
|
8
|
+
// never per frame. One `dispatchWorkgroups` per frame, no levels, no barriers.
|
|
9
|
+
// See docs/derived-uniforms-extensible.md § "Dispatcher".
|
|
10
|
+
|
|
11
|
+
import type { DerivedUniformRegistry } from "./registry.js";
|
|
12
|
+
import type { RecordsBuffer } from "./records.js";
|
|
13
|
+
import { buildUberKernel } from "./codegen.js";
|
|
14
|
+
import { DF32_MAT4_BYTES, type SlotIndex } from "./slots.js";
|
|
15
|
+
|
|
16
|
+
/** Upload the changed-constituent value range to the constituents GPU buffer. */
|
|
17
|
+
export function uploadConstituentsRange(
|
|
18
|
+
device: GPUDevice,
|
|
19
|
+
buf: GPUBuffer,
|
|
20
|
+
mirror: Float32Array,
|
|
21
|
+
dirty: ReadonlySet<SlotIndex>,
|
|
22
|
+
): void {
|
|
23
|
+
let minSlot = Infinity;
|
|
24
|
+
let maxSlot = -1;
|
|
25
|
+
for (const s of dirty) {
|
|
26
|
+
if (s < minSlot) minSlot = s;
|
|
27
|
+
if (s > maxSlot) maxSlot = s;
|
|
28
|
+
}
|
|
29
|
+
if (maxSlot < 0) return;
|
|
30
|
+
const startByte = minSlot * DF32_MAT4_BYTES;
|
|
31
|
+
const endByte = (maxSlot + 1) * DF32_MAT4_BYTES;
|
|
32
|
+
device.queue.writeBuffer(buf, startByte, mirror.buffer, mirror.byteOffset + startByte, endByte - startByte);
|
|
33
|
+
}
|
|
24
34
|
|
|
25
|
-
/** Live getters for the GPU
|
|
26
|
-
* may replace these buffers in-place when capacity grows; the
|
|
27
|
-
* dispatcher rebinds each frame. */
|
|
35
|
+
/** Live getters for the GPU buffers the kernel binds (scene may grow/replace them). */
|
|
28
36
|
export interface DerivedUniformsResources {
|
|
37
|
+
/** `array<vec2<f32>>` — df32 trafo halves (the constituents heap). */
|
|
29
38
|
readonly constituentsBuf: () => GPUBuffer;
|
|
30
|
-
|
|
39
|
+
/** `array<f32>` — the main heap (drawHeaders); written by the kernel. */
|
|
40
|
+
readonly mainHeapBuf: () => GPUBuffer;
|
|
31
41
|
}
|
|
32
42
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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;
|
|
43
|
+
function bglEntries(): GPUBindGroupLayoutEntry[] {
|
|
44
|
+
return [
|
|
45
|
+
{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
|
|
46
|
+
{ binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } },
|
|
47
|
+
{ binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
|
|
48
|
+
{ binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } },
|
|
49
|
+
];
|
|
50
|
+
}
|
|
49
51
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
/** GPU mirror of a CPU `RecordsBuffer` + the `Count` uniform. */
|
|
53
|
+
class RecordsGpu {
|
|
54
|
+
private readonly device: GPUDevice;
|
|
55
|
+
private buf: GPUBuffer | undefined;
|
|
56
|
+
private bufCapBytes = 0;
|
|
57
|
+
private uploadedGen = -1;
|
|
58
|
+
readonly countBuf: GPUBuffer;
|
|
59
|
+
private readonly countCpu = new Uint32Array(1);
|
|
60
|
+
private uploadedCount = -1;
|
|
52
61
|
|
|
53
|
-
constructor(device: GPUDevice
|
|
62
|
+
constructor(device: GPUDevice) {
|
|
54
63
|
this.device = device;
|
|
55
|
-
this.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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;
|
|
64
|
+
this.countBuf = device.createBuffer({
|
|
65
|
+
label: "derivedUniforms.recordCount",
|
|
66
|
+
size: 16, // uniform min binding size
|
|
67
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
68
|
+
});
|
|
92
69
|
}
|
|
93
70
|
|
|
94
|
-
/**
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
this.
|
|
71
|
+
/** Re-upload the packed record data if it changed since last sync, and refresh `Count`. */
|
|
72
|
+
sync(records: RecordsBuffer): void {
|
|
73
|
+
const data = records.data;
|
|
74
|
+
const needBytes = Math.max(16, data.byteLength);
|
|
75
|
+
if (this.buf === undefined || this.bufCapBytes < needBytes) {
|
|
76
|
+
if (this.buf !== undefined) this.buf.destroy();
|
|
77
|
+
let cap = Math.max(16, this.bufCapBytes || 16);
|
|
78
|
+
while (cap < needBytes) cap *= 2;
|
|
79
|
+
this.buf = this.device.createBuffer({
|
|
103
80
|
label: "derivedUniforms.records",
|
|
104
|
-
size:
|
|
81
|
+
size: cap,
|
|
105
82
|
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
106
83
|
});
|
|
107
|
-
this.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
84
|
+
this.bufCapBytes = cap;
|
|
85
|
+
this.uploadedGen = -1; // force re-upload into the fresh buffer
|
|
86
|
+
}
|
|
87
|
+
if (records.generation !== this.uploadedGen && data.byteLength > 0) {
|
|
88
|
+
this.device.queue.writeBuffer(this.buf!, 0, data.buffer, data.byteOffset, data.byteLength);
|
|
89
|
+
this.uploadedGen = records.generation;
|
|
111
90
|
}
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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;
|
|
91
|
+
if (records.recordCount !== this.uploadedCount) {
|
|
92
|
+
this.countCpu[0] = records.recordCount;
|
|
93
|
+
this.device.queue.writeBuffer(this.countBuf, 0, this.countCpu.buffer, this.countCpu.byteOffset, 4);
|
|
94
|
+
this.uploadedCount = records.recordCount;
|
|
125
95
|
}
|
|
126
96
|
}
|
|
127
97
|
|
|
128
98
|
buffer(): GPUBuffer {
|
|
129
|
-
if (this.
|
|
130
|
-
|
|
131
|
-
this.gpu = this.device.createBuffer({
|
|
99
|
+
if (this.buf === undefined) {
|
|
100
|
+
this.buf = this.device.createBuffer({
|
|
132
101
|
label: "derivedUniforms.records.stub",
|
|
133
|
-
size:
|
|
102
|
+
size: 16,
|
|
134
103
|
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
135
104
|
});
|
|
136
|
-
this.
|
|
105
|
+
this.bufCapBytes = 16;
|
|
137
106
|
}
|
|
138
|
-
return this.
|
|
107
|
+
return this.buf;
|
|
139
108
|
}
|
|
140
109
|
|
|
141
110
|
dispose(): void {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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;
|
|
111
|
+
this.buf?.destroy();
|
|
112
|
+
this.buf = undefined;
|
|
113
|
+
this.countBuf.destroy();
|
|
173
114
|
}
|
|
174
115
|
}
|
|
175
116
|
|
|
176
|
-
/**
|
|
177
|
-
|
|
178
|
-
readonly
|
|
179
|
-
readonly bgl:
|
|
117
|
+
/** Recompilable uber-kernel pipeline. Rebuilt on `(registry.version, strideU32)` change. */
|
|
118
|
+
class UberPipeline {
|
|
119
|
+
private readonly device: GPUDevice;
|
|
120
|
+
readonly bgl: GPUBindGroupLayout;
|
|
121
|
+
private pipe: GPUComputePipeline | undefined;
|
|
122
|
+
private builtVersion = -1;
|
|
123
|
+
private builtStride = -1;
|
|
180
124
|
|
|
181
125
|
constructor(device: GPUDevice) {
|
|
182
|
-
|
|
183
|
-
this.bgl = device.createBindGroupLayout({
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
|
|
126
|
+
this.device = device;
|
|
127
|
+
this.bgl = device.createBindGroupLayout({ entries: bglEntries() });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
pipeline(registry: DerivedUniformRegistry, strideU32: number): GPUComputePipeline | undefined {
|
|
131
|
+
if (registry.size === 0) return undefined;
|
|
132
|
+
if (this.pipe !== undefined && this.builtVersion === registry.version && this.builtStride === strideU32) {
|
|
133
|
+
return this.pipe;
|
|
134
|
+
}
|
|
135
|
+
const { wgsl } = buildUberKernel(registry, strideU32);
|
|
136
|
+
const module = this.device.createShaderModule({ code: wgsl, label: "derivedUniforms.uber" });
|
|
137
|
+
this.pipe = this.device.createComputePipeline({
|
|
138
|
+
label: "derivedUniforms.uber",
|
|
139
|
+
layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.bgl] }),
|
|
193
140
|
compute: { module, entryPoint: "main" },
|
|
194
141
|
});
|
|
142
|
+
this.builtVersion = registry.version;
|
|
143
|
+
this.builtStride = strideU32;
|
|
144
|
+
return this.pipe;
|
|
195
145
|
}
|
|
196
146
|
}
|
|
197
147
|
|
|
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
148
|
export class DerivedUniformsDispatcher {
|
|
202
149
|
private readonly device: GPUDevice;
|
|
203
|
-
private readonly pipe: DerivedUniformsPipeline;
|
|
204
150
|
private readonly resources: DerivedUniformsResources;
|
|
205
|
-
readonly
|
|
206
|
-
|
|
151
|
+
private readonly gpu: RecordsGpu;
|
|
152
|
+
private readonly pipe: UberPipeline;
|
|
207
153
|
private cachedBg: GPUBindGroup | undefined;
|
|
208
|
-
private
|
|
209
|
-
private bgKeyHeap: GPUBuffer | undefined;
|
|
210
|
-
private bgKeyRecs: GPUBuffer | undefined;
|
|
154
|
+
private bgKey: [GPUBuffer | undefined, GPUBuffer | undefined, GPUBuffer | undefined] = [undefined, undefined, undefined];
|
|
211
155
|
|
|
212
|
-
|
|
213
|
-
|
|
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;
|
|
156
|
+
constructor(device: GPUDevice, resources: DerivedUniformsResources) {
|
|
157
|
+
this.device = device;
|
|
226
158
|
this.resources = resources;
|
|
227
|
-
this.
|
|
228
|
-
this.
|
|
229
|
-
label: "derivedUniforms.recordCount",
|
|
230
|
-
size: 16, // uniform min size
|
|
231
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
232
|
-
});
|
|
159
|
+
this.gpu = new RecordsGpu(device);
|
|
160
|
+
this.pipe = new UberPipeline(device);
|
|
233
161
|
}
|
|
234
162
|
|
|
235
|
-
/**
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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;
|
|
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 {
|
|
165
|
+
if (records.recordCount === 0) return false;
|
|
166
|
+
const pipeline = this.pipe.pipeline(registry, records.strideWords);
|
|
167
|
+
if (pipeline === undefined) return false;
|
|
168
|
+
this.gpu.sync(records);
|
|
259
169
|
const pass = enc.beginComputePass({ label: "derivedUniforms.uber" });
|
|
260
|
-
pass.setPipeline(
|
|
261
|
-
|
|
170
|
+
pass.setPipeline(pipeline);
|
|
171
|
+
pass.setBindGroup(0, this.bindGroup());
|
|
172
|
+
pass.dispatchWorkgroups(Math.ceil(records.recordCount / 64));
|
|
262
173
|
pass.end();
|
|
263
|
-
return
|
|
174
|
+
return true;
|
|
264
175
|
}
|
|
265
176
|
|
|
266
177
|
dispose(): void {
|
|
267
|
-
this.
|
|
268
|
-
this.countBuf.destroy();
|
|
178
|
+
this.gpu.dispose();
|
|
269
179
|
}
|
|
270
180
|
|
|
271
181
|
private bindGroup(): GPUBindGroup {
|
|
272
182
|
const consts = this.resources.constituentsBuf();
|
|
273
|
-
const heap
|
|
274
|
-
const recs
|
|
275
|
-
if (
|
|
276
|
-
this.cachedBg !== undefined &&
|
|
277
|
-
this.bgKeyConsts === consts && this.bgKeyHeap === heap &&
|
|
278
|
-
this.bgKeyRecs === recs
|
|
279
|
-
) {
|
|
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) {
|
|
280
186
|
return this.cachedBg;
|
|
281
187
|
}
|
|
282
188
|
this.cachedBg = this.device.createBindGroup({
|
|
189
|
+
label: "derivedUniforms.uber",
|
|
283
190
|
layout: this.pipe.bgl,
|
|
284
191
|
entries: [
|
|
285
192
|
{ binding: 0, resource: { buffer: consts } },
|
|
286
193
|
{ binding: 1, resource: { buffer: heap } },
|
|
287
194
|
{ binding: 2, resource: { buffer: recs } },
|
|
288
|
-
{ binding: 3, resource: { buffer: this.countBuf } },
|
|
195
|
+
{ binding: 3, resource: { buffer: this.gpu.countBuf } },
|
|
289
196
|
],
|
|
290
197
|
});
|
|
291
|
-
this.
|
|
292
|
-
this.bgKeyRecs = recs;
|
|
198
|
+
this.bgKey = [consts, heap, recs];
|
|
293
199
|
return this.cachedBg;
|
|
294
200
|
}
|
|
295
201
|
}
|
|
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,89 @@
|
|
|
1
|
+
// §7 v2 — chain flattening.
|
|
2
|
+
//
|
|
3
|
+
// A rule whose IR mentions another derived-uniform name has that producer's
|
|
4
|
+
// IR substituted in place (recursively) until the rule reads only non-derived
|
|
5
|
+
// inputs (constituents / host uniforms). After flattening every
|
|
6
|
+
// record is independent ⇒ one compute dispatch, no levels, no barriers.
|
|
7
|
+
// See docs/derived-uniforms-extensible.md § "Chain flattening".
|
|
8
|
+
|
|
9
|
+
import type { Expr, Type } from "@aardworx/wombat.shader/ir";
|
|
10
|
+
import { visitExprChildren, stableStringify } from "@aardworx/wombat.shader/ir";
|
|
11
|
+
import { mapExpr } from "@aardworx/wombat.shader/passes";
|
|
12
|
+
import type { DerivedRule } from "./rule.js";
|
|
13
|
+
|
|
14
|
+
export interface RuleInput {
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly type: Type;
|
|
17
|
+
/** `true` ⇒ the leaf appeared as `Inverse(ReadInput(name))` — for a constituent
|
|
18
|
+
* trafo this selects the stored backward half (free); the forward half otherwise. */
|
|
19
|
+
readonly inverse: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The rule's input leaves in **first-appearance order** (DFS pre-order of `ir`),
|
|
24
|
+
* deduplicated by `(name, inverse)`. A leaf is either `ReadInput("Uniform", name)`
|
|
25
|
+
* or `Inverse(ReadInput("Uniform", name))`. Order matters: it is the order the
|
|
26
|
+
* generated kernel arm consumes its arguments (e.g. the multiplication order of a
|
|
27
|
+
* matmul chain). Throws if a name is used at two structurally-different types.
|
|
28
|
+
*/
|
|
29
|
+
export function inputsOf(ir: Expr): RuleInput[] {
|
|
30
|
+
const out: RuleInput[] = [];
|
|
31
|
+
const seen = new Set<string>(); // key = `${inverse ? "i" : "v"}:${name}`
|
|
32
|
+
const typeByName = new Map<string, Type>();
|
|
33
|
+
const record = (name: string, type: Type, inverse: boolean): void => {
|
|
34
|
+
const prev = typeByName.get(name);
|
|
35
|
+
if (prev !== undefined && !typesEqual(prev, type)) {
|
|
36
|
+
throw new Error(`derived rule: uniform '${name}' used at conflicting types`);
|
|
37
|
+
}
|
|
38
|
+
typeByName.set(name, type);
|
|
39
|
+
const key = `${inverse ? "i" : "v"}:${name}`;
|
|
40
|
+
if (!seen.has(key)) {
|
|
41
|
+
seen.add(key);
|
|
42
|
+
out.push({ name, type, inverse });
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const walk = (e: Expr): void => {
|
|
46
|
+
if (e.kind === "ReadInput" && e.scope === "Uniform") {
|
|
47
|
+
record(e.name, e.type, false);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (e.kind === "Inverse" && e.value.kind === "ReadInput" && e.value.scope === "Uniform") {
|
|
51
|
+
record(e.value.name, e.value.type, true);
|
|
52
|
+
return; // do not also count the inner ReadInput as a separate forward leaf
|
|
53
|
+
}
|
|
54
|
+
visitExprChildren(e, walk);
|
|
55
|
+
};
|
|
56
|
+
walk(ir);
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Substitute every derived-uniform leaf in `ir` with its (recursively
|
|
62
|
+
* flattened) producer. `name` is the uniform name `ir` is being registered
|
|
63
|
+
* under; it is pushed on the expansion stack so a self-reference — or any
|
|
64
|
+
* cycle through other rules — is caught.
|
|
65
|
+
*/
|
|
66
|
+
export function flatten(
|
|
67
|
+
name: string,
|
|
68
|
+
ir: Expr,
|
|
69
|
+
derived: ReadonlyMap<string, DerivedRule>,
|
|
70
|
+
_stack: readonly string[] = [],
|
|
71
|
+
): Expr {
|
|
72
|
+
if (_stack.includes(name)) {
|
|
73
|
+
throw new Error(`derived uniform cycle: ${[..._stack, name].join(" → ")}`);
|
|
74
|
+
}
|
|
75
|
+
const stack = [..._stack, name];
|
|
76
|
+
return mapExpr(ir, (e) => {
|
|
77
|
+
if (e.kind === "ReadInput" && e.scope === "Uniform") {
|
|
78
|
+
const producer = derived.get(e.name);
|
|
79
|
+
if (producer !== undefined) {
|
|
80
|
+
return flatten(e.name, producer.ir, derived, stack);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return e;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function typesEqual(a: Type, b: Type): boolean {
|
|
88
|
+
return a === b || stableStringify(a) === stableStringify(b);
|
|
89
|
+
}
|