@aardworx/wombat.rendering 0.8.0 → 0.9.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/command.d.ts +7 -9
- package/dist/core/command.d.ts.map +1 -1
- package/dist/core/renderTask.d.ts +64 -1
- package/dist/core/renderTask.d.ts.map +1 -1
- package/dist/core/renderTask.js +3 -1
- package/dist/core/renderTask.js.map +1 -1
- package/dist/resources/preparedRenderObject.js +3 -3
- package/dist/resources/preparedRenderObject.js.map +1 -1
- package/dist/resources/uniformBuffer.d.ts.map +1 -1
- package/dist/resources/uniformBuffer.js +17 -0
- package/dist/resources/uniformBuffer.js.map +1 -1
- package/dist/runtime/derivedUniforms/dispatch.d.ts +105 -0
- package/dist/runtime/derivedUniforms/dispatch.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/dispatch.js +355 -0
- package/dist/runtime/derivedUniforms/dispatch.js.map +1 -0
- package/dist/runtime/derivedUniforms/index.d.ts +6 -0
- package/dist/runtime/derivedUniforms/index.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/index.js +26 -0
- package/dist/runtime/derivedUniforms/index.js.map +1 -0
- package/dist/runtime/derivedUniforms/recipes.d.ts +42 -0
- package/dist/runtime/derivedUniforms/recipes.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/recipes.js +134 -0
- package/dist/runtime/derivedUniforms/recipes.js.map +1 -0
- package/dist/runtime/derivedUniforms/sceneIntegration.d.ts +56 -0
- package/dist/runtime/derivedUniforms/sceneIntegration.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/sceneIntegration.js +171 -0
- package/dist/runtime/derivedUniforms/sceneIntegration.js.map +1 -0
- package/dist/runtime/derivedUniforms/slotId.d.ts +15 -0
- package/dist/runtime/derivedUniforms/slotId.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/slotId.js +30 -0
- package/dist/runtime/derivedUniforms/slotId.js.map +1 -0
- package/dist/runtime/derivedUniforms/slots.d.ts +49 -0
- package/dist/runtime/derivedUniforms/slots.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/slots.js +123 -0
- package/dist/runtime/derivedUniforms/slots.js.map +1 -0
- package/dist/runtime/derivedUniforms/uberKernel.wgsl.d.ts +13 -0
- package/dist/runtime/derivedUniforms/uberKernel.wgsl.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/uberKernel.wgsl.js +218 -0
- package/dist/runtime/derivedUniforms/uberKernel.wgsl.js.map +1 -0
- package/dist/runtime/flattenTree.d.ts +5 -0
- package/dist/runtime/flattenTree.d.ts.map +1 -0
- package/dist/runtime/flattenTree.js +45 -0
- package/dist/runtime/flattenTree.js.map +1 -0
- package/dist/runtime/heapAdapter.d.ts +16 -0
- package/dist/runtime/heapAdapter.d.ts.map +1 -0
- package/dist/runtime/heapAdapter.js +263 -0
- package/dist/runtime/heapAdapter.js.map +1 -0
- package/dist/runtime/heapEffect.d.ts +235 -0
- package/dist/runtime/heapEffect.d.ts.map +1 -0
- package/dist/runtime/heapEffect.js +518 -0
- package/dist/runtime/heapEffect.js.map +1 -0
- package/dist/runtime/heapEffectIR.d.ts +34 -0
- package/dist/runtime/heapEffectIR.d.ts.map +1 -0
- package/dist/runtime/heapEffectIR.js +1161 -0
- package/dist/runtime/heapEffectIR.js.map +1 -0
- package/dist/runtime/heapEligibility.d.ts +9 -0
- package/dist/runtime/heapEligibility.d.ts.map +1 -0
- package/dist/runtime/heapEligibility.js +195 -0
- package/dist/runtime/heapEligibility.js.map +1 -0
- package/dist/runtime/heapScene.d.ts +240 -31
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +3334 -265
- package/dist/runtime/heapScene.js.map +1 -1
- package/dist/runtime/heapShaderFamily.d.ts +132 -0
- package/dist/runtime/heapShaderFamily.d.ts.map +1 -0
- package/dist/runtime/heapShaderFamily.js +866 -0
- package/dist/runtime/heapShaderFamily.js.map +1 -0
- package/dist/runtime/hybridScene.d.ts +133 -0
- package/dist/runtime/hybridScene.d.ts.map +1 -0
- package/dist/runtime/hybridScene.js +159 -0
- package/dist/runtime/hybridScene.js.map +1 -0
- package/dist/runtime/index.d.ts +8 -0
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +9 -0
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/renderTask.d.ts +21 -4
- package/dist/runtime/renderTask.d.ts.map +1 -1
- package/dist/runtime/renderTask.js +127 -32
- package/dist/runtime/renderTask.js.map +1 -1
- package/dist/runtime/runtime.d.ts +33 -3
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +12 -3
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/scenePass.d.ts +28 -3
- package/dist/runtime/scenePass.d.ts.map +1 -1
- package/dist/runtime/scenePass.js +37 -4
- package/dist/runtime/scenePass.js.map +1 -1
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.d.ts +35 -0
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.d.ts.map +1 -0
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.js +247 -0
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.js.map +1 -0
- package/dist/runtime/textureAtlas/atlasPool.d.ts +240 -0
- package/dist/runtime/textureAtlas/atlasPool.d.ts.map +1 -0
- package/dist/runtime/textureAtlas/atlasPool.js +798 -0
- package/dist/runtime/textureAtlas/atlasPool.js.map +1 -0
- package/dist/runtime/textureAtlas/bvhTree2d.d.ts +33 -0
- package/dist/runtime/textureAtlas/bvhTree2d.d.ts.map +1 -0
- package/dist/runtime/textureAtlas/bvhTree2d.js +501 -0
- package/dist/runtime/textureAtlas/bvhTree2d.js.map +1 -0
- package/dist/runtime/textureAtlas/packer.d.ts +50 -0
- package/dist/runtime/textureAtlas/packer.d.ts.map +1 -0
- package/dist/runtime/textureAtlas/packer.js +390 -0
- package/dist/runtime/textureAtlas/packer.js.map +1 -0
- package/dist/window/loop.d.ts +14 -0
- package/dist/window/loop.d.ts.map +1 -1
- package/dist/window/loop.js +18 -5
- package/dist/window/loop.js.map +1 -1
- package/package.json +5 -4
- package/src/core/command.ts +9 -9
- package/src/core/renderTask.ts +55 -2
- package/src/resources/preparedRenderObject.ts +3 -3
- package/src/resources/uniformBuffer.ts +15 -0
- package/src/runtime/derivedUniforms/dispatch.ts +407 -0
- package/src/runtime/derivedUniforms/index.ts +52 -0
- package/src/runtime/derivedUniforms/recipes.ts +159 -0
- package/src/runtime/derivedUniforms/sceneIntegration.ts +239 -0
- package/src/runtime/derivedUniforms/slotId.ts +35 -0
- package/src/runtime/derivedUniforms/slots.ts +155 -0
- package/src/runtime/derivedUniforms/uberKernel.wgsl.ts +220 -0
- package/src/runtime/flattenTree.ts +46 -0
- package/src/runtime/heapAdapter.ts +314 -0
- package/src/runtime/heapEffect.ts +719 -0
- package/src/runtime/heapEffectIR.ts +1211 -0
- package/src/runtime/heapEligibility.ts +189 -0
- package/src/runtime/heapScene.ts +3981 -349
- package/src/runtime/heapShaderFamily.ts +1102 -0
- package/src/runtime/hybridScene.ts +310 -0
- package/src/runtime/index.ts +46 -0
- package/src/runtime/renderTask.ts +167 -32
- package/src/runtime/runtime.ts +37 -4
- package/src/runtime/scenePass.ts +39 -4
- package/src/runtime/textureAtlas/atlasMipGutterKernel.ts +291 -0
- package/src/runtime/textureAtlas/atlasPool.ts +991 -0
- package/src/runtime/textureAtlas/bvhTree2d.ts +530 -0
- package/src/runtime/textureAtlas/packer.ts +429 -0
- package/src/window/loop.ts +32 -6
package/src/runtime/runtime.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// (eventually) `renderTo(...)`.
|
|
4
4
|
|
|
5
5
|
import type { Command, CompiledEffect, Effect, FramebufferSignature, IRenderTask, RenderTree } from "../core/index.js";
|
|
6
|
-
import type { alist } from "@aardworx/wombat.adaptive";
|
|
6
|
+
import type { alist, aval } from "@aardworx/wombat.adaptive";
|
|
7
7
|
import { compileRenderTask, type RuntimeContext } from "./renderTask.js";
|
|
8
8
|
import { renderTo, type RenderToOptions, type RenderToResult } from "./renderTo.js";
|
|
9
9
|
|
|
@@ -17,6 +17,30 @@ export interface RuntimeOptions {
|
|
|
17
17
|
* …).
|
|
18
18
|
*/
|
|
19
19
|
readonly compileEffect?: (effect: Effect, signature: FramebufferSignature) => CompiledEffect;
|
|
20
|
+
/**
|
|
21
|
+
* Global on/off toggle for the heap fast path. When `false`,
|
|
22
|
+
* every RO routes through the legacy per-RO renderer. Reactive:
|
|
23
|
+
* ROs migrate between heap and legacy subsets when the cval
|
|
24
|
+
* flips. Useful for A/B perf comparisons. Default: heap on.
|
|
25
|
+
*/
|
|
26
|
+
readonly heapEnabled?: aval<boolean>;
|
|
27
|
+
/**
|
|
28
|
+
* §6 family-merge opt-in. When `true`, all heap-eligible effects
|
|
29
|
+
* collapse into one bucket per pipelineState via layoutId-switch
|
|
30
|
+
* dispatch. Default `false`: each effect lands in its own bucket /
|
|
31
|
+
* shader / pipeline. Per-effect bucketing is at-or-better than
|
|
32
|
+
* merged on tested workloads; merge stays opt-in until the v2
|
|
33
|
+
* trace-based auto-trigger lands.
|
|
34
|
+
*/
|
|
35
|
+
readonly enableFamilyMerge?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* §7 derived-uniforms opt-in. When true, ModelView / ViewProj /
|
|
38
|
+
* ModelViewProj / NormalMatrix / *TrafoInv / etc. are produced by a
|
|
39
|
+
* df32 GPU compute pre-pass instead of being supplied per-RO by the
|
|
40
|
+
* uniform provider. ROs MUST supply ModelTrafo / ViewTrafo /
|
|
41
|
+
* ProjTrafo as `aval<Trafo3d>` for any derived they consume.
|
|
42
|
+
*/
|
|
43
|
+
readonly enableDerivedUniforms?: boolean;
|
|
20
44
|
}
|
|
21
45
|
|
|
22
46
|
/**
|
|
@@ -50,6 +74,9 @@ export class Runtime {
|
|
|
50
74
|
target: "wgsl",
|
|
51
75
|
fragmentOutputLayout: layoutFromSignature(sig),
|
|
52
76
|
})),
|
|
77
|
+
...(opts.heapEnabled !== undefined ? { heapEnabled: opts.heapEnabled } : {}),
|
|
78
|
+
...(opts.enableFamilyMerge === true ? { enableFamilyMerge: true } : {}),
|
|
79
|
+
...(opts.enableDerivedUniforms === true ? { enableDerivedUniforms: true } : {}),
|
|
53
80
|
};
|
|
54
81
|
// `device.lost` is a real-WebGPU promise; mock devices may not
|
|
55
82
|
// expose it. Treat as "never lost" in that case.
|
|
@@ -88,10 +115,16 @@ export class Runtime {
|
|
|
88
115
|
// cleanest path is to discard the old Runtime and construct a new
|
|
89
116
|
// one from the same source `alist<Command>`.
|
|
90
117
|
|
|
91
|
-
/**
|
|
92
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Compile an `alist<Command>` into a runnable `IRenderTask`. The
|
|
120
|
+
* `signature` is constant across the task's lifetime — pipelines
|
|
121
|
+
* specialise against it at compile time. Framebuffer instances are
|
|
122
|
+
* supplied at `task.run(framebuffer, token)`; their signatures
|
|
123
|
+
* must match.
|
|
124
|
+
*/
|
|
125
|
+
compile(signature: FramebufferSignature, commands: alist<Command>): IRenderTask {
|
|
93
126
|
if (this._disposed) throw new Error("Runtime: compile after disposeAll");
|
|
94
|
-
const task = compileRenderTask(this.ctx, commands);
|
|
127
|
+
const task = compileRenderTask(this.ctx, signature, commands);
|
|
95
128
|
this._tasks.add(task);
|
|
96
129
|
const origDispose = task.dispose.bind(task);
|
|
97
130
|
task.dispose = () => { origDispose(); this._tasks.delete(task); };
|
package/src/runtime/scenePass.ts
CHANGED
|
@@ -261,17 +261,52 @@ export class ScenePass {
|
|
|
261
261
|
}
|
|
262
262
|
|
|
263
263
|
/**
|
|
264
|
-
* Pull deltas from every dynamic subtree
|
|
265
|
-
* walkers
|
|
266
|
-
*
|
|
264
|
+
* Pull deltas from every dynamic subtree and splice the affected
|
|
265
|
+
* walkers. CPU-only — no GPU work. After this returns, the leaf
|
|
266
|
+
* set is current and any per-leaf prepared resources have been
|
|
267
|
+
* brought up to date. Idempotent on a clean adaptive graph.
|
|
268
|
+
*
|
|
269
|
+
* Hybrid composition: call this BEFORE opening the render pass,
|
|
270
|
+
* alongside any other backend's `update`. Then open the pass and
|
|
271
|
+
* call `encodeIntoPass`.
|
|
267
272
|
*/
|
|
268
|
-
|
|
273
|
+
update(token: AdaptiveToken): void {
|
|
269
274
|
this.root.update(token);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* The current ordered list of leaves. Reads the walker tree's
|
|
279
|
+
* cached state — does not pull deltas. Call `update(token)` first
|
|
280
|
+
* if avals may have marked since last collect.
|
|
281
|
+
*/
|
|
282
|
+
collect(): PreparedRenderObject[] {
|
|
270
283
|
const out: PreparedRenderObject[] = [];
|
|
271
284
|
this.root.emit(out);
|
|
272
285
|
return out;
|
|
273
286
|
}
|
|
274
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Encode each leaf's draw into an existing render pass. No
|
|
290
|
+
* begin/end; caller owns the pass. Hybrid renderers call this on
|
|
291
|
+
* the legacy path's pass alongside the heap path's `encodeIntoPass`.
|
|
292
|
+
*
|
|
293
|
+
* Must be preceded by `update(token)` in the same frame.
|
|
294
|
+
*/
|
|
295
|
+
encodeIntoPass(passEnc: GPURenderPassEncoder, token: AdaptiveToken): void {
|
|
296
|
+
const leaves = this.collect();
|
|
297
|
+
for (const leaf of leaves) leaf.record(passEnc, token);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Convenience: `update` + `collect`. The legacy entry point.
|
|
302
|
+
* Pre-existing callers keep working; new code paths prefer the
|
|
303
|
+
* split methods.
|
|
304
|
+
*/
|
|
305
|
+
resolve(token: AdaptiveToken): PreparedRenderObject[] {
|
|
306
|
+
this.update(token);
|
|
307
|
+
return this.collect();
|
|
308
|
+
}
|
|
309
|
+
|
|
275
310
|
dispose(): void {
|
|
276
311
|
this.root.dispose();
|
|
277
312
|
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// Compute kernel that builds an atlas sub-rect's mip pyramid AND fills
|
|
2
|
+
// both gutter rings entirely on GPU, using only core WebGPU 1.0
|
|
3
|
+
// features (no `readonly-and-readwrite-storage-textures`).
|
|
4
|
+
//
|
|
5
|
+
// Architecture
|
|
6
|
+
// ============
|
|
7
|
+
// The whole operation runs inside a per-acquire **scratch buffer**
|
|
8
|
+
// laid out exactly like the destination region of the atlas page
|
|
9
|
+
// (bounding rect of the Iliffe pyramid + gutter, in atlas-pixel
|
|
10
|
+
// coordinates relative to the sub-rect origin). Each pixel is one
|
|
11
|
+
// packed `u32` (rgba8). Rows are padded to 256-byte alignment so the
|
|
12
|
+
// final `copyBufferToTexture` is straight-through.
|
|
13
|
+
//
|
|
14
|
+
// Per acquire:
|
|
15
|
+
// 1. CPU writes the raw source pixels into the scratch buffer at
|
|
16
|
+
// the mip-0 interior offset (via `mappedAtCreation`). One JS
|
|
17
|
+
// loop, no upload validation pitfalls.
|
|
18
|
+
// 2. For each mip k > 0: dispatch the `interior` kernel, which
|
|
19
|
+
// reads 2×2 from mip-(k-1) in the buffer and writes the average
|
|
20
|
+
// to mip-k interior, all in-buffer. Each mip lives in its own
|
|
21
|
+
// compute pass — the pass boundary acts as a barrier so
|
|
22
|
+
// mip-(k-1) is fully written before mip-k reads from it.
|
|
23
|
+
// 3. For each mip k: dispatch the `gutter` kernel, which copies
|
|
24
|
+
// the appropriate clamp / wrap interior cell into each gutter
|
|
25
|
+
// cell. Also one compute pass per mip.
|
|
26
|
+
// 4. `copyBufferToTexture` writes the full scratch buffer to the
|
|
27
|
+
// atlas page at the sub-rect origin.
|
|
28
|
+
//
|
|
29
|
+
// The kernel reads and writes ONE storage buffer through a
|
|
30
|
+
// `read_write` binding. WebGPU validates a single binding (even with
|
|
31
|
+
// read_write access) as one usage entry per resource — no hazard.
|
|
32
|
+
//
|
|
33
|
+
// No device features required. No same-texture binding pitfalls. The
|
|
34
|
+
// scratch buffer is per-acquire and freed after submission completes.
|
|
35
|
+
|
|
36
|
+
const WGSL = /* wgsl */`
|
|
37
|
+
struct Params {
|
|
38
|
+
buf_stride_u32: u32,
|
|
39
|
+
_pad0: u32,
|
|
40
|
+
src_origin_in_buf: vec2<u32>,
|
|
41
|
+
src_size: vec2<u32>,
|
|
42
|
+
dst_origin_in_buf: vec2<u32>,
|
|
43
|
+
dst_size: vec2<u32>,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@group(0) @binding(0) var<storage, read_write> buf: array<u32>;
|
|
47
|
+
@group(0) @binding(1) var<uniform> P: Params;
|
|
48
|
+
|
|
49
|
+
fn loadRgba(x: u32, y: u32) -> vec4<f32> {
|
|
50
|
+
let idx = y * P.buf_stride_u32 + x;
|
|
51
|
+
return unpack4x8unorm(buf[idx]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fn storeRgba(x: u32, y: u32, v: vec4<f32>) {
|
|
55
|
+
let idx = y * P.buf_stride_u32 + x;
|
|
56
|
+
buf[idx] = pack4x8unorm(v);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@compute @workgroup_size(8, 8, 1)
|
|
60
|
+
fn interior(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
61
|
+
if (gid.x >= P.dst_size.x || gid.y >= P.dst_size.y) { return; }
|
|
62
|
+
let sx0 = P.src_origin_in_buf.x + gid.x * 2u;
|
|
63
|
+
let sy0 = P.src_origin_in_buf.y + gid.y * 2u;
|
|
64
|
+
let sxMax = P.src_origin_in_buf.x + P.src_size.x - 1u;
|
|
65
|
+
let syMax = P.src_origin_in_buf.y + P.src_size.y - 1u;
|
|
66
|
+
let sx1 = min(sx0 + 1u, sxMax);
|
|
67
|
+
let sy1 = min(sy0 + 1u, syMax);
|
|
68
|
+
let a = loadRgba(sx0, sy0);
|
|
69
|
+
let b = loadRgba(sx1, sy0);
|
|
70
|
+
let c = loadRgba(sx0, sy1);
|
|
71
|
+
let d = loadRgba(sx1, sy1);
|
|
72
|
+
let avg = (a + b + c + d) * 0.25;
|
|
73
|
+
storeRgba(P.dst_origin_in_buf.x + gid.x, P.dst_origin_in_buf.y + gid.y, avg);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@compute @workgroup_size(8, 8, 1)
|
|
77
|
+
fn gutter(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
78
|
+
let totW = P.dst_size.x + 4u;
|
|
79
|
+
let totH = P.dst_size.y + 4u;
|
|
80
|
+
if (gid.x >= totW || gid.y >= totH) { return; }
|
|
81
|
+
let dx = i32(gid.x) - 2;
|
|
82
|
+
let dy = i32(gid.y) - 2;
|
|
83
|
+
let dw = i32(P.dst_size.x);
|
|
84
|
+
let dh = i32(P.dst_size.y);
|
|
85
|
+
if (dx >= 0 && dx < dw && dy >= 0 && dy < dh) { return; }
|
|
86
|
+
var sx: i32;
|
|
87
|
+
var sy: i32;
|
|
88
|
+
if (dx == -2) { sx = dw - 1; }
|
|
89
|
+
else if (dx == -1) { sx = 0; }
|
|
90
|
+
else if (dx == dw) { sx = dw - 1; }
|
|
91
|
+
else if (dx == dw + 1) { sx = 0; }
|
|
92
|
+
else { sx = dx; }
|
|
93
|
+
if (dy == -2) { sy = dh - 1; }
|
|
94
|
+
else if (dy == -1) { sy = 0; }
|
|
95
|
+
else if (dy == dh) { sy = dh - 1; }
|
|
96
|
+
else if (dy == dh + 1) { sy = 0; }
|
|
97
|
+
else { sy = dy; }
|
|
98
|
+
let v = loadRgba(u32(i32(P.dst_origin_in_buf.x) + sx),
|
|
99
|
+
u32(i32(P.dst_origin_in_buf.y) + sy));
|
|
100
|
+
storeRgba(u32(i32(P.dst_origin_in_buf.x) + dx),
|
|
101
|
+
u32(i32(P.dst_origin_in_buf.y) + dy),
|
|
102
|
+
v);
|
|
103
|
+
}
|
|
104
|
+
`;
|
|
105
|
+
|
|
106
|
+
interface KernelCache {
|
|
107
|
+
bindGroupLayout: GPUBindGroupLayout;
|
|
108
|
+
interiorPipeline: GPUComputePipeline;
|
|
109
|
+
gutterPipeline: GPUComputePipeline;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const caches = new WeakMap<GPUDevice, KernelCache>();
|
|
113
|
+
|
|
114
|
+
function getKernel(device: GPUDevice): KernelCache {
|
|
115
|
+
const cached = caches.get(device);
|
|
116
|
+
if (cached !== undefined) return cached;
|
|
117
|
+
const module = device.createShaderModule({ code: WGSL, label: "atlas/mipGutterKernel" });
|
|
118
|
+
const bindGroupLayout = device.createBindGroupLayout({
|
|
119
|
+
label: "atlas/mipGutterKernel/bgl",
|
|
120
|
+
entries: [
|
|
121
|
+
{ binding: 0, visibility: 0x4 /* COMPUTE */, buffer: { type: "storage" } },
|
|
122
|
+
{ binding: 1, visibility: 0x4 /* COMPUTE */, buffer: { type: "uniform" } },
|
|
123
|
+
],
|
|
124
|
+
});
|
|
125
|
+
const layout = device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] });
|
|
126
|
+
const interiorPipeline = device.createComputePipeline({
|
|
127
|
+
layout, compute: { module, entryPoint: "interior" },
|
|
128
|
+
label: "atlas/mipGutterKernel/interior",
|
|
129
|
+
});
|
|
130
|
+
const gutterPipeline = device.createComputePipeline({
|
|
131
|
+
layout, compute: { module, entryPoint: "gutter" },
|
|
132
|
+
label: "atlas/mipGutterKernel/gutter",
|
|
133
|
+
});
|
|
134
|
+
const entry: KernelCache = { bindGroupLayout, interiorPipeline, gutterPipeline };
|
|
135
|
+
caches.set(device, entry);
|
|
136
|
+
return entry;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Per-mip slot description. `origin`/`size` are in *bounding-rect-
|
|
141
|
+
* relative pixel coordinates* (not atlas-page coordinates) — the
|
|
142
|
+
* scratch buffer is laid out as the bounding rect with row stride
|
|
143
|
+
* `bufStrideU32` u32s per row. Mip 0's interior origin matches
|
|
144
|
+
* `acq.origin` − sub-rect origin.
|
|
145
|
+
*/
|
|
146
|
+
export interface MipSlot {
|
|
147
|
+
readonly origin: { x: number; y: number };
|
|
148
|
+
readonly size: { w: number; h: number };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Build the mip pyramid + gutters on GPU and upload the result to a
|
|
153
|
+
* sub-rect of `page`. `srcPixels` is the raw RGBA8 source for mip 0
|
|
154
|
+
* (`srcW × srcH` pixels). The kernel:
|
|
155
|
+
* - Allocates a per-acquire scratch buffer.
|
|
156
|
+
* - Initialises the buffer at creation with mip-0 source pixels at
|
|
157
|
+
* the mip-0 interior offset.
|
|
158
|
+
* - Dispatches per-mip interior + gutter compute passes.
|
|
159
|
+
* - copyBufferToTexture into the page at (subRectX, subRectY).
|
|
160
|
+
*
|
|
161
|
+
* Caller must ensure the page has COPY_DST usage (atlas pages
|
|
162
|
+
* always do).
|
|
163
|
+
*
|
|
164
|
+
* Buffer is destroyed once submitted work completes (fire-and-forget
|
|
165
|
+
* via `onSubmittedWorkDone`).
|
|
166
|
+
*/
|
|
167
|
+
export function buildMipsAndGutterOnGpu(
|
|
168
|
+
device: GPUDevice,
|
|
169
|
+
page: GPUTexture,
|
|
170
|
+
subRectX: number, subRectY: number,
|
|
171
|
+
boundsW: number, boundsH: number,
|
|
172
|
+
srcPixels: Uint8Array,
|
|
173
|
+
srcW: number, srcH: number,
|
|
174
|
+
mips: readonly MipSlot[],
|
|
175
|
+
): void {
|
|
176
|
+
if (mips.length === 0) return;
|
|
177
|
+
const { bindGroupLayout, interiorPipeline, gutterPipeline } = getKernel(device);
|
|
178
|
+
|
|
179
|
+
// Buffer layout: rows padded to 256 bytes for the final
|
|
180
|
+
// copyBufferToTexture. Stride in u32: 256 / 4 = 64 minimum, or
|
|
181
|
+
// ceil(boundsW * 4 / 256) * 256 / 4.
|
|
182
|
+
const rowBytes = Math.max(256, Math.ceil(boundsW * 4 / 256) * 256);
|
|
183
|
+
const bufStrideU32 = rowBytes / 4;
|
|
184
|
+
const bufSize = rowBytes * boundsH;
|
|
185
|
+
|
|
186
|
+
const scratch = device.createBuffer({
|
|
187
|
+
label: `atlas/mipGutter/scratch(${boundsW}x${boundsH})`,
|
|
188
|
+
size: bufSize,
|
|
189
|
+
usage: 0x80 /* STORAGE */ | 0x04 /* COPY_SRC */ | 0x08 /* COPY_DST */,
|
|
190
|
+
mappedAtCreation: true,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// CPU init: write mip-0 source pixels at the mip-0 interior offset
|
|
194
|
+
// inside the buffer. mips[0].origin is the interior offset.
|
|
195
|
+
const mip0 = mips[0]!;
|
|
196
|
+
{
|
|
197
|
+
const mapped = new Uint32Array(scratch.getMappedRange());
|
|
198
|
+
for (let y = 0; y < srcH; y++) {
|
|
199
|
+
for (let x = 0; x < srcW; x++) {
|
|
200
|
+
const si = (y * srcW + x) * 4;
|
|
201
|
+
const r = srcPixels[si]!;
|
|
202
|
+
const g = srcPixels[si + 1]!;
|
|
203
|
+
const b = srcPixels[si + 2]!;
|
|
204
|
+
const a = srcPixels[si + 3]!;
|
|
205
|
+
// little-endian rgba8: byte0=r, byte1=g, byte2=b, byte3=a.
|
|
206
|
+
const packed = (a << 24) | (b << 16) | (g << 8) | r;
|
|
207
|
+
mapped[(mip0.origin.y + y) * bufStrideU32 + (mip0.origin.x + x)] = packed >>> 0;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
scratch.unmap();
|
|
212
|
+
|
|
213
|
+
const enc = device.createCommandEncoder({ label: "atlas/mipGutterKernel" });
|
|
214
|
+
|
|
215
|
+
const ubos: GPUBuffer[] = [];
|
|
216
|
+
const makeUbo = (
|
|
217
|
+
srcO: { x: number; y: number }, srcS: { w: number; h: number },
|
|
218
|
+
dstO: { x: number; y: number }, dstS: { w: number; h: number },
|
|
219
|
+
): GPUBuffer => {
|
|
220
|
+
const buf = device.createBuffer({
|
|
221
|
+
size: 48, // 12 u32, padded
|
|
222
|
+
usage: 0x40 /* UNIFORM */ | 0x08 /* COPY_DST */,
|
|
223
|
+
label: "atlas/mipGutter/params",
|
|
224
|
+
});
|
|
225
|
+
const u = new Uint32Array(12);
|
|
226
|
+
u[0] = bufStrideU32;
|
|
227
|
+
u[1] = 0;
|
|
228
|
+
u[2] = srcO.x; u[3] = srcO.y;
|
|
229
|
+
u[4] = srcS.w; u[5] = srcS.h;
|
|
230
|
+
u[6] = dstO.x; u[7] = dstO.y;
|
|
231
|
+
u[8] = dstS.w; u[9] = dstS.h;
|
|
232
|
+
device.queue.writeBuffer(buf, 0, u);
|
|
233
|
+
ubos.push(buf);
|
|
234
|
+
return buf;
|
|
235
|
+
};
|
|
236
|
+
const makeBg = (ubo: GPUBuffer): GPUBindGroup =>
|
|
237
|
+
device.createBindGroup({
|
|
238
|
+
layout: bindGroupLayout,
|
|
239
|
+
entries: [
|
|
240
|
+
{ binding: 0, resource: { buffer: scratch } },
|
|
241
|
+
{ binding: 1, resource: { buffer: ubo } },
|
|
242
|
+
],
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Interior passes: one compute pass per mip > 0 (pass boundary
|
|
246
|
+
// gives us the barrier ensuring mip-(k-1) is fully written before
|
|
247
|
+
// mip-k reads it).
|
|
248
|
+
for (let k = 1; k < mips.length; k++) {
|
|
249
|
+
const src = mips[k - 1]!;
|
|
250
|
+
const dst = mips[k]!;
|
|
251
|
+
const ubo = makeUbo(src.origin, src.size, dst.origin, dst.size);
|
|
252
|
+
const pass = enc.beginComputePass({ label: `atlas/mipGutter/interior/${k}` });
|
|
253
|
+
pass.setPipeline(interiorPipeline);
|
|
254
|
+
pass.setBindGroup(0, makeBg(ubo));
|
|
255
|
+
pass.dispatchWorkgroups(Math.ceil(dst.size.w / 8), Math.ceil(dst.size.h / 8), 1);
|
|
256
|
+
pass.end();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Gutter passes: one per mip (each pass reads mip-k interior,
|
|
260
|
+
// which was either CPU-uploaded for k=0 or written by the interior
|
|
261
|
+
// pass for k>0).
|
|
262
|
+
for (let k = 0; k < mips.length; k++) {
|
|
263
|
+
const dst = mips[k]!;
|
|
264
|
+
const ubo = makeUbo(dst.origin, dst.size, dst.origin, dst.size);
|
|
265
|
+
const pass = enc.beginComputePass({ label: `atlas/mipGutter/gutter/${k}` });
|
|
266
|
+
pass.setPipeline(gutterPipeline);
|
|
267
|
+
pass.setBindGroup(0, makeBg(ubo));
|
|
268
|
+
pass.dispatchWorkgroups(
|
|
269
|
+
Math.ceil((dst.size.w + 4) / 8),
|
|
270
|
+
Math.ceil((dst.size.h + 4) / 8),
|
|
271
|
+
1,
|
|
272
|
+
);
|
|
273
|
+
pass.end();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Final upload: buffer → page.
|
|
277
|
+
enc.copyBufferToTexture(
|
|
278
|
+
{ buffer: scratch, bytesPerRow: rowBytes, rowsPerImage: boundsH },
|
|
279
|
+
{ texture: page, origin: { x: subRectX, y: subRectY, z: 0 } },
|
|
280
|
+
{ width: boundsW, height: boundsH, depthOrArrayLayers: 1 },
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
device.queue.submit([enc.finish()]);
|
|
284
|
+
|
|
285
|
+
// Lifetime: destroy scratch + per-pass UBOs once submitted work is
|
|
286
|
+
// done. The submit holds a ref until completion.
|
|
287
|
+
void device.queue.onSubmittedWorkDone().then(() => {
|
|
288
|
+
scratch.destroy();
|
|
289
|
+
for (const b of ubos) b.destroy();
|
|
290
|
+
});
|
|
291
|
+
}
|