@aardworx/wombat.rendering 0.1.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 (46) hide show
  1. package/package.json +67 -0
  2. package/src/commands/clear.ts +66 -0
  3. package/src/commands/index.ts +11 -0
  4. package/src/commands/render.ts +106 -0
  5. package/src/core/acquire.ts +22 -0
  6. package/src/core/adaptiveResource.ts +88 -0
  7. package/src/core/buffer.ts +45 -0
  8. package/src/core/bufferView.ts +26 -0
  9. package/src/core/clear.ts +16 -0
  10. package/src/core/command.ts +50 -0
  11. package/src/core/drawCall.ts +22 -0
  12. package/src/core/framebuffer.ts +37 -0
  13. package/src/core/framebufferSignature.ts +19 -0
  14. package/src/core/index.ts +116 -0
  15. package/src/core/pipelineState.ts +64 -0
  16. package/src/core/renderContext.ts +48 -0
  17. package/src/core/renderObject.ts +57 -0
  18. package/src/core/renderTask.ts +17 -0
  19. package/src/core/renderTree.ts +29 -0
  20. package/src/core/sampler.ts +12 -0
  21. package/src/core/shader.ts +28 -0
  22. package/src/core/texture.ts +63 -0
  23. package/src/index.ts +18 -0
  24. package/src/resources/adaptiveBuffer.ts +135 -0
  25. package/src/resources/adaptiveSampler.ts +82 -0
  26. package/src/resources/adaptiveTexture.ts +269 -0
  27. package/src/resources/framebuffer.ts +163 -0
  28. package/src/resources/framebufferSignature.ts +36 -0
  29. package/src/resources/index.ts +82 -0
  30. package/src/resources/mipGen.ts +148 -0
  31. package/src/resources/preparedComputeShader.ts +426 -0
  32. package/src/resources/preparedRenderObject.ts +498 -0
  33. package/src/resources/renderPipeline.ts +148 -0
  34. package/src/resources/shaderDiagnostics.ts +52 -0
  35. package/src/resources/sourceMapDecoder.ts +133 -0
  36. package/src/resources/uniformBuffer.ts +170 -0
  37. package/src/resources/webgpuFlags.ts +43 -0
  38. package/src/runtime/copy.ts +13 -0
  39. package/src/runtime/index.ts +24 -0
  40. package/src/runtime/renderTask.ts +137 -0
  41. package/src/runtime/renderTo.ts +154 -0
  42. package/src/runtime/runtime.ts +113 -0
  43. package/src/runtime/scenePass.ts +273 -0
  44. package/src/window/canvas.ts +252 -0
  45. package/src/window/index.ts +14 -0
  46. package/src/window/loop.ts +62 -0
@@ -0,0 +1,52 @@
1
+ // Shader-compile error reporting. WebGPU's `createShaderModule`
2
+ // is synchronous and never throws on parse errors; the errors
3
+ // surface only via `module.getCompilationInfo()`, which is async.
4
+ // We fire-and-forget that call after creation and log structured
5
+ // messages including the originating TS source location decoded
6
+ // from the supplied source map.
7
+
8
+ import type { SourceMap } from "@aardworx/wombat.shader/ir";
9
+ import { decodeLine } from "./sourceMapDecoder.js";
10
+
11
+ export interface ShaderDiagnosticsOptions {
12
+ /** Human-friendly tag prefixed to log messages. */
13
+ readonly label?: string;
14
+ /** Source map produced by wombat.shader for this stage. */
15
+ readonly sourceMap?: SourceMap | null;
16
+ /** Optional override for the logger. Default: console. */
17
+ readonly logger?: { error(...args: unknown[]): void; warn(...args: unknown[]): void };
18
+ }
19
+
20
+ export function installShaderDiagnostics(
21
+ module: GPUShaderModule,
22
+ source: string,
23
+ opts: ShaderDiagnosticsOptions = {},
24
+ ): void {
25
+ // getCompilationInfo is part of the WebGPU spec; some test mocks
26
+ // may not implement it. Detect and skip silently.
27
+ const info = (module as { getCompilationInfo?: () => Promise<GPUCompilationInfo> }).getCompilationInfo;
28
+ if (typeof info !== "function") return;
29
+ const log = opts.logger ?? console;
30
+ const tag = opts.label ?? "shader";
31
+
32
+ info.call(module).then((compInfo) => {
33
+ if (compInfo.messages.length === 0) return;
34
+ const map = opts.sourceMap ?? null;
35
+ for (const m of compInfo.messages) {
36
+ const lineText = source.split("\n")[Math.max(0, m.lineNum - 1)] ?? "";
37
+ const head = `[${tag}] ${m.type} ${m.lineNum}:${m.linePos}: ${m.message}`;
38
+ const body = lineText.length > 0 ? `\n ${lineText.trim()}` : "";
39
+ const decoded = map ? decodeLine(map, m.lineNum) : null;
40
+ const origin = decoded
41
+ ? `\n → ${decoded.file}:${decoded.line}:${decoded.column}`
42
+ : (map && map.sources.length > 0
43
+ ? `\n (originated in ${map.sources.join(", ")})`
44
+ : "");
45
+ const line = `${head}${body}${origin}`;
46
+ if (m.type === "error") log.error(line);
47
+ else log.warn(line);
48
+ }
49
+ }).catch(() => {
50
+ // Mock environments / older browsers may reject; silent swallow.
51
+ });
52
+ }
@@ -0,0 +1,133 @@
1
+ // Decoder for v3 source maps. Supports both line-granular maps
2
+ // (one segment per generated line at col 0) and multi-segment
3
+ // lines (per-Expr granularity). Segments encode
4
+ // (genColDelta, sourceIdxDelta, sourceLineDelta, sourceColDelta)
5
+ // as base64-VLQ, with deltas accumulating across the whole map
6
+ // except for genCol which resets at every `;` line boundary.
7
+ //
8
+ // `decodeLine(map, line)` returns the position for the *first*
9
+ // segment on the line; `decodePosition(map, line, col)` picks the
10
+ // closest preceding segment. Both fall back to the most recent
11
+ // mapped line if the queried line is unmapped, which is what
12
+ // `installShaderDiagnostics` wants for "go to source" navigation.
13
+
14
+ import type { SourceMap } from "@aardworx/wombat.shader/ir";
15
+
16
+ const VLQ_INDEX = new Map<string, number>();
17
+ {
18
+ const a = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
19
+ for (let i = 0; i < a.length; i++) VLQ_INDEX.set(a.charAt(i), i);
20
+ }
21
+
22
+ function vlqDecode(s: string): number[] {
23
+ const out: number[] = [];
24
+ let value = 0;
25
+ let shift = 0;
26
+ for (let i = 0; i < s.length; i++) {
27
+ const ch = s.charAt(i);
28
+ const digit = VLQ_INDEX.get(ch);
29
+ if (digit === undefined) throw new Error(`bad VLQ char: ${ch}`);
30
+ const cont = (digit & 0b100000) !== 0;
31
+ value |= (digit & 0b11111) << shift;
32
+ if (cont) {
33
+ shift += 5;
34
+ } else {
35
+ const negative = (value & 1) === 1;
36
+ const magnitude = value >>> 1;
37
+ out.push(negative ? -magnitude : magnitude);
38
+ value = 0;
39
+ shift = 0;
40
+ }
41
+ }
42
+ return out;
43
+ }
44
+
45
+ export interface DecodedPosition {
46
+ /** Source file as listed in `SourceMap.sources`. */
47
+ readonly file: string;
48
+ /** 1-based line in the original source. */
49
+ readonly line: number;
50
+ /** 1-based column in the original source. */
51
+ readonly column: number;
52
+ }
53
+
54
+ interface DecodedSegment {
55
+ readonly genCol: number;
56
+ readonly idx: number;
57
+ readonly line: number;
58
+ readonly col: number;
59
+ }
60
+
61
+ /** Walk the map up to (and including) `targetLine`, returning each line's segments. */
62
+ function segmentsUpTo(map: SourceMap, targetLine: number): DecodedSegment[][] {
63
+ const lines = map.mappings.split(";");
64
+ const out: DecodedSegment[][] = [];
65
+ let srcIdx = 0;
66
+ let srcLine = 0;
67
+ let srcCol = 0;
68
+ for (let i = 0; i <= targetLine && i < lines.length; i++) {
69
+ const segs = (lines[i] ?? "").split(",").filter(s => s.length > 0);
70
+ let prevGenCol = 0;
71
+ const lineOut: DecodedSegment[] = [];
72
+ for (const sStr of segs) {
73
+ const fields = vlqDecode(sStr);
74
+ if (fields.length < 4) continue;
75
+ const genCol = prevGenCol + fields[0]!;
76
+ srcIdx += fields[1]!;
77
+ srcLine += fields[2]!;
78
+ srcCol += fields[3]!;
79
+ lineOut.push({ genCol, idx: srcIdx, line: srcLine, col: srcCol });
80
+ prevGenCol = genCol;
81
+ }
82
+ out.push(lineOut);
83
+ }
84
+ return out;
85
+ }
86
+
87
+ function makePosition(map: SourceMap, s: DecodedSegment): DecodedPosition | null {
88
+ const file = map.sources[s.idx];
89
+ if (file === undefined) return null;
90
+ return { file, line: s.line + 1, column: s.col + 1 };
91
+ }
92
+
93
+ /**
94
+ * Closest-preceding-segment lookup. Given a 1-based generated
95
+ * `line` + 0-based generated `col`, returns the originating source
96
+ * position. Falls back to the last segment on a previous mapped
97
+ * line if `line` itself has no segments.
98
+ */
99
+ export function decodePosition(
100
+ map: SourceMap,
101
+ generatedLine: number,
102
+ generatedCol = 0,
103
+ ): DecodedPosition | null {
104
+ const target = generatedLine - 1;
105
+ if (target < 0) return null;
106
+ const allLines = segmentsUpTo(map, target);
107
+ // Prefer a segment on the target line at or before generatedCol.
108
+ const onTarget = allLines[target];
109
+ if (onTarget && onTarget.length > 0) {
110
+ let pick: DecodedSegment | undefined;
111
+ for (const s of onTarget) {
112
+ if (s.genCol <= generatedCol) pick = s;
113
+ else break;
114
+ }
115
+ if (pick) return makePosition(map, pick);
116
+ return makePosition(map, onTarget[0]!);
117
+ }
118
+ // Fallback: last segment on the most recent mapped line.
119
+ for (let i = target - 1; i >= 0; i--) {
120
+ const segs = allLines[i];
121
+ if (segs && segs.length > 0) return makePosition(map, segs[segs.length - 1]!);
122
+ }
123
+ return null;
124
+ }
125
+
126
+ /**
127
+ * Backwards-compatible line-only decoder: returns the *first*
128
+ * segment of `generatedLine`, or the most recent mapped line for
129
+ * unmapped lines. Equivalent to `decodePosition(map, line, 0)`.
130
+ */
131
+ export function decodeLine(map: SourceMap, generatedLine: number): DecodedPosition | null {
132
+ return decodePosition(map, generatedLine, 0);
133
+ }
@@ -0,0 +1,170 @@
1
+ // prepareUniformBuffer — pack a name-keyed bag of `aval<unknown>`
2
+ // uniforms into a single UBO described by a `UniformBlockInfo`
3
+ // (from wombat.shader's ProgramInterface).
4
+ //
5
+ // The layout dictates byte offsets + sizes per field; this packer
6
+ // trusts those numbers and just encodes values into a Float32-
7
+ // addressable scratch buffer, then uploads via `queue.writeBuffer`
8
+ // on each change.
9
+ //
10
+ // Value encoding by type:
11
+ // - `number` → 1 × f32 at offset
12
+ // - `Float32Array` → memcpy floats
13
+ // - `Int32Array`/`Uint32Array` → memcpy as-is (also valid for f32 storage)
14
+ // - any object with `_data: Float32Array | Int32Array | Uint32Array`
15
+ // (V2f/V3f/V4f/M44f/M33f/M22f, etc. from wombat.base) → memcpy `_data`
16
+ //
17
+ // Missing inputs (a layout field with no corresponding entry in
18
+ // the bag) are left zero-initialised. Extras in the bag are
19
+ // silently ignored — same "pull, don't push" semantics as the
20
+ // rest of the renderer.
21
+
22
+ import {
23
+ AdaptiveResource,
24
+ tryAcquire,
25
+ tryRelease,
26
+ type UniformBlockInfo,
27
+ } from "../core/index.js";
28
+ import {
29
+ type AdaptiveToken,
30
+ type aval,
31
+ type HashMap,
32
+ } from "@aardworx/wombat.adaptive";
33
+ import { BufferUsage } from "./webgpuFlags.js";
34
+
35
+ export interface PackedView {
36
+ readonly bytes: ArrayBuffer;
37
+ readonly f32: Float32Array;
38
+ readonly i32: Int32Array;
39
+ readonly u32: Uint32Array;
40
+ }
41
+
42
+ export function makePackedView(byteSize: number): PackedView {
43
+ const bytes = new ArrayBuffer(roundUp4(byteSize));
44
+ return {
45
+ bytes,
46
+ f32: new Float32Array(bytes),
47
+ i32: new Int32Array(bytes),
48
+ u32: new Uint32Array(bytes),
49
+ };
50
+ }
51
+
52
+ class UniformBufferResource extends AdaptiveResource<GPUBuffer> {
53
+ private _gpu: GPUBuffer | undefined;
54
+ private readonly _scratch: PackedView;
55
+
56
+ constructor(
57
+ private readonly device: GPUDevice,
58
+ private readonly layout: UniformBlockInfo,
59
+ private readonly inputs: HashMap<string, aval<unknown>>,
60
+ private readonly label: string | undefined,
61
+ ) {
62
+ super();
63
+ const bytes = new ArrayBuffer(roundUp4(layout.size));
64
+ this._scratch = {
65
+ bytes,
66
+ f32: new Float32Array(bytes),
67
+ i32: new Int32Array(bytes),
68
+ u32: new Uint32Array(bytes),
69
+ };
70
+ }
71
+
72
+ protected create(): void {
73
+ for (const f of this.layout.fields) {
74
+ const av = this.inputs.tryFind(f.name);
75
+ if (av !== undefined) tryAcquire(av);
76
+ }
77
+ }
78
+
79
+ protected destroy(): void {
80
+ if (this._gpu !== undefined) {
81
+ this._gpu.destroy();
82
+ this._gpu = undefined;
83
+ }
84
+ for (const f of this.layout.fields) {
85
+ const av = this.inputs.tryFind(f.name);
86
+ if (av !== undefined) tryRelease(av);
87
+ }
88
+ }
89
+
90
+ override compute(token: AdaptiveToken): GPUBuffer {
91
+ // Pack scratch.
92
+ new Uint8Array(this._scratch.bytes).fill(0);
93
+ for (const field of this.layout.fields) {
94
+ const av = this.inputs.tryFind(field.name);
95
+ if (av === undefined) continue;
96
+ writeField(this._scratch, field.offset, av.getValue(token));
97
+ }
98
+ if (this._gpu === undefined) {
99
+ const desc: GPUBufferDescriptor = {
100
+ size: this._scratch.bytes.byteLength,
101
+ usage: BufferUsage.UNIFORM | BufferUsage.COPY_DST,
102
+ ...(this.label !== undefined ? { label: this.label } : {}),
103
+ };
104
+ this._gpu = this.device.createBuffer(desc);
105
+ }
106
+ this.device.queue.writeBuffer(this._gpu, 0, this._scratch.bytes);
107
+ return this._gpu;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Write a value into a UBO scratch buffer at the given byte offset.
113
+ * `fieldType`, when supplied, disambiguates a bare `number`:
114
+ * - Int (signed) → i32
115
+ * - Int (unsigned) → u32
116
+ * - Float (default) → f32
117
+ * Without it, numbers are assumed to be f32.
118
+ */
119
+ export function writeField(
120
+ view: PackedView,
121
+ offset: number,
122
+ value: unknown,
123
+ fieldType?: import("../core/index.js").UniformFieldInfo["type"],
124
+ ): void {
125
+ if (typeof value === "number") {
126
+ if (fieldType?.kind === "Int") {
127
+ if (fieldType.signed) view.i32[offset >> 2] = value;
128
+ else view.u32[offset >> 2] = value;
129
+ } else {
130
+ view.f32[offset >> 2] = value;
131
+ }
132
+ return;
133
+ }
134
+ if (value instanceof Float32Array) {
135
+ view.f32.set(value, offset >> 2);
136
+ return;
137
+ }
138
+ if (value instanceof Int32Array) {
139
+ view.i32.set(value, offset >> 2);
140
+ return;
141
+ }
142
+ if (value instanceof Uint32Array) {
143
+ view.u32.set(value, offset >> 2);
144
+ return;
145
+ }
146
+ if (value !== null && typeof value === "object" && "_data" in (value as object)) {
147
+ const data = (value as { _data: unknown })._data;
148
+ if (data instanceof Float32Array) { view.f32.set(data, offset >> 2); return; }
149
+ if (data instanceof Int32Array) { view.i32.set(data, offset >> 2); return; }
150
+ if (data instanceof Uint32Array) { view.u32.set(data, offset >> 2); return; }
151
+ }
152
+ throw new Error(`prepareUniformBuffer: unsupported uniform value at offset ${offset}: ${String(value)}`);
153
+ }
154
+
155
+ function roundUp4(n: number): number {
156
+ return (n + 3) & ~3;
157
+ }
158
+
159
+ export interface PrepareUniformBufferOptions {
160
+ readonly label?: string;
161
+ }
162
+
163
+ export function prepareUniformBuffer(
164
+ device: GPUDevice,
165
+ layout: UniformBlockInfo,
166
+ inputs: HashMap<string, aval<unknown>>,
167
+ opts: PrepareUniformBufferOptions = {},
168
+ ): AdaptiveResource<GPUBuffer> {
169
+ return new UniformBufferResource(device, layout, inputs, opts.label);
170
+ }
@@ -0,0 +1,43 @@
1
+ // `GPUBufferUsage` / `GPUTextureUsage` / `GPUMapMode` etc. are
2
+ // runtime-provided in the browser and Dawn-Node, but `@webgpu/types`
3
+ // is a types-only package — the constants don't exist when running
4
+ // under plain Node (vitest). Vendor the flag values here so the
5
+ // resource layer can compose usage flags portably.
6
+ //
7
+ // Numbers are taken from the WebGPU spec. They are part of the
8
+ // stable API surface.
9
+
10
+ export const BufferUsage = {
11
+ MAP_READ: 0x0001,
12
+ MAP_WRITE: 0x0002,
13
+ COPY_SRC: 0x0004,
14
+ COPY_DST: 0x0008,
15
+ INDEX: 0x0010,
16
+ VERTEX: 0x0020,
17
+ UNIFORM: 0x0040,
18
+ STORAGE: 0x0080,
19
+ INDIRECT: 0x0100,
20
+ QUERY_RESOLVE: 0x0200,
21
+ } as const;
22
+
23
+ export const TextureUsage = {
24
+ COPY_SRC: 0x01,
25
+ COPY_DST: 0x02,
26
+ TEXTURE_BINDING: 0x04,
27
+ STORAGE_BINDING: 0x08,
28
+ RENDER_ATTACHMENT: 0x10,
29
+ } as const;
30
+
31
+ export const ShaderStage = {
32
+ VERTEX: 0x1,
33
+ FRAGMENT: 0x2,
34
+ COMPUTE: 0x4,
35
+ } as const;
36
+
37
+ export const ColorWrite = {
38
+ RED: 0x1,
39
+ GREEN: 0x2,
40
+ BLUE: 0x4,
41
+ ALPHA: 0x8,
42
+ ALL: 0xf,
43
+ } as const;
@@ -0,0 +1,13 @@
1
+ // copy(cmd, spec) — encode a buffer↔buffer or texture↔texture
2
+ // transfer described by a `CopySpec`.
3
+
4
+ import type { CopySpec } from "../core/index.js";
5
+
6
+ export function copy(cmd: GPUCommandEncoder, spec: CopySpec): void {
7
+ if (spec.kind === "buffer") {
8
+ const range = spec.range ?? { srcOffset: 0, dstOffset: 0, size: spec.src.size };
9
+ cmd.copyBufferToBuffer(spec.src, range.srcOffset, spec.dst, range.dstOffset, range.size);
10
+ } else {
11
+ cmd.copyTextureToTexture(spec.src, spec.dst, spec.size);
12
+ }
13
+ }
@@ -0,0 +1,24 @@
1
+ // Public API of @aardworx/wombat.rendering-runtime.
2
+
3
+ export {
4
+ Runtime,
5
+ type RuntimeOptions,
6
+ } from "./runtime.js";
7
+
8
+ export {
9
+ compileRenderTask,
10
+ type RuntimeContext,
11
+ } from "./renderTask.js";
12
+
13
+ export { copy } from "./copy.js";
14
+
15
+ export {
16
+ renderTo,
17
+ type RenderToOptions,
18
+ type RenderToResult,
19
+ } from "./renderTo.js";
20
+
21
+ export {
22
+ ScenePass,
23
+ type WalkerStats,
24
+ } from "./scenePass.js";
@@ -0,0 +1,137 @@
1
+ // RenderTask — compile an `alist<Command>` into a runnable
2
+ // IRenderTask.
3
+ //
4
+ // Per `Render` command we lazily build a `ScenePass` that holds a
5
+ // stateful tree-walker subscribed to the dynamic subtrees
6
+ // (`Adaptive` / `OrderedFromList` / `UnorderedFromSet`). Each frame:
7
+ // - The walker pulls deltas from its readers (O(deltas) work).
8
+ // - It produces the ordered list of `PreparedRenderObject`s.
9
+ // - We open the render pass and call `record(pass, token)` per leaf.
10
+ //
11
+ // The previous per-frame `collectLeaves` recursion through the tree
12
+ // is gone. Static subtrees prep their leaves once at construction;
13
+ // dynamic subtrees splice on delta — see scenePass.ts.
14
+
15
+ import {
16
+ RenderContext,
17
+ type ClearValues,
18
+ type Command,
19
+ type CompiledEffect,
20
+ type Effect,
21
+ type IFramebuffer,
22
+ type IRenderTask,
23
+ type RenderTree,
24
+ } from "../core/index.js";
25
+ import { beginPassDescriptor, clear } from "../commands/index.js";
26
+ import { AdaptiveToken, type alist, type aval } from "@aardworx/wombat.adaptive";
27
+ import { copy } from "./copy.js";
28
+ import { ScenePass } from "./scenePass.js";
29
+
30
+ export interface RuntimeContext {
31
+ readonly device: GPUDevice;
32
+ readonly compileEffect: (effect: Effect) => CompiledEffect;
33
+ }
34
+
35
+ class RenderTask implements IRenderTask {
36
+ /**
37
+ * Cache of `ScenePass`es keyed on the `Render` command itself.
38
+ * Holding ScenePass per command means the same `RenderTree`
39
+ * reused across frames keeps its incrementally-maintained
40
+ * walker state.
41
+ */
42
+ private readonly _scenes = new Map<unknown, ScenePass>();
43
+ private _disposed = false;
44
+
45
+ constructor(
46
+ private readonly ctx: RuntimeContext,
47
+ private readonly commands: alist<Command>,
48
+ ) {}
49
+
50
+ run(token: AdaptiveToken): void {
51
+ if (this._disposed) throw new Error("RenderTask: run after dispose");
52
+ const enc = this.ctx.device.createCommandEncoder();
53
+ this.encode(enc, token);
54
+ this.ctx.device.queue.submit([enc.finish()]);
55
+ }
56
+
57
+ encode(enc: GPUCommandEncoder, token: AdaptiveToken): void {
58
+ if (this._disposed) throw new Error("RenderTask: encode after dispose");
59
+ RenderContext.withEncoder(enc, () => {
60
+ const arr: Command[] = [];
61
+ for (const c of this.commands.content.getValue(token)) arr.push(c);
62
+ for (let i = 0; i < arr.length; i++) {
63
+ const c = arr[i]!;
64
+ if (c.kind === "Clear") {
65
+ const next = arr[i + 1];
66
+ if (next !== undefined && next.kind === "Render" && next.output === c.output) {
67
+ this.encodeRenderCommand(enc, next, token, c.values);
68
+ i++;
69
+ continue;
70
+ }
71
+ }
72
+ this.encodeCommand(enc, c, token);
73
+ }
74
+ });
75
+ }
76
+
77
+ dispose(): void {
78
+ if (this._disposed) return;
79
+ for (const s of this._scenes.values()) s.dispose();
80
+ this._scenes.clear();
81
+ this._disposed = true;
82
+ }
83
+
84
+ private encodeCommand(enc: GPUCommandEncoder, c: Command, token: AdaptiveToken): void {
85
+ switch (c.kind) {
86
+ case "Clear": clear(enc, c.output.getValue(token), c.values); return;
87
+ case "Copy": copy(enc, c.copy); return;
88
+ case "Custom": c.encode(enc); return;
89
+ case "Render": this.encodeRenderCommand(enc, c, token); return;
90
+ }
91
+ }
92
+
93
+ private encodeRenderCommand(
94
+ enc: GPUCommandEncoder,
95
+ cmd: Extract<Command, { kind: "Render" }>,
96
+ token: AdaptiveToken,
97
+ clearValues?: ClearValues,
98
+ ): void {
99
+ const output = cmd.output.getValue(token);
100
+ const scene = this.sceneFor(cmd, output, cmd.tree);
101
+ const leaves = scene.resolve(token);
102
+ if (leaves.length === 0 && clearValues === undefined) return;
103
+ // Either we have draws or we need to clear — open a single pass.
104
+ const pass = enc.beginRenderPass(beginPassDescriptor(output, clearValues));
105
+ for (const leaf of leaves) leaf.record(pass, token);
106
+ pass.end();
107
+ }
108
+
109
+ private sceneFor(
110
+ cmd: Extract<Command, { kind: "Render" }>,
111
+ output: IFramebuffer,
112
+ tree: RenderTree,
113
+ ): ScenePass {
114
+ let s = this._scenes.get(cmd);
115
+ if (s === undefined) {
116
+ s = new ScenePass(this.ctx.device, output.signature, tree, this.ctx.compileEffect);
117
+ this._scenes.set(cmd, s);
118
+ }
119
+ return s;
120
+ // Note: `output.signature` is read here only to seed the
121
+ // ScenePass; subsequent frames continue to use this signature
122
+ // even if the framebuffer aval emits a different sig. That
123
+ // matches the `(RenderObject, signature)` cache invariant —
124
+ // changing signature requires a fresh Render command.
125
+ }
126
+ }
127
+
128
+ export function compileRenderTask(ctx: RuntimeContext, commands: alist<Command>): IRenderTask & {
129
+ encode(enc: GPUCommandEncoder, token: AdaptiveToken): void;
130
+ } {
131
+ return new RenderTask(ctx, commands);
132
+ }
133
+
134
+ // `RenderTree` carries an `aval<RenderTree>` inside its `Adaptive`
135
+ // variant; this `void` reference keeps the `aval` import live.
136
+ const _avalGuard: ((x: aval<unknown>) => void) | undefined = undefined;
137
+ void _avalGuard;