vgpu5 symbolsView source ↗

Symbols in this topic

Frame

frame() is both a callable one-frame submit helper and a FrameRunner. It creates one command encoder, lets you encode any number of explicit-target render passes, then submits once.

Import

TypeScript
1
import type { Frame, FramePass, FramePassOptions, FrameLoopHandle, FrameRunner } 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import type { Bundle, ClearColor, Draw, DrawCallOptions, Effect, Target, TimerSpan, Visibility, VisibilityQuery } from "vgpu";
 
interface FramePassOptions {
  readonly target: Target;
  readonly clear?: boolean | ClearColor;
  readonly clearDepth?: number;
  readonly clearStencil?: number;
  readonly depthReadOnly?: boolean;
  readonly viewport?: {
    readonly x?: number;
    readonly y?: number;
    readonly width: number;
    readonly height: number;
    readonly minDepth?: number;
    readonly maxDepth?: number;
  };
  readonly scissor?: readonly [number, number, number, number];
  readonly timer?: TimerSpan;
  readonly visibility?: Visibility;
}
 
interface FrameLoopHandle { stop(): void; }
interface FrameLoopOptions { readonly fps?: number; }
type FrameLoopCallback = (frame: Frame) => void;
 
declare class Frame {
  done: Promise<void>;
  pass(target: Target, body: Effect | Draw | ((pass: FramePass) => void)): void;
  pass(options: FramePassOptions, body: Effect | Draw | ((pass: FramePass) => void)): void;
  submit(): void;
  cancel(): void;
}
 
declare class FramePass {
  readonly target: Target;
  draw(drawable: Draw | Effect, opts?: DrawCallOptions): void;
  occlusion(query: VisibilityQuery, body: Draw | Effect | (() => void)): void;
  bundles(...bundles: readonly Bundle[]): void;
}
 
declare class FrameRunner {
  frame(cb?: (frame: Frame) => void): Frame;
  loop(cb: FrameLoopCallback, opts?: FrameLoopOptions): FrameLoopHandle;
}

Parameters

ParamTypeRequiredDefaultNotes
frame.cb(frame: Frame) => voidundefinedIf supplied, called and then frame.submit() runs in finally. If omitted, submit manually.
target.clearColorClearColor[0, 0, 0, 1]Writable default clear color of the pass target, used when pass clear is omitted or true. Set it at creation (surface(gpu, canvas, { clearColor }), target(gpu, { size, clearColor })) or assign it later. Assign a GPUColor object or [r, g, b, a].
frame.pass.targetTarget | FramePassOptionsPass a bare target for the allocation-free common case, or an options bag when customizing clear/preserve behavior.
opts.targetTargetRequired inside FramePassOptions. Use a Surface from surface(gpu, canvas) or an offscreen Target from target(gpu, { size }).
opts.clearboolean | ClearColortrueOmitted or true clears with target.clearColor; false preserves existing color and depth with load ops; a color clears this pass with that color.
opts.clearDepthnumber1Depth clear value used when the pass clears, in [0, 1]. Clear to 0 and give draws depth: { compare: "greater" } for reversed-Z, which evens out float depth precision and cuts distant z-fighting. Invalid alongside clear: false, which preserves depth, and on a target without depth (the value would have nowhere to land).
opts.clearStencilnumber0Stencil clear value used when the pass clears; integer in [0, 0xFFFFFFFF], masked to the stencil aspect's bit width by taking the LSBs (values above 0xFF are legal on 8-bit aspects). Clear to 0 before stencil-masking draws mark pixels via DrawOptions.stencil — portals, mirrors, UI cutouts. Requires a target depth format with a stencil aspect (e.g. depth: "depth24plus-stencil8"). Invalid alongside clear: false, which preserves stencil.
opts.depthReadOnlybooleanfalseOpens the pass with a read-only depth attachment: draws depth-test against it and may sample target.depth in the same pass — the soft-particles/SSAO setup. Every draw in the pass needs depth: { write: false } (or depth: false); the default depth state writes and throws VGPU-PASS-DEPTH-READONLY at encode. Effects always keep the writing default, so an Effect cannot run in a depthReadOnly pass on a depth target. Combined depth-stencil formats mark the stencil aspect read-only too. Requires a target with depth; invalid alongside clearDepth/clearStencil (color clear still applies), and invalid on MSAA targets, whose depth aspect is stored with storeOp: "discard" — there is no retained depth to read.
opts.viewport{ x?, y?, width, height, minDepth?, maxDepth? }full targetRestricts rasterization to a sub-rectangle for every draw in this pass. Use it for split-screen views, minimaps, and picture-in-picture insets. Floats (fractional values allowed); defaults x/y 0, minDepth 0, maxDepth 1. May extend past the target — validated at pass open against device limits, not the attachment.
opts.scissorreadonly [number, number, number, number]full targetClips every draw in this pass to [x, y, width, height]. Use it for UI clipping and partial redraw of damaged regions. Non-negative integers; x + width and y + height must fit the target's current pixel size at pass open (targets are resizable). Never affects the clear — loadOp: "clear" fills the whole attachment; to clear a sub-rectangle, draw it inside a scissored pass.
opts.timerTimerSpanundefinedTimes this pass on the GPU: pass timer.span(name) from a timer(gpu) (needs the "timestamp-query" device feature). See Timer for results, capacity, and feature gating.
opts.visibilityVisibilityundefinedEnables occlusion queries in this pass: pass a visibility(gpu) instance, then wrap proxy draws in pass.occlusion(handle, body). Requires a target with a depth attachment. See Visibility for handle semantics and capacity.
frame.pass.bodyEffect | Draw | ((pass: FramePass) => void)Pass a drawable directly for a single draw, or a callback to encode multiple draw and bundle commands.
pass.draw.drawableDraw | EffectA main API (vgpu) draw or fullscreen effect.
pass.draw.optsDrawCallOptions{}Per-call counts and dynamic offsets. Target is the frame pass target.
pass.occlusion.queryVisibilityQueryStable handle from vis.query(label) of the same visibility instance the pass was opened with. One use per handle per frame, across passes too.
pass.occlusion.bodyDraw | Effect | (() => void)Wrapped in beginOcclusionQuery/endOcclusionQuery. The body ALWAYS executes — it is the proxy the GPU measures; condition your real draws on q.hidden outside the scope.
pass.bundles.bundlesreadonly Bundle[]Bundles recorded by bundle(gpu, { target }, cb).
runner.loop.cb(frame: Frame) => voidCalled on each scheduled frame; frame is submitted in finally. Surface auto-resize runs before this callback.
runner.loop.opts.fpsnumber0 (uncapped)Positive values cap by minimum frame interval 1000 / fps; omitted or non-positive uses every rAF/timer tick.

Returns: frame(gpu) / FrameRunner.frame() return Frame; Frame.pass(), Frame.submit(), and Frame.cancel() return void; FramePass.draw(), .occlusion(), and .bundles() return void; loop() returns FrameLoopHandle with stop().

Throws:

  • VGPU-TARGET-REQUIRED — a runtime JS call omitted the frame pass target. Name a Surface or offscreen Target in every frame.pass.
  • VGPU-CLEAR-COLOR-INVALIDtarget.clearColor / surface.clearColor (at creation or on assignment) or a pass clear color is not four finite numbers. Assign [r, g, b, a] or a GPUColor object.
  • VGPU-PASS-PRESERVE-MSAAclear: false on an MSAA target; multisample attachments use storeOp: "discard", so there is nothing to preserve. Render accumulation/preserve passes into a non-MSAA target.
  • VGPU-PASS-CLEARDEPTH-INVALIDclearDepth is not a number in [0, 1], or the target has no depth attachment (the option would have no effect). Create the target with depth: true, or drop clearDepth.
  • VGPU-PASS-PRESERVE-CLEARDEPTHclearDepth combined with clear: false; preserved depth is never cleared. Drop one of the two.
  • VGPU-PASS-CLEARSTENCIL-INVALIDclearStencil is not an integer in [0, 0xFFFFFFFF], or the target's depth format has no stencil aspect (the option would have no effect). Create the target with depth: "depth24plus-stencil8".
  • VGPU-PASS-PRESERVE-CLEARSTENCILclearStencil combined with clear: false; preserved stencil is never cleared. Drop one of the two.
  • VGPU-PASS-DEPTH-READONLYdepthReadOnly is not a boolean, is set on a target without depth, or is combined with clearDepth/clearStencil (read-only aspects omit their load/store ops and are never cleared). Also thrown at encode when a draw writes depth — add depth: { write: false } — or writes stencil (a draw writes stencil when any stencil op on an unculled face is not "keep" and its stencil writeMask is nonzero — use "keep" ops or writeMask: 0); and when pass.bundles(...) is called in such a pass — bundle() records writable depth/stencil, and WebGPU only executes read-only-recorded bundles there, so encode those draws with pass.draw(...) instead.
  • VGPU-PASS-DEPTH-READONLY-MSAAdepthReadOnly on an MSAA target; multisampled depth is stored with storeOp: "discard", so a read-only pass would depth-test against discarded contents instead of failing loudly. Use a non-MSAA target for read-only depth.
  • VGPU-PASS-VIEWPORT-INVALIDviewport is malformed or outside device limits: width/height in [0, maxTextureDimension2D], x/y at least -2 × maxTextureDimension2D with x + width/y + height at most 2 × maxTextureDimension2D − 1, minDepth/maxDepth in [0, 1] with minDepth <= maxDepth. The message names the offending field.
  • VGPU-PASS-SCISSOR-INVALIDscissor is malformed or exceeds the target's current pixel size. Shrink the rectangle, or resize it from surface.onResize(...).
  • VGPU-TIMER-INVALIDtimer is not a TimerSpan from timer.span(name), repeats a span name within one frame, or belongs to a different gpu's or disposed timer.
  • VGPU-TIMER-CAPACITY — one frame timed more spans than the per-frame limit (see Timer).
  • VGPU-VIS-INVALIDvisibility is not a Visibility from this gpu's visibility(gpu), or occlusion() received a non-VisibilityQuery value or a handle of a different visibility instance.
  • VGPU-VIS-NO-DEPTHvisibility on a pass whose target has no depth attachment; nothing is depth-tested, so every query would report visible. Create the target with depth: true.
  • VGPU-VIS-DISPOSED — the visibility instance or the handle is disposed.
  • VGPU-VIS-CAPACITY — the occlusion() call exceeded the declared capacity. Raise VisibilityOptions.capacity.
  • VGPU-QUERY-NO-VISIBILITYocclusion() in a pass opened without visibility. Pass the instance in FramePassOptions.visibility.
  • VGPU-QUERY-NESTEDocclusion() inside an active occlusion() body; one scope at a time. Close the outer scope first.
  • VGPU-QUERY-DUPLICATE — a handle used twice within one frame, across passes too (see Visibility). Create one handle per queried object.
  • VGPU-FRAME-REENTRANT — a frame started from another frame or from a surface resize callback. Encode everything in one frame callback.
  • VGPU-FRAME-CANCELED — a frame.pass(...), or a retained FramePass operation, on a frame closed by cancel(); its command encoder was dropped, so the work would never run. Open a new frame(gpu).
  • VGPU-FRAME-PASS-ACTIVEframe.cancel() from inside that frame's active pass callback. Return from frame.pass(...) first, then cancel, so resources referenced by the native pass descriptor stay alive until the pass closes.
  • VGPU-FRAME-SUBMITTEDframe.cancel() on a frame that was already submitted; queued GPU work cannot be taken back, and the frame needs no cleanup. Cancel only frames you decided not to submit.
  • VGPU-R3-BUNDLE-STALE / VGPU-R3-BUNDLE-INVALID — replaying a bundle whose recorded resources changed identity, or a value not created by bundle(). Re-record the bundle.
  • Binding errors such as VGPU-R1-BINDING-NEVER-SET propagate during encoding. Raw claimed-group validation is delivered asynchronously through gpu.onError.

Examples

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { init, draw, frame, target } from "vgpu/mock";
 
const gpu = await init();
const scene = target(gpu, { size: [64, 64], format: "rgba8unorm", clearColor: [0.02, 0.02, 0.04, 1] });
scene.clearColor = [0.02, 0.02, 0.04, 1]; // and it stays writable at runtime
const drawable = draw(gpu, { shader: `
  @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
    var p = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
    return vec4f(p[vi], 0, 1);
  }
  @fragment fn fs_main() -> @location(0) vec4f { return vec4f(0.2, 0.4, 1.0, 1.0); }
` });
 
frame(gpu, (currentFrame) => {
  currentFrame.pass(scene, (pass) => pass.draw(drawable)); // clears with scene.clearColor
});
TypeScript
1
2
3
4
5
6
7
8
9
import { init, effect, frameLoop, target } from "vgpu/mock";
 
const gpu = await init();
const colorTarget = target(gpu, { size: [16, 16] });
const shader = effect(gpu, `@fragment fn fs_main() -> @location(0) vec4f { return vec4f(1); }`);
const handle = frameLoop(gpu, (frame) => {
  frame.pass({ target: colorTarget, clear: false }, shader); // preserve color and depth
}, { fps: 30 });
handle.stop();
TypeScript
1
2
3
4
5
6
7
8
9
10
11
import { init, effect, frame, target } from "vgpu/mock";
 
const gpu = await init();
const screen = target(gpu, { size: [640, 360] });
const p1View = effect(gpu, `@fragment fn fs_main() -> @location(0) vec4f { return vec4f(0.1, 0.3, 0.6, 1); }`);
const p2View = effect(gpu, `@fragment fn fs_main() -> @location(0) vec4f { return vec4f(0.6, 0.3, 0.1, 1); }`);
 
frame(gpu, (currentFrame) => {
  currentFrame.pass({ target: screen, viewport: { width: 320, height: 360 } }, p1View); // player 1, left half
  currentFrame.pass({ target: screen, clear: false, viewport: { x: 320, width: 320, height: 360 } }, p2View); // player 2, right half
});

Split-screen: the first pass clears the whole target and rasterizes one camera into the left viewport; the second preserves it (clear: false) and rasterizes the other camera into the right viewport.

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
import { init, draw, effect, frame, target } from "vgpu/mock";
 
const gpu = await init();
const scene = target(gpu, { size: [256, 256], depth: true });
const opaque = effect(gpu, `@fragment fn fs_main() -> @location(0) vec4f { return vec4f(0.2, 0.5, 0.2, 1); }`);
const particles = draw(gpu, {
  shader: `
    @group(0) @binding(0) var sceneDepth: texture_depth_2d;
    @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
      var p = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
      return vec4f(p[vi], 0.4, 1);
    }
    @fragment fn fs_main(@builtin(position) pos: vec4f) -> @location(0) vec4f {
      let d = textureLoad(sceneDepth, vec2i(pos.xy), 0);
      let fade = clamp((d - pos.z) * 32.0, 0.0, 1.0); // fade out where the particle nears geometry
      return vec4f(0.9, 0.6, 0.3, 1.0) * fade;
    }
  `,
  depth: { write: false }, // required: the pass depth is read-only
  blend: "additive",
  set: { sceneDepth: scene.depth },
});
 
frame(gpu, (currentFrame) => {
  currentFrame.pass(scene, opaque); // pass 1: opaque geometry writes depth
  currentFrame.pass({ target: scene, clear: false, depthReadOnly: true }, particles); // pass 2: test + sample that depth
});

Soft particles: pass 2 depth-tests against the opaque depth while sampling the same scene.depth texture, which WebGPU only allows because the attachment is read-only.

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { init, effect, frame, target, visibility } from "vgpu/mock";
 
const gpu = await init();
const scene = target(gpu, { size: [64, 64], depth: true });
const vis = visibility(gpu);
const proxy = effect(gpu, `@fragment fn fs_main() -> @location(0) vec4f { return vec4f(1); }`);
 
const currentFrame = frame(gpu); // manual frame: nothing submits it for you
currentFrame.pass({ target: scene, visibility: vis }, (pass) => pass.occlusion(vis.query("statue"), proxy));
 
if (sceneChangedUnderneath()) {
  currentFrame.cancel(); // no GPU work runs, and the occlusion query set stops being retained for this frame
  vis.dispose(); // released now instead of at gpu.dispose()
} else {
  currentFrame.submit();
}
 
function sceneChangedUnderneath(): boolean { return true; }

Cancelling a manual frame: cancel() drops the encoder and releases the telemetry retains the frame took, so a timer(gpu) / visibility(gpu) can be disposed for good without submitting work you no longer want.

Notes

  • Frame, FramePass, and FrameRunner are type-only public exports. Create frames through frame(), not new Frame(...).
  • There is no default target and no implicit canvas target; every frame.pass names its target.
  • Omitted clear and clear: true clear with target.clearColor. Pass a color to clear one pass with that color without changing the target default: pass color > target.clearColor > the built-in [0, 0, 0, 1].
  • Draws replayed via pass.bundles(...) inside an occlusion scope count toward the active query — render bundles have no query methods, but their draws execute inside the scope.
  • viewport and scissor are set once right after the pass opens and apply to every draw in the pass, including replayed bundles. Both are in physical pixels: surfaces size their textures by devicePixelRatio, so a CSS-pixel rectangle must be scaled by dpr.
  • clear: false preserves color and depth contents within the same target. On Surface, repeated passes in one frame layer onto the same current texture; the first preserved surface pass of a new browser frame reads the swapchain's fresh contents, not the previous frame's image.
  • Hot loops: options bags and pass callbacks are read synchronously, so you can hoist and reuse them. For zero-per-frame-JS-cost replay, record stable work with bundle() and replay the bundle.
  • frame.cancel() discards a frame you decided not to submit: its command encoder is dropped, so nothing it encoded ever runs, and every timer(gpu) / visibility(gpu) attached to it releases the query set it was holding for that frame — no result, no phantom timing, no phantom "hidden". It is the explicit way out of the retain a manual frame(gpu) otherwise keeps until gpu.dispose(): a frame is never assumed abandoned, because an old frame can still be submitted. Frames run by frame(gpu, cb) / frameLoop(gpu, cb) submit themselves and need no cancel.
  • Cancelling is idempotent, like submitting: a second cancel() does nothing, and submit() after cancel() is a no-op — so calling cancel() in a frame(gpu, cb) callback after its frame.pass(...) calls have returned is safe, the runner's submit in finally simply finds a closed frame. cancel() from inside an active pass callback throws VGPU-FRAME-PASS-ACTIVE, because the native pass descriptor still references its telemetry resources; return from frame.pass(...) before canceling. The reverse is also an error: cancel() after submit() throws VGPU-FRAME-SUBMITTED (the work is already on the queue and cannot be taken back), and pass() or a retained FramePass operation after cancel() throws VGPU-FRAME-CANCELED (it would encode into a dropped encoder and silently never run).
  • frame.done is resolve-only. Await it as a completion/timing signal for readbacks, benchmarks, deterministic tests, or teardown; use gpu.onError plus await gpu.settled() for asynchronous errors.
  • Do not await frame.done inside a RAF/frame loop. Schedule the next frame as soon as frame(gpu) returns, or you serialize CPU and GPU work.
  • See also: frame, Surface, Effect, Draw, Bundle, Target, Timer, Visibility.