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. Oprócz tego rozwija się jednak inny ważny projekt: webgpu.h C API. Ten plik nagłówka C zawiera listę wszystkich dostępnych procedur i struktur danych WebGPU. Stanowi warstwę abstrakcji sprzętowej niezależną od platformy, co pozwala tworzyć aplikacje na konkretne 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.
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 internet 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 Emscripten, który ma wiązania implementujące 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 na razie 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 cmake -B build
, aby utworzyć pliki kompilacji w podfolderze „build/”, a cmake --build build
, aby faktycznie skompilować aplikację 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 narysować coś na ekranie.
Get Dawn
Aby narysować trójkąt, możesz skorzystać z Dawn, czyli wieloplatformowej implementacji procesora WebGPU 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. Poniższe polecenia pobierają go do podfolderu „dawny/”.
$ 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 dostępna w webgpu_glfw
dla wygody umożliwia pisanie kodu niezależnego 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: Zwróć uwagę, że parametr glfwWindowHint()
jest tu używany do żądania braku inicjalizacji 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();
}
Przebudowanie aplikacji i uruchomienie jej tak jak dotychczas powoduje wyświetlenie pustego okna. Robisz postępy!
Pobranie urządzenia GPU
W JavaScript element navigator.gpu
jest punktem wejścia do dostępu do GPU. W języku 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 main()
.
…
#include <webgpu/webgpu_cpp.h>
wgpu::Instance instance;
…
int main() {
instance = wgpu::CreateInstance();
Start();
}
Dostęp do GPU jest asynchroniczny ze względu na kształt interfejsu JavaScript API. W C++ utwórz 2 funkcje pomocnicze o nazwach GetAdapter()
i GetDevice()
, które zwracają odpowiednio funkcję wywołania zwrotnego z argumentem wgpu::Adapter
i 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));
}
Aby ułatwić sobie dostęp, u góry pliku main.cpp
zadeklaruj 2 zmiennych wgpu::Adapter
i wgpu::Device
. Zmodyfikuj funkcję main()
tak, aby wywoływała funkcję GetAdapter()
i przypisała jej wywołanie zwrotne do funkcji adapter
, a następnie wywołaj funkcję GetDevice()
i przypisz jej wywołanie zwrotne do funkcji device
, zanim wywołasz funkcję 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();
});
});
}
Narysuj trójkąt
Łańcuch wymiany nie jest widoczny w interfejsie JavaScript API, ponieważ zajmuje się nim 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 rysunku na kanwie 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};
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ą kodu poniżej. 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::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();
}
Na koniec wysyłaj polecenia renderowania do GPU w 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 wyświetlenie długo oczekiwanego czerwonego trójkąta w oknie. Zrób sobie przerwę – zasługujesz na odpoczynek.
Kompilowanie do WebAssembly
Przyjrzyjmy się teraz minimalnym zmianom wymaganym do dostosowania istniejącej bazy kodu, aby wyświetlała czerwony trójkąt w oknie przeglądarki. Aplikacja jest skompilowana za pomocą Emscripten, czyli narzędzia do kompilowania programów C/C++ do WebAssembly, które ma wiązania implementujące webgpu.h na podstawie interfejsu JavaScript API.
Aktualizowanie ustawień CMake
Po zainstalowaniu Emscripten zaktualizuj plik kompilacji CMakeLists.txt
w następujący sposób.
Trzeba tylko zmienić zaznaczony kod.
set_target_properties
służy do automatycznego dodawania do pliku docelowego rozszerzenia „html”. Inaczej mówiąc, wygenerujesz plik „app.html”.- Opcja linku do aplikacji
USE_WEBGPU
jest wymagana do włączenia obsługi WebGPU w Emscripten. Bez niego plikmain.cpp
nie będzie mieć dostępu do plikuwebgpu/webgpu_cpp.h
. - Opcja
USE_GLFW
link do aplikacji jest też wymagana, aby można było ponownie użyć kodu 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()
Aktualizowanie kodu
W Emscripten tworzenie wgpu::surface
wymaga elementu HTML canvas. W tym celu wywołaj funkcję instance.CreateSurface()
i określ selektor #canvas
, aby dopasować odpowiedni element kanwy HTML na stronie HTML wygenerowanej przez Emscripten.
Zamiast pętli while wywołaj funkcję emscripten_set_main_loop(Render)
, aby mieć pewność, że funkcja Render()
jest wywoływana z odpowiednią częstotliwością, która jest odpowiednio dopasowana do przeglądarki i monitora.
#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
}
Utwórz aplikację za pomocą Emscripten
Jedyną zmianą, jaką 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?
Oto, 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;
W międzyczasie zgłaszaj sugestie i pytania dotyczące problemów z WebGPU w Emscripten oraz Dawn.
Zasoby
Możesz też 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.
Poświadczenia
Ten artykuł został sprawdzony przez Corentina Walleza, Kaia Ninomiya i Rachel Andrew.
Zdjęcie autorstwa Marc-Olivier Jodoin z Unsplash.