@aardworx/wombat.rendering 0.9.33 → 0.19.0
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 +16 -0
- package/dist/core/renderObject.d.ts.map +1 -1
- package/dist/runtime/derivedModes/kernelCodegen.d.ts +90 -0
- package/dist/runtime/derivedModes/kernelCodegen.d.ts.map +1 -0
- package/dist/runtime/derivedModes/kernelCodegen.js +623 -0
- package/dist/runtime/derivedModes/kernelCodegen.js.map +1 -0
- package/dist/runtime/derivedModes/modeKeyCpu.d.ts.map +1 -1
- package/dist/runtime/derivedModes/modeKeyCpu.js +23 -67
- package/dist/runtime/derivedModes/modeKeyCpu.js.map +1 -1
- package/dist/runtime/derivedModes/partitionDispatcher.d.ts +79 -0
- package/dist/runtime/derivedModes/partitionDispatcher.d.ts.map +1 -0
- package/dist/runtime/derivedModes/partitionDispatcher.js +303 -0
- package/dist/runtime/derivedModes/partitionDispatcher.js.map +1 -0
- package/dist/runtime/derivedModes/partitionKernel.d.ts +12 -0
- package/dist/runtime/derivedModes/partitionKernel.d.ts.map +1 -0
- package/dist/runtime/derivedModes/partitionKernel.js +127 -0
- package/dist/runtime/derivedModes/partitionKernel.js.map +1 -0
- package/dist/runtime/derivedModes/partitionKernelLayout.d.ts +4 -0
- package/dist/runtime/derivedModes/partitionKernelLayout.d.ts.map +1 -0
- package/dist/runtime/derivedModes/partitionKernelLayout.js +24 -0
- package/dist/runtime/derivedModes/partitionKernelLayout.js.map +1 -0
- package/dist/runtime/derivedModes/rule.d.ts +38 -86
- package/dist/runtime/derivedModes/rule.d.ts.map +1 -1
- package/dist/runtime/derivedModes/rule.js +34 -74
- package/dist/runtime/derivedModes/rule.js.map +1 -1
- package/dist/runtime/derivedUniforms/dispatch.d.ts +10 -8
- package/dist/runtime/derivedUniforms/dispatch.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/dispatch.js +31 -18
- package/dist/runtime/derivedUniforms/dispatch.js.map +1 -1
- package/dist/runtime/derivedUniforms/marker.d.ts +90 -11
- package/dist/runtime/derivedUniforms/marker.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/marker.js +161 -15
- package/dist/runtime/derivedUniforms/marker.js.map +1 -1
- package/dist/runtime/derivedUniforms/sceneIntegration.d.ts +25 -6
- package/dist/runtime/derivedUniforms/sceneIntegration.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/sceneIntegration.js +61 -20
- package/dist/runtime/derivedUniforms/sceneIntegration.js.map +1 -1
- package/dist/runtime/heapAdapter.d.ts.map +1 -1
- package/dist/runtime/heapAdapter.js +17 -0
- package/dist/runtime/heapAdapter.js.map +1 -1
- package/dist/runtime/heapEligibility.d.ts.map +1 -1
- package/dist/runtime/heapEligibility.js +22 -2
- package/dist/runtime/heapEligibility.js.map +1 -1
- package/dist/runtime/heapScene/chunkedArena.d.ts +76 -0
- package/dist/runtime/heapScene/chunkedArena.d.ts.map +1 -0
- package/dist/runtime/heapScene/chunkedArena.js +223 -0
- package/dist/runtime/heapScene/chunkedArena.js.map +1 -0
- package/dist/runtime/heapScene/freelist.d.ts +55 -0
- package/dist/runtime/heapScene/freelist.d.ts.map +1 -0
- package/dist/runtime/heapScene/freelist.js +188 -0
- package/dist/runtime/heapScene/freelist.js.map +1 -0
- package/dist/runtime/heapScene/growBuffer.d.ts +23 -2
- package/dist/runtime/heapScene/growBuffer.d.ts.map +1 -1
- package/dist/runtime/heapScene/growBuffer.js +35 -3
- package/dist/runtime/heapScene/growBuffer.js.map +1 -1
- package/dist/runtime/heapScene/pools.d.ts +84 -42
- package/dist/runtime/heapScene/pools.d.ts.map +1 -1
- package/dist/runtime/heapScene/pools.js +309 -134
- package/dist/runtime/heapScene/pools.js.map +1 -1
- package/dist/runtime/heapScene.d.ts +24 -0
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +1158 -92
- package/dist/runtime/heapScene.js.map +1 -1
- package/dist/runtime/hybridScene.d.ts +15 -0
- package/dist/runtime/hybridScene.d.ts.map +1 -1
- package/dist/runtime/hybridScene.js +16 -0
- package/dist/runtime/hybridScene.js.map +1 -1
- package/dist/runtime/index.d.ts +4 -3
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +4 -3
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/renderTask.d.ts +5 -0
- package/dist/runtime/renderTask.d.ts.map +1 -1
- package/dist/runtime/renderTask.js +1 -0
- package/dist/runtime/renderTask.js.map +1 -1
- package/dist/runtime/runtime.d.ts +6 -0
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +1 -0
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/textureAtlas/atlasPool.d.ts +12 -0
- package/dist/runtime/textureAtlas/atlasPool.d.ts.map +1 -1
- package/dist/runtime/textureAtlas/atlasPool.js +20 -0
- package/dist/runtime/textureAtlas/atlasPool.js.map +1 -1
- package/package.json +1 -1
- package/src/core/renderObject.ts +16 -0
- package/src/runtime/derivedModes/kernelCodegen.ts +673 -0
- package/src/runtime/derivedModes/modeKeyCpu.ts +22 -60
- package/src/runtime/derivedModes/partitionDispatcher.ts +349 -0
- package/src/runtime/derivedModes/partitionKernelLayout.ts +25 -0
- package/src/runtime/derivedModes/rule.ts +66 -145
- package/src/runtime/derivedUniforms/dispatch.ts +48 -21
- package/src/runtime/derivedUniforms/marker.ts +210 -16
- package/src/runtime/derivedUniforms/sceneIntegration.ts +67 -20
- package/src/runtime/heapAdapter.ts +17 -0
- package/src/runtime/heapEligibility.ts +22 -1
- package/src/runtime/heapScene/chunkedArena.ts +238 -0
- package/src/runtime/heapScene/freelist.ts +196 -0
- package/src/runtime/heapScene/growBuffer.ts +39 -2
- package/src/runtime/heapScene/pools.ts +350 -143
- package/src/runtime/heapScene.ts +1350 -88
- package/src/runtime/hybridScene.ts +31 -0
- package/src/runtime/index.ts +12 -9
- package/src/runtime/renderTask.ts +6 -0
- package/src/runtime/runtime.ts +7 -0
- package/src/runtime/textureAtlas/atlasPool.ts +19 -0
- package/src/runtime/derivedModes/gpuDispatcher.ts +0 -226
- package/src/runtime/derivedModes/gpuKernel.ts +0 -98
|
@@ -46,27 +46,6 @@ export interface RoModeRules {
|
|
|
46
46
|
readonly alphaToCoverage?: DerivedModeRule<"alphaToCoverage">;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
/**
|
|
50
|
-
* Uniforms accessor exposed to a derived-mode rule's `(u, declared)`
|
|
51
|
-
* closure. Reads each `u.<Name>` from the supplied aval map at
|
|
52
|
-
* evaluation time and tracks which avals were accessed so the
|
|
53
|
-
* `ModeKeyTracker` can subscribe to them.
|
|
54
|
-
*/
|
|
55
|
-
function uniformsProxy(
|
|
56
|
-
uniformAvals: ReadonlyMap<string, aval<unknown>>,
|
|
57
|
-
tok: AdaptiveToken,
|
|
58
|
-
recordDep: ((av: aval<unknown>) => void) | undefined,
|
|
59
|
-
): Record<string, unknown> {
|
|
60
|
-
return new Proxy({}, {
|
|
61
|
-
get(_target, prop): unknown {
|
|
62
|
-
if (typeof prop !== "string") return undefined;
|
|
63
|
-
const av = uniformAvals.get(prop);
|
|
64
|
-
if (av === undefined) return undefined;
|
|
65
|
-
if (recordDep !== undefined) recordDep(av);
|
|
66
|
-
return av.getValue(tok);
|
|
67
|
-
},
|
|
68
|
-
}) as Record<string, unknown>;
|
|
69
|
-
}
|
|
70
49
|
|
|
71
50
|
/**
|
|
72
51
|
* One-shot snapshot of the current values of every leaf aval in a
|
|
@@ -117,41 +96,13 @@ function applyRules(
|
|
|
117
96
|
readonly recordDep?: (av: aval<unknown>) => void;
|
|
118
97
|
},
|
|
119
98
|
): PipelineStateDescriptor {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const v = rules.cull.evaluate(proxy, out.cullMode);
|
|
128
|
-
if (v !== out.cullMode) out = { ...out, cullMode: v };
|
|
129
|
-
}
|
|
130
|
-
if (rules.frontFace !== undefined) {
|
|
131
|
-
const v = rules.frontFace.evaluate(proxy, out.frontFace);
|
|
132
|
-
if (v !== out.frontFace) out = { ...out, frontFace: v };
|
|
133
|
-
}
|
|
134
|
-
if (rules.topology !== undefined) {
|
|
135
|
-
const v = rules.topology.evaluate(proxy, out.topology);
|
|
136
|
-
if (v !== out.topology) out = { ...out, topology: v, stripIndexFormat: stripFormatFor(v) };
|
|
137
|
-
}
|
|
138
|
-
if (rules.alphaToCoverage !== undefined) {
|
|
139
|
-
const v = rules.alphaToCoverage.evaluate(proxy, out.alphaToCoverage);
|
|
140
|
-
if (v !== out.alphaToCoverage) out = { ...out, alphaToCoverage: v };
|
|
141
|
-
}
|
|
142
|
-
if (out.depth !== undefined && (rules.depthCompare !== undefined || rules.depthWrite !== undefined)) {
|
|
143
|
-
let depthOut = out.depth;
|
|
144
|
-
if (rules.depthCompare !== undefined) {
|
|
145
|
-
const v = rules.depthCompare.evaluate(proxy, depthOut.compare);
|
|
146
|
-
if (v !== depthOut.compare) depthOut = { ...depthOut, compare: v };
|
|
147
|
-
}
|
|
148
|
-
if (rules.depthWrite !== undefined) {
|
|
149
|
-
const v = rules.depthWrite.evaluate(proxy, depthOut.write);
|
|
150
|
-
if (v !== depthOut.write) depthOut = { ...depthOut, write: v };
|
|
151
|
-
}
|
|
152
|
-
if (depthOut !== out.depth) out = { ...out, depth: depthOut };
|
|
153
|
-
}
|
|
154
|
-
return out;
|
|
99
|
+
// Phase 5c.3 — rules are IR-traced and run on the GPU. We never
|
|
100
|
+
// CPU-evaluate them here; the descriptor returned reflects the
|
|
101
|
+
// raw PipelineState aval values (= the "declared" value for each
|
|
102
|
+
// axis). The heap runtime creates one bucket slot per
|
|
103
|
+
// `rule.domain` entry and the partition kernel routes per record.
|
|
104
|
+
void options;
|
|
105
|
+
return d;
|
|
155
106
|
}
|
|
156
107
|
|
|
157
108
|
function buildAttachmentsFor(
|
|
@@ -289,10 +240,21 @@ export class ModeKeyTracker implements IDisposable {
|
|
|
289
240
|
}
|
|
290
241
|
if (ps.alphaToCoverage !== undefined) visit(ps.alphaToCoverage);
|
|
291
242
|
}
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
243
|
+
// Phase 5c.3: rule-input deps will be walked from the rule's
|
|
244
|
+
// shader-IR body (analyseInputUniforms — pending M6/M7 wire-up).
|
|
245
|
+
// For now we only visit `declared` if it's an aval — leaves
|
|
246
|
+
// referenced from the rule body don't trigger partition re-
|
|
247
|
+
// dispatch until the heap side reads the IR. Hidden behind the
|
|
248
|
+
// initGpuRouting stub that throws on attached rules.
|
|
249
|
+
if (this.modeRules !== undefined) {
|
|
250
|
+
for (const rule of Object.values(this.modeRules) as DerivedModeRule[]) {
|
|
251
|
+
if (rule === undefined) continue;
|
|
252
|
+
const d = rule.declared;
|
|
253
|
+
if (typeof d === "object" && d !== null && "getValue" in (d as object)) {
|
|
254
|
+
visit(d as aval<unknown>);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
296
258
|
for (const av of this.ruleDeps) visit(av);
|
|
297
259
|
}
|
|
298
260
|
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
// Phase 5c.3 — per-bucket partition dispatcher.
|
|
2
|
+
//
|
|
3
|
+
// Owns the master record buffer + per-slot atomic counters + the
|
|
4
|
+
// compute pipelines. One instance per GPU-routed bucket. The WGSL
|
|
5
|
+
// kernel is codegen'd by `emitPartitionKernel(...)` upstream (in
|
|
6
|
+
// heapScene) and handed to the dispatcher as a string — declared
|
|
7
|
+
// values are baked in as Consts, so a `declared`-aval mark
|
|
8
|
+
// triggers `rebuildKernel(newWgsl)` to swap to a freshly-codegen'd
|
|
9
|
+
// kernel for the new declared.
|
|
10
|
+
//
|
|
11
|
+
// `dispatch(enc)` encodes:
|
|
12
|
+
// 1. clear pass (zero every slot's atomic counter)
|
|
13
|
+
// 2. partition pass (one thread per record; evaluates the per-
|
|
14
|
+
// axis rule, scatters into the matching slot via the kernel's
|
|
15
|
+
// embedded SLOT_LOOKUP table)
|
|
16
|
+
//
|
|
17
|
+
// After dispatch the existing scan kernel runs per slot, reading the
|
|
18
|
+
// slot's drawTable that the partition just populated.
|
|
19
|
+
|
|
20
|
+
import { partitionRecordU32, partitionRecordBytes, PARTITION_RECORD_PREFIX_U32 } from "./partitionKernelLayout.js";
|
|
21
|
+
|
|
22
|
+
const WG_SIZE = 64;
|
|
23
|
+
const POW2 = (n: number): number => { let p = 1; while (p < n) p <<= 1; return Math.max(64, p); };
|
|
24
|
+
|
|
25
|
+
export interface PartitionSceneSpec {
|
|
26
|
+
/** Total slot count = `resolved.length` (for the single-axis v1). */
|
|
27
|
+
readonly totalSlots: number;
|
|
28
|
+
/** Externally-owned slot draw buffers (one per slot). */
|
|
29
|
+
readonly slotDrawBufs: ReadonlyArray<GPUBuffer>;
|
|
30
|
+
/** Initial record capacity. */
|
|
31
|
+
readonly initialRecords?: number;
|
|
32
|
+
/** Codegen'd kernel WGSL. Embeds `declared` as a Const + the
|
|
33
|
+
* SLOT_LOOKUP table. Swap via `rebuildKernel(...)` on declared mark. */
|
|
34
|
+
readonly kernelWGSL: string;
|
|
35
|
+
/** Number of per-record uniform refs (= bucket.uniformOrder.length).
|
|
36
|
+
* The master record's width is `6 + numUniforms` u32s. Grow with
|
|
37
|
+
* `growUniforms(...)` when a new rule reads a uniform not yet in
|
|
38
|
+
* the bucket's order. */
|
|
39
|
+
readonly numUniforms: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class GpuPartitionScene {
|
|
43
|
+
readonly device: GPUDevice;
|
|
44
|
+
readonly label: string;
|
|
45
|
+
get totalSlots(): number { return this._totalSlots; }
|
|
46
|
+
|
|
47
|
+
masterBuf: GPUBuffer;
|
|
48
|
+
masterShadow: Uint32Array;
|
|
49
|
+
numRecords = 0;
|
|
50
|
+
capacity = 0;
|
|
51
|
+
|
|
52
|
+
slotCountBufs: GPUBuffer[];
|
|
53
|
+
slotDrawBufs: GPUBuffer[];
|
|
54
|
+
// `totalSlots` is mutable — `growSlots(...)` extends both the
|
|
55
|
+
// dispatcher and the kernel layout to accommodate new pipeline
|
|
56
|
+
// slots that appear when a new rule is registered on the bucket.
|
|
57
|
+
private _totalSlots: number;
|
|
58
|
+
// Per-bucket master record width = 6 + numUniforms. Mutable —
|
|
59
|
+
// `growUniforms(...)` widens existing records in-place so a rule
|
|
60
|
+
// reading a new uniform can land without rebuilding the master.
|
|
61
|
+
private _numUniforms: number;
|
|
62
|
+
get numUniforms(): number { return this._numUniforms; }
|
|
63
|
+
get recordU32(): number { return partitionRecordU32(this._numUniforms); }
|
|
64
|
+
get recordBytes(): number { return partitionRecordBytes(this._numUniforms); }
|
|
65
|
+
|
|
66
|
+
/** params: [numRecords] — single u32 (padded to 16 bytes for WGSL
|
|
67
|
+
* uniform alignment). `declared` is no longer carried here; the
|
|
68
|
+
* kernel WGSL bakes it as a Const at codegen time. */
|
|
69
|
+
paramsBuf: GPUBuffer;
|
|
70
|
+
|
|
71
|
+
private kernelWGSL: string;
|
|
72
|
+
|
|
73
|
+
private bindGroup: GPUBindGroup | null = null;
|
|
74
|
+
private layout: GPUBindGroupLayout | null = null;
|
|
75
|
+
private clearPipeline: GPUComputePipeline | null = null;
|
|
76
|
+
private partitionPipeline: GPUComputePipeline | null = null;
|
|
77
|
+
|
|
78
|
+
constructor(device: GPUDevice, label: string, spec: PartitionSceneSpec) {
|
|
79
|
+
if (spec.slotDrawBufs.length !== spec.totalSlots) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`GpuPartitionScene: slotDrawBufs.length (${spec.slotDrawBufs.length}) != totalSlots (${spec.totalSlots})`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
this.device = device;
|
|
85
|
+
this.label = label;
|
|
86
|
+
this._totalSlots = spec.totalSlots;
|
|
87
|
+
this._numUniforms = spec.numUniforms;
|
|
88
|
+
this.capacity = POW2(spec.initialRecords ?? 16);
|
|
89
|
+
const bytes = this.capacity * partitionRecordBytes(this._numUniforms);
|
|
90
|
+
this.masterBuf = device.createBuffer({
|
|
91
|
+
label: `${label}/master`, size: bytes,
|
|
92
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
93
|
+
});
|
|
94
|
+
this.masterShadow = new Uint32Array(bytes / 4);
|
|
95
|
+
this.slotCountBufs = new Array(spec.totalSlots).fill(0).map((_, i) =>
|
|
96
|
+
device.createBuffer({
|
|
97
|
+
label: `${label}/slot${i}Count`, size: 4,
|
|
98
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
this.slotDrawBufs = [...spec.slotDrawBufs];
|
|
102
|
+
this.paramsBuf = device.createBuffer({
|
|
103
|
+
label: `${label}/params`, size: 16,
|
|
104
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
105
|
+
});
|
|
106
|
+
this.kernelWGSL = spec.kernelWGSL;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Swap to a freshly-codegen'd kernel WGSL string. Triggered when
|
|
110
|
+
* `declared` (baked as a Const in the kernel) changes — the
|
|
111
|
+
* pipelines + bind group are reset so the next dispatch
|
|
112
|
+
* recompiles. The master records, slot count buffers and slot
|
|
113
|
+
* draw buffers are kept. */
|
|
114
|
+
rebuildKernel(kernelWGSL: string): void {
|
|
115
|
+
this.kernelWGSL = kernelWGSL;
|
|
116
|
+
this.clearPipeline = null;
|
|
117
|
+
this.partitionPipeline = null;
|
|
118
|
+
// layout shape is invariant across declared values (slot count
|
|
119
|
+
// is fixed for a given bucket); keep it.
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
rebindSlotDrawBufs(slotDraws: ReadonlyArray<GPUBuffer>): void {
|
|
123
|
+
if (slotDraws.length !== this._totalSlots) {
|
|
124
|
+
throw new Error(`rebindSlotDrawBufs: expected ${this._totalSlots} buffers, got ${slotDraws.length}`);
|
|
125
|
+
}
|
|
126
|
+
this.slotDrawBufs = [...slotDraws];
|
|
127
|
+
this.bindGroup = null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Grow the bucket's slot count when a new rule registers and the
|
|
131
|
+
* union of its outputs introduces fresh slots. Allocates the
|
|
132
|
+
* additional atomic-count buffers and discards the bind-group
|
|
133
|
+
* layout (different N = different binding shape). The caller
|
|
134
|
+
* must follow up with `rebuildKernel(...)` so the new kernel
|
|
135
|
+
* knows about the new slot count. */
|
|
136
|
+
growSlots(newCount: number, newSlotDraws: ReadonlyArray<GPUBuffer>): void {
|
|
137
|
+
if (newCount < this._totalSlots) {
|
|
138
|
+
throw new Error(`growSlots: cannot shrink (have ${this._totalSlots}, asked for ${newCount})`);
|
|
139
|
+
}
|
|
140
|
+
if (newSlotDraws.length !== newCount) {
|
|
141
|
+
throw new Error(`growSlots: slotDraws.length ${newSlotDraws.length} != newCount ${newCount}`);
|
|
142
|
+
}
|
|
143
|
+
for (let i = this._totalSlots; i < newCount; i++) {
|
|
144
|
+
this.slotCountBufs.push(this.device.createBuffer({
|
|
145
|
+
label: `${this.label}/slot${i}Count`, size: 4,
|
|
146
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
this._totalSlots = newCount;
|
|
150
|
+
this.slotDrawBufs = [...newSlotDraws];
|
|
151
|
+
this.layout = null;
|
|
152
|
+
this.clearPipeline = null;
|
|
153
|
+
this.partitionPipeline = null;
|
|
154
|
+
this.bindGroup = null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private ensurePipelines(): void {
|
|
158
|
+
if (this.layout === null) {
|
|
159
|
+
const entries: GPUBindGroupLayoutEntry[] = [
|
|
160
|
+
{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, // arena
|
|
161
|
+
{ binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, // master
|
|
162
|
+
{ binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } }, // params
|
|
163
|
+
];
|
|
164
|
+
let nb = 3;
|
|
165
|
+
for (let i = 0; i < this.totalSlots; i++) {
|
|
166
|
+
entries.push({ binding: nb + i, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } });
|
|
167
|
+
}
|
|
168
|
+
nb += this.totalSlots;
|
|
169
|
+
for (let i = 0; i < this.totalSlots; i++) {
|
|
170
|
+
entries.push({ binding: nb + i, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } });
|
|
171
|
+
}
|
|
172
|
+
this.layout = this.device.createBindGroupLayout({ label: `${this.label}/bgl`, entries });
|
|
173
|
+
}
|
|
174
|
+
if (this.clearPipeline === null || this.partitionPipeline === null) {
|
|
175
|
+
const module = this.device.createShaderModule({
|
|
176
|
+
code: this.kernelWGSL,
|
|
177
|
+
label: `${this.label}/module`,
|
|
178
|
+
});
|
|
179
|
+
const plLayout = this.device.createPipelineLayout({ bindGroupLayouts: [this.layout!] });
|
|
180
|
+
this.clearPipeline = this.device.createComputePipeline({
|
|
181
|
+
label: `${this.label}/clear`, layout: plLayout,
|
|
182
|
+
compute: { module, entryPoint: "clear" },
|
|
183
|
+
});
|
|
184
|
+
this.partitionPipeline = this.device.createComputePipeline({
|
|
185
|
+
label: `${this.label}/partition`, layout: plLayout,
|
|
186
|
+
compute: { module, entryPoint: "partitionRecords" },
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private ensureBindGroup(arenaBuf: GPUBuffer): void {
|
|
192
|
+
this.ensurePipelines();
|
|
193
|
+
if (this.bindGroup === null) {
|
|
194
|
+
const entries: GPUBindGroupEntry[] = [
|
|
195
|
+
{ binding: 0, resource: { buffer: arenaBuf } },
|
|
196
|
+
{ binding: 1, resource: { buffer: this.masterBuf } },
|
|
197
|
+
{ binding: 2, resource: { buffer: this.paramsBuf } },
|
|
198
|
+
];
|
|
199
|
+
let nb = 3;
|
|
200
|
+
for (let i = 0; i < this.totalSlots; i++) {
|
|
201
|
+
entries.push({ binding: nb + i, resource: { buffer: this.slotCountBufs[i]! } });
|
|
202
|
+
}
|
|
203
|
+
nb += this.totalSlots;
|
|
204
|
+
for (let i = 0; i < this.totalSlots; i++) {
|
|
205
|
+
entries.push({ binding: nb + i, resource: { buffer: this.slotDrawBufs[i]! } });
|
|
206
|
+
}
|
|
207
|
+
this.bindGroup = this.device.createBindGroup({
|
|
208
|
+
label: `${this.label}/bg`,
|
|
209
|
+
layout: this.layout!,
|
|
210
|
+
entries,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
appendRecord(
|
|
216
|
+
drawIdx: number,
|
|
217
|
+
indexStart: number,
|
|
218
|
+
indexCount: number,
|
|
219
|
+
instanceCount: number,
|
|
220
|
+
comboId: number,
|
|
221
|
+
uniformRefs: ReadonlyArray<number>,
|
|
222
|
+
): number {
|
|
223
|
+
if (uniformRefs.length !== this._numUniforms) {
|
|
224
|
+
throw new Error(`GpuPartitionScene/appendRecord: expected ${this._numUniforms} uniform refs, got ${uniformRefs.length}`);
|
|
225
|
+
}
|
|
226
|
+
if (this.numRecords >= this.capacity) this.grow();
|
|
227
|
+
const i = this.numRecords;
|
|
228
|
+
const ru32 = this.recordU32;
|
|
229
|
+
const o = i * ru32;
|
|
230
|
+
this.masterShadow[o + 0] = 0;
|
|
231
|
+
this.masterShadow[o + 1] = drawIdx;
|
|
232
|
+
this.masterShadow[o + 2] = indexStart;
|
|
233
|
+
this.masterShadow[o + 3] = indexCount;
|
|
234
|
+
this.masterShadow[o + 4] = instanceCount;
|
|
235
|
+
this.masterShadow[o + 5] = comboId >>> 0;
|
|
236
|
+
for (let k = 0; k < this._numUniforms; k++) {
|
|
237
|
+
this.masterShadow[o + PARTITION_RECORD_PREFIX_U32 + k] = (uniformRefs[k] ?? 0) >>> 0;
|
|
238
|
+
}
|
|
239
|
+
this.numRecords = i + 1;
|
|
240
|
+
return i;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Update an existing record's comboId. Used when an RO already
|
|
244
|
+
* in the master is re-classified (rare; only when rule rebucket
|
|
245
|
+
* fires on aval marks). */
|
|
246
|
+
setRecordComboId(recordIdx: number, comboId: number): void {
|
|
247
|
+
if (recordIdx < 0 || recordIdx >= this.numRecords) return;
|
|
248
|
+
this.masterShadow[recordIdx * this.recordU32 + 5] = comboId >>> 0;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Update an existing record's uniform ref by uniform index. */
|
|
252
|
+
setRecordUniformRef(recordIdx: number, uniformIdx: number, ref: number): void {
|
|
253
|
+
if (recordIdx < 0 || recordIdx >= this.numRecords) return;
|
|
254
|
+
if (uniformIdx < 0 || uniformIdx >= this._numUniforms) return;
|
|
255
|
+
this.masterShadow[recordIdx * this.recordU32 + PARTITION_RECORD_PREFIX_U32 + uniformIdx] = ref >>> 0;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
removeRecord(recordIdx: number): number {
|
|
259
|
+
if (recordIdx < 0 || recordIdx >= this.numRecords) return -1;
|
|
260
|
+
const last = this.numRecords - 1;
|
|
261
|
+
const ru32 = this.recordU32;
|
|
262
|
+
if (recordIdx !== last) {
|
|
263
|
+
const dst = recordIdx * ru32;
|
|
264
|
+
const src = last * ru32;
|
|
265
|
+
for (let k = 0; k < ru32; k++) {
|
|
266
|
+
this.masterShadow[dst + k] = this.masterShadow[src + k]!;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
this.numRecords = last;
|
|
270
|
+
return recordIdx !== last ? last : -1;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Widen the per-record uniform tail from `_numUniforms` to
|
|
274
|
+
* `newNumUniforms`. Existing records are restrided in-place; the
|
|
275
|
+
* new tail slots are zero-filled (rules added later that read
|
|
276
|
+
* the new uniform will only do so under their own combo, where
|
|
277
|
+
* records carry real refs). The kernel WGSL must be rebuilt
|
|
278
|
+
* separately (`rebuildKernel`). */
|
|
279
|
+
growUniforms(newNumUniforms: number): void {
|
|
280
|
+
if (newNumUniforms <= this._numUniforms) return;
|
|
281
|
+
const oldU32 = partitionRecordU32(this._numUniforms);
|
|
282
|
+
const newU32 = partitionRecordU32(newNumUniforms);
|
|
283
|
+
const newBytes = this.capacity * newU32 * 4;
|
|
284
|
+
const grown = new Uint32Array(newBytes / 4);
|
|
285
|
+
// Restride existing records into the wider layout.
|
|
286
|
+
for (let i = 0; i < this.numRecords; i++) {
|
|
287
|
+
const oldOff = i * oldU32;
|
|
288
|
+
const newOff = i * newU32;
|
|
289
|
+
for (let k = 0; k < oldU32; k++) grown[newOff + k] = this.masterShadow[oldOff + k]!;
|
|
290
|
+
// Trailing newU32-oldU32 slots stay 0.
|
|
291
|
+
}
|
|
292
|
+
this.masterShadow = grown;
|
|
293
|
+
this.masterBuf.destroy();
|
|
294
|
+
this.masterBuf = this.device.createBuffer({
|
|
295
|
+
label: `${this.label}/master`, size: newBytes,
|
|
296
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
297
|
+
});
|
|
298
|
+
this._numUniforms = newNumUniforms;
|
|
299
|
+
this.bindGroup = null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private grow(): void {
|
|
303
|
+
const newCap = POW2(this.numRecords + 1);
|
|
304
|
+
const bytes = newCap * this.recordBytes;
|
|
305
|
+
const grown = new Uint32Array(bytes / 4);
|
|
306
|
+
grown.set(this.masterShadow);
|
|
307
|
+
this.masterShadow = grown;
|
|
308
|
+
this.masterBuf.destroy();
|
|
309
|
+
this.masterBuf = this.device.createBuffer({
|
|
310
|
+
label: `${this.label}/master`, size: bytes,
|
|
311
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
312
|
+
});
|
|
313
|
+
this.capacity = newCap;
|
|
314
|
+
this.bindGroup = null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
flush(): void {
|
|
318
|
+
if (this.numRecords === 0) return;
|
|
319
|
+
const bytes = this.numRecords * this.recordBytes;
|
|
320
|
+
this.device.queue.writeBuffer(
|
|
321
|
+
this.masterBuf, 0,
|
|
322
|
+
this.masterShadow.buffer, this.masterShadow.byteOffset, bytes,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Encode clear + partition into `enc`. */
|
|
327
|
+
dispatch(arenaBuf: GPUBuffer, enc: GPUCommandEncoder): void {
|
|
328
|
+
this.ensureBindGroup(arenaBuf);
|
|
329
|
+
this.device.queue.writeBuffer(
|
|
330
|
+
this.paramsBuf, 0,
|
|
331
|
+
new Uint32Array([this.numRecords, 0, 0, 0]).buffer, 0, 16,
|
|
332
|
+
);
|
|
333
|
+
const pass = enc.beginComputePass({ label: `${this.label}/pass` });
|
|
334
|
+
pass.setBindGroup(0, this.bindGroup!);
|
|
335
|
+
pass.setPipeline(this.clearPipeline!);
|
|
336
|
+
pass.dispatchWorkgroups(1);
|
|
337
|
+
if (this.numRecords > 0) {
|
|
338
|
+
pass.setPipeline(this.partitionPipeline!);
|
|
339
|
+
pass.dispatchWorkgroups(Math.ceil(this.numRecords / WG_SIZE));
|
|
340
|
+
}
|
|
341
|
+
pass.end();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
dispose(): void {
|
|
345
|
+
this.masterBuf.destroy();
|
|
346
|
+
for (const b of this.slotCountBufs) b.destroy();
|
|
347
|
+
this.paramsBuf.destroy();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Partition kernel record layout — shared by the codegen + dispatcher.
|
|
2
|
+
//
|
|
3
|
+
// Each record in the master buffer has a FIXED 6-u32 prefix and a
|
|
4
|
+
// per-bucket variable tail of K uniform refs:
|
|
5
|
+
//
|
|
6
|
+
// [firstEmit, drawIdx, indexStart, indexCount, instanceCount, comboId,
|
|
7
|
+
// ref0, ref1, …, ref{K-1}]
|
|
8
|
+
//
|
|
9
|
+
// `firstEmit` is reserved (GPU-written by the downstream scan); CPU
|
|
10
|
+
// writes 0. `comboId` selects which combo of axis-rules the partition
|
|
11
|
+
// kernel evaluates — a combo is a per-RO choice of (at most one) rule
|
|
12
|
+
// per mode axis. Each `refI` is the per-RO arena byte offset of the
|
|
13
|
+
// I-th uniform appearing across all rules in the bucket
|
|
14
|
+
// (`bucket.uniformOrder`). The kernel reads `r.refI` for whichever
|
|
15
|
+
// uniform a given rule body declares via `ReadInput("Uniform", name)`
|
|
16
|
+
// — the codegen translates name → refI lookup.
|
|
17
|
+
|
|
18
|
+
export const PARTITION_RECORD_PREFIX_U32 = 6;
|
|
19
|
+
|
|
20
|
+
export function partitionRecordU32(numUniforms: number): number {
|
|
21
|
+
return PARTITION_RECORD_PREFIX_U32 + numUniforms;
|
|
22
|
+
}
|
|
23
|
+
export function partitionRecordBytes(numUniforms: number): number {
|
|
24
|
+
return partitionRecordU32(numUniforms) * 4;
|
|
25
|
+
}
|