@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.
Files changed (107) hide show
  1. package/dist/core/renderObject.d.ts +16 -0
  2. package/dist/core/renderObject.d.ts.map +1 -1
  3. package/dist/runtime/derivedModes/kernelCodegen.d.ts +90 -0
  4. package/dist/runtime/derivedModes/kernelCodegen.d.ts.map +1 -0
  5. package/dist/runtime/derivedModes/kernelCodegen.js +623 -0
  6. package/dist/runtime/derivedModes/kernelCodegen.js.map +1 -0
  7. package/dist/runtime/derivedModes/modeKeyCpu.d.ts.map +1 -1
  8. package/dist/runtime/derivedModes/modeKeyCpu.js +23 -67
  9. package/dist/runtime/derivedModes/modeKeyCpu.js.map +1 -1
  10. package/dist/runtime/derivedModes/partitionDispatcher.d.ts +79 -0
  11. package/dist/runtime/derivedModes/partitionDispatcher.d.ts.map +1 -0
  12. package/dist/runtime/derivedModes/partitionDispatcher.js +303 -0
  13. package/dist/runtime/derivedModes/partitionDispatcher.js.map +1 -0
  14. package/dist/runtime/derivedModes/partitionKernel.d.ts +12 -0
  15. package/dist/runtime/derivedModes/partitionKernel.d.ts.map +1 -0
  16. package/dist/runtime/derivedModes/partitionKernel.js +127 -0
  17. package/dist/runtime/derivedModes/partitionKernel.js.map +1 -0
  18. package/dist/runtime/derivedModes/partitionKernelLayout.d.ts +4 -0
  19. package/dist/runtime/derivedModes/partitionKernelLayout.d.ts.map +1 -0
  20. package/dist/runtime/derivedModes/partitionKernelLayout.js +24 -0
  21. package/dist/runtime/derivedModes/partitionKernelLayout.js.map +1 -0
  22. package/dist/runtime/derivedModes/rule.d.ts +38 -86
  23. package/dist/runtime/derivedModes/rule.d.ts.map +1 -1
  24. package/dist/runtime/derivedModes/rule.js +34 -74
  25. package/dist/runtime/derivedModes/rule.js.map +1 -1
  26. package/dist/runtime/derivedUniforms/dispatch.d.ts +10 -8
  27. package/dist/runtime/derivedUniforms/dispatch.d.ts.map +1 -1
  28. package/dist/runtime/derivedUniforms/dispatch.js +31 -18
  29. package/dist/runtime/derivedUniforms/dispatch.js.map +1 -1
  30. package/dist/runtime/derivedUniforms/marker.d.ts +90 -11
  31. package/dist/runtime/derivedUniforms/marker.d.ts.map +1 -1
  32. package/dist/runtime/derivedUniforms/marker.js +161 -15
  33. package/dist/runtime/derivedUniforms/marker.js.map +1 -1
  34. package/dist/runtime/derivedUniforms/sceneIntegration.d.ts +25 -6
  35. package/dist/runtime/derivedUniforms/sceneIntegration.d.ts.map +1 -1
  36. package/dist/runtime/derivedUniforms/sceneIntegration.js +61 -20
  37. package/dist/runtime/derivedUniforms/sceneIntegration.js.map +1 -1
  38. package/dist/runtime/heapAdapter.d.ts.map +1 -1
  39. package/dist/runtime/heapAdapter.js +17 -0
  40. package/dist/runtime/heapAdapter.js.map +1 -1
  41. package/dist/runtime/heapEligibility.d.ts.map +1 -1
  42. package/dist/runtime/heapEligibility.js +22 -2
  43. package/dist/runtime/heapEligibility.js.map +1 -1
  44. package/dist/runtime/heapScene/chunkedArena.d.ts +76 -0
  45. package/dist/runtime/heapScene/chunkedArena.d.ts.map +1 -0
  46. package/dist/runtime/heapScene/chunkedArena.js +223 -0
  47. package/dist/runtime/heapScene/chunkedArena.js.map +1 -0
  48. package/dist/runtime/heapScene/freelist.d.ts +55 -0
  49. package/dist/runtime/heapScene/freelist.d.ts.map +1 -0
  50. package/dist/runtime/heapScene/freelist.js +188 -0
  51. package/dist/runtime/heapScene/freelist.js.map +1 -0
  52. package/dist/runtime/heapScene/growBuffer.d.ts +23 -2
  53. package/dist/runtime/heapScene/growBuffer.d.ts.map +1 -1
  54. package/dist/runtime/heapScene/growBuffer.js +35 -3
  55. package/dist/runtime/heapScene/growBuffer.js.map +1 -1
  56. package/dist/runtime/heapScene/pools.d.ts +84 -42
  57. package/dist/runtime/heapScene/pools.d.ts.map +1 -1
  58. package/dist/runtime/heapScene/pools.js +309 -134
  59. package/dist/runtime/heapScene/pools.js.map +1 -1
  60. package/dist/runtime/heapScene.d.ts +24 -0
  61. package/dist/runtime/heapScene.d.ts.map +1 -1
  62. package/dist/runtime/heapScene.js +1158 -92
  63. package/dist/runtime/heapScene.js.map +1 -1
  64. package/dist/runtime/hybridScene.d.ts +15 -0
  65. package/dist/runtime/hybridScene.d.ts.map +1 -1
  66. package/dist/runtime/hybridScene.js +16 -0
  67. package/dist/runtime/hybridScene.js.map +1 -1
  68. package/dist/runtime/index.d.ts +4 -3
  69. package/dist/runtime/index.d.ts.map +1 -1
  70. package/dist/runtime/index.js +4 -3
  71. package/dist/runtime/index.js.map +1 -1
  72. package/dist/runtime/renderTask.d.ts +5 -0
  73. package/dist/runtime/renderTask.d.ts.map +1 -1
  74. package/dist/runtime/renderTask.js +1 -0
  75. package/dist/runtime/renderTask.js.map +1 -1
  76. package/dist/runtime/runtime.d.ts +6 -0
  77. package/dist/runtime/runtime.d.ts.map +1 -1
  78. package/dist/runtime/runtime.js +1 -0
  79. package/dist/runtime/runtime.js.map +1 -1
  80. package/dist/runtime/textureAtlas/atlasPool.d.ts +12 -0
  81. package/dist/runtime/textureAtlas/atlasPool.d.ts.map +1 -1
  82. package/dist/runtime/textureAtlas/atlasPool.js +20 -0
  83. package/dist/runtime/textureAtlas/atlasPool.js.map +1 -1
  84. package/package.json +1 -1
  85. package/src/core/renderObject.ts +16 -0
  86. package/src/runtime/derivedModes/kernelCodegen.ts +673 -0
  87. package/src/runtime/derivedModes/modeKeyCpu.ts +22 -60
  88. package/src/runtime/derivedModes/partitionDispatcher.ts +349 -0
  89. package/src/runtime/derivedModes/partitionKernelLayout.ts +25 -0
  90. package/src/runtime/derivedModes/rule.ts +66 -145
  91. package/src/runtime/derivedUniforms/dispatch.ts +48 -21
  92. package/src/runtime/derivedUniforms/marker.ts +210 -16
  93. package/src/runtime/derivedUniforms/sceneIntegration.ts +67 -20
  94. package/src/runtime/heapAdapter.ts +17 -0
  95. package/src/runtime/heapEligibility.ts +22 -1
  96. package/src/runtime/heapScene/chunkedArena.ts +238 -0
  97. package/src/runtime/heapScene/freelist.ts +196 -0
  98. package/src/runtime/heapScene/growBuffer.ts +39 -2
  99. package/src/runtime/heapScene/pools.ts +350 -143
  100. package/src/runtime/heapScene.ts +1350 -88
  101. package/src/runtime/hybridScene.ts +31 -0
  102. package/src/runtime/index.ts +12 -9
  103. package/src/runtime/renderTask.ts +6 -0
  104. package/src/runtime/runtime.ts +7 -0
  105. package/src/runtime/textureAtlas/atlasPool.ts +19 -0
  106. package/src/runtime/derivedModes/gpuDispatcher.ts +0 -226
  107. 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
- const rules = options.modeRules;
121
- if (rules === undefined) return d;
122
- const uniforms = options.uniformAvals ?? new Map();
123
- const tok = options.token ?? AdaptiveToken.top;
124
- const proxy = uniformsProxy(uniforms, tok, options.recordDep);
125
- let out = d;
126
- if (rules.cull !== undefined) {
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
- // Rule-input deps discovered on the last evaluation. The set is
293
- // refreshed each `recompute()`; callers re-subscribe accordingly.
294
- // Surfaced regardless of `ps` because a rule can fire without
295
- // any underlying PipelineState aval (e.g. ps === undefined).
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
+ }