Skip to content

@typegpu/kitworkgroup

Workgroup reduce and scan

Combine one value from each invocation of a workgroup into one result, or into what comes before each.

kit.workgroup

shader

workgroup.sum(schema) → Monoid

workgroup.min(schema) → Monoid

workgroup.max(schema) → Monoid

workgroup.bounds(schema) → Monoid

and workgroup.reduce, workgroup.scan

schematakes
a numeric scalar or vector, or a struct or array of them
Monoidreturns
a monoid for reduce and scan
schematakes
a scalar or vector of numbers
Monoidreturns
a monoid of { low, high } for reduce and scan
types
MonoidOrdered

Tap a value in the last row, or use the arrow keys, to light the values that reach it. Each row is one step between barriers. A reduce halves the invocations that add at each step, then every invocation reads the first. A scan adds the value reach invocations back, then each invocation reads the one before it.

The invocations of a workgroup often need one answer from all of them: the average brightness of their samples, the box around their points, or where each writes in a packed list. Each answer takes an array in workgroup memory, a loop of steps and a barrier between them. workgroup.reduce and workgroup.scan write that loop once, for any monoid: a schema, an identity value and a combine function.

import {
import workgroup
workgroup
} from '@typegpu/kit';
const
const SIZE: 256
SIZE
= 256;
const
const Box: workgroup.Monoid<d.WgslStruct<{
low: d.Vec3f;
high: d.Vec3f;
}>>
Box
=
import workgroup
workgroup
.
function bounds<d.Vec3f>(schema: d.Vec3f): workgroup.Monoid<d.WgslStruct<{
low: d.Vec3f;
high: d.Vec3f;
}>>

Keeps the box around points: low is their min and high their max. Give each invocation's point as schema({ low: point, high: point }).

@param ― schema a scalar or vector of numbers

@returns ― a monoid of { low, high } for reduce and scan

bounds
(
import d
d
.
const vec3f: d.Vec3f
vec3f
);
const
const enclose: (value: {
low: d.v3f;
high: d.v3f;
}, lid: number) => {
low: d.v3f;
high: d.v3f;
}
enclose
=
import workgroup
workgroup
.
function reduce<d.WgslStruct<{
low: d.Vec3f;
high: d.Vec3f;
}>>(monoid: workgroup.Monoid<d.WgslStruct<{
low: d.Vec3f;
high: d.Vec3f;
}>>, size: number): (value: {
low: d.v3f;
high: d.v3f;
}, lid: number) => {
low: d.v3f;
high: d.v3f;
}

Returns a 'use gpu' function (value, lid) that combines value from each of the size invocations of a workgroup and returns the result to every one of them. lid is the invocation's localInvocationIndex. Every invocation must call it, before any of them returns: one with nothing to add passes monoid.identity(). Each call to reduce declares its own workgroup array, and the function can run several times in one kernel.

@param ― monoid how to combine values, such as workgroup.sum(d.f32)

@param ― size the workgroup's invocation count, a power of two

@returns ― a 'use gpu' function for kernels of that workgroup size

reduce
(
const Box: workgroup.Monoid<d.WgslStruct<{
low: d.Vec3f;
high: d.Vec3f;
}>>
Box
,
const SIZE: 256
SIZE
);
const
const offset: (value: number, lid: number) => number
offset
=
import workgroup
workgroup
.
function scan<d.U32>(monoid: workgroup.Monoid<d.U32>, size: number): (value: number, lid: number) => number

Returns a 'use gpu' function (value, lid) that gives each of the size invocations of a workgroup the combination of the values of the invocations before it in lid order, and the first monoid.identity(). Combine the result with value to include the invocation's own. Calls follow the rules of reduce.

@param ― monoid how to combine values, such as workgroup.sum(d.u32) for offsets to write at

@param ― size the workgroup's invocation count, a power of two

@returns ― a 'use gpu' function for kernels of that workgroup size

scan
(
import workgroup
workgroup
.
function sum<d.U32>(schema: d.U32): workgroup.Monoid<d.U32>

Adds values from zero. A struct or array adds field by field, so one reduction sums several quantities at once.

@param ― schema a numeric scalar or vector, or a struct or array of them

@returns ― a monoid for reduce and scan

sum
(
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 SIZE: 256
SIZE
);
const
const box: TgpuMutable<d.WgslStruct<{
low: d.Vec3f;
high: d.Vec3f;
}>>
box
=
const root: TgpuRoot
root
.
TgpuRoot.createMutable<d.WgslStruct<{
low: d.Vec3f;
high: d.Vec3f;
}>>(typeSchema: d.WgslStruct<{
low: d.Vec3f;
high: d.Vec3f;
}>, initial?: ((buffer: TgpuBuffer<NoInfer<d.WgslStruct<{
low: d.Vec3f;
high: d.Vec3f;
}>>>) => void) | d.InferInput<NoInfer<d.WgslStruct<{
low: d.Vec3f;
high: d.Vec3f;
}>>> | undefined): TgpuMutable<d.WgslStruct<{
low: d.Vec3f;
high: d.Vec3f;
}>> (+1 overload)

Allocates memory on the GPU, allows passing data between host and shader. Can be mutated in-place on the GPU. For a general-purpose buffer, use

TgpuRoot.createBuffer

.

@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)

createMutable
(
const Box: workgroup.Monoid<d.WgslStruct<{
low: d.Vec3f;
high: d.Vec3f;
}>>
Box
.
Monoid<WgslStruct<{ low: Vec3f; high: Vec3f; }>>.schema: d.WgslStruct<{
low: d.Vec3f;
high: d.Vec3f;
}>
schema
);
const
const pack: TgpuComputeFn<{
lid: d.BuiltinLocalInvocationIndex;
}>
pack
=
const tgpu: {
const: typeof import("node_modules/typegpu/core/constant/tgpuConstant").constant;
fn: typeof import("node_modules/typegpu/core/function/tgpuFn").fn;
comptime: typeof import("node_modules/typegpu/core/function/comptime").comptime;
resolve: typeof import("node_modules/typegpu/core/resolve/tgpuResolve").resolve;
resolveWithContext: typeof import("node_modules/typegpu/core/resolve/tgpuResolve").resolveWithContext;
init: typeof import("node_modules/typegpu/core/root/init").init;
initFromDevice: typeof import("node_modules/typegpu/core/root/init").initFromDevice;
slot: typeof import("node_modules/typegpu/core/slot/slot").slot;
lazy: typeof import("node_modules/typegpu/core/slot/lazy").lazy;
... 10 more ...;
'~unstable': typeof import("node_modules/typegpu/tgpuUnstable");
}

@module ― typegpu

tgpu
.
computeFn: <{
lid: d.BuiltinLocalInvocationIndex;
}>(options: {
in: {
lid: d.BuiltinLocalInvocationIndex;
};
workgroupSize: number[];
}) => TgpuComputeFnShell<{
lid: d.BuiltinLocalInvocationIndex;
}> (+1 overload)
computeFn
({
workgroupSize: number[]
workgroupSize
: [
const SIZE: 256
SIZE
],
in: {
lid: d.BuiltinLocalInvocationIndex;
}
in
: {
lid: d.BuiltinLocalInvocationIndex
lid
:
import d
d
.
const builtin: {
readonly vertexIndex: d.BuiltinVertexIndex;
readonly instanceIndex: d.BuiltinInstanceIndex;
readonly clipDistances: d.BuiltinClipDistances;
readonly position: d.BuiltinPosition;
readonly frontFacing: d.BuiltinFrontFacing;
readonly fragDepth: d.BuiltinFragDepth;
readonly primitiveIndex: d.BuiltinPrimitiveIndex;
readonly sampleIndex: d.BuiltinSampleIndex;
... 11 more ...;
readonly numSubgroups: d.BuiltinNumSubgroups;
}
export builtin
builtin
.
localInvocationIndex: d.BuiltinLocalInvocationIndex
localInvocationIndex
},
})(({
lid: number
lid
}) => {
'use gpu';
const
const point: d.v3f
point
=
const points: TgpuReadonly<d.WgslArray<d.Vec3f>>
points
.
TgpuReadonly<WgslArray<Vec3f>>.$: d.v3f[]
$
[
lid: number
lid
]!;
const
const above: boolean
above
=
const point: d.v3f
point
.
v3f.y: number
y
>= 0;
let
let mine: {
low: d.v3f;
high: d.v3f;
}
mine
=
const Box: workgroup.Monoid<d.WgslStruct<{
low: d.Vec3f;
high: d.Vec3f;
}>>
Box
.
Monoid<WgslStruct<{ low: Vec3f; high: Vec3f; }>>.identity: () => {
low: d.v3f;
high: d.v3f;
}
identity
(); // every invocation calls both, adding nothing if below
if (
const above: boolean
above
)
let mine: {
low: d.v3f;
high: d.v3f;
}
mine
=
const Box: workgroup.Monoid<d.WgslStruct<{
low: d.Vec3f;
high: d.Vec3f;
}>>
Box
.
Monoid<WgslStruct<{ low: Vec3f; high: Vec3f; }>>.schema: d.WgslStruct
(props?: {
low: d.v3f;
high: d.v3f;
} | undefined) => {
low: d.v3f;
high: d.v3f;
}
schema
({
low: d.v3f
low
:
const point: d.v3f
point
,
high: d.v3f
high
:
const point: d.v3f
point
});
const
const around: {
low: d.v3f;
high: d.v3f;
}
around
=
const enclose: (value: {
low: d.v3f;
high: d.v3f;
}, lid: number) => {
low: d.v3f;
high: d.v3f;
}
enclose
(
let mine: {
low: d.v3f;
high: d.v3f;
}
mine
,
lid: number
lid
);
const
const at: number
at
=
const offset: (value: number, lid: number) => number
offset
(
import std
std
.
function select(f: number, t: number, cond: boolean): number (+2 overloads)
export select

Returns t if cond is true, and f otherwise. Component-wise if cond is a vector.

@example select(1, 2, false) // returns 1 select(1, 2, true) // returns 2 select(vec2i(1, 2), vec2i(3, 4), true) // returns vec2i(3, 4) select(vec2i(1, 2), vec2i(3, 4), vec2b(false, true)) // returns vec2i(1, 4)

select
(
import d
d
.
function u32(v?: number | boolean): number
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
(0),
import d
d
.
function u32(v?: number | boolean): number
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
(1),
const above: boolean
above
),
lid: number
lid
); // above ones before it
if (
const above: boolean
above
)
const kept: TgpuMutable<d.WgslArray<d.Vec3f>>
kept
.
TgpuMutable<WgslArray<Vec3f>>.$: d.v3f[]
$
[
const at: number
at
] =
import d
d
.
const vec3f: d.Vec3f
(v: AnyNumericVec3Instance) => d.v3f (+5 overloads)
vec3f
(
const point: d.v3f
point
);
if (
lid: number
lid
=== 0)
const box: TgpuMutable<d.WgslStruct<{
low: d.Vec3f;
high: d.Vec3f;
}>>
box
.
TgpuMutable<WgslStruct<{ low: Vec3f; high: Vec3f; }>>.$: {
low: d.v3f;
high: d.v3f;
}
$
=
const Box: workgroup.Monoid<d.WgslStruct<{
low: d.Vec3f;
high: d.Vec3f;
}>>
Box
.
Monoid<WgslStruct<{ low: Vec3f; high: Vec3f; }>>.schema: d.WgslStruct
(props?: {
low: d.v3f;
high: d.v3f;
} | undefined) => {
low: d.v3f;
high: d.v3f;
}
schema
(
const around: {
low: d.v3f;
high: d.v3f;
}
around
);
});
  • sum(schema) adds from zero. A struct or an array adds field by field, so one reduction sums several quantities.
  • min(schema) and max(schema) compare scalars, and vectors per component, from the highest or lowest value the schema holds.
  • bounds(schema) keeps { low, high }, the min and max of the points it is given.

Write your own as { schema, identity, combine }, with 'use gpu' functions. This one finds the brightest sample, and the lowest index on a tie:

const
const Sample: d.WgslStruct<{
value: d.F32;
at: d.U32;
}>
Sample
=
import d
d
.
struct<{
value: d.F32;
at: d.U32;
}>(props: {
value: d.F32;
at: d.U32;
}): d.WgslStruct<{
value: d.F32;
at: 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
({
value: d.F32
value
:
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
,
at: d.U32
at
:
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
const brightest: workgroup.Monoid<d.WgslStruct<{
value: d.F32;
at: d.U32;
}>>
brightest
:
import workgroup
workgroup
.
interface Monoid<T extends d.AnyWgslData = d.AnyWgslData>

This type combines values of one schema. combine must be associative, and also commutative for reduce. Combining a value with identity() must leave it unchanged. Both functions are 'use gpu' and also run on the CPU, so a test can fold the same values.

Monoid
<typeof
const Sample: d.WgslStruct<{
value: d.F32;
at: d.U32;
}>
Sample
> = {
Monoid<WgslStruct<{ value: F32; at: U32; }>>.schema: d.WgslStruct<{
value: d.F32;
at: d.U32;
}>
schema
:
const Sample: d.WgslStruct<{
value: d.F32;
at: d.U32;
}>
Sample
,
Monoid<WgslStruct<{ value: F32; at: U32; }>>.identity: () => {
value: number;
at: number;
}
identity
: () => {
'use gpu';
return
const Sample: d.WgslStruct
(props?: {
value: number;
at: number;
} | undefined) => {
value: number;
at: number;
}
Sample
({
value: number
value
: -1,
at: number
at
: 0 });
},
Monoid<WgslStruct<{ value: F32; at: U32; }>>.combine: (a: {
value: number;
at: number;
}, b: {
value: number;
at: number;
}) => {
value: number;
at: number;
}
combine
: (
a: {
value: number;
at: number;
}
a
,
b: {
value: number;
at: number;
}
b
) => {
'use gpu';
if (
a: {
value: number;
at: number;
}
a
.
value: number
value
>
b: {
value: number;
at: number;
}
b
.
value: number
value
|| (
a: {
value: number;
at: number;
}
a
.
value: number
value
===
b: {
value: number;
at: number;
}
b
.
value: number
value
&&
a: {
value: number;
at: number;
}
a
.
at: number
at
<
b: {
value: number;
at: number;
}
b
.
at: number
at
)) return
const Sample: d.WgslStruct
(props?: {
value: number;
at: number;
} | undefined) => {
value: number;
at: number;
}
Sample
(
a: {
value: number;
at: number;
}
a
);
return
const Sample: d.WgslStruct
(props?: {
value: number;
at: number;
} | undefined) => {
value: number;
at: number;
}
Sample
(
b: {
value: number;
at: number;
}
b
);
},
};

combine must be associative, and also commutative for reduce, which combines the invocations out of order. identity and combine also run on the CPU, so a test can fold the values the kernel combined and compare.

  • reduce(monoid, size) and scan(monoid, size) return a function of (value, lid) for kernels of size invocations, with lid the localInvocationIndex. reduce returns the combination of every value to every invocation. scan returns the combination of the values before the invocation, and identity() to the first. Combine it with value to include the invocation’s own.
  • Every invocation of the workgroup calls the function, before any of them returns. Barriers wait for all of them, so one that skips the call stalls or corrupts the rest. An invocation with nothing to add passes monoid.identity().
  • Each call to reduce or scan declares its own array of size values in workgroup memory. The function can run several times in one kernel.
  • size is a power of two.
  • The array takes size values of the schema from the workgroup memory WebGPU guarantees, 16 384 bytes. sum over 64 invocations of nine vec4f takes 9 216.
  • The functions combine within one workgroup. Combine the results of several workgroups in a later dispatch, or with atomics.