Skip to content

@typegpu/kitbindings

Shared bindings

Combine module bindings in one app-owned layout and replace resources without rebuilding pipelines.

kit.bindings

module

bindings.layout(entries) → Layout

bindings.share(root, parts) → Shared

entriestakes
the bindings, as tgpu.bindGroupLayout takes them
Layoutreturns
a layout whose bind makes a part from resources
rootborrows
the root
partstakes
parts from any modules and your own, by key
Sharedreturns
a layout you set on each pass, with the group for the parts' resources
types
EntriesResourcesValuesPartBoundBindOptionsLayoutShared
records

Set shared.group() on each pass that reads a part, or give the layout to passes.create as shared.

owns

a bind group

The shared layout recreates it when a part's resources change.

allocates

on change

A bind group from group(), after a shared part's resources change.

injects
  • definition

    Each part's accessors, filled from the shared layout or its own resources.

binds
  • none

    Parts declare entries without binding resources. Each entry uses one binding in the group that holds it.

Give each owner a layout, then share them to fit this pipeline within four bind groups. Highlighted bindings can change through a bind group; fixed resources require new pipelines.

A fixed resource such as root.createUniform(…) goes in the pipeline’s automatic bind group. Replacing that resource requires another pipeline, even for a shadow map resized by a quality setting. A tgpu.bindGroupLayout lets you swap resources, but uses one of each pipeline’s 4 bind groups.

With bindings, a module declares what its shaders read without owning a group. Your app combines module bindings and its own bindings in one layout, then sets one group per pass. Resources in that layout can change without rebuilding pipelines.

import {
import bindings
bindings
} from '@typegpu/kit';
// A fog module declares what its shaders read, beside them
const
const volume: bindings.Layout<{
readonly density: {
readonly texture: d.WgslTexture3d<d.F32>;
};
readonly smooth: {
readonly sampler: "filtering";
};
}>
volume
=
import bindings
bindings
.
function layout<{
readonly density: {
readonly texture: d.WgslTexture3d<d.F32>;
};
readonly smooth: {
readonly sampler: "filtering";
};
}>(entries: {
readonly density: {
readonly texture: d.WgslTexture3d<d.F32>;
};
readonly smooth: {
readonly sampler: "filtering";
};
}): bindings.Layout<{
readonly density: {
readonly texture: d.WgslTexture3d<d.F32>;
};
readonly smooth: {
readonly sampler: "filtering";
};
}>

Declares bindings for module shaders from the entries that tgpu.bindGroupLayout takes. Define the layout once beside the shaders. A part made with bind can bind its resources directly. Call share before creating pipelines to put the part in a shared layout.

@param ― entries the bindings, as tgpu.bindGroupLayout takes them

@returns ― a layout whose bind makes a part from resources

layout
({
density: {
readonly texture: d.WgslTexture3d<d.F32>;
}
density
: {
texture: d.WgslTexture3d<d.F32>
texture
:
import d
d
.
texture3d<d.F32>(sampleType: d.F32): d.WgslTexture3d<d.F32> (+1 overload)
export texture3d
texture3d
(
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
) },
smooth: {
readonly sampler: "filtering";
}
smooth
: {
sampler: "filtering"
sampler
: 'filtering' },
});
const
const fogAt: (p: d.v3f) => number
fogAt
= (
p: d.v3f
p
:
import d
d
.
export v3f

Interface representing its WGSL vector type counterpart: vec3f or vec3. A vector with 3 elements of type f32

v3f
) => {
'use gpu';
return
import std
std
.
textureSampleLevel<d.texture3d<d.F32>>(texture: d.texture3d<d.F32>, sampler: d.sampler, coords: d.v3f, level: number): d.v4f (+13 overloads)
export textureSampleLevel
textureSampleLevel
(
const volume: bindings.Layout<{
readonly density: {
readonly texture: d.WgslTexture3d<d.F32>;
};
readonly smooth: {
readonly sampler: "filtering";
};
}>
volume
.
Layout<{ readonly density: { readonly texture: WgslTexture3d<F32>; }; readonly smooth: { readonly sampler: "filtering"; }; }>.$: {
readonly density: d.texture3d<d.F32>;
readonly smooth: d.sampler;
}

Provides shader access to the entries, as a tgpu.bindGroupLayout's $ does.

$
.
density: d.texture3d<d.F32>
density
,
const volume: bindings.Layout<{
readonly density: {
readonly texture: d.WgslTexture3d<d.F32>;
};
readonly smooth: {
readonly sampler: "filtering";
};
}>
volume
.
Layout<{ readonly density: { readonly texture: WgslTexture3d<F32>; }; readonly smooth: { readonly sampler: "filtering"; }; }>.$: {
readonly density: d.texture3d<d.F32>;
readonly smooth: d.sampler;
}

Provides shader access to the entries, as a tgpu.bindGroupLayout's $ does.

$
.
smooth: d.sampler
smooth
,
p: d.v3f
p
, 0).
v4f.x: number
x
;
};
const
const fog: bindings.Bound<{
readonly density: {
readonly texture: d.WgslTexture3d<d.F32>;
};
readonly smooth: {
readonly sampler: "filtering";
};
}>
fog
=
const volume: bindings.Layout<{
readonly density: {
readonly texture: d.WgslTexture3d<d.F32>;
};
readonly smooth: {
readonly sampler: "filtering";
};
}>
volume
.
Layout<{ readonly density: { readonly texture: WgslTexture3d<F32>; }; readonly smooth: { readonly sampler: "filtering"; }; }>.bind(resources: bindings.Resources<{
readonly density: {
readonly texture: d.WgslTexture3d<d.F32>;
};
readonly smooth: {
readonly sampler: "filtering";
};
}>, options?: bindings.BindOptions): bindings.Bound<{
readonly density: {
readonly texture: d.WgslTexture3d<d.F32>;
};
readonly smooth: {
readonly sampler: "filtering";
};
}>

Binds one set of resources, using the same inputs as root.createBindGroup.

bind
({
density: GPUTextureView | GPUTexture | (SampledFlag & TgpuTexture<{
size: readonly number[];
format: GPUTextureFormat;
dimension: "3d";
sampleCount?: 1 | undefined;
}>) | TgpuTextureView<d.WgslTexture<WgslTextureProps>>
density
:
const grid: (size: number) => TgpuTexture<{
size: [number, number, number];
format: "r8unorm";
dimension: "3d";
}> & SampledFlag
grid
(32),
smooth: TgpuSampler | GPUSampler
smooth
});
// The app takes the fog's part, and any others, into one layout it owns
const
const shared: bindings.Shared
shared
=
import bindings
bindings
.
function share(root: TgpuRoot, parts: Record<string, bindings.Part>): bindings.Shared

Combines parts from modules or your own code in one layout. Call it before creating pipelines. Set its group on each pass with pass.setBindGroup(shared.layout, shared.group()), or give it to passes.create as shared, which sets it in every pass it begins. Every pipeline in that pass that reads a part then uses the shared group.

@param ― parts parts from any modules and your own, by key

@returns ― a layout you set on each pass, with the group for the parts' resources

share
(
const root: TgpuRoot
root
, {
fog: bindings.Bound<{
readonly density: {
readonly texture: d.WgslTexture3d<d.F32>;
};
readonly smooth: {
readonly sampler: "filtering";
};
}>
fog
});
const
const pipeline: TgpuComputePipeline
pipeline
=
const root: TgpuRoot
root
.
WithBinding.pipe(transform: (cfg: Configurable) => Configurable): WithBinding
pipe
(
const fog: bindings.Bound<{
readonly density: {
readonly texture: d.WgslTexture3d<d.F32>;
};
readonly smooth: {
readonly sampler: "filtering";
};
}>
fog
.
Bound<{ readonly density: { readonly texture: WgslTexture3d<F32>; }; readonly smooth: { readonly sampler: "filtering"; }; }>.inject(values?: {
readonly density: d.texture3d<d.F32>;
readonly smooth: d.sampler;
} | undefined): Inject

Fills shader accessors from the shared layout after share takes this part. Otherwise, it binds the resources in each pipeline's automatic group.

inject
()).
WithBinding.createComputePipeline<any>(descriptor: TgpuComputePipeline.Descriptor<any>): TgpuComputePipeline
createComputePipeline
({
compute: TgpuComputeFn<any>
compute
:
const kernel: TgpuComputeFn<any>
kernel
});
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
, ({
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
}) => {
const
const pass: TgpuComputePass
pass
=
encoder: TgpuCommandEncoder

Records commands for submission after the recorder returns. submit() replaces this encoder. Read it at each call instead of keeping it, and let the frame submit it.

encoder
.
TgpuCommandEncoder.beginComputePass(descriptor?: TgpuComputePassDescriptor): TgpuComputePass

Begins recording a compute pass

beginComputePass
();
const pass: TgpuComputePass
pass
.
TgpuComputePass.setBindGroup<Record<string, TgpuLayoutEntry | null>>(bindGroupLayout: TgpuBindGroupLayout<Record<string, TgpuLayoutEntry | null>>, bindGroup: GPUBindGroup | TgpuBindGroup<Record<string, TgpuLayoutEntry | null>>): void (+1 overload)

Associates a bind group with the given layout

setBindGroup
(
const shared: bindings.Shared
shared
.
Shared.layout: TgpuBindGroupLayout<Record<string, TgpuLayoutEntry | null>>

Contains every part's entries. Each entry is named key_name using the part's key in share.

layout
,
const shared: bindings.Shared
shared
.
Shared.group(): TgpuBindGroup

Returns the group for the current resources, for pass.setBindGroup(shared.layout, …). Creates a group only when a resource has changed. Otherwise, it allocates nothing.

group
());
const pass: TgpuComputePass
pass
.
TgpuComputePass.setPipeline(pipeline: TgpuComputePipeline): void

Sets the current

TgpuComputePipeline

for subsequent dispatches

setPipeline
(
const pipeline: TgpuComputePipeline
pipeline
);
const pass: TgpuComputePass
pass
.
TgpuComputePass.dispatchWorkgroups(x: number, y?: number, z?: number): void
dispatchWorkgroups
(64);
const pass: TgpuComputePass
pass
.
TgpuComputePass.end(): void

Completes the recording of this compute pass

end
();
});
const fog: bindings.Bound<{
readonly density: {
readonly texture: d.WgslTexture3d<d.F32>;
};
readonly smooth: {
readonly sampler: "filtering";
};
}>
fog
.
Bound<{ readonly density: { readonly texture: WgslTexture3d<F32>; }; readonly smooth: { readonly sampler: "filtering"; }; }>.replace(resources: Partial<bindings.Resources<{
readonly density: {
readonly texture: d.WgslTexture3d<d.F32>;
};
readonly smooth: {
readonly sampler: "filtering";
};
}>>): void

Replaces resources without creating pipelines. The next shared group uses the replacements. Throws if a pipeline already binds the resources directly, because that pipeline would keep the old resources.

replace
({
density?: GPUTextureView | GPUTexture | (SampledFlag & TgpuTexture<{
size: readonly number[];
format: GPUTextureFormat;
dimension: "3d";
sampleCount?: 1 | undefined;
}>) | TgpuTextureView<d.WgslTexture<WgslTextureProps>> | undefined
density
:
const grid: (size: number) => TgpuTexture<{
size: [number, number, number];
format: "r8unorm";
dimension: "3d";
}> & SampledFlag
grid
(64) }); // the next group binds it, with no new pipelines
  • bindings.layout(entries) takes tgpu.bindGroupLayout’s entries, and shaders read its $. Define it at module level, beside the shaders.
  • layout.bind(resources) gives a part. A module makes its part in create, hands it out as part, and pipes part.inject() into its own inject().
  • A pipeline reads one part per layout, so two fog volumes in one shader need two layouts.
  • A drawing added to a description reads a part through its own inject(), as in buffers and textures of your own.
  • Until an app shares it, a part uses fixed bindings without a separate group. These require TypeGPU resources. A raw GPUBuffer throws when the pipeline resolves. replace throws once a pipeline holds fixed resources, so a part whose resources change asks for its share.

bindings.share(root, parts) combines parts in one app-owned layout. Each entry uses its part’s key as a prefix, as in fog_density, fog_smooth and shadow_map.

  • Call it before creating the pipelines that read the parts. It checks every part before taking any, and throws for one that already has an owner or that a pipeline holds as fixed bindings.
  • A shared part’s own inject() reads from the shared layout. shared.inject() fills every part, for one with no fill of its own like the noise cache.
  • pass.setBindGroup(shared.layout, shared.group()) covers every pipeline in the pass. Without it, the dispatch or draw throws Missing bind groups for layouts. A description of passes given { shared } sets it in every pass it begins.
  • part.replace({ density: bigger }) swaps resources for the next shared.group(), with no new pipelines.
  • shared.group() creates a group only when an entry’s identity changes. A steady frame allocates nothing. Call it after a swap to allocate the group before the next frame.
  • Call replace between frames, then destroy the old resource. Already submitted work finishes with it. A later pass using the destroyed resource fails at submit.

A pipeline that holds a part as fixed bindings keeps the resources it was built with. A part whose resources change while the program runs, such as a texture that follows the canvas, needs its share before that pipeline builds. Give it unshared, a message naming the share to add, and a pipeline that reads the part without one throws it as it builds, at the first frame or warm-up rather than at the first change:

const
const fog: bindings.Bound<{
readonly density: {
readonly texture: d.WgslTexture3d<d.F32>;
};
readonly smooth: {
readonly sampler: "filtering";
};
}>
fog
=
const volume: bindings.Layout<{
readonly density: {
readonly texture: d.WgslTexture3d<d.F32>;
};
readonly smooth: {
readonly sampler: "filtering";
};
}>
volume
.
Layout<{ readonly density: { readonly texture: WgslTexture3d<F32>; }; readonly smooth: { readonly sampler: "filtering"; }; }>.bind(resources: bindings.Resources<{
readonly density: {
readonly texture: d.WgslTexture3d<d.F32>;
};
readonly smooth: {
readonly sampler: "filtering";
};
}>, options?: bindings.BindOptions): bindings.Bound<{
readonly density: {
readonly texture: d.WgslTexture3d<d.F32>;
};
readonly smooth: {
readonly sampler: "filtering";
};
}>

Binds one set of resources, using the same inputs as root.createBindGroup.

bind
(
{
density: GPUTextureView | GPUTexture | (SampledFlag & TgpuTexture<{
size: readonly number[];
format: GPUTextureFormat;
dimension: "3d";
sampleCount?: 1 | undefined;
}>) | TgpuTextureView<d.WgslTexture<WgslTextureProps>>
density
:
const grid: (size: number) => TgpuTexture<{
size: [number, number, number];
format: "r8unorm";
dimension: "3d";
}> & SampledFlag
grid
(32),
smooth: TgpuSampler | GPUSampler
smooth
},
{
BindOptions.unshared?: string | undefined

Makes a pipeline that reads the part throw this message as it builds, unless a bindings.share holds the part. Give it to a part whose resources change while the program runs, and name the call that shares it, so a missing share fails before the first change. Default none: the pipeline binds the resources in its automatic group, and replace throws after that.

unshared
: 'The fog grows: `bindings.share(root, { fog })` before the pipelines that read it.' },
);
  • Ambient occlusion makes its part this way, since a new depth size replaces its texture.
  • A shadow replaces its map only when you call its resize. An unshared shadow works until then, and resize throws at that call before it changes anything.

Any object with these members can be a part, including an object from a package that doesn’t depend on TypeGPU Kit:

Member
entries The layout entries, as tgpu.bindGroupLayout takes them.
resources What to bind now, read whenever a group is due.
inject(values?) Fills the part’s shaders from values, the $ of a layout holding its entries.
check?() Throws if share can’t take the part now, such as one with an owner.
ownedBy?(values) Called by share, so the part’s own fill can follow its owner.

@typegpu/noise’s dynamic Perlin cache can join through an adapter:

import {
(alias) namespace perlin3d
import perlin3d
perlin3d
} from '@typegpu/noise';
const
const config: DynamicPerlin3DCacheConfig<"perlin3dCache__">
config
=
(alias) namespace perlin3d
import perlin3d
perlin3d
.
index.dynamicCacheConfig(options?: {
prefix?: undefined;
}): DynamicPerlin3DCacheConfig<"perlin3dCache__"> (+1 overload)
export index.dynamicCacheConfig

Used to instantiate caches for perlin noise generation, which reduce the amount of redundant calculations if sampling is done more than once. Their domain can be changed at runtime, which makes this cache dynamic (as opposed to perlin3d.staticCache, which is simpler at the cost of rigidity).

@param ―

options A set of general options for instances of this cache configuration.

--- Basic usage

@example

const perlinCacheConfig = perlin3d.dynamicCacheConfig();
// Contains all resources that the perlin cache needs access to
const dynamicLayout = tgpu.bindGroupLayout({ ...perlinCacheConfig.layout });
// ...
const root = await tgpu.init();
// Instantiating the cache with an initial size.
const perlinCache = perlinCacheConfig.instance(root, d.vec3u(10, 10, 1));
const pipeline = root
// Plugging the cache into the pipeline
.pipe(perlinCacheConfig.inject(dynamicLayout.$))
// ...
.createRenderPipeline({
// ...
});
const frame = () => {
// A bind group to fulfill the resource needs of the cache
const group = root.createBindGroup(dynamicLayout, { ...perlinCache.bindings });
pipeline
.with(group)
// ...
.draw(3);
};

dynamicCacheConfig
();
const
const cache: DynamicPerlin3DCache<"perlin3dCache__">
cache
=
const config: DynamicPerlin3DCacheConfig<"perlin3dCache__">
config
.
DynamicPerlin3DCacheConfig<"perlin3dCache__">.instance(root: TgpuRoot, initialSize: d.v3u): DynamicPerlin3DCache<"perlin3dCache__">
instance
(
const root: TgpuRoot
root
,
import d
d
.
const vec3u: d.Vec3u
(x: number, y: number, z: number) => d.v3u (+5 overloads)
vec3u
(16, 16, 16));
const
const noise: bindings.Part<{
readonly perlin3dCache__size: {
uniform: d.Vec4u;
};
readonly perlin3dCache__memory: {
storage: (elementCount: number) => d.WgslArray<d.Vec3f>;
access: "readonly";
};
}>
noise
:
import bindings
bindings
.
interface Part<E extends bindings.Entries = bindings.Entries>

Declares bindings that another layout can hold, such as one made by share. A bound bindings.layout implements this interface. Other modules can implement it too, such as the dynamic cache from @typegpu/noise with { entries: config.layout, resources: cache.bindings, inject: config.inject }.

Part
<typeof
const config: DynamicPerlin3DCacheConfig<"perlin3dCache__">
config
.
DynamicPerlin3DCacheConfig<"perlin3dCache__">.layout: {
readonly perlin3dCache__size: {
uniform: d.Vec4u;
};
readonly perlin3dCache__memory: {
storage: (elementCount: number) => d.WgslArray<d.Vec3f>;
access: "readonly";
};
}
layout
> = {
Part<{ readonly perlin3dCache__size: { uniform: d.Vec4u; }; readonly perlin3dCache__memory: { storage: (elementCount: number) => d.WgslArray<d.Vec3f>; access: "readonly"; }; }>.entries: {
readonly perlin3dCache__size: {
uniform: d.Vec4u;
};
readonly perlin3dCache__memory: {
storage: (elementCount: number) => d.WgslArray<d.Vec3f>;
access: "readonly";
};
}
entries
:
const config: DynamicPerlin3DCacheConfig<"perlin3dCache__">
config
.
DynamicPerlin3DCacheConfig<"perlin3dCache__">.layout: {
readonly perlin3dCache__size: {
uniform: d.Vec4u;
};
readonly perlin3dCache__memory: {
storage: (elementCount: number) => d.WgslArray<d.Vec3f>;
access: "readonly";
};
}
layout
,
get
Part<{ readonly perlin3dCache__size: { uniform: d.Vec4u; }; readonly perlin3dCache__memory: { storage: (elementCount: number) => d.WgslArray<d.Vec3f>; access: "readonly"; }; }>.resources: bindings.Resources<{
readonly perlin3dCache__size: {
uniform: d.Vec4u;
};
readonly perlin3dCache__memory: {
storage: (elementCount: number) => d.WgslArray<d.Vec3f>;
access: "readonly";
};
}>

Provides the resources to bind. group() reads them and compares each entry by identity.

resources
() {
return
const cache: DynamicPerlin3DCache<"perlin3dCache__">
cache
.
DynamicPerlin3DCache<"perlin3dCache__">.bindings: {
perlin3dCache__size: TgpuBuffer<d.Vec4u> & UniformFlag;
perlin3dCache__memory: TgpuBuffer<d.WgslArray<d.Vec3f>> & StorageFlag;
}
bindings
;
},
Part<{ readonly perlin3dCache__size: { uniform: d.Vec4u; }; readonly perlin3dCache__memory: { storage: (elementCount: number) => d.WgslArray<d.Vec3f>; access: "readonly"; }; }>.inject(values?: {
readonly perlin3dCache__size: d.v4u;
readonly perlin3dCache__memory: d.v3f[];
} | undefined): Inject

Fills shader accessors from values, the $ of a layout containing this part's entries.

inject
(
values: {
readonly perlin3dCache__size: d.v4u;
readonly perlin3dCache__memory: d.v3f[];
} | undefined
values
) {
if (!
values: {
readonly perlin3dCache__size: d.v4u;
readonly perlin3dCache__memory: d.v3f[];
} | undefined
values
) throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
('The noise cache has no group of its own; share it.');
return
const config: DynamicPerlin3DCacheConfig<"perlin3dCache__">
config
.
DynamicPerlin3DCacheConfig<"perlin3dCache__">.inject(layoutValue: {
readonly perlin3dCache__size: DataAccessorIn<d.Vec4u>;
readonly perlin3dCache__memory: DataAccessorIn<d.WgslArray<d.Vec3f>>;
}): (cfg: Configurable) => Configurable
inject
(
values: {
readonly perlin3dCache__size: d.v4u;
readonly perlin3dCache__memory: d.v3f[];
}
values
);
},
};
const
const shared: bindings.Shared
shared
=
import bindings
bindings
.
function share(root: TgpuRoot, parts: Record<string, bindings.Part>): bindings.Shared

Combines parts from modules or your own code in one layout. Call it before creating pipelines. Set its group on each pass with pass.setBindGroup(shared.layout, shared.group()), or give it to passes.create as shared, which sets it in every pass it begins. Every pipeline in that pass that reads a part then uses the shared group.

@param ― parts parts from any modules and your own, by key

@returns ― a layout you set on each pass, with the group for the parts' resources

share
(
const root: TgpuRoot
root
, {
noise: bindings.Part<{
readonly perlin3dCache__size: {
uniform: d.Vec4u;
};
readonly perlin3dCache__memory: {
storage: (elementCount: number) => d.WgslArray<d.Vec3f>;
access: "readonly";
};
}>
noise
});

Resize with cache.size = next. The next shared.group() binds the resized cache.

A pipeline with more groups than the device’s maxBindGroups, 4 on every device, fails with an error such as bindGroupLayoutCount (5) is larger than the maximum allowed (4). A census counts each pipeline’s groups, including the automatic group.

  • Sharing keeps the same binding count per stage. A shared layout exposes every entry to every stage, as the automatic group does, so a pipeline that reads one part counts every part. pipelines.compile names the bindings of a pipeline that goes over.
  • Per-draw bindings, such as a mesh’s textures, keep their own layout.
  • Groups owned by one module stay separate. The neighbour grid’s sort uses 4 groups of its own.
  • A pipeline that reads a shared part and runs outside the app’s passes gets no group, and throws Missing bind groups for layouts.
  • shared.layout is typed loosely, without its entries’ names.
  • shared.group() costs one identity check per entry per call; parts carry no version number.