Tworzenie aplikacji za pomocą WebGPU

François Beaufort
François Beaufort

Data publikacji: 20 lipca 2023 r., ostatnia aktualizacja: 27 sierpnia 2025 r.

Dla programistów stron internetowych WebGPU to interfejs API grafiki internetowej, który zapewnia ujednolicony i szybki dostęp do procesorów graficznych. WebGPU udostępnia nowoczesne możliwości sprzętowe i umożliwia renderowanie oraz wykonywanie operacji obliczeniowych na GPU, podobnie jak Direct3D 12, Metal i Vulkan.

To prawda, ale ta historia jest niepełna. WebGPU to efekt współpracy wielu firm, w tym Apple, Google, Intel, Mozilla i Microsoft. Niektórzy z nich zdali sobie sprawę, że WebGPU może być czymś więcej niż tylko interfejsem API JavaScriptu. Może być wieloplatformowym interfejsem API grafiki dla deweloperów w różnych ekosystemach, nie tylko w internecie.

Aby zrealizować główny przypadek użycia, w Chrome 113 wprowadziliśmy interfejs JavaScript API. Równolegle z nim powstał jednak inny ważny projekt: interfejs API webgpu.h w języku C. Ten plik nagłówkowy C zawiera listę wszystkich dostępnych procedur i struktur danych WebGPU. Jest to niezależna od platformy warstwa abstrakcji sprzętu, która umożliwia tworzenie aplikacji na konkretne platformy dzięki zapewnieniu spójnego interfejsu na różnych platformach.

Z tego dokumentu dowiesz się, jak napisać małą aplikację w C++ korzystającą z WebGPU, która działa zarówno w internecie, jak i na określonych platformach. Spoiler alert: uzyskasz ten sam czerwony trójkąt, który pojawia się w oknie przeglądarki i oknie na komputerze, przy minimalnych zmianach w bazie kodu.

Zrzut ekranu przedstawiający czerwony trójkąt renderowany przez WebGPU w oknie przeglądarki i oknie na komputerze z systemem macOS.
Ten sam trójkąt renderowany za pomocą WebGPU w oknie przeglądarki i oknie na komputerze.

Jak to działa?

Aby zobaczyć gotową aplikację, zapoznaj się z repozytorium aplikacji wieloplatformowej WebGPU.

Jest to minimalistyczny przykład w C++, który pokazuje, jak za pomocą WebGPU tworzyć aplikacje na komputery i do internetu z jednego kodu. W tle używa webgpu.h z WebGPU jako niezależnej od platformy warstwy abstrakcji sprzętowej za pomocą otoki C++ o nazwie webgpu_cpp.h.

W internecie aplikacja jest tworzona na podstawie emdawnwebgpu (Emscripten Dawn WebGPU), która ma powiązania implementujące webgpu.h na podstawie interfejsu JavaScript API. Na niektórych platformach, takich jak macOS czy Windows, ten projekt można skompilować na podstawie Dawn, czyli wieloplatformowej implementacji WebGPU w Chromium. Warto wspomnieć, że istnieje też wgpu-native, czyli implementacja webgpu.h w języku Rust, ale nie jest ona używana w tym dokumencie.

Rozpocznij

Na początek potrzebujesz kompilatora C++ i CMake, aby obsługiwać kompilacje na różnych platformach w standardowy sposób. W odpowiednim 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.22) # 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 polecenie cmake --build build, aby 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 danych wyjściowych, ponieważ musisz mieć możliwość rysowania elementów na ekranie.

Get Dawn

Aby narysować trójkąt, możesz skorzystać z Dawn, czyli wieloplatformowej implementacji WebGPU w Chromium. Obejmuje to bibliotekę C++ GLFW do rysowania na ekranie. Jednym ze sposobów pobrania Dawn jest dodanie go jako podmodułu Git do repozytorium. Poniższe polecenia pobierają go w podfolderze „dawn/”.

$ git init
$ git submodule add https://dawn.googlesource.com/dawn

Następnie dodaj do pliku CMakeLists.txt te wiersze:

  • Opcja CMake DAWN_FETCH_DEPENDENCIES pobiera wszystkie zależności Dawn.
  • Opcja CMake DAWN_BUILD_MONOLITHIC_LIBRARY łączy wszystkie komponenty Dawn w jedną bibliotekę.
  • Folder dawn/ jest uwzględniony w miejscu docelowym.
  • Aplikacja będzie zależeć od platform webgpu_dawn, webgpu_glfwglfw, dzięki czemu będzie można ich później używać w pliku main.cpp.

set(DAWN_FETCH_DEPENDENCIES ON)
set(DAWN_BUILD_MONOLITHIC_LIBRARY STATIC)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)

target_link_libraries(app PRIVATE webgpu_dawn webgpu_glfw glfw)

Otwieranie okna

Teraz, gdy Dawn jest dostępny, użyj GLFW, aby narysować elementy na ekranie. Ta biblioteka, która jest dołączona do webgpu_glfw, umożliwia pisanie kodu niezależnego od platformy do zarządzania oknami.

Aby otworzyć okno o nazwie „WebGPU window” i rozdzielczości 512 x 512, zaktualizuj plik main.cpp w sposób podany poniżej. Pamiętaj, że w tym przypadku używamy wartości glfwWindowHint(), aby nie żądać inicjowania 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 uruchomienie jej w taki sam sposób jak wcześniej spowoduje wyświetlenie pustego okna. Robisz postępy!

Zrzut ekranu pustego okna macOS.
Puste okno.

Pobieranie urządzenia GPU

W JavaScript punktem wejścia do GPU jest navigator.gpu. W C++ musisz ręcznie utworzyć zmienną wgpu::Instance, która będzie służyć do tego samego celu. Dla wygody zadeklaruj instance na początku pliku main.cpp i wywołaj wgpu::CreateInstance() wewnątrz Init().

#include <webgpu/webgpu_cpp.h>


wgpu::Instance instance;


void Init() {
  static const auto kTimedWaitAny = wgpu::InstanceFeatureName::TimedWaitAny;
  wgpu::InstanceDescriptor instanceDesc{.requiredFeatureCount = 1,
                                        .requiredFeatures = &kTimedWaitAny};
  instance = wgpu::CreateInstance(&instanceDesc);
}

int main() {
  Init();
  Start();
}

Zadeklaruj 2 zmienne: wgpu::Adapterwgpu::Device u góry pliku main.cpp. Zmodyfikuj funkcję Init() tak, aby wywoływała funkcję instance.RequestAdapter() i przypisywała jej wywołanie zwrotne wyniku do zmiennej adapter, a następnie wywoływała funkcję adapter.RequestDevice() i przypisywała jej wywołanie zwrotne wyniku do zmiennej 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ż przeglądarka sama się nim zajmuje. W C++ musisz utworzyć go ręcznie. Ponownie dla wygody zadeklaruj zmienną wgpu::Surface u góry pliku main.cpp. Zaraz po utworzeniu okna GLFW w Start() wywołaj przydatną funkcję wgpu::glfw::CreateSurfaceForWindow(), aby utworzyć wgpu::Surface (podobny do elementu canvas w HTML) i skonfigurować go, wywołując nową funkcję pomocniczą ConfigureSurface()InitGraphics(). Musisz też wywołać funkcję surface.Present(), aby w pętli while wyświetlić kolejną teksturę. Nie ma to widocznego efektu, ponieważ nie następuje jeszcze renderowanie.

#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ć dostęp, zadeklaruj zmienną wgpu::RenderPipeline u góry pliku main.cpp i wywołaj funkcję pomocniczą CreateRenderPipeline() w 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::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 wyślij polecenia renderowania do procesora graficznego 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 spowoduje wyświetlenie w oknie długo oczekiwanego czerwonego trójkąta. Zrób sobie przerwę – zasługujesz na to.

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

Kompilowanie do WebAssembly

Przyjrzyjmy się teraz minimalnym zmianom, które należy wprowadzić w dotychczasowym kodzie, aby narysować ten czerwony trójkąt w oknie przeglądarki. Aplikacja jest ponownie zbudowana na podstawie emdawnwebgpu (Emscripten Dawn WebGPU), która ma powiązania implementujące webgpu.h na podstawie interfejsu JavaScript API. Korzysta z Emscripten, narzędzia do kompilowania programów w językach C/C++ do WebAssembly.

Aktualizowanie ustawień CMake

Po zainstalowaniu Emscripten zaktualizuj CMakeLists.txt plik kompilacji w ten sposób: Zaznaczony kod to jedyna rzecz, którą musisz zmienić.

  • set_target_properties służy do automatycznego dodawania rozszerzenia pliku „html” do pliku docelowego. 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 miał dostępu do pliku webgpu/webgpu_cpp.h.
  • Opcja ASYNCIFY=1 linku do aplikacji umożliwia synchronicznemu kodowi C++ interakcję z asynchronicznym kodem JavaScript.
  • Opcja USE_GLFW=3 linku do aplikacji informuje Emscripten, aby używał wbudowanej implementacji interfejsu GLFW 3 w JavaScripcie.
cmake_minimum_required(VERSION 3.22) # 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)
set(DAWN_BUILD_MONOLITHIC_LIBRARY STATIC)
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 webgpu_dawn webgpu_glfw glfw)
endif()

Aktualizowanie kodu

Zamiast pętli while wywołaj funkcję emscripten_set_main_loop(Render), aby mieć pewność, że funkcja Render() jest wywoływana z odpowiednią płynnością, która jest zgodna z przeglądarką i monitorem.

#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

Jedyna zmiana, jakiej wymaga skompilowanie aplikacji za pomocą Emscripten, to dodanie do poleceń cmake magicznego skryptu powłoki emcmake. Tym razem wygeneruj aplikację w build-web podfolderze i uruchom serwer HTTP. Na koniec otwórz przeglądarkę i wejdź na stronę 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?

Oto czego możesz się spodziewać w przyszłości:

  • Ulepszenia stabilizacji interfejsów API webgpu.h i webgpu_cpp.h.
  • Wstępna obsługa Dawn na Androidzie i iOS.

W międzyczasie zgłaszaj problemy z WebGPU w Emscriptenproblemy z Dawn, podając sugestie i zadając pytania.

Zasoby

Możesz zapoznać się z kodem źródłowym tej aplikacji.

Jeśli chcesz dowiedzieć się więcej o tworzeniu od podstaw natywnych aplikacji 3D w C++ za pomocą WebGPU, zapoznaj się z dokumentacją Learn WebGPU for C++przykładami Dawn Native WebGPU.

Jeśli interesuje Cię Rust, możesz też zapoznać się z biblioteką graficzną wgpu opartą na WebGPU. Zapoznaj się z prezentacją hello-triangle.

Podziękowania

Ten artykuł został sprawdzony przez Corentina Walleza, Kaia NinomiyęRachel Andrew.

Zdjęcie: Marc-Olivier Jodoin, Unsplash