Skip to content

Commit d6d6c08

Browse files
authored
docs: Create a shader compilation tool (#1466)
1 parent 3d074ea commit d6d6c08

File tree

8 files changed

+438
-1
lines changed

8 files changed

+438
-1
lines changed

apps/typegpu-docs/astro.config.mjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import starlightBlog from 'starlight-blog';
99
import starlightTypeDoc, { typeDocSidebarGroup } from 'starlight-typedoc';
1010
import typegpu from 'unplugin-typegpu/rollup';
1111
import { imagetools } from 'vite-imagetools';
12+
import wasm from 'vite-plugin-wasm';
1213

1314
/**
1415
* @template T
@@ -26,10 +27,16 @@ export default defineConfig({
2627
vite: {
2728
// Allowing query params, for invalidation
2829
plugins: [
30+
wasm(),
2931
tailwindVite(),
3032
typegpu({ include: [/\.m?[jt]sx?/] }),
3133
/** @type {any} */ imagetools(),
3234
],
35+
ssr: {
36+
noExternal: [
37+
'wgsl-wasm-transpiler-bundler',
38+
],
39+
},
3340
},
3441
integrations: [
3542
starlight({

apps/typegpu-docs/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@
5252
"typegpu": "workspace:*",
5353
"typescript": "catalog:types",
5454
"unplugin-typegpu": "workspace:*",
55-
"wgpu-matrix": "catalog:example"
55+
"wgpu-matrix": "catalog:example",
56+
"wgsl-wasm-transpiler-bundler": "^0.0.1"
5657
},
5758
"devDependencies": {
5859
"@observablehq/plot": "^0.6.17",
@@ -66,6 +67,7 @@
6667
"tailwindcss": "^4.1.7",
6768
"tailwindcss-motion": "^1.0.1",
6869
"vite-imagetools": "catalog:frontend",
70+
"vite-plugin-wasm": "^3.4.1",
6971
"yaml": "^2.8.0"
7072
}
7173
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
import { Image } from 'astro:assets';
3+
import PageLayout from '../../layouts/PageLayout.astro';
4+
import TranslatorApp from './translator-app';
5+
import TypeGPULogoDark from '../../assets/typegpu-logo-dark.svg';
6+
---
7+
8+
<PageLayout title="WGSL Translator | TypeGPU" theme="dark">
9+
<h1 class="w-full flex items-center justify-center">
10+
<Image
11+
src={TypeGPULogoDark}
12+
alt="TypeGPU Logo"
13+
class="w-40 relative"
14+
/>
15+
<p class="text-lg font-sans">— translator</p>
16+
</h1>
17+
18+
<TranslatorApp client:only="react" />
19+
</PageLayout>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
export const DEFAULT_WGSL = `@vertex
2+
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> @builtin(position) vec4<f32> {
3+
let pos = array<vec2<f32>, 3>(
4+
vec2<f32>(-0.5, -0.5),
5+
vec2<f32>( 0.5, -0.5),
6+
vec2<f32>( 0.0, 0.5)
7+
);
8+
return vec4<f32>(pos[vertex_index], 0.0, 1.0);
9+
}
10+
11+
@fragment
12+
fn fs_main() -> @location(0) vec4<f32> {
13+
return vec4<f32>(1.0, 0.0, 0.0, 1.0);
14+
}`;
15+
16+
export const LANGUAGE_MAP: Record<string, string> = {
17+
wgsl: 'wgsl',
18+
glsl: 'cpp',
19+
hlsl: 'cpp',
20+
metal: 'cpp',
21+
spirv: 'plaintext',
22+
'spirv-asm': 'plaintext',
23+
};
24+
25+
export const commonEditorOptions = {
26+
minimap: { enabled: false },
27+
fontSize: 14,
28+
fontFamily: 'Monaco, "Cascadia Code", "Roboto Mono", monospace',
29+
wordWrap: 'off' as const,
30+
scrollBeyondLastLine: false,
31+
automaticLayout: true,
32+
tabSize: 2,
33+
renderWhitespace: 'selection' as const,
34+
lineNumbers: 'on' as const,
35+
folding: true,
36+
bracketPairColorization: { enabled: true },
37+
} as const;
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { useCallback, useEffect, useReducer } from 'react';
2+
import { DEFAULT_WGSL } from './constants.ts';
3+
import { compile, getErrorMessage, initializeWasm } from './wgslTool.ts';
4+
5+
type State = {
6+
status: 'initializing' | 'ready' | 'compiling' | 'success' | 'error';
7+
errorMessage?: string;
8+
formats: string[];
9+
wgslCode: string;
10+
output: string;
11+
format: string;
12+
loadingEditor: boolean;
13+
};
14+
15+
type Action =
16+
| { type: 'INIT_SUCCESS'; payload: string[] }
17+
| { type: 'INIT_FAILURE'; payload: string }
18+
| { type: 'COMPILE_START' }
19+
| { type: 'COMPILE_SUCCESS'; payload: string }
20+
| { type: 'COMPILE_FAILURE'; payload: string }
21+
| { type: 'SET_WGSL_CODE'; payload: string }
22+
| { type: 'SET_FORMAT'; payload: string }
23+
| { type: 'EDITOR_LOADED' };
24+
25+
const getInitialState = (): State => {
26+
const persistedFormat = typeof window !== 'undefined'
27+
? localStorage.getItem('translator_format')
28+
: null;
29+
return {
30+
status: 'initializing',
31+
formats: [],
32+
wgslCode: DEFAULT_WGSL,
33+
output: '',
34+
format: persistedFormat || 'glsl',
35+
loadingEditor: true,
36+
};
37+
};
38+
39+
function reducer(state: State, action: Action): State {
40+
switch (action.type) {
41+
case 'INIT_SUCCESS':
42+
return {
43+
...state,
44+
status: 'ready',
45+
formats: action.payload,
46+
};
47+
case 'INIT_FAILURE':
48+
return {
49+
...state,
50+
status: 'error',
51+
errorMessage: action.payload,
52+
};
53+
case 'COMPILE_START':
54+
return {
55+
...state,
56+
status: 'compiling',
57+
};
58+
case 'COMPILE_SUCCESS':
59+
return {
60+
...state,
61+
status: 'success',
62+
output: action.payload,
63+
};
64+
case 'COMPILE_FAILURE':
65+
return {
66+
...state,
67+
status: 'error',
68+
errorMessage: action.payload,
69+
output: '',
70+
};
71+
case 'SET_WGSL_CODE':
72+
return { ...state, wgslCode: action.payload };
73+
case 'SET_FORMAT':
74+
return { ...state, format: action.payload, output: '' };
75+
case 'EDITOR_LOADED':
76+
return { ...state, loadingEditor: false };
77+
default:
78+
return state;
79+
}
80+
}
81+
82+
export function useShaderTranslator() {
83+
const [state, dispatch] = useReducer(reducer, undefined, getInitialState);
84+
const {
85+
status,
86+
errorMessage,
87+
formats,
88+
wgslCode,
89+
output,
90+
format,
91+
loadingEditor,
92+
} = state;
93+
94+
useEffect(() => {
95+
try {
96+
const fmts = initializeWasm();
97+
dispatch({ type: 'INIT_SUCCESS', payload: fmts });
98+
} catch (err) {
99+
dispatch({ type: 'INIT_FAILURE', payload: getErrorMessage(err) });
100+
}
101+
}, []);
102+
103+
useEffect(() => {
104+
localStorage.setItem('translator_format', format);
105+
}, [format]);
106+
107+
const handleCompile = useCallback(() => {
108+
if (status === 'compiling') return;
109+
dispatch({ type: 'COMPILE_START' });
110+
try {
111+
const result = compile(wgslCode, format);
112+
dispatch({ type: 'COMPILE_SUCCESS', payload: result });
113+
} catch (err) {
114+
dispatch({ type: 'COMPILE_FAILURE', payload: getErrorMessage(err) });
115+
}
116+
}, [wgslCode, format, status]);
117+
118+
const setWgslCode = (code: string) => {
119+
dispatch({ type: 'SET_WGSL_CODE', payload: code });
120+
};
121+
122+
const setFormat = (newFormat: string) => {
123+
dispatch({ type: 'SET_FORMAT', payload: newFormat });
124+
};
125+
126+
const setEditorLoaded = () => {
127+
dispatch({ type: 'EDITOR_LOADED' });
128+
};
129+
130+
const canCompile = formats.length > 0 && !loadingEditor &&
131+
status !== 'compiling';
132+
133+
return {
134+
status,
135+
errorMessage,
136+
formats,
137+
wgslCode,
138+
output,
139+
format,
140+
loadingEditor,
141+
canCompile,
142+
setWgslCode,
143+
setFormat,
144+
setEditorLoaded,
145+
handleCompile,
146+
};
147+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {
2+
compileShader,
3+
getSupportedFormats,
4+
init,
5+
} from 'wgsl-wasm-transpiler-bundler';
6+
7+
/** Normalize any thrown value into a readable string */
8+
export function getErrorMessage(err: unknown): string {
9+
if (typeof err === 'string') return err;
10+
if (err instanceof Error) return err.message;
11+
return String(err);
12+
}
13+
14+
export function initializeWasm() {
15+
init();
16+
return getSupportedFormats();
17+
}
18+
19+
export function compile(wgslCode: string, format: string) {
20+
return compileShader(wgslCode, format);
21+
}

0 commit comments

Comments
 (0)