Tworzenie aplikacji za pomocą WebGPU

François Beaufort
François Beaufort

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.

Zrzut ekranu przedstawiający czerwony trójkąt korzystający z WebGPU w oknie przeglądarki i oknie aplikacji na komputerze z systemem macOS.
Ten sam trójkąt obsługiwany przez WebGPU w oknie przeglądarki i oknie na komputerze.

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, glfwwebgpu_glfw, aby można było później używać ich w pliku main.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!

Zrzut ekranu pustego okna macOS.
Puste okno.

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::Adapterwgpu::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()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.

Zrzut ekranu przedstawiający czerwony trójkąt w oknie macOS.
Czerwony trójkąt w oknie na komputerze.

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 plik main.cpp nie będzie mieć dostępu do pliku webgpu/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
Zrzut ekranu przedstawiający czerwony trójkąt w oknie przeglądarki.
Czerwony trójkąt w oknie przeglądarki.

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 Emscriptenproblemó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++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ęRachel Andrew.

Zdjęcie autorstwa Marc-Olivier JodoinUnsplash.