Symbols in this topic
Surface
Canvas-backed render target created by surface(gpu, canvas, opts). Use it for browser canvases, OffscreenCanvas, multi-canvas rendering, and resize-driven derived targets.
Import
import type { Surface, SurfaceOptions, SurfaceResizeEvent } from "vgpu";Signature
import type { Target } from "vgpu";
interface SurfaceOptions {
readonly autoResize?: boolean;
readonly dpr?: number | readonly [number, number];
readonly size?: readonly [number, number];
readonly format?: GPUTextureFormat;
readonly alphaMode?: GPUCanvasAlphaMode;
readonly colorSpace?: PredefinedColorSpace;
readonly label?: string;
}
interface SurfaceResizeEvent {
readonly width: number;
readonly height: number;
readonly dpr: number;
readonly surface: Surface;
}
interface Surface extends Target {
readonly canvas: HTMLCanvasElement | OffscreenCanvas;
readonly context: GPUCanvasContext;
readonly autoResize: boolean;
readonly layoutBacked: boolean;
readonly dpr: number;
readonly disposed: boolean;
onResize(cb: (event: SurfaceResizeEvent) => void): () => void;
dispose(): void;
}Parameters
| Param | Type | Required | Default | Notes |
|---|---|---|---|---|
| surface.canvas | HTMLCanvasElement | OffscreenCanvas | ✔ | — | Must return a GPUCanvasContext from getContext("webgpu"). |
| surface.opts | SurfaceOptions | ✖ | {} | Canvas configuration and resize behavior. |
| opts.autoResize | boolean | ✖ | true for layout-backed canvases, false when size is provided or when the canvas has no numeric clientWidth | Auto-resize is checked at the frame boundary before user frame callbacks. Explicit true on buffer-only canvases throws. |
| opts.dpr | number | readonly [number, number] | ✖ | globalThis.devicePixelRatio ?? 1 | Number fixes DPR. Tuple clamps runtime DPR to [min, max]; layout-backed surfaces re-read DPR each frame. |
| opts.size | readonly [number, number] | ✖ | Layout-backed: clientWidth/clientHeight × dpr; buffer-only: existing canvas.width/height | Physical pixel size. When provided, initial canvas buffer is set and autoResize defaults to false. |
| opts.format | GPUTextureFormat | ✖ | navigator.gpu.getPreferredCanvasFormat() ?? "bgra8unorm" | Canvas swapchain format. |
| opts.alphaMode | GPUCanvasAlphaMode | ✖ | "premultiplied" | Passed to GPUCanvasContext.configure. |
| opts.colorSpace | PredefinedColorSpace | ✖ | "srgb" | Passed to GPUCanvasContext.configure. |
| opts.clearColor | ClearColor | ✖ | [0, 0, 0, 1] | Default clear color of this surface, used by passes that clear without naming one. Writable at runtime as surface.clearColor; a pass clear color still wins for that pass. Four finite numbers, or a GPUColor object. |
| opts.label | string | ✖ | undefined | Used in error messages and texture labels. |
| onResize.cb | (event: SurfaceResizeEvent) => void | ✔ | — | Called synchronously immediately on subscription and after future size changes. |
| event.width | number | ✔ | — | Physical pixel width, equal to surface.size[0] and canvas.width. |
| event.height | number | ✔ | — | Physical pixel height, equal to surface.size[1] and canvas.height. |
| event.dpr | number | ✔ | — | Effective DPR used for the current size. |
| event.surface | Surface | ✔ | — | Surface that resized, useful for shared handlers. |
| surface.resize.size | readonly [number, number] | ✔ | — | Manual physical pixel size. Values are floored and clamped to at least 1. |
Returns: surface(gpu) returns Surface; onResize() returns an unsubscribe function; dispose() returns void.
Throws: VGPU-SURFACE-CONTEXT when getContext("webgpu") returns null; VGPU-SURFACE-DUPLICATE when a live surface already owns the canvas; VGPU-SURFACE-AUTORESIZE-UNSUPPORTED for explicit autoResize: true on buffer-only canvases; VGPU-SURFACE-DISPOSED when using a disposed surface; VGPU-SURFACE-RESIZE-REENTRANT when resizing the same surface from its own resize callback; VGPU-FRAME-REENTRANT when frame(gpu) is called from any onResize callback. The immediate onResize fire on subscription also counts as being inside an onResize callback, so call frame(gpu) before subscribing or from code outside the callback.
Examples
import { init, effect, frame, surface } from "vgpu";
declare const canvas: HTMLCanvasElement;
const gpu = await init();
const canvasSurface = surface(gpu, canvas, { dpr: [1, 2] });
const wave = effect(gpu, `@fragment fn fs_main() -> @location(0) vec4f { return vec4f(0.2, 0.6, 1, 1); }`);
frame(gpu, (currentFrame) => {
currentFrame.pass({ target: canvasSurface }, (pass) => pass.draw(wave));
});import { init, effect, frame, surface, target } from "vgpu/mock";
const gpu = await init();
declare const canvas: HTMLCanvasElement;
const canvasSurface = surface(gpu, canvas);
const bloomSize = (w: number, h: number): [number, number] => [w / 2, h / 2];
const bloom = target(gpu, { size: bloomSize(canvasSurface.size[0], canvasSurface.size[1]) });
const brightPass = effect(gpu, `
struct Params { resolution: vec2f }
@group(0) @binding(0) var<uniform> params: Params;
@fragment fn fs_main() -> @location(0) vec4f { return vec4f(1); }
`, { set: { params: { resolution: bloom.size } } });
const composite = effect(gpu, `@fragment fn fs_main() -> @location(0) vec4f { return vec4f(1); }`);
canvasSurface.onResize(({ width, height }) => {
bloom.resize(bloomSize(width, height));
brightPass.set({ params: { resolution: bloom.size } });
});
frame(gpu, (currentFrame) => {
currentFrame.pass({ target: bloom }, (pass) => pass.draw(brightPass));
currentFrame.pass({ target: canvasSurface }, (pass) => pass.draw(composite));
});import { init, effect, frame, surface } from "vgpu";
declare const canvasA: HTMLCanvasElement;
declare const canvasB: HTMLCanvasElement;
const gpu = await init();
const main = surface(gpu, canvasA);
const preview = surface(gpu, canvasB, { autoResize: false, size: [320, 180] });
const shader = effect(gpu, `@fragment fn fs_main() -> @location(0) vec4f { return vec4f(1); }`);
frame(gpu, (currentFrame) => {
currentFrame.pass({ target: main }, (p) => p.draw(shader));
currentFrame.pass({ target: preview }, (p) => p.draw(shader));
});import { init, surface, target } from "vgpu";
declare const offscreen: OffscreenCanvas;
declare function postMessage(message: unknown): void;
const gpu = await init();
const canvasSurface = surface(gpu, offscreen);
const half = target(gpu, { size: [Math.max(1, canvasSurface.size[0] / 2), Math.max(1, canvasSurface.size[1] / 2)] });
canvasSurface.onResize(({ width, height }) => {
half.resize([width / 2, height / 2]);
postMessage({ type: "resized", width, height });
});
canvasSurface.resize([640, 360]);import { init, bundle, effect, frame, surface } from "vgpu/mock";
declare const canvas: HTMLCanvasElement;
const gpu = await init();
const canvasSurface = surface(gpu, canvas);
const draw = effect(gpu, `@fragment fn fs_main() -> @location(0) vec4f { return vec4f(1); }`);
let statics = bundle(gpu, { target: canvasSurface }, (recorded) => recorded.draw(draw));
// Drawing onto a resized surface keeps the same bundle valid as long as the render signature matches.
frame(gpu, (currentFrame) => currentFrame.pass({ target: canvasSurface }, (pass) => pass.bundles(statics)));Notes
- Use a
Surfacefor the swapchain/backbuffer: it is an ephemeral current-frame render target, not a stable reusable or ping-pong intermediate. Usetarget(gpu, ...)for intermediate, reusable, sampleable/readable images; seeTargetfor the contrast. - A surface pass may be the final presentation pass; do not use a surface as a ping-pong resource. For post-processing, render into a
Target, then sample it in a draw or effect targeting the surface in the same frame. - Layout-backed detection is structural:
typeof canvas.clientWidth === "number"; it does not useinstanceof. - Resize callbacks run in surface creation order at the frame boundary, before the user frame callback.
- Manual
surface.resize()fires callbacks synchronously at the call site and works forOffscreenCanvas. surface.read()returns RGBA bytes. Canvas formatsbgra8unormandbgra8unorm-srgbare supported and swizzled to RGBA, which matters on platforms wherenavigator.gpu.getPreferredCanvasFormat()returns BGRA.surface.readFloats()returns the same pixels decoded to aFloat32Arrayof components (unorm8canvas formats normalized to[0, 1]); it is the readback to use if a surface is ever configured with a float format.- A canvas can have only one live surface. Call
surface.dispose()before creating another one for the same canvas. - See also:
init,surface,Target,Frame,Bundle.