@aardworx/wombat.rendering 0.9.17 → 0.9.19
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/renderObject.d.ts +19 -0
- package/dist/core/renderObject.d.ts.map +1 -1
- package/dist/runtime/derivedModes/gpuDispatcher.d.ts +46 -0
- package/dist/runtime/derivedModes/gpuDispatcher.d.ts.map +1 -0
- package/dist/runtime/derivedModes/gpuDispatcher.js +170 -0
- package/dist/runtime/derivedModes/gpuDispatcher.js.map +1 -0
- package/dist/runtime/derivedModes/gpuKernel.d.ts +9 -0
- package/dist/runtime/derivedModes/gpuKernel.d.ts.map +1 -0
- package/dist/runtime/derivedModes/gpuKernel.js +97 -0
- package/dist/runtime/derivedModes/gpuKernel.js.map +1 -0
- package/dist/runtime/derivedModes/rule.d.ts +33 -0
- package/dist/runtime/derivedModes/rule.d.ts.map +1 -1
- package/dist/runtime/derivedModes/rule.js +39 -0
- package/dist/runtime/derivedModes/rule.js.map +1 -1
- package/dist/runtime/heapAdapter.d.ts.map +1 -1
- package/dist/runtime/heapAdapter.js +1 -0
- package/dist/runtime/heapAdapter.js.map +1 -1
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +78 -0
- package/dist/runtime/heapScene.js.map +1 -1
- package/dist/runtime/index.d.ts +3 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +3 -1
- package/dist/runtime/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/renderObject.ts +19 -0
- package/src/runtime/derivedModes/gpuDispatcher.ts +190 -0
- package/src/runtime/derivedModes/gpuKernel.ts +98 -0
- package/src/runtime/derivedModes/rule.ts +62 -0
- package/src/runtime/heapAdapter.ts +1 -0
- package/src/runtime/heapScene.ts +76 -0
- package/src/runtime/index.ts +9 -0
package/dist/runtime/index.js
CHANGED
|
@@ -21,5 +21,7 @@ export { PipelineCache, encodeModeKey, decodeModeKey, MAX_ATTACHMENTS, DEFAULT_D
|
|
|
21
21
|
export { ModeKeyTracker, snapshotDescriptor, } from "./derivedModes/modeKeyCpu.js";
|
|
22
22
|
export { SlotTable, } from "./derivedModes/slotTable.js";
|
|
23
23
|
export { partitionCPU, PARTITION_HISTOGRAM_WGSL, PARTITION_SCATTER_WGSL, MAX_SLOTS_PER_BUCKET, } from "./derivedModes/partition.js";
|
|
24
|
-
export { derivedMode, isDerivedModeRule, flipCull, } from "./derivedModes/rule.js";
|
|
24
|
+
export { derivedMode, isDerivedModeRule, flipCull, gpuFlipCullByDeterminant, } from "./derivedModes/rule.js";
|
|
25
|
+
export { GpuDerivedModesScene } from "./derivedModes/gpuDispatcher.js";
|
|
26
|
+
export { GPU_FLIP_CULL_BY_DET_WGSL, CULL_TO_U32, U32_TO_CULL, } from "./derivedModes/gpuKernel.js";
|
|
25
27
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"AAAA,oDAAoD;AAEpD,OAAO,EACL,OAAO,GAER,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,iBAAiB,GAElB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EACL,QAAQ,GAGT,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,SAAS,GAEV,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,cAAc,GAOf,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,iBAAiB,GAGlB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,uFAAuF;AACvF,OAAO,EACL,cAAc,EAAE,WAAW,EAC3B,UAAU,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,EAE7C,sBAAsB,EAAE,qBAAqB,GAC9C,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EACL,kBAAkB,GAGnB,MAAM,kBAAkB,CAAC;AAE1B,uEAAuE;AACvE,OAAO,EACL,SAAS,EACT,mBAAmB,GACpB,MAAM,6BAA6B,CAAC;AAErC,OAAO,EACL,cAAc,EACd,KAAK,IAAI,mBAAmB,EAC5B,OAAO,IAAI,qBAAqB,EAChC,KAAK,IAAI,mBAAmB,EAC5B,SAAS,IAAI,uBAAuB,EACpC,MAAM,IAAI,oBAAoB,EAC9B,UAAU,IAAI,wBAAwB,EACtC,UAAU,IAAI,wBAAwB,GACvC,MAAM,0BAA0B,CAAC;AAElC,OAAO,EACL,SAAS,EACT,eAAe,EACf,aAAa,EACb,0BAA0B,EAC1B,kBAAkB,EAClB,gBAAgB,GAKjB,MAAM,6BAA6B,CAAC;AAErC,sEAAsE;AACtE,OAAO,EACL,aAAa,EACb,aAAa,EACb,aAAa,EACb,eAAe,EACf,kBAAkB,EAClB,wBAAwB,EACxB,gBAAgB,GAWjB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EACL,cAAc,EACd,kBAAkB,GACnB,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EACL,SAAS,GAEV,MAAM,6BAA6B,CAAC;AAErC,OAAO,EACL,YAAY,EACZ,wBAAwB,EACxB,sBAAsB,EACtB,oBAAoB,GAGrB,MAAM,6BAA6B,CAAC;AAErC,OAAO,EACL,WAAW,EACX,iBAAiB,EACjB,QAAQ,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"AAAA,oDAAoD;AAEpD,OAAO,EACL,OAAO,GAER,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,iBAAiB,GAElB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EACL,QAAQ,GAGT,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,SAAS,GAEV,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,cAAc,GAOf,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,iBAAiB,GAGlB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,uFAAuF;AACvF,OAAO,EACL,cAAc,EAAE,WAAW,EAC3B,UAAU,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,EAE7C,sBAAsB,EAAE,qBAAqB,GAC9C,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EACL,kBAAkB,GAGnB,MAAM,kBAAkB,CAAC;AAE1B,uEAAuE;AACvE,OAAO,EACL,SAAS,EACT,mBAAmB,GACpB,MAAM,6BAA6B,CAAC;AAErC,OAAO,EACL,cAAc,EACd,KAAK,IAAI,mBAAmB,EAC5B,OAAO,IAAI,qBAAqB,EAChC,KAAK,IAAI,mBAAmB,EAC5B,SAAS,IAAI,uBAAuB,EACpC,MAAM,IAAI,oBAAoB,EAC9B,UAAU,IAAI,wBAAwB,EACtC,UAAU,IAAI,wBAAwB,GACvC,MAAM,0BAA0B,CAAC;AAElC,OAAO,EACL,SAAS,EACT,eAAe,EACf,aAAa,EACb,0BAA0B,EAC1B,kBAAkB,EAClB,gBAAgB,GAKjB,MAAM,6BAA6B,CAAC;AAErC,sEAAsE;AACtE,OAAO,EACL,aAAa,EACb,aAAa,EACb,aAAa,EACb,eAAe,EACf,kBAAkB,EAClB,wBAAwB,EACxB,gBAAgB,GAWjB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EACL,cAAc,EACd,kBAAkB,GACnB,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EACL,SAAS,GAEV,MAAM,6BAA6B,CAAC;AAErC,OAAO,EACL,YAAY,EACZ,wBAAwB,EACxB,sBAAsB,EACtB,oBAAoB,GAGrB,MAAM,6BAA6B,CAAC;AAErC,OAAO,EACL,WAAW,EACX,iBAAiB,EACjB,QAAQ,EACR,wBAAwB,GAMzB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AACvE,OAAO,EACL,yBAAyB,EACzB,WAAW,EACX,WAAW,GACZ,MAAM,6BAA6B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aardworx/wombat.rendering",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.19",
|
|
4
4
|
"description": "WebGPU rendering layer for the Wombat TypeScript stack — RenderObject/RenderTask/runtime + window glue, port of Aardvark.Rendering's lower layers on top of WebGPU and wombat.shader.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "krauthaufen",
|
package/src/core/renderObject.ts
CHANGED
|
@@ -67,4 +67,23 @@ export interface RenderObject {
|
|
|
67
67
|
/** Index buffer for indexed draws. */
|
|
68
68
|
readonly indices?: BufferView;
|
|
69
69
|
readonly drawCall: aval<DrawCall>;
|
|
70
|
+
/**
|
|
71
|
+
* Per-axis derived-mode rules — `(u, declared) => modeValue`
|
|
72
|
+
* closures evaluated against this RO's uniforms at scene-build
|
|
73
|
+
* (and on reactive marks). See
|
|
74
|
+
* `@aardworx/wombat.rendering/runtime` — `derivedMode(...)`.
|
|
75
|
+
*
|
|
76
|
+
* A rule overrides the corresponding `pipelineState.rasterizer.*`
|
|
77
|
+
* (or `pipelineState.depth.*`) aval value. v1 evaluates rules
|
|
78
|
+
* CPU-side; v2 (deferred) lowers them to a GPU compute kernel.
|
|
79
|
+
* See `docs/derived-modes.md`.
|
|
80
|
+
*/
|
|
81
|
+
readonly modeRules?: {
|
|
82
|
+
readonly cull?: import("../runtime/derivedModes/rule.js").DerivedModeRule<"cull">;
|
|
83
|
+
readonly frontFace?: import("../runtime/derivedModes/rule.js").DerivedModeRule<"frontFace">;
|
|
84
|
+
readonly topology?: import("../runtime/derivedModes/rule.js").DerivedModeRule<"topology">;
|
|
85
|
+
readonly depthCompare?: import("../runtime/derivedModes/rule.js").DerivedModeRule<"depthCompare">;
|
|
86
|
+
readonly depthWrite?: import("../runtime/derivedModes/rule.js").DerivedModeRule<"depthWrite">;
|
|
87
|
+
readonly alphaToCoverage?: import("../runtime/derivedModes/rule.js").DerivedModeRule<"alphaToCoverage">;
|
|
88
|
+
};
|
|
70
89
|
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// Scene-level dispatcher for the v1 GPU-evaluated derived-mode rule
|
|
2
|
+
// (flip-cull-by-determinant; see ./gpuKernel.ts).
|
|
3
|
+
//
|
|
4
|
+
// Lifecycle
|
|
5
|
+
// =========
|
|
6
|
+
//
|
|
7
|
+
// - Construct once per heap scene with the device.
|
|
8
|
+
// - registerRo(drawId, modelRef, declaredCull): register an RO that
|
|
9
|
+
// has the GPU flip-cull rule. modelRef = byte offset into the
|
|
10
|
+
// arena where the RO's ModelTrafo mat4 lives.
|
|
11
|
+
// - deregisterRo(drawId): unregister on removeDraw.
|
|
12
|
+
// - markDirty(): caller hints that an input changed; the next
|
|
13
|
+
// `dispatch()` will run the kernel.
|
|
14
|
+
// - dispatch(arenaBuf, enc): if dirty, encode the compute pass into
|
|
15
|
+
// `enc`. Always copies the output buffer into a staging buffer for
|
|
16
|
+
// CPU readback.
|
|
17
|
+
// - finish(): mapAsync the staging buffer and resolve with the
|
|
18
|
+
// per-RO output array. The scene calls this AFTER submit and
|
|
19
|
+
// before deciding rebuckets. The promise resolves on the next
|
|
20
|
+
// microtask after the GPU completes the copy.
|
|
21
|
+
//
|
|
22
|
+
// The buffers grow lazily (pow2) as ROs are added.
|
|
23
|
+
|
|
24
|
+
import { GPU_FLIP_CULL_BY_DET_WGSL, CULL_TO_U32 } from "./gpuKernel.js";
|
|
25
|
+
import type { CullMode } from "../pipelineCache/index.js";
|
|
26
|
+
|
|
27
|
+
const NO_RULE = 0xFFFFFFFF;
|
|
28
|
+
|
|
29
|
+
interface RoEntry {
|
|
30
|
+
/** Byte offset of the RO's ModelTrafo mat4 in the arena. */
|
|
31
|
+
modelRef: number;
|
|
32
|
+
/** SG-declared cull mode, encoded as u32 (0=none/1=front/2=back). */
|
|
33
|
+
declaredU32: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const WG_SIZE = 64;
|
|
37
|
+
const POW2 = (n: number): number => { let p = 1; while (p < n) p <<= 1; return Math.max(64, p); };
|
|
38
|
+
|
|
39
|
+
export class GpuDerivedModesScene {
|
|
40
|
+
readonly device: GPUDevice;
|
|
41
|
+
|
|
42
|
+
private readonly entries: (RoEntry | undefined)[] = [];
|
|
43
|
+
private liveCount = 0;
|
|
44
|
+
private dirty = false;
|
|
45
|
+
|
|
46
|
+
// GPU resources — lazily allocated on first registerRo.
|
|
47
|
+
private inputBuf: GPUBuffer | null = null;
|
|
48
|
+
private outputBuf: GPUBuffer | null = null;
|
|
49
|
+
private stagingBuf: GPUBuffer | null = null;
|
|
50
|
+
private paramsBuf: GPUBuffer | null = null;
|
|
51
|
+
private pipeline: GPUComputePipeline | null = null;
|
|
52
|
+
private bindGroup: GPUBindGroup | null = null;
|
|
53
|
+
private layout: GPUBindGroupLayout | null = null;
|
|
54
|
+
/** Capacity in number of u32 entries (= numROs the buffers can hold). */
|
|
55
|
+
private capacity = 0;
|
|
56
|
+
/** Last-known arena buffer, used to detect bind-group invalidation. */
|
|
57
|
+
private arenaBuf: GPUBuffer | null = null;
|
|
58
|
+
/** Last-known declared (for kernel uniform). Common case: all RO
|
|
59
|
+
* rules share the same declared (= the SG scope's value). */
|
|
60
|
+
private declared: number = CULL_TO_U32.back;
|
|
61
|
+
|
|
62
|
+
/** Per-RO last-known output. CPU mirror, populated by finish(). */
|
|
63
|
+
readonly lastOutput: Uint32Array;
|
|
64
|
+
|
|
65
|
+
constructor(device: GPUDevice) {
|
|
66
|
+
this.device = device;
|
|
67
|
+
this.lastOutput = new Uint32Array(0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get registered(): number { return this.liveCount; }
|
|
71
|
+
|
|
72
|
+
registerRo(drawId: number, modelRef: number, declared: CullMode): void {
|
|
73
|
+
this.entries[drawId] = { modelRef, declaredU32: CULL_TO_U32[declared] };
|
|
74
|
+
this.liveCount++;
|
|
75
|
+
this.dirty = true;
|
|
76
|
+
this.declared = CULL_TO_U32[declared];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
deregisterRo(drawId: number): void {
|
|
80
|
+
if (this.entries[drawId] === undefined) return;
|
|
81
|
+
this.entries[drawId] = undefined;
|
|
82
|
+
this.liveCount--;
|
|
83
|
+
this.dirty = true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
markDirty(): void { this.dirty = true; }
|
|
87
|
+
consumeDirty(): boolean { const was = this.dirty; this.dirty = false; return was; }
|
|
88
|
+
|
|
89
|
+
private ensureResources(arenaBuf: GPUBuffer, numROs: number): void {
|
|
90
|
+
const needCap = POW2(Math.max(1, numROs));
|
|
91
|
+
if (this.outputBuf === null || needCap > this.capacity) {
|
|
92
|
+
this.inputBuf ?.destroy();
|
|
93
|
+
this.outputBuf ?.destroy();
|
|
94
|
+
this.stagingBuf?.destroy();
|
|
95
|
+
const bytes = needCap * 4;
|
|
96
|
+
this.inputBuf = this.device.createBuffer({ size: bytes, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, label: "gpuDerivedModes/in" });
|
|
97
|
+
this.outputBuf = this.device.createBuffer({ size: bytes, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, label: "gpuDerivedModes/out" });
|
|
98
|
+
this.stagingBuf = this.device.createBuffer({ size: bytes, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST, label: "gpuDerivedModes/staging" });
|
|
99
|
+
(this as { lastOutput: Uint32Array }).lastOutput = new Uint32Array(needCap);
|
|
100
|
+
this.capacity = needCap;
|
|
101
|
+
this.bindGroup = null;
|
|
102
|
+
}
|
|
103
|
+
if (this.paramsBuf === null) {
|
|
104
|
+
this.paramsBuf = this.device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, label: "gpuDerivedModes/params" });
|
|
105
|
+
}
|
|
106
|
+
if (this.layout === null) {
|
|
107
|
+
this.layout = this.device.createBindGroupLayout({
|
|
108
|
+
label: "gpuDerivedModes/bgl",
|
|
109
|
+
entries: [
|
|
110
|
+
{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
|
|
111
|
+
{ binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
|
|
112
|
+
{ binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } },
|
|
113
|
+
{ binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } },
|
|
114
|
+
],
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (this.pipeline === null) {
|
|
118
|
+
const module = this.device.createShaderModule({ code: GPU_FLIP_CULL_BY_DET_WGSL, label: "gpuDerivedModes/module" });
|
|
119
|
+
this.pipeline = this.device.createComputePipeline({
|
|
120
|
+
label: "gpuDerivedModes/pipeline",
|
|
121
|
+
layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.layout] }),
|
|
122
|
+
compute: { module, entryPoint: "evaluate" },
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
if (this.bindGroup === null || this.arenaBuf !== arenaBuf) {
|
|
126
|
+
this.bindGroup = this.device.createBindGroup({
|
|
127
|
+
label: "gpuDerivedModes/bindGroup",
|
|
128
|
+
layout: this.layout,
|
|
129
|
+
entries: [
|
|
130
|
+
{ binding: 0, resource: { buffer: arenaBuf } },
|
|
131
|
+
{ binding: 1, resource: { buffer: this.inputBuf! } },
|
|
132
|
+
{ binding: 2, resource: { buffer: this.outputBuf! } },
|
|
133
|
+
{ binding: 3, resource: { buffer: this.paramsBuf! } },
|
|
134
|
+
],
|
|
135
|
+
});
|
|
136
|
+
this.arenaBuf = arenaBuf;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Upload current per-RO input refs + params; called when dirty. */
|
|
141
|
+
private uploadInputs(numROs: number): void {
|
|
142
|
+
const refs = new Uint32Array(this.capacity).fill(NO_RULE);
|
|
143
|
+
for (let i = 0; i < numROs; i++) {
|
|
144
|
+
const e = this.entries[i];
|
|
145
|
+
if (e !== undefined) refs[i] = e.modelRef;
|
|
146
|
+
}
|
|
147
|
+
this.device.queue.writeBuffer(this.inputBuf!, 0, refs.buffer, refs.byteOffset, refs.byteLength);
|
|
148
|
+
const params = new Uint32Array([numROs, this.declared, 0, 0]);
|
|
149
|
+
this.device.queue.writeBuffer(this.paramsBuf!, 0, params.buffer, params.byteOffset, params.byteLength);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Encode the compute dispatch + the staging copy into `enc`. Caller
|
|
154
|
+
* is responsible for `device.queue.submit([enc.finish()])`. Returns
|
|
155
|
+
* the number of ROs the kernel was dispatched against.
|
|
156
|
+
*/
|
|
157
|
+
dispatch(arenaBuf: GPUBuffer, numROs: number, enc: GPUCommandEncoder): number {
|
|
158
|
+
this.ensureResources(arenaBuf, numROs);
|
|
159
|
+
this.uploadInputs(numROs);
|
|
160
|
+
const pass = enc.beginComputePass({ label: "gpuDerivedModes/pass" });
|
|
161
|
+
pass.setPipeline(this.pipeline!);
|
|
162
|
+
pass.setBindGroup(0, this.bindGroup!);
|
|
163
|
+
pass.dispatchWorkgroups(Math.ceil(numROs / WG_SIZE));
|
|
164
|
+
pass.end();
|
|
165
|
+
const bytes = numROs * 4;
|
|
166
|
+
enc.copyBufferToBuffer(this.outputBuf!, 0, this.stagingBuf!, 0, bytes);
|
|
167
|
+
return numROs;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* mapAsync the staging buffer + copy values into `lastOutput`.
|
|
172
|
+
* Resolves on the GPU finishing the copy. Caller diffs lastOutput
|
|
173
|
+
* against its own cached values to find changed ROs.
|
|
174
|
+
*/
|
|
175
|
+
async finish(numROs: number): Promise<void> {
|
|
176
|
+
if (this.stagingBuf === null || numROs === 0) return;
|
|
177
|
+
const bytes = numROs * 4;
|
|
178
|
+
await this.stagingBuf.mapAsync(GPUMapMode.READ, 0, bytes);
|
|
179
|
+
const view = new Uint32Array(this.stagingBuf.getMappedRange(0, bytes).slice(0));
|
|
180
|
+
this.stagingBuf.unmap();
|
|
181
|
+
(this as { lastOutput: Uint32Array }).lastOutput.set(view, 0);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
dispose(): void {
|
|
185
|
+
this.inputBuf ?.destroy();
|
|
186
|
+
this.outputBuf ?.destroy();
|
|
187
|
+
this.stagingBuf?.destroy();
|
|
188
|
+
this.paramsBuf ?.destroy();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// GPU-evaluated derived-mode rules — minimum viable kernel.
|
|
2
|
+
//
|
|
3
|
+
// CPU eval (Task 2 Phase 1-3, 0.9.17) runs rule closures in JS. For
|
|
4
|
+
// rules whose inputs live GPU-side (per-RO trafos, etc.) JS evaluation
|
|
5
|
+
// fans out CPU work that's wasted: the trafo bytes are right there in
|
|
6
|
+
// the arena. This module runs the rule on the GPU instead, in one
|
|
7
|
+
// dispatch over all dirty ROs per frame.
|
|
8
|
+
//
|
|
9
|
+
// v1 scope is deliberately narrow: ONE hard-coded WGSL kernel for
|
|
10
|
+
// "flip cullMode by sign of det(upper3x3 of an arena mat4)". Picked
|
|
11
|
+
// because it's the motivating case from docs/derived-modes.md and
|
|
12
|
+
// proves the GPU-eval path end-to-end. Generalizes via a traced IR
|
|
13
|
+
// builder in v2 (see derived-mode-rules-plan.md Phase 5).
|
|
14
|
+
//
|
|
15
|
+
// Architecture
|
|
16
|
+
// ============
|
|
17
|
+
//
|
|
18
|
+
// - One **module-wide** `GpuDerivedModesScene` per heap scene. Holds:
|
|
19
|
+
// * `roOutputBuf` — `array<u32>` sized to scene.numROs.
|
|
20
|
+
// Each entry = the rule's modeKey contribution.
|
|
21
|
+
// * `roInputBuf` — `array<u32>` (one ref-into-arena per RO)
|
|
22
|
+
// telling the kernel where to read the input.
|
|
23
|
+
// * `stagingBuf` — MAP_READ + COPY_DST mirror of roOutputBuf,
|
|
24
|
+
// for the per-frame readback.
|
|
25
|
+
// - Dispatch each frame in `update()`, ONLY when something dirty.
|
|
26
|
+
// - `mapAsync(staging)` resolves on a microtask; the CPU diffs the
|
|
27
|
+
// buffer against `roLastKey[]`. Any change → mark drawId dirty;
|
|
28
|
+
// the existing rebucket flow does the rest.
|
|
29
|
+
//
|
|
30
|
+
// The dirty gate keeps steady-state cost ≈ 0 — when no input aval
|
|
31
|
+
// marks AND no RO add/remove, we skip the dispatch + readback.
|
|
32
|
+
|
|
33
|
+
export const GPU_FLIP_CULL_BY_DET_WGSL = /* wgsl */ `
|
|
34
|
+
// Per-RO record. Index i corresponds to drawId i in the scene's
|
|
35
|
+
// flat RO table.
|
|
36
|
+
struct GpuRuleParams {
|
|
37
|
+
numROs: u32,
|
|
38
|
+
declared: u32, // 0 = none, 1 = front, 2 = back (must match CULL_MODE[] order in bitfield.ts)
|
|
39
|
+
_pad0: u32,
|
|
40
|
+
_pad1: u32,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
@group(0) @binding(0) var<storage, read> arena: array<u32>;
|
|
44
|
+
// One per-RO entry: byte offset within the arena of the ROs ModelTrafo
|
|
45
|
+
// allocation header. Data is at offset + 16 (ALLOC_HEADER_PAD_TO).
|
|
46
|
+
// 0xFFFFFFFFu means "this RO does NOT have a flip-cull-by-det rule".
|
|
47
|
+
@group(0) @binding(1) var<storage, read> roInputRefs: array<u32>;
|
|
48
|
+
@group(0) @binding(2) var<storage, read_write> roOutputs: array<u32>;
|
|
49
|
+
@group(0) @binding(3) var<uniform> uIn: GpuRuleParams;
|
|
50
|
+
|
|
51
|
+
// Read a row-major mat4 from the arena at byte offset \`refBytes + 16\`
|
|
52
|
+
// (header is 16 bytes wide due to ALLOC_HEADER_PAD_TO). Returns the
|
|
53
|
+
// 3×3 upper-left in row-major order: [m00,m01,m02, m10,m11,m12, m20,m21,m22].
|
|
54
|
+
fn loadUpper3x3(refBytes: u32) -> array<f32, 9> {
|
|
55
|
+
let baseU32 = (refBytes + 16u) >> 2u;
|
|
56
|
+
// The arena is a Float32 storage of mat4 in row-major, 16 floats.
|
|
57
|
+
// Read 9 of them.
|
|
58
|
+
return array<f32, 9>(
|
|
59
|
+
bitcast<f32>(arena[baseU32 + 0u]), bitcast<f32>(arena[baseU32 + 1u]), bitcast<f32>(arena[baseU32 + 2u]),
|
|
60
|
+
bitcast<f32>(arena[baseU32 + 4u]), bitcast<f32>(arena[baseU32 + 5u]), bitcast<f32>(arena[baseU32 + 6u]),
|
|
61
|
+
bitcast<f32>(arena[baseU32 + 8u]), bitcast<f32>(arena[baseU32 + 9u]), bitcast<f32>(arena[baseU32 + 10u]),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
fn det3x3(m: array<f32, 9>) -> f32 {
|
|
66
|
+
return m[0] * (m[4] * m[8] - m[5] * m[7])
|
|
67
|
+
- m[1] * (m[3] * m[8] - m[5] * m[6])
|
|
68
|
+
+ m[2] * (m[3] * m[7] - m[4] * m[6]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Mirror of bitfield.ts CULL_MODE[] order: 0=none, 1=front, 2=back.
|
|
72
|
+
fn flipCull(c: u32) -> u32 {
|
|
73
|
+
if (c == 1u) { return 2u; } // front -> back
|
|
74
|
+
if (c == 2u) { return 1u; } // back -> front
|
|
75
|
+
return c; // none
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@compute @workgroup_size(64)
|
|
79
|
+
fn evaluate(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
80
|
+
let i = gid.x;
|
|
81
|
+
if (i >= uIn.numROs) { return; }
|
|
82
|
+
let ref = roInputRefs[i];
|
|
83
|
+
if (ref == 0xFFFFFFFFu) {
|
|
84
|
+
// No rule on this RO; leave existing output untouched. Allows a
|
|
85
|
+
// single kernel dispatch to skip non-participating ROs.
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
let m = loadUpper3x3(ref);
|
|
89
|
+
let d = det3x3(m);
|
|
90
|
+
let declared = uIn.declared;
|
|
91
|
+
let outVal = select(declared, flipCull(declared), d < 0.0);
|
|
92
|
+
roOutputs[i] = outVal;
|
|
93
|
+
}
|
|
94
|
+
`;
|
|
95
|
+
|
|
96
|
+
/** Map between bitfield.ts's CULL_MODE order and the kernel's u32 enum. */
|
|
97
|
+
export const CULL_TO_U32 = { "none": 0, "front": 1, "back": 2 } as const;
|
|
98
|
+
export const U32_TO_CULL = ["none", "front", "back"] as const;
|
|
@@ -54,10 +54,29 @@ export type DerivedModeBuilder<A extends ModeAxis> = (
|
|
|
54
54
|
declared: ModeValue<A>,
|
|
55
55
|
) => ModeValue<A>;
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Opt-in GPU evaluation marker. When present on a rule, the heap
|
|
59
|
+
* scene runs a hard-coded WGSL kernel (see `./gpuKernel.ts`) instead
|
|
60
|
+
* of the CPU closure. The closure is retained for fallback (e.g.
|
|
61
|
+
* MockGPU tests, devices without compute, or before frame 0).
|
|
62
|
+
*
|
|
63
|
+
* v1 supports one kernel: `"flipCullByDeterminant"`. v2 generalizes
|
|
64
|
+
* via a traced IR builder.
|
|
65
|
+
*/
|
|
66
|
+
export interface GpuRuleSpec<A extends ModeAxis = ModeAxis> {
|
|
67
|
+
readonly kernel: "flipCullByDeterminant";
|
|
68
|
+
/** Name of the input uniform the kernel reads (e.g. "ModelTrafo"). */
|
|
69
|
+
readonly inputUniform: string;
|
|
70
|
+
/** Declared (SG-scope) value the kernel mutates by. */
|
|
71
|
+
readonly declared: ModeValue<A>;
|
|
72
|
+
}
|
|
73
|
+
|
|
57
74
|
export interface DerivedModeRule<A extends ModeAxis = ModeAxis> {
|
|
58
75
|
readonly __derivedModeRule: true;
|
|
59
76
|
readonly axis: A;
|
|
60
77
|
readonly evaluate: DerivedModeBuilder<A>;
|
|
78
|
+
/** Opt-in GPU evaluation. Absent → CPU closure path. */
|
|
79
|
+
readonly gpu?: GpuRuleSpec<A>;
|
|
61
80
|
/**
|
|
62
81
|
* Conservative over-approximation of the rule's output set. Used by
|
|
63
82
|
* the build-time pre-warm in v2 to enumerate the pipelines a scene
|
|
@@ -108,3 +127,46 @@ export function isDerivedModeRule(x: unknown): x is DerivedModeRule {
|
|
|
108
127
|
export function flipCull(c: CullMode): CullMode {
|
|
109
128
|
return c === "back" ? "front" : c === "front" ? "back" : "none";
|
|
110
129
|
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Construct a derived-mode rule that flips cullMode based on the sign
|
|
133
|
+
* of `det(upperLeft3x3(u.<inputUniform>))`. Runs on the GPU when
|
|
134
|
+
* heapScene's compute path is available; the same logic is mirrored
|
|
135
|
+
* in the CPU closure for tests and fallback.
|
|
136
|
+
*
|
|
137
|
+
* const rule = gpuFlipCullByDeterminant("ModelTrafo", "back");
|
|
138
|
+
* spec.modeRules = { cull: rule };
|
|
139
|
+
*
|
|
140
|
+
* Useful for mirrored geometry: scenes that animate a per-RO trafo
|
|
141
|
+
* with potentially-negative scale don't pay CPU per-RO determinant
|
|
142
|
+
* cost — the kernel walks the arena once per frame for ALL ROs that
|
|
143
|
+
* carry this rule.
|
|
144
|
+
*/
|
|
145
|
+
export function gpuFlipCullByDeterminant(
|
|
146
|
+
inputUniform: string,
|
|
147
|
+
declared: CullMode = "back",
|
|
148
|
+
): DerivedModeRule<"cull"> {
|
|
149
|
+
return {
|
|
150
|
+
__derivedModeRule: true,
|
|
151
|
+
axis: "cull",
|
|
152
|
+
domain: ["back", "front", "none"],
|
|
153
|
+
gpu: { kernel: "flipCullByDeterminant", inputUniform, declared },
|
|
154
|
+
evaluate: (u, declaredFromCtx) => {
|
|
155
|
+
const m = u[inputUniform] as { forward?: { _data?: Float64Array | Float32Array } } | { _data?: Float64Array | Float32Array };
|
|
156
|
+
// Accept Trafo3d ({forward: M44d}), M44 ({_data}), or row-major
|
|
157
|
+
// Float32Array. Reads upper-left 3x3.
|
|
158
|
+
const data: ArrayLike<number> | undefined =
|
|
159
|
+
(m as { forward?: { _data?: ArrayLike<number> } }).forward?._data ??
|
|
160
|
+
(m as { _data?: ArrayLike<number> })._data ??
|
|
161
|
+
(m as unknown as ArrayLike<number>);
|
|
162
|
+
if (data === undefined) return declaredFromCtx;
|
|
163
|
+
const m00 = data[0]!, m01 = data[1]!, m02 = data[2]!;
|
|
164
|
+
const m10 = data[4]!, m11 = data[5]!, m12 = data[6]!;
|
|
165
|
+
const m20 = data[8]!, m21 = data[9]!, m22 = data[10]!;
|
|
166
|
+
const det = m00 * (m11 * m22 - m12 * m21)
|
|
167
|
+
- m01 * (m10 * m22 - m12 * m20)
|
|
168
|
+
+ m02 * (m10 * m21 - m11 * m20);
|
|
169
|
+
return det < 0 ? flipCull(declaredFromCtx) : declaredFromCtx;
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
@@ -349,5 +349,6 @@ export function renderObjectToHeapSpec(
|
|
|
349
349
|
...(dc.instanceCount > 1 ? { instanceCount: dc.instanceCount } : {}),
|
|
350
350
|
indices,
|
|
351
351
|
...(textures !== undefined ? { textures } : {}),
|
|
352
|
+
...(ro.modeRules !== undefined ? { modeRules: ro.modeRules } : {}),
|
|
352
353
|
};
|
|
353
354
|
}
|
package/src/runtime/heapScene.ts
CHANGED
|
@@ -94,6 +94,8 @@ import {
|
|
|
94
94
|
} from "./heapScene/pools.js";
|
|
95
95
|
import { encodeModeKey, type PipelineStateDescriptor } from "./pipelineCache/index.js";
|
|
96
96
|
import { snapshotDescriptor, ModeKeyTracker } from "./derivedModes/modeKeyCpu.js";
|
|
97
|
+
import { GpuDerivedModesScene } from "./derivedModes/gpuDispatcher.js";
|
|
98
|
+
import { U32_TO_CULL } from "./derivedModes/gpuKernel.js";
|
|
97
99
|
import { addMarkingCallback } from "@aardworx/wombat.adaptive";
|
|
98
100
|
|
|
99
101
|
// GrowBuffer + ALIGN16 + MIN_BUFFER_BYTES + POW2 live in ./heapScene/growBuffer.
|
|
@@ -887,6 +889,14 @@ export function buildHeapScene(
|
|
|
887
889
|
* Drained on removeDraw to release slots + records. */
|
|
888
890
|
const derivedByDrawId = new Map<number, RoRegistration>();
|
|
889
891
|
|
|
892
|
+
// ─── GPU-eval mode rules (Task 2 Phase 5, narrow) ─────────────────
|
|
893
|
+
// Lazily allocated on first registration; one dispatcher per scene.
|
|
894
|
+
let gpuModesScene: GpuDerivedModesScene | undefined;
|
|
895
|
+
/** Last per-RO GPU output we routed to a bucket. Diff against
|
|
896
|
+
* newly-readback values to decide rebuckets without re-bucketing
|
|
897
|
+
* un-changed ROs. */
|
|
898
|
+
const gpuModesLastKey = new Map<number, number>();
|
|
899
|
+
|
|
890
900
|
/** The derived-uniform rule for a uniform binding on this RO, if any: an explicit
|
|
891
901
|
* `derivedUniform(...)` value in `spec.inputs`, or a standard trafo recipe by name
|
|
892
902
|
* (recipes apply only when their base-trafo leaves are actually bound on this RO —
|
|
@@ -2076,6 +2086,21 @@ export function buildHeapScene(
|
|
|
2076
2086
|
drawIdToModeTracker[drawId] = tracker;
|
|
2077
2087
|
tracker.forEachLeaf((av) => subscribeModeLeaf(av, drawId));
|
|
2078
2088
|
|
|
2089
|
+
// ─── GPU-eval mode rule registration (Task 2 Phase 5) ────────────
|
|
2090
|
+
// If spec.modeRules.cull is GPU-flavoured, register the RO with
|
|
2091
|
+
// the scene-wide dispatcher. Provide the arena byte offset of the
|
|
2092
|
+
// RO's input uniform so the kernel reads from the right slot.
|
|
2093
|
+
{
|
|
2094
|
+
const gpuRule = spec.modeRules?.cull?.gpu;
|
|
2095
|
+
if (gpuRule !== undefined && gpuRule.kernel === "flipCullByDeterminant") {
|
|
2096
|
+
const inputRef = perDrawRefs.get(gpuRule.inputUniform);
|
|
2097
|
+
if (inputRef !== undefined) {
|
|
2098
|
+
if (gpuModesScene === undefined) gpuModesScene = new GpuDerivedModesScene(device);
|
|
2099
|
+
gpuModesScene.registerRo(drawId, inputRef, gpuRule.declared);
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2079
2104
|
// ─── §7 derived-uniforms registration ────────────────────────────
|
|
2080
2105
|
// A uniform binding on this RO is either a value (aval/constant) or a rule —
|
|
2081
2106
|
// collect the rules (explicit `derivedUniform(...)` values + standard trafo
|
|
@@ -2236,6 +2261,10 @@ export function buildHeapScene(
|
|
|
2236
2261
|
}
|
|
2237
2262
|
drawIdToModeTracker[drawId] = undefined;
|
|
2238
2263
|
dirtyModeKeyDrawIds.delete(drawId);
|
|
2264
|
+
if (gpuModesScene !== undefined) {
|
|
2265
|
+
gpuModesScene.deregisterRo(drawId);
|
|
2266
|
+
gpuModesLastKey.delete(drawId);
|
|
2267
|
+
}
|
|
2239
2268
|
|
|
2240
2269
|
stats.totalDraws--;
|
|
2241
2270
|
stats.geometryBytes = arenaBytes(arena);
|
|
@@ -2560,6 +2589,52 @@ export function buildHeapScene(
|
|
|
2560
2589
|
* we just swap the bind group + dispatch shape per bucket.
|
|
2561
2590
|
*/
|
|
2562
2591
|
function encodeComputePrep(enc: GPUCommandEncoder, token: AdaptiveToken): void {
|
|
2592
|
+
// GPU-eval derived-mode rules (Task 2 Phase 5): dispatch the
|
|
2593
|
+
// hardcoded determinant-flip-cull kernel + start readback.
|
|
2594
|
+
// Resolved on the next frame's update() — one-frame lag for the
|
|
2595
|
+
// first appearance of a state transition. CPU fallback runs at
|
|
2596
|
+
// addDraw time so the initial bucket assignment is still correct.
|
|
2597
|
+
if (gpuModesScene !== undefined && gpuModesScene.registered > 0) {
|
|
2598
|
+
const numROs = nextDrawId; // upper bound; sparse entries are skipped via 0xFFFFFFFF sentinel
|
|
2599
|
+
gpuModesScene.dispatch(arena.attrs.buffer, numROs, enc);
|
|
2600
|
+
// mapAsync the staging buffer in the background. When it
|
|
2601
|
+
// resolves, diff against gpuModesLastKey to schedule rebuckets.
|
|
2602
|
+
gpuModesScene.finish(numROs).then(() => {
|
|
2603
|
+
const out = gpuModesScene!.lastOutput;
|
|
2604
|
+
for (let i = 0; i < numROs; i++) {
|
|
2605
|
+
const cur = out[i] ?? 0xFFFFFFFF;
|
|
2606
|
+
const prev = gpuModesLastKey.get(i);
|
|
2607
|
+
if (cur === 0xFFFFFFFF) continue; // no rule on this RO
|
|
2608
|
+
if (prev === cur) continue;
|
|
2609
|
+
gpuModesLastKey.set(i, cur);
|
|
2610
|
+
// Project the kernel's u32 enum back to a CullMode value,
|
|
2611
|
+
// store on the tracker's modeRules so the next snapshot
|
|
2612
|
+
// picks it up, then schedule a rebucket.
|
|
2613
|
+
const tracker = drawIdToModeTracker[i];
|
|
2614
|
+
const spec = drawIdToSpec[i];
|
|
2615
|
+
if (tracker === undefined || spec === undefined) continue;
|
|
2616
|
+
const cull = U32_TO_CULL[cur];
|
|
2617
|
+
if (cull === undefined) continue;
|
|
2618
|
+
// Inject the GPU result by patching the spec's CPU rule to
|
|
2619
|
+
// return a constant. Slightly hacky but contained: the next
|
|
2620
|
+
// recompute() will read this and route to the right bucket.
|
|
2621
|
+
const patched: typeof spec.modeRules = {
|
|
2622
|
+
...spec.modeRules,
|
|
2623
|
+
cull: {
|
|
2624
|
+
__derivedModeRule: true, axis: "cull",
|
|
2625
|
+
evaluate: () => cull,
|
|
2626
|
+
},
|
|
2627
|
+
};
|
|
2628
|
+
(spec as { modeRules?: typeof spec.modeRules }).modeRules = patched;
|
|
2629
|
+
dirtyModeKeyDrawIds.add(i);
|
|
2630
|
+
}
|
|
2631
|
+
}).catch((e) => {
|
|
2632
|
+
// mapAsync can reject if the device was lost or the scene was
|
|
2633
|
+
// disposed mid-readback. Log but don't crash the frame.
|
|
2634
|
+
console.warn("gpuModesScene readback failed:", e);
|
|
2635
|
+
});
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2563
2638
|
// §7: derived-uniforms dispatch first — writes into per-bucket
|
|
2564
2639
|
// drawHeap regions before the scan reads anything. One dispatcher
|
|
2565
2640
|
// per bucket; constituents are shared so dirty state propagates
|
|
@@ -2687,6 +2762,7 @@ export function buildHeapScene(
|
|
|
2687
2762
|
arena.attrs.destroy();
|
|
2688
2763
|
arena.indices.destroy();
|
|
2689
2764
|
for (const b of buckets) destroyBucketResources(b);
|
|
2765
|
+
gpuModesScene?.dispose();
|
|
2690
2766
|
}
|
|
2691
2767
|
|
|
2692
2768
|
// Test-only escape hatch for inspecting megacall bucket state. Not
|
package/src/runtime/index.ts
CHANGED
|
@@ -131,8 +131,17 @@ export {
|
|
|
131
131
|
derivedMode,
|
|
132
132
|
isDerivedModeRule,
|
|
133
133
|
flipCull,
|
|
134
|
+
gpuFlipCullByDeterminant,
|
|
134
135
|
type DerivedModeRule,
|
|
135
136
|
type DerivedModeBuilder,
|
|
137
|
+
type GpuRuleSpec,
|
|
136
138
|
type ModeAxis,
|
|
137
139
|
type ModeValue,
|
|
138
140
|
} from "./derivedModes/rule.js";
|
|
141
|
+
|
|
142
|
+
export { GpuDerivedModesScene } from "./derivedModes/gpuDispatcher.js";
|
|
143
|
+
export {
|
|
144
|
+
GPU_FLIP_CULL_BY_DET_WGSL,
|
|
145
|
+
CULL_TO_U32,
|
|
146
|
+
U32_TO_CULL,
|
|
147
|
+
} from "./derivedModes/gpuKernel.js";
|