Criar um app com a WebGPU

François Beaufort
François Beaufort

Publicado: 20 de julho de 2023, Última atualização: 17 de junho de 2025

Para desenvolvedores da Web, a WebGPU é uma API de gráficos da Web que oferece acesso unificado e rápido a GPUs. O WebGPU expõe recursos de hardware modernos e permite renderização e operações de computação em uma GPU, semelhante ao Direct3D 12, Metal e Vulkan.

Embora seja verdade, essa história está incompleta. O WebGPU é o resultado de um esforço colaborativo, incluindo grandes empresas, como Apple, Google, Intel, Mozilla e Microsoft. Entre eles, alguns perceberam que a WebGPU poderia ser mais do que uma API JavaScript, mas uma API de gráficos multiplataforma para desenvolvedores em vários ecossistemas, além da Web.

Para atender ao caso de uso principal, uma API JavaScript foi introduzida no Chrome 113. No entanto, outro projeto importante foi desenvolvido junto com ele: a API C webgpu.h. Este arquivo de cabeçalho C lista todos os procedimentos e estruturas de dados disponíveis da WebGPU. Ele serve como uma camada de abstração de hardware independente da plataforma, permitindo que você crie aplicativos específicos da plataforma fornecendo uma interface consistente em diferentes plataformas.

Neste documento, você vai aprender a criar um pequeno app C++ usando a WebGPU que é executado na Web e em plataformas específicas. Alerta de spoiler: você vai receber o mesmo triângulo vermelho que aparece em uma janela do navegador e em uma janela de computador com ajustes mínimos na sua base de código.

Captura de tela de um triângulo vermelho com tecnologia da WebGPU em uma janela do navegador e uma janela de área de trabalho no macOS.
O mesmo triângulo com suporte da WebGPU em uma janela de navegador e uma janela de computador.

Como funciona?

Para conferir o aplicativo completo, confira o repositório do app multiplataforma WebGPU.

O app é um exemplo minimalista em C++ que mostra como usar a WebGPU para criar apps para computador e Web com uma única base de código. Por trás dos bastidores, ele usa o webgpu.h da WebGPU como uma camada de abstração de hardware independente de plataforma usando um wrapper C++ chamado webgpu_cpp.h.

Na Web, o app é criado com base em emdawnwebgpu (Emscripten Dawn WebGPU), que tem vinculações que implementam webgpu.h sobre a API JavaScript. Em plataformas específicas, como macOS ou Windows, esse projeto pode ser criado com base na Dawn, a implementação da WebGPU entre plataformas do Chromium. Vale mencionar que a wgpu-native, uma implementação Rust do webgpu.h, também existe, mas não é usada neste documento.

Primeiros passos

Para começar, você precisa de um compilador C++ e do CMake para processar builds multiplataforma de maneira padrão. Em uma pasta dedicada, crie um arquivo de origem main.cpp e um arquivo de build CMakeLists.txt.

O arquivo main.cpp precisa conter uma função main() vazia por enquanto.

int main() {}

O arquivo CMakeLists.txt contém informações básicas sobre o projeto. A última linha especifica que o nome do executável é "app" e o código-fonte é main.cpp.

cmake_minimum_required(VERSION 3.13) # CMake version check
project(app)                         # Create project "app"
set(CMAKE_CXX_STANDARD 20)           # Enable C++20 standard

add_executable(app "main.cpp")

Execute cmake -B build para criar arquivos de build em uma subpasta "build/" e cmake --build build para criar o app e gerar o arquivo executável.

# Build the app with CMake.
$ cmake -B build && cmake --build build

# Run the app.
$ ./build/app

O app é executado, mas ainda não há saída, porque você precisa de uma maneira de desenhar coisas na tela.

Acessar Dawn

Para desenhar o triângulo, você pode aproveitar o Dawn, a implementação da WebGPU em várias plataformas do Chromium. Isso inclui a biblioteca C++ GLFW para desenhar na tela. Uma maneira de fazer o download do Dawn é adicioná-lo como um submódulo do Git ao seu repositório. Os comandos a seguir buscam o arquivo em uma subpasta "dawn/".

$ git init
$ git submodule add https://dawn.googlesource.com/dawn

Em seguida, anexe ao arquivo CMakeLists.txt da seguinte maneira:

  • A opção DAWN_FETCH_DEPENDENCIES do CMake busca todas as dependências do Dawn.
  • A subpasta dawn/ está incluída no destino.
  • O app vai depender dos destinos dawn::webgpu_dawn, glfw e webgpu_glfw para que você possa usá-los no arquivo main.cpp mais tarde.

set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)

target_link_libraries(app PRIVATE dawn::webgpu_dawn glfw webgpu_glfw)

Abrir uma janela

Agora que o Dawn está disponível, use o GLFW para desenhar coisas na tela. Essa biblioteca, incluída em webgpu_glfw para sua conveniência, permite que você escreva código independente de plataforma para gerenciamento de janelas.

Para abrir uma janela chamada "Janela WebGPU" com uma resolução de 512x512, atualize o arquivo main.cpp conforme abaixo. glfwWindowHint() é usado aqui para solicitar a não inicialização de uma API de gráficos específica.

#include <GLFW/glfw3.h>

const uint32_t kWidth = 512;
const uint32_t kHeight = 512;

void Start() {
  if (!glfwInit()) {
    return;
  }

  glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
  GLFWwindow* window =
      glfwCreateWindow(kWidth, kHeight, "WebGPU window", nullptr, nullptr);

  while (!glfwWindowShouldClose(window)) {
    glfwPollEvents();
    // TODO: Render a triangle using WebGPU.
  }
}

int main() {
  Start();
}

A recriação do app e a execução dele como antes agora resultam em uma janela vazia. Você está progredindo.

Captura de tela de uma janela vazia do macOS.
Uma janela vazia.

Conseguir dispositivo de GPU

Em JavaScript, navigator.gpu é o ponto de entrada para acessar a GPU. Em C++, é necessário criar manualmente uma variável wgpu::Instance que seja usada para o mesmo propósito. Para sua conveniência, declare instance na parte de cima do arquivo main.cpp e chame wgpu::CreateInstance() dentro de Init().

#include <webgpu/webgpu_cpp.h>


wgpu::Instance instance;


void Init() {
  wgpu::InstanceDescriptor instanceDesc{
      .capabilities = {.timedWaitAnyEnable = true}};
  instance = wgpu::CreateInstance(&instanceDesc);
}

int main() {
  Init();
  Start();
}

Declare duas variáveis wgpu::Adapter e wgpu::Device na parte de cima do arquivo main.cpp. Atualize a função Init() para chamar instance.RequestAdapter() e atribuir o callback de resultado a adapter. Em seguida, chame adapter.RequestDevice() e atribua o callback de resultado a device.

#include <iostream>

#include <dawn/webgpu_cpp_print.h>


wgpu::Adapter adapter;
wgpu::Device device;


void Init() {
  

  wgpu::Future f1 = instance.RequestAdapter(
      nullptr, wgpu::CallbackMode::WaitAnyOnly,
      [](wgpu::RequestAdapterStatus status, wgpu::Adapter a,
         wgpu::StringView message) {
        if (status != wgpu::RequestAdapterStatus::Success) {
          std::cout << "RequestAdapter: " << message << "\n";
          exit(0);
        }
        adapter = std::move(a);
      });
  instance.WaitAny(f1, UINT64_MAX);

  wgpu::DeviceDescriptor desc{};
  desc.SetUncapturedErrorCallback([](const wgpu::Device&,
                                     wgpu::ErrorType errorType,
                                     wgpu::StringView message) {
    std::cout << "Error: " << errorType << " - message: " << message << "\n";
  });

  wgpu::Future f2 = adapter.RequestDevice(
      &desc, wgpu::CallbackMode::WaitAnyOnly,
      [](wgpu::RequestDeviceStatus status, wgpu::Device d,
         wgpu::StringView message) {
        if (status != wgpu::RequestDeviceStatus::Success) {
          std::cout << "RequestDevice: " << message << "\n";
          exit(0);
        }
        device = std::move(d);
      });
  instance.WaitAny(f2, UINT64_MAX);
}

Desenhe um triângulo

A cadeia de troca não é exposta na API JavaScript, porque o navegador cuida disso. Em C++, é necessário criar manualmente. Mais uma vez, para sua conveniência, declare uma variável wgpu::Surface na parte de cima do arquivo main.cpp. Logo após criar a janela GLFW em Start(), chame a função wgpu::glfw::CreateSurfaceForWindow() para criar um wgpu::Surface (semelhante a uma tela HTML) e configure-o chamando a nova função auxiliar ConfigureSurface() em InitGraphics(). Você também precisa chamar surface.Present() para apresentar a próxima textura no loop while. Isso não tem efeito visível, porque ainda não há renderização.

#include <webgpu/webgpu_glfw.h>


wgpu::Surface surface;
wgpu::TextureFormat format;

void ConfigureSurface() {
  wgpu::SurfaceCapabilities capabilities;
  surface.GetCapabilities(adapter, &capabilities);
  format = capabilities.formats[0];

  wgpu::SurfaceConfiguration config{.device = device,
                                    .format = format,
                                    .width = kWidth,
                                    .height = kHeight,
                                    .presentMode = wgpu::PresentMode::Fifo};
  surface.Configure(&config);
}

void InitGraphics() {
  ConfigureSurface();
}

void Render() {
  // TODO: Render a triangle using WebGPU.
}

void Start() {
  
  surface = wgpu::glfw::CreateSurfaceForWindow(instance, window);

  InitGraphics();

  while (!glfwWindowShouldClose(window)) {
    glfwPollEvents();
    Render();
    surface.Present();
    instance.ProcessEvents();
  }
}

Agora é uma boa hora para criar o pipeline de renderização com o código abaixo. Para facilitar o acesso, declare uma variável wgpu::RenderPipeline na parte de cima do arquivo main.cpp e chame a função auxiliar CreateRenderPipeline() em InitGraphics().

wgpu::RenderPipeline pipeline;


const char shaderCode[] = R"(
    @vertex fn vertexMain(@builtin(vertex_index) i : u32) ->
      @builtin(position) vec4f {
        const pos = array(vec2f(0, 1), vec2f(-1, -1), vec2f(1, -1));
        return vec4f(pos[i], 0, 1);
    }
    @fragment fn fragmentMain() -> @location(0) vec4f {
        return vec4f(1, 0, 0, 1);
    }
)";

void CreateRenderPipeline() {
  wgpu::ShaderSourceWGSL wgsl{{.code = shaderCode}};

  wgpu::ShaderModuleDescriptor shaderModuleDescriptor{.nextInChain = &wgsl};
  wgpu::ShaderModule shaderModule =
      device.CreateShaderModule(&shaderModuleDescriptor);

  wgpu::ColorTargetState colorTargetState{.format = format};

  wgpu::FragmentState fragmentState{
      .module = shaderModule, .targetCount = 1, .targets = &colorTargetState};

  wgpu::RenderPipelineDescriptor descriptor{.vertex = {.module = shaderModule},
                                            .fragment = &fragmentState};
  pipeline = device.CreateRenderPipeline(&descriptor);
}

void InitGraphics() {
  
  CreateRenderPipeline();
}

Por fim, envie comandos de renderização para a GPU na função Render() chamada em cada frame.

void Render() {
  wgpu::SurfaceTexture surfaceTexture;
  surface.GetCurrentTexture(&surfaceTexture);

  wgpu::RenderPassColorAttachment attachment{
      .view = surfaceTexture.texture.CreateView(),
      .loadOp = wgpu::LoadOp::Clear,
      .storeOp = wgpu::StoreOp::Store};

  wgpu::RenderPassDescriptor renderpass{.colorAttachmentCount = 1,
                                        .colorAttachments = &attachment};

  wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
  wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderpass);
  pass.SetPipeline(pipeline);
  pass.Draw(3);
  pass.End();
  wgpu::CommandBuffer commands = encoder.Finish();
  device.GetQueue().Submit(1, &commands);
}

A recriação do app com o CMake e a execução dele agora resultam no tão esperado triângulo vermelho em uma janela. Faça uma pausa, você merece.

Captura de tela de um triângulo vermelho em uma janela do macOS.
Um triângulo vermelho em uma janela do computador.

Compilar para o WebAssembly

Vamos conferir as mudanças mínimas necessárias para ajustar a base de código atual e desenhar esse triângulo vermelho em uma janela do navegador. Novamente, o app é criado com base em emdawnwebgpu (Emscripten Dawn WebGPU), que tem vinculações que implementam webgpu.h na API JavaScript. Ele usa o Emscripten, uma ferramenta para compilar programas C/C++ para o WebAssembly.

Atualizar as configurações do CMake

Depois que o Emscripten for instalado, atualize o arquivo de build CMakeLists.txt da seguinte maneira. O código destacado é a única coisa que você precisa mudar.

  • set_target_properties é usado para adicionar automaticamente a extensão de arquivo "html" ao arquivo de destino. Em outras palavras, você vai gerar um arquivo "app.html".
  • A biblioteca de link de destino emdawnwebgpu_cpp ativa o suporte ao WebGPU no Emscripten. Sem ele, o arquivo main.cpp não pode acessar o arquivo webgpu/webgpu_cpp.h.
  • A opção de vinculação de app ASYNCIFY=1 permite que o código C++ síncrono interaja com o JavaScript assíncrono.
  • A opção de link do app USE_GLFW=3 informa ao Emscripten que ele deve usar a implementação JavaScript integrada da API GLFW 3.
cmake_minimum_required(VERSION 3.13) # CMake version check
project(app)                         # Create project "app"
set(CMAKE_CXX_STANDARD 20)           # Enable C++20 standard

add_executable(app "main.cpp")

set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)

if(EMSCRIPTEN)
  set_target_properties(app PROPERTIES SUFFIX ".html")
  target_link_libraries(app PRIVATE emdawnwebgpu_cpp webgpu_glfw)
  target_link_options(app PRIVATE "-sASYNCIFY=1" "-sUSE_GLFW=3")
else()
  target_link_libraries(app PRIVATE dawn::webgpu_dawn glfw webgpu_glfw)
endif()

Atualizar o código

Em vez de usar um loop while, chame emscripten_set_main_loop(Render) para garantir que a função Render() seja chamada em uma taxa adequada e suave que se alinhe corretamente com o navegador e o monitor.

#include <iostream>

#include <GLFW/glfw3.h>
#if defined(__EMSCRIPTEN__)
#include <emscripten/emscripten.h>
#endif
#include <dawn/webgpu_cpp_print.h>
#include <webgpu/webgpu_cpp.h>
#include <webgpu/webgpu_glfw.h>
void Start() {
  
#if defined(__EMSCRIPTEN__)
  emscripten_set_main_loop(Render, 0, false);
#else
  while (!glfwWindowShouldClose(window)) {
    glfwPollEvents();
    Render();
    surface.Present();
    instance.ProcessEvents();
  }
#endif
}

Criar o app com o Emscripten

A única mudança necessária para criar o app com o Emscripten é inserir os comandos cmake com o script de shell emcmake mágico. Dessa vez, gere o app em uma subpasta build-web e inicie um servidor HTTP. Por fim, abra o navegador e acesse build-web/app.html.

# Build the app with Emscripten.
$ emcmake cmake -B build-web && cmake --build build-web

# Start a HTTP server.
$ npx http-server
Captura de tela de um triângulo vermelho em uma janela do navegador.
Um triângulo vermelho em uma janela do navegador.

A seguir

Confira o que você pode esperar no futuro:

  • Melhorias na estabilização das APIs webgpu.h e webgpu_cpp.h.
  • Suporte inicial do Dawn para Android e iOS.

Enquanto isso, envie problemas da WebGPU para Emscripten e problemas do Dawn com sugestões e perguntas.

Recursos

Confira o código-fonte deste app.

Se você quiser se aprofundar na criação de aplicativos 3D nativos em C++ do zero com a WebGPU, consulte a documentação de Aprenda a WebGPU para C++ e os exemplos de WebGPU nativa do Dawn.

Se você tem interesse em Rust, também pode conferir a biblioteca gráfica wgpu baseada na WebGPU. Confira a demonstração hello-triangle.

Agradecimentos

Este artigo foi revisado por Corentin Wallez, Kai Ninomiya e Rachel Andrew.

Foto de Marc-Olivier Jodoin no Unsplash.