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.
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.
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,(
frame:frames.Frame
frame)=>{
constscheduler: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.
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:
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,(
frame:frames.Frame
frame)=>{
constscheduler: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
constcompute: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.
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(
constcompute:TgpuComputePass
compute,...
let point:[number,number]
point);
let point:[number,number]|undefined
point=
var undefined
undefined;
constcompute:TgpuComputePass
compute.
TgpuComputePass.end(): void
Completes the recording of this compute pass
end();
consthits: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.
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.
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(
consthits: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.
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()).
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.
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.
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
constflag:{
set:boolean;
}
flag={
set: boolean
set:false};
const
constraised:NoInfer<{
ready:()=>boolean;
}&coroutines.Wait<Frame>>
raised=
import coroutines
coroutines.
functionwait<{
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
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.
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.