Skip to content

Commit 1a649bd

Browse files
authored
docs: Explain slots (#1451)
1 parent bc070f0 commit 1a649bd

File tree

2 files changed

+209
-39
lines changed

2 files changed

+209
-39
lines changed

apps/typegpu-docs/astro.config.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,10 @@ export default defineConfig({
126126
slug: 'fundamentals/timestamp-queries',
127127
badge: { text: 'new' },
128128
},
129-
DEV && {
129+
{
130130
label: 'Slots',
131131
slug: 'fundamentals/slots',
132+
badge: { text: 'new' },
132133
},
133134
// {
134135
// label: 'Basic Principles',
Lines changed: 207 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,168 @@
11
---
22
title: Slots
33
description: Slots can be used to inject arbitrary values into shader code at a granular level.
4-
draft: true
54
---
5+
Slots are a powerful dependency injection mechanism in TypeGPU that allows you to write shader logic without having to tightly couple to specific resources.
6+
Similarly to bind group layouts, you can think of slots as of typed "holes" in TypeGPU shaders, that can be filled in later on from the outside.
7+
The main differences between slots and bound resources include the following:
68

7-
You can think of slots as holes in the shader that can be filled in from the outside.
9+
- Slots are filled in before the shader module compile time, instead of just before the shader execution starts.
10+
- Slots can contain not only buffers, but also basically anything that is allowed in TGSL, even TypeGPU functions.
811

9-
```ts
10-
const filterColorSlot = tgpu.slot<vec3f>(); // => TgpuSlot<vec3f>
12+
Main use cases for slots include:
13+
14+
- Generics -- instead of rewriting a function for each generic parameter, it suffices to fill a slot with a different value.
15+
- Passing callbacks -- high-level libraries based on TypeGPU can leave slots for user-defined functions to call.
16+
{/*- Conditional compilation -- an `if` statement with a boolean slot can be pruned at compile time if the slot is filled with `false`.*/}
17+
18+
## Basic usage
19+
20+
A slot is created using the `tgpu.slot()` function and can optionally take a default value:
21+
22+
```ts twoslash
23+
import tgpu from 'typegpu';
24+
import * as d from 'typegpu/data';
25+
// ---cut---
26+
const filterColorSlot = tgpu.slot<d.v3f>(); // Slot for a 3D vector.
27+
const mySlot = tgpu.slot<number>(42); // Slot with a default value.
28+
29+
interface Config {
30+
fogEnabled: boolean;
31+
tint: d.v3f;
32+
}
1133

12-
const filter = tgpu.fn([vec3f])((color) => {
13-
const filterColor = filterColorSlot.$; // => vec3f
34+
const configSlot = tgpu.slot<Config>();
35+
```
36+
37+
You can access a slot's value using either:
38+
39+
- `.value` property,
40+
- `.$` shorthand property.
41+
42+
```typescript
43+
const value = mySlot.value; // or mySlot.$
44+
```
1445

46+
:::caution
47+
Slot values can only be accessed inside TypeGPU functions.
48+
Attempting to access them outside will throw an error.
49+
:::
50+
51+
Slots are resolved during the TypeGPU resolution phase.
52+
The slot object itself does not hold any value.
53+
Instead, you can bind it in one of two ways.
54+
55+
### Binding after defining a function
56+
57+
The first way to bind a value to a slot is to call the `with` method on a `TgpuFn` instance:
58+
59+
```ts twoslash
60+
import tgpu from 'typegpu';
61+
import * as d from 'typegpu/data';
62+
import { mul } from 'typegpu/std';
63+
// ---cut---
64+
const filterColorSlot = tgpu.slot<d.v3f>();
65+
66+
const filter = tgpu.fn([d.vec3f], d.vec3f)((color) => {
67+
const filterColor = filterColorSlot.$;
1568
return mul(color, filterColor);
1669
});
1770

1871
// Bind the filter function with a red color.
1972
const filterWithRed = filter
20-
.with(filterColorSlot, vec3f(1, 0, 0));
73+
.with(filterColorSlot, d.vec3f(1, 0, 0));
2174

2275
// Bind the filter function with a green color.
2376
const filterWithGreen = filter
24-
.with(filterColorSlot, vec3f(0, 1, 0));
77+
.with(filterColorSlot, d.vec3f(0, 1, 0));
2578
```
2679

27-
### Conditional compilation
80+
In the example above, after resolution we are left with two different WGSL functions:
2881

29-
```ts
30-
const safeLoopSlot = tgpu.slot(false);
82+
```wgsl
83+
fn filter_0(color: vec3f) -> vec3f{
84+
var filterColor = vec3f(1, 0, 0);
85+
return (color * filterColor);
86+
}
3187
32-
const processObjects = tgpu.fn([f32], f32)(() => {
33-
let len = objects.length;
88+
fn filter_1(color: vec3f) -> vec3f{
89+
var filterColor = vec3f(0, 1, 0);
90+
return (color * filterColor);
91+
}
92+
```
3493

35-
if (safeLoopSlot.$) { // <- evaluated at compile time
36-
// This will be included in the shader code only if
37-
// `safeLoopSlot` is bound to true in the caller's scope.
38-
len = max(len, 9999);
39-
}
94+
### Binding during pipeline creation
4095

41-
for (let i = 0; i < len; ++i) {
42-
// ...
43-
}
44-
});
96+
The other way to fill in a slot is to call the `with` method during the creation of a `TgpuComputePipeline` or `TgpuRenderPipeline`:
97+
98+
```ts twoslash
99+
import tgpu from 'typegpu';
100+
import * as d from 'typegpu/data';
101+
102+
const root = await tgpu.init();
103+
// ---cut---
104+
const resultBuffer = root.createMutable(d.i32, 0);
105+
const multiplierSlot = tgpu.slot<number>();
106+
107+
const computeMultiply = tgpu['~unstable']
108+
.computeFn({ workgroupSize: [1] })(() => {
109+
resultBuffer.$ = resultBuffer.$ * multiplierSlot.$;
110+
});
111+
112+
const pipeline = root['~unstable']
113+
.with(multiplierSlot, 3)
114+
.withCompute(computeMultiply)
115+
.createPipeline();
45116
```
46117

47-
### Inversion of control
118+
The pipeline above resolves to the following WGSL:
48119

49-
```ts
120+
```wgsl
121+
@group(0) @binding(0) var<storage, read_write> resultBuffer_1: i32;
122+
123+
@compute @workgroup_size(1) fn computeMultiply_0(){
124+
resultBuffer_1 = (resultBuffer_1 * 3);
125+
}
126+
```
127+
128+
:::note
129+
Static code analysis does not verify whether all slots have been filled.
130+
If you forget to assign a value to a slot, a runtime error will be thrown instead.
131+
:::
132+
133+
## Inversion of control
134+
135+
Slots allow libraries to expose customization points to their users.
136+
They enable internal behavior to be modified without sacrificing type safety or performance.
137+
138+
```ts twoslash
50139
// physics.ts
140+
import tgpu from 'typegpu';
141+
import * as d from 'typegpu/data';
142+
import { add, mul } from 'typegpu/std';
51143

52-
const defaultGravity = tgpu.fn([vec2f], vec2f)(() => vec2f(0, 9.8));
144+
const root = await tgpu.init();
53145

54-
export const getGravitySlot = tgpu.slot(defaultGravity);
55-
// ^ TgpuSlot<TgpuFn<[Vec2f], Vec2f>>
146+
const Obj = d.struct({
147+
position: d.vec2f,
148+
velocity: d.vec2f,
149+
});
150+
151+
const objects = root.createReadonly(d.arrayOf(Obj, 256));
152+
const deltaTime = root.createUniform(d.f32);
153+
154+
// ---cut---
155+
const defaultGravity = tgpu.fn([d.vec2f], d.vec2f)((pos) => {
156+
return d.vec2f(0, -9.8);
157+
});
158+
159+
export const gravitySlot = tgpu.slot(defaultGravity);
160+
// ^?
56161

57162
export const stepPhysics = tgpu.fn([])(() => {
58163
for (const obj of objects.$) {
59164
// Calling whatever implementation was provided.
60-
const gravity = getGravitySlot.$(obj.position);
165+
const gravity = gravitySlot.$(obj.position);
61166

62167
obj.velocity = add(obj.velocity, mul(gravity, deltaTime.$));
63168
}
@@ -67,16 +172,80 @@ export const stepPhysics = tgpu.fn([])(() => {
67172
```ts
68173
// main.ts
69174

70-
import { stepPhysics, getGravitySlot } from './physics';
175+
import { stepPhysics, gravitySlot } from './physics.ts';
176+
177+
const gravityTowardsCenter = tgpu.fn([vec2f], vec2f)((pos) => {
178+
return mul(normalize(pos), -1);
179+
});
71180

72-
const getGravityTowardsCenter = tgpu
73-
.fn([vec2f], vec2f)((position) => mul(normalize(position), -1));
181+
const stepPhysicsCustomized = stepPhysics
182+
.with(gravitySlot, gravityTowardsCenter);
74183

75-
const stepPhysicsAltered = stepPhysics
76-
.with(getGravitySlot, getGravityTowardsCenter);
184+
const main = tgpu['~unstable'].computeFn()(() => {
185+
stepPhysicsCustomized(); // <- Will use altered gravity.
186+
});
187+
```
77188

78-
const main = tgpu
79-
.computeFn()(() => {
80-
stepPhysicsAltered(); // <- Will use altered gravity.
81-
});
82-
```
189+
{/*
190+
TODO: re-add and rewrite this section back when we actually prune branches.
191+
192+
## Conditional compilation
193+
194+
Slots can be used for conditional compilation of segments of shaders.
195+
When the slot holding a boolean value is filled with `false`, all `if` statements depending on the value of that slot will be pruned by our tree-shaking mechanism.
196+
197+
```ts twoslash
198+
import tgpu from 'typegpu';
199+
import * as d from 'typegpu/data';
200+
import { max } from 'typegpu/std';
201+
202+
const root = await tgpu.init();
203+
204+
const Obj = d.struct({
205+
pos: d.vec3f,
206+
});
207+
208+
const objects = root.createReadonly(d.arrayOf(Obj, 256));
209+
210+
// ---cut---
211+
const safeLoopSlot = tgpu.slot(false);
212+
213+
const processObjects = tgpu.fn([])(() => {
214+
let len = objects.$.length;
215+
216+
if (safeLoopSlot.$) { // <- evaluated at compile time
217+
// This will be included in the shader code only if
218+
// `safeLoopSlot` is bound to true in the caller's scope.
219+
len = max(len, 9999);
220+
}
221+
222+
for (let i = 0; i < len; ++i) {
223+
// ...
224+
}
225+
});
226+
```
227+
228+
*/}
229+
230+
## Slots in raw WGSL
231+
232+
It is possible to use slots even in TypeGPU functions that are implemented in WGSL:
233+
234+
```ts twoslash
235+
import tgpu from 'typegpu';
236+
import * as d from 'typegpu/data';
237+
// ---cut---
238+
const colorSlot = tgpu.slot(d.vec3f(1, 0, 0));
239+
240+
const getColor = tgpu.fn([], d.vec3f)`() {
241+
return colorSlot;
242+
}`.$uses({ colorSlot });
243+
```
244+
245+
The code above resolves to the following WGSL:
246+
247+
```wgsl
248+
fn getColor() -> vec3f {
249+
return vec3f(1, 0, 0);
250+
}
251+
```

0 commit comments

Comments
 (0)