@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.
- package/package.json +67 -0
- package/src/commands/clear.ts +66 -0
- package/src/commands/index.ts +11 -0
- package/src/commands/render.ts +106 -0
- package/src/core/acquire.ts +22 -0
- package/src/core/adaptiveResource.ts +88 -0
- package/src/core/buffer.ts +45 -0
- package/src/core/bufferView.ts +26 -0
- package/src/core/clear.ts +16 -0
- package/src/core/command.ts +50 -0
- package/src/core/drawCall.ts +22 -0
- package/src/core/framebuffer.ts +37 -0
- package/src/core/framebufferSignature.ts +19 -0
- package/src/core/index.ts +116 -0
- package/src/core/pipelineState.ts +64 -0
- package/src/core/renderContext.ts +48 -0
- package/src/core/renderObject.ts +57 -0
- package/src/core/renderTask.ts +17 -0
- package/src/core/renderTree.ts +29 -0
- package/src/core/sampler.ts +12 -0
- package/src/core/shader.ts +28 -0
- package/src/core/texture.ts +63 -0
- package/src/index.ts +18 -0
- package/src/resources/adaptiveBuffer.ts +135 -0
- package/src/resources/adaptiveSampler.ts +82 -0
- package/src/resources/adaptiveTexture.ts +269 -0
- package/src/resources/framebuffer.ts +163 -0
- package/src/resources/framebufferSignature.ts +36 -0
- package/src/resources/index.ts +82 -0
- package/src/resources/mipGen.ts +148 -0
- package/src/resources/preparedComputeShader.ts +426 -0
- package/src/resources/preparedRenderObject.ts +498 -0
- package/src/resources/renderPipeline.ts +148 -0
- package/src/resources/shaderDiagnostics.ts +52 -0
- package/src/resources/sourceMapDecoder.ts +133 -0
- package/src/resources/uniformBuffer.ts +170 -0
- package/src/resources/webgpuFlags.ts +43 -0
- package/src/runtime/copy.ts +13 -0
- package/src/runtime/index.ts +24 -0
- package/src/runtime/renderTask.ts +137 -0
- package/src/runtime/renderTo.ts +154 -0
- package/src/runtime/runtime.ts +113 -0
- package/src/runtime/scenePass.ts +273 -0
- package/src/window/canvas.ts +252 -0
- package/src/window/index.ts +14 -0
- 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
|
+
}
|