Per gli sviluppatori web, WebGPU è un'API di grafica web che fornisce accesso unificato e rapido alle GPU. WebGPU espone funzionalità hardware moderne e consente 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 un impegno collaborativo che include grandi aziende come Apple, Google, Intel, Mozilla e Microsoft. Tra questi, alcuni si sono resi conto che WebGPU potrebbe essere più di un'API JavaScript, ma un'API grafica cross-platform per gli sviluppatori di diversi ecosistemi, oltre al web.
Per soddisfare il caso d'uso principale, è stata introdotta un'API JavaScript in Chrome 113. Tuttavia, è stato sviluppato un altro progetto importante: 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, consentendo di creare applicazioni specifiche per la piattaforma fornendo un'interfaccia coerente su piattaforme diverse.
In questo documento imparerai a scrivere una piccola app C++ che utilizza WebGPU ed è in grado di funzionare sia sul web che su piattaforme specifiche. Spoiler alert: vedrai lo stesso triangolo rosso che compare in una finestra del browser e in una finestra del computer con modifiche minime al codice di base.
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 desktop e web 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 viene compilata in base a Emscripten, che dispone di legamenti che implementano webgpu.h sull'API JavaScript. Su piattaforme specifiche come macOS o Windows, questo progetto può essere compilato in base a Dawn, l'implementazione di WebGPU multipiattaforma di Chromium. Vale la pena ricordare che esiste anche wgpu-native, un'implementazione in Rust di webgpu.h, ma non viene utilizzato in questo documento.
Inizia
Per iniziare, devi disporre di un compilatore C++ e di CMake per gestire le build multipiattaforma in modo standard. In una cartella dedicata, crea un file di origine main.cpp
e un file di compilazione CMakeLists.txt
.
Per il momento, il file main.cpp
dovrebbe 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 codice sorgente è 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")
Esegui cmake -B build
per creare i file di compilazione in una sottocartella "build/" e cmake --build build
per compilare 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 devi trovare un modo per disegnare elementi sullo schermo.
Get Dawn
Per disegnare il triangolo, puoi utilizzare Dawn, l'implementazione di WebGPU multipiattaforma 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 il seguente testo al file CMakeLists.txt
:
- L'opzione CMake
DAWN_FETCH_DEPENDENCIES
recupera tutte le dipendenze di Dawn. - La sottocartella
dawn/
è inclusa nella destinazione. - La tua app dipenderà dai target
dawn::webgpu_dawn
,glfw
ewebgpu_glfw
, in modo da poterli utilizzare in un secondo momento nel filemain.cpp
.
…
set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)
target_link_libraries(app PRIVATE dawn::webgpu_dawn glfw webgpu_glfw)
Aprire una finestra
Ora che Dawn è disponibile, utilizza GLFW per disegnare 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 "Finestra WebGPU" con una risoluzione di 512 x 512, aggiorna il file main.cpp
come indicato di seguito. Tieni presente che qui viene utilizzato glfwWindowHint()
per non richiedere l'inizializzazione di una determinata API grafica.
#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 ricostruzione dell'app e l'esecuzione come prima ora generano una finestra vuota. Stai facendo progressi.
Ottenere il dispositivo GPU
In JavaScript, navigator.gpu
è il punto di contatto per accedere alla GPU. In C++, devi creare manualmente una variabile wgpu::Instance
utilizzata per lo stesso scopo. Per praticità, dichiara instance
nella parte superiore del file main.cpp
e chiama wgpu::CreateInstance()
all'interno di main()
.
…
#include <webgpu/webgpu_cpp.h>
wgpu::Instance instance;
…
int main() {
instance = wgpu::CreateInstance();
Start();
}
L'accesso alla GPU è asincrono a causa della forma dell'API JavaScript. In C++, crea due funzioni helper chiamate GetAdapter()
e GetDevice()
che restituiscono rispettivamente una funzione di callback con un wgpu::Adapter
e un 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));
}
Per un accesso più facile, dichiara due variabili wgpu::Adapter
e wgpu::Device
nella parte superiore del file main.cpp
. Aggiorna la funzione main()
per chiamare GetAdapter()
e assegnarne il callback del risultato a adapter
, quindi chiama GetDevice()
e assegna il relativo callback del risultato a device
prima di chiamare 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();
});
});
}
Disegna un triangolo
La catena di scambio non è esposta nell'API JavaScript perché è gestita dal browser. 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 configuralo chiamando la nuova funzione di assistenza ConfigureSurface()
in InitGraphics()
. Devi anche chiamare surface.Present()
per presentare la trama successiva nel ciclo while. Ciò non ha alcun effetto visibile perché non è ancora stato eseguito il 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};
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ù facile, 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::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();
}
Infine, invia i comandi di rendering alla GPU nella funzione Render()
chiamata 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);
}
Ora, se ricostruisci l'app con CMake e la esegui, viene visualizzato il tanto atteso triangolo rosso in una finestra. Prenditi una pausa, te la meriti.
Compilare in WebAssembly
Vediamo ora le modifiche minime necessarie per modificare la base di codice esistente in modo da disegnare questo triangolo rosso in una finestra del browser. Anche in questo caso, l'app è creata in base a Emscripten, uno strumento per la compilazione di programmi C/C++ in WebAssembly, che dispone di legamenti che implementano webgpu.h sull'API JavaScript.
Aggiorna le impostazioni di CMake
Una volta installato Emscripten, aggiorna il file di compilazione 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".- L'opzione di collegamento dell'app
USE_WEBGPU
è obbligatoria per attivare il supporto di WebGPU in Emscripten. Senza questo file, il filemain.cpp
non può accedere al filewebgpu/webgpu_cpp.h
. - Anche l'opzione di link all'app
USE_GLFW
è obbligatoria qui per consentirti di riutilizzare il codice 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()
Aggiorna il codice
In Emscripten, la creazione di un wgpu::surface
richiede un elemento canvas HTML. A questo scopo, chiama instance.CreateSurface()
e specifica il selettore #canvas
per trovare l'elemento canvas HTML appropriato nella pagina HTML generata da Emscripten.
Anziché utilizzare un ciclo while, chiama emscripten_set_main_loop(Render)
per assicurarti che la funzione Render()
venga chiamata a una frequenza regolare appropriata che si allinei correttamente al browser e al 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
}
Crea l'app con Emscripten
L'unica modifica necessaria per compilare l'app con Emscripten è anteporre i comandi cmake
allo 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 il sito 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
Passaggi successivi
Ecco cosa puoi aspettarti in futuro:
- Miglioramenti alla stabilità delle API webgpu.h e webgpu_cpp.h.
- Supporto iniziale di Dawn per Android e iOS.
Nel frattempo, segnala i problemi di WebGPU per Emscripten e i problemi di Dawn con suggerimenti e domande.
Risorse
Puoi esaminare il codice sorgente di questa app.
Per saperne di più sulla creazione di applicazioni 3D native in C++ da zero con WebGPU, consulta la documentazione di WebGPU per 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 esaminato da Corentin Wallez, Kai Ninomiya e Rachel Andrew.
Foto di Marc-Olivier Jodoin su Unsplash.