Compila una app con WebGPU

François Beaufort
François Beaufort

Fecha de publicación: 20 de julio de 2023; última actualización: 27 de agosto de 2025

Para los desarrolladores web, WebGPU es una API de gráficos web que proporciona acceso unificado y rápido a las GPUs. WebGPU expone capacidades de hardware modernas y permite operaciones de renderización y procesamiento en una GPU, de manera similar a Direct3D 12, Metal y Vulkan.

Si bien es cierto, esa historia está incompleta. WebGPU es el resultado de un esfuerzo colaborativo, que incluye a empresas importantes como Apple, Google, Intel, Mozilla y Microsoft. Entre ellos, algunos se dieron cuenta de que WebGPU podría ser más que una API de JavaScript, sino una API de gráficos multiplataforma para desarrolladores de todos los ecosistemas, además de la Web.

Para cumplir con el caso de uso principal, se introdujo una API de JavaScript en Chrome 113. Sin embargo, junto con él, se desarrolló otro proyecto significativo: la API de C webgpu.h. En este archivo de encabezado de C, se enumeran todos los procedimientos y estructuras de datos disponibles de WebGPU. Funciona como una capa de abstracción de hardware independiente de la plataforma, lo que te permite compilar aplicaciones específicas para cada plataforma proporcionando una interfaz coherente en diferentes plataformas.

En este documento, aprenderás a escribir una pequeña app en C++ con WebGPU que se ejecute en la Web y en plataformas específicas. Alerta de spoiler: Obtendrás el mismo triángulo rojo que aparece en una ventana del navegador y una ventana de escritorio con ajustes mínimos en tu base de código.

Captura de pantalla de un triángulo rojo renderizado con WebGPU en una ventana del navegador y una ventana de escritorio en macOS.
El mismo triángulo potenciado por WebGPU en una ventana del navegador y una ventana de escritorio.

¿Cómo funciona?

Para ver la aplicación completa, consulta el repositorio de la app multiplataforma de WebGPU.

La app es un ejemplo minimalista de C++ que muestra cómo usar WebGPU para compilar apps para computadoras y la Web a partir de una sola base de código. De forma interna, usa webgpu.h de WebGPU como una capa de abstracción de hardware independiente de la plataforma a través de un wrapper de C++ llamado webgpu_cpp.h.

En la Web, la app se compila en emdawnwebgpu (Emscripten Dawn WebGPU), que tiene vinculaciones que implementan webgpu.h sobre la API de JavaScript. En plataformas específicas, como macOS o Windows, este proyecto se puede compilar con Dawn, la implementación multiplataforma de WebGPU de Chromium. Vale la pena mencionar que también existe wgpu-native, una implementación de webgpu.h en Rust, pero no se usa en este documento.

Comenzar

Para comenzar, necesitas un compilador de C++ y CMake para controlar las compilaciones multiplataforma de forma estándar. Dentro de una carpeta dedicada, crea un archivo fuente main.cpp y un archivo de compilación CMakeLists.txt.

Por ahora, el archivo main.cpp debe contener una función main() vacía.

int main() {}

El archivo CMakeLists.txt contiene información básica sobre el proyecto. La última línea especifica que el nombre del ejecutable es "app" y que su código fuente es main.cpp.

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

add_executable(app "main.cpp")

Ejecuta cmake -B build para crear archivos de compilación en una subcarpeta "build/" y cmake --build build para compilar la app y generar el archivo ejecutable.

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

# Run the app.
$ ./build/app

La app se ejecuta, pero aún no hay resultados, ya que necesitas una forma de dibujar elementos en la pantalla.

Obtén Dawn

Para dibujar tu triángulo, puedes aprovechar Dawn, la implementación multiplataforma de WebGPU de Chromium. Esto incluye la biblioteca GLFW de C++ para dibujar en la pantalla. Una forma de descargar Dawn es agregarlo como un submódulo de Git a tu repositorio. Los siguientes comandos lo recuperan en una subcarpeta "dawn/".

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

Luego, agrega al archivo CMakeLists.txt lo siguiente:

  • La opción DAWN_FETCH_DEPENDENCIES de CMake recupera todas las dependencias de Dawn.
  • La opción DAWN_BUILD_MONOLITHIC_LIBRARY de CMake agrupa todos los componentes de Dawn en una sola biblioteca.
  • La subcarpeta dawn/ se incluye en el destino.
  • Tu app dependerá de los destinos webgpu_dawn, webgpu_glfw y glfw para que puedas usarlos en el archivo main.cpp más adelante.

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

target_link_libraries(app PRIVATE webgpu_dawn webgpu_glfw glfw)

Cómo abrir una ventana

Ahora que Dawn está disponible, usa GLFW para dibujar elementos en la pantalla. Esta biblioteca, que se incluye en webgpu_glfw para mayor comodidad, te permite escribir código independiente de la plataforma para la administración de ventanas.

Para abrir una ventana llamada "WebGPU window" con una resolución de 512 x 512, actualiza el archivo main.cpp como se muestra a continuación. Ten en cuenta que aquí se usa glfwWindowHint() para solicitar que no se inicialice ninguna API de gráficos en particular.

#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();
}

Si vuelves a compilar la app y la ejecutas como antes, ahora se mostrará una ventana vacía. ¡Estás progresando!

Captura de pantalla de una ventana vacía de macOS.
Una ventana vacía.

Obtén el dispositivo de GPU

En JavaScript, navigator.gpu es tu punto de entrada para acceder a la GPU. En C++, debes crear manualmente una variable wgpu::Instance que se use para el mismo propósito. Para mayor comodidad, declara instance en la parte superior del archivo main.cpp y llama a wgpu::CreateInstance() dentro de Init().

#include <webgpu/webgpu_cpp.h>


wgpu::Instance instance;


void Init() {
  static const auto kTimedWaitAny = wgpu::InstanceFeatureName::TimedWaitAny;
  wgpu::InstanceDescriptor instanceDesc{.requiredFeatureCount = 1,
                                        .requiredFeatures = &kTimedWaitAny};
  instance = wgpu::CreateInstance(&instanceDesc);
}

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

Declara dos variables wgpu::Adapter y wgpu::Device en la parte superior del archivo main.cpp. Actualiza la función Init() para llamar a instance.RequestAdapter() y asignar su devolución de llamada de resultado a adapter. Luego, llama a adapter.RequestDevice() y asigna su devolución de llamada 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);
}

Cómo dibujar un triángulo

La cadena de intercambio no se expone en la API de JavaScript, ya que el navegador se encarga de ella. En C++, debes crearlo de forma manual. Una vez más, para mayor comodidad, declara una variable wgpu::Surface en la parte superior del archivo main.cpp. Justo después de crear la ventana de GLFW en Start(), llama a la práctica función wgpu::glfw::CreateSurfaceForWindow() para crear un wgpu::Surface (similar a un lienzo HTML) y configúralo llamando a la nueva función de ayuda ConfigureSurface() en InitGraphics(). También debes llamar a surface.Present() para presentar la siguiente textura en el bucle while. Esto no tiene ningún efecto visible, ya que aún no se realiza ninguna renderización.

#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();
  }
}

Ahora es un buen momento para crear la canalización de renderización con el siguiente código. Para facilitar el acceso, declara una variable wgpu::RenderPipeline en la parte superior del archivo main.cpp y llama a la función auxiliar CreateRenderPipeline() en 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 último, envía comandos de renderización a la GPU en la función Render() que se llama en cada fotograma.

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);
}

Si vuelves a compilar la app con CMake y la ejecutas, ahora se mostrará el triángulo rojo tan esperado en una ventana. Toma un descanso, te lo mereces.

Captura de pantalla de un triángulo rojo en una ventana de macOS.
Un triángulo rojo en una ventana de escritorio.

Compila para WebAssembly

Ahora veamos los cambios mínimos necesarios para ajustar tu base de código existente y dibujar este triángulo rojo en una ventana del navegador. Nuevamente, la app se compila en emdawnwebgpu (Emscripten Dawn WebGPU), que tiene vinculaciones que implementan webgpu.h sobre la API de JavaScript. Utiliza Emscripten, una herramienta para compilar programas en C/C++ en WebAssembly.

Actualiza la configuración de CMake

Una vez que se instale Emscripten, actualiza el archivo de compilación CMakeLists.txt de la siguiente manera. El código destacado es lo único que debes cambiar.

  • set_target_properties se usa para agregar automáticamente la extensión de archivo "html" al archivo de destino. En otras palabras, generarás un archivo "app.html".
  • La biblioteca de vínculos de destino emdawnwebgpu_cpp habilita la compatibilidad con WebGPU en Emscripten. Sin él, tu archivo main.cpp no podrá acceder al archivo webgpu/webgpu_cpp.h.
  • La opción de vínculo de la app ASYNCIFY=1 permite que el código C++ síncrono interactúe con JavaScript asíncrono.
  • La opción de vínculo de la app USE_GLFW=3 le indica a Emscripten que use su implementación integrada de JavaScript de la API de GLFW 3.
cmake_minimum_required(VERSION 3.22) # 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)
set(DAWN_BUILD_MONOLITHIC_LIBRARY STATIC)
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 webgpu_dawn webgpu_glfw glfw)
endif()

Actualiza el código

En lugar de usar un bucle while, llama a emscripten_set_main_loop(Render) para asegurarte de que se llame a la función Render() a una velocidad uniforme adecuada que se alinee correctamente con el navegador y el 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
}

Compila la app con Emscripten

El único cambio necesario para compilar la app con Emscripten es anteponer los comandos cmake con la secuencia de comandos emcmake mágica. Esta vez, genera la app en una subcarpeta build-web y, luego, inicia un servidor HTTP. Por último, abre el navegador y visita 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 pantalla de un triángulo rojo en una ventana del navegador.
Un triángulo rojo en una ventana del navegador.

¿Qué sigue?

Esto es lo que puedes esperar en el futuro:

  • Se realizaron mejoras en la estabilización de las APIs de webgpu.h y webgpu_cpp.h.
  • Se agregó compatibilidad inicial con Dawn para iOS y Android.

Mientras tanto, informa los problemas de WebGPU para Emscripten y los problemas de Dawn con sugerencias y preguntas.

Recursos

Puedes explorar el código fuente de esta app.

Si quieres profundizar en la creación de aplicaciones nativas en 3D en C++ desde cero con WebGPU, consulta la documentación de Learn WebGPU for C++ y los ejemplos de Dawn Native WebGPU.

Si te interesa Rust, también puedes explorar la biblioteca de gráficos wgpu basada en WebGPU. Consulta la demostración de hello-triangle.

Reconocimientos

Corentin Wallez, Kai Ninomiya y Rachel Andrew revisaron este artículo.

Foto de Marc-Olivier Jodoin en Unsplash.