Skip to content

Commit 28ce667

Browse files
authored
feat: Ref/Value behavior tracking (#1755)
1 parent d51b214 commit 28ce667

File tree

130 files changed

+4090
-2134
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

130 files changed

+4090
-2134
lines changed

apps/typegpu-docs/astro.config.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,10 @@ export default defineConfig({
274274
label: 'Naming Convention',
275275
slug: 'reference/naming-convention',
276276
},
277+
DEV && {
278+
label: 'Shader Generation',
279+
slug: 'reference/shader-generation',
280+
},
277281
typeDocSidebarGroup,
278282
]),
279283
},
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
---
2+
title: Shader Generation
3+
draft: true
4+
---
5+
6+
TypeGPU houses a very powerful shader generator, capable of generating efficient WGSL code that closely matches the input
7+
JavaScript code.
8+
9+
## The phases of code generation
10+
11+
The whole end-to-end process of turning JS into WGSL can be split into two phases:
12+
- Parse the JS code into an AST.
13+
- Collapse each AST node into a [snippet](#snippets) depth first, gradually building up the final WGSL code.
14+
15+
We found that we don't always have enough information to do both phases as a build step (before the code reaches the browser).
16+
For example, the type of a struct could only be known at runtime, or could be imported from another file, which complicates static analysis:
17+
18+
```ts twoslash
19+
import * as d from 'typegpu/data';
20+
21+
declare const getUserSettings: () => Promise<{ halfPrecision: boolean }>;
22+
// ---cut---
23+
const half = (await getUserSettings()).halfPrecision;
24+
25+
// Determining the precision based on a runtime parameter
26+
const vec3 = half ? d.vec3h : d.vec3f;
27+
28+
const Boid = d.struct({
29+
pos: vec3,
30+
vel: vec3,
31+
});
32+
33+
const createBoid = () => {
34+
'use gpu';
35+
return Boid({ pos: vec3(), vel: vec3(0, 1, 0) });
36+
};
37+
38+
const boid = createBoid();
39+
// ^?
40+
```
41+
42+
:::caution
43+
We could do everything at runtime, transforming the code a bit so that the TypeGPU shader generator can have access to
44+
a function's JS source code, along with references to values referenced from the outer scope.
45+
46+
The code shipped to the browser could look like so:
47+
```js
48+
const createBoid = () => {
49+
'use gpu';
50+
return Boid({ pos: vec3(), vel: vec3(0, 1, 0) });
51+
};
52+
53+
// Associate metadata with the function, which the TypeGPU generator can later use
54+
(globalThis.__TYPEGPU_META__ ??= new WeakMap()).set(createBoid, {
55+
v: 1,
56+
name: 'createBoid',
57+
code: `() => {
58+
'use gpu';
59+
return Boid({ pos: vec3(), vel: vec3(0, 1, 0) });
60+
}`,
61+
get externals() { return { Boid, vec3 }; },
62+
});
63+
```
64+
65+
However, parsing code at runtime requires both shipping the parser to the end user, and having to spend time parsing the code,
66+
sacrificing load times and performance.
67+
:::
68+
69+
In order to avoid parsing at runtime while keeping the desired flexibility, we parse the AST at build time and compress it into
70+
our custom format called [tinyest](https://npmjs.com/package/tinyest). It retains only information required for WGSL code
71+
generation.
72+
73+
The code shipped to the browser looks more like this:
74+
```js
75+
const createBoid = () => {
76+
'use gpu';
77+
return Boid({ pos: vec3(), vel: vec3(0, 1, 0) });
78+
};
79+
80+
(globalThis.__TYPEGPU_META__ ??= new WeakMap()).set(createBoid, {
81+
v: 1,
82+
name: 'createBoid',
83+
// NOTE: Not meant to be read by humans
84+
ast: {params:[],body:[0,[[10,[6,"Boid",[[104,{pos:[6,"vec3",[]],vel:[6,"vec3",[[5,"0"],[5,"1"],[5,"0"]]]}]]]]]],externalNames:["Boid","vec3"]},
85+
get externals() { return { Boid, vec3 }; },
86+
});
87+
```
88+
89+
## Snippets
90+
91+
Snippets are the basis for TypeGPU shader code generation. They are immutable objects that hold three values:
92+
- *value*: A piece of WGSL code, or something "resolvable" to a piece of WGSL code
93+
- *dataType*: The inferred WGSL type of `value` [(more here)](#data-types)
94+
- *origin*: An enumerable of where `value` came from (if it's a reference to an existing value, or ephemeral)
95+
[(more here)](#origins)
96+
97+
```ts
98+
// A simple snippet of a piece of WGSL code
99+
const foo = snip(
100+
/* value */ 'vec3f(1, 2, 3)',
101+
/* dataType */ d.vec3f,
102+
/* origin */ 'constant'
103+
); // => Snippet
104+
105+
// A simple snippet of something resolvable to a piece of WGSL code
106+
const bar = snip(
107+
/* value */ d.vec3f(1, 2, 3),
108+
/* dataType */ d.vec3f,
109+
/* origin */ 'constant'
110+
); // => Snippet
111+
```
112+
113+
If a snippet contains a value that isn't yet resolved WGSL, we defer that resolution as late as possible, so that we can
114+
perform optimizations as we generate. For example, if we're evaluating the given expression `3 * 4`, we first interpret
115+
both operands as snippets `snip(3, abstractInt, 'constant')` and `snip(4, abstractInt, 'constant')` respectively.
116+
Since both are not yet resolved (or in other words, known at compile time), we can perform the multiplication at compile time,
117+
resulting in a new snippet `snip(12, abstractInt, 'constant')`.
118+
119+
:::note
120+
If we were instead resolving eagerly, the resulting snippet would be `snip('3 * 4', abstractInt, 'constant')`.
121+
:::
122+
123+
### Data Types
124+
125+
The data types that accompany snippets are just [TypeGPU Data Schemas](/TypeGPU/fundamentals/data-schemas). This information
126+
can be used by parent expressions to generate different code.
127+
128+
:::note
129+
Data type inference is the basis for generating signatures for functions just from the arguments passed to them.
130+
:::
131+
132+
### Origins
133+
134+
Origins are enumerable values that describe where a value came from (or didn't come from). Used mainly for:
135+
- Determining if we're using a value that refers to something else (to create an implicit pointer). This mimics the behavior we
136+
expect in JS, and doesn't perform unwanted copies on data. Example:
137+
```ts
138+
const foo = () => {
139+
'use gpu';
140+
// The type of both expressions is `Boid`, yet one is a
141+
// reference to an existing value, and the other is a
142+
// value-type (ephemeral) and would disappear if we didn't
143+
// assign it to a variable or use it.
144+
const firstBoid = layout.$.boids[0];
145+
const newBoid = Boid();
146+
const copiedBoid = Boid(firstBoid);
147+
148+
const boidPos = newBoid.pos;
149+
};
150+
```
151+
Generates:
152+
```wgsl
153+
fn foo() {
154+
let firstBoid = (&boids[0]); // typed as ptr<storage, Boid, read_write>
155+
var newBoid = Boid(); // typed as Boid
156+
var copiedBoid = firstBoid; // typed as Boid
157+
158+
let boidPos = (&newBoid.pos); // typed as ptr<function, vec3f>
159+
}
160+
```
161+
- Detecting illegal uses of our APIs. One example is mutating a value that was passed in as an argument. Since we want the developer to have control over
162+
passing something as value or as reference (pointer), we have to limit the dev's ability to mutate values that were passed in as arguments if they didn't
163+
use refs (pointer instances). Otherwise, the generated WGSL won't act as we expect.
164+
```ts
165+
const advance = (pos: d.v3f) => {
166+
'use gpu';
167+
// `pos` has the origin 'argument'. Property accesses on arguments
168+
// return snippets that also have the origin 'argument'.
169+
//
170+
// If we try to mutate a snippet that has the origin 'argument',
171+
// we'll get a resolution error.
172+
pos.x += 1;
173+
};
174+
175+
const main = () => {
176+
'use gpu';
177+
const pos = d.vec3f(0, 0, 0);
178+
advance(pos);
179+
// pos.x === 1 in JS
180+
};
181+
```
182+
Would generate:
183+
```wgsl
184+
fn advance(pos: vec3f) {
185+
pos.x += 1;
186+
}
187+
188+
fn main() {
189+
let pos = vec3f(0, 0, 0);
190+
advance(pos);
191+
// pos.x === 0 in WGSL
192+
}
193+
```
194+
195+
There are essentially three types of origins:
196+
- **Ephemeral Origins**: These origins represent values that are created or derived from other values. They are typically used for creating new instances or
197+
performing operations that produce new values. Examples include creating a new `Boid` instance or calculating a new position based on an existing one. These
198+
include `'runtime'` and `'constant'`.
199+
- **Referential Origins**: These origins represent references to existing values. They are typically used for accessing or modifying existing data. Examples
200+
include accessing the position of an existing `Boid` instance or modifying the position of an existing `Boid` instance. These include `'uniform'`, `'mutable'`, `'readonly'`, `'workgroup'`, `'private'`, `'function'`, `'handle'`, `'constant-ref'` and `'this-function'`.
201+
- `'uniform'`, `'mutable'`, `'readonly'`, `'workgroup'`, `'private'`, `'function'`, `'handle'` all reflect address spaces that values can belong to, and
202+
we use them to determine what kind of pointer type they are.
203+
- `'constant-ref'` is a reference to a value stored in a `tgpu.const`. They're different from `constant`s, as we know that even if they're referential (non-primitive), the developer cannot mutate them.
204+
- `'this-function'` lets us track whether values originates from the function we're currently generating, or the function that called us.
205+
- **Argument Origins**: This group is dedicated to exactly one origin: 'argument'. It represents values that are passed as arguments to functions.

apps/typegpu-docs/src/examples/image-processing/blur/index.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,7 @@ const ioLayout = tgpu.bindGroupLayout({
6666
outTexture: { storageTexture: d.textureStorage2d('rgba8unorm') },
6767
});
6868

69-
const tileData = tgpu.workgroupVar(
70-
d.arrayOf(d.arrayOf(d.vec3f, 128), 4),
71-
);
69+
const tileData = tgpu.workgroupVar(d.arrayOf(d.arrayOf(d.vec3f, 128), 4));
7270

7371
const computeFn = tgpu['~unstable'].computeFn({
7472
in: {
Lines changed: 31 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,12 @@
11
import * as d from 'typegpu/data';
22
import * as std from 'typegpu/std';
33
import * as p from './params.ts';
4-
import { computeBindGroupLayout as layout, ModelData } from './schemas.ts';
4+
import { computeBindGroupLayout as layout } from './schemas.ts';
55
import { projectPointOnLine } from './tgsl-helpers.ts';
66

77
export const simulate = (fishIndex: number) => {
88
'use gpu';
9-
// TODO: replace it with struct copy when Chromium is fixed
10-
const fishData = ModelData({
11-
position: layout.$.currentFishData[fishIndex].position,
12-
direction: layout.$.currentFishData[fishIndex].direction,
13-
scale: layout.$.currentFishData[fishIndex].scale,
14-
variant: layout.$.currentFishData[fishIndex].variant,
15-
applySeaDesaturation:
16-
layout.$.currentFishData[fishIndex].applySeaDesaturation,
17-
applySeaFog: layout.$.currentFishData[fishIndex].applySeaFog,
18-
applySinWave: layout.$.currentFishData[fishIndex].applySinWave,
19-
});
9+
const fishData = layout.$.currentFishData[fishIndex];
2010
let separation = d.vec3f();
2111
let alignment = d.vec3f();
2212
let alignmentCount = 0;
@@ -30,34 +20,22 @@ export const simulate = (fishIndex: number) => {
3020
continue;
3121
}
3222

33-
// TODO: replace it with struct copy when Chromium is fixed
34-
const other = ModelData({
35-
position: layout.$.currentFishData[i].position,
36-
direction: layout.$.currentFishData[i].direction,
37-
scale: layout.$.currentFishData[i].scale,
38-
variant: layout.$.currentFishData[i].variant,
39-
applySeaDesaturation: layout.$.currentFishData[i].applySeaDesaturation,
40-
applySeaFog: layout.$.currentFishData[i].applySeaFog,
41-
applySinWave: layout.$.currentFishData[i].applySinWave,
42-
});
43-
const dist = std.length(std.sub(fishData.position, other.position));
23+
const other = layout.$.currentFishData[i];
24+
const dist = std.length(fishData.position.sub(other.position));
4425
if (dist < layout.$.fishBehavior.separationDist) {
45-
separation = std.add(
46-
separation,
47-
std.sub(fishData.position, other.position),
48-
);
26+
separation = separation.add(fishData.position.sub(other.position));
4927
}
5028
if (dist < layout.$.fishBehavior.alignmentDist) {
51-
alignment = std.add(alignment, other.direction);
29+
alignment = alignment.add(other.direction);
5230
alignmentCount = alignmentCount + 1;
5331
}
5432
if (dist < layout.$.fishBehavior.cohesionDist) {
55-
cohesion = std.add(cohesion, other.position);
33+
cohesion = cohesion.add(other.position);
5634
cohesionCount = cohesionCount + 1;
5735
}
5836
}
5937
if (alignmentCount > 0) {
60-
alignment = std.mul(1 / d.f32(alignmentCount), alignment);
38+
alignment = alignment.mul(1 / d.f32(alignmentCount));
6139
}
6240
if (cohesionCount > 0) {
6341
cohesion = std.sub(
@@ -75,12 +53,12 @@ export const simulate = (fishIndex: number) => {
7553

7654
if (axisPosition > axisAquariumSize - distance) {
7755
const str = axisPosition - (axisAquariumSize - distance);
78-
wallRepulsion = std.sub(wallRepulsion, std.mul(str, repulsion));
56+
wallRepulsion = wallRepulsion.sub(repulsion.mul(str));
7957
}
8058

8159
if (axisPosition < -axisAquariumSize + distance) {
8260
const str = -axisAquariumSize + distance - axisPosition;
83-
wallRepulsion = std.add(wallRepulsion, std.mul(str, repulsion));
61+
wallRepulsion = wallRepulsion.add(repulsion.mul(str));
8462
}
8563
}
8664

@@ -89,42 +67,38 @@ export const simulate = (fishIndex: number) => {
8967
fishData.position,
9068
layout.$.mouseRay.line,
9169
);
92-
const diff = std.sub(fishData.position, proj);
70+
const diff = fishData.position.sub(proj);
9371
const limit = p.fishMouseRayRepulsionDistance;
9472
const str = std.pow(2, std.clamp(limit - std.length(diff), 0, limit)) - 1;
95-
rayRepulsion = std.mul(str, std.normalize(diff));
73+
rayRepulsion = std.normalize(diff).mul(str);
9674
}
9775

98-
fishData.direction = std.add(
99-
fishData.direction,
100-
std.mul(layout.$.fishBehavior.separationStr, separation),
76+
let direction = d.vec3f(fishData.direction);
77+
78+
direction = direction.add(
79+
separation.mul(layout.$.fishBehavior.separationStr),
10180
);
102-
fishData.direction = std.add(
103-
fishData.direction,
104-
std.mul(layout.$.fishBehavior.alignmentStr, alignment),
81+
direction = direction.add(
82+
alignment.mul(layout.$.fishBehavior.alignmentStr),
10583
);
106-
fishData.direction = std.add(
107-
fishData.direction,
108-
std.mul(layout.$.fishBehavior.cohesionStr, cohesion),
84+
direction = direction.add(
85+
cohesion.mul(layout.$.fishBehavior.cohesionStr),
10986
);
110-
fishData.direction = std.add(
111-
fishData.direction,
112-
std.mul(p.fishWallRepulsionStrength, wallRepulsion),
87+
direction = direction.add(
88+
wallRepulsion.mul(p.fishWallRepulsionStrength),
11389
);
114-
fishData.direction = std.add(
115-
fishData.direction,
116-
std.mul(p.fishMouseRayRepulsionStrength, rayRepulsion),
90+
direction = direction.add(
91+
rayRepulsion.mul(p.fishMouseRayRepulsionStrength),
11792
);
118-
119-
fishData.direction = std.mul(
120-
std.clamp(std.length(fishData.direction), 0.0, 0.01),
121-
std.normalize(fishData.direction),
93+
direction = std.normalize(direction).mul(
94+
std.clamp(std.length(fishData.direction), 0, 0.01),
12295
);
12396

124-
const translation = std.mul(
97+
const translation = direction.mul(
12598
d.f32(std.min(999, layout.$.timePassed)) / 8,
126-
fishData.direction,
12799
);
128-
fishData.position = std.add(fishData.position, translation);
129-
layout.$.nextFishData[fishIndex] = fishData;
100+
101+
const nextFishData = layout.$.nextFishData[fishIndex];
102+
nextFishData.position = fishData.position.add(translation);
103+
nextFishData.direction = d.vec3f(direction);
130104
};

0 commit comments

Comments
 (0)