Voor webontwikkelaars is WebGPU een webgrafische API die uniforme en snelle toegang tot GPU's biedt. WebGPU biedt moderne hardwaremogelijkheden en maakt weergave- en berekeningsbewerkingen op een GPU mogelijk, vergelijkbaar met Direct3D 12, Metal en Vulkan.
Hoewel waar, is dat verhaal onvolledig. WebGPU is het resultaat van een gezamenlijke inspanning van grote bedrijven, zoals Apple, Google, Intel, Mozilla en Microsoft. Onder hen realiseerden sommigen zich dat WebGPU meer zou kunnen zijn dan een Javascript API, maar een platformonafhankelijke grafische API voor ontwikkelaars in andere ecosystemen dan het web.
Om aan de primaire use case te voldoen, werd een JavaScript API geïntroduceerd in Chrome 113. Daarnaast is er echter nog een ander belangrijk project ontwikkeld: de webgpu.h C API. Dit C-headerbestand bevat alle beschikbare procedures en datastructuren van WebGPU. Het dient als een platformonafhankelijke hardware-abstractielaag, waardoor u platformspecifieke applicaties kunt bouwen door een consistente interface op verschillende platforms te bieden.
In dit document leert u hoe u een kleine C++-app schrijft met behulp van WebGPU die zowel op internet als op specifieke platforms kan worden uitgevoerd. Spoiler alert: u krijgt dezelfde rode driehoek die verschijnt in een browservenster en een desktopvenster met minimale aanpassingen aan uw codebase.
![Schermafbeelding van een rode driehoek mogelijk gemaakt door WebGPU in een browservenster en een desktopvenster op macOS.](https://developer.chrome.google.cn/static/docs/web-platform/webgpu/build-app/image/screenshot-a-red-triangl-82836ca46abb9.png?authuser=5&hl=nl)
Hoe werkt het?
Om de voltooide applicatie te zien, ga naar de WebGPU platformonafhankelijke app- repository.
De app is een minimalistisch C++-voorbeeld dat laat zien hoe u WebGPU kunt gebruiken om desktop- en web-apps te bouwen vanuit één enkele codebase. Onder de motorkap gebruikt het WebGPU's webgpu.h als een platformonafhankelijke hardware-abstractielaag via een C++-wrapper genaamd webgpu_cpp.h .
Op internet is de app gebouwd tegen Emscripten , die bindingen heeft die webgpu.h implementeren bovenop de JavaScript-API. Op specifieke platforms zoals macOS of Windows kan dit project worden gebouwd tegen Dawn , de platformonafhankelijke WebGPU-implementatie van Chromium. Het is de moeite waard om te vermelden dat wgpu-native , een Rust-implementatie van webgpu.h, ook bestaat, maar niet in dit document wordt gebruikt.
Begin
Om te beginnen heb je een C++-compiler en CMake nodig om platformonafhankelijke builds op een standaard manier af te handelen. Maak in een speciale map een main.cpp
bronbestand en een CMakeLists.txt
buildbestand.
Het bestand main.cpp
zou voorlopig een lege functie main()
moeten bevatten.
int main() {}
Het CMakeLists.txt
-bestand bevat basisinformatie over het project. De laatste regel geeft aan dat de naam van het uitvoerbare bestand "app" is en dat de broncode main.cpp
is.
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")
Voer cmake -B build
uit om build-bestanden te maken in een "build/" submap en cmake --build build
om de app daadwerkelijk te bouwen en het uitvoerbare bestand te genereren.
# Build the app with CMake.
$ cmake -B build && cmake --build build
# Run the app.
$ ./build/app
De app werkt, maar er is nog geen uitvoer, omdat je een manier nodig hebt om dingen op het scherm te tekenen.
Neem Dawn
Om uw driehoek te tekenen, kunt u profiteren van Dawn , de platformonafhankelijke WebGPU-implementatie van Chromium. Dit omvat de GLFW C++-bibliotheek voor tekenen naar het scherm. Eén manier om Dawn te downloaden is door het als een git-submodule aan je repository toe te voegen. Met de volgende opdrachten wordt het opgehaald in een submap "dawn/".
$ git init
$ git submodule add https://dawn.googlesource.com/dawn
Voeg vervolgens als volgt toe aan het bestand CMakeLists.txt
:
- De optie CMake
DAWN_FETCH_DEPENDENCIES
haalt alle Dawn-afhankelijkheden op. - De map
dawn/
sub is opgenomen in het doel. - Uw app is afhankelijk van
webgpu_cpp
,webgpu_dawn
,glfw
enwebgpu_glfw
-doelen, zodat u ze later in het bestandmain.cpp
kunt gebruiken.
…
set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)
target_link_libraries(app PRIVATE webgpu_cpp webgpu_dawn glfw webgpu_glfw)
Open een raam
Nu Dawn beschikbaar is, kun je GLFW gebruiken om dingen op het scherm te tekenen. Met deze bibliotheek die voor het gemak is opgenomen in webgpu_glfw
, kunt u code schrijven die platformonafhankelijk is voor vensterbeheer.
Om een venster met de naam "WebGPU-venster" te openen met een resolutie van 512x512, updatet u het bestand main.cpp
zoals hieronder. Merk op dat glfwWindowHint()
hier wordt gebruikt om geen specifieke grafische API-initialisatie aan te vragen.
#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();
}
Het opnieuw opbouwen van de app en het uitvoeren ervan zoals voorheen resulteert nu in een leeg venster. Je boekt vooruitgang!
![Schermafbeelding van een leeg macOS-venster.](https://developer.chrome.google.cn/static/docs/web-platform/webgpu/build-app/image/screenshot-a-empty-macos-55308df210fa3.png?authuser=5&hl=nl)
GPU-apparaat downloaden
In JavaScript is navigator.gpu
uw toegangspunt voor toegang tot de GPU. In C++ moet u handmatig een wgpu::Instance
variabele maken die voor hetzelfde doel wordt gebruikt. Voor het gemak declareert u instance
bovenaan het bestand main.cpp
en roept u wgpu::CreateInstance()
binnen main()
aan.
…
#include <webgpu/webgpu_cpp.h>
wgpu::Instance instance;
…
int main() {
instance = wgpu::CreateInstance();
Start();
}
Toegang tot de GPU is asynchroon vanwege de vorm van de JavaScript-API. Maak in C++ twee helperfuncties genaamd GetAdapter()
en GetDevice()
die respectievelijk een callback-functie retourneren met een wgpu::Adapter
en een 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));
}
Voor eenvoudigere toegang declareert u twee variabelen wgpu::Adapter
en wgpu::Device
bovenaan het bestand main.cpp
. Werk de functie main()
bij om GetAdapter()
aan te roepen en wijs de resultaat-callback toe aan adapter
Roep vervolgens GetDevice()
aan en wijs de resultaat-callback toe aan device
voordat u Start()
aanroept.
wgpu::Adapter adapter;
wgpu::Device device;
…
int main() {
instance = wgpu::CreateInstance();
GetAdapter([](wgpu::Adapter a) {
adapter = a;
GetDevice([](wgpu::Device d) {
device = d;
Start();
});
});
}
Teken een driehoek
De swapketen wordt niet weergegeven in de JavaScript-API, omdat de browser hiervoor zorgt. In C++ moet je het handmatig maken. Declareer voor het gemak nogmaals een wgpu::Surface
variabele bovenaan het bestand main.cpp
. Net nadat u het GLFW-venster in Start()
hebt gemaakt, roept u de handige functie wgpu::glfw::CreateSurfaceForWindow()
aan om een wgpu::Surface
te maken (vergelijkbaar met een HTML-canvas) en configureert u deze door de nieuwe helper ConfigureSurface()
functie aan te roepen in InitGraphics()
. Je moet ook surface.Present()
aanroepen om de volgende texture in de while-lus te presenteren. Dit heeft geen zichtbaar effect omdat er nog geen weergave plaatsvindt.
#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();
}
}
Dit is een goed moment om de renderpijplijn te maken met de onderstaande code. Voor eenvoudigere toegang declareert u een wgpu::RenderPipeline
variabele bovenaan het bestand main.cpp
en roept u de helperfunctie CreateRenderPipeline()
aan 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();
}
Stuur ten slotte renderingopdrachten naar de GPU in de functie Render()
die elk frame wordt genoemd.
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);
}
Het opnieuw opbouwen van de app met CMake en het uitvoeren ervan resulteert nu in de langverwachte rode driehoek in een venster! Neem een pauze, je verdient het.
![Schermafbeelding van een rode driehoek in een macOS-venster.](https://developer.chrome.google.cn/static/docs/web-platform/webgpu/build-app/image/screenshot-a-red-triangl-403ff28f43f9.png?authuser=5&hl=nl)
Compileren naar WebAssembly
Laten we nu eens kijken naar de minimale wijzigingen die nodig zijn om uw bestaande codebase aan te passen om deze rode driehoek in een browservenster te tekenen. Nogmaals, de app is gebouwd tegen Emscripten , een tool voor het compileren van C/C++-programma's naar WebAssembly, die bindingen heeft die webgpu.h implementeren bovenop de JavaScript-API.
Update CMake-instellingen
Zodra Emscripten is geïnstalleerd, werkt u het CMakeLists.txt
buildbestand als volgt bij. De gemarkeerde code is het enige dat u hoeft te wijzigen.
-
set_target_properties
wordt gebruikt om automatisch de bestandsextensie "html" aan het doelbestand toe te voegen. Met andere woorden, u genereert een "app.html"-bestand. - De app-linkoptie
USE_WEBGPU
is vereist om WebGPU-ondersteuning in Emscripten in te schakelen. Zonder dit bestand heeft uwmain.cpp
-bestand geen toegang tot het bestandwebgpu/webgpu_cpp.h
. - Ook hier is de app-linkoptie
USE_GLFW
vereist, zodat u uw GLFW-code opnieuw kunt gebruiken.
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()
Werk de code bij
In Emscripten vereist het maken van een wgpu::surface
een HTML-canvaselement. Roep hiervoor instance.CreateSurface()
aan en specificeer de #canvas
selector zodat deze overeenkomt met het juiste HTML-canvaselement op de HTML-pagina die door Emscripten is gegenereerd.
In plaats van een while-lus te gebruiken, roept u emscripten_set_main_loop(Render)
aan om er zeker van te zijn dat de Render()
functie met de juiste, soepele snelheid wordt aangeroepen, zodat deze goed aansluit bij de browser en de 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
}
Bouw de app met Emscripten
De enige verandering die nodig is om de app met Emscripten te bouwen, is door de cmake
opdrachten vooraf te laten gaan door het magische emcmake
shellscript. Genereer deze keer de app in een build-web
submap en start een HTTP-server. Open ten slotte uw browser en bezoek 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 van een rode driehoek in een browservenster.](https://developer.chrome.google.cn/static/docs/web-platform/webgpu/build-app/image/screenshot-a-red-triangl-4d54843f1cde3.png?authuser=5&hl=nl)
Wat is het volgende
Dit is wat u in de toekomst kunt verwachten:
- Verbeteringen in de stabilisatie van webgpu.h en webgpu_cpp.h API's.
- Dawn initiële ondersteuning voor Android en iOS.
Dien in de tussentijd WebGPU-problemen voor Emscripten- en Dawn-problemen in met suggesties en vragen.
Bronnen
Voel je vrij om de broncode van deze app te verkennen.
Als je je verder wilt verdiepen in het helemaal opnieuw maken van native 3D-applicaties in C++ met WebGPU, bekijk dan de Learn WebGPU voor C++-documentatie en Dawn Native WebGPU-voorbeelden .
Als je geïnteresseerd bent in Rust, kun je ook de wgpu grafische bibliotheek verkennen op basis van WebGPU. Kijk eens naar hun hello-triangle- demo.
Dankbetuigingen
Dit artikel is beoordeeld door Corentin Wallez , Kai Ninomiya en Rachel Andrew .
Foto door Marc-Olivier Jodoin op Unsplash .