@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,252 @@
1
+ // attachCanvas — bind a `GPUDevice` to an `HTMLCanvasElement` /
2
+ // `OffscreenCanvas` and expose the swap-chain as an
3
+ // `aval<IFramebuffer>` so render tasks just plug it into a
4
+ // `Render` command's `output`.
5
+ //
6
+ // Each call to `framebuffer.getValue(token)` returns a fresh
7
+ // `IFramebuffer` whose color view is the current swap-chain
8
+ // texture (`context.getCurrentTexture().createView()`). Inside
9
+ // the render loop we mark the framebuffer aval outdated once per
10
+ // rAF tick so the runtime samples the new texture.
11
+ //
12
+ // The size aval is fed by a `ResizeObserver`; on size change we
13
+ // re-`configure()` the canvas context and bump a cval that the
14
+ // framebuffer aval depends on.
15
+ //
16
+ // MSAA: when `sampleCount > 1`, we allocate a hidden multisample
17
+ // color (and depth) texture sized to the canvas; the swap-chain
18
+ // texture becomes the resolve target. The render-pass descriptor
19
+ // builder in `@aardworx/wombat.rendering-commands` already wires
20
+ // `resolveTarget` based on the framebuffer's `resolveColors`.
21
+
22
+ import {
23
+ type IFramebuffer,
24
+ type FramebufferSignature,
25
+ } from "../core/index.js";
26
+ import {
27
+ HashMap,
28
+ cval,
29
+ transact,
30
+ type aval,
31
+ type ChangeableValue,
32
+ } from "@aardworx/wombat.adaptive";
33
+
34
+ export interface AttachCanvasOptions {
35
+ /** Texture format for the swap-chain. Default: device's preferred format. */
36
+ readonly format?: GPUTextureFormat;
37
+ /** Optional depth/stencil format. When set, an off-screen depth texture is co-allocated. */
38
+ readonly depthFormat?: GPUTextureFormat;
39
+ /** Pre-multiplied / opaque alpha mode. Default `"premultiplied"`. */
40
+ readonly alphaMode?: GPUCanvasAlphaMode;
41
+ /** Logical → physical pixel scaling. Default: `window.devicePixelRatio`. */
42
+ readonly devicePixelRatio?: number;
43
+ /** Color attachment name in the resulting `FramebufferSignature`. Default `"color"`. */
44
+ readonly colorAttachmentName?: string;
45
+ /**
46
+ * MSAA sample count. Default 1. With `> 1`, a hidden multisample
47
+ * color texture (and depth, if `depthFormat` is set) is allocated
48
+ * sized to the canvas; the swap-chain texture becomes the resolve
49
+ * target. Common values: 1 (off), 4 (fast MSAA).
50
+ */
51
+ readonly sampleCount?: number;
52
+ }
53
+
54
+ export interface CanvasAttachment {
55
+ /** Reactive framebuffer; bump per frame via `markFrame()`. */
56
+ readonly framebuffer: aval<IFramebuffer>;
57
+ /** Reactive `{ width, height }` driven by ResizeObserver. */
58
+ readonly size: aval<{ readonly width: number; readonly height: number }>;
59
+ /** The signature derived from the chosen formats. */
60
+ readonly signature: FramebufferSignature;
61
+ /** Mark a new frame: invalidates the framebuffer aval so its next read produces a fresh swap-chain texture. */
62
+ markFrame(): void;
63
+ /** Disconnect the resize observer + clear the canvas context. Idempotent. */
64
+ dispose(): void;
65
+ }
66
+
67
+ export function attachCanvas(
68
+ device: GPUDevice,
69
+ canvas: HTMLCanvasElement | OffscreenCanvas,
70
+ opts: AttachCanvasOptions = {},
71
+ ): CanvasAttachment {
72
+ const ctx = canvas.getContext("webgpu") as GPUCanvasContext | null;
73
+ if (ctx === null) throw new Error("attachCanvas: getContext('webgpu') returned null");
74
+ const colorName = opts.colorAttachmentName ?? "color";
75
+ const format = opts.format ?? navigator.gpu.getPreferredCanvasFormat();
76
+ const alphaMode = opts.alphaMode ?? "premultiplied";
77
+ const sampleCount = opts.sampleCount ?? 1;
78
+
79
+ const dpr = opts.devicePixelRatio ?? (typeof window !== "undefined" ? window.devicePixelRatio : 1);
80
+ // initial size from the canvas's CSS size (or its native dimensions for OffscreenCanvas).
81
+ const initial = currentSize(canvas, dpr);
82
+ const sizeC: ChangeableValue<{ width: number; height: number }> = cval({ width: initial.width, height: initial.height });
83
+ const frameC: ChangeableValue<number> = cval(0);
84
+
85
+ // Apply the initial configure. We re-configure on every size change.
86
+ configure(ctx, device, format, alphaMode);
87
+ applySize(canvas, initial.width, initial.height);
88
+
89
+ // Hidden multisample color texture, present only when sampleCount > 1.
90
+ let msaaColor: GPUTexture | undefined;
91
+ const ensureMsaaColor = (w: number, h: number): GPUTexture | undefined => {
92
+ if (sampleCount <= 1) return undefined;
93
+ if (msaaColor !== undefined) msaaColor.destroy();
94
+ msaaColor = device.createTexture({
95
+ size: { width: w, height: h, depthOrArrayLayers: 1 },
96
+ format,
97
+ sampleCount,
98
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
99
+ label: "canvas.color.msaa",
100
+ });
101
+ return msaaColor;
102
+ };
103
+ ensureMsaaColor(initial.width, initial.height);
104
+
105
+ // Optional shared depth texture re-allocated on size change.
106
+ let depthTexture: GPUTexture | undefined;
107
+ const ensureDepth = (w: number, h: number): GPUTexture | undefined => {
108
+ if (opts.depthFormat === undefined) return undefined;
109
+ if (depthTexture !== undefined) depthTexture.destroy();
110
+ depthTexture = device.createTexture({
111
+ size: { width: w, height: h, depthOrArrayLayers: 1 },
112
+ format: opts.depthFormat,
113
+ sampleCount,
114
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
115
+ label: "canvas.depth",
116
+ });
117
+ return depthTexture;
118
+ };
119
+ ensureDepth(initial.width, initial.height);
120
+
121
+ // ResizeObserver — only meaningful for HTMLCanvasElement.
122
+ let observer: ResizeObserver | undefined;
123
+ if (typeof ResizeObserver !== "undefined" && canvas instanceof (globalThis as unknown as { HTMLCanvasElement: typeof HTMLCanvasElement }).HTMLCanvasElement) {
124
+ observer = new ResizeObserver((entries) => {
125
+ for (const e of entries) {
126
+ const cssWidth = e.contentBoxSize?.[0]?.inlineSize ?? canvas.clientWidth;
127
+ const cssHeight = e.contentBoxSize?.[0]?.blockSize ?? canvas.clientHeight;
128
+ const w = Math.max(1, Math.floor(cssWidth * dpr));
129
+ const h = Math.max(1, Math.floor(cssHeight * dpr));
130
+ if (w !== sizeC.value.width || h !== sizeC.value.height) {
131
+ applySize(canvas, w, h);
132
+ ensureMsaaColor(w, h);
133
+ ensureDepth(w, h);
134
+ transact(() => {
135
+ sizeC.value = { width: w, height: h };
136
+ });
137
+ }
138
+ }
139
+ });
140
+ observer.observe(canvas);
141
+ }
142
+
143
+ const signature: FramebufferSignature = {
144
+ colors: HashMap.empty<string, GPUTextureFormat>().add(colorName, format),
145
+ sampleCount,
146
+ ...(opts.depthFormat !== undefined
147
+ ? { depthStencil: depthAttachmentSig(opts.depthFormat) }
148
+ : {}),
149
+ };
150
+
151
+ // The framebuffer aval depends on (size, frame). Reading it pulls
152
+ // a fresh swap-chain texture; the frameC bump after each markFrame()
153
+ // is what re-fires the chain on the same size.
154
+ const framebuffer: aval<IFramebuffer> = sizeC.bind((sz) =>
155
+ frameC.map(() => makeFramebuffer(ctx, signature, sz, msaaColor, depthTexture, colorName)),
156
+ );
157
+
158
+ return {
159
+ framebuffer,
160
+ size: sizeC,
161
+ signature,
162
+ markFrame() {
163
+ transact(() => { frameC.value = frameC.value + 1; });
164
+ },
165
+ dispose() {
166
+ if (observer !== undefined) observer.disconnect();
167
+ if (msaaColor !== undefined) { msaaColor.destroy(); msaaColor = undefined; }
168
+ if (depthTexture !== undefined) { depthTexture.destroy(); depthTexture = undefined; }
169
+ try { ctx.unconfigure(); } catch { /* already gone */ }
170
+ },
171
+ };
172
+ }
173
+
174
+ function configure(
175
+ ctx: GPUCanvasContext,
176
+ device: GPUDevice,
177
+ format: GPUTextureFormat,
178
+ alphaMode: GPUCanvasAlphaMode,
179
+ ): void {
180
+ ctx.configure({
181
+ device, format, alphaMode,
182
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC,
183
+ });
184
+ }
185
+
186
+ function currentSize(canvas: HTMLCanvasElement | OffscreenCanvas, dpr: number): { width: number; height: number } {
187
+ if ((canvas as HTMLCanvasElement).clientWidth !== undefined) {
188
+ const c = canvas as HTMLCanvasElement;
189
+ return {
190
+ width: Math.max(1, Math.floor((c.clientWidth || c.width) * dpr)),
191
+ height: Math.max(1, Math.floor((c.clientHeight || c.height) * dpr)),
192
+ };
193
+ }
194
+ const c = canvas as OffscreenCanvas;
195
+ return { width: c.width, height: c.height };
196
+ }
197
+
198
+ function applySize(canvas: HTMLCanvasElement | OffscreenCanvas, w: number, h: number): void {
199
+ if ((canvas as HTMLCanvasElement).clientWidth !== undefined) {
200
+ const c = canvas as HTMLCanvasElement;
201
+ c.width = w;
202
+ c.height = h;
203
+ } else {
204
+ const c = canvas as OffscreenCanvas;
205
+ c.width = w;
206
+ c.height = h;
207
+ }
208
+ }
209
+
210
+ function makeFramebuffer(
211
+ ctx: GPUCanvasContext,
212
+ signature: FramebufferSignature,
213
+ size: { width: number; height: number },
214
+ msaaColor: GPUTexture | undefined,
215
+ depthTexture: GPUTexture | undefined,
216
+ colorName: string,
217
+ ): IFramebuffer {
218
+ const swap = ctx.getCurrentTexture();
219
+ const swapView = swap.createView();
220
+ const msaa = signature.sampleCount > 1;
221
+ // Pass attachment view: the multisample texture when MSAA is on,
222
+ // otherwise the swap-chain texture directly.
223
+ const passView = msaa
224
+ ? (msaaColor ?? swap).createView()
225
+ : swapView;
226
+ const colors = HashMap.empty<string, GPUTextureView>().add(colorName, passView);
227
+ const resolveColors = msaa
228
+ ? HashMap.empty<string, GPUTextureView>().add(colorName, swapView)
229
+ : undefined;
230
+ // `colorTextures` exposes the *sampleable* texture (always the
231
+ // swap-chain — multisample textures are not sampleable in the
232
+ // normal sense).
233
+ const colorTextures = HashMap.empty<string, GPUTexture>().add(colorName, swap);
234
+ const fb: IFramebuffer = {
235
+ signature,
236
+ colors,
237
+ colorTextures,
238
+ ...(resolveColors !== undefined ? { resolveColors } : {}),
239
+ width: size.width,
240
+ height: size.height,
241
+ ...(depthTexture !== undefined
242
+ ? { depthStencil: depthTexture.createView(), depthStencilTexture: depthTexture }
243
+ : {}),
244
+ };
245
+ return fb;
246
+ }
247
+
248
+ function depthAttachmentSig(format: GPUTextureFormat) {
249
+ const hasDepth = /^depth/.test(format) || format === "depth24plus-stencil8" || format === "depth32float-stencil8";
250
+ const hasStencil = /stencil/.test(format);
251
+ return { format, hasDepth, hasStencil };
252
+ }
@@ -0,0 +1,14 @@
1
+ // Public API of @aardworx/wombat.rendering-window.
2
+
3
+ export {
4
+ attachCanvas,
5
+ type AttachCanvasOptions,
6
+ type CanvasAttachment,
7
+ } from "./canvas.js";
8
+
9
+ export {
10
+ runFrame,
11
+ type FrameCallback,
12
+ type FrameInfo,
13
+ type RunFrameOptions,
14
+ } from "./loop.js";
@@ -0,0 +1,62 @@
1
+ // runFrame — requestAnimationFrame-driven loop integrated with
2
+ // wombat.adaptive transactions. Each tick:
3
+ // 1. Bumps the canvas attachment's frame counter so its
4
+ // framebuffer aval invalidates and the next read returns the
5
+ // current swap-chain texture.
6
+ // 2. Calls the user's `frame(token)` callback. The user runs
7
+ // their `IRenderTask` here.
8
+ //
9
+ // Returns a `stop()` to cancel the loop.
10
+
11
+ import type { AdaptiveToken } from "@aardworx/wombat.adaptive";
12
+ import { AdaptiveToken as Tok } from "@aardworx/wombat.adaptive";
13
+ import type { CanvasAttachment } from "./canvas.js";
14
+
15
+ export interface RunFrameOptions {
16
+ /** Stop after N frames. Useful for tests + finite animations. */
17
+ readonly maxFrames?: number;
18
+ }
19
+
20
+ export type FrameCallback = (token: AdaptiveToken, info: FrameInfo) => void;
21
+
22
+ export interface FrameInfo {
23
+ readonly frame: number;
24
+ readonly timestampMs: number;
25
+ readonly deltaMs: number;
26
+ }
27
+
28
+ export function runFrame(
29
+ attachment: CanvasAttachment,
30
+ frame: FrameCallback,
31
+ opts: RunFrameOptions = {},
32
+ ): { stop(): void } {
33
+ let stopped = false;
34
+ let frameNo = 0;
35
+ let lastTime = performance.now();
36
+ let rafId = 0;
37
+
38
+ const tick = (now: number) => {
39
+ if (stopped) return;
40
+ const delta = now - lastTime;
41
+ lastTime = now;
42
+ attachment.markFrame();
43
+ try {
44
+ frame(Tok.top, { frame: frameNo, timestampMs: now, deltaMs: delta });
45
+ } catch (e) {
46
+ stopped = true;
47
+ console.error("runFrame: error in frame callback", e);
48
+ throw e;
49
+ }
50
+ frameNo++;
51
+ if (opts.maxFrames !== undefined && frameNo >= opts.maxFrames) return;
52
+ rafId = requestAnimationFrame(tick);
53
+ };
54
+
55
+ rafId = requestAnimationFrame(tick);
56
+ return {
57
+ stop() {
58
+ stopped = true;
59
+ cancelAnimationFrame(rafId);
60
+ },
61
+ };
62
+ }