vgpu3 symbolsView source ↗

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

TypeScript
1
import type { Surface, SurfaceOptions, SurfaceResizeEvent } from "vgpu";

Signature

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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

ParamTypeRequiredDefaultNotes
surface.canvasHTMLCanvasElement | OffscreenCanvasMust return a GPUCanvasContext from getContext("webgpu").
surface.optsSurfaceOptions{}Canvas configuration and resize behavior.
opts.autoResizebooleantrue for layout-backed canvases, false when size is provided or when the canvas has no numeric clientWidthAuto-resize is checked at the frame boundary before user frame callbacks. Explicit true on buffer-only canvases throws.
opts.dprnumber | readonly [number, number]globalThis.devicePixelRatio ?? 1Number fixes DPR. Tuple clamps runtime DPR to [min, max]; layout-backed surfaces re-read DPR each frame.
opts.sizereadonly [number, number]Layout-backed: clientWidth/clientHeight × dpr; buffer-only: existing canvas.width/heightPhysical pixel size. When provided, initial canvas buffer is set and autoResize defaults to false.
opts.formatGPUTextureFormatnavigator.gpu.getPreferredCanvasFormat() ?? "bgra8unorm"Canvas swapchain format.
opts.alphaModeGPUCanvasAlphaMode"premultiplied"Passed to GPUCanvasContext.configure.
opts.colorSpacePredefinedColorSpace"srgb"Passed to GPUCanvasContext.configure.
opts.clearColorClearColor[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.labelstringundefinedUsed in error messages and texture labels.
onResize.cb(event: SurfaceResizeEvent) => voidCalled synchronously immediately on subscription and after future size changes.
event.widthnumberPhysical pixel width, equal to surface.size[0] and canvas.width.
event.heightnumberPhysical pixel height, equal to surface.size[1] and canvas.height.
event.dprnumberEffective DPR used for the current size.
event.surfaceSurfaceSurface that resized, useful for shared handlers.
surface.resize.sizereadonly [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

TypeScript
1
2
3
4
5
6
7
8
9
10
11
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));
});
TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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));
});
TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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));
});
TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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]);
TypeScript
1
2
3
4
5
6
7
8
9
10
11
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 Surface for the swapchain/backbuffer: it is an ephemeral current-frame render target, not a stable reusable or ping-pong intermediate. Use target(gpu, ...) for intermediate, reusable, sampleable/readable images; see Target for 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 use instanceof.
  • 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 for OffscreenCanvas.
  • surface.read() returns RGBA bytes. Canvas formats bgra8unorm and bgra8unorm-srgb are supported and swizzled to RGBA, which matters on platforms where navigator.gpu.getPreferredCanvasFormat() returns BGRA.
  • surface.readFloats() returns the same pixels decoded to a Float32Array of components (unorm8 canvas 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.