Creare un'app con WebGPU

François Beaufort
François Beaufort

Pubblicato: 20 luglio 2023, ultimo aggiornamento: 27 agosto 2025

Per gli sviluppatori web, WebGPU è un'API grafica web che fornisce un accesso unificato e rapido alle GPU. WebGPU espone le funzionalità hardware moderne e consente di eseguire operazioni di rendering e calcolo su una GPU, in modo simile a Direct3D 12, Metal e Vulkan.

Anche se è vero, questa storia è incompleta. WebGPU è il risultato di uno sforzo collaborativo, che ha coinvolto importanti aziende come Apple, Google, Intel, Mozilla e Microsoft. Tra questi, alcuni si sono resi conto che WebGPU poteva essere più di un'API JavaScript, ma un'API grafica multipiattaforma per sviluppatori in tutti gli ecosistemi, oltre al web.

Per soddisfare il caso d'uso principale, in Chrome 113 è stata introdotta un'API JavaScript. Tuttavia, è stato sviluppato un altro progetto significativo: l'API C webgpu.h. Questo file di intestazione C elenca tutte le procedure e le strutture di dati disponibili di WebGPU. Funge da livello di astrazione hardware indipendente dalla piattaforma, consentendoti di creare applicazioni specifiche per la piattaforma fornendo un'interfaccia coerente su piattaforme diverse.

In questo documento imparerai a scrivere una piccola app C++ utilizzando WebGPU che viene eseguita sia sul web che su piattaforme specifiche. Attenzione spoiler: otterrai lo stesso triangolo rosso che appare in una finestra del browser e in una finestra del desktop con modifiche minime al tuo codice.

Screenshot di un triangolo rosso basato su WebGPU in una finestra del browser e una finestra del desktop su macOS.
Lo stesso triangolo basato su WebGPU in una finestra del browser e in una finestra del desktop.

Come funziona?

Per visualizzare l'applicazione completata, consulta il repository dell'app multipiattaforma WebGPU.

L'app è un esempio minimalista in C++ che mostra come utilizzare WebGPU per creare app web e desktop da un unico codebase. Sotto il cofano, utilizza webgpu.h di WebGPU come livello di astrazione hardware indipendente dalla piattaforma tramite un wrapper C++ chiamato webgpu_cpp.h.

Sul web, l'app è basata su emdawnwebgpu (Emscripten Dawn WebGPU), che ha binding che implementano webgpu.h sopra l'API JavaScript. Su piattaforme specifiche come macOS o Windows, questo progetto può essere creato in base a Dawn, l'implementazione multipiattaforma di WebGPU di Chromium. Vale la pena menzionare che esiste anche wgpu-native, un'implementazione Rust di webgpu.h, ma non viene utilizzata in questo documento.

Inizia

Per iniziare, hai bisogno di un compilatore C++ e di CMake per gestire le build multipiattaforma in modo standard. All'interno di una cartella dedicata, crea un file di origine main.cpp e un file di build CMakeLists.txt.

Per il momento, il file main.cpp deve contenere una funzione main() vuota.

int main() {}

Il file CMakeLists.txt contiene informazioni di base sul progetto. L'ultima riga specifica che il nome dell'eseguibile è "app" e il relativo codice sorgente è 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")

Esegui cmake -B build per creare i file di build in una sottocartella "build/" e cmake --build build per creare effettivamente l'app e generare il file eseguibile.

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

# Run the app.
$ ./build/app

L'app viene eseguita, ma non è ancora presente alcun output, in quanto è necessario un modo per disegnare elementi sullo schermo.

Get Dawn

Per disegnare il triangolo, puoi sfruttare Dawn, l'implementazione multipiattaforma di WebGPU di Chromium. È inclusa la libreria C++ GLFW per il disegno sullo schermo. Un modo per scaricare Dawn è aggiungerlo come sottomodulo Git al tuo repository. I seguenti comandi lo recuperano in una sottocartella "dawn/".

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

Quindi, aggiungi al file CMakeLists.txt quanto segue:

  • L'opzione CMake DAWN_FETCH_DEPENDENCIES recupera tutte le dipendenze di Dawn.
  • L'opzione CMake DAWN_BUILD_MONOLITHIC_LIBRARY raggruppa tutti i componenti di Dawn in un'unica libreria.
  • La sottocartella dawn/ è inclusa nella destinazione.
  • La tua app dipenderà dai target webgpu_dawn, webgpu_glfw e glfw, in modo da poterli utilizzare in un secondo momento nel file main.cpp.

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)

Aprire una finestra

Ora che Dawn è disponibile, usa GLFW per disegnare elementi sullo schermo. Questa libreria inclusa in webgpu_glfw per comodità ti consente di scrivere codice indipendente dalla piattaforma per la gestione delle finestre.

Per aprire una finestra denominata "WebGPU window" con una risoluzione di 512x512, aggiorna il file main.cpp come indicato di seguito. Tieni presente che qui viene utilizzato glfwWindowHint() per richiedere l'inizializzazione di nessuna API grafica specifica.

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

La ricreazione dell'app e la sua esecuzione come prima ora comporta una finestra vuota. Stai facendo progressi!

Screenshot di una finestra macOS vuota.
Una finestra vuota.

Ottieni dispositivo GPU

In JavaScript, navigator.gpu è il punto di accesso alla GPU. In C++, devi creare manualmente una variabile wgpu::Instance utilizzata per lo stesso scopo. Per comodità, dichiara instance nella parte superiore del file main.cpp e chiama wgpu::CreateInstance() all'interno di 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();
}

Dichiara due variabili wgpu::Adapter e wgpu::Device nella parte superiore del file main.cpp. Aggiorna la funzione Init() per chiamare instance.RequestAdapter() e assegnare il relativo callback dei risultati a adapter, quindi chiama adapter.RequestDevice() e assegna il relativo callback dei risultati 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);
}

Disegnare un triangolo

La swap chain non è esposta nell'API JavaScript perché il browser se ne occupa. In C++, devi crearlo manualmente. Ancora una volta, per comodità, dichiara una variabile wgpu::Surface nella parte superiore del file main.cpp. Subito dopo aver creato la finestra GLFW in Start(), chiama la pratica funzione wgpu::glfw::CreateSurfaceForWindow() per creare un wgpu::Surface (simile a un canvas HTML) e configurarlo chiamando la nuova funzione helper ConfigureSurface() in InitGraphics(). Devi anche chiamare surface.Present() per presentare la texture successiva nel ciclo while. Ciò non ha alcun effetto visibile, in quanto non è ancora in corso alcun rendering.

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

Ora è un buon momento per creare la pipeline di rendering con il codice riportato di seguito. Per un accesso più semplice, dichiara una variabile wgpu::RenderPipeline nella parte superiore del file main.cpp e chiama la funzione helper CreateRenderPipeline() in 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();
}

Infine, invia i comandi di rendering alla GPU nella funzione Render() chiamata a ogni 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);
}

La ricompilazione dell'app con CMake e la sua esecuzione ora restituiscono il tanto atteso triangolo rosso in una finestra. Prenditi una pausa, te la meriti.

Screenshot di un triangolo rosso in una finestra di macOS.
Un triangolo rosso in una finestra del computer.

Compilare in WebAssembly

Diamo un'occhiata ora alle modifiche minime necessarie per adattare la base di codice esistente per disegnare questo triangolo rosso in una finestra del browser. Anche in questo caso, l'app è creata in base a emdawnwebgpu (Emscripten Dawn WebGPU), che dispone di binding che implementano webgpu.h sopra l'API JavaScript. Utilizza Emscripten, uno strumento per compilare programmi C/C++ in WebAssembly.

Aggiorna le impostazioni di CMake

Una volta installato Emscripten, aggiorna il file di build CMakeLists.txt come segue. Il codice evidenziato è l'unica cosa che devi modificare.

  • set_target_properties viene utilizzato per aggiungere automaticamente l'estensione del file "html" al file di destinazione. In altre parole, genererai un file "app.html".
  • La libreria di link di destinazione emdawnwebgpu_cpp consente il supporto di WebGPU in Emscripten. Senza, il file main.cpp non può accedere al file webgpu/webgpu_cpp.h.
  • L'opzione di link all'app ASYNCIFY=1 consente al codice C++ sincrono di interagire con JavaScript asincrono.
  • L'opzione USE_GLFW=3 per i link per app indica a Emscripten di utilizzare la sua implementazione JavaScript integrata dell'API 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()

Aggiorna il codice

Anziché utilizzare un ciclo while, chiama emscripten_set_main_loop(Render) per assicurarti che la funzione Render() venga chiamata a una velocità uniforme adeguata che si allinei correttamente con il browser e il 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
}

Crea l'app con Emscripten

L'unica modifica necessaria per creare l'app con Emscripten è anteporre ai comandi cmake lo script shell emcmake magico. Questa volta, genera l'app in una sottocartella build-web e avvia un server HTTP. Infine, apri il browser e visita la pagina 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
Screenshot di un triangolo rosso in una finestra del browser.
Un triangolo rosso nella finestra di un browser.

Passaggi successivi

Ecco cosa puoi aspettarti in futuro:

  • Miglioramenti alla stabilizzazione delle API webgpu.h e webgpu_cpp.h.
  • Supporto iniziale di Dawn per Android e iOS.

Nel frattempo, invia segnalazioni di problemi relativi a WebGPU per Emscripten e problemi relativi a Dawn con suggerimenti e domande.

Risorse

Puoi esaminare il codice sorgente di questa app.

Se vuoi approfondire la creazione di applicazioni 3D native in C++ da zero con WebGPU, consulta la documentazione Learn WebGPU for C++ e gli esempi di Dawn Native WebGPU.

Se ti interessa Rust, puoi anche esplorare la libreria grafica wgpu basata su WebGPU. Dai un'occhiata alla demo hello-triangle.

Riconoscimenti

Questo articolo è stato rivisto da Corentin Wallez, Kai Ninomiya e Rachel Andrew.

Foto di Marc-Olivier Jodoin su Unsplash.