WebGPU: computação gráfica e GPGPU direto no browser sem plugins

1. O que é WebGPU e por que substitui WebGL

WebGPU é uma API gráfica moderna e de baixo nível que permite acesso direto à GPU diretamente no navegador, sem necessidade de plugins ou instalações adicionais. Ela surge como sucessora do WebGL, que se baseava no legado OpenGL ES 2.0/3.0 e apresentava limitações significativas: drivers instáveis, ausência de compute shaders, modelo de estado global e overhead de validação em tempo real.

A nova API foi projetada para espelhar as arquiteturas modernas de GPU — Vulkan, Metal e Direct3D 12 — oferecendo controle explícito sobre recursos, filas de comando e sincronização. Seus casos de uso vão desde renderização 3D avançada até simulações físicas, processamento de imagens, filtros em tempo real e até inferência de modelos de machine learning no navegador.

2. Arquitetura fundamental: GPUAdapter, Device, Queue e Pipeline

A arquitetura do WebGPU segue um modelo em camadas:

  • GPUAdapter: representa uma placa de vídeo ou driver gráfico disponível. Permite verificar recursos (limites de buffers, texturas, features como compute shaders) e solicitar um dispositivo.
  • GPUDevice: é o objeto central que cria todos os recursos da GPU — buffers, texturas, bind groups, pipelines. Cada device opera de forma isolada.
  • GPUQueue: fila de comandos onde submetemos operações. A sincronização é feita com mapAsync para leitura de buffers e com fences para coordenar múltiplas submissões.
  • Shader Modules e Pipelines: shaders são compilados a partir de código WGSL em módulos. Pipelines combinam esses módulos com configurações de estado (rasterização, blend, profundidade).
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
const queue = device.queue;

3. WGSL (WebGPU Shading Language): a linguagem de shaders do futuro

WGSL é a linguagem de shaders padrão do WebGPU. Sua sintaxe é inspirada em Rust e SPIR-V, com tipos seguros e sem ponteiros explícitos. Diferente de GLSL/HLSL, o modelo de memória é mais restrito e seguro, prevenindo acessos fora dos limites.

Exemplo de shader de vértice e fragmento para um triângulo colorido:

// Vertex shader
@vertex
fn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4f {
    var pos = array<vec2f, 3>(
        vec2f(0.0, 0.5),
        vec2f(-0.5, -0.5),
        vec2f(0.5, -0.5)
    );
    return vec4f(pos[idx], 0.0, 1.0);
}

// Fragment shader
@fragment
fn fs_main() -> @location(0) vec4f {
    return vec4f(0.0, 0.5, 1.0, 1.0);
}

4. Renderização gráfica: do triângulo ao pipeline completo

Para renderizar um quadrado texturizado, configuramos a swap chain (canvas), criamos buffers de vértices com coordenadas UV e um sampler para filtrar a textura.

// Configuração do canvas
const context = canvas.getContext('webgpu');
context.configure({
    device: device,
    format: navigator.gpu.getPreferredCanvasFormat(),
    alphaMode: 'premultiplied'
});

// Buffer de vértices com posição e UV
const vertices = new Float32Array([
    -0.5, -0.5, 0.0, 0.0,
     0.5, -0.5, 1.0, 0.0,
     0.5,  0.5, 1.0, 1.0,
    -0.5,  0.5, 0.0, 1.0
]);
const vertexBuffer = device.createBuffer({
    size: vertices.byteLength,
    usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
    mappedAtCreation: true
});
new Float32Array(vertexBuffer.getMappedRange()).set(vertices);
vertexBuffer.unmap();

O pipeline de renderização combina vertex shader, rasterização, fragment shader e blend. Cada frame submetemos um render pass que desenha os vértices na textura alvo.

5. Compute Shaders e GPGPU (General-Purpose GPU)

Compute shaders permitem executar cálculos arbitrários na GPU, organizados em grupos de trabalho (workgroups) e invocações (threads). Exemplo clássico é a soma de vetores em paralelo:

// WGSL compute shader para soma de vetores
@compute @workgroup_size(256)
fn main(@builtin(global_invocation_id) id: vec3u) {
    let idx = id.x;
    if (idx < arrayLength(&a)) {
        result[idx] = a[idx] + b[idx];
    }
}

Aplicações práticas incluem simulação de partículas, FFT, filtros de imagem (blur, detecção de bordas) e inferência de redes neurais leves. A leitura/escrita é feita via storage buffers e storage textures.

6. Gerenciamento de memória e desempenho

O gerenciamento eficiente de memória é crucial:

  • Buffers mapeados (staging): para upload/download de dados entre CPU e GPU.
  • Device-local buffers: alocados diretamente na VRAM para máxima performance.
  • Uniform vs Storage buffers: uniform para dados pequenos e constantes por draw call; storage para grandes volumes de dados mutáveis.
  • Bind Groups: organizam recursos (buffers, texturas, samplers) em layouts que o pipeline consome.

Dicas de otimização:
- Reutilize pipelines e bind groups sempre que possível.
- Use double buffering para evitar stalls entre CPU e GPU.
- Reduza validação desnecessária com device.createRenderPipelineAsync.

7. Comparação com alternativas e limitações atuais

WebGPU vs WebGL 2.0: WebGPU oferece até 3x mais desempenho em cenários compute-heavy, suporte nativo a compute shaders, bindless resources e depuração superior via Chrome DevTools. WebGL ainda é mais amplamente suportado em dispositivos antigos.

WebGPU vs WASM + WASI: WebAssembly com WASI permite executar código nativo no navegador, mas sem acesso direto à GPU. WebGPU complementa WASM fornecendo aceleração gráfica e paralela.

Limitações atuais: suporte restrito a Chrome/Edge (desktop e Android) e Firefox (em desenvolvimento). Ausência de ray tracing nativo (extensão em discussão). Sem suporte a tessellation shaders e mesh shaders por enquanto.

Futuro: extensões como ray tracing, mesh shaders e suporte a Node.js via bibliotecas Dawn (Google) e wgpu (Mozilla) estão em andamento.

8. Exemplo completo integrado: simulação de N-corpos com GPU

Abaixo, a estrutura de uma simulação gravitacional de N-corpos executando compute shader a cada frame:

<!DOCTYPE html>
<html>
<body>
<canvas id="gpuCanvas"></canvas>
<script>
// 1. Inicialização
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
const canvas = document.getElementById('gpuCanvas');
const context = canvas.getContext('webgpu');
context.configure({
    device: device,
    format: 'bgra8unorm'
});

// 2. Buffers de posição e velocidade (N partículas)
const N = 1024;
const posBuffer = device.createBuffer({ /* size, usage, mappedAtCreation */ });
const velBuffer = device.createBuffer({ /* size, usage, mappedAtCreation */ });

// 3. Compute shader para forças gravitacionais
const computeShader = `
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) id: vec3u) {
    let i = id.x;
    if (i >= ${N}u) return;
    var force = vec2f(0.0);
    for (var j = 0u; j < ${N}u; j = j + 1u) {
        let diff = pos[i] - pos[j];
        let distSq = max(dot(diff, diff), 0.01);
        force = force - (diff / (distSq * sqrt(distSq))) * mass[j];
    }
    vel[i] = vel[i] + force * dt;
    pos[i] = pos[i] + vel[i] * dt;
}
`;

// 4. Pipeline de renderização (pontos coloridos)
const renderPipeline = device.createRenderPipeline({
    vertex: { module: vertexModule, entryPoint: 'main' },
    fragment: { module: fragmentModule, entryPoint: 'main', targets: [{ format: 'bgra8unorm' }] }
});

// 5. Loop de animação
function frame() {
    const commandEncoder = device.createCommandEncoder();

    // Compute pass
    const computePass = commandEncoder.beginComputePass();
    computePass.setPipeline(computePipeline);
    computePass.setBindGroup(0, bindGroup);
    computePass.dispatchWorkgroups(Math.ceil(N / 64));
    computePass.end();

    // Render pass
    const renderPass = commandEncoder.beginRenderPass({ colorAttachments: [...] });
    renderPass.setPipeline(renderPipeline);
    renderPass.setVertexBuffer(0, posBuffer);
    renderPass.draw(N);
    renderPass.end();

    queue.submit([commandEncoder.finish()]);
    requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>
</body>
</html>

O loop submete um compute pass para atualizar posições e velocidades, seguido de um render pass que desenha pontos coloridos na tela. A cada frame, a GPU processa N² interações gravitacionais em paralelo.

Referências