Skip to content

@typegpu/kitsorter

Sorter

Sort a buffer of keys on the GPU, carrying a payload such as indices along with them.

kit.sorter

module

sorter.create(root, keys, options?) → Sorter

rootborrows
the root
keystakes
the buffer to sort, with storage usage
Sorterreturns
a sorter you destroy
types
KeySorter
Optionscreate · 6 fields
values?

Storage<V>= no payload

Holds a payload, such as indices into your own data, that moves with its key.

out?

{ keys: Storage<K>; values?: Storage<V> }= sorting keys and values in place

Receives the sorted keys and payload, and leaves keys and values as they were.

direction?

'ascending' | 'descending'= 'ascending'

Puts the largest key first with 'descending'.

range?

[min: number, max: number]= the whole range of the key's type

Clamps each key into [min, max].

keyBits?

number= what range needs, or 32

Sorts by this many low bits of the key that range or key makes, eight a pass.

key?

(key: number) => number= the key's own order

Maps a key to a u32 whose order is the sort order, in place of the map for the key's type.

records
  • sortcompute

sort in a compute pass after whatever writes the keys.

owns

buffers ·pipelines ·bind groups

Counts per 2 048 keys, a copy of the keys and payload to sort through, and the scan of the counts.

lifecycle

initAsync()destroy()[Symbol.dispose]

Tap a key, or use the arrow keys, to follow it and its payload letter through the passes. Each pass orders the keys by one digit, lowest first, and keeps keys with the same digit in the order the last pass left them, so the two 42s never swap. The sorter takes eight bits a pass where the figure takes one decimal digit.

Blended splats and particles draw back to front, so their order changes whenever the camera moves. A list drawn by depth, or rows grouped by a cell or a material, needs the same thing: keys a kernel wrote, sorted, with the index of each key’s row beside it. sorter.create sorts a buffer of keys in a compute pass of your frame, and moves a payload buffer with them. Keys that compare equal keep their order.

import {
import sorter
sorter
} from '@typegpu/kit';
const
const COUNT: 65536
COUNT
= 65_536;
const
const depths: TgpuBuffer<d.WgslArray<d.F32>> & StorageFlag
depths
=
const root: TgpuRoot
root
.
TgpuRoot.createBuffer<d.WgslArray<d.F32>>(typeSchema: d.WgslArray<d.F32>, initial?: ((buffer: TgpuBuffer<NoInfer<d.WgslArray<d.F32>>>) => void) | d.InferInput<NoInfer<d.WgslArray<d.F32>>> | undefined): TgpuBuffer<d.WgslArray<d.F32>> (+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.F32>(elementType: d.F32, elementCount: number): d.WgslArray<d.F32> (+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 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
,
const COUNT: 65536
COUNT
)).
TgpuBuffer<WgslArray<F32>>.$usage<["storage"]>(usages_0: "storage"): TgpuBuffer<d.WgslArray<d.F32>> & StorageFlag
$usage
('storage');
const
const order: TgpuBuffer<d.WgslArray<d.U32>> & StorageFlag
order
=
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
,
const COUNT: 65536
COUNT
)).
TgpuBuffer<WgslArray<U32>>.$usage<["storage"]>(usages_0: "storage"): TgpuBuffer<d.WgslArray<d.U32>> & StorageFlag
$usage
('storage');
const
const backToFront: sorter.Sorter
backToFront
=
import sorter
sorter
.
function create<d.F32, d.U32>(root: TgpuRoot, keys: Storage<d.F32>, options?: sorter.Options<d.F32, d.U32>): sorter.Sorter

Creates a stable radix sort of a buffer of keys, eight bits a pass, which carries a payload along. Keys that compare equal keep their order, so a sort repeats exactly. Its cost follows the buffer's length and keyBits, not the keys' order. An f32 sort ranks -0 and +0 equal.

@owns ― buffers, pipelines, bind groups: counts per 2 048 keys, a copy of the keys and payload to sort through, and the scan of the counts

@records ― sort in a compute pass after whatever writes the keys

@param ― keys the buffer to sort, with storage usage

@returns ― a sorter you destroy

create
(
const root: TgpuRoot
root
,
const depths: TgpuBuffer<d.WgslArray<d.F32>> & StorageFlag
depths
, {
Options<F32, U32>.values?: Storage<d.U32> | undefined

Holds a payload, such as indices into your own data, that moves with its key. It has as many elements as keys. With out, it requires out.values. Default no payload.

values
:
const order: TgpuBuffer<d.WgslArray<d.U32>> & StorageFlag
order
,
Options<K extends Key = Key, V extends d.AnyWgslData = AnyWgslData>.range?: [min: number, max: number] | undefined

Clamps each key into [min, max]. Integer keys sort from min, in the bits max - min needs; f32 keys sort in 2^keyBits even steps, where keys in one step keep their order. Default the whole range of the key's type.

range
: [0, 1],
Options<K extends Key = Key, V extends d.AnyWgslData = AnyWgslData>.keyBits?: number | undefined

Sorts by this many low bits of the key that range or key makes, eight a pass. Below 32 on i32 and f32 keys requires range or key. Default what range needs, or 32.

keyBits
: 16, // two passes
Options<K extends Key = Key, V extends d.AnyWgslData = AnyWgslData>.direction?: "ascending" | "descending" | undefined

Puts the largest key first with 'descending'. Default 'ascending'.

direction
: 'descending',
});
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
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
();
const compute: TgpuComputePass
compute
.
TgpuComputePass.setPipeline(pipeline: TgpuComputePipeline): void

Sets the current

TgpuComputePipeline

for subsequent dispatches

setPipeline
(
const depthPipeline: TgpuComputePipeline
depthPipeline
); // writes each splat's depth and its index
const compute: TgpuComputePass
compute
.
TgpuComputePass.dispatchWorkgroups(x: number, y?: number, z?: number): void
dispatchWorkgroups
(
const COUNT: 65536
COUNT
/ 64);
const backToFront: sorter.Sorter
backToFront
.
Sorter.sort(pass: TgpuComputePass): void

Records the sort into a compute pass.

sort
(
const compute: TgpuComputePass
compute
);
const compute: TgpuComputePass
compute
.
TgpuComputePass.end(): void

Completes the recording of this compute pass

end
();
// ... draw splat `order[i]` as instance i
});

Keys are u32, i32 or f32. The sort takes eight bits of the key a pass, lowest first, so keyBits sets its cost:

  • Integer keys within a known range sort in the bits max - min needs. Ids below 1 000 take 10 bits, two passes.
  • f32 keys with a range sort in 2^keyBits even steps. Keys within one step keep their order, so 16 bits suffice for depths on a screen.
  • Without range, a key sorts in all 32 bits, four passes. Negative i32 and f32 keys sort below positive ones.

values moves with the keys, element for element. It is usually each key’s index, which a later kernel or draw reads its row through. The sort runs in place by default. With out, it writes the sorted keys and payload to other buffers and leaves keys and values as they were; this also saves the copy back that an odd number of passes ends with in place.

sort(pass) records a count, a scan and a scatter per eight bits, and sets its own pipelines and bind groups. Record it in a compute pass after the kernel that writes the keys.

  • Every sort covers the whole buffer. Give unused slots a key that sorts last, as grid.radix does for empty slots.
  • A sorter holds a second copy of the keys and payload to sort through, and 1 KB of counts per 2 048 keys.