GPU-Shader in TypeScript schreiben mit TypeGPU
Erstellen Sie GPU-Shader mit TypeGPU in TypeScript. Richten Sie typisierte Puffer ein, führen Sie einen Compute-Shader aus und erkennen Sie Bindungsfehler beim Kompilieren.
TypeGPU ist eine TypeScript-Bibliothek, die WebGPU in typisierte Primitive kapselt. Dadurch prüft der TypeScript-Compiler Ihre Buffer-Schemas, Bind-Group-Layouts und GPU-Funktionen, statt dass solche Fehler erst zur Laufzeit auf der GPU auftreten.
Wenn Sie schon ein WebGPU-Tutorial durchgearbeitet haben, kennen Sie das: Die JavaScript-Seite besteht die Typprüfung problemlos, doch im WGSL-String ist ein Struct-Feld um einen Float verschoben, und zurück bekommen Sie lediglich einen Buffer voller Datenmüll, ohne jede Fehlermeldung. Diese Anleitung setzt voraus, dass Sie bereits wissen, was WebGPU ist. Falls nicht, beginnen Sie mit WebGPU-Effekte in eine UI einbauen und wie sich WebGPU von WebGL unterscheidet. Hier richten Sie TypeGPU ein, erstellen typisierte Buffer, führen einen Compute-Shader von Anfang bis Ende aus und sehen den Typfehler, den Ihnen reines WebGPU nicht liefern kann.
Das Wichtigste in Kürze
- Um einen TypeGPU-Shader als TypeScript-Funktion zu schreiben, markieren Sie ihn mit
'use gpu'und fügen das Build-Pluginunplugin-typegpuhinzu, das ansonsten ein optionaler Bestandteil der Bibliothek ist. - Jeder TypeGPU-Buffer wird aus einem Schema wie
d.arrayOf(d.u32, 16)erstellt. Das Schema legt die Bytegröße des Buffers fest, bestimmt, wie JavaScript-Werte in Binärdaten umgewandelt werden, und definiert, welche Werte.write()akzeptiert. - In einem WGSL-Array belegt jedes
vec3f-Element 16 Byte. TypeGPU fügt das Padding bei einfachen Arrays und Objekten automatisch ein, kopiert einFloat32Arrayjedoch Byte für Byte, sodass Sie das Padding dort selbst ergänzen müssen. - Übergeben Sie
root.createBindGroupeinen Buffer, dessen Schema oder Usage nicht zum Layout passt, lehnt TypeScript ihn bereits zur Kompilierzeit ab. - TypeGPU befindet sich noch vor Version 1.0 (Release-Linie 0.12.x), und Minor-Releases haben bereits veraltete APIs entfernt.
Warum WGSL und die Grenze zwischen JS und GPU die meisten WebGPU-Bugs verursachen
Die meisten WebGPU-Bugs entstehen nicht in der Shader-Logik, sondern an der Schnittstelle. WebGPU-Shader werden in WGSL geschrieben, einer eigenständigen Sprache, die Ihr TypeScript-Tooling als undurchsichtigen String behandelt. Das Byte-Layout Ihrer Buffer, die numerischen Binding-Indizes und die Übereinstimmung zwischen jeder Bind Group und den Deklarationen im Shader werden allesamt manuell synchron gehalten. Ist eines davon falsch, erhalten Sie in der Regel stillschweigend fehlerhafte Daten statt eines Compilerfehlers.
TypeGPU bringt beide Seiten in TypeScript zusammen. Ein einziges Schema beschreibt die Daten auf der CPU und auf der GPU, und auch Shader-Funktionen werden in TypeScript geschrieben.
TypeGPU mit Vite einrichten
Installieren Sie die Runtime sowie anschließend das Build-Plugin und die WebGPU-Typdefinitionen als Dev-Dependencies:
npm install typegpu
npm install --save-dev unplugin-typegpu @webgpu/types
Registrieren Sie die WebGPU-Typen in tsconfig.json. Falls TypeScript WebGPU-Typen wie GPUDevice nicht kennt, verweist der Troubleshooting-Abschnitt des Getting-Started-Guides auf genau diese Einstellung.
{ "compilerOptions": { "types": ["@webgpu/types"] } }
unplugin-typegpu ist für TypeGPU insgesamt optional. Sie benötigen es, um Shader als 'use gpu'-TypeScript-Funktionen zu schreiben. Da es auf unplugin basiert, gibt es neben Vite auch Varianten für esbuild, Farm, Rolldown, Rollup, Rspack und webpack sowie ein separates Babel-Plugin für React Native.
// vite.config.ts
import { defineConfig } from 'vite';
import typegpu from 'unplugin-typegpu/vite';
export default defineConfig({
plugins: [typegpu()],
});
Zur Laufzeit liefert Ihnen await tgpu.init() ein root-Objekt. Alle im Folgenden verwendeten Buffer, Bind Groups und Pipelines werden daraus erstellt.
Typisierte Buffer und Schemas
Ein TypeGPU-Buffer entsteht immer aus einem Schema, das mit d.*-Konstruktoren aufgebaut wird. Wie die TypeGPU-Referenz zu Buffern erläutert, ermittelt TypeGPU anhand des Schemas, wie groß der Buffer sein muss, wie JavaScript-Werte in Bytes und zurück umgewandelt werden und welche Typen die Methoden .write() und .read() erwarten.
import { tgpu, d } from 'typegpu';
const root = await tgpu.init();
const Point = d.struct({
position: d.vec3f,
weight: d.f32,
});
const points = root
.createBuffer(d.arrayOf(Point, 2), [
{ position: d.vec3f(0, 0, 0), weight: 1 },
{ position: d.vec3f(1, 2, 3), weight: 0.5 },
])
.$usage('storage');
points.write([
{ position: d.vec3f(4, 5, 6), weight: 2 },
{ position: d.vec3f(7, 8, 9), weight: 3 },
]);
console.log(await points.read());
.$usage('storage') setzt das WebGPU-Usage-Flag und hinterlegt es zugleich im Typ des Buffers. Lässt sich ein Buffer nicht mappen, kopiert .read() seinen Inhalt über einen temporären Staging-Buffer, sodass Sie mapAsync nie selbst aufrufen müssen.
Gemäß den WGSL-Alignment-Regeln belegt ein vec3f-Array-Element 16 Byte: 12 Byte Daten und 4 Byte Padding. Übergeben Sie einfache Arrays oder Objekte an .write(), fügt TypeGPU dieses Padding automatisch ein. Übergeben Sie dagegen ein Float32Array, kopiert .write() dessen Bytes unverändert, sodass das Array das Padding bereits enthalten muss:
const dirs = root.createBuffer(d.arrayOf(d.vec3f, 2)).$usage('storage');
dirs.write([[1, 0, 0], [0, 1, 0]]); // padded for you
dirs.write(new Float32Array([1, 0, 0, 0, 0, 1, 0, 0])); // every 4th float is padding
Ein Compute-Shader von Anfang bis Ende
Das folgende vollständige TypeGPU-Programm verdoppelt ein Array aus acht Ganzzahlen auf der GPU und liest das Ergebnis zurück. Eine 'use gpu'-Funktion, die an root.createGuardedComputePipeline übergeben wird, erhält einen Thread-Index. Der Aufruf dispatchThreads(n) startet n logische Threads, wobei der Index jeden Wert von 0 bis n − 1 genau einmal annimmt.
import { tgpu, d } from 'typegpu';
const root = await tgpu.init();
const N = 8;
const values = root
.createBuffer(d.arrayOf(d.u32, N), [1, 2, 3, 4, 5, 6, 7, 8])
.$usage('storage');
const layout = tgpu.bindGroupLayout({
values: { storage: d.arrayOf(d.u32, N), access: 'mutable' },
});
const bindGroup = root.createBindGroup(layout, { values });
const double = root.createGuardedComputePipeline((x) => {
'use gpu';
if (x >= N) {
return;
}
layout.$.values[x] = layout.$.values[x] + layout.$.values[x];
});
double.with(bindGroup).dispatchThreads(N);
console.log(await values.read()); // [2, 4, 6, 8, 10, 12, 14, 16]
Das Layout deklariert, was der Shader erwartet, und die Bind Group liefert den konkreten Buffer. Sie müssen weder eine Workgroup-Anzahl berechnen noch einen Staging-Buffer schreiben. Wenn Sie keine Buffer austauschen müssen, ist root.createMutable(schema) eine kürzere Alternative mit fest gebundener Ressource: Der Shader greift über .$ darauf zu, und die Ergebnisse lesen Sie mit .read() zurück.
Bind-Group-Layouts und Typsicherheit zur Kompilierzeit
Die Prüfung von Bind Groups zur Kompilierzeit ist der Punkt, an dem sich TypeGPU am deutlichsten von reinem WebGPU unterscheidet. Ein Bind-Group-Layout beschreibt die Ressourcen, die ein Shader erwartet, und eine Bind Group stellt die konkreten Buffer bereit. In TypeGPU scheitert ein Buffer mit falschem Schema oder falschen Usage-Flags bereits an der Typprüfung, sobald Sie die Bind Group erstellen. Der Guide zu Bind Groups dokumentiert dieses Verhalten.
Storage-Einträge sind standardmäßig schreibgeschützt (readonly), weshalb das obige Layout access: 'mutable' setzt. Da der Shader auf layout.$ statt auf einen bestimmten Buffer verweist, kann eine einzige Pipeline mit unterschiedlichen Daten arbeiten:
const more = root
.createBuffer(d.arrayOf(d.u32, N), [10, 20, 30, 40, 50, 60, 70, 80])
.$usage('storage');
const bindGroupB = root.createBindGroup(layout, { values: more });
double.with(bindGroupB).dispatchThreads(N); // same shader, new data
const floats = root.createBuffer(d.arrayOf(d.f32, N)).$usage('storage');
root.createBindGroup(layout, {
values: floats, // type error: d.f32 elements don't match the d.u32 layout entry
});
Bind Groups sind unveränderlich. Erstellen Sie eine pro Ressourcenkombination und cachen Sie sie, wenn Sie nur eine kleine, feste Menge davon haben. In reinem WebGPU weiß der Compiler nichts über die WGSL-Deklarationen. Bindet man dort einen Float-Buffer an eine Stelle, an der der Shader u32 erwartet, kompiliert der Code fehlerfrei und liefert stillschweigend falsche Zahlen.
Derselbe Shader in WGSL und in TypeGPU
Ein TypeGPU-Shader und sein handgeschriebenes WGSL-Pendant sind etwa gleich groß. Der Unterschied liegt im umgebenden Host-Code. Die handgeschriebene WGSL-Version des Verdopplungs-Shaders sieht so aus:
@group(0) @binding(0) var<storage, read_write> values: array<u32>;
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) id: vec3u) {
if (id.x >= arrayLength(&values)) {
return;
}
values[id.x] = values[id.x] * 2u;
}
Und hier die TypeGPU-Version aus dem vorherigen Beispiel:
const double = root.createGuardedComputePipeline((x) => {
'use gpu';
if (x >= N) {
return;
}
layout.$.values[x] = layout.$.values[x] + layout.$.values[x];
});
Um die WGSL-Version mit reinem WebGPU auszuführen, müssen Sie den gesamten folgenden Host-Code von Hand schreiben:
device.createShaderModulemit dem WGSL-Quellcode als Stringdevice.createComputePipeline- Bind-Group-Einträge mit numerischen
binding-Indizes, die zu@binding(0)passen müssen - eine Berechnung der Workgroup-Anzahl für
dispatchWorkgroups - einen
MAP_READ-Staging-Buffer, einen Copy-Befehl undmapAsync, um die Ergebnisse zurückzulesen
Mit TypeGPU ersetzt das Layout-Objekt die numerischen Indizes, und .read() übernimmt das Staging.
Grenzen, Ausweichmöglichkeiten und Browser-Unterstützung
TypeGPU befindet sich noch vor Version 1.0. Die Release-Linie 0.12 erschien am 12. August 2026, und Minor-Versionen haben bereits veraltete APIs entfernt. Lesen Sie daher vor einem Upgrade den Migrationsleitfaden für 0.12 sowie die Leitfäden späterer Versionen. Die Bibliothek steht unter MIT-Lizenz und wird von Software Mansion entwickelt.
Bei Bedarf auf reines WebGPU zurückgreifen. Sie können einen bestehenden GPUBuffer mit root.createBuffer(schema, existingBuffer) kapseln, um typisierte Lese- und Schreibzugriffe zu erhalten. Für den Lebenszyklus dieses Buffers sind Sie dann selbst verantwortlich und müssen sicherstellen, dass sein Inhalt dem Schema entspricht. Damit Sie .write() aufrufen können, benötigt er zudem COPY_DST:
const raw = root.device.createBuffer({
size: 4,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
});
const counter = root.createBuffer(d.u32, raw);
counter.write(42);
Indizes absichern. WebGPU verhindert zwar, dass Zugriffe außerhalb der Grenzen Speicher jenseits des Bindings erreichen, macht sie dadurch aber nicht korrekt. Sichern Sie den Index immer dann ab, wenn die Dispatch-Größe die Array-Länge übersteigen kann.
Threadübergreifende Lesezugriffe vermeiden. GPU-Threads laufen in keiner garantierten Reihenfolge. Liest ein Thread ein Element, das ein anderer Thread im selben Dispatch schreibt, ist das Ergebnis nichtdeterministisch:
// Nondeterministic: thread x reads index x + 1 while thread x + 1 may be writing it
const smear = root.createGuardedComputePipeline((x) => {
'use gpu';
if (x + 1 >= N) {
return;
}
layout.$.values[x] = layout.$.values[x] + layout.$.values[x + 1];
});
Außerdem begrenzt WebGPU die Anzahl der Workgroups, die entlang einer einzelnen Achse dispatcht werden können. Verteilen Sie die Arbeit bei großen Grids auf x und y.
Browser-Unterstützung. TypeGPU läuft überall dort, wo auch WebGPU läuft. Prüfen Sie die aktuelle WebGPU-Unterstützung auf caniuse für Ihre Zielgruppe.
Wissen, wann man es nicht einsetzen sollte. Die Dokumentation von TypeGPU selbst empfiehlt ein Rendering-Framework, wenn Sie ein interaktives 3D-Modell auf einer Seite darstellen möchten. Für diese Aufgabe eignet sich 3D-Animationen mit Three.js erstellen besser.
Fazit
TypeGPU behebt den Teil von WebGPU, in dem Bugs am schwersten aufzuspüren sind, nämlich die Grenze zwischen JavaScript-Daten und Shader-Speicher, indem es beide Seiten mit einem einzigen TypeScript-Schema beschreibt und jedes Binding einer Typprüfung unterzieht. Fügen Sie zunächst unplugin-typegpu zu einem Vite-Projekt hinzu und führen Sie das Verdopplungsbeispiel aus. Übergeben Sie anschließend testweise einen unpassenden Buffer an createBindGroup, um zu sehen, wie der Compiler den Fehler abfängt.
FAQs
Kann ich meine bestehenden WGSL-Shader mit TypeGPU verwenden?
Ja. Das CLI-Tool tgpu-gen liest bestehende WGSL-Shader-Dateien ein und generiert TypeScript-Dateien mit den passenden TypeGPU-Definitionen. Es unterstützt Glob-Patterns sowie einen Watch-Modus, der die Definitionen neu generiert, sobald sich die Shader ändern. Zudem akzeptieren TypeGPU-Function-Shells ihre Implementierung entweder als WGSL-String oder als JavaScript-Funktion, sodass Sie eine Codebasis Funktion für Funktion migrieren können.
Kann eine 'use gpu'-Funktion auch auf der CPU ausgeführt werden?
Ja. Eine mit 'use gpu' markierte Funktion ist weiterhin eine normale TypeScript-Funktion. Sie können sie also direkt in JavaScript aufrufen und erhalten ein Ergebnis zurück. Ebenso können Sie sie an tgpu.resolve übergeben, um WGSL zu generieren, oder sie über eine Pipeline auf der GPU ausführen. So lassen sich Hilfsfunktionen für Shader mit Ihrem gewohnten JavaScript-Test-Runner per Unit-Test prüfen, bevor sie auf der GPU laufen.
Wie erhalte ich das generierte WGSL aus TypeGPU?
Rufen Sie tgpu.resolve mit den benötigten TypeGPU-Ressourcen auf, etwa 'use gpu'-Funktionen und Schemas. Die Funktion gibt einen einsatzbereiten WGSL-String zurück, der sämtliche transitiven Abhängigkeiten enthält. Das ist nützlich, wenn eine andere WebGPU-Engine oder ein Material-System reines WGSL erwartet. Wenn Sie TypeGPU-Pipelines verwenden, müssen Sie resolve nicht selbst aufrufen, da diese den Shader intern auflösen und kompilieren.
Funktioniert TypeGPU in React Native?
Ja. TypeGPU läuft in React Native über das Paket react-native-webgpu. Um 'use gpu'-Shader zu schreiben, fügen Sie unplugin-typegpu/babel zur plugins-Liste in babel.config.js hinzu und leeren bei Bedarf anschließend den Metro-Cache. React Native WebGPU läuft nicht in Expo Go, daher benötigt ein Expo-Projekt expo prebuild. Im iOS-Simulator müssen Sie außerdem die Metal API Validation in Xcode deaktivieren.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k