Escribir shaders de GPU en TypeScript con TypeGPU
Crea shaders GPU en TypeScript con TypeGPU. Configura buffers tipados, ejecuta un shader de cómputo y detecta incompatibilidades al compilar.
TypeGPU es una biblioteca de TypeScript que envuelve WebGPU en primitivas tipadas. Así, el compilador de TypeScript verifica tus esquemas de buffers, los layouts de bind groups y las funciones de GPU, en lugar de que esos errores aparezcan en tiempo de ejecución en la GPU.
Si has seguido algún tutorial de WebGPU, ya sabes cómo va esto. El lado de JavaScript pasa la verificación de tipos sin problemas, pero luego el string de WGSL tiene un campo de struct desplazado un float, y lo único que obtienes es un buffer lleno de basura sin ningún error. Esta guía da por hecho que ya sabes qué es WebGPU. Si no es así, empieza por añadir efectos de WebGPU a una UI y por en qué se diferencia WebGPU de WebGL. Aquí configurarás TypeGPU, crearás buffers tipados, ejecutarás un compute shader de principio a fin y verás el error de tipos que WebGPU puro no puede ofrecerte.
Puntos clave
- Para escribir un shader de TypeGPU como una función de TypeScript, la marcas con
'use gpu'y añades el plugin de buildunplugin-typegpu, que por lo demás es una parte opcional de la biblioteca. - Todo buffer de TypeGPU se crea a partir de un esquema como
d.arrayOf(d.u32, 16). El esquema define el tamaño en bytes del buffer, cómo se convierten los valores de JavaScript a binario y qué valores acepta.write(). - En un array de WGSL, cada elemento
vec3focupa 16 bytes. TypeGPU añade el relleno (padding) por ti en los arrays y objetos simples, pero copia unFloat32Arraybyte a byte, así que tienes que añadir el relleno tú mismo. - Si pasas a
root.createBindGroupun buffer cuyo esquema o uso no coincide con el layout, TypeScript lo rechaza en tiempo de compilación. - TypeGPU está en fase previa a la 1.0 (la línea 0.12.x), y algunas versiones menores han eliminado APIs obsoletas.
Por qué WGSL y la frontera entre JS y GPU causan la mayoría de los bugs de WebGPU
La mayoría de los bugs de WebGPU no provienen de la lógica del shader, sino de la frontera. Los shaders de WebGPU se escriben en WGSL, un lenguaje independiente que tus herramientas de TypeScript tratan como un string opaco. La disposición en bytes de tus buffers, los índices numéricos de binding y la correspondencia entre cada bind group y lo que declara el shader se mantienen sincronizados a mano. Si te equivocas en uno, lo habitual es obtener datos incorrectos de forma silenciosa en lugar de un error del compilador.
TypeGPU lleva ambos lados a TypeScript. Un único esquema describe los datos tanto en la CPU como en la GPU, y las funciones de shader también se escriben en TypeScript.
Configurar TypeGPU con Vite
Instala el runtime y, después, el plugin de build y las definiciones de tipos de WebGPU como dependencias de desarrollo:
npm install typegpu
npm install --save-dev unplugin-typegpu @webgpu/types
Registra los tipos de WebGPU en tsconfig.json. Si TypeScript no reconoce tipos de WebGPU como GPUDevice, la sección de solución de problemas de la guía de primeros pasos te remite a esta configuración.
{ "compilerOptions": { "types": ["@webgpu/types"] } }
unplugin-typegpu es opcional para TypeGPU en su conjunto, pero lo necesitas para escribir shaders como funciones de TypeScript con 'use gpu'. Está construido sobre unplugin, así que, además de Vite, ofrece versiones para esbuild, Farm, Rolldown, Rollup, Rspack y webpack, y un plugin de Babel independiente para React Native.
// vite.config.ts
import { defineConfig } from 'vite';
import typegpu from 'unplugin-typegpu/vite';
export default defineConfig({
plugins: [typegpu()],
});
En tiempo de ejecución, await tgpu.init() te devuelve un objeto root. Todos los buffers, bind groups y pipelines que verás a continuación se crean a partir de él.
Buffers tipados y esquemas
Un buffer de TypeGPU siempre se crea a partir de un esquema construido con los constructores d.*. Como explica la referencia de buffers de TypeGPU, TypeGPU usa el esquema para calcular qué tamaño debe tener el buffer, cómo convertir valores de JavaScript en bytes y viceversa, y qué tipos aceptan sus métodos .write() y .read().
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') establece el flag de uso de WebGPU y, además, lo registra en el tipo del buffer. Cuando un buffer no se puede mapear, .read() copia su contenido a través de un staging buffer temporal, de modo que nunca tienes que escribir mapAsync tú mismo.
Las reglas de alineación de WGSL hacen que cada elemento de un array de vec3f ocupe 16 bytes: 12 bytes de datos y 4 de relleno. Si pasas arrays u objetos simples a .write(), TypeGPU inserta ese relleno por ti. Si pasas un Float32Array, .write() copia sus bytes tal cual, así que el array ya debe incluir el relleno:
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
Un compute shader de principio a fin
El siguiente programa completo de TypeGPU duplica un array de ocho enteros en la GPU y lee el resultado de vuelta. Una función 'use gpu' que se pasa a root.createGuardedComputePipeline recibe un índice de hilo. Al llamar a dispatchThreads(n) se ejecutan n hilos lógicos, y el índice toma cada valor de 0 a n − 1 exactamente una vez.
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]
El layout declara lo que espera el shader y el bind group proporciona el buffer real. En ningún momento calculas el número de workgroups ni escribes un staging buffer. Si no necesitas intercambiar buffers, root.createMutable(schema) es una alternativa más concisa con un recurso fijo: el shader lo lee a través de .$ y tú lees los resultados con .read().
Layouts de bind groups y seguridad de tipos en tiempo de compilación
La verificación de bind groups en tiempo de compilación es donde TypeGPU más se aleja de WebGPU puro. Un layout de bind group describe los recursos que espera un shader, y un bind group proporciona los buffers concretos. En TypeGPU, un buffer con un esquema o unos flags de uso incorrectos no supera la verificación de tipos al crear el bind group. La guía de bind groups documenta este comportamiento.
Las entradas de storage son de solo lectura por defecto; por eso el layout anterior establece access: 'mutable'. Como el shader hace referencia a layout.$ y no a un buffer concreto, un mismo pipeline puede ejecutarse sobre datos distintos:
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
});
Los bind groups son inmutables. Crea uno por cada combinación de recursos y guárdalos en caché si tienes un conjunto pequeño y fijo. En WebGPU puro, el compilador no tiene ni idea de lo que declara el WGSL, así que vincular un buffer de floats donde el shader espera u32 compila sin problemas y produce números erróneos de forma silenciosa.
El mismo shader en WGSL frente a TypeGPU
Un shader de TypeGPU y su equivalente escrito a mano en WGSL tienen más o menos el mismo tamaño. La diferencia está en el código del host que los rodea. Así es la versión en WGSL escrita a mano del shader que duplica valores:
@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;
}
Y esta es la versión de TypeGPU del ejemplo anterior:
const double = root.createGuardedComputePipeline((x) => {
'use gpu';
if (x >= N) {
return;
}
layout.$.values[x] = layout.$.values[x] + layout.$.values[x];
});
Ejecutar la versión en WGSL con WebGPU puro implica escribir a mano todo este código del host:
device.createShaderModulecon el string del código fuente WGSLdevice.createComputePipeline- entradas del bind group con índices numéricos de
bindingque deben coincidir con@binding(0) - el cálculo del número de workgroups para
dispatchWorkgroups - un staging buffer con
MAP_READ, un comando de copia ymapAsyncpara leer los resultados
Con TypeGPU, el objeto de layout sustituye a los índices numéricos y .read() se encarga del staging.
Limitaciones, vías de escape y compatibilidad con navegadores
TypeGPU está en fase previa a la 1.0. La línea de versiones 0.12 se publicó el 12 de agosto de 2026, y algunas versiones menores han eliminado APIs obsoletas, así que lee la guía de migración a la 0.12 y las guías posteriores antes de actualizar. La biblioteca tiene licencia MIT y está desarrollada por Software Mansion.
Recurre a WebGPU puro cuando lo necesites. Puedes envolver un GPUBuffer existente con root.createBuffer(schema, existingBuffer) para obtener lecturas y escrituras tipadas. En ese caso, el ciclo de vida de ese buffer es responsabilidad tuya y debes asegurarte de que su contenido coincide con el esquema. Para llamar a .write(), además, necesita 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);
Protege los índices. WebGPU impide que los accesos fuera de límites lleguen a memoria ajena al binding, pero eso no los hace correctos. Comprueba el índice siempre que el tamaño del dispatch pueda ser mayor que el array.
Evita lecturas entre hilos. Los hilos de la GPU se ejecutan sin un orden garantizado. Si un hilo lee un elemento que otro hilo escribe en el mismo dispatch, el resultado es no determinista:
// 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];
});
WebGPU también limita cuántos workgroups puedes despachar en un único eje. Para cuadrículas grandes, reparte el trabajo entre x e y.
Compatibilidad con navegadores. TypeGPU funciona allí donde funcione WebGPU. Consulta la compatibilidad actual de WebGPU en caniuse para tu público objetivo.
Sabe cuándo no usarlo. La propia documentación de TypeGPU recomienda un framework de renderizado para mostrar un modelo 3D interactivo en una página. Para ese caso, crear animaciones 3D con Three.js es la opción más adecuada.
Conclusión
TypeGPU resuelve la parte de WebGPU en la que los bugs son más difíciles de localizar, la frontera entre los datos de JavaScript y la memoria del shader, describiendo ambos lados con un único esquema de TypeScript y verificando el tipo de cada binding. Empieza añadiendo unplugin-typegpu a un proyecto de Vite y ejecutando el ejemplo de duplicación. Después, prueba a pasar un buffer incompatible a createBindGroup para ver cómo el compilador lo detecta.
Preguntas frecuentes
¿Puedo usar mis shaders WGSL existentes con TypeGPU?
Sí. La CLI tgpu-gen lee archivos de shaders WGSL existentes y genera archivos de TypeScript con las definiciones de TypeGPU correspondientes. Admite patrones glob y un modo watch que regenera las definiciones cuando cambian los shaders. Además, los shells de funciones de TypeGPU aceptan su implementación como un string de WGSL o como una función de JavaScript, así que puedes migrar un código base función a función.
¿Puede una función 'use gpu' ejecutarse también en la CPU?
Sí. Una función marcada con 'use gpu' sigue siendo una función normal de TypeScript, así que puedes llamarla directamente en JavaScript y obtener un resultado. También puedes pasarla a tgpu.resolve para generar WGSL, o ejecutarla en la GPU mediante un pipeline. Esto te permite hacer pruebas unitarias de la lógica auxiliar de los shaders con tu test runner de JavaScript habitual antes de ejecutarla en la GPU.
¿Cómo obtengo el WGSL generado por TypeGPU?
Llama a tgpu.resolve con los recursos de TypeGPU que necesites, como funciones 'use gpu' y esquemas. Devuelve un string de WGSL listo para usar que incluye todas sus dependencias transitivas. Resulta útil cuando otro motor de WebGPU o sistema de materiales espera WGSL puro. No necesitas llamar a resolve tú mismo cuando usas pipelines de TypeGPU, porque estos resuelven y compilan el shader internamente.
¿Funciona TypeGPU en React Native?
Sí. TypeGPU funciona en React Native a través del paquete react-native-webgpu. Para escribir shaders 'use gpu', añade unplugin-typegpu/babel a la lista de plugins en babel.config.js y, si es necesario, limpia la caché de Metro. React Native WebGPU no funciona en Expo Go, por lo que un proyecto de Expo necesita expo prebuild. En el simulador de iOS, además, tienes que desactivar Metal API Validation en Xcode.
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