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 a ele: a API C webgpu.h. Este arquivo de cabeçalho C lista todos os procedimentos e estruturas de dados disponíveis da WebGPU. Ela 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.
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. Em segundo plano, 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 o Emscripten, que tem vinculações que implementam o webgpu.h na 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 usar 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
ewebgpu_glfw
para que você possa usá-los no arquivomain.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. O 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.
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 main()
.
…
#include <webgpu/webgpu_cpp.h>
wgpu::Instance instance;
…
int main() {
instance = wgpu::CreateInstance();
Start();
}
O acesso à GPU é assíncrono devido à forma da API JavaScript. Em C++, crie duas funções auxiliares chamadas GetAdapter()
e GetDevice()
que retornam, respectivamente, uma função de callback com um wgpu::Adapter
e um wgpu::Device
.
#include <iostream>
…
void GetAdapter(void (*callback)(wgpu::Adapter)) {
instance.RequestAdapter(
nullptr,
[](WGPURequestAdapterStatus status, WGPUAdapter cAdapter,
const char* message, void* userdata) {
if (status != WGPURequestAdapterStatus_Success) {
exit(0);
}
wgpu::Adapter adapter = wgpu::Adapter::Acquire(cAdapter);
reinterpret_cast<void (*)(wgpu::Adapter)>(userdata)(adapter);
}, reinterpret_cast<void*>(callback));
}
void GetDevice(void (*callback)(wgpu::Device)) {
adapter.RequestDevice(
nullptr,
[](WGPURequestDeviceStatus status, WGPUDevice cDevice,
const char* message, void* userdata) {
wgpu::Device device = wgpu::Device::Acquire(cDevice);
device.SetUncapturedErrorCallback(
[](WGPUErrorType type, const char* message, void* userdata) {
std::cout << "Error: " << type << " - message: " << message;
},
nullptr);
reinterpret_cast<void (*)(wgpu::Device)>(userdata)(device);
}, reinterpret_cast<void*>(callback));
}
Para facilitar o acesso, declare duas variáveis wgpu::Adapter
e wgpu::Device
na parte de cima do arquivo main.cpp
. Atualize a função main()
para chamar GetAdapter()
e atribuir o callback de resultado a adapter
. Em seguida, chame GetDevice()
e atribua o callback de resultado a device
antes de chamar Start()
.
wgpu::Adapter adapter;
wgpu::Device device;
…
int main() {
instance = wgpu::CreateInstance();
GetAdapter([](wgpu::Adapter a) {
adapter = a;
GetDevice([](wgpu::Device d) {
device = d;
Start();
});
});
}
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};
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::ShaderModuleWGSLDescriptor wgslDesc{};
wgslDesc.code = shaderCode;
wgpu::ShaderModuleDescriptor shaderModuleDescriptor{
.nextInChain = &wgslDesc};
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.
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 no Emscripten, uma ferramenta para compilar programas C/C++ para WebAssembly, que tem vinculações que implementam o webgpu.h na API JavaScript.
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 opção de link do app
USE_WEBGPU
é necessária para ativar o suporte à WebGPU no Emscripten. Sem ele, o arquivomain.cpp
não pode acessar o arquivowebgpu/webgpu_cpp.h
. - A opção de link de app
USE_GLFW
também é necessária para que você possa reutilizar seu código GLFW.
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")
if(EMSCRIPTEN)
set_target_properties(app PROPERTIES SUFFIX ".html")
target_link_options(app PRIVATE "-sUSE_WEBGPU=1" "-sUSE_GLFW=3")
else()
set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)
target_link_libraries(app PRIVATE dawn::webgpu_dawn glfw webgpu_glfw)
endif()
Atualizar o código
No Emscripten, a criação de um wgpu::surface
requer um elemento de tela HTML. Para isso, chame instance.CreateSurface()
e especifique o seletor #canvas
para corresponder ao elemento de tela HTML apropriado na página HTML gerada pelo Emscripten.
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 <GLFW/glfw3.h>
#include <webgpu/webgpu_cpp.h>
#include <iostream>
#if defined(__EMSCRIPTEN__)
#include <emscripten/emscripten.h>
#else
#include <webgpu/webgpu_glfw.h>
#endif
void Start() {
if (!glfwInit()) {
return;
}
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
GLFWwindow* window =
glfwCreateWindow(kWidth, kHeight, "WebGPU window", nullptr, nullptr);
#if defined(__EMSCRIPTEN__)
wgpu::SurfaceDescriptorFromCanvasHTMLSelector canvasDesc{};
canvasDesc.selector = "#canvas";
wgpu::SurfaceDescriptor surfaceDesc{.nextInChain = &canvasDesc};
surface = instance.CreateSurface(&surfaceDesc);
#else
surface = wgpu::glfw::CreateSurfaceForWindow(instance, window);
#endif
InitGraphics();
#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
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.