Escrevendo shaders de GPU em TypeScript com TypeGPU
Crie shaders GPU em TypeScript com TypeGPU. Configure buffers tipados, execute um shader de computação e detecte incompatibilidades na compilação.
TypeGPU é uma biblioteca TypeScript que encapsula a WebGPU em primitivas tipadas. Assim, o compilador do TypeScript verifica seus schemas de buffer, layouts de bind group e funções de GPU, em vez de esses erros só aparecerem em tempo de execução, na GPU.
Se você já seguiu algum tutorial de WebGPU, sabe como é. O lado JavaScript passa na verificação de tipos sem problemas, mas a string WGSL tem um campo de struct deslocado em um float, e tudo o que você recebe de volta é um buffer cheio de lixo, sem nenhuma mensagem de erro. Este guia pressupõe que você já sabe o que é WebGPU. Caso contrário, comece por adicionar efeitos WebGPU a uma UI e como a WebGPU difere da WebGL. Aqui você vai configurar o TypeGPU, criar buffers tipados, executar um compute shader de ponta a ponta e ver o erro de tipo que a WebGPU pura não consegue oferecer.
Principais pontos
- Para escrever um shader TypeGPU como uma função TypeScript, você a marca com
'use gpu'e adiciona o plugin de buildunplugin-typegpu, que, fora isso, é uma parte opcional da biblioteca. - Todo buffer do TypeGPU é criado a partir de um schema, como
d.arrayOf(d.u32, 16). O schema define o tamanho do buffer em bytes, como os valores JavaScript são convertidos para binário e quais valores o.write()aceita. - Em um array WGSL, cada elemento
vec3focupa 16 bytes. O TypeGPU aplica o padding automaticamente em arrays e objetos comuns, mas copia umFloat32Arraybyte a byte, então você precisa adicionar o padding por conta própria. - Se você passar para
root.createBindGroupum buffer cujo schema ou usage não corresponda ao layout, o TypeScript o rejeita em tempo de compilação. - O TypeGPU ainda está em versão pré-1.0 (a linha 0.12.x), e versões minor já removeram APIs depreciadas.
Por que a WGSL e a fronteira entre JS e GPU causam a maioria dos bugs de WebGPU
A maioria dos bugs de WebGPU não vem da lógica do shader. Eles acontecem na fronteira. Os shaders da WebGPU são escritos em WGSL, uma linguagem separada que o seu ferramental TypeScript trata como uma string opaca. O layout de bytes dos seus buffers, os índices numéricos de binding e a correspondência entre cada bind group e o que o shader declara são todos mantidos em sincronia manualmente. Erre um deles e, em geral, você verá dados errados de forma silenciosa, em vez de um erro de compilação.
O TypeGPU coloca os dois lados em TypeScript. Um único schema descreve os dados na CPU e na GPU, e as funções de shader também são escritas em TypeScript.
Configurando o TypeGPU com Vite
Instale o runtime e, em seguida, o plugin de build e as definições de tipo da WebGPU como dependências de desenvolvimento:
npm install typegpu
npm install --save-dev unplugin-typegpu @webgpu/types
Registre os tipos da WebGPU no tsconfig.json. Se o TypeScript não reconhecer tipos da WebGPU como GPUDevice, a seção de solução de problemas do guia Getting Started indica essa configuração.
{ "compilerOptions": { "types": ["@webgpu/types"] } }
O unplugin-typegpu é opcional para o TypeGPU como um todo. Você precisa dele para escrever shaders como funções TypeScript com 'use gpu'. Ele é construído sobre o unplugin, então, além do Vite, oferece versões para esbuild, Farm, Rolldown, Rollup, Rspack e webpack, além de um plugin Babel separado para React Native.
// vite.config.ts
import { defineConfig } from 'vite';
import typegpu from 'unplugin-typegpu/vite';
export default defineConfig({
plugins: [typegpu()],
});
Em tempo de execução, await tgpu.init() fornece um objeto root. Todos os buffers, bind groups e pipelines a seguir são criados a partir dele.
Buffers tipados e schemas
Um buffer do TypeGPU sempre vem de um schema construído com os construtores d.*. Como explica a referência de buffers do TypeGPU, o TypeGPU usa o schema para determinar o tamanho que o buffer precisa ter, como converter valores JavaScript em bytes e vice-versa, e quais tipos os métodos .write() e .read() aceitam.
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') define a flag de usage da WebGPU e também a registra no tipo do buffer. Quando um buffer não pode ser mapeado, .read() copia seu conteúdo por meio de um staging buffer temporário, então você nunca precisa escrever mapAsync.
As regras de alinhamento da WGSL fazem com que cada elemento de um array de vec3f ocupe 16 bytes: 12 bytes de dados e 4 bytes de padding. Se você passar arrays ou objetos comuns para .write(), o TypeGPU insere esse padding para você. Se passar um Float32Array, .write() copia os bytes exatamente como foram fornecidos, então o array já precisa incluir o padding:
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
Um compute shader de ponta a ponta
O programa TypeGPU completo abaixo dobra um array de oito inteiros na GPU e lê o resultado de volta. Uma função 'use gpu' passada para root.createGuardedComputePipeline recebe um índice de thread. Chamar dispatchThreads(n) executa n threads lógicas, e o índice assume cada valor de 0 a n − 1 exatamente uma 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]
O layout declara o que o shader espera, e o bind group fornece o buffer concreto. Você nunca calcula a quantidade de workgroups nem escreve um staging buffer. Se não precisar trocar buffers, root.createMutable(schema) é uma alternativa mais curta com recurso fixo: o shader o lê por meio de .$, e você lê os resultados de volta com .read().
Layouts de bind group e type safety em tempo de compilação
A verificação de bind groups em tempo de compilação é onde o TypeGPU mais se distancia da WebGPU pura. Um layout de bind group descreve os recursos que um shader espera, e um bind group fornece os buffers concretos. No TypeGPU, um buffer com schema ou flags de usage incorretos falha na verificação de tipos no momento em que você cria o bind group. O guia de bind groups documenta esse comportamento.
Entradas de storage são somente leitura por padrão, e é por isso que o layout acima define access: 'mutable'. Como o shader faz referência a layout.$ e não a um buffer específico, um único pipeline pode ser executado com dados diferentes:
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 são imutáveis. Crie um para cada combinação de recursos e faça cache deles se você tiver um conjunto pequeno e fixo. Na WebGPU pura, o compilador não tem ideia do que a WGSL declara, então vincular um buffer de floats onde o shader espera u32 compila sem problemas e, silenciosamente, produz números errados.
O mesmo shader em WGSL vs TypeGPU
Um shader TypeGPU e seu equivalente escrito à mão em WGSL têm mais ou menos o mesmo tamanho. A diferença está no código host ao redor deles. A versão em WGSL escrita à mão do shader de duplicação fica assim:
@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;
}
E a versão TypeGPU do exemplo anterior:
const double = root.createGuardedComputePipeline((x) => {
'use gpu';
if (x >= N) {
return;
}
layout.$.values[x] = layout.$.values[x] + layout.$.values[x];
});
Executar a versão WGSL com WebGPU pura significa escrever todo este código host manualmente:
device.createShaderModulecom a string do código-fonte WGSLdevice.createComputePipeline- entradas de bind group com índices numéricos de
bindingque precisam corresponder a@binding(0) - um cálculo da quantidade de workgroups para
dispatchWorkgroups - um staging buffer com
MAP_READ, um comando de cópia emapAsyncpara ler os resultados de volta
Com o TypeGPU, o objeto de layout substitui os índices numéricos, e .read() cuida do staging.
Limitações, rotas de escape e suporte nos navegadores
O TypeGPU está em versão pré-1.0. A linha de releases 0.12 foi lançada em 12 de agosto de 2026, e versões minor já removeram APIs depreciadas, então leia o guia de migração da 0.12 e os guias posteriores antes de atualizar. A biblioteca é licenciada sob MIT e desenvolvida pela Software Mansion.
Recorra à WebGPU pura quando precisar. Você pode encapsular um GPUBuffer existente com root.createBuffer(schema, existingBuffer) para obter leituras e escritas tipadas. A partir daí, o ciclo de vida desse buffer passa a ser sua responsabilidade, e você deve garantir que o conteúdo dele corresponda ao schema. Para chamar .write(), ele também precisa de 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);
Proteja os índices. A WebGPU impede que acessos fora dos limites alcancem memória fora do binding, mas isso não os torna corretos. Proteja o índice sempre que o tamanho do dispatch puder ser maior que o array.
Evite leituras entre threads. Threads de GPU são executadas sem ordem garantida. Se uma thread lê um elemento que outra thread escreve no mesmo dispatch, o resultado é não determinístico:
// 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];
});
A WebGPU também limita quantos workgroups você pode despachar ao longo de um único eixo. Para grids grandes, divida o trabalho entre x e y.
Suporte nos navegadores. O TypeGPU funciona onde quer que a WebGPU funcione. Consulte a cobertura atual da WebGPU no caniuse para o seu público-alvo.
Saiba quando não usar. A própria documentação do TypeGPU recomenda um framework de renderização para colocar um modelo 3D interativo em uma página. Para esse tipo de tarefa, criar animações 3D com Three.js é a opção mais adequada.
Conclusão
O TypeGPU resolve a parte da WebGPU em que os bugs são mais difíceis de rastrear, a fronteira entre os dados JavaScript e a memória do shader, descrevendo os dois lados com um único schema TypeScript e verificando os tipos de cada binding. Comece adicionando o unplugin-typegpu a um projeto Vite e executando o exemplo de duplicação. Depois, tente passar um buffer incompatível para createBindGroup e veja o compilador detectar o erro.
Perguntas frequentes
Posso usar meus shaders WGSL existentes com o TypeGPU?
Sim. A CLI tgpu-gen lê arquivos de shader WGSL existentes e gera arquivos TypeScript com as definições TypeGPU correspondentes. Ela aceita padrões glob e um modo watch que regenera as definições quando os shaders mudam. As function shells do TypeGPU também aceitam sua implementação como uma string WGSL ou como uma função JavaScript, então você pode migrar uma base de código uma função por vez.
Uma função 'use gpu' também pode ser executada na CPU?
Sim. Uma função marcada com 'use gpu' continua sendo uma função TypeScript comum, então você pode chamá-la diretamente em JavaScript e obter um resultado. Você também pode passá-la para tgpu.resolve para gerar WGSL ou executá-la na GPU por meio de um pipeline. Isso permite escrever testes unitários para a lógica auxiliar dos shaders no seu test runner JavaScript habitual antes de executá-la na GPU.
Como obtenho o WGSL gerado pelo TypeGPU?
Chame tgpu.resolve com os recursos TypeGPU de que você precisa, como funções 'use gpu' e schemas. Ele retorna uma string WGSL pronta para uso que inclui todas as dependências transitivas. Isso é útil quando outro engine WebGPU ou sistema de materiais espera WGSL puro. Você não precisa chamar resolve manualmente ao usar pipelines do TypeGPU, porque eles resolvem e compilam o shader internamente.
O TypeGPU funciona no React Native?
Sim. O TypeGPU funciona no React Native por meio do pacote react-native-webgpu. Para escrever shaders 'use gpu', adicione unplugin-typegpu/babel à lista de plugins no babel.config.js e, se necessário, limpe o cache do Metro. O React Native WebGPU não funciona no Expo Go, então um projeto Expo precisa de expo prebuild. No simulador de iOS, também é necessário desativar o Metal API Validation no 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