Skip to content

Commit 07ee6b7

Browse files
committed
Writing internal docs about shader generation
1 parent dbcd394 commit 07ee6b7

File tree

2 files changed

+136
-0
lines changed

2 files changed

+136
-0
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: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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 the 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

0 commit comments

Comments
 (0)