Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
e5068dd
Tracking referentiality
iwoplaza Sep 26, 2025
c5189be
More progress on the implementation
iwoplaza Sep 27, 2025
eb8f520
Mostly works now
iwoplaza Sep 27, 2025
5312d1d
Track ref address space
iwoplaza Sep 27, 2025
52124a7
Enforcing copy when assigning
iwoplaza Sep 27, 2025
07ed411
JS const with ref value is WGSL let with pointer
iwoplaza Sep 27, 2025
ba2a4ff
Fix ptr return types, and invalid ref and deref op order
iwoplaza Sep 27, 2025
7b7e5dc
Const statements
iwoplaza Sep 27, 2025
9a75b1e
Not allowing references to be returned from a function (unless it's a
iwoplaza Sep 27, 2025
5dc3884
Move member access exceptions to `accessProp` so that it's shared with
iwoplaza Sep 27, 2025
755d13b
Better indexing
iwoplaza Sep 27, 2025
ec2657f
Indexing arrays at comptime
iwoplaza Sep 27, 2025
8497a35
Constant tracking
iwoplaza Sep 27, 2025
cc349ff
Infix
iwoplaza Sep 27, 2025
77ac0cc
Apply formatting
iwoplaza Sep 27, 2025
04fa183
Merge branch 'main' into feat/ref-value
iwoplaza Oct 7, 2025
aee85a6
A few tweaks
iwoplaza Oct 7, 2025
5173502
Update wgslGenerator.ts
iwoplaza Oct 7, 2025
c3bf598
Fixes
iwoplaza Oct 7, 2025
40b3f7a
Self review
iwoplaza Oct 7, 2025
7c5900b
Update accessor.ts
iwoplaza Oct 7, 2025
50741ce
More tweaks
iwoplaza Oct 7, 2025
4dff99e
Merge branch 'main' into feat/ref-value
iwoplaza Oct 7, 2025
51c2919
Apply suggestion from @aleksanderkatan
iwoplaza Oct 8, 2025
b035d45
Apply suggestion from @aleksanderkatan
iwoplaza Oct 8, 2025
3932b2f
Update stable-fluid.test.ts
iwoplaza Oct 9, 2025
62936f8
Simplify 3D Fish compute
iwoplaza Oct 9, 2025
14c7282
Update compute.ts
iwoplaza Oct 9, 2025
e90832d
Review fixes
iwoplaza Oct 9, 2025
549bc71
Merge branch 'main' into feat/ref-value
iwoplaza Oct 10, 2025
e3b3bba
Merge branch 'main' into feat/ref-value
iwoplaza Oct 13, 2025
1e2df56
Updates after changing 'kernel' to 'use gpu'
iwoplaza Oct 13, 2025
bbc0508
feat: Better constant handling for ref/value tracking (#1801)
iwoplaza Oct 16, 2025
1f8392e
Merge branch 'main' into feat/ref-value
iwoplaza Oct 30, 2025
3824a3a
Update snapshots
iwoplaza Oct 30, 2025
b4f2c0b
Rename ref to origin
iwoplaza Oct 31, 2025
beb2295
Explicit refs
iwoplaza Nov 1, 2025
808deef
Merge branch 'main' into feat/ref-value
iwoplaza Nov 3, 2025
109dbe9
Implicit function pointers don't cause shell-less functions to generate
iwoplaza Nov 3, 2025
0fdadf5
Using std.neg when resolving unary `-` operator, and emitting `let` when
iwoplaza Nov 3, 2025
04fb153
Fix Disco example
iwoplaza Nov 3, 2025
2a6527e
🦕
iwoplaza Nov 3, 2025
59f2618
Updating gravity example
iwoplaza Nov 3, 2025
f85150b
More updates
iwoplaza Nov 3, 2025
05270d0
Update gravity.test.ts
iwoplaza Nov 3, 2025
66b89cf
Fixed!
iwoplaza Nov 3, 2025
2ed5955
Update Gravity code
iwoplaza Nov 3, 2025
4ccdd96
Working on umiform refs
iwoplaza Nov 4, 2025
dbcd394
Merge branch 'main' into feat/ref-value
iwoplaza Nov 4, 2025
07ee6b7
Writing internal docs about shader generation
iwoplaza Nov 4, 2025
79252cb
More useful refs
iwoplaza Nov 4, 2025
c853f5c
Simplify and document
iwoplaza Nov 4, 2025
21776b3
Test for updating a whole struct, returning refs
iwoplaza Nov 4, 2025
b1fe352
Updates
iwoplaza Nov 4, 2025
5805cac
Simplify implicit pointer dereferencing
iwoplaza Nov 5, 2025
37914d5
Merge branch 'main' into feat/ref-value
iwoplaza Nov 6, 2025
685479d
More tests and restrictions
iwoplaza Nov 6, 2025
171af79
More test coverage for argument origin tracking
iwoplaza Nov 6, 2025
c6b0537
Update shader-generation.mdx
iwoplaza Nov 6, 2025
b3e979e
Update shader-generation.mdx
iwoplaza Nov 6, 2025
e0b5cc7
🦕
iwoplaza Nov 6, 2025
39b5704
Merge branch 'main' into feat/ref-value
iwoplaza Nov 6, 2025
c31bb13
Better handling of arguments
iwoplaza Nov 6, 2025
6003bc3
Update pointers.ts
iwoplaza Nov 6, 2025
74d1291
Fix for referencing implicit pointers
iwoplaza Nov 7, 2025
756384d
Cleanup 🧹
iwoplaza Nov 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/typegpu-docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,10 @@ export default defineConfig({
label: 'Naming Convention',
slug: 'reference/naming-convention',
},
DEV && {
label: 'Shader Generation',
slug: 'reference/shader-generation',
},
typeDocSidebarGroup,
]),
},
Expand Down
199 changes: 199 additions & 0 deletions apps/typegpu-docs/src/content/docs/reference/shader-generation.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
---
title: Shader Generation
draft: true
---

TypeGPU houses a very powerful shader generator, capable of generating efficient WGSL code that closely matches the input
JavaScript code.

## The phases of code generation

The whole end-to-end process of turning JS into WGSL can be split into two phases:
- Parse the JS code into an AST.
- Collapse each AST node into a [snippet](#snippets) depth first, gradually building up the final WGSL code.

We found that we don't always have enough information to do both phases as a build step (before the code reaches the browser).
For example, the type of a struct could only be known at runtime, or could be imported from another file, which complicates static analysis:

```ts twoslash
import * as d from 'typegpu/data';

declare const getUserSettings: () => Promise<{ halfPrecision: boolean }>;
// ---cut---
const half = (await getUserSettings()).halfPrecision;

// Determining the precision based on a runtime parameter
const vec3 = half ? d.vec3h : d.vec3f;

const Boid = d.struct({
pos: vec3,
vel: vec3,
});

const createBoid = () => {
'use gpu';
return Boid({ pos: vec3(), vel: vec3(0, 1, 0) });
};

const boid = createBoid();
// ^?
```

:::caution
We could do everything at runtime, transforming the code a bit so that the TypeGPU shader generator can have access to
a function's JS source code, along with references to values referenced from the outer scope.

The code shipped to the browser could look like so:
```js
const createBoid = () => {
'use gpu';
return Boid({ pos: vec3(), vel: vec3(0, 1, 0) });
};

// Associate metadata with the function, which the TypeGPU generator can later use
(globalThis.__TYPEGPU_META__ ??= new WeakMap()).set(createBoid, {
v: 1,
name: 'createBoid',
code: `() => {
'use gpu';
return Boid({ pos: vec3(), vel: vec3(0, 1, 0) });
}`,
get externals() { return { Boid, vec3 }; },
});
```

However, parsing code at runtime requires both shipping the parser to the end user, and having to spend time parsing the code,
sacrificing load times and performance.
:::

In order to avoid parsing at runtime while keeping the desired flexibility, we parse the AST at build time and compress it into
our custom format called [tinyest](https://npmjs.com/package/tinyest). It retains only information required for WGSL code
generation.

The code shipped to the browser looks more like this:
```js
const createBoid = () => {
'use gpu';
return Boid({ pos: vec3(), vel: vec3(0, 1, 0) });
};

(globalThis.__TYPEGPU_META__ ??= new WeakMap()).set(createBoid, {
v: 1,
name: 'createBoid',
// NOTE: Not meant to be read by humans
ast: {params:[],body:[0,[[10,[6,"Boid",[[104,{pos:[6,"vec3",[]],vel:[6,"vec3",[[5,"0"],[5,"1"],[5,"0"]]]}]]]]]],externalNames:["Boid","vec3"]},
get externals() { return { Boid, vec3 }; },
});
```

## Snippets

Snippets are the basis for TypeGPU shader code generation. They are immutable objects that hold three values:
- *value*: A piece of WGSL code, or something "resolvable" to a piece of WGSL code
- *dataType*: The inferred WGSL type of `value` [(more here)](#data-types)
- *origin*: An enumerable of where the value came from (if it's a reference to an existing value, or ephemeral)
[(more here)](#origins)

```ts
// A simple snippet of a piece of WGSL code
const foo = snip(
/* value */ 'vec3f(1, 2, 3)',
/* dataType */ d.vec3f,
/* origin */ 'constant'
); // => Snippet

// A simple snippet of something resolvable to a piece of WGSL code
const bar = snip(
/* value */ d.vec3f(1, 2, 3),
/* dataType */ d.vec3f,
/* origin */ 'constant'
); // => Snippet
```

If a snippet contains a value that isn't yet resolved WGSL, we defer that resolution as late as possible, so that we can
perform optimizations as we generate. For example, if we're evaluating the given expression `3 * 4`, we first interpret
both operands as snippets `snip(3, abstractInt, 'constant')` and `snip(4, abstractInt, 'constant')` respectively.
Since both are not yet resolved (or in other words, known at compile time), we can perform the multiplication at compile time,
resulting in a new snippet `snip(12, abstractInt, 'constant')`.

:::note
If we were instead resolving eagerly, the resulting snippet would be `snip('3 * 4', abstractInt, 'constant')`.
:::

### Data Types

The data types that accompany snippets are just [TypeGPU Data Schemas](/TypeGPU/fundamentals/data-schemas). This information
can be used by parent expressions to generate different code.

:::note
Data type inference is the basis for generating signatures for functions just from the arguments passed to them.
:::

### Origins

Origins are enumerable values that describe where a value came from (or didn't come from). Used mainly for:
- Determining if we're using a value that refers to something else (to create an implicit pointer). This mimics the behavior we
expect in JS, and doesn't perform unwanted copies on data. Example:
```ts
const foo = () => {
'use gpu';
// The type of both expressions is `Boid`, yet one is a
// reference to an existing value, and the other is a
// value-type (ephemeral) and would disappear if we didn't
// assign it to a variable or use it.
const firstBoid = layout.$.boids[0];
const newBoid = Boid();

const boidPos = newBoid.pos;
};
```
Generates:
```wgsl
fn foo() {
let firstBoid = (&boids[0]); // typed as ptr<storage, Boid, read_write>
var newBoid = Boid(); // typed as Boid

let boidPos = (&newBoid.pos); // typed as ptr<function, vec3f>
}
```
- 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
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
use refs (pointer instances). Otherwise, the generated WGSL won't act as we expect.
```ts
const advance = (pos: d.v3f) => {
'use gpu';
// `pos` has the origin 'argument'. Property accesses on arguments
// return snippets that also have the origin 'argument'.
//
// If we try to mutate a snippet that has the origin 'argument',
// we'll get a resolution error.
pos.x += 1;
};

const main = () => {
'use gpu';
const pos = d.vec3f(0, 0, 0);
advance(pos);
// pos.x === 1 in JS
};
```
Generates:
```wgsl
fn advance(pos: vec3f) {
pos.x += 1;
}

fn main() {
let pos = vec3f(0, 0, 0);
advance(pos);
// pos.x === 0 in WGSL
}
```

There are essentially three types of origins:
- **Ephemeral Origins**: These origins represent values that are created or derived from other values. They are typically used for creating new instances or
performing operations that produce new values. Examples include creating a new `Boid` instance or calculating a new position based on an existing one. These
include `'runtime'` and `'constant'`.
- **Referential Origins**: These origins represent references to existing values. They are typically used for accessing or modifying existing data. Examples
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'` and `'constant-ref'`.
- **Argument Origins**: This group is dedicated to exactly one origin: 'argument'. It represents values that are passed as arguments to functions.
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,7 @@ const ioLayout = tgpu.bindGroupLayout({
outTexture: { storageTexture: d.textureStorage2d('rgba8unorm') },
});

const tileData = tgpu.workgroupVar(
d.arrayOf(d.arrayOf(d.vec3f, 128), 4),
);
const tileData = tgpu.workgroupVar(d.arrayOf(d.arrayOf(d.vec3f, 128), 4));

const computeFn = tgpu['~unstable'].computeFn({
in: {
Expand Down
88 changes: 31 additions & 57 deletions apps/typegpu-docs/src/examples/rendering/3d-fish/compute.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,12 @@
import * as d from 'typegpu/data';
import * as std from 'typegpu/std';
import * as p from './params.ts';
import { computeBindGroupLayout as layout, ModelData } from './schemas.ts';
import { computeBindGroupLayout as layout } from './schemas.ts';
import { projectPointOnLine } from './tgsl-helpers.ts';

export const simulate = (fishIndex: number) => {
'use gpu';
// TODO: replace it with struct copy when Chromium is fixed
const fishData = ModelData({
position: layout.$.currentFishData[fishIndex].position,
direction: layout.$.currentFishData[fishIndex].direction,
scale: layout.$.currentFishData[fishIndex].scale,
variant: layout.$.currentFishData[fishIndex].variant,
applySeaDesaturation:
layout.$.currentFishData[fishIndex].applySeaDesaturation,
applySeaFog: layout.$.currentFishData[fishIndex].applySeaFog,
applySinWave: layout.$.currentFishData[fishIndex].applySinWave,
});
const fishData = layout.$.currentFishData[fishIndex];
let separation = d.vec3f();
let alignment = d.vec3f();
let alignmentCount = 0;
Expand All @@ -30,34 +20,22 @@ export const simulate = (fishIndex: number) => {
continue;
}

// TODO: replace it with struct copy when Chromium is fixed
const other = ModelData({
position: layout.$.currentFishData[i].position,
direction: layout.$.currentFishData[i].direction,
scale: layout.$.currentFishData[i].scale,
variant: layout.$.currentFishData[i].variant,
applySeaDesaturation: layout.$.currentFishData[i].applySeaDesaturation,
applySeaFog: layout.$.currentFishData[i].applySeaFog,
applySinWave: layout.$.currentFishData[i].applySinWave,
});
const dist = std.length(std.sub(fishData.position, other.position));
const other = layout.$.currentFishData[i];
const dist = std.length(fishData.position.sub(other.position));
if (dist < layout.$.fishBehavior.separationDist) {
separation = std.add(
separation,
std.sub(fishData.position, other.position),
);
separation = separation.add(fishData.position.sub(other.position));
}
if (dist < layout.$.fishBehavior.alignmentDist) {
alignment = std.add(alignment, other.direction);
alignment = alignment.add(other.direction);
alignmentCount = alignmentCount + 1;
}
if (dist < layout.$.fishBehavior.cohesionDist) {
cohesion = std.add(cohesion, other.position);
cohesion = cohesion.add(other.position);
cohesionCount = cohesionCount + 1;
}
}
if (alignmentCount > 0) {
alignment = std.mul(1 / d.f32(alignmentCount), alignment);
alignment = alignment.mul(1 / d.f32(alignmentCount));
}
if (cohesionCount > 0) {
cohesion = std.sub(
Expand All @@ -75,12 +53,12 @@ export const simulate = (fishIndex: number) => {

if (axisPosition > axisAquariumSize - distance) {
const str = axisPosition - (axisAquariumSize - distance);
wallRepulsion = std.sub(wallRepulsion, std.mul(str, repulsion));
wallRepulsion = wallRepulsion.sub(repulsion.mul(str));
}

if (axisPosition < -axisAquariumSize + distance) {
const str = -axisAquariumSize + distance - axisPosition;
wallRepulsion = std.add(wallRepulsion, std.mul(str, repulsion));
wallRepulsion = wallRepulsion.add(repulsion.mul(str));
}
}

Expand All @@ -89,42 +67,38 @@ export const simulate = (fishIndex: number) => {
fishData.position,
layout.$.mouseRay.line,
);
const diff = std.sub(fishData.position, proj);
const diff = fishData.position.sub(proj);
const limit = p.fishMouseRayRepulsionDistance;
const str = std.pow(2, std.clamp(limit - std.length(diff), 0, limit)) - 1;
rayRepulsion = std.mul(str, std.normalize(diff));
rayRepulsion = std.normalize(diff).mul(str);
}

fishData.direction = std.add(
fishData.direction,
std.mul(layout.$.fishBehavior.separationStr, separation),
let direction = d.vec3f(fishData.direction);

direction = direction.add(
separation.mul(layout.$.fishBehavior.separationStr),
);
fishData.direction = std.add(
fishData.direction,
std.mul(layout.$.fishBehavior.alignmentStr, alignment),
direction = direction.add(
alignment.mul(layout.$.fishBehavior.alignmentStr),
);
fishData.direction = std.add(
fishData.direction,
std.mul(layout.$.fishBehavior.cohesionStr, cohesion),
direction = direction.add(
cohesion.mul(layout.$.fishBehavior.cohesionStr),
);
fishData.direction = std.add(
fishData.direction,
std.mul(p.fishWallRepulsionStrength, wallRepulsion),
direction = direction.add(
wallRepulsion.mul(p.fishWallRepulsionStrength),
);
fishData.direction = std.add(
fishData.direction,
std.mul(p.fishMouseRayRepulsionStrength, rayRepulsion),
direction = direction.add(
rayRepulsion.mul(p.fishMouseRayRepulsionStrength),
);

fishData.direction = std.mul(
std.clamp(std.length(fishData.direction), 0.0, 0.01),
std.normalize(fishData.direction),
direction = std.normalize(direction).mul(
std.clamp(std.length(fishData.direction), 0, 0.01),
);

const translation = std.mul(
const translation = direction.mul(
d.f32(std.min(999, layout.$.timePassed)) / 8,
fishData.direction,
);
fishData.position = std.add(fishData.position, translation);
layout.$.nextFishData[fishIndex] = fishData;

const nextFishData = layout.$.nextFishData[fishIndex];
nextFishData.position = fishData.position.add(translation);
nextFishData.direction = d.vec3f(direction);
};
Loading