Skip to content

Write a module

Write a module that moves 65 536 particles through noise, use it from your scene and test what it records.

Build the flow field example: 65 536 particles that drift through Perlin noise. You write the particles as a module, createFlowField, the way the kit writes its own. A module is a function, its factory, that borrows your root, allocates its GPU resources and returns methods that record work into passes you open. Your scene makes the module and records each frame. Modules is the one-page reference for what you write here.

The GPU runs the same small function for every particle at once. You write those functions in TypeScript, and TypeGPU turns them into shaders, the programs the GPU runs. Slime mould explains them at a slower pace.

  1. Put the particles in a buffer, and scatter them.

    A buffer is GPU memory that holds many values of one shape. Each Particle holds a position and the direction it last moved in. Positions are in clip space, where the canvas spans −1 to 1 on both axes.

    Create flow-field.ts. The factory takes the root and an options object, and allocates the buffer once, when the scene makes the module. Nothing allocates while a frame records.

    flow-field.ts
    import {
    function owner(message?: string): Owner

    Collects what a scene or module makes, to free it all in one call, and guards its methods. Pass each thing through it as you make it, and return its destroy as your own. Take a resource that is replaced during the module's life, such as a texture that follows the canvas size, as one cleanup that frees the current one. Methods that only read CPU state or do optional work, such as capture, take or a camera's update, should return early on destroyed instead of calling usable.

    const own = owner('The swarm was destroyed.');
    const screen = own(targets.create(root, surface, { hdr }));
    const settings = own(root.createUniform(Settings));
    own(() => {
    for (const buffer of reads) buffer.destroy();
    });
    return { step(pass) { own.usable(); … }, destroy: own.destroy, [Symbol.dispose]: own.destroy };

    @param ― message what usable throws, naming the module: "The table was destroyed."

    @returns ― an owner you pass each thing to and destroy

    owner
    , type
    (alias) interface SizeSource
    import SizeSource

    Supplies a size in pixels, such as a canvas surface. Read at each fit.

    SizeSource
    } from '@typegpu/kit';
    import {
    const randf: {
    seed: TgpuFn<(seed: d.F32) => d.Void>;
    seed2: TgpuFn<(seed: d.Vec2f) => d.Void>;
    seed3: TgpuFn<(seed: d.Vec3f) => d.Void>;
    seed4: TgpuFn<(seed: d.Vec4f) => d.Void>;
    sample: TgpuFn<() => d.F32>;
    sampleExclusive: TgpuFn<() => d.F32>;
    normal: TgpuFn<(mu: d.F32, sigma: d.F32) => d.F32>;
    ... 10 more ...;
    onUnitSphere: TgpuFn<...>;
    }
    randf
    } from '@typegpu/noise';
    import {
    import d
    d
    ,
    const tgpu: {
    const: {
    <TDataType extends d.AnyData>(dataType: TDataType, value: d.InferGPU<TDataType>): TgpuConst<TDataType>;
    <TElement extends d.AnyWgslData>(dataType: (elementCount: number) => d.WgslArray<TElement>, value: d.InferGPU<d.WgslArray<TElement>>): TgpuConst<d.WgslArray<TElement>>;
    };
    fn: {
    <Args extends d.BaseData[] | []>(argTypes: Args, returnType?: undefined): TgpuFnShell<Args, d.Void>;
    <Args extends d.BaseData[] | [], Return extends d.BaseData>(argTypes: Args, returnType: Return): TgpuFnShell<Args, Return>;
    <T extends AnyFn>(inner: T): TgpuGenericFn<T>;
    };
    ... 17 more ...;
    '~unstable': typeof import("../../node_modules/typegpu/tgpuUnstable");
    }

    @module ― typegpu

    tgpu
    , type
    (alias) interface TgpuComputePass
    import TgpuComputePass

    A compute pass recording into a

    TgpuCommandEncoder

    .

    Dispatch either by binding TypeGPU pipelines to it (pipeline.with(pass).dispatchWorkgroups(...)), or proxy-style via pass.setPipeline(pipeline) followed by pass.dispatchWorkgroups(...).

    Call end() when done recording.

    TgpuComputePass
    , type
    (alias) interface TgpuRoot
    import TgpuRoot
    TgpuRoot
    } from 'typegpu';
    const
    const WORKGROUP: 64
    WORKGROUP
    = 64;
    const
    const Particle: d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>
    Particle
    =
    import d
    d
    .
    struct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>(props: {
    position: d.Vec2f;
    direction: d.Vec2f;
    }): d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>
    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
    ({
    position: d.Vec2f
    position
    :
    import d
    d
    .
    const vec2f: d.Vec2f
    vec2f
    ,
    direction: d.Vec2f
    direction
    :
    import d
    d
    .
    const vec2f: d.Vec2f
    vec2f
    });
    export interface
    interface FlowFieldOptions
    FlowFieldOptions
    {
    readonly
    FlowFieldOptions.count: number
    count
    : number;
    /** Seconds per `step`. */
    readonly
    FlowFieldOptions.step: number

    Seconds per step.

    step
    : number;
    readonly
    FlowFieldOptions.format: GPUTextureFormat
    format
    :
    type GPUTextureFormat = "astc-10x10-unorm" | "astc-10x10-unorm-srgb" | "astc-10x5-unorm" | "astc-10x5-unorm-srgb" | "astc-10x6-unorm" | "astc-10x6-unorm-srgb" | "astc-10x8-unorm" | "astc-10x8-unorm-srgb" | "astc-12x10-unorm" | "astc-12x10-unorm-srgb" | "astc-12x12-unorm" | "astc-12x12-unorm-srgb" | "astc-4x4-unorm" | "astc-4x4-unorm-srgb" | "astc-5x4-unorm" | "astc-5x4-unorm-srgb" | "astc-5x5-unorm" | "astc-5x5-unorm-srgb" | "astc-6x5-unorm" | "astc-6x5-unorm-srgb" | "astc-6x6-unorm" | "astc-6x6-unorm-srgb" | "astc-8x5-unorm" | "astc-8x5-unorm-srgb" | ... 76 more ... | "stencil8"
    GPUTextureFormat
    ;
    /** What the particles are drawn into, such as the canvas surface. */
    readonly
    FlowFieldOptions.size: SizeSource

    What the particles are drawn into, such as the canvas surface.

    size
    :
    (alias) interface SizeSource
    import SizeSource

    Supplies a size in pixels, such as a canvas surface. Read at each fit.

    SizeSource
    ;
    }
    export function
    function createFlowField(root: TgpuRoot, options: FlowFieldOptions): {}
    createFlowField
    (
    root: TgpuRoot
    root
    :
    (alias) interface TgpuRoot
    import TgpuRoot
    TgpuRoot
    ,
    options: FlowFieldOptions
    options
    :
    interface FlowFieldOptions
    FlowFieldOptions
    ) {
    const {
    const count: number
    count
    ,
    const step: number

    Seconds per step.

    step
    ,
    const format: GPUTextureFormat
    format
    ,
    const size: SizeSource

    What the particles are drawn into, such as the canvas surface.

    size
    } =
    options: FlowFieldOptions
    options
    ;
    const
    const own: Owner
    own
    =
    function owner(message?: string): Owner

    Collects what a scene or module makes, to free it all in one call, and guards its methods. Pass each thing through it as you make it, and return its destroy as your own. Take a resource that is replaced during the module's life, such as a texture that follows the canvas size, as one cleanup that frees the current one. Methods that only read CPU state or do optional work, such as capture, take or a camera's update, should return early on destroyed instead of calling usable.

    const own = owner('The swarm was destroyed.');
    const screen = own(targets.create(root, surface, { hdr }));
    const settings = own(root.createUniform(Settings));
    own(() => {
    for (const buffer of reads) buffer.destroy();
    });
    return { step(pass) { own.usable(); … }, destroy: own.destroy, [Symbol.dispose]: own.destroy };

    @param ― message what usable throws, naming the module: "The table was destroyed."

    @returns ― an owner you pass each thing to and destroy

    owner
    ('The flow field was destroyed.');
    const
    const particles: TgpuBuffer<d.WgslArray<d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>>> & StorageFlag
    particles
    =
    const own: Owner
    <TgpuBuffer<d.WgslArray<d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>>> & StorageFlag>(item: TgpuBuffer<d.WgslArray<d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>>> & StorageFlag) => TgpuBuffer<d.WgslArray<d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>>> & StorageFlag

    Takes item to free at destroy and returns it. Passes undefined through. Frees a fixed resource, such as one from root.createUniform, through its buffer, and runs a cleanup function. After destroy, frees item at once.

    own
    (
    root: TgpuRoot
    root
    .
    TgpuRoot.createBuffer<d.WgslArray<d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>>>(typeSchema: d.WgslArray<d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>>, initial?: ((buffer: TgpuBuffer<NoInfer<d.WgslArray<d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>>>>) => void) | d.InferInput<NoInfer<d.WgslArray<d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>>>> | undefined): TgpuBuffer<d.WgslArray<d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>>> (+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<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>>(elementType: d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>, elementCount: number): d.WgslArray<d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>> (+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 Particle: d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>
    Particle
    ,
    const count: number
    count
    )).
    TgpuBuffer<WgslArray<WgslStruct<{ position: Vec2f; direction: Vec2f; }>>>.$usage<["storage"]>(usages_0: "storage"): TgpuBuffer<d.WgslArray<d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>>> & StorageFlag
    $usage
    ('storage'));
    const
    const writable: TgpuMutable<d.WgslArray<d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>>>
    writable
    =
    const particles: TgpuBuffer<d.WgslArray<d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>>> & StorageFlag
    particles
    .
    TgpuBuffer<WgslArray<WgslStruct<{ position: Vec2f; direction: Vec2f; }>>>.as<"mutable">(usage: "mutable"): TgpuMutable<d.WgslArray<d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>>>
    as
    ('mutable');
    const
    const readable: TgpuReadonly<d.WgslArray<d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>>>
    readable
    =
    const particles: TgpuBuffer<d.WgslArray<d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>>> & StorageFlag
    particles
    .
    TgpuBuffer<WgslArray<WgslStruct<{ position: Vec2f; direction: Vec2f; }>>>.as<"readonly">(usage: "readonly"): TgpuReadonly<d.WgslArray<d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>>>
    as
    ('readonly');
    • size is anything with a width and height in pixels, such as the canvas surface. The module reads it when it draws.
    • owner collects what you pass through own and frees it all at destroy. Free the particle buffer with the module; the small uniforms below can wait for the garbage collector.
    • writable and readable are the same buffer, bound for writing in kernels and for reading in the vertex function. WebGPU forbids writable storage in a vertex function.

    A kernel is a compute function the GPU runs many times at once. Each run is a thread, numbered by gid.x from 0, and thread n places particle n. Threads start in workgroups of 64, so dispatch enough workgroups to cover every particle. The last workgroup can run past the end of the buffer, and the bounds check stops those threads:

    Change the particle count to see dispatch round up to whole workgroups of 64 threads. The bounds check if (gid.x >= count) return stops extra threads before buffer access.

    Add the seed kernel and return the module’s first recording method:

    flow-field.ts
    /** A random position across the canvas, in clip space. */
    const
    const spawn: () => d.v2f

    A random position across the canvas, in clip space.

    spawn
    = () => {
    'use gpu';
    return
    import d
    d
    .
    const vec2f: d.Vec2f
    (x: number, y: number) => d.v2f (+3 overloads)
    vec2f
    (
    const randf: {
    seed: TgpuFn<(seed: d.F32) => d.Void>;
    seed2: TgpuFn<(seed: d.Vec2f) => d.Void>;
    seed3: TgpuFn<(seed: d.Vec3f) => d.Void>;
    seed4: TgpuFn<(seed: d.Vec4f) => d.Void>;
    sample: TgpuFn<() => d.F32>;
    sampleExclusive: TgpuFn<() => d.F32>;
    normal: TgpuFn<(mu: d.F32, sigma: d.F32) => d.F32>;
    ... 10 more ...;
    onUnitSphere: TgpuFn<...>;
    }
    randf
    .
    sample: () => number

    Returns a random f32 value in [0, 1) range.

    sample
    (),
    const randf: {
    seed: TgpuFn<(seed: d.F32) => d.Void>;
    seed2: TgpuFn<(seed: d.Vec2f) => d.Void>;
    seed3: TgpuFn<(seed: d.Vec3f) => d.Void>;
    seed4: TgpuFn<(seed: d.Vec4f) => d.Void>;
    sample: TgpuFn<() => d.F32>;
    sampleExclusive: TgpuFn<() => d.F32>;
    normal: TgpuFn<(mu: d.F32, sigma: d.F32) => d.F32>;
    ... 10 more ...;
    onUnitSphere: TgpuFn<...>;
    }
    randf
    .
    sample: () => number

    Returns a random f32 value in [0, 1) range.

    sample
    ()) * 2 - 1;
    };
    // Inside createFlowField
    const
    const seedValue: TgpuUniform<d.F32>
    seedValue
    =
    root: TgpuRoot
    root
    .
    TgpuRoot.createUniform<d.F32>(typeSchema: d.F32, initial?: number | ((buffer: TgpuBuffer<NoInfer<d.F32>>) => void) | undefined): TgpuUniform<d.F32> (+1 overload)

    Allocates memory on the GPU, allows passing data between host and shader. Read-only on the GPU, optimized for small data. 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)

    createUniform
    (
    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
    const workgroups: number
    workgroups
    =
    var Math: Math

    An intrinsic object that provides basic mathematics functionality and constants.

    Math
    .
    Math.ceil(x: number): number

    Returns the smallest integer greater than or equal to its numeric argument.

    @param ― x A numeric expression.

    ceil
    (
    const count: number
    count
    /
    const WORKGROUP: 64
    WORKGROUP
    );
    const
    const seedKernel: TgpuComputeFn<{
    gid: d.BuiltinGlobalInvocationId;
    }>
    seedKernel
    =
    const tgpu: {
    const: {
    <TDataType extends d.AnyData>(dataType: TDataType, value: d.InferGPU<TDataType>): TgpuConst<TDataType>;
    <TElement extends d.AnyWgslData>(dataType: (elementCount: number) => d.WgslArray<TElement>, value: d.InferGPU<d.WgslArray<TElement>>): TgpuConst<d.WgslArray<TElement>>;
    };
    fn: {
    <Args extends d.BaseData[] | []>(argTypes: Args, returnType?: undefined): TgpuFnShell<Args, d.Void>;
    <Args extends d.BaseData[] | [], Return extends d.BaseData>(argTypes: Args, returnType: Return): TgpuFnShell<Args, Return>;
    <T extends AnyFn>(inner: T): TgpuGenericFn<T>;
    };
    ... 17 more ...;
    '~unstable': typeof import("../../node_modules/typegpu/tgpuUnstable");
    }

    @module ― typegpu

    tgpu
    .
    computeFn: <{
    gid: d.BuiltinGlobalInvocationId;
    }>(options: {
    in: {
    gid: d.BuiltinGlobalInvocationId;
    };
    workgroupSize: number[];
    }) => TgpuComputeFnShell<{
    gid: d.BuiltinGlobalInvocationId;
    }> (+1 overload)
    computeFn
    ({
    workgroupSize: number[]
    workgroupSize
    : [
    const WORKGROUP: 64
    WORKGROUP
    ],
    in: {
    gid: d.BuiltinGlobalInvocationId;
    }
    in
    : {
    gid: d.BuiltinGlobalInvocationId
    gid
    :
    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
    .
    globalInvocationId: d.BuiltinGlobalInvocationId
    globalInvocationId
    },
    })(({
    gid: d.v3u
    gid
    }) => {
    'use gpu';
    if (
    gid: d.v3u
    gid
    .
    v3u.x: number
    x
    >=
    const count: number
    count
    ) return;
    const randf: {
    seed: TgpuFn<(seed: d.F32) => d.Void>;
    seed2: TgpuFn<(seed: d.Vec2f) => d.Void>;
    seed3: TgpuFn<(seed: d.Vec3f) => d.Void>;
    seed4: TgpuFn<(seed: d.Vec4f) => d.Void>;
    sample: TgpuFn<() => d.F32>;
    sampleExclusive: TgpuFn<() => d.F32>;
    normal: TgpuFn<(mu: d.F32, sigma: d.F32) => d.F32>;
    ... 10 more ...;
    onUnitSphere: TgpuFn<...>;
    }
    randf
    .
    seed2: (seed: d.v2f) => void

    Threads do not share the generator's State. As a result, unless you change the seed in each thread, each thread will produce the same sequence. randf.randSeed2 sets the private seed of the thread.

    @param ― seed seed value to set. For the best results, all elements should be in [-1000, 1000] range.

    seed2
    (
    import d
    d
    .
    const vec2f: d.Vec2f
    (x: number, y: number) => d.v2f (+3 overloads)
    vec2f
    (
    import d
    d
    .
    function f32(v?: number | boolean): number
    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
    (
    gid: d.v3u
    gid
    .
    v3u.x: number
    x
    ),
    const seedValue: TgpuUniform<d.F32>
    seedValue
    .
    TgpuUniform<F32>.$: number
    $
    ));
    const writable: TgpuMutable<d.WgslArray<d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>>>
    writable
    .
    TgpuMutable<WgslArray<WgslStruct<{ position: Vec2f; direction: Vec2f; }>>>.$: {
    position: d.v2f;
    direction: d.v2f;
    }[]
    $
    [
    gid: d.v3u
    gid
    .
    v3u.x: number
    x
    ] =
    const Particle: d.WgslStruct
    (props?: {
    position: d.v2f;
    direction: d.v2f;
    } | undefined) => {
    position: d.v2f;
    direction: d.v2f;
    }
    Particle
    ({
    position: d.v2f
    position
    :
    const spawn: () => d.v2f

    A random position across the canvas, in clip space.

    spawn
    (),
    direction: d.v2f
    direction
    :
    import d
    d
    .
    const vec2f: d.Vec2f
    () => d.v2f (+3 overloads)
    vec2f
    () });
    });
    const
    const seedPipeline: TgpuComputePipeline
    seedPipeline
    =
    root: TgpuRoot
    root
    .
    WithBinding.createComputePipeline<{
    gid: d.BuiltinGlobalInvocationId;
    }>(descriptor: TgpuComputePipeline.Descriptor<{
    gid: d.BuiltinGlobalInvocationId;
    }>): TgpuComputePipeline
    createComputePipeline
    ({
    compute: TgpuComputeFn<{
    gid: d.BuiltinGlobalInvocationId;
    }>
    compute
    :
    const seedKernel: TgpuComputeFn<{
    gid: d.BuiltinGlobalInvocationId;
    }>
    seedKernel
    });
    return {
    function seed(pass: TgpuComputePass): void
    seed
    (
    pass: TgpuComputePass
    pass
    :
    (alias) interface TgpuComputePass
    import TgpuComputePass

    A compute pass recording into a

    TgpuCommandEncoder

    .

    Dispatch either by binding TypeGPU pipelines to it (pipeline.with(pass).dispatchWorkgroups(...)), or proxy-style via pass.setPipeline(pipeline) followed by pass.dispatchWorkgroups(...).

    Call end() when done recording.

    TgpuComputePass
    ) {
    const own: Owner
    own
    .
    Owner.usable(): void

    Throws the owner's message if it is destroyed. Call first in methods that use GPU resources, including inject, recording methods, writes and getters that return GPU objects.

    usable
    ();
    const seedValue: TgpuUniform<d.F32>
    seedValue
    .
    TgpuBufferBindingBase<F32>.write(data: number, options?: BufferWriteOptions): void
    write
    (
    var Math: Math

    An intrinsic object that provides basic mathematics functionality and constants.

    Math
    .
    Math.random(): number

    Returns a pseudorandom number between 0 and 1.

    random
    ());
    pass: TgpuComputePass
    pass
    .
    TgpuComputePass.setPipeline(pipeline: TgpuComputePipeline): void

    Sets the current

    TgpuComputePipeline

    for subsequent dispatches

    setPipeline
    (
    const seedPipeline: TgpuComputePipeline
    seedPipeline
    );
    pass: TgpuComputePass
    pass
    .
    TgpuComputePass.dispatchWorkgroups(x: number, y?: number, z?: number): void
    dispatchWorkgroups
    (
    const workgroups: number
    workgroups
    );
    },
    destroy: () => void
    destroy
    :
    const own: Owner
    own
    .
    Owner.destroy(): void

    Frees everything it took, the last taken first, once; later calls do nothing. When one throws, frees the rest and then rethrows. Also [Symbol.dispose].

    destroy
    ,
    [
    var Symbol: SymbolConstructor
    Symbol
    .
    SymbolConstructor.dispose: typeof Symbol.dispose

    A method that is used to release resources held by an object. Called by the semantics of the using statement.

    dispose
    ]:
    const own: Owner
    own
    .
    Owner.destroy(): void

    Frees everything it took, the last taken first, once; later calls do nothing. When one throws, frees the rest and then rethrows. Also [Symbol.dispose].

    destroy
    ,
    };
    }
    • randf from @typegpu/noise gives each thread its own random numbers once you seed it. Seed it with the thread number and a value the CPU picks, so every reseed scatters the particles differently.
    • seedValue is a uniform, a small value the CPU writes and every thread reads.
    • seed takes a compute pass that the caller opens, so a scene can record several modules into one pass. It records a dispatch and returns; the scene submits the frame.
    • own.usable() throws the owner’s message once the module is destroyed, so a stale scene fails with a clear error rather than binding a freed buffer. destroy and [Symbol.dispose] are the same function, so using frees the module at the end of a block.

    Nothing draws yet. The scene uses the module in step 3.

  2. Draw them.

    A draw runs two functions. The vertex function places the corners of shapes on the screen, and the fragment function colours each pixel they cover. Draw each particle as a small square of four corners, one instance per particle.

    The square’s size is in pixels, so the vertex function needs the canvas size, and the point size is a setting the reader can change. Put the settings in a struct uniform at module scope, so other code can name its type, and the canvas size in a uniform of its own:

    flow-field.ts
    export const
    const FlowSettings: d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>
    FlowSettings
    =
    import d
    d
    .
    struct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>(props: {
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }): d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>
    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
    ({
    /** Clip-space units per second. */
    speed: d.F32

    Clip-space units per second.

    speed
    :
    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
    ,
    /** Noise cells across the height. */
    scale: d.F32

    Noise cells across the height.

    scale
    :
    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
    ,
    /** Particle diameter in pixels. */
    pointSize: d.F32

    Particle diameter in pixels.

    pointSize
    :
    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
    ,
    });
    export const
    const defaults: {
    speed: number;
    scale: number;
    pointSize: number;
    }
    defaults
    :
    import d
    d
    .
    type Infer<T> = T extends {
    readonly [$repr]: infer TRepr;
    } ? TRepr : T
    export Infer

    Extracts the inferred representation of a resource. For inferring types as seen by the GPU, see

    InferGPU

    @example type A = Infer // => number type B = Infer<WgslArray> // => number[] type C = Infer<Atomic> // => number

    Infer
    <typeof
    const FlowSettings: d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>
    FlowSettings
    > = {
    speed: number

    Clip-space units per second.

    speed
    : 0.2,
    scale: number

    Noise cells across the height.

    scale
    : 1.5,
    pointSize: number

    Particle diameter in pixels.

    pointSize
    : 2 };
    // In FlowFieldOptions
    /** Initial values for speed, scale and point size. Override `defaults`. */
    readonly
    FlowFieldOptions.settings?: Partial<d.InferInput<d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>>> | undefined

    Initial values for speed, scale and point size. Override defaults.

    settings
    ?:
    type Partial<T> = { [P in keyof T]?: T[P] | undefined; }

    Make all properties in T optional

    Partial
    <
    import d
    d
    .
    type InferInput<T> = d.Infer<T> | (T extends {
    readonly [$inRepr]: infer TRepr;
    } ? TRepr : never)
    export InferInput

    Extracts the inferred input (write-side) representation of a resource.

    @example type A = InferInput // => v3f | [number, number, number] | Float32Array type B = InferInput<WgslArray> // => (v3f | [number, number, number] | Float32Array)[] | Float32Array type C = InferInput // => number (same as Infer) const arrayOfStructs = d.arrayOf(d.struct({ pos: d.vec3f, id: d.f32 }), 4); type D = d.InferInput; // { pos: d.v3f | Float32Array | [number, number, number]; id: number; }[]

    InferInput
    <typeof
    const FlowSettings: d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>
    FlowSettings
    >>;
    // Inside createFlowField
    const
    const settings: TgpuUniform<d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>>
    settings
    =
    root: TgpuRoot
    root
    .
    TgpuRoot.createUniform<d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>>(typeSchema: d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>, initial?: ((buffer: TgpuBuffer<NoInfer<d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>>>) => void) | d.InferInput<NoInfer<d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>>> | undefined): TgpuUniform<d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>> (+1 overload)

    Allocates memory on the GPU, allows passing data between host and shader. Read-only on the GPU, optimized for small data. 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)

    createUniform
    (
    const FlowSettings: d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>
    FlowSettings
    , { ...
    const defaults: {
    speed: number;
    scale: number;
    pointSize: number;
    }
    defaults
    , ...
    options: FlowFieldOptions
    options
    .
    FlowFieldOptions.settings?: Partial<d.InferInput<d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>>> | undefined

    Initial values for speed, scale and point size. Override defaults.

    settings
    });
    const
    const viewport: TgpuUniform<d.Vec2f>
    viewport
    =
    root: TgpuRoot
    root
    .
    TgpuRoot.createUniform<d.Vec2f>(typeSchema: d.Vec2f, initial?: ((buffer: TgpuBuffer<NoInfer<d.Vec2f>>) => void) | d.InferInput<NoInfer<d.Vec2f>> | undefined): TgpuUniform<d.Vec2f> (+1 overload)

    Allocates memory on the GPU, allows passing data between host and shader. Read-only on the GPU, optimized for small data. 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)

    createUniform
    (
    import d
    d
    .
    const vec2f: d.Vec2f
    vec2f
    ,
    import d
    d
    .
    const vec2f: d.Vec2f
    (x: number, y: number) => d.v2f (+3 overloads)
    vec2f
    (
    const size: SizeSource
    size
    .
    SizeSource.width: number
    width
    ,
    const size: SizeSource
    size
    .
    SizeSource.height: number
    height
    ));

    speed and scale steer the particles from step 4. Below the uniforms, write the two functions. The vertex function runs four times per particle. It reads the particle at instanceIndex, turns vertexIndex into a corner of the square and colours the particle by its direction. The fragment function fades the square into a soft dot:

    flow-field.ts
    const
    const vertex: TgpuVertexFn<{}, {
    color: d.Vec3f;
    corner: d.Vec2f;
    }>
    vertex
    =
    const tgpu: {
    const: {
    <TDataType extends d.AnyData>(dataType: TDataType, value: d.InferGPU<TDataType>): TgpuConst<TDataType>;
    <TElement extends d.AnyWgslData>(dataType: (elementCount: number) => d.WgslArray<TElement>, value: d.InferGPU<d.WgslArray<TElement>>): TgpuConst<d.WgslArray<TElement>>;
    };
    fn: {
    <Args extends d.BaseData[] | []>(argTypes: Args, returnType?: undefined): TgpuFnShell<Args, d.Void>;
    <Args extends d.BaseData[] | [], Return extends d.BaseData>(argTypes: Args, returnType: Return): TgpuFnShell<Args, Return>;
    <T extends AnyFn>(inner: T): TgpuGenericFn<T>;
    };
    ... 17 more ...;
    '~unstable': typeof import("../../node_modules/typegpu/tgpuUnstable");
    }

    @module ― typegpu

    tgpu
    .
    vertexFn: <{
    vertexIndex: d.BuiltinVertexIndex;
    instanceIndex: d.BuiltinInstanceIndex;
    }, {
    position: d.BuiltinPosition;
    color: d.Vec3f;
    corner: d.Vec2f;
    }>(options: {
    in: {
    vertexIndex: d.BuiltinVertexIndex;
    instanceIndex: d.BuiltinInstanceIndex;
    };
    out: {
    position: d.BuiltinPosition;
    color: d.Vec3f;
    corner: d.Vec2f;
    };
    }) => TgpuVertexFnShell<{
    vertexIndex: d.BuiltinVertexIndex;
    instanceIndex: d.BuiltinInstanceIndex;
    }, {
    position: d.BuiltinPosition;
    color: d.Vec3f;
    corner: d.Vec2f;
    }> (+1 overload)
    vertexFn
    ({
    in: {
    vertexIndex: d.BuiltinVertexIndex;
    instanceIndex: d.BuiltinInstanceIndex;
    }
    in
    : {
    vertexIndex: d.BuiltinVertexIndex
    vertexIndex
    :
    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
    .
    vertexIndex: d.BuiltinVertexIndex
    vertexIndex
    ,
    instanceIndex: d.BuiltinInstanceIndex
    instanceIndex
    :
    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
    .
    instanceIndex: d.BuiltinInstanceIndex
    instanceIndex
    },
    out: {
    position: d.BuiltinPosition;
    color: d.Vec3f;
    corner: d.Vec2f;
    }
    out
    : {
    position: d.BuiltinPosition
    position
    :
    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
    .
    position: d.BuiltinPosition
    position
    ,
    color: d.Vec3f
    color
    :
    import d
    d
    .
    const vec3f: d.Vec3f
    vec3f
    ,
    corner: d.Vec2f
    corner
    :
    import d
    d
    .
    const vec2f: d.Vec2f
    vec2f
    },
    })(({
    vertexIndex: number
    vertexIndex
    ,
    instanceIndex: number
    instanceIndex
    }) => {
    'use gpu';
    const
    const particle: {
    position: d.v2f;
    direction: d.v2f;
    }
    particle
    =
    const readable: TgpuReadonly<d.WgslArray<d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>>>
    readable
    .
    TgpuReadonly<WgslArray<WgslStruct<{ position: Vec2f; direction: Vec2f; }>>>.$: {
    position: d.v2f;
    direction: d.v2f;
    }[]
    $
    [
    instanceIndex: number
    instanceIndex
    ];
    const
    const corner: d.v2f
    corner
    =
    import d
    d
    .
    const vec2f: d.Vec2f
    (x: number, y: number) => d.v2f (+3 overloads)
    vec2f
    (
    import d
    d
    .
    function f32(v?: number | boolean): number
    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
    (
    vertexIndex: number
    vertexIndex
    & 1),
    import d
    d
    .
    function f32(v?: number | boolean): number
    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
    (
    vertexIndex: number
    vertexIndex
    >>> 1)) * 2 - 1;
    const
    const extent: d.v2f
    extent
    = (
    const corner: d.v2f
    corner
    *
    const settings: TgpuUniform<d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>>
    settings
    .
    TgpuUniform<WgslStruct<{ speed: F32; scale: F32; pointSize: F32; }>>.$: {
    speed: number;
    scale: number;
    pointSize: number;
    }
    $
    .
    pointSize: number
    pointSize
    ) /
    const viewport: TgpuUniform<d.Vec2f>
    viewport
    .
    TgpuUniform<Vec2f>.$: d.v2f
    $
    ;
    const
    const center: d.v2f
    center
    =
    const particle: {
    position: d.v2f;
    direction: d.v2f;
    }
    particle
    .
    position: d.v2f
    position
    ;
    return {
    position: d.v4f
    position
    :
    import d
    d
    .
    const vec4f: d.Vec4f
    (v0: AnyNumericVec2Instance, z: number, w: number) => d.v4f (+9 overloads)
    vec4f
    (
    const center: d.v2f
    center
    +
    const extent: d.v2f
    extent
    , 0, 1),
    color: d.v3f
    color
    :
    import d
    d
    .
    const vec3f: d.Vec3f
    (v0: AnyNumericVec2Instance, z: number) => d.v3f (+5 overloads)
    vec3f
    (
    const particle: {
    position: d.v2f;
    direction: d.v2f;
    }
    particle
    .
    direction: d.v2f
    direction
    * 0.5 + 0.5, 0.9),
    corner: d.v2f
    corner
    ,
    };
    });
    const
    const fragment: TgpuFragmentFn<{
    color: d.Vec3f;
    corner: d.Vec2f;
    }, d.Vec4f>
    fragment
    =
    const tgpu: {
    const: {
    <TDataType extends d.AnyData>(dataType: TDataType, value: d.InferGPU<TDataType>): TgpuConst<TDataType>;
    <TElement extends d.AnyWgslData>(dataType: (elementCount: number) => d.WgslArray<TElement>, value: d.InferGPU<d.WgslArray<TElement>>): TgpuConst<d.WgslArray<TElement>>;
    };
    fn: {
    <Args extends d.BaseData[] | []>(argTypes: Args, returnType?: undefined): TgpuFnShell<Args, d.Void>;
    <Args extends d.BaseData[] | [], Return extends d.BaseData>(argTypes: Args, returnType: Return): TgpuFnShell<Args, Return>;
    <T extends AnyFn>(inner: T): TgpuGenericFn<T>;
    };
    ... 17 more ...;
    '~unstable': typeof import("../../node_modules/typegpu/tgpuUnstable");
    }

    @module ― typegpu

    tgpu
    .
    fragmentFn: <{
    color: d.Vec3f;
    corner: d.Vec2f;
    }, d.Vec4f>(options: {
    in: {
    color: d.Vec3f;
    corner: d.Vec2f;
    };
    out: d.Vec4f;
    }) => TgpuFragmentFnShell<{
    color: d.Vec3f;
    corner: d.Vec2f;
    }, d.Vec4f> (+1 overload)
    fragmentFn
    ({
    in: {
    color: d.Vec3f;
    corner: d.Vec2f;
    }
    in
    : {
    color: d.Vec3f
    color
    :
    import d
    d
    .
    const vec3f: d.Vec3f
    vec3f
    ,
    corner: d.Vec2f
    corner
    :
    import d
    d
    .
    const vec2f: d.Vec2f
    vec2f
    },
    out: d.Vec4f
    out
    :
    import d
    d
    .
    const vec4f: d.Vec4f
    vec4f
    ,
    })(({
    color: d.v3f
    color
    ,
    corner: d.v2f
    corner
    }) => {
    'use gpu';
    const
    const falloff: number
    falloff
    = 1 -
    import std
    std
    .
    function smoothstep(edge0: number, edge1: number, x: number): number (+1 overload)
    export smoothstep
    smoothstep
    (0.4, 1,
    import std
    std
    .
    length<d.v2f>(value: d.v2f): number (+1 overload)
    export length
    length
    (
    corner: d.v2f
    corner
    ));
    return
    import d
    d
    .
    const vec4f: d.Vec4f
    (v0: AnyNumericVec3Instance, w: number) => d.v4f (+9 overloads)
    vec4f
    (
    color: d.v3f
    color
    * (
    const falloff: number
    falloff
    * 0.5),
    const falloff: number
    falloff
    );
    });

    Make the render pipeline with additive blending, so dots that overlap brighten. Return draw, which takes a render pass, and initAsync. Import pipelines from @typegpu/kit and the type TgpuRenderCommands from typegpu:

    flow-field.ts
    const
    const drawPipeline: TgpuRenderPipeline<d.Vec4f>
    drawPipeline
    =
    const root: TgpuRoot
    root
    .
    WithBinding.createRenderPipeline<{}, {}, {
    color: d.Vec3f;
    corner: d.Vec2f;
    }, d.Vec4f>(descriptor: TgpuRenderPipeline<in Targets = never>.DescriptorBase & {
    attribs?: {} | undefined;
    vertex: TgpuVertexFn<{}, {
    color: d.Vec3f;
    corner: d.Vec2f;
    }> | ((input: AutoVertexIn<InferGPURecord<AttribRecordToDefaultDataTypes<{}>>>) => AutoVertexOut<AnyAutoCustoms>);
    fragment: TgpuFragmentFn<{
    color: d.Vec3f;
    corner: d.Vec2f;
    } & Record<string, AnyFragmentInputBuiltin>, d.Vec4f> | ((input: AutoFragmentIn<...>) => d.v4f | (AnyAutoCustoms & Partial<...>));
    targets?: TgpuColorTargetState;
    }): TgpuRenderPipeline<...> (+2 overloads)
    createRenderPipeline
    ({
    vertex: TgpuVertexFn<{}, {
    color: d.Vec3f;
    corner: d.Vec2f;
    }> | ((input: AutoVertexIn<InferGPURecord<AttribRecordToDefaultDataTypes<{}>>>) => AutoVertexOut<AnyAutoCustoms>)
    vertex
    ,
    fragment: TgpuFragmentFn<{
    color: d.Vec3f;
    corner: d.Vec2f;
    } & Record<string, AnyFragmentInputBuiltin>, d.Vec4f> | ((input: AutoFragmentIn<InferGPURecord<{
    color: d.Vec3f;
    corner: d.Vec2f;
    }>>) => d.v4f | (AnyAutoCustoms & Partial<InferGPURecord<{
    readonly $fragDepth: d.BuiltinFragDepth;
    readonly $sampleMask: d.BuiltinSampleMask;
    }>>))
    fragment
    ,
    targets?: TgpuColorTargetState
    targets
    : {
    format?: GPUTextureFormat | undefined

    The

    GPUTextureFormat

    of this color target. The pipeline will only be compatible with

    GPURenderPassEncoder

    s which use a

    GPUTextureView

    of this format in the corresponding color attachment.

    @default ― navigator.gpu.getPreferredCanvasFormat()

    format
    ,
    blend?: GPUBlendState | undefined

    The blending behavior for this color target. If left undefined, disables blending for this color target.

    blend
    :
    import pipelines
    pipelines
    .
    const blend: Readonly<{
    add: Readonly<{
    color: GPUBlendComponent;
    alpha: GPUBlendComponent;
    }>;
    over: Readonly<{
    color: GPUBlendComponent;
    alpha: GPUBlendComponent;
    }>;
    straight: Readonly<{
    color: GPUBlendComponent;
    alpha: GPUBlendComponent;
    }>;
    }>

    Provides blend states for pipeline targets.

    blend
    .
    add: Readonly<{
    color: GPUBlendComponent;
    alpha: GPUBlendComponent;
    }>

    Adds fragment values to the target, for sparks, glow or bloom.

    add
    },
    TgpuRenderPipeline<in Targets = never>.DescriptorBase.primitive?: TgpuPrimitiveState

    Describes the primitive-related properties of the pipeline.

    primitive
    : {
    topology: "triangle-strip"
    topology
    : 'triangle-strip' },
    });
    let
    let width: number
    width
    =
    const size: SizeSource
    size
    .
    SizeSource.width: number
    width
    ;
    let
    let height: number
    height
    =
    const size: SizeSource
    size
    .
    SizeSource.height: number
    height
    ;
    return {
    settings: TgpuUniform<d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>>
    settings
    ,
    async
    function initAsync(): Promise<void>
    initAsync
    () {
    await
    var Promise: PromiseConstructor

    Represents the completion of an asynchronous operation

    Promise
    .
    PromiseConstructor.all<[Promise<void>, Promise<void>]>(values: [Promise<void>, Promise<void>]): Promise<[void, void]> (+1 overload)

    Creates a Promise that is resolved with an array of results when all of the provided Promises resolve, or rejected when any Promise is rejected.

    @param ― values An array of Promises.

    @returns ― A new Promise.

    all
    ([
    const seedPipeline: {
    initAsync(): Promise<void>;
    }
    seedPipeline
    .
    function initAsync(): Promise<void>
    initAsync
    (),
    const drawPipeline: TgpuRenderPipeline<d.Vec4f>
    drawPipeline
    .
    TgpuRenderPipeline<Vec4f>.initAsync(): Promise<void>

    Immediately resolves the pipeline, then awaits device.createRenderPipelineAsync(). NOTE: it is not necessary to initialize pipelines manually.

    initAsync
    ()]);
    },
    function draw(pass: TgpuRenderCommands): void
    draw
    (
    pass: TgpuRenderCommands
    pass
    :
    (alias) interface TgpuRenderCommands
    import TgpuRenderCommands

    The draw-recording surface shared by render passes and render bundle encoders, mirroring

    GPURenderCommandsMixin

    .

    Draw either by binding TypeGPU pipelines to it (pipeline.with(pass).draw(...)), or proxy-style via pass.setPipeline(pipeline) followed by pass.draw(...). Pipeline resolution is lazy: shaders compile on the first draw.

    TgpuRenderCommands
    ) {
    const own: Owner
    own
    .
    Owner.usable(): void

    Throws the owner's message if it is destroyed. Call first in methods that use GPU resources, including inject, recording methods, writes and getters that return GPU objects.

    usable
    ();
    if (
    const size: SizeSource
    size
    .
    SizeSource.width: number
    width
    !==
    let width: number
    width
    ||
    const size: SizeSource
    size
    .
    SizeSource.height: number
    height
    !==
    let height: number
    height
    ) {
    let width: number
    width
    =
    const size: SizeSource
    size
    .
    SizeSource.width: number
    width
    ;
    let height: number
    height
    =
    const size: SizeSource
    size
    .
    SizeSource.height: number
    height
    ;
    const viewport: TgpuUniform<d.Vec2f>
    viewport
    .
    TgpuBufferBindingBase<Vec2f>.write(data: d.InferInput<d.Vec2f>, options?: BufferWriteOptions): void
    write
    (
    import d
    d
    .
    const vec2f: d.Vec2f
    (x: number, y: number) => d.v2f (+3 overloads)
    vec2f
    (
    let width: number
    width
    ,
    let height: number
    height
    ));
    }
    pass: TgpuRenderCommands
    pass
    .
    TgpuRenderCommands.setPipeline(pipeline: TgpuRenderPipeline): void

    Sets the current

    TgpuRenderPipeline

    for subsequent draw calls

    setPipeline
    (
    const drawPipeline: TgpuRenderPipeline<d.Vec4f>
    drawPipeline
    );
    pass: TgpuRenderCommands
    pass
    .
    TgpuRenderCommands.draw(vertexCount: number, instanceCount?: number, firstVertex?: number, firstInstance?: number): void
    draw
    (4,
    const count: number
    count
    );
    },
    // seed, destroy and [Symbol.dispose] from step 1
    };
    • A triangle strip of four corners makes the square from two triangles. draw(4, count) draws it once for every particle.
    • draw writes viewport only when the canvas size changes. It compares against the size it last wrote, so a steady frame uploads nothing.
    • initAsync compiles the pipelines ahead of the first frame. Without it, each pipeline compiles when it first records, and that frame stalls.
    • settings is returned so the scene can write it when a setting changes, in step 6.
  3. Run the module in your scene.

    Replace scene.ts from Getting started. The scene makes the field and waits for its pipelines. Each frame records into an encoder, a list of GPU commands that the loop submits when frame returns. Commands come in passes: a compute pass for the seed, then a render pass that clears the canvas and draws:

    scene.ts
    import {
    function owner(message?: string): Owner

    Collects what a scene or module makes, to free it all in one call, and guards its methods. Pass each thing through it as you make it, and return its destroy as your own. Take a resource that is replaced during the module's life, such as a texture that follows the canvas size, as one cleanup that frees the current one. Methods that only read CPU state or do optional work, such as capture, take or a camera's update, should return early on destroyed instead of calling usable.

    const own = owner('The swarm was destroyed.');
    const screen = own(targets.create(root, surface, { hdr }));
    const settings = own(root.createUniform(Settings));
    own(() => {
    for (const buffer of reads) buffer.destroy();
    });
    return { step(pass) { own.usable(); … }, destroy: own.destroy, [Symbol.dispose]: own.destroy };

    @param ― message what usable throws, naming the module: "The table was destroyed."

    @returns ― an owner you pass each thing to and destroy

    owner
    , type
    (alias) interface Frame
    import Frame

    Represents one frame being recorded. Each run creates a separate object, so modules can compare frames by identity.

    Frame
    , type
    (alias) interface Surface
    import Surface

    Provides a destination for frames, such as a canvas from web.surface or native.surface, or a texture from offscreenSurface in a test. Pass the frame to view for code that works with every destination.

    Surface
    } from '@typegpu/kit';
    import type {
    (alias) interface TgpuRoot
    import TgpuRoot
    TgpuRoot
    } from 'typegpu';
    import {
    function createFlowField(root: TgpuRoot, options: FlowFieldOptions): {
    settings: TgpuUniform<WgslStruct<{
    speed: F32;
    scale: F32;
    pointSize: F32;
    }>>;
    initAsync(): Promise<void>;
    seed(pass: TgpuComputePass): void;
    step(pass: TgpuComputePass): void;
    draw(pass: TgpuRenderCommands): void;
    destroy: () => void;
    [Symbol.dispose]: () => void;
    }

    Advects particles through Perlin noise. Owns the buffers and records into caller passes.

    createFlowField
    } from './flow-field.ts';
    export async function
    function createScene(root: TgpuRoot, surface: Surface): Promise<{
    frame(frame: Frame): void;
    destroy: () => void;
    }>
    createScene
    (
    root: TgpuRoot
    root
    :
    (alias) interface TgpuRoot
    import TgpuRoot
    TgpuRoot
    ,
    surface: Surface
    surface
    :
    (alias) interface Surface
    import Surface

    Provides a destination for frames, such as a canvas from web.surface or native.surface, or a texture from offscreenSurface in a test. Pass the frame to view for code that works with every destination.

    Surface
    ) {
    const
    const own: Owner
    own
    =
    function owner(message?: string): Owner

    Collects what a scene or module makes, to free it all in one call, and guards its methods. Pass each thing through it as you make it, and return its destroy as your own. Take a resource that is replaced during the module's life, such as a texture that follows the canvas size, as one cleanup that frees the current one. Methods that only read CPU state or do optional work, such as capture, take or a camera's update, should return early on destroyed instead of calling usable.

    const own = owner('The swarm was destroyed.');
    const screen = own(targets.create(root, surface, { hdr }));
    const settings = own(root.createUniform(Settings));
    own(() => {
    for (const buffer of reads) buffer.destroy();
    });
    return { step(pass) { own.usable(); … }, destroy: own.destroy, [Symbol.dispose]: own.destroy };

    @param ― message what usable throws, naming the module: "The table was destroyed."

    @returns ― an owner you pass each thing to and destroy

    owner
    ();
    const
    const field: {
    settings: TgpuUniform<WgslStruct<{
    speed: F32;
    scale: F32;
    pointSize: F32;
    }>>;
    initAsync(): Promise<void>;
    seed(pass: TgpuComputePass): void;
    step(pass: TgpuComputePass): void;
    draw(pass: TgpuRenderCommands): void;
    destroy: () => void;
    [Symbol.dispose]: () => void;
    }
    field
    =
    const own: Owner
    <{
    settings: TgpuUniform<WgslStruct<{
    speed: F32;
    scale: F32;
    pointSize: F32;
    }>>;
    initAsync(): Promise<void>;
    seed(pass: TgpuComputePass): void;
    step(pass: TgpuComputePass): void;
    draw(pass: TgpuRenderCommands): void;
    destroy: () => void;
    [Symbol.dispose]: () => void;
    }>(item: {
    settings: TgpuUniform<WgslStruct<{
    speed: F32;
    scale: F32;
    pointSize: F32;
    }>>;
    initAsync(): Promise<void>;
    seed(pass: TgpuComputePass): void;
    step(pass: TgpuComputePass): void;
    draw(pass: TgpuRenderCommands): void;
    destroy: () => void;
    [Symbol.dispose]: () => void;
    }) => {
    settings: TgpuUniform<WgslStruct<{
    speed: F32;
    scale: F32;
    pointSize: F32;
    }>>;
    initAsync(): Promise<void>;
    seed(pass: TgpuComputePass): void;
    step(pass: TgpuComputePass): void;
    draw(pass: TgpuRenderCommands): void;
    destroy: () => void;
    [Symbol.dispose]: () => void;
    }

    Takes item to free at destroy and returns it. Passes undefined through. Frees a fixed resource, such as one from root.createUniform, through its buffer, and runs a cleanup function. After destroy, frees item at once.

    own
    (
    function createFlowField(root: TgpuRoot, options: FlowFieldOptions): {
    settings: TgpuUniform<WgslStruct<{
    speed: F32;
    scale: F32;
    pointSize: F32;
    }>>;
    initAsync(): Promise<void>;
    seed(pass: TgpuComputePass): void;
    step(pass: TgpuComputePass): void;
    draw(pass: TgpuRenderCommands): void;
    destroy: () => void;
    [Symbol.dispose]: () => void;
    }

    Advects particles through Perlin noise. Owns the buffers and records into caller passes.

    createFlowField
    (
    root: TgpuRoot
    root
    , {
    FlowFieldOptions.count: number
    count
    : 65_536,
    FlowFieldOptions.step: number

    Seconds per step.

    step
    : 1 / 60,
    FlowFieldOptions.format: GPUTextureFormat
    format
    :
    surface: Surface
    surface
    .
    Surface.format: GPUTextureFormat
    format
    ,
    FlowFieldOptions.size: SizeSource

    What the particles are drawn into, such as the canvas surface.

    size
    :
    surface: Surface
    surface
    ,
    }),
    );
    await
    const field: {
    settings: TgpuUniform<WgslStruct<{
    speed: F32;
    scale: F32;
    pointSize: F32;
    }>>;
    initAsync(): Promise<void>;
    seed(pass: TgpuComputePass): void;
    step(pass: TgpuComputePass): void;
    draw(pass: TgpuRenderCommands): void;
    destroy: () => void;
    [Symbol.dispose]: () => void;
    }
    field
    .
    function initAsync(): Promise<void>
    initAsync
    ();
    let
    let seeding: boolean
    seeding
    = true;
    return {
    function frame(frame: Frame): void
    frame
    (
    frame: Frame
    frame
    :
    (alias) interface Frame
    import Frame

    Represents one frame being recorded. Each run creates a separate object, so modules can compare frames by identity.

    Frame
    ) {
    const
    const compute: TgpuComputePass
    compute
    =
    frame: 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 seeding: boolean
    seeding
    )
    const field: {
    settings: TgpuUniform<WgslStruct<{
    speed: F32;
    scale: F32;
    pointSize: F32;
    }>>;
    initAsync(): Promise<void>;
    seed(pass: TgpuComputePass): void;
    step(pass: TgpuComputePass): void;
    draw(pass: TgpuRenderCommands): void;
    destroy: () => void;
    [Symbol.dispose]: () => void;
    }
    field
    .
    function seed(pass: TgpuComputePass): void
    seed
    (
    const compute: TgpuComputePass
    compute
    );
    let seeding: boolean
    seeding
    = false;
    const compute: TgpuComputePass
    compute
    .
    TgpuComputePass.end(): void

    Completes the recording of this compute pass

    end
    ();
    const
    const pass: TgpuRenderPass
    pass
    =
    frame: 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.beginRenderPass(descriptor: TgpuRenderPassDescriptor): TgpuRenderPass

    Begins recording a render pass. Attachment views accept TypeGPU textures, texture views and canvas contexts, next to raw

    GPUTextureView

    s.

    beginRenderPass
    ({
    TgpuRenderPassDescriptor.colorAttachments?: ColorAttachment | readonly (ColorAttachment | null)[] | undefined
    colorAttachments
    : {
    ColorAttachment.view: GPUTextureView | (ColorTextureConstraint & RenderFlag) | TgpuTextureView<WgslTexture<WgslTextureProps>> | TgpuTextureRenderView | GPUCanvasContext

    A

    GPUTextureView

    describing the texture subresource that will be output to for this color attachment.

    view
    :
    surface: Surface
    surface
    .
    Surface.view(frame: Frame): GPUTextureView

    Returns a view of the current texture, created once per texture. React Native requires the frame to present after submission.

    view
    (
    frame: Frame
    frame
    ),
    ColorAttachment.clearValue?: readonly [number, number, number, number] | GPUColor | undefined

    Indicates the value to clear

    GPURenderPassColorAttachment#view

    to prior to executing the render pass. If not provided, defaults to {r: 0, g: 0, b: 0, a: 0}. Ignored if

    GPURenderPassColorAttachment#loadOp

    is not

    GPULoadOp# "clear"

    . The components of

    GPURenderPassColorAttachment#clearValue

    are all double values. They are converted to a texel value of texture format matching the render attachment. If conversion fails, a validation error is generated.

    clearValue
    : [0.01, 0.015, 0.03, 1] },
    });
    const field: {
    settings: TgpuUniform<WgslStruct<{
    speed: F32;
    scale: F32;
    pointSize: F32;
    }>>;
    initAsync(): Promise<void>;
    seed(pass: TgpuComputePass): void;
    step(pass: TgpuComputePass): void;
    draw(pass: TgpuRenderCommands): void;
    destroy: () => void;
    [Symbol.dispose]: () => void;
    }
    field
    .
    function draw(pass: TgpuRenderCommands): void
    draw
    (
    const pass: TgpuRenderPass
    pass
    );
    const pass: TgpuRenderPass
    pass
    .
    TgpuRenderPass.end(): void

    Completes the recording of this render pass

    end
    ();
    },
    destroy: () => void
    destroy
    :
    const own: Owner
    own
    .
    Owner.destroy(): void

    Frees everything it took, the last taken first, once; later calls do nothing. When one throws, frees the rest and then rethrows. Also [Symbol.dispose].

    destroy
    ,
    };
    }

    The scene owns the field, so it passes it through an owner of its own and returns that owner’s destroy. The host calls it when the scene closes. The root and surface are borrowed from the host, so the scene leaves them alone. The seed and the draw go into one encoder, so the GPU scatters the particles before it draws them.

    The particles appear, scattered and still:

    flow-field65 536 particles, each steered by the Perlin noise under it.
    Reseed to scatter the particles again. The simulation is paused, as yours is until step 4.
  4. Move them through noise.

    Each step, every particle samples Perlin noise at its position, turns the value into an angle and moves that way. Nearby positions get similar angles, so the particles flow in smooth streams. Import perlin2d from @typegpu/noise and std from typegpu, and add the step kernel:

    flow-field.ts
    const
    const TAU: number
    TAU
    =
    var Math: Math

    An intrinsic object that provides basic mathematics functionality and constants.

    Math
    .
    Math.PI: number

    Pi. This is the ratio of the circumference of a circle to its diameter.

    PI
    * 2;
    /** Chance per step of respawning a particle elsewhere. Prevents particles collecting in sinks. */
    const
    const RESPAWN: number

    Chance per step of respawning a particle elsewhere. Prevents particles collecting in sinks.

    RESPAWN
    = 1 / 240;
    // Inside createFlowField
    const
    const stepKernel: TgpuComputeFn<{
    gid: d.BuiltinGlobalInvocationId;
    }>
    stepKernel
    =
    const tgpu: {
    const: {
    <TDataType extends d.AnyData>(dataType: TDataType, value: d.InferGPU<TDataType>): TgpuConst<TDataType>;
    <TElement extends d.AnyWgslData>(dataType: (elementCount: number) => d.WgslArray<TElement>, value: d.InferGPU<d.WgslArray<TElement>>): TgpuConst<d.WgslArray<TElement>>;
    };
    fn: {
    <Args extends d.BaseData[] | []>(argTypes: Args, returnType?: undefined): TgpuFnShell<Args, d.Void>;
    <Args extends d.BaseData[] | [], Return extends d.BaseData>(argTypes: Args, returnType: Return): TgpuFnShell<Args, Return>;
    <T extends AnyFn>(inner: T): TgpuGenericFn<T>;
    };
    ... 17 more ...;
    '~unstable': typeof import("../../node_modules/typegpu/tgpuUnstable");
    }

    @module ― typegpu

    tgpu
    .
    computeFn: <{
    gid: d.BuiltinGlobalInvocationId;
    }>(options: {
    in: {
    gid: d.BuiltinGlobalInvocationId;
    };
    workgroupSize: number[];
    }) => TgpuComputeFnShell<{
    gid: d.BuiltinGlobalInvocationId;
    }> (+1 overload)
    computeFn
    ({
    workgroupSize: number[]
    workgroupSize
    : [
    const WORKGROUP: 64
    WORKGROUP
    ],
    in: {
    gid: d.BuiltinGlobalInvocationId;
    }
    in
    : {
    gid: d.BuiltinGlobalInvocationId
    gid
    :
    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
    .
    globalInvocationId: d.BuiltinGlobalInvocationId
    globalInvocationId
    },
    })(({
    gid: d.v3u
    gid
    }) => {
    'use gpu';
    if (
    gid: d.v3u
    gid
    .
    v3u.x: number
    x
    >=
    const count: number
    count
    ) return;
    const
    const particle: {
    position: d.v2f;
    direction: d.v2f;
    }
    particle
    =
    const writable: TgpuMutable<d.WgslArray<d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>>>
    writable
    .
    TgpuMutable<WgslArray<WgslStruct<{ position: Vec2f; direction: Vec2f; }>>>.$: {
    position: d.v2f;
    direction: d.v2f;
    }[]
    $
    [
    gid: d.v3u
    gid
    .
    v3u.x: number
    x
    ];
    const
    const aspect: number
    aspect
    =
    const viewport: TgpuUniform<d.Vec2f>
    viewport
    .
    TgpuUniform<Vec2f>.$: d.v2f
    $
    .
    v2f.x: number
    x
    /
    const viewport: TgpuUniform<d.Vec2f>
    viewport
    .
    TgpuUniform<Vec2f>.$: d.v2f
    $
    .
    v2f.y: number
    y
    ;
    const
    const sample: d.v2f
    sample
    =
    const particle: {
    position: d.v2f;
    direction: d.v2f;
    }
    particle
    .
    position: d.v2f
    position
    *
    import d
    d
    .
    const vec2f: d.Vec2f
    (x: number, y: number) => d.v2f (+3 overloads)
    vec2f
    (
    const aspect: number
    aspect
    , 1) *
    const settings: TgpuUniform<d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>>
    settings
    .
    TgpuUniform<WgslStruct<{ speed: F32; scale: F32; pointSize: F32; }>>.$: {
    speed: number;
    scale: number;
    pointSize: number;
    }
    $
    .
    scale: number
    scale
    ;
    const
    const angle: number
    angle
    =
    (alias) namespace perlin2d
    import perlin2d
    perlin2d
    .
    index$1.sample(pos: d.v2f): number
    export index$1.sample

    Returns value of Perlin Noise at point pos

    sample
    (
    const sample: d.v2f
    sample
    ) *
    const TAU: number
    TAU
    ;
    const
    const direction: d.v2f
    direction
    =
    import d
    d
    .
    const vec2f: d.Vec2f
    (x: number, y: number) => d.v2f (+3 overloads)
    vec2f
    (
    import std
    std
    .
    function cos(value: number): number (+1 overload)
    export cos
    cos
    (
    const angle: number
    angle
    ),
    import std
    std
    .
    function sin(value: number): number (+1 overload)
    export sin
    sin
    (
    const angle: number
    angle
    ));
    let
    let position: d.v2f
    position
    =
    const particle: {
    position: d.v2f;
    direction: d.v2f;
    }
    particle
    .
    position: d.v2f
    position
    +
    const direction: d.v2f
    direction
    * (
    const settings: TgpuUniform<d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>>
    settings
    .
    TgpuUniform<WgslStruct<{ speed: F32; scale: F32; pointSize: F32; }>>.$: {
    speed: number;
    scale: number;
    pointSize: number;
    }
    $
    .
    speed: number
    speed
    *
    const step: number
    step
    );
    const randf: {
    seed: TgpuFn<(seed: d.F32) => d.Void>;
    seed2: TgpuFn<(seed: d.Vec2f) => d.Void>;
    seed3: TgpuFn<(seed: d.Vec3f) => d.Void>;
    seed4: TgpuFn<(seed: d.Vec4f) => d.Void>;
    sample: TgpuFn<() => d.F32>;
    sampleExclusive: TgpuFn<() => d.F32>;
    normal: TgpuFn<(mu: d.F32, sigma: d.F32) => d.F32>;
    ... 10 more ...;
    onUnitSphere: TgpuFn<...>;
    }
    randf
    .
    seed3: (seed: d.v3f) => void

    Threads do not share the generator's State. As a result, unless you change the seed in each thread, each thread will produce the same sequence. randf.randSeed3 sets the private seed of the thread. Sets the private seed of the thread.

    @param ― seed seed value to set. For the best results, all elements should be in [-1000, 1000] range.

    seed3
    (
    import d
    d
    .
    const vec3f: d.Vec3f
    (v0: AnyNumericVec2Instance, z: number) => d.v3f (+5 overloads)
    vec3f
    (
    let position: d.v2f
    position
    ,
    import d
    d
    .
    function f32(v?: number | boolean): number
    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
    (
    gid: d.v3u
    gid
    .
    v3u.x: number
    x
    )));
    if (
    const randf: {
    seed: TgpuFn<(seed: d.F32) => d.Void>;
    seed2: TgpuFn<(seed: d.Vec2f) => d.Void>;
    seed3: TgpuFn<(seed: d.Vec3f) => d.Void>;
    seed4: TgpuFn<(seed: d.Vec4f) => d.Void>;
    sample: TgpuFn<() => d.F32>;
    sampleExclusive: TgpuFn<() => d.F32>;
    normal: TgpuFn<(mu: d.F32, sigma: d.F32) => d.F32>;
    ... 10 more ...;
    onUnitSphere: TgpuFn<...>;
    }
    randf
    .
    sample: () => number

    Returns a random f32 value in [0, 1) range.

    sample
    () <
    const RESPAWN: number

    Chance per step of respawning a particle elsewhere. Prevents particles collecting in sinks.

    RESPAWN
    ||
    import std
    std
    .
    function any(value: AnyBooleanVecInstance): boolean
    export any

    Returns true if any component of value is true.

    @example any(vec2b(false, true)) // returns true any(vec3b(false, false, false)) // returns false

    any
    (
    import std
    std
    .
    gt<d.v2f>(lhs: d.v2f, rhs: d.v2f): d.v2b
    export gt

    Checks component-wise whether lhs > rhs. This function does not return bool, for that use-case, wrap the result in all.

    @example gt(vec2f(0.0, 0.0), vec2f(0.0, 1.0)) // returns vec2b(false, false) gt(vec3u(0, 1, 2), vec3u(2, 1, 0)) // returns vec3b(false, false, true) all(gt(vec4i(2, 3, 4, 5), vec4i(1, 2, 3, 4))) // returns true

    gt
    (
    import std
    std
    .
    abs<d.v2f>(value: d.v2f): d.v2f (+1 overload)
    export abs
    abs
    (
    let position: d.v2f
    position
    ),
    import d
    d
    .
    const vec2f: d.Vec2f
    (xy: number) => d.v2f (+3 overloads)
    vec2f
    (1)))) {
    let position: d.v2f
    position
    =
    const spawn: () => d.v2f
    spawn
    ();
    }
    const writable: TgpuMutable<d.WgslArray<d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>>>
    writable
    .
    TgpuMutable<WgslArray<WgslStruct<{ position: Vec2f; direction: Vec2f; }>>>.$: {
    position: d.v2f;
    direction: d.v2f;
    }[]
    $
    [
    gid: d.v3u
    gid
    .
    v3u.x: number
    x
    ] =
    const Particle: d.WgslStruct
    (props?: {
    position: d.v2f;
    direction: d.v2f;
    } | undefined) => {
    position: d.v2f;
    direction: d.v2f;
    }
    Particle
    ({
    position: d.v2f
    position
    ,
    direction: d.v2f
    direction
    });
    });
    const
    const stepPipeline: TgpuComputePipeline
    stepPipeline
    =
    const root: TgpuRoot
    root
    .
    WithBinding.createComputePipeline<{
    gid: d.BuiltinGlobalInvocationId;
    }>(descriptor: TgpuComputePipeline.Descriptor<{
    gid: d.BuiltinGlobalInvocationId;
    }>): TgpuComputePipeline
    createComputePipeline
    ({
    compute: TgpuComputeFn<{
    gid: d.BuiltinGlobalInvocationId;
    }>
    compute
    :
    const stepKernel: TgpuComputeFn<{
    gid: d.BuiltinGlobalInvocationId;
    }>
    stepKernel
    });
    • The sample position is stretched by the canvas’s aspect ratio, so the noise’s cells stay square on a wide canvas.
    • step is a constant from the options. The kernel captures it, so no frame uploads the step length.
    • A few particles respawn at random each step, and every particle that leaves the canvas does. Without that, particles would gather where the streams meet and leave the rest of the canvas empty.

    Return step beside seed, recording one fixed step into the caller’s compute pass, and compile its pipeline in initAsync:

    flow-field.ts
    async
    function initAsync(): Promise<void>
    initAsync
    () {
    await
    var Promise: PromiseConstructor

    Represents the completion of an asynchronous operation

    Promise
    .
    PromiseConstructor.all<[Promise<void>, Promise<void>, Promise<void>]>(values: [Promise<void>, Promise<void>, Promise<void>]): Promise<[void, void, void]> (+1 overload)

    Creates a Promise that is resolved with an array of results when all of the provided Promises resolve, or rejected when any Promise is rejected.

    @param ― values An array of Promises.

    @returns ― A new Promise.

    all
    ([
    const seedPipeline: {
    initAsync(): Promise<void>;
    }
    seedPipeline
    .
    function initAsync(): Promise<void>
    initAsync
    (),
    const stepPipeline: TgpuComputePipeline
    stepPipeline
    .
    TgpuComputePipeline.initAsync(): Promise<void>

    Immediately resolves the pipeline, then awaits device.createComputePipelineAsync(). NOTE: it is not necessary to initialize pipelines manually.

    initAsync
    (),
    const drawPipeline: {
    initAsync(): Promise<void>;
    }
    drawPipeline
    .
    function initAsync(): Promise<void>
    initAsync
    (),
    ]);
    },
    function step(pass: TgpuComputePass): void
    step
    (
    pass: TgpuComputePass
    pass
    :
    (alias) interface TgpuComputePass
    import TgpuComputePass

    A compute pass recording into a

    TgpuCommandEncoder

    .

    Dispatch either by binding TypeGPU pipelines to it (pipeline.with(pass).dispatchWorkgroups(...)), or proxy-style via pass.setPipeline(pipeline) followed by pass.dispatchWorkgroups(...).

    Call end() when done recording.

    TgpuComputePass
    ) {
    const own: Owner
    own
    .
    Owner.usable(): void

    Throws the owner's message if it is destroyed. Call first in methods that use GPU resources, including inject, recording methods, writes and getters that return GPU objects.

    usable
    ();
    pass: TgpuComputePass
    pass
    .
    TgpuComputePass.setPipeline(pipeline: TgpuComputePipeline): void

    Sets the current

    TgpuComputePipeline

    for subsequent dispatches

    setPipeline
    (
    const stepPipeline: TgpuComputePipeline
    stepPipeline
    );
    pass: TgpuComputePass
    pass
    .
    TgpuComputePass.dispatchWorkgroups(x: number, y?: number, z?: number): void
    dispatchWorkgroups
    (
    const workgroups: number
    workgroups
    );
    },

    Stepping once per frame would move the particles twice as fast at 120 Hz as at 60 Hz. A fixed step counts how many 1/60 s steps fit in the time since the last frame and carries the rest into the next:

    Stall a frame or change the display rate. Step length stays fixed; catch-up frames run at most maxSteps and drop time beyond the cap, shown at one fifth speed.

    In scene.ts, import clock from @typegpu/kit, make the fixed step, and step the field after the seed:

    scene.ts
    // In createScene
    const
    const sim: clock.FixedStep
    sim
    =
    import clock
    clock
    .
    function fixedStep(options: clock.FixedStepOptions): clock.FixedStep

    Accumulates elapsed time for fixed simulation steps. Accumulates nothing while paused. Throws unless step is finite and above 0 and maxSteps is an integer above 0.

    @returns ― a clock whose advance says how many steps to run each frame

    fixedStep
    ({
    FixedStepOptions.step: number

    Sets the step duration in seconds, such as 1 / 120.

    step
    : 1 / 60 });
    const
    const field: {
    settings: TgpuUniform<WgslStruct<{
    speed: F32;
    scale: F32;
    pointSize: F32;
    }>>;
    initAsync(): Promise<void>;
    seed(pass: TgpuComputePass): void;
    step(pass: TgpuComputePass): void;
    draw(pass: TgpuRenderCommands): void;
    destroy: () => void;
    [Symbol.dispose]: () => void;
    }
    field
    =
    const own: Owner
    <{
    settings: TgpuUniform<WgslStruct<{
    speed: F32;
    scale: F32;
    pointSize: F32;
    }>>;
    initAsync(): Promise<void>;
    seed(pass: TgpuComputePass): void;
    step(pass: TgpuComputePass): void;
    draw(pass: TgpuRenderCommands): void;
    destroy: () => void;
    [Symbol.dispose]: () => void;
    }>(item: {
    settings: TgpuUniform<WgslStruct<{
    speed: F32;
    scale: F32;
    pointSize: F32;
    }>>;
    initAsync(): Promise<void>;
    seed(pass: TgpuComputePass): void;
    step(pass: TgpuComputePass): void;
    draw(pass: TgpuRenderCommands): void;
    destroy: () => void;
    [Symbol.dispose]: () => void;
    }) => {
    settings: TgpuUniform<WgslStruct<{
    speed: F32;
    scale: F32;
    pointSize: F32;
    }>>;
    initAsync(): Promise<void>;
    seed(pass: TgpuComputePass): void;
    step(pass: TgpuComputePass): void;
    draw(pass: TgpuRenderCommands): void;
    destroy: () => void;
    [Symbol.dispose]: () => void;
    }

    Takes item to free at destroy and returns it. Passes undefined through. Frees a fixed resource, such as one from root.createUniform, through its buffer, and runs a cleanup function. After destroy, frees item at once.

    own
    (
    function createFlowField(root: TgpuRoot, options: FlowFieldOptions): {
    settings: TgpuUniform<WgslStruct<{
    speed: F32;
    scale: F32;
    pointSize: F32;
    }>>;
    initAsync(): Promise<void>;
    seed(pass: TgpuComputePass): void;
    step(pass: TgpuComputePass): void;
    draw(pass: TgpuRenderCommands): void;
    destroy: () => void;
    [Symbol.dispose]: () => void;
    }

    Advects particles through Perlin noise. Owns the buffers and records into caller passes.

    createFlowField
    (
    const root: TgpuRoot
    root
    , {
    FlowFieldOptions.count: number
    count
    : 65_536,
    FlowFieldOptions.step: number

    Seconds per step.

    step
    :
    const sim: clock.FixedStep
    sim
    .
    FixedStep.step: number

    Sets the step duration in seconds. A new value applies from the next advance: unstepped time carries over, capped at maxSteps new steps, and time keeps the seconds already stepped. Particles and machines take its value when created and keep it. Throws unless the value is finite and above 0.

    step
    ,
    FlowFieldOptions.format: GPUTextureFormat
    format
    :
    const surface: Surface
    surface
    .
    Surface.format: GPUTextureFormat
    format
    ,
    FlowFieldOptions.size: SizeSource

    What the particles are drawn into, such as the canvas surface.

    size
    :
    const surface: Surface
    surface
    ,
    }),
    );
    // In frame, after the seed
    for (let
    let steps: number
    steps
    =
    const sim: clock.FixedStep
    sim
    .
    FixedStep.advance(dt: number): number

    Adds dt seconds and returns the number of steps to run. Each step lasts step seconds, so a GPU kernel can use a fixed duration. All steps in a frame see the same queued CPU writes. Keep a step counter on the GPU for values that must change per step.

    advance
    (
    const frame: Frame
    frame
    .
    Frame.dt: number

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

    dt
    );
    let steps: number
    steps
    > 0;
    let steps: number
    steps
    --) {
    const field: {
    settings: TgpuUniform<WgslStruct<{
    speed: F32;
    scale: F32;
    pointSize: F32;
    }>>;
    initAsync(): Promise<void>;
    seed(pass: TgpuComputePass): void;
    step(pass: TgpuComputePass): void;
    draw(pass: TgpuRenderCommands): void;
    destroy: () => void;
    [Symbol.dispose]: () => void;
    }
    field
    .
    function step(pass: TgpuComputePass): void
    step
    (
    const compute: TgpuComputePass
    compute
    );
    }

    The particles stream across the canvas. At 30 frames a second, each frame runs two steps in its one compute pass; at 120, half the frames run none.

  5. Move on every frame.

    On a display faster than 60 Hz, the particles hold still on the frames that run no step, then jump a whole step, so they stutter. sim.ahead is the time since the last step, in seconds. Draw each particle that far along its direction, and it moves on every frame while the simulation still steps 60 times a second:

    Change the display rate. Ticks mark the particle after each 60 Hz step and rings mark where the last frames drew it, at one sixth speed. Drawn at its last step, the particle holds still for several frames, stacked here; drawn sim.ahead seconds along, it moves on every frame.

    ahead changes every frame, so it gets a uniform of its own, which the scene owns and writes. The module reads it through an accessor, a value its shader reads whose source the pipeline decides. Filled with the scene’s uniform, the shader reads the uniform; without one, it reads 0. Import the type TgpuUniform from typegpu:

    flow-field.ts
    // @filename: ../../packages/typegpu-kit-examples/src/flow-field/flow-field-tutorial-7.ts
    import {
    import pipelines
    pipelines
    } from '@typegpu/kit';
    import {
    import d
    d
    ,
    const tgpu: {
    const: {
    <TDataType extends d.AnyData>(dataType: TDataType, value: d.InferGPU<TDataType>): TgpuConst<TDataType>;
    <TElement extends d.AnyWgslData>(dataType: (elementCount: number) => d.WgslArray<TElement>, value: d.InferGPU<d.WgslArray<TElement>>): TgpuConst<d.WgslArray<TElement>>;
    };
    fn: {
    <Args extends d.BaseData[] | []>(argTypes: Args, returnType?: undefined): TgpuFnShell<Args, d.Void>;
    <Args extends d.BaseData[] | [], Return extends d.BaseData>(argTypes: Args, returnType: Return): TgpuFnShell<Args, Return>;
    <T extends AnyFn>(inner: T): TgpuGenericFn<T>;
    };
    ... 17 more ...;
    '~unstable': typeof import("../../node_modules/typegpu/tgpuUnstable");
    }

    @module ― typegpu

    tgpu
    , type
    (alias) interface TgpuRoot
    import TgpuRoot
    TgpuRoot
    , type
    interface TgpuUniform<out TData extends BaseData>
    TgpuUniform
    } from 'typegpu';
    declare const
    const root: TgpuRoot
    root
    :
    (alias) interface TgpuRoot
    import TgpuRoot
    TgpuRoot
    ,
    const format: GPUTextureFormat
    format
    :
    type GPUTextureFormat = "astc-10x10-unorm" | "astc-10x10-unorm-srgb" | "astc-10x5-unorm" | "astc-10x5-unorm-srgb" | "astc-10x6-unorm" | "astc-10x6-unorm-srgb" | "astc-10x8-unorm" | "astc-10x8-unorm-srgb" | "astc-12x10-unorm" | "astc-12x10-unorm-srgb" | "astc-12x12-unorm" | "astc-12x12-unorm-srgb" | "astc-4x4-unorm" | "astc-4x4-unorm-srgb" | "astc-5x4-unorm" | "astc-5x4-unorm-srgb" | "astc-5x5-unorm" | "astc-5x5-unorm-srgb" | "astc-6x5-unorm" | "astc-6x5-unorm-srgb" | "astc-6x6-unorm" | "astc-6x6-unorm-srgb" | "astc-8x5-unorm" | "astc-8x5-unorm-srgb" | ... 76 more ... | "stencil8"
    GPUTextureFormat
    ;
    const
    const Particle: d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>
    Particle
    =
    import d
    d
    .
    struct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>(props: {
    position: d.Vec2f;
    direction: d.Vec2f;
    }): d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>
    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
    ({
    position: d.Vec2f
    position
    :
    import d
    d
    .
    const vec2f: d.Vec2f
    vec2f
    ,
    direction: d.Vec2f
    direction
    :
    import d
    d
    .
    const vec2f: d.Vec2f
    vec2f
    });
    const
    const FlowSettings: d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>
    FlowSettings
    =
    import d
    d
    .
    struct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>(props: {
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }): d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>
    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
    ({
    speed: d.F32
    speed
    :
    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
    ,
    scale: d.F32
    scale
    :
    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
    ,
    pointSize: d.F32
    pointSize
    :
    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
    const readable: TgpuReadonly<d.WgslArray<d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>>>
    readable
    =
    const root: TgpuRoot
    root
    .
    TgpuRoot.createBuffer<d.WgslArray<d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>>>(typeSchema: d.WgslArray<d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>>, initial?: ((buffer: TgpuBuffer<NoInfer<d.WgslArray<d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>>>>) => void) | d.InferInput<NoInfer<d.WgslArray<d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>>>> | undefined): TgpuBuffer<d.WgslArray<d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>>> (+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<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>>(elementType: d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>, elementCount: number): d.WgslArray<d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>> (+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 Particle: d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>
    Particle
    , 4)).
    TgpuBuffer<WgslArray<WgslStruct<{ position: Vec2f; direction: Vec2f; }>>>.$usage<["storage"]>(usages_0: "storage"): TgpuBuffer<d.WgslArray<d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>>> & StorageFlag
    $usage
    ('storage').
    TgpuBuffer<WgslArray<WgslStruct<{ position: Vec2f; direction: Vec2f; }>>>.as<"readonly">(usage: "readonly"): TgpuReadonly<d.WgslArray<d.WgslStruct<{
    position: d.Vec2f;
    direction: d.Vec2f;
    }>>>
    as
    ('readonly');
    const
    const settings: TgpuUniform<d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>>
    settings
    =
    const root: TgpuRoot
    root
    .
    TgpuRoot.createUniform<d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>>(typeSchema: d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>, initial?: ((buffer: TgpuBuffer<NoInfer<d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>>>) => void) | d.InferInput<NoInfer<d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>>> | undefined): TgpuUniform<d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>> (+1 overload)

    Allocates memory on the GPU, allows passing data between host and shader. Read-only on the GPU, optimized for small data. 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)

    createUniform
    (
    const FlowSettings: d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>
    FlowSettings
    );
    // In FlowFieldOptions
    /**
    * Seconds since the last fixed step, written from `sim.ahead` each frame. Particles are drawn
    * that far along their direction, so they move at the display rate. Default 0.
    */
    readonly
    FlowFieldOptions.ahead?: TgpuUniform<d.F32> | undefined

    Seconds since the last fixed step, written from sim.ahead each frame. Particles are drawn that far along their direction, so they move at the display rate. Default 0.

    ahead
    ?:
    interface TgpuUniform<out TData extends BaseData>
    TgpuUniform
    <
    import d
    d
    .
    export F32

    32-bit float schema representing a single WGSL f32 value.

    F32
    >;
    // Inside createFlowField
    const
    const ahead: TgpuAccessor<d.F32>
    ahead
    =
    const tgpu: {
    const: {
    <TDataType extends d.AnyData>(dataType: TDataType, value: d.InferGPU<TDataType>): TgpuConst<TDataType>;
    <TElement extends d.AnyWgslData>(dataType: (elementCount: number) => d.WgslArray<TElement>, value: d.InferGPU<d.WgslArray<TElement>>): TgpuConst<d.WgslArray<TElement>>;
    };
    fn: {
    <Args extends d.BaseData[] | []>(argTypes: Args, returnType?: undefined): TgpuFnShell<Args, d.Void>;
    <Args extends d.BaseData[] | [], Return extends d.BaseData>(argTypes: Args, returnType: Return): TgpuFnShell<Args, Return>;
    <T extends AnyFn>(inner: T): TgpuGenericFn<T>;
    };
    ... 17 more ...;
    '~unstable': typeof import("../../node_modules/typegpu/tgpuUnstable");
    }

    @module ― typegpu

    tgpu
    .
    accessor: <d.F32>(schema: d.F32, defaultValue?: DataAccessorIn<NoInfer<d.F32>> | undefined) => TgpuAccessor<d.F32> (+1 overload)
    accessor
    (
    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
    );
    // In vertex, in place of the center at the particle's position
    const
    const center: d.v2f
    center
    =
    const particle: {
    position: d.v2f;
    direction: d.v2f;
    }
    particle
    .
    position: d.v2f
    position
    +
    const particle: {
    position: d.v2f;
    direction: d.v2f;
    }
    particle
    .
    direction: d.v2f
    direction
    * (
    const settings: TgpuUniform<d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>>
    settings
    .
    TgpuUniform<WgslStruct<{ speed: F32; scale: F32; pointSize: F32; }>>.$: {
    speed: number;
    scale: number;
    pointSize: number;
    }
    $
    .
    speed: number
    speed
    *
    const ahead: TgpuAccessor<d.F32>
    ahead
    .
    TgpuAccessor<F32>.$: number
    $
    );
    // In place of root.createRenderPipeline
    const
    const drawPipeline: TgpuRenderPipeline<d.Vec4f>
    drawPipeline
    =
    const root: TgpuRoot
    root
    .
    Withable<WithBinding>.with<d.F32>(accessor: TgpuAccessor<d.F32>, value: DataAccessorIn<NoInfer<d.F32>>): WithBinding (+2 overloads)
    with
    (
    const ahead: TgpuAccessor<d.F32>
    ahead
    ,
    const options: FlowFieldOptions
    options
    .
    FlowFieldOptions.ahead?: TgpuUniform<d.F32> | undefined

    Seconds since the last fixed step, written from sim.ahead each frame. Particles are drawn that far along their direction, so they move at the display rate. Default 0.

    ahead
    ?? 0).
    WithBinding.createRenderPipeline<{}, {}, {}, d.Vec4f>(descriptor: TgpuRenderPipeline<in Targets = never>.DescriptorBase & {
    attribs?: {} | undefined;
    vertex: TgpuVertexFn<{}, {}> | ((input: AutoVertexIn<InferGPURecord<AttribRecordToDefaultDataTypes<{}>>>) => AutoVertexOut<{}>);
    fragment: TgpuFragmentFn<{} & Record<string, AnyFragmentInputBuiltin>, d.Vec4f> | ((input: AutoFragmentIn<InferGPURecord<{}>>) => d.v4f | (AnyAutoCustoms & Partial<...>));
    targets?: TgpuColorTargetState;
    }): TgpuRenderPipeline<...> (+2 overloads)
    createRenderPipeline
    ({
    // ...the same description as before
    });

    In scene.ts, import d from typegpu, make the uniform, hand it to the field and write it after the steps:

    scene.ts
    // In createScene
    const
    const ahead: TgpuUniform<d.F32>
    ahead
    =
    const root: TgpuRoot
    root
    .
    TgpuRoot.createUniform<d.F32>(typeSchema: d.F32, initial?: number | ((buffer: TgpuBuffer<NoInfer<d.F32>>) => void) | undefined): TgpuUniform<d.F32> (+1 overload)

    Allocates memory on the GPU, allows passing data between host and shader. Read-only on the GPU, optimized for small data. 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)

    createUniform
    (
    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
    const field: {
    settings: TgpuUniform<d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>>;
    initAsync(): Promise<void>;
    seed(pass: TgpuComputePass): void;
    step(pass: TgpuComputePass): void;
    draw(pass: TgpuRenderCommands): void;
    destroy: () => void;
    [Symbol.dispose]: () => void;
    }
    field
    =
    const own: Owner
    <{
    settings: TgpuUniform<d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>>;
    initAsync(): Promise<void>;
    seed(pass: TgpuComputePass): void;
    step(pass: TgpuComputePass): void;
    draw(pass: TgpuRenderCommands): void;
    destroy: () => void;
    [Symbol.dispose]: () => void;
    }>(item: {
    settings: TgpuUniform<d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>>;
    initAsync(): Promise<void>;
    seed(pass: TgpuComputePass): void;
    step(pass: TgpuComputePass): void;
    draw(pass: TgpuRenderCommands): void;
    destroy: () => void;
    [Symbol.dispose]: () => void;
    }) => {
    settings: TgpuUniform<d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>>;
    initAsync(): Promise<void>;
    seed(pass: TgpuComputePass): void;
    step(pass: TgpuComputePass): void;
    draw(pass: TgpuRenderCommands): void;
    destroy: () => void;
    [Symbol.dispose]: () => void;
    }

    Takes item to free at destroy and returns it. Passes undefined through. Frees a fixed resource, such as one from root.createUniform, through its buffer, and runs a cleanup function. After destroy, frees item at once.

    own
    (
    function createFlowField(root: TgpuRoot, options: FlowFieldOptions): {
    settings: TgpuUniform<d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>>;
    initAsync(): Promise<void>;
    seed(pass: TgpuComputePass): void;
    step(pass: TgpuComputePass): void;
    draw(pass: TgpuRenderCommands): void;
    destroy: () => void;
    [Symbol.dispose]: () => void;
    }

    Advects particles through Perlin noise. Owns the buffers and records into caller passes.

    createFlowField
    (
    const root: TgpuRoot
    root
    , {
    FlowFieldOptions.count: number
    count
    : 65_536,
    FlowFieldOptions.step: number

    Seconds per step.

    step
    :
    const sim: clock.FixedStep
    sim
    .
    FixedStep.step: number

    Sets the step duration in seconds. A new value applies from the next advance: unstepped time carries over, capped at maxSteps new steps, and time keeps the seconds already stepped. Particles and machines take its value when created and keep it. Throws unless the value is finite and above 0.

    step
    ,
    FlowFieldOptions.ahead?: TgpuUniform<d.F32> | undefined

    Seconds since the last fixed step, written from sim.ahead each frame. Particles are drawn that far along their direction, so they move at the display rate. Default 0.

    ahead
    ,
    FlowFieldOptions.format: GPUTextureFormat
    format
    :
    const surface: Surface
    surface
    .
    Surface.format: GPUTextureFormat
    format
    ,
    FlowFieldOptions.size: SizeSource

    What the particles are drawn into, such as the canvas surface.

    size
    :
    const surface: Surface
    surface
    ,
    }),
    );
    // In frame, after the steps
    const ahead: TgpuUniform<d.F32>
    ahead
    .
    TgpuBufferBindingBase<F32>.write(data: number, options?: BufferWriteOptions): void
    write
    (
    const sim: clock.FixedStep
    sim
    .
    FixedStep.ahead: number

    Reports the unstepped time in seconds, capped at one step. Equals alpha * step. Use it to draw objects that far along their velocity between simulation steps.

    ahead
    );

    On a 120 Hz or faster display, the particles now move smoothly on every frame.

  6. Change the settings.

    Writing a uniform uploads all of it, so group values by how often they change. The field has four:

    Uniform Holds Written
    settings speed, scale, point size by the scene, when a setting changes
    viewport the canvas size by draw, when the canvas size changes
    ahead the time since a step by the scene, once a frame
    seedValue the random seed by seed

    A steady frame uploads 4 bytes, the ahead value. Data on the GPU has the rule for every module. Keep the settings in a plain object, hand it to the field as its starting values, and return them with a write that uploads them, and a reseed. Import defaults from ./flow-field.ts:

    scene.ts
    // In createScene
    const
    const settings: {
    speed: number;
    scale: number;
    pointSize: number;
    }
    settings
    = { ...
    const defaults: {
    speed: number;
    scale: number;
    pointSize: number;
    }
    defaults
    };
    const
    const field: {
    settings: TgpuUniform<d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>>;
    initAsync(): Promise<void>;
    seed(pass: TgpuComputePass): void;
    step(pass: TgpuComputePass): void;
    draw(pass: TgpuRenderCommands): void;
    destroy: () => void;
    [Symbol.dispose]: () => void;
    }
    field
    =
    const own: Owner
    <{
    settings: TgpuUniform<d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>>;
    initAsync(): Promise<void>;
    seed(pass: TgpuComputePass): void;
    step(pass: TgpuComputePass): void;
    draw(pass: TgpuRenderCommands): void;
    destroy: () => void;
    [Symbol.dispose]: () => void;
    }>(item: {
    settings: TgpuUniform<d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>>;
    initAsync(): Promise<void>;
    seed(pass: TgpuComputePass): void;
    step(pass: TgpuComputePass): void;
    draw(pass: TgpuRenderCommands): void;
    destroy: () => void;
    [Symbol.dispose]: () => void;
    }) => {
    settings: TgpuUniform<d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>>;
    initAsync(): Promise<void>;
    seed(pass: TgpuComputePass): void;
    step(pass: TgpuComputePass): void;
    draw(pass: TgpuRenderCommands): void;
    destroy: () => void;
    [Symbol.dispose]: () => void;
    }

    Takes item to free at destroy and returns it. Passes undefined through. Frees a fixed resource, such as one from root.createUniform, through its buffer, and runs a cleanup function. After destroy, frees item at once.

    own
    (
    function createFlowField(root: TgpuRoot, options: FlowFieldOptions): {
    settings: TgpuUniform<d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>>;
    initAsync(): Promise<void>;
    seed(pass: TgpuComputePass): void;
    step(pass: TgpuComputePass): void;
    draw(pass: TgpuRenderCommands): void;
    destroy: () => void;
    [Symbol.dispose]: () => void;
    }

    Advects particles through Perlin noise. Owns the buffers and records into caller passes.

    createFlowField
    (
    const root: TgpuRoot
    root
    , {
    FlowFieldOptions.count: number
    count
    : 65_536,
    FlowFieldOptions.step: number

    Seconds per step.

    step
    :
    const sim: clock.FixedStep
    sim
    .
    FixedStep.step: number

    Sets the step duration in seconds. A new value applies from the next advance: unstepped time carries over, capped at maxSteps new steps, and time keeps the seconds already stepped. Particles and machines take its value when created and keep it. Throws unless the value is finite and above 0.

    step
    ,
    FlowFieldOptions.ahead?: TgpuUniform<d.F32> | undefined

    Seconds since the last fixed step, written from sim.ahead each frame. Particles are drawn that far along their direction, so they move at the display rate. Default 0.

    ahead
    ,
    FlowFieldOptions.format: GPUTextureFormat
    format
    :
    const surface: Surface
    surface
    .
    Surface.format: GPUTextureFormat
    format
    ,
    FlowFieldOptions.size: SizeSource

    What the particles are drawn into, such as the canvas surface.

    size
    :
    const surface: Surface
    surface
    ,
    FlowFieldOptions.settings?: Partial<d.InferInput<d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>>> | undefined

    Initial values for speed, scale and point size. Override defaults.

    settings
    ,
    }),
    );
    // In the returned object
    sim: clock.FixedStep
    sim
    ,
    settings: {
    speed: number;
    scale: number;
    pointSize: number;
    }
    settings
    ,
    write: () => void
    write
    : () =>
    const field: {
    settings: TgpuUniform<d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>>;
    initAsync(): Promise<void>;
    seed(pass: TgpuComputePass): void;
    step(pass: TgpuComputePass): void;
    draw(pass: TgpuRenderCommands): void;
    destroy: () => void;
    [Symbol.dispose]: () => void;
    }
    field
    .
    settings: TgpuUniform<d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>>
    settings
    .
    TgpuBufferBindingBase<WgslStruct<{ speed: F32; scale: F32; pointSize: F32; }>>.write(data: d.InferInput<d.WgslStruct<{
    speed: d.F32;
    scale: d.F32;
    pointSize: d.F32;
    }>>, options?: BufferWriteOptions): void
    write
    (
    const settings: {
    speed: number;
    scale: number;
    pointSize: number;
    }
    settings
    ),
    reseed: () => void
    reseed
    : () => {
    let seeding: boolean
    seeding
    = true;
    },

    The page decides how to show the settings. Add lil-gui to the project:

    npm i lil-gui

    In index.ts, build the panel in attach, which web.host calls once the scene is made. The host disposes what attach returns when it tears the scene down:

    index.ts
    import * as
    import web
    web
    from '@typegpu/kit/web';
    import
    class GUI
    GUI
    from 'lil-gui';
    import {
    function createScene(root: TgpuRoot, surface: Surface): Promise<{
    sim: FixedStep;
    settings: {
    speed: number;
    scale: number;
    pointSize: number;
    };
    write(): void;
    reseed(): void;
    frame(frame: Frame): void;
    destroy(): void;
    }>
    createScene
    } from './scene.ts';
    import web
    web
    .
    function host<{
    sim: FixedStep;
    settings: {
    speed: number;
    scale: number;
    pointSize: number;
    };
    write(): void;
    reseed(): void;
    frame(frame: Frame): void;
    destroy(): void;
    }>(createScene: web.CreateScene<Record<never, never>, {
    sim: FixedStep;
    settings: {
    speed: number;
    scale: number;
    pointSize: number;
    };
    write(): void;
    reseed(): void;
    frame(frame: Frame): void;
    destroy(): void;
    }>, hosting: web.HostOptions<{
    sim: FixedStep;
    settings: {
    speed: number;
    scale: number;
    pointSize: number;
    };
    write(): void;
    reseed(): void;
    frame(frame: Frame): void;
    destroy(): void;
    }>): web.Hosted<...> (+1 overload)

    Runs a scene on a canvas with a root, surface, frame loop and animation. destroy tears them down in order. Use send to control the scene or onFrame to read it. If scene options are optional, call host(createScene, { canvas }). Throws if both device and root are supplied.

    @param ― createScene the scene file's createScene

    @param ― options the scene's third argument

    @param ― hosting the canvas, the device or root, and what the page runs beside the scene

    @returns ― a hosted scene you send closures to and destroy

    host
    (
    function createScene(root: TgpuRoot, surface: Surface): Promise<{
    sim: FixedStep;
    settings: {
    speed: number;
    scale: number;
    pointSize: number;
    };
    write(): void;
    reseed(): void;
    frame(frame: Frame): void;
    destroy(): void;
    }>
    createScene
    , {
    HostOptions<S extends Scene>.canvas: HTMLCanvasElement

    Supplies the canvas for the scene.

    canvas
    :
    var document: Document

    window.document returns a reference to the document contained in the window.

    MDN Reference

    document
    .
    ParentNode.querySelector<"canvas">(selectors: "canvas"): HTMLCanvasElement | null (+4 overloads)

    Returns the first element that is a descendant of node that matches selectors.

    MDN Reference

    querySelector
    ('canvas')!,
    HostOptions<{ sim: FixedStep; settings: { speed: number; scale: number; pointSize: number; }; write(): void; reseed(): void; frame(frame: Frame): void; destroy(): void; }, Surface>.attach?: ((stage: Stage<{
    sim: FixedStep;
    settings: {
    speed: number;
    scale: number;
    pointSize: number;
    };
    write(): void;
    reseed(): void;
    frame(frame: Frame): void;
    destroy(): void;
    }, web.Surface>) => Disposable | void) | undefined

    Connects page code, such as orbit controls, to the stage after scene creation. The host disposes its return value with the scene. Default none.

    attach
    ({
    scene: {
    sim: FixedStep;
    settings: {
    speed: number;
    scale: number;
    pointSize: number;
    };
    write(): void;
    reseed(): void;
    frame(frame: Frame): void;
    destroy(): void;
    }
    scene
    }) {
    const
    const gui: GUI
    gui
    = new
    new GUI({ parent, autoPlace, container, width, title, closeFolders, injectStyles, touchStyles }?: {
    autoPlace?: boolean;
    container?: Node;
    width?: number;
    title?: string;
    closeFolders?: boolean;
    injectStyles?: boolean;
    touchStyles?: number;
    parent?: GUI;
    }): GUI

    Creates a panel that holds controllers.

    @example new GUI(); new GUI( { container: document.getElementById( 'custom' ) } );

    @param ― options

    @param ― options.autoPlace Adds the GUI to document.body and fixes it to the top right of the page.

    @param ― options.container Adds the GUI to this DOM element. Overrides autoPlace.

    @param ― options.width Width of the GUI in pixels, usually set when name labels become too long. Note that you can make name labels wider in CSS with .lil‑gui { ‑‑name‑width: 55% }.

    @param ― options.title Name to display in the title bar.

    @param ― options.closeFolders Pass true to close all folders in this GUI by default.

    @param ― options.injectStyles Injects the default stylesheet into the page if this is the first GUI. Pass false to use your own stylesheet.

    @param ― options.touchStyles Makes controllers larger on touch devices. Pass false to disable touch styles.

    @param ― options.parent Adds this GUI as a child in another GUI. Usually this is done for you by addFolder().

    GUI
    ({
    title?: string | undefined
    title
    : 'flow field' });
    const gui: GUI
    gui
    .
    GUI.add<{
    speed: number;
    scale: number;
    pointSize: number;
    }>(object: {
    speed: number;
    scale: number;
    pointSize: number;
    }, property: "speed" | "scale" | "pointSize", $1?: number | object | any[], max?: number, step?: number): Controller

    Adds a controller to the GUI, inferring controller type using the typeof operator.

    @example gui.add( object, 'property' ); gui.add( object, 'number', 0, 100, 1 ); gui.add( object, 'options', [ 1, 2, 3 ] );

    @param ― object The object the controller will modify.

    @param ― property Name of the property to control.

    @param ― $1 Minimum value for number controllers, or the set of selectable values for a dropdown.

    @param ― max Maximum value for number controllers.

    @param ― step Step value for number controllers.

    @returns

    add
    (
    scene: {
    sim: FixedStep;
    settings: {
    speed: number;
    scale: number;
    pointSize: number;
    };
    write(): void;
    reseed(): void;
    frame(frame: Frame): void;
    destroy(): void;
    }
    scene
    .
    settings: {
    speed: number;
    scale: number;
    pointSize: number;
    }
    settings
    , 'speed', 0, 1, 0.01);
    const gui: GUI
    gui
    .
    GUI.add<{
    speed: number;
    scale: number;
    pointSize: number;
    }>(object: {
    speed: number;
    scale: number;
    pointSize: number;
    }, property: "speed" | "scale" | "pointSize", $1?: number | object | any[], max?: number, step?: number): Controller

    Adds a controller to the GUI, inferring controller type using the typeof operator.

    @example gui.add( object, 'property' ); gui.add( object, 'number', 0, 100, 1 ); gui.add( object, 'options', [ 1, 2, 3 ] );

    @param ― object The object the controller will modify.

    @param ― property Name of the property to control.

    @param ― $1 Minimum value for number controllers, or the set of selectable values for a dropdown.

    @param ― max Maximum value for number controllers.

    @param ― step Step value for number controllers.

    @returns

    add
    (
    scene: {
    sim: FixedStep;
    settings: {
    speed: number;
    scale: number;
    pointSize: number;
    };
    write(): void;
    reseed(): void;
    frame(frame: Frame): void;
    destroy(): void;
    }
    scene
    .
    settings: {
    speed: number;
    scale: number;
    pointSize: number;
    }
    settings
    , 'scale', 0.1, 6, 0.1);
    const gui: GUI
    gui
    .
    GUI.add<{
    speed: number;
    scale: number;
    pointSize: number;
    }>(object: {
    speed: number;
    scale: number;
    pointSize: number;
    }, property: "speed" | "scale" | "pointSize", $1?: number | object | any[], max?: number, step?: number): Controller

    Adds a controller to the GUI, inferring controller type using the typeof operator.

    @example gui.add( object, 'property' ); gui.add( object, 'number', 0, 100, 1 ); gui.add( object, 'options', [ 1, 2, 3 ] );

    @param ― object The object the controller will modify.

    @param ― property Name of the property to control.

    @param ― $1 Minimum value for number controllers, or the set of selectable values for a dropdown.

    @param ― max Maximum value for number controllers.

    @param ― step Step value for number controllers.

    @returns

    add
    (
    scene: {
    sim: FixedStep;
    settings: {
    speed: number;
    scale: number;
    pointSize: number;
    };
    write(): void;
    reseed(): void;
    frame(frame: Frame): void;
    destroy(): void;
    }
    scene
    .
    settings: {
    speed: number;
    scale: number;
    pointSize: number;
    }
    settings
    , 'pointSize', 0.5, 8, 0.5);
    const gui: GUI
    gui
    .
    GUI.onChange(callback: (arg0: {
    object: object;
    property: string;
    value: any;
    controller: Controller;
    }) => any): GUI

    Pass a function to be called whenever a controller in this GUI changes.

    @param ― callback

    @returns

    @example gui.onChange( event => { event.object // object that was modified event.property // string, name of property event.value // new value of controller event.controller // controller that was modified } );

    onChange
    (
    scene: {
    sim: FixedStep;
    settings: {
    speed: number;
    scale: number;
    pointSize: number;
    };
    write(): void;
    reseed(): void;
    frame(frame: Frame): void;
    destroy(): void;
    }
    scene
    .
    function write(): void
    write
    );
    const gui: GUI
    gui
    .
    GUI.add<FixedStep>(object: FixedStep, property: keyof FixedStep, $1?: number | object | any[], max?: number, step?: number): Controller

    Adds a controller to the GUI, inferring controller type using the typeof operator.

    @example gui.add( object, 'property' ); gui.add( object, 'number', 0, 100, 1 ); gui.add( object, 'options', [ 1, 2, 3 ] );

    @param ― object The object the controller will modify.

    @param ― property Name of the property to control.

    @param ― $1 Minimum value for number controllers, or the set of selectable values for a dropdown.

    @param ― max Maximum value for number controllers.

    @param ― step Step value for number controllers.

    @returns

    add
    (
    scene: {
    sim: FixedStep;
    settings: {
    speed: number;
    scale: number;
    pointSize: number;
    };
    write(): void;
    reseed(): void;
    frame(frame: Frame): void;
    destroy(): void;
    }
    scene
    .
    sim: FixedStep
    sim
    , 'paused');
    const gui: GUI
    gui
    .
    GUI.add<{
    sim: FixedStep;
    settings: {
    speed: number;
    scale: number;
    pointSize: number;
    };
    write(): void;
    reseed(): void;
    frame(frame: Frame): void;
    destroy(): void;
    }>(object: {
    sim: FixedStep;
    settings: {
    speed: number;
    scale: number;
    pointSize: number;
    };
    write(): void;
    reseed(): void;
    frame(frame: Frame): void;
    destroy(): void;
    }, property: "reseed" | "sim" | "settings" | "write" | "frame" | "destroy", $1?: number | object | any[], max?: number, step?: number): Controller

    Adds a controller to the GUI, inferring controller type using the typeof operator.

    @example gui.add( object, 'property' ); gui.add( object, 'number', 0, 100, 1 ); gui.add( object, 'options', [ 1, 2, 3 ] );

    @param ― object The object the controller will modify.

    @param ― property Name of the property to control.

    @param ― $1 Minimum value for number controllers, or the set of selectable values for a dropdown.

    @param ― max Maximum value for number controllers.

    @param ― step Step value for number controllers.

    @returns

    add
    (
    scene: {
    sim: FixedStep;
    settings: {
    speed: number;
    scale: number;
    pointSize: number;
    };
    write(): void;
    reseed(): void;
    frame(frame: Frame): void;
    destroy(): void;
    }
    scene
    , 'reseed');
    return { [
    var Symbol: SymbolConstructor
    Symbol
    .
    SymbolConstructor.dispose: typeof Symbol.dispose

    A method that is used to release resources held by an object. Called by the semantics of the using statement.

    dispose
    ]: () =>
    const gui: GUI
    gui
    .
    GUI.destroy(): void

    Destroys all DOM elements and event listeners associated with this GUI.

    destroy
    () };
    },
    });

    The panel changes the field as you drag. Pausing stops the steps and holds the particles still. The figure has the same controls, and its settings writes readout counts uploads of settings. It rises only when you change a setting:

    flow-field65 536 particles, each steered by the Perlin noise under it.
    Change the speed or point size to add one uniform write. Reseeding and waiting leave the count unchanged.
  7. Test the module.

    A test of the module checks what it records, without a GPU. recordingRoot() from @typegpu/kit/testing is a root on a stand-in device that logs commands and writes, and offscreenSurface stands in for the canvas. Add Vitest, which reads vite.config.ts, so the TypeGPU plugin transforms your module in tests too:

    npm i -D vitest

    Create flow-field.test.ts beside the module. Make the field on a recording root and record frames as a scene would:

    flow-field.test.ts
    import {
    import frames
    frames
    } from '@typegpu/kit';
    import {
    function offscreenSurface(root: TgpuRoot, options: OffscreenSurfaceOptions): OffscreenSurface

    Creates a surface backed by a texture for running scene frames without a canvas. Accepts a recording root or a real device. view returns one view per texture. destroy destroys the texture, and later calls to view throw.

    @param ― root any root, recording or real

    @returns ― a surface you draw into and destroy

    offscreenSurface
    ,
    function recordingRoot(options?: RecordingOptions): RecordingRoot

    Creates a recording WebGPU test double under a TypeGPU root. TypeGPU resolves shaders to WGSL. The double keeps the WGSL and records commands without compiling or executing shaders. Use it to check resources, pipelines and recording order without a GPU. Queue writes take effect, and submitted buffer copies and clears update bytes, so tests can check uploads and readbacks.

    Throws for invalid mapping usage, out-of-bounds or unaligned copies and writes, missing resource usage flags, writes to destroyed buffers, and mapping in the wrong state. Also throws for non-attachment render targets, bind groups with incompatible resource usages, finishing an encoder with an open pass, and submitting destroyed resources. Throws at the call, where a device would report most of these errors later. Reports WebGPU's default limits and records render bundles, which replay into the pass that executes them. A device member it lacks throws an error that names it.

    @returns ― the root, a log of what it saw, and the bytes it holds

    recordingRoot
    } from '@typegpu/kit/testing';
    import {
    const expect: ExpectStatic
    expect
    ,
    const it: TestAPI

    Defines a test case with a given name and test function. The test function can optionally be configured with test options.

    @param ― name - The name of the test or a function that will be used as a test name.

    @param ― optionsOrFn - Optional. The test options or the test function if no explicit name is provided.

    @param ― optionsOrTest - Optional. The test function or options, depending on the previous parameters.

    @throws ― {Error} If called inside another test function.

    @example

    // Define a simple test
    it('adds two numbers', () => {
    expect(add(1, 2)).toBe(3);
    });

    @example

    // Define a test with options
    it('subtracts two numbers', { retry: 3 }, () => {
    expect(subtract(5, 2)).toBe(3);
    });

    it
    } from 'vitest';
    import {
    function createFlowField(root: TgpuRoot, options: FlowFieldOptions): {
    settings: TgpuUniform<WgslStruct<{
    speed: F32;
    scale: F32;
    pointSize: F32;
    }>>;
    initAsync(): Promise<void>;
    seed(pass: TgpuComputePass): void;
    step(pass: TgpuComputePass): void;
    draw(pass: TgpuRenderCommands): void;
    destroy: () => void;
    [Symbol.dispose]: () => void;
    }

    Advects particles through Perlin noise. Owns the buffers and records into caller passes.

    createFlowField
    } from './flow-field.ts';
    it<object>(name: string | Function, fn?: TestFunction<object> | undefined, options?: number): void (+1 overload)

    Defines a test case with a given name and test function. The test function can optionally be configured with test options.

    @param ― name - The name of the test or a function that will be used as a test name.

    @param ― optionsOrFn - Optional. The test options or the test function if no explicit name is provided.

    @param ― optionsOrTest - Optional. The test function or options, depending on the previous parameters.

    @throws ― {Error} If called inside another test function.

    @example

    // Define a simple test
    it('adds two numbers', () => {
    expect(add(1, 2)).toBe(3);
    });

    @example

    // Define a test with options
    it('subtracts two numbers', { retry: 3 }, () => {
    expect(subtract(5, 2)).toBe(3);
    });

    it
    ('records into the passes it is given, and a steady frame writes and creates nothing', async () => {
    const {
    const root: TgpuRoot
    root
    ,
    const log: RecordingLog
    log
    } =
    function recordingRoot(options?: RecordingOptions): RecordingRoot

    Creates a recording WebGPU test double under a TypeGPU root. TypeGPU resolves shaders to WGSL. The double keeps the WGSL and records commands without compiling or executing shaders. Use it to check resources, pipelines and recording order without a GPU. Queue writes take effect, and submitted buffer copies and clears update bytes, so tests can check uploads and readbacks.

    Throws for invalid mapping usage, out-of-bounds or unaligned copies and writes, missing resource usage flags, writes to destroyed buffers, and mapping in the wrong state. Also throws for non-attachment render targets, bind groups with incompatible resource usages, finishing an encoder with an open pass, and submitting destroyed resources. Throws at the call, where a device would report most of these errors later. Reports WebGPU's default limits and records render bundles, which replay into the pass that executes them. A device member it lacks throws an error that names it.

    @returns ― the root, a log of what it saw, and the bytes it holds

    recordingRoot
    ();
    const
    const surface: OffscreenSurface
    surface
    =
    function offscreenSurface(root: TgpuRoot, options: OffscreenSurfaceOptions): OffscreenSurface

    Creates a surface backed by a texture for running scene frames without a canvas. Accepts a recording root or a real device. view returns one view per texture. destroy destroys the texture, and later calls to view throw.

    @param ― root any root, recording or real

    @returns ― a surface you draw into and destroy

    offscreenSurface
    (
    const root: TgpuRoot
    root
    , {
    OffscreenSurfaceOptions.width: number

    Sets the texture width in pixels.

    width
    : 64,
    OffscreenSurfaceOptions.height: number

    Sets the texture height in pixels.

    height
    : 36,
    OffscreenSurfaceOptions.format: GPUTextureFormat

    Sets the texture format, as a canvas preferred format would.

    format
    : 'bgra8unorm' });
    const
    const field: {
    settings: TgpuUniform<WgslStruct<{
    speed: F32;
    scale: F32;
    pointSize: F32;
    }>>;
    initAsync(): Promise<void>;
    seed(pass: TgpuComputePass): void;
    step(pass: TgpuComputePass): void;
    draw(pass: TgpuRenderCommands): void;
    destroy: () => void;
    [Symbol.dispose]: () => void;
    }
    field
    =
    function createFlowField(root: TgpuRoot, options: FlowFieldOptions): {
    settings: TgpuUniform<WgslStruct<{
    speed: F32;
    scale: F32;
    pointSize: F32;
    }>>;
    initAsync(): Promise<void>;
    seed(pass: TgpuComputePass): void;
    step(pass: TgpuComputePass): void;
    draw(pass: TgpuRenderCommands): void;
    destroy: () => void;
    [Symbol.dispose]: () => void;
    }

    Advects particles through Perlin noise. Owns the buffers and records into caller passes.

    createFlowField
    (
    const root: TgpuRoot
    root
    , {
    FlowFieldOptions.count: number
    count
    : 1000,
    FlowFieldOptions.step: number

    Seconds per step.

    step
    : 1 / 60,
    FlowFieldOptions.format: GPUTextureFormat
    format
    :
    const surface: OffscreenSurface
    surface
    .
    Surface.format: GPUTextureFormat
    format
    ,
    FlowFieldOptions.size: SizeSource

    What the particles are drawn into, such as the canvas surface.

    size
    :
    const surface: OffscreenSurface
    surface
    ,
    });
    await
    const field: {
    settings: TgpuUniform<WgslStruct<{
    speed: F32;
    scale: F32;
    pointSize: F32;
    }>>;
    initAsync(): Promise<void>;
    seed(pass: TgpuComputePass): void;
    step(pass: TgpuComputePass): void;
    draw(pass: TgpuRenderCommands): void;
    destroy: () => void;
    [Symbol.dispose]: () => void;
    }
    field
    .
    function initAsync(): Promise<void>
    initAsync
    ();
    expect<number>(actual: number, message?: string): Assertion<void, number> (+1 overload)
    expect
    (
    const log: RecordingLog
    log
    .
    RecordingLog.pipelines: number
    pipelines
    ).
    JestAssertion<void, number>.toBe: <number>(expected: number) => void

    Checks that a value is what you expect. It calls Object.is to compare values. Don't use toBe with floating-point numbers.

    @example expect(result).toBe(42); expect(status).toBe(true);

    toBe
    (3);
    let
    let seeding: boolean
    seeding
    = true;
    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
    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 seeding: boolean
    seeding
    )
    const field: {
    settings: TgpuUniform<WgslStruct<{
    speed: F32;
    scale: F32;
    pointSize: F32;
    }>>;
    initAsync(): Promise<void>;
    seed(pass: TgpuComputePass): void;
    step(pass: TgpuComputePass): void;
    draw(pass: TgpuRenderCommands): void;
    destroy: () => void;
    [Symbol.dispose]: () => void;
    }
    field
    .
    function seed(pass: TgpuComputePass): void
    seed
    (
    const compute: TgpuComputePass
    compute
    );
    let seeding: boolean
    seeding
    = false;
    const field: {
    settings: TgpuUniform<WgslStruct<{
    speed: F32;
    scale: F32;
    pointSize: F32;
    }>>;
    initAsync(): Promise<void>;
    seed(pass: TgpuComputePass): void;
    step(pass: TgpuComputePass): void;
    draw(pass: TgpuRenderCommands): void;
    destroy: () => void;
    [Symbol.dispose]: () => void;
    }
    field
    .
    function step(pass: TgpuComputePass): void
    step
    (
    const compute: TgpuComputePass
    compute
    );
    const compute: TgpuComputePass
    compute
    .
    TgpuComputePass.end(): void

    Completes the recording of this compute pass

    end
    ();
    const
    const pass: TgpuRenderPass
    pass
    =
    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.beginRenderPass(descriptor: TgpuRenderPassDescriptor): TgpuRenderPass

    Begins recording a render pass. Attachment views accept TypeGPU textures, texture views and canvas contexts, next to raw

    GPUTextureView

    s.

    beginRenderPass
    ({
    TgpuRenderPassDescriptor.colorAttachments?: ColorAttachment | readonly (ColorAttachment | null)[] | undefined
    colorAttachments
    : {
    ColorAttachment.view: GPUTextureView | (ColorTextureConstraint & RenderFlag) | TgpuTextureView<WgslTexture<WgslTextureProps>> | TgpuTextureRenderView | GPUCanvasContext

    A

    GPUTextureView

    describing the texture subresource that will be output to for this color attachment.

    view
    :
    const surface: OffscreenSurface
    surface
    .
    Surface.view(frame: frames.Frame): GPUTextureView

    Returns a view of the current texture, created once per texture. React Native requires the frame to present after submission.

    view
    (
    frame: frames.Frame
    frame
    ) },
    });
    const field: {
    settings: TgpuUniform<WgslStruct<{
    speed: F32;
    scale: F32;
    pointSize: F32;
    }>>;
    initAsync(): Promise<void>;
    seed(pass: TgpuComputePass): void;
    step(pass: TgpuComputePass): void;
    draw(pass: TgpuRenderCommands): void;
    destroy: () => void;
    [Symbol.dispose]: () => void;
    }
    field
    .
    function draw(pass: TgpuRenderCommands): void
    draw
    (
    const pass: TgpuRenderPass
    pass
    );
    const pass: TgpuRenderPass
    pass
    .
    TgpuRenderPass.end(): void

    Completes the recording of this render pass

    end
    ();
    });

    initAsync compiled all three pipelines, so none compiles during a frame. Run two frames, which seed and make TypeGPU’s first-use objects, then check a third:

    flow-field.test.ts
    // In the test, after the loop
    const loop: frames.Frames
    loop
    .
    Frames.run(seconds: number): boolean

    Records and submits one frame with a fresh Frame and encoder. Requires finite, nondecreasing seconds. Throws for an invalid time, a nested frame or an asynchronous recorder. Returns false if the loop is destroyed before or during recording. Runs all after-submit callbacks. If any throw, the frame still counts and run rethrows their errors.

    run
    (0);
    const loop: frames.Frames
    loop
    .
    Frames.run(seconds: number): boolean

    Records and submits one frame with a fresh Frame and encoder. Requires finite, nondecreasing seconds. Throws for an invalid time, a nested frame or an asynchronous recorder. Returns false if the loop is destroyed before or during recording. Runs all after-submit callbacks. If any throw, the frame still counts and run rethrows their errors.

    run
    (1 / 60);
    const
    const counts: RecordingCounts
    counts
    =
    const log: RecordingLog
    log
    .
    RecordingLog.counts(): RecordingCounts

    Returns counts of created objects. Steady frames should leave these counts unchanged.

    counts
    ();
    const log: RecordingLog
    log
    .
    RecordingLog.events: string[]

    Lists pass, draw, dispatch and submit calls in recording order.

    events
    .
    Array<string>.length: number

    Gets or sets the length of the array. This is a number one higher than the highest index in the array.

    length
    = 0;
    const log: RecordingLog
    log
    .
    RecordingLog.writes: {
    buffer: GPUBuffer;
    offset: number;
    size: number;
    }[]

    Records every queue.writeBuffer in call order, with sizes in bytes.

    writes
    .
    Array<T>.length: number

    Gets or sets the length of the array. This is a number one higher than the highest index in the array.

    length
    = 0;
    const loop: frames.Frames
    loop
    .
    Frames.run(seconds: number): boolean

    Records and submits one frame with a fresh Frame and encoder. Requires finite, nondecreasing seconds. Throws for an invalid time, a nested frame or an asynchronous recorder. Returns false if the loop is destroyed before or during recording. Runs all after-submit callbacks. If any throw, the frame still counts and run rethrows their errors.

    run
    (2 / 60);
    expect<string[]>(actual: string[], message?: string): Assertion<void, string[]> (+1 overload)
    expect
    (
    const log: RecordingLog
    log
    .
    RecordingLog.events: string[]

    Lists pass, draw, dispatch and submit calls in recording order.

    events
    ).
    JestAssertion<void, string[]>.toEqual: <string[]>(expected: string[]) => void

    Used when you want to check that two objects have the same value. This matcher recursively checks the equality of all fields, rather than checking for object identity.

    @example expect(user).toEqual({ name: 'Alice', age: 30 });

    toEqual
    ([
    'begin-compute',
    'dispatch',
    'end-compute',
    'begin-render',
    'draw',
    'end-render',
    'submit',
    ]);
    expect<{
    buffer: GPUBuffer;
    offset: number;
    size: number;
    }[]>(actual: {
    buffer: GPUBuffer;
    offset: number;
    size: number;
    }[], message?: string): Assertion<void, {
    buffer: GPUBuffer;
    offset: number;
    size: number;
    }[]> (+1 overload)
    expect
    (
    const log: RecordingLog
    log
    .
    RecordingLog.writes: {
    buffer: GPUBuffer;
    offset: number;
    size: number;
    }[]

    Records every queue.writeBuffer in call order, with sizes in bytes.

    writes
    ).
    JestAssertion<void, { buffer: GPUBuffer; offset: number; size: number; }[]>.toEqual: <never[]>(expected: never[]) => void

    Used when you want to check that two objects have the same value. This matcher recursively checks the equality of all fields, rather than checking for object identity.

    @example expect(user).toEqual({ name: 'Alice', age: 30 });

    toEqual
    ([]);
    expect<RecordingCounts>(actual: RecordingCounts, message?: string): Assertion<void, RecordingCounts> (+1 overload)
    expect
    (
    const log: RecordingLog
    log
    .
    RecordingLog.counts(): RecordingCounts

    Returns counts of created objects. Steady frames should leave these counts unchanged.

    counts
    ()).
    JestAssertion<void, RecordingCounts>.toEqual: <RecordingCounts>(expected: RecordingCounts) => void

    Used when you want to check that two objects have the same value. This matcher recursively checks the equality of all fields, rather than checking for object identity.

    @example expect(user).toEqual({ name: 'Alice', age: 30 });

    toEqual
    (
    const counts: RecordingCounts
    counts
    );
    const surface: OffscreenSurface
    surface
    .
    OffscreenSurface.setSize(size: FixedSize): void

    Replaces the texture with one of the given size. Reports the supplied pixelRatio, or 1 if omitted.

    setSize
    ({
    FixedSize.width: number
    width
    : 128,
    FixedSize.height: number
    height
    : 72 });
    const loop: frames.Frames
    loop
    .
    Frames.run(seconds: number): boolean

    Records and submits one frame with a fresh Frame and encoder. Requires finite, nondecreasing seconds. Throws for an invalid time, a nested frame or an asynchronous recorder. Returns false if the loop is destroyed before or during recording. Runs all after-submit callbacks. If any throw, the frame still counts and run rethrows their errors.

    run
    (3 / 60);
    expect<number[]>(actual: number[], message?: string): Assertion<void, number[]> (+1 overload)
    expect
    (
    const log: RecordingLog
    log
    .
    RecordingLog.writes: {
    buffer: GPUBuffer;
    offset: number;
    size: number;
    }[]

    Records every queue.writeBuffer in call order, with sizes in bytes.

    writes
    .
    Array<{ buffer: GPUBuffer; offset: number; size: number; }>.map<number>(callbackfn: (value: {
    buffer: GPUBuffer;
    offset: number;
    size: number;
    }, index: number, array: {
    buffer: GPUBuffer;
    offset: number;
    size: number;
    }[]) => number, thisArg?: any): number[]

    Calls a defined callback function on each element of an array, and returns an array that contains the results.

    @param ― callbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.

    @param ― thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.

    map
    ((
    write: {
    buffer: GPUBuffer;
    offset: number;
    size: number;
    }
    write
    ) =>
    write: {
    buffer: GPUBuffer;
    offset: number;
    size: number;
    }
    write
    .
    size: number
    size
    )).
    JestAssertion<void, number[]>.toEqual: <number[]>(expected: number[]) => void

    Used when you want to check that two objects have the same value. This matcher recursively checks the equality of all fields, rather than checking for object identity.

    @example expect(user).toEqual({ name: 'Alice', age: 30 });

    toEqual
    ([8]); // the viewport
    const field: {
    settings: TgpuUniform<WgslStruct<{
    speed: F32;
    scale: F32;
    pointSize: F32;
    }>>;
    initAsync(): Promise<void>;
    seed(pass: TgpuComputePass): void;
    step(pass: TgpuComputePass): void;
    draw(pass: TgpuRenderCommands): void;
    destroy: () => void;
    [Symbol.dispose]: () => void;
    }
    field
    .
    destroy: () => void

    Frees everything it took, the last taken first, once; later calls do nothing. When one throws, frees the rest and then rethrows. Also [Symbol.dispose].

    destroy
    ();
    expect<() => boolean>(actual: () => boolean, message?: string): Assertion<void, () => boolean> (+1 overload)
    expect
    (() =>
    const loop: frames.Frames
    loop
    .
    Frames.run(seconds: number): boolean

    Records and submits one frame with a fresh Frame and encoder. Requires finite, nondecreasing seconds. Throws for an invalid time, a nested frame or an asynchronous recorder. Returns false if the loop is destroyed before or during recording. Runs all after-submit callbacks. If any throw, the frame still counts and run rethrows their errors.

    run
    (4 / 60)).
    JestAssertion<void, () => boolean>.toThrow: (expected?: any) => void

    Used to test that a function throws when it is called.

    Also under the alias expect.toThrowError.

    @example expect(() => functionWithError()).toThrow('Error message'); expect(() => parseJSON('invalid')).toThrow(SyntaxError); expect(() => { throw 42 }).toThrow(42);

    toThrow
    ('The flow field was destroyed.');
    • The steady frame records one step and one draw into the passes it was given, submits once, writes nothing and creates no GPU object. Without a scene, nothing writes ahead.
    • A new canvas size writes the 8 bytes of viewport, once.
    • After destroy, recording throws the owner’s message.

    Run pnpm vitest. Testing covers the rest of the log, and how to compile the WGSL on a real GPU.

  • Directorysrc/
    • index.ts the page: web.host and the settings panel
    • scene.ts createScene(root, surface) with the field, the fixed step and the frame
    • flow-field.ts the module
    • flow-field.test.ts its test on a recording root
  • Let the field drift over time. Sample perlin3d with the elapsed time as the third coordinate in place of perlin2d, from a uniform the scene writes once a step.
  • Add finer eddies. Sample the noise again at twice the scale and add half of it to the angle.
  • Give Particle an age, count it up in the step kernel and fade each particle in after it respawns.
  • Package the module as the kit does, as Modules describes.