Data publikacji: 20 lipca 2023 r., ostatnia aktualizacja: 17 czerwca 2025 r.
Dla programistów internetowych WebGPU to interfejs API do grafiki internetowej, który zapewnia szybki i jednolity dostęp do kart graficznych. WebGPU udostępnia nowoczesne możliwości sprzętowe i umożliwia renderowanie oraz operacje obliczeniowe na karcie graficznej, podobnie jak Direct3D 12, Metal i Vulkan.
To prawda, ale ta historia jest niepełna. WebGPU to efekt współpracy z udziałem takich firm jak Apple, Google, Intel, Mozilla i Microsoft. Niektórzy z nich zorientowali się, że WebGPU może być nie tylko interfejsem API JavaScript, ale też interfejsem API grafiki na wiele platform dla deweloperów w różnych środowiskach niż tylko w internecie.
Aby spełnić podstawowe wymagania, w Chrome 113 wprowadziliśmy interfejs JavaScript API. Obok niego powstał jednak jeszcze jeden ważny projekt: interfejs webgpu.h C. Ten plik nagłówka C zawiera listę wszystkich dostępnych procedur i struktur danych WebGPU. Stanowi on warstwę abstrakcji sprzętowej niezależną od platformy, co pozwala tworzyć aplikacje na poszczególne platformy, zapewniając spójny interfejs na różnych platformach.
Z tego dokumentu dowiesz się, jak napisać małą aplikację w C++ z użyciem WebGPU, która działa zarówno w internecie, jak i na określonych platformach. Spoiler: zobaczysz ten sam czerwony trójkąt, który pojawia się w oknie przeglądarki i na komputerze z minimalnymi zmianami w kodzie źródłowym.

Jak to działa?
Aby zobaczyć gotową aplikację, zajrzyj do repozytorium aplikacji na wiele platform WebGPU.
Aplikacja jest minimalistycznym przykładem w języku C++, który pokazuje, jak używać WebGPU do tworzenia aplikacji na komputery i aplikacji internetowych na podstawie jednego kodu źródłowego. Pod maską używa pliku webgpu.h z WebGPU jako warstwy abstrakcji sprzętowej niezależnej od platformy za pomocą owijarki C++ o nazwie webgpu_cpp.h.
W internecie aplikacja jest tworzona na podstawie emdawnwebgpu (Emscripten Dawn WebGPU), która ma implementacje webgpu.h na podstawie interfejsu JavaScript API. Na niektórych platformach, takich jak macOS czy Windows, ten projekt można skompilować z użyciem Dawn, czyli nowej implementacji WebGPU w Chromium. Warto wspomnieć, że istnieje też biblioteka wgpu-native, która jest implementacją webgpu.h w języku Rust, ale nie jest używana w tym dokumencie.
Rozpocznij
Na początek potrzebujesz kompilatora C++ i CMake, aby obsługiwać kompilacje na wiele platform w standardowy sposób. W dedykowanym folderze utwórz plik źródłowy main.cpp
i plik kompilacji CMakeLists.txt
.
Plik main.cpp
powinien zawierać pustą funkcję main()
.
int main() {}
Plik CMakeLists.txt
zawiera podstawowe informacje o projekcie. Ostatni wiersz określa, że nazwa pliku wykonywalnego to „app”, a jego kod źródłowy to 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")
Uruchom polecenie cmake -B build
, aby utworzyć pliki kompilacji w podfolderze „build/”, a potem polecenie cmake --build build
, aby przeprowadzić kompilację aplikacji i wygenerować plik wykonywalny.
# Build the app with CMake.
$ cmake -B build && cmake --build build
# Run the app.
$ ./build/app
Aplikacja działa, ale nie ma jeszcze żadnego wyjścia, ponieważ musisz mieć sposób na rysowanie na ekranie.
Get Dawn
Aby narysować trójkąt, możesz skorzystać z Dawn, czyli implementacji WebGPU na różnych platformach w Chromium. Obejmuje to bibliotekę GLFW w języku C++, która służy do rysowania na ekranie. Jednym ze sposobów pobrania Dawn jest dodanie go jako podmodułu Git do repozytorium. Te polecenia pobierają go z podfolderu „dawn/”.
$ git init
$ git submodule add https://dawn.googlesource.com/dawn
Następnie dodaj do pliku CMakeLists.txt
te informacje:
- Opcja CMake
DAWN_FETCH_DEPENDENCIES
pobiera wszystkie zależności Dawn. - Folder podrzędny
dawn/
jest uwzględniony w docelowym folderze. - Twoja aplikacja będzie zależeć od celów
dawn::webgpu_dawn
,glfw
iwebgpu_glfw
, aby można było później używać ich w plikumain.cpp
.
…
set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)
target_link_libraries(app PRIVATE dawn::webgpu_dawn glfw webgpu_glfw)
Otwieranie okna
Teraz, gdy Dawn jest dostępna, możesz rysować na ekranie za pomocą GLFW. Ta biblioteka zawarta w webgpu_glfw
umożliwia wygodne pisanie kodu, który jest niezależny od platformy w przypadku zarządzania oknami.
Aby otworzyć okno o nazwie „Okno WebGPU” o rozdzielczości 512 x 512, zaktualizuj plik main.cpp
w ten sposób: Pamiętaj, że parametr glfwWindowHint()
jest tu używany do żądania nieinicjowania żadnego konkretnego interfejsu API grafiki.
#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();
}
Ponowne skompilowanie aplikacji i jej uruchomienie powoduje teraz wyświetlenie pustego okna. Robisz postępy!

Pobieranie danych z urządzenia GPU
W JavaScript element navigator.gpu
jest punktem wejścia do GPU. W C++ musisz ręcznie utworzyć zmienną wgpu::Instance
, która służy do tego samego celu. Dla wygody zadeklaruj zmienną instance
u góry pliku main.cpp
i wywołuj ją jako wgpu::CreateInstance()
w pliku Init()
.
#include <webgpu/webgpu_cpp.h>
…
wgpu::Instance instance;
…
void Init() {
wgpu::InstanceDescriptor instanceDesc{
.capabilities = {.timedWaitAnyEnable = true}};
instance = wgpu::CreateInstance(&instanceDesc);
}
int main() {
Init();
Start();
}
Na początku pliku main.cpp
zadeklaruj 2 zmiennych wgpu::Adapter
i wgpu::Device
. Zmodyfikuj funkcję Init()
tak, aby wywoływała funkcję instance.RequestAdapter()
i przypisała jej wywołanie zwrotne do funkcji adapter
, a potem wywołaj funkcję adapter.RequestDevice()
i przypisz jej wywołanie zwrotne do funkcji 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);
}
Narysuj trójkąt
Łańcuch wymiany nie jest udostępniany w interfejsie JavaScript API, ponieważ zajmuje się tym przeglądarka. W języku C++ musisz go utworzyć ręcznie. Ponownie, dla wygody, zadeklaruj zmienną wgpu::Surface
u góry pliku main.cpp
. Zaraz po utworzeniu okna GLFW w Start()
wywołaj wygodną funkcję wgpu::glfw::CreateSurfaceForWindow()
, aby utworzyć wgpu::Surface
(podobny do obrazu HTML), i skonfiguruj go, wywołując nową pomocniczą funkcję ConfigureSurface()
w InitGraphics()
. Musisz też wywołać surface.Present()
, aby wyświetlić następną teksturę w pętli while. Nie ma to żadnego widocznego wpływu, ponieważ nie ma jeszcze renderowania.
#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();
}
}
Teraz możesz utworzyć potok renderowania za pomocą poniższego kodu. Aby ułatwić sobie dostęp, na początku pliku main.cpp
zadeklaruj zmienną wgpu::RenderPipeline
i w pliku InitGraphics()
wywołaj funkcję pomocniczą CreateRenderPipeline()
.
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(); }
Na koniec wysyłaj polecenia renderowania do GPU w ramach funkcji Render()
wywoływanej w każdej klatce.
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);
}
Ponowne skompilowanie aplikacji za pomocą CMake i jej uruchomienie powoduje teraz wyświetlenie długo oczekiwanego czerwonego trójkąta w oknie. Zrób sobie przerwę – zasługujesz na odpoczynek.

Kompilowanie kodu na WebAssembly
Przyjrzyjmy się teraz minimalnym zmianom wymaganym do dostosowania istniejącego kodu źródłowego, aby wyświetlał czerwony trójkąt w oknie przeglądarki. Aplikacja jest kompilowana z użyciem biblioteki emdawnwebgpu (Emscripten Dawn WebGPU), która ma implementacje webgpu.h na podstawie interfejsu JavaScript API. Używa ono Emscripten, narzędzia do kompilowania programów C/C++ do WebAssembly.
Aktualizowanie ustawień CMake
Po zainstalowaniu Emscripten zaktualizuj plik kompilacji CMakeLists.txt
w ten sposób:
Wystarczy zmienić tylko podświetlony kod.
set_target_properties
służy do automatycznego dodawania do pliku docelowego rozszerzenia „html”. Inaczej mówiąc, wygenerujesz plik „app.html”.- Biblioteka linków docelowych
emdawnwebgpu_cpp
umożliwia obsługę WebGPU w Emscripten. Bez niego plikmain.cpp
nie będzie mieć dostępu do plikuwebgpu/webgpu_cpp.h
. - Opcja
ASYNCIFY=1
umożliwia interakcję kodu synchronicznego C++ z kodem asynchronicznym JavaScript. - Opcja linku do aplikacji
USE_GLFW=3
informuje Emscripten, aby użył wbudowanej implementacji JavaScript interfejsu GLFW 3.
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")
set(DAWN_FETCH_DEPENDENCIES ON)
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 dawn::webgpu_dawn glfw webgpu_glfw)
endif()
Aktualizowanie kodu
Zamiast pętli while wywołuj funkcję emscripten_set_main_loop(Render)
, aby mieć pewność, że funkcja Render()
jest wywoływana z odpowiednią szybkością, która jest odpowiednio dopasowana do przeglądarki i monitora.
#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
}
Kompilowanie aplikacji za pomocą Emscripten
Jedyną zmianą, którą należy wprowadzić, aby skompilować aplikację za pomocą Emscripten, jest dodanie do poleceń cmake
magicznego skryptu powłoki emcmake
. Tym razem wygeneruj aplikację w podfolderze build-web
i uruchom serwer HTTP. Na koniec otwórz przeglądarkę i wejdź na 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

Co dalej?
Czego możesz się spodziewać w przyszłości:
- Poprawki w stabilności interfejsów API webgpu.h i webgpu_cpp.h.
- wstępna obsługa Dawn na Androida i iOS;
Tymczasem prosimy o przesyłanie sugestii i pytań dotyczących WebGPU w Emscripten i problemów z Dawn.
Zasoby
Możesz zapoznać się z kodem źródłowym tej aplikacji.
Jeśli chcesz dowiedzieć się więcej o tworzeniu natywnych aplikacji 3D w C++ od podstaw za pomocą WebGPU, zapoznaj się z dokumentacją WebGPU dla C++ i przykładami natywnych aplikacji WebGPU.
Jeśli interesuje Cię Rust, możesz też zapoznać się z biblioteką graficzną wgpu opartą na WebGPU. Obejrzyj ich prezentację hello-triangle.
Podziękowania
Ten artykuł został sprawdzony przez Corentina Walleza, Kaia Ninomiyę i Rachel Andrew.
Zdjęcie autorstwa Marc-Olivier Jodoin z Unsplash.