Skip to content

@typegpu/kitcoroutines

Coroutines

Functions that pause at yield* and resume when your frame calls the scheduler.

kit.coroutines

cpu

coroutines.create(options?) → Coroutines

coroutines.wait(parts)

coroutines.frames(count) → Wait

coroutines.seconds(duration) → Wait

and coroutines.budget, coroutines.deferred, coroutines.until

Coroutinesreturns
a scheduler you start coroutines on and destroy
partstakes
ready, and optionally value, begin and leave
counttakes
how many frames to wait
Waitreturns
a wait to yield*
durationtakes
seconds to wait
helpers
nextFrame
types
SuspensionWaitDeferredCoroutineCoroutines
Optionscreate · 3 fields
onError?

(error: unknown) => void= reportError where available, otherwise console.error

Receives errors from failed coroutines with no waiters, and from cleanup during cancellation.

now?

() => number= performance.now()

Supplies the wall clock in milliseconds for resume timestamps and budget.

time?

(frame: Frame) => number= frame.time

Supplies the clock in seconds that seconds waits count, such as a fixed step's time, so timed waits stop while a game is paused.

records
  • runframe

Call run once per frame while recording to resume coroutines whose waits are complete.

lifecycle

destroy()[Symbol.dispose]

Press the switch and step through openDoor. The marked line is paused, and white lines ran in the last frame.

A door may open half a second after its switch, a level may load while the game draws, or a pick may wait for the GPU. A frame loop needs flags and timers to track these tasks. A coroutine pauses at yield* and resumes when your frame calls the scheduler. An async function can resume between frames.

const
const door: {
open: number;
}
door
= {
open: number
open
: 0 };
const
const scheduler: coroutines.Coroutines
scheduler
=
import coroutines
coroutines
.
function create(options?: coroutines.Options): coroutines.Coroutines

Creates a scheduler for coroutines that advance when you call run with a frame.

@returns ― a scheduler you start coroutines on and destroy

create
();
function*
function openDoor(): Generator<coroutines.Wait<unknown>, void, unknown>
openDoor
() {
yield*
import coroutines
coroutines
.
function seconds(duration: number): coroutines.Wait<frames.Frame>

Waits until the scheduler's time has advanced by duration seconds, frame time by default. Throws unless duration is finite and nonnegative.

@param ― duration seconds to wait

@returns ― a wait to yield*

seconds
(0.5);
while (
const door: {
open: number;
}
door
.
open: number
open
< 1) {
const
const frame: frames.Frame
frame
= yield*
import coroutines
coroutines
.
const nextFrame: coroutines.Wait<frames.Frame>

Waits until run in the next frame. Coroutines share this wait, and resuming through it allocates nothing.

nextFrame
;
const door: {
open: number;
}
door
.
open: number
open
=
var Math: Math

An intrinsic object that provides basic mathematics functionality and constants.

Math
.
Math.min(...values: number[]): number

Returns the smaller of a set of supplied numeric expressions.

@param ― values Numeric expressions to be evaluated.

min
(
const door: {
open: number;
}
door
.
open: number
open
+
const frame: frames.Frame
frame
.
Frame.dt: number

Reports seconds since the previous submitted frame. The first frame has a dt of zero.

dt
* 2, 1);
}
}
function onSwitch(press: () => void): void
onSwitch
(() =>
const scheduler: coroutines.Coroutines
scheduler
.
Coroutines.start<void>(process: Generator<coroutines.Wait<unknown>, void, unknown>): coroutines.Coroutine<void>

Schedules a coroutine for the next run, or later in the current run if called from a coroutine. If the caller first yields it with yield* scheduler.start(child()), the child starts at once and the caller owns it. Cancelling the owner cancels the child. Cancelling other waiters only stops their waits. After destroy, returns a cancelled coroutine without running it.

start
(
function openDoor(): Generator<coroutines.Wait<unknown>, void, unknown>
openDoor
()));
import frames
frames
.
function create(root: TgpuRoot, record: Recorder): frames.Frames

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
(
const root: TgpuRoot
root
, (
frame: frames.Frame
frame
) => {
const scheduler: coroutines.Coroutines
scheduler
.
Coroutines.run(frame: frames.Frame): void

Resumes every coroutine whose wait is complete. Call while a frame is recording. Coroutine and wait errors go to the coroutine's yield* or to onError, rather than escaping run.

run
(
frame: frames.Frame
frame
); // resumes every coroutine whose wait is over
// ... your passes draw the door at door.open
});

Coroutines run only when you call scheduler.run(frame). Place the call where their work belongs in the frame.

Wait Resumes with When
coroutines.nextFrame the frame at the next frame’s run
coroutines.frames(n) the frame n frames later
coroutines.seconds(s) the frame once the scheduler’s time has advanced by s
coroutines.budget(ms) the frame in the same frame while it has run for less than ms since resuming, else next
scheduler.start(p) p’s return value when p finishes; its error is thrown here
coroutines.until(p) p’s value at the first run after promise p settles; a rejection is thrown here
  • A wait that could never resume, such as coroutines.seconds(NaN), throws when you make it.
  • Each frames(n), seconds(s) or budget(ms) call allocates a wait. Create it outside a hot loop. Resuming through a shared wait allocates nothing.
  • seconds counts frame.time unless you pass coroutines.create({ time }). Pass time: () => sim.time to count a fixed step’s time, so timed waits stop while the game is paused.
  • budget measures the wall clock, performance.now() unless you pass now.

A started coroutine is also a wait. yield* scheduler.start(pick(x, y)) resumes with what pick returns, as await does. Here pick asks the picker for the candidate under a point and waits for its readback:

const
const scheduler: coroutines.Coroutines
scheduler
=
import coroutines
coroutines
.
function create(options?: coroutines.Options): coroutines.Coroutines

Creates a scheduler for coroutines that advance when you call run with a frame.

@returns ― a scheduler you start coroutines on and destroy

create
();
let
let point: [number, number] | undefined
point
: [number, number] | undefined;
const
const loop: frames.Frames
loop
=
import frames
frames
.
function create(root: TgpuRoot, record: Recorder): frames.Frames

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
(
const root: TgpuRoot
root
, (
frame: frames.Frame
frame
) => {
const scheduler: coroutines.Coroutines
scheduler
.
Coroutines.run(frame: frames.Frame): void

Resumes every coroutine whose wait is complete. Call while a frame is recording. Coroutine and wait errors go to the coroutine's yield* or to onError, rather than escaping run.

run
(
frame: frames.Frame
frame
); // before the passes, so a pick asked for here is recorded below
const
const compute: TgpuComputePass
compute
=
frame: frames.Frame
frame
.
Frame.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
.
TgpuCommandEncoder.beginComputePass(descriptor?: TgpuComputePassDescriptor): TgpuComputePass

Begins recording a compute pass

beginComputePass
();
if (
let point: [number, number] | undefined
point
)
const hits: picker.Picker
hits
.
Picker.pick(pass: TgpuComputePass, x: number, y: number): void

Picks at x, y from 0 to 1 across the view, from the top-left corner, as view.rayAt and pointing take them. Uses the camera given to create. Records one pick per frame. A second call replaces the first.

pick
(
const compute: TgpuComputePass
compute
, ...
let point: [number, number]
point
);
let point: [number, number] | undefined
point
=
var undefined
undefined
;
const compute: TgpuComputePass
compute
.
TgpuComputePass.end(): void

Completes the recording of this compute pass

end
();
const hits: picker.Picker
hits
.
Picker.result: Readback<number | null>

Reads the picked candidate index to the CPU, or null if nothing was found. Capture after the picker pass, then call take() or await next(). Without hz, each pick requests a copy for the next capture.

result
.
Readback<number | null>.capture(frame: frames.Frame): void

Records a copy when a next() is waiting, a request is pending or the capture rate is due. Maps the copy after submission. Call after the pass that writes the buffer. A second call in the same frame does nothing.

capture
(
frame: frames.Frame
frame
); // after the pass that picks
// ... your render passes
});
function*
function pick(x: number, y: number): Generator<coroutines.Wait<unknown>, number | null, unknown>
pick
(
x: number
x
: number,
y: number
y
: number) {
let point: [number, number] | undefined
point
= [
x: number
x
,
y: number
y
];
return yield*
import coroutines
coroutines
.
function until<number | null>(promise: PromiseLike<number | null>): coroutines.Wait<number | null>

Waits for a promise, such as a readback's next() or pipelines.compile. Resumes at the first run after it settles, with its value or its error.

@param ― promise the promise to wait for

@returns ― a wait to yield*

until
(
const hits: picker.Picker
hits
.
Picker.result: Readback<number | null>

Reads the picked candidate index to the CPU, or null if nothing was found. Capture after the picker pass, then call take() or await next(). Without hz, each pick requests a copy for the next capture.

result
.
Readback<number | null>.next(): Promise<number | null>

Returns a promise for the value from the next capture, and requests that capture. Promises asked for before one capture share its copy. Rejects if the readback is destroyed or mapping fails. Waits indefinitely without a capture. In a coroutine, wait with yield* coroutines.until(probe.next()).

next
()); // a frame or two later
}
function*
function game(): Generator<coroutines.Wait<unknown>, void, unknown>
game
() {
const candidate = yield*
const scheduler: coroutines.Coroutines
scheduler
.
Coroutines.start<number | null>(process: Generator<coroutines.Wait<unknown>, number | null, unknown>): coroutines.Coroutine<number | null>

Schedules a coroutine for the next run, or later in the current run if called from a coroutine. If the caller first yields it with yield* scheduler.start(child()), the child starts at once and the caller owns it. Cancelling the owner cancels the child. Cancelling other waiters only stops their waits. After destroy, returns a cancelled coroutine without running it.

start
(
function pick(x: number, y: number): Generator<coroutines.Wait<unknown>, number | null, unknown>
pick
(10, 20));
const candidate: number | null
yield*
import coroutines
coroutines
.
function seconds(duration: number): coroutines.Wait<frames.Frame>

Waits until the scheduler's time has advanced by duration seconds, frame time by default. Throws unless duration is finite and nonnegative.

@param ― duration seconds to wait

@returns ― a wait to yield*

seconds
(0.5);
if (
const candidate: number | null
candidate
!== null)
function select(candidate: number): void
select
(
const candidate: number
candidate
);
}
const
const main: coroutines.Coroutine<void>
main
=
const scheduler: coroutines.Coroutines
scheduler
.
Coroutines.start<void>(process: Generator<coroutines.Wait<unknown>, void, unknown>): coroutines.Coroutine<void>

Schedules a coroutine for the next run, or later in the current run if called from a coroutine. If the caller first yields it with yield* scheduler.start(child()), the child starts at once and the caller owns it. Cancelling the owner cancels the child. Cancelling other waiters only stops their waits. After destroy, returns a cancelled coroutine without running it.

start
(
function game(): Generator<coroutines.Wait<unknown>, void, unknown>
game
());
const main: coroutines.Coroutine<void>
main
.
Coroutine<void>.cancel(): void

Calls its wait's leave, or cancels the child coroutine it owns and waits on, then runs this coroutine's finally blocks and using disposals. Other coroutines waiting on it receive an error.

cancel
(); // runs finally blocks and disposes `using` resources
  • start returns a coroutine with status, result, error and cancel(). Its status is running, done, failed or cancelled.
  • A coroutine started by a running one starts in the same frame, at once if its starter yields it, as game does.
  • One waiting on another resumes in the frame the other finishes.
Cancel game while it waits on pick to see which finally blocks run and whether the read completes. Each block marks a running coroutine; lines show waits.

coroutines.until(promise) and coroutines.deferred(), which a callback resolves or rejects, cover most waits. coroutines.wait makes one from these members:

Member Called
ready(frame, since) at every run until it returns true
value(frame) optionally, once it is ready; the coroutine resumes with what it returns, or else with the frame
begin(frame) optionally, in the frame a coroutine yields it, once per coroutine
leave() optionally, when a coroutine waiting on it is cancelled

since holds the index of the frame where the coroutine yielded and the scheduler’s time then, which clock(frame) reads for a later frame. It also holds resumedAt, the start of its last resume, and now(), the wall clock. One wait can serve many coroutines, as nextFrame does, and resuming through it allocates nothing:

const
const flag: {
set: boolean;
}
flag
= {
set: boolean
set
: false };
const
const raised: NoInfer<{
ready: () => boolean;
} & coroutines.Wait<Frame>>
raised
=
import coroutines
coroutines
.
function wait<{
ready: () => boolean;
}>(parts: {
ready: () => boolean;
}): NoInfer<{
ready: () => boolean;
} & coroutines.Wait<Frame>>

Makes a new wait with the members of parts, which it leaves unchanged. The wait yields itself once, then returns the resume value, the frame without value. Many coroutines can share it, and resuming through it allocates nothing.

@param ― parts ready, and optionally value, begin and leave

@returns ― a wait to yield*

wait
({
ready: () => boolean
ready
: () =>
const flag: {
set: boolean;
}
flag
.
set: boolean
set
});

If a coroutine throws, the frame and other coroutines continue. run doesn’t throw.

  • A coroutine waiting on it gets the error at its yield*, as await does.
  • If no coroutine waits on it, onError reports the error. It defaults to reportError, which reports an uncaught error without throwing, or to console.error where reportError is missing.
  • A wait that throws, in begin, ready or value, throws at the coroutine’s yield*, and so does a bare yield of something that isn’t a wait.
  • coroutine.cancel() closes the generator at once. It runs finally blocks and disposes using resources. Its wait gets a leave call, and coroutines waiting on it get an error.
  • scheduler.destroy() cancels everything. A coroutine started after that comes back cancelled.
  • A coroutine that starts another and immediately waits on it with yield* scheduler.start(child()) owns that child. Cancelling the owner cancels the child first. In the figure, cancelling game runs pick’s finally, then game’s.
  • Any other waiter only stops waiting. Cancelling a coroutine that waits on a promise leaves the promise’s work running.
  • Order work inside a frame with call order.
  • Check a cadence beside a pass for work such as GI every fourth frame. A coroutine can’t reach a pass opened later.
  • Connect modules through their arguments. No module requires coroutines.
  • A stack trace from a resumed coroutine shows the scheduler’s run, not where the coroutine was started.
  • Copies in a coroutine read last frame’s results if run comes before the passes, or this frame’s results if it comes after them. Nothing warns if placing run earlier adds a frame of delay.
  • A coroutine has no owner unless its starter yields it immediately. Otherwise, cancelling the starter leaves it running. Cancel it in the starter’s finally.