使用 WebGPU 建構應用程式

François Beaufort
François Beaufort

對網路開發人員而言,WebGPU 是一種網路圖形 API,可提供統一且快速的 GPU 存取方式。WebGPU 會公開新式硬體功能,並允許在 GPU 上執行算繪和運算作業,類似於 Direct3D 12、Metal 和 Vulkan。

事實是,這個故事不夠完整。WebGPU 是促進合作的成果,其中包括 Apple、Google、Intel、Mozilla 和 Microsoft 等各大公司。其中有些人意識到,WebGPU 不只是 JavaScript API,而是跨平台圖形 API,可供網頁以外的各個生態系統開發人員使用。

為滿足主要用途,我們在 Chrome 113 中推出了 JavaScript API。不過,我們也同時開發了另一個重要專案:webgpu.h C API。這個 C 標頭檔案會列出 WebGPU 的所有可用程序和資料結構。Cloud Run 可做為各平台通用的硬體抽象層,可讓您透過不同平台提供一致的介面,建構特定平台專用的應用程式。

在本文件中,您將瞭解如何使用 WebGPU 編寫在網路和特定平台上執行的小型 C++ 應用程式。劇透警告:您會看到瀏覽器視窗和電腦視窗中顯示的相同紅色三角形,只需對程式碼庫進行少許調整即可。

螢幕截圖:內含 WebGPU 技術在瀏覽器視窗中,以及 macOS 桌面視窗中的紅色三角形螢幕截圖。
瀏覽器視窗和電腦視窗中,由 WebGPU 支援的相同三角形。

運作方式

如要查看完成的應用程式,請前往 WebGPU 跨平台應用程式存放區。

這個應用程式是簡化版的 C++ 範例,可說明如何使用 WebGPU 從單一程式碼集建構電腦和網頁應用程式。實際上,它會透過名為 webgpu_cpp.h 的 C++ 包裝函式,使用 WebGPU 的 webgpu.h 做為不分平台的硬體抽象層。

在網頁上,應用程式是以 Emscripten 為基礎建構而成,而該通訊協定在 JavaScript API 上方設有實作 webgpu.h 的繫結。在 macOS 或 Windows 等特定平台上,這個專案可根據 Chromium 的跨平台 WebGPU 實作 Dawn 來建構。值得一提的是,wgpu-native 也是 webgpu.h 的 Rust 實作項目,但本文並未使用。

開始使用

首先,您需要 C++ 編譯器和 CMake,以標準方式處理跨平台建構作業。在專用資料夾中,建立 main.cpp 來源檔案和 CMakeLists.txt 建構檔案。

main.cpp 檔案目前應包含空白的 main() 函式。

int main() {}

CMakeLists.txt 檔案含有專案的基本資訊。最後一行指定可執行檔的名稱為「app」,其原始碼為 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")

請執行 cmake -B build 在「build/」子資料夾中建立建構檔案,然後執行 cmake --build build 實際建構應用程式並產生可執行檔案。

# Build the app with CMake.
$ cmake -B build && cmake --build build

# Run the app.
$ ./build/app

應用程式會執行,但尚未產生任何輸出內容,因為您需要在畫面上繪製內容。

取得 Dawn

如要繪製三角形,您可以利用 Dawn,這是 Chromium 的跨平台 WebGPU 實作。這包括用於在螢幕上繪圖的 GLFW C++ 程式庫。下載 Dawn 的方式之一是將其新增為存放區的 git 子模組。下列指令會在「dawn/」子資料夾中擷取金鑰。

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

接著,請按照下列方式附加至 CMakeLists.txt 檔案:

  • CMake DAWN_FETCH_DEPENDENCIES 選項會擷取所有 Dawn 依附元件。
  • 目標會包含 dawn/ 子資料夾。
  • 您的應用程式會依賴 dawn::webgpu_dawnglfwwebgpu_glfw 目標,以便您稍後在 main.cpp 檔案中使用這些目標。
…
set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)
target_link_libraries(app PRIVATE dawn::webgpu_dawn glfw webgpu_glfw)

開啟視窗

有了 Dawn,您就可以使用 GLFW 在畫面上繪製內容。webgpu_glfw 中包含的這個程式庫可讓您編寫不受平台限制的視窗管理程式碼。

如要開啟名為「WebGPU window」的視窗,解析度為 512x512,請更新 main.cpp 檔案,如下所示。請注意,這裡使用 glfwWindowHint() 要求不使用特定的圖形 API 初始化。

#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();
}

重新建構應用程式並照常執行後,現在會產生空白視窗。你有進步!

空白 macOS 視窗的螢幕截圖。
空白視窗。

取得 GPU 裝置

在 JavaScript 中,navigator.gpu 是存取 GPU 的進入點。在 C++ 中,您必須手動建立用於相同用途的 wgpu::Instance 變數。為了方便起見,請在 main.cpp 檔案頂端宣告 instance,然後在 main() 中呼叫 wgpu::CreateInstance()

…
#include <webgpu/webgpu_cpp.h>

wgpu::Instance instance;
…

int main() {
  instance = wgpu::CreateInstance();
  Start();
}

由於 JavaScript API 的形狀,因此存取 GPU 是採用非同步方式。在 C++ 中,建立兩個名為 GetAdapter()GetDevice() 的輔助函式,分別傳回含有 wgpu::Adapterwgpu::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));
}

為方便存取,請在 main.cpp 檔案頂端宣告兩個變數 wgpu::Adapterwgpu::Device。更新 main() 函式以呼叫 GetAdapter(),並將其結果回呼指派給 adapter,然後呼叫 GetDevice(),並在呼叫 Start() 之前將其結果回呼指派給 device

wgpu::Adapter adapter;
wgpu::Device device;
…

int main() {
  instance = wgpu::CreateInstance();
  GetAdapter([](wgpu::Adapter a) {
    adapter = a;
    GetDevice([](wgpu::Device d) {
      device = d;
      Start();
    });
  });
}

繪製三角形

瀏覽器會負責處理,因此不會在 JavaScript API 中公開交換鏈。在 C++ 中,您必須手動建立此類型。再次提醒,為了方便您參考,請在 main.cpp 檔案頂端宣告 wgpu::Surface 變數。在 Start() 中建立 GLFW 視窗後,請呼叫方便使用的 wgpu::glfw::CreateSurfaceForWindow() 函式,建立 wgpu::Surface (類似 HTML 畫布),然後在 InitGraphics() 中呼叫新的輔助 ConfigureSurface() 函式進行設定。您還需要呼叫 surface.Present(),才能在 while 迴圈中顯示下一個材質。由於尚未發生任何轉譯作業,這項操作不會造成明顯影響。

#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();
  }
}

您現在可以使用下列程式碼建立算繪管道。為方便存取,請在 main.cpp 檔案頂端宣告 wgpu::RenderPipeline 變數,並在 InitGraphics() 中呼叫輔助函式 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();
}

最後,在每個影格呼叫的 Render() 函式中,將轉譯指令傳送至 GPU。

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);
}

現在使用 CMake 重新建構應用程式並加以執行,會導致視窗中長久的紅色三角形!休息片刻,你值得好好休息。

macOS 視窗中的紅色三角形螢幕截圖。
電腦視窗中的紅色三角形。

編譯為 WebAssembly

讓我們看看,您需要做出哪些最少的變更,才能調整現有的程式碼集,在瀏覽器視窗中繪製這個紅色三角形。同樣地,應用程式會根據 Emscripten 建構,這是一種將 C/C++ 程式編譯為 WebAssembly 的工具,其中包含在 JavaScript API 上實作 webgpu.h 的繫結

更新 CMake 設定

安裝 Emscripten 後,請按照下列方式更新 CMakeLists.txt 建構檔案。您只需要變更醒目顯示的程式碼。

  • set_target_properties 是用來自動在目標檔案中加入「html」副檔名。換句話說,您會產生「app.html」檔案。
  • 如要啟用 Emscripten 中的 WebGPU 支援功能,必須使用 USE_WEBGPU 應用程式連結選項。否則,main.cpp 檔案就無法存取 webgpu/webgpu_cpp.h 檔案。
  • 您也必須在此處使用 USE_GLFW 應用程式連結選項,才能重複使用 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()

更新程式碼

在 Emscripten 中,建立 wgpu::surface 需要 HTML 畫布元素。在此情況下,請呼叫 instance.CreateSurface() 並指定 #canvas 選取器,使其符合 Emscripten 產生的 HTML 畫布元素。

請呼叫 emscripten_set_main_loop(Render),而不是使用 while 迴圈,以確保 Render() 函式以適當的平順速率呼叫,並與瀏覽器和監視器保持一致。

#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
}

使用 Emscripten 建構應用程式

如要使用 Emscripten 建構應用程式,只需在前面加上 magical emcmake 殼層指令碼 cmake 指令即可。這次,請在 build-web 子資料夾中產生應用程式,並啟動 HTTP 伺服器。最後,開啟瀏覽器並前往 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
瀏覽器視窗中的紅色三角形螢幕截圖。
瀏覽器視窗中的紅色三角形。

後續步驟

以下說明未來可能發生的異動:

  • 改善 webgpu.h 和 webgpu_cpp.h API 的穩定性。
  • Dawn 初步支援 Android 和 iOS。

在此同時,請針對 Emscripten 的 WebGPU 問題Dawn 問題提出建議和問題。

資源

歡迎探索這個應用程式的原始碼

如要進一步瞭解如何使用 WebGPU 在 C++ 中從頭開始建立原生 3D 應用程式,請參閱 瞭解 C++ 適用的 WebGPU 說明文件Dawn 原生 WebGPU 範例

如果您對 Rust 感興趣,也可以探索以 WebGPU 為基礎的 wgpu 圖形程式庫。觀看 hello-triangle 示範

特別銘謝

本文評論者為 Corentin WallezKai NinomiyaRachel Andrew

相片來源:Marc-Olivier JodoinUnsplash 網站上提供。