Make the frame throw to see writes arrive while recorded commands and callbacks are dropped. Calls appear on the left and GPU execution on the right, with queue writes before the command buffer.
Modules record into an encoder they are handed. The loop makes one each frame, runs your recorder with it and submits once, so a GPU capture shows the whole frame.
Records a frame into one encoder and submits once, with a clock. Borrows the root. The
synchronous recorder borrows each frame's encoder.
@submits ― always once a frame after your recorder returns, and again at each frame.submit()
@param ― record your frame, recorded into each frame's encoder
@returns ― a loop you run with each frame's time and destroy
create(
constroot:TgpuRoot
root,({
encoder: TgpuCommandEncoder
Records commands for submission after the recorder returns. submit() replaces this encoder.
Read it at each call instead of keeping it, and let the frame submit it.
encoder,
time: number
Reports seconds since the first submitted frame.
time,
dt: number
Reports seconds since the previous submitted frame. The first frame has a dt of zero.
Runs the loop on each scheduled frame. Time advances by at most maxDt per frame, never steps
back, and stops while the surface is paused. Destroying the animation keeps the loop alive.
Another animation on the same loop continues from its recorded time. A frame error stops the
animation.
@param ― loop the loop to run on each unpaused frame. Continues from its seconds if it ran before
@returns ― an animation you destroy to stop the frames
animate(
constloop:frames.Frames
loop);// calls loop.run(seconds) at every animation frame
time is seconds since the first submitted frame. dt is seconds since the previous frame and is 0 on the first. index counts frames submitted before this one. loop.seconds holds the time of the last submitted frame, so a frame that throws leaves it unchanged.
interval estimates the display’s frame interval. It follows the shortest recent frames, so a frame that misses the display barely moves it. Budget per-frame work with it, as dynamic resolution does.
The loop schedules nothing. run(seconds) records one frame and submits it; a time that isn’t finite, or goes back, throws a RangeError.
The recorder is synchronous. Returning a promise throws a TypeError, and so does calling run from inside it.
If the recorder throws or calls destroy(), the frame submits no further commands and the clock doesn’t advance. Queue writes and commands already sent with frame.submit() still run. After destroy(), run returns false.
frames.animate(loop, options) runs the loop at every animation frame, on the web and React Native.
Each frame advances time by at most maxDt, which defaults to 0.1 s. This caps the time added after a stall or a hidden tab resumes.
With a surface, animation and time stop while it is paused. The first frame after resume() has a dt of 0. On React Native, pause it from AppState. On the web, pause it for a canvas scrolled out of view or a pause menu; browsers already stop a hidden tab’s frames.
destroy() on what it returns requests no more frames; inside a frame, such as at game over, it makes that frame the last. The loop stays yours.
An error ends animation and reaches onError once. Without onError, the animation frame callback throws it.
frame.time is the loop’s own time, so it drifts from performance.now() after the first pause or stall. Read performance.now() for wall time.
A canvas that draws only while on screen pauses its surface from an IntersectionObserver:
Runs the loop on each scheduled frame. Time advances by at most maxDt per frame, never steps
back, and stops while the surface is paused. Destroying the animation keeps the loop alive.
Another animation on the same loop continues from its recorded time. A frame error stops the
animation.
@param ― loop the loop to run on each unpaused frame. Continues from its seconds if it ran before
@returns ― an animation you destroy to stop the frames
animate(
constloop:frames.Frames
loop,{
AnimateOptions.surface?:{
readonly paused: boolean;
}|undefined
Stops frames and elapsed time while paused is true. The first frame after resume() has a
dt of 0. On React Native, pass the canvas surface and pause it from AppState. On the web,
pause it from an IntersectionObserver or a pause menu. Without a surface, frames run until
the animation is destroyed. Default none.
surface});
new
var IntersectionObserver:new(callback:IntersectionObserverCallback,options?:IntersectionObserverInit)=>IntersectionObserver
The IntersectionObserver interface of the Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document's viewport. The ancestor element or viewport is referred to as the root.
The isIntersecting read-only property of the IntersectionObserverEntry interface is a Boolean value which is true if the target element intersects with the intersection observer's root.
The observe() method of the IntersectionObserver interface adds an element to the set of target elements being watched by the IntersectionObserver. One observer has one set of thresholds and one root, but can watch multiple target elements for visibility changes in keeping with those.
A readback records its copy in the frame but can only map it once the copy is submitted. A module that has to wait for the submit registers a callback while it records:
function
functioncreateProbe(root:TgpuRoot):{
record(frame:Frame):void;
}
createProbe(
root: TgpuRoot
root:
(alias)interfaceTgpuRoot
importTgpuRoot
TgpuRoot/* , ... */){
const
constmap:()=>void
map=()=>{
// staging.mapAsync(GPUMapMode.READ).then(...)
};
return{
functionrecord(frame:Frame):void
record(
frame: Frame
frame:
(alias)interfaceFrame
importFrame
Represents one frame being recorded. Each run creates a separate object, so modules can
compare frames by identity.
Frame){
// copy into the staging buffer on frame.encoder
frame: Frame
frame.
Frame.afterSubmit(callback: ()=>void): void
Calls callback after the frame's last submit, in registration order. Drops it if the frame
does not finish. Throws unless this frame is recording, so a retained frame cannot add
callbacks to a later one. Create the callback outside the frame to avoid allocating.
afterSubmit(
constmap:()=>void
map);
},
};
}
Callbacks run right after the frame’s last submit, in registration order, and only for frames that finish.
Every callback runs even if one throws. The frame still counts. run throws the callback error afterwards, or an AggregateError if several failed.
afterSubmit throws unless its own frame is recording. Each run makes a new Frame, so a module that acts once a frame tells frames apart by it.
Create the callback once, as above, so recording allocates nothing.
frame.submit() submits the commands recorded so far. The next access to frame.encoder creates another encoder, and the loop submits the remaining commands when your recorder returns. Try the per-step values first. Each early submit adds a queue.submit and splits the frame in a GPU capture.
function
functionframe(frame:Frame):void
frame(
frame: Frame
frame:
(alias)interfaceFrame
importFrame
Represents one frame being recorded. Each run creates a separate object, so modules can
compare frames by identity.
Records commands for submission after the recorder returns. submit() replaces this encoder.
Read it at each call instead of keeping it, and let the frame submit it.
Submits the recorded commands and replaces encoder for further recording. Each call adds one
queue.submit. End every pass first, because an open pass invalidates the command buffer.
Only the program's recorder should call this. Throws unless this frame is recording. Does
nothing after the loop is destroyed.
submit();// queued ahead of the next write
}
// the draws go into a fresh frame.encoder, which the loop submits
}
End every pass first. A submit with a pass open is a WebGPU validation error that voids everything recorded since the last submit; a recording root throws there instead.
Only the recorder you pass to frames.create calls it; a module never submits. A module reads frame.encoder at each call and never keeps it, since a kept encoder may have been submitted.
A recording method that takes a frame or an encoder reads the encoder with frames.encoderOf(target) at each call.
A frame ending with submit() sends no empty command buffer. It creates another encoder only if frame.encoder is read again.
Submit once per frame unless an early submit is required. Use one when:
A step reads a value you can’t supply another way, such as a library kernel that reads its own createUniform.
A library call submits its own work during a frame. Examples include a pipeline drawn without an encoder and a mesh pool’s add without an encoder. That work runs before your frame’s pending commands. Submit first to preserve order, or pass the encoder where supported.
A command may be invalid. WebGPU invalidates its whole command buffer, while commands submitted before it still run.
The GPU could start sooner. Submitting the simulation before recording draws lets the GPU work while the CPU records the rest.
A benchmark or GPU capture needs the same steps on every run, regardless of display rate. Leave out frames.animate and web.host, make the loop yourself, and call run once per step:
The performance.now() method returns a high resolution timestamp in milliseconds. It represents the time elapsed since Performance.timeOrigin (the time when navigation has started in window contexts, or the time when the worker is run in Worker and ServiceWorker contexts).
Records and submits one frame with a fresh Frame and encoder. Requires finite, nondecreasing
seconds. Throws for an invalid time, a nested frame or an asynchronous recorder. Returns
false if the loop is destroyed before or during recording. Runs all after-submit callbacks. If
any throw, the frame still counts and run rethrows their errors.
run(
let i:number
i*
constSTEP:number
STEP);// one step, one submit
}
await
constroot:TgpuRoot
root.
TgpuRoot.device: GPUDevice
The GPU device associated with this root.
device.
GPUDevice.queue: GPUQueue
The queue read-only property of the GPUDevice interface returns the primary GPUQueue for the device.
Returns a Promise that resolves once this queue finishes processing all the work submitted
up to this moment.
Resolution of this Promise implies the completion of
The performance.now() method returns a high resolution timestamp in milliseconds. It represents the time elapsed since Performance.timeOrigin (the time when navigation has started in window contexts, or the time when the worker is run in Worker and ServiceWorker contexts).
run takes the time, not the step, so i * STEP gives every frame a dt of one step.
Each run submits, so a uniform written in one step lands after the step before it was submitted. No count on the GPU is needed here.
onSubmittedWorkDone() resolves once the GPU has finished everything submitted. Wait for it before stopping a timer or reading results.
A fixed step of the same length runs exactly one step per frame. A canvas surface of a fixed size draws the same pixels on every screen, and offscreenSurface from @typegpu/kit/testing gives the scene a surface without a canvas.