Skip to content

@typegpu/kitreadback

Readback

Copy a buffer into your frame's encoder and read it on the CPU after submit without stalling.

kit.readback

module

readback.create(root, source, { bytes, … }) → Readback

readback.create(root, source, { decode, … }) → Readback

readback.create(root, source, options?) → Readback

rootborrows
the root
sourcetakes
the buffer it copies
Readbackreturns
a reader you capture into and destroy
types
Readback
BytesOptionscreate · 4 fields
bytes

true

Reads the copied bytes without decoding them through the buffer's schema.

decode?

(bytes: ArrayBuffer) => T= a copy of the bytes

Converts the copied bytes to the value returned by next and latest, such as typed arrays of your own.

staging?

number= 3

Sets the number of staging buffers.

hz?

number= none

Also captures for latest this many times per second on average, in the frame closest to each tick.

DecodeOptionscreate · 3 fields
decode

(value: d.Infer<S>) => T

Converts the source buffer value, read by its schema, to the value returned by next and latest, such as a candidate index or null.

staging?

number= 3

Sets the number of staging buffers.

hz?

number= none

Also captures for latest this many times per second on average, in the frame closest to each tick.

Optionscreate · 2 fields
staging?

number= 3

Sets the number of staging buffers.

hz?

number= none

Also captures for latest this many times per second on average, in the frame closest to each tick.

records
  • captureframe

capture after the pass that writes the buffer; a second capture in a frame does nothing.

owns

3 buffers

Staging buffers mapped in turn, as many as staging.

lifecycle

destroy()[Symbol.dispose]

Press read() to wait for the next capture of a buffer containing the frame index. Set 60 Hz, a slow map and one staging slot to see captures skipped while the buffer maps.

The GPU executes commands after the CPU records them. A result such as the object under a cursor therefore reaches the CPU in a later frame. TypeGPU’s buffer.read() creates a staging buffer, submit and promise per call. A readback reuses a ring of staging buffers, records copies in your frame’s encoder and maps them after submit.

import {
import readback
readback
} from '@typegpu/kit';
const
const hits: TgpuBuffer<d.WgslArray<d.U32>> & StorageFlag
hits
=
const root: TgpuRoot
root
.
TgpuRoot.createBuffer<d.WgslArray<d.U32>>(typeSchema: d.WgslArray<d.U32>, initial?: ((buffer: TgpuBuffer<NoInfer<d.WgslArray<d.U32>>>) => void) | d.InferInput<NoInfer<d.WgslArray<d.U32>>> | undefined): TgpuBuffer<d.WgslArray<d.U32>> (+1 overload)

Allocates memory on the GPU, allows passing data between host and shader.

@remarks ― Typed wrapper around a GPUBuffer.

@param ― typeSchema The type of data that this buffer will hold.

@param ― initial Either initial value of the buffer, or an initializer to execute on the mapped buffer. (optional)

createBuffer
(
import d
d
.
arrayOf<d.U32>(elementType: d.U32, elementCount: number): d.WgslArray<d.U32> (+2 overloads)
export arrayOf

@location ― . Wrap align/size in a struct instead, e.g. d.arrayOf(d.struct({ value: d.align(16, d.u32) }), n).

arrayOf
(
import d
d
.
const u32: d.U32
export u32

A schema that represents an unsigned 32-bit integer value. (equivalent to u32 in WGSL)

Can also be called to cast a value to an u32 in accordance with WGSL casting rules.

@example const value = u32(); // 0

@example const value = u32(7); // 7

@example const value = u32(3.14); // 3

@example const value = u32(-1); // 4294967295

@example const value = u32(-3.1); // 0

u32
, 4)).
TgpuBuffer<WgslArray<U32>>.$usage<["storage"]>(usages_0: "storage"): TgpuBuffer<d.WgslArray<d.U32>> & StorageFlag
$usage
('storage');
const
const probe: readback.Readback<number[]>
probe
=
import readback
readback
.
function create<d.WgslArray<d.U32>>(root: TgpuRoot, source: TgpuBuffer<d.WgslArray<d.U32>>, options?: readback.Options): readback.Readback<number[]> (+2 overloads)

Copies a buffer to the CPU without stalling, using a ring of staging buffers. Records one copy per capture. Throws unless staging is an integer above 0 and hz, when set, is finite and above 0.

@owns ― 3 buffers: staging buffers mapped in turn, as many as staging

@records ― capture after the pass that writes the buffer; a second capture in a frame does nothing.

@param ― source the buffer it copies

@returns ― a reader you capture into and destroy

create
(
const root: TgpuRoot
root
,
const hits: TgpuBuffer<d.WgslArray<d.U32>> & StorageFlag
hits
, {
Options.hz?: number | undefined

Also captures for latest this many times per second on average, in the frame closest to each tick. Without a rate, only next() and request cause captures. Default none.

hz
: 30 });
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
) => {
// ... the pass that writes `hits`
const probe: readback.Readback<number[]>
probe
.
Readback<number[]>.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 it
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
);
const
const polled: number[] | undefined
polled
=
const probe: readback.Readback<number[]>
probe
.
Readback<number[]>.take(): number[] | undefined

Returns latest once per completed copy, then undefined until the next copy completes. Each value goes to one caller. Callers sharing a readback split the values between them.

take
(); // polled: each new value once, or undefined
if (
const polled: number[] | undefined
polled
)
function onHits(count: number): void
onHits
(
const polled: number[]
polled
[0]!);
});
function*
function react(): Generator<coroutines.Wait<unknown>, void, unknown>
react
() {
const
const counts: number[]
counts
= yield*
import coroutines
coroutines
.
function until<number[]>(promise: PromiseLike<number[]>): coroutines.Wait<number[]>

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 probe: readback.Readback<number[]>
probe
.
Readback<number[]>.next(): Promise<number[]>

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
()); // or waited on: the next capture
function onHits(count: number): void
onHits
(
const counts: number[]
counts
[0]!);
}

capture(frame) copies the whole buffer at that point in the command order. Place it after the pass that fills a counter, or before the passes when a picker must read the frame already on screen.

  • It records only while a next() waits, after a request(), or when hz is due.
  • While every staging buffer is mapping, it is skipped and tried again next frame.
  • Record the pass that fills the buffer every frame and let hz pick the frames that copy; a pass that counts costs microseconds. Gating that pass with a clock of your own makes two cadences that disagree, so a copy can land in a frame the pass skipped.
  • A frame captures once; a second call does nothing.
  • A frame that is never submitted hands its reads to the next capture.
  • latest holds the newest value. latestFrame identifies the frame that copied it. Reads return the buffer’s value transformed by decode, if supplied. If decode throws, reads waiting on that copy fail with its error.
  • take() returns latest once per completed copy, then undefined until another copy completes. Two callers split those values. If both need every value, have each read latest and track latestFrame.
  • next() returns a promise for the next capture’s value. Promises asked for until then share one copy. It stays pending until a capture completes. In a coroutine, wait for it with yield* coroutines.until(probe.next()).
  • request() makes the next capture copy with no read waiting, for a buffer written on demand rather than every frame. A picker’s pick requests its own copy. A capture skipped while every staging buffer maps leaves the request open.
  • destroy(), or using, destroys the staging buffers and rejects waiting and later promises. capture then records nothing, and take and latest give undefined.
  • For counters that only increase, compare each read with the last value to handle late or repeated reads. Events does that for named counters.

Reads decode the whole schema into JavaScript values, one object per struct, so a large array of structs allocates thousands of objects per read. Pass bytes: true to skip decoding. decode then receives the copied bytes, which follow the schema’s WGSL layout with its padding. They are valid only during the call, so read them into arrays of your own. Without decode, reads return a copy as an ArrayBuffer.

const
const Cell: d.WgslStruct<{
level: d.F32;
source: d.U32;
}>
Cell
=
import d
d
.
struct<{
level: d.F32;
source: d.U32;
}>(props: {
level: d.F32;
source: d.U32;
}): d.WgslStruct<{
level: d.F32;
source: d.U32;
}>
export struct

Creates a struct schema that can be used to construct GPU buffers. Ensures proper alignment and padding of properties (as opposed to a d.unstruct schema). The order of members matches the passed in properties object.

@example const CircleStruct = d.struct({ radius: d.f32, pos: d.vec3f });

@param ― props Record with string keys and TgpuData values, each entry describing one struct member.

struct
({
level: d.F32
level
:
import d
d
.
const f32: d.F32
export f32

A schema that represents a 32-bit float value. (equivalent to f32 in WGSL)

Can also be called to cast a value to an f32.

@example const value = f32(); // 0

@example const value = f32(1.23); // 1.23

@example const value = f32(true); // 1

f32
,
source: d.U32
source
:
import d
d
.
const u32: d.U32
export u32

A schema that represents an unsigned 32-bit integer value. (equivalent to u32 in WGSL)

Can also be called to cast a value to an u32 in accordance with WGSL casting rules.

@example const value = u32(); // 0

@example const value = u32(7); // 7

@example const value = u32(3.14); // 3

@example const value = u32(-1); // 4294967295

@example const value = u32(-3.1); // 0

u32
}); // 8 bytes a cell
const
const cells: TgpuBuffer<d.WgslArray<d.WgslStruct<{
level: d.F32;
source: d.U32;
}>>> & StorageFlag
cells
=
const root: TgpuRoot
root
.
TgpuRoot.createBuffer<d.WgslArray<d.WgslStruct<{
level: d.F32;
source: d.U32;
}>>>(typeSchema: d.WgslArray<d.WgslStruct<{
level: d.F32;
source: d.U32;
}>>, initial?: ((buffer: TgpuBuffer<NoInfer<d.WgslArray<d.WgslStruct<{
level: d.F32;
source: d.U32;
}>>>>) => void) | d.InferInput<NoInfer<d.WgslArray<d.WgslStruct<{
level: d.F32;
source: d.U32;
}>>>> | undefined): TgpuBuffer<d.WgslArray<d.WgslStruct<{
level: d.F32;
source: d.U32;
}>>> (+1 overload)

Allocates memory on the GPU, allows passing data between host and shader.

@remarks ― Typed wrapper around a GPUBuffer.

@param ― typeSchema The type of data that this buffer will hold.

@param ― initial Either initial value of the buffer, or an initializer to execute on the mapped buffer. (optional)

createBuffer
(
import d
d
.
arrayOf<d.WgslStruct<{
level: d.F32;
source: d.U32;
}>>(elementType: d.WgslStruct<{
level: d.F32;
source: d.U32;
}>, elementCount: number): d.WgslArray<d.WgslStruct<{
level: d.F32;
source: d.U32;
}>> (+2 overloads)
export arrayOf

@location ― . Wrap align/size in a struct instead, e.g. d.arrayOf(d.struct({ value: d.align(16, d.u32) }), n).

arrayOf
(
const Cell: d.WgslStruct<{
level: d.F32;
source: d.U32;
}>
Cell
, 12288)).
TgpuBuffer<WgslArray<WgslStruct<{ level: F32; source: U32; }>>>.$usage<["storage"]>(usages_0: "storage"): TgpuBuffer<d.WgslArray<d.WgslStruct<{
level: d.F32;
source: d.U32;
}>>> & StorageFlag
$usage
('storage');
const
const levels: Float32Array<ArrayBuffer>
levels
= new
var Float32Array: Float32ArrayConstructor
new (length: number) => Float32Array<ArrayBuffer> (+6 overloads)
Float32Array
(12288);
const
const field: readback.Readback<Float32Array<ArrayBuffer>>
field
=
import readback
readback
.
function create<d.WgslArray<d.WgslStruct<{
level: d.F32;
source: d.U32;
}>>, Float32Array<ArrayBuffer>>(root: TgpuRoot, source: TgpuBuffer<d.WgslArray<d.WgslStruct<{
level: d.F32;
source: d.U32;
}>>>, options: readback.BytesOptions<Float32Array<ArrayBuffer>>): readback.Readback<Float32Array<ArrayBuffer>> (+2 overloads)

Copies a buffer to the CPU without stalling, using a ring of staging buffers. Records one copy per capture. Throws unless staging is an integer above 0 and hz, when set, is finite and above 0.

@owns ― 3 buffers: staging buffers mapped in turn, as many as staging

@records ― capture after the pass that writes the buffer; a second capture in a frame does nothing.

@param ― source the buffer it copies

@returns ― a reader you capture into and destroy

create
(
const root: TgpuRoot
root
,
const cells: TgpuBuffer<d.WgslArray<d.WgslStruct<{
level: d.F32;
source: d.U32;
}>>> & StorageFlag
cells
, {
BytesOptions<T = ArrayBuffer>.bytes: true

Reads the copied bytes without decoding them through the buffer's schema.

bytes
: true,
BytesOptions<Float32Array<ArrayBuffer>>.decode?: ((bytes: ArrayBuffer) => Float32Array<ArrayBuffer>) | undefined

Converts the copied bytes to the value returned by next and latest, such as typed arrays of your own. The bytes are valid only during the call, so copy what you keep. If it throws, reads for that copy fail with the error. Default a copy of the bytes.

decode
: (
bytes: ArrayBuffer
bytes
) => {
const
const words: Float32Array<ArrayBuffer>
words
= new
var Float32Array: Float32ArrayConstructor
new <ArrayBuffer>(buffer: ArrayBuffer, byteOffset?: number, length?: number) => Float32Array<ArrayBuffer> (+6 overloads)
Float32Array
(
bytes: ArrayBuffer
bytes
);
for (let
let i: number
i
= 0;
let i: number
i
<
const levels: Float32Array<ArrayBuffer>
levels
.
Float32Array<ArrayBuffer>.length: number

The length of the array.

length
;
let i: number
i
++)
const levels: Float32Array<ArrayBuffer>
levels
[
let i: number
i
] =
const words: Float32Array<ArrayBuffer>
words
[
let i: number
i
* 2]!;
return
const levels: Float32Array<ArrayBuffer>
levels
;
},
});

hz schedules captures at that rate and updates latest without a waiting read. It uses a cadence, so the first capture is immediate.

  • Call reset() after clearing a buffer or reloading a scene to discard earlier copies. It sets latest to undefined and latestFrame to -1, and moves waiting reads to the next capture.
  • To restart counters, clear the buffer in the frame, before the passes that count, and reset(). Otherwise a copy made before the clear can land after it, and counters that take differences run backwards.
  • A late map never replaces a newer latest.
  • A promise that already resolved keeps its value, even if its coroutine resumes after a reset. Compare a held result with latest.
  • A table’s changes reach the GPU at the first submit after its flush. Compare latestFrame with that frame’s index when a value must be newer than a change.

Device loss rejects every map. Promises waiting on a copy reject with the map’s error, later captures also fail, and latest keeps its last value. Create the modules again on another root to recover.

  • A capture placed before the pass that fills the buffer reads the previous frame’s value, and nothing catches that.
  • A capture copies the whole buffer without a range option.
  • A readback reads a fixed buffer. A varying list read once, on demand, needs code of its own.