Para desenvolvedores Web, a WebGPU é uma API gráfica da Web que fornece acesso o acesso a GPUs. A WebGPU expõe capacidades modernas de hardware e permite a renderização e operações de computação em uma GPU, semelhantes a Direct3D 12, Metal e Vulkan.
Embora verdadeiro, essa história está incompleta. A WebGPU é o resultado de um processo incluindo grandes empresas, como Apple, Google, Intel, Mozilla e da Microsoft. Entre elas, alguns perceberam que a WebGPU poderia ser mais do que uma API JavaScript, mas um conjunto de elementos gráficos API para desenvolvedores em vários ecossistemas, além da Web.
Para atender ao caso de uso principal, foi usada uma API JavaScript lançados no Chrome 113. No entanto, outro processo foi desenvolvido junto com ele: o webgpu.h C API. 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 de plataforma, o que permite que você crie aplicativos específicos da plataforma, fornecendo uma interface consistente em diferentes plataformas.
Neste documento, você aprenderá a criar um pequeno app em C++ usando a WebGPU que pode ser executada na Web e em plataformas específicas. Spoiler: você vai encontrar o mesmo triângulo vermelho que aparece na janela do navegador e em uma 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 para criar apps da Web e de área de trabalho usando 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 por um wrapper C++ chamado webgpu_cpp.h.
Na Web, o app é criado com o Emscripten, que tem vinculações para implementar webgpu.h com a API JavaScript. Em plataformas específicas, como macOS ou Windows, esse projeto pode ser criado com base na Dawn, a implementação WebGPU multiplataforma do Chromium. O wgpu-native, uma implementação do Rust do webgpu.h, também existe, mas não é usado 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 uma
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 um "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 é necessário desenhar na tela.
Amanhecer
Para desenhar seu triângulo, use o Dawn, a implementação 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 seguintes comandos o buscam em uma "dawn/" subpasta.
$ 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/
é incluída no destino. - Seu app vai depender dos destinos
dawn::webgpu_dawn
,glfw
ewebgpu_glfw
para que você possa usá-los no arquivomain.cpp
depois.
…
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. Por conveniência, essa biblioteca incluída em webgpu_glfw
permite que você escreva códigos independentes de plataforma para gerenciamento de janelas.
Para abrir uma janela chamada "Janela WebGPU" com uma resolução de 512 x 512, atualize o arquivo main.cpp
conforme mostrado abaixo. Observe que glfwWindowHint()
é usado aqui para solicitar nenhuma inicialização específica da API grá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 e a execução do app como antes agora resulta em uma janela vazia. Você está progredindo!
Acessar dispositivo GPU
Em JavaScript, navigator.gpu
é o ponto de entrada para acessar a GPU. Em C++, você precisa criar manualmente uma variável wgpu::Instance
que seja usada para a mesma finalidade. Para facilitar, 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 um wgpu::Adapter
e um 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 de cima do arquivo main.cpp
. Atualize a função main()
para chamar GetAdapter()
e atribua o callback resultante a adapter
. Em seguida, chame GetDevice()
e atribua o callback resultante 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 faz isso. Em C++, você precisa criá-la manualmente. Mais uma vez, por conveniência, declare uma variável wgpu::Surface
na parte superior do arquivo main.cpp
. Logo após criar a janela GLFW no Start()
, chame a função wgpu::glfw::CreateSurfaceForWindow()
útil 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.
#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 à GPU na função Render()
com o nome 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 no WebAssembly
Vamos dar uma olhada nas mudanças mínimas necessárias para ajustar sua base de código e desenhar esse triângulo vermelho em uma janela do navegador. Mais uma vez, o app foi criado com a Emscripten, uma ferramenta para compilar programas em C/C++ para o WebAssembly, que tem vinculações para implementar webgpu.h com a 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 alterar.
set_target_properties
é usado para adicionar automaticamente o "html" ao arquivo de destino. Em outras palavras, você vai gerar um "app.html" .- A opção de link de app
USE_WEBGPU
é necessária para ativar o suporte à WebGPU no Emscripten. Sem isso, o 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 o 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 pela 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 suave adequada, 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 o Emscripten é incluir o script de shell emcmake
magical no início dos comandos cmake
. Desta 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
Saiba o que esperar no futuro:
- Melhorias na estabilização das APIs webgpu.h e webgpu_cpp.h.
- Suporte inicial predefinido para Android e iOS.
Enquanto isso, informe os problemas da WebGPU relacionados ao Emscripten e aos problemas do Dawn com sugestões e perguntas.
Recursos
Fique à vontade para explorar o código-fonte desse aplicativo.
Se você quiser se aprofundar na criação de aplicativos 3D nativos em C++ do zero com a WebGPU, consulte a documentação Aprender sobre WebGPU para C++ e Exemplos da WebGPU nativa do Dawn (em inglês).
Se você tiver interesse no Rust, também poderá explorar a biblioteca gráfica 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.