Para desenvolvedores da Web, a WebGPU é uma API gráfica da Web que fornece acesso unificado e rápido às GPUs. A WebGPU expõe recursos modernos de hardware e permite operações de renderização e computação em uma GPU, semelhantes a Direct3D 12, Metal e Vulkan.
Embora verdadeira, essa história está incompleta. A WebGPU é o resultado de um esforço colaborativo que inclui 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 gráfica multiplataforma para desenvolvedores em ecossistemas diferentes, 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 em conjunto: a API C webgpu.h. Esse 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 de 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ê verá o mesmo triângulo vermelho que aparece na janela do navegador e na janela da área de trabalho, com ajustes mínimos na sua base de código.
Como funciona?
Para ver o aplicativo concluído, confira o repositório App multiplataforma WebGPU.
O app é um exemplo minimalista de C++ que mostra como usar a WebGPU na criação de apps da Web e para computador com uma única base de código. Internamente, 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 bindings implementando webgpu.h sobre a API JavaScript. Em plataformas específicas, como macOS ou Windows, esse projeto pode ser criado com o Dawn, a implementação da WebGPU multiplataforma do Chromium. É importante mencionar que wgpu-native, uma implementação do Rust do webgpu.h, também existe, mas não é usada neste documento.
Começar
Para começar, você precisa de um compilador C++ e o CMake para processar builds multiplataforma de maneira padrão. Dentro de 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 tem saída, porque você precisa de uma maneira de desenhar coisas na tela.
Amanhecer
Para desenhar o triângulo, você pode aproveitar a Dawn, a implementação da WebGPU multiplataforma 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 git ao seu repositório. Os comandos a seguir o buscam 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. - Seu app dependerá dos destinos
webgpu_cpp
,webgpu_dawn
,glfw
ewebgpu_glfw
para que você possa usá-los no arquivomain.cpp
posteriormente.
…
set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)
target_link_libraries(app PRIVATE webgpu_cpp webgpu_dawn glfw webgpu_glfw)
Abrir uma janela
Agora que o Dawn está disponível, use o GLFW para desenhar elementos na tela. Por conveniência, essa biblioteca incluída em webgpu_glfw
permite escrever um código independente da plataforma para o gerenciamento de janelas.
Para abrir uma janela chamada "Janela WebGPU" com uma resolução de 512 x 512, atualize o arquivo main.cpp
conforme abaixo. Observe que glfwWindowHint()
é usado aqui para não solicitar uma inicialização específica da API de gráficos.
#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();
}
Recriar o app e executá-lo como antes resulta em uma janela vazia. Você está progredindo!
Acessar dispositivo da GPU
Em JavaScript, navigator.gpu
é seu ponto de entrada para acessar a GPU. Em C++, você precisa criar manualmente uma variável wgpu::Instance
usada para a mesma finalidade. Por 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 ao formato da API JavaScript. Em C++, crie duas funções auxiliares chamadas GetAdapter()
e GetDevice()
, que retornam uma função de callback com wgpu::Adapter
e wgpu::Device
, respectivamente.
#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 superior do arquivo main.cpp
. Atualize a função main()
para chamar GetAdapter()
e atribua 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();
});
});
}
Desenhar um triângulo
A cadeia de troca não é exposta na API JavaScript porque o navegador cuida disso. Em C++, é preciso criá-la manualmente. Mais uma vez, por conveniência, declare uma variável wgpu::Surface
na parte de cima do arquivo main.cpp
. Logo depois de criar a janela GLFW em Start()
, chame a útil 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()
. Também é necessário chamar surface.Present()
para apresentar a próxima textura na repetição "while". Isso não tem efeito visível, porque ainda não há renderização acontecendo.
#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 é um bom momento 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 de 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 resulta no tão esperado triângulo vermelho em uma janela. Faça uma pausa, você merece.
Compilar para o WebAssembly
Agora, vamos conferir as mudanças mínimas necessárias para ajustar sua base de código e desenhar esse triângulo vermelho em uma janela do navegador. Novamente, o app é compilado com o Emscripten, uma ferramenta de compilação de programas C/C++ para o WebAssembly, que tem bindings implementando webgpu.h sobre a API JavaScript.
Atualizar configurações do CMake
Depois que o Emscripten estiver instalado, atualize o arquivo de build CMakeLists.txt
desta forma.
A única coisa que você precisa mudar é o código destacado.
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 isso, seu arquivomain.cpp
não pode acessar o arquivowebgpu/webgpu_cpp.h
. - A opção de link do app
USE_GLFW
também é necessária aqui 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 webgpu_cpp webgpu_dawn glfw webgpu_glfw)
endif()
Atualizar o código
Na linguagem Emscripten, a criação de uma wgpu::surface
exige um elemento de tela HTML. Para isso, chame instance.CreateSurface()
e especifique o seletor #canvas
para corresponder ao elemento adequado de tela HTML na página HTML gerada pelo Emscripten.
Em vez de usar uma repetição "while", chame emscripten_set_main_loop(Render)
para garantir que a função Render()
seja chamada em uma taxa uniforme adequada e alinhada 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 Emscripten é anexar os comandos cmake
com o script de shell mágico emcmake
. Desta vez, gere o app em uma subpasta build-web
e inicie um servidor HTTP. Por fim, abra o navegador e visite 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
O que esperar no futuro:
- Melhorias na estabilização das APIs webgpu.h e webgpu_cpp.h.
- Suporte inicial à Dawn para Android e iOS.
Enquanto isso, envie sugestões e perguntas para problemas da WebGPU com o Emscripten e Problemas do Dawn.
Recursos
Fique à vontade para explorar o código-fonte desse app.
Se você quiser se aprofundar na criação de aplicativos 3D nativos em C++ do zero com a WebGPU, confira a documentação sobre WebGPU para C++ e Exemplos de WebGPU nativa da Dawn.
Se você tiver interesse no Rust, também poderá explorar a biblioteca de gráficos wgpu baseada na WebGPU. Veja a demonstração do hello-triangle.
Agradecimentos
Este artigo foi revisado por Corentin Wallez, Kai Ninomiya e Rachel Andrew.
Foto de Marc-Olivier Jodoin no Unsplash.