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 所有可用的程序和資料結構。它可做為各平台通用的硬體抽象層,可讓您在不同平台上提供一致的介面,藉此建構特定平台的應用程式。
本文件將說明如何使用可在網路和特定平台上執行的 WebGPU 編寫小型 C++ 應用程式。劇透警示:您可看到與瀏覽器視窗相同的紅色三角形和桌面視窗,只需對程式碼集進行少許調整即可。
運作原理
如要查看已完成的應用程式,請參閱 WebGPU 跨平台應用程式存放區。
這款應用程式是極簡風的 C++ 範例,示範如何使用 WebGPU 從單一程式碼集建構電腦版和網頁應用程式。基本上,這個程式庫會透過名為 webgpu_cpp.h 的 C++ 包裝函式,使用 WebGPU 的 webgpu.h 做為各平台通用的硬體抽象層。
在網站上,應用程式是依據 Emscripten 打造而成,該繫結在 JavaScript API 上方實作 webgpu.h。在 macOS 或 Windows 等特定平台上,您可以根據 Dawn (Chromium 的跨平台 WebGPU 實作) 建構這項專案。值得一提的是 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 (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/
子資料夾包含在目標中。- 您的應用程式會依附於
webgpu_cpp
、webgpu_dawn
、glfw
和webgpu_glfw
目標,方便稍後在main.cpp
檔案中使用。
…
set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)
target_link_libraries(app PRIVATE webgpu_cpp webgpu_dawn glfw webgpu_glfw)
開啟視窗
現在「Dawn」已可使用,請使用 GLFW 在螢幕上繪製內容。為方便起見,此 webgpu_glfw
中包含的程式庫可讓您編寫適用於各種視窗的程式碼。
如要開啟名稱為「WebGPU 視窗」且解析度為 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();
}
重新建構並執行應用程式,現在會產生空白視窗。對你很有進展!
取得 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::Adapter
和 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));
}
為方便存取,請在 main.cpp
檔案頂端宣告兩個變數 wgpu::Adapter
和 wgpu::Device
。更新 main()
函式以呼叫 GetAdapter()
,並指派其結果回呼給 adapter
,然後在呼叫 Start()
之前呼叫 GetDevice()
,並指派其結果回呼給 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()
,以在迴圈中顯示下一個紋理。由於尚未進行轉譯,這不會產生任何可見效果。
#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 重新建構並執行應用程式,會導致視窗中出現等待長的紅色三角形!休息一下吧!
與 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 webgpu_cpp webgpu_dawn glfw webgpu_glfw)
endif()
更新程式碼
在 Emscripten 中,建立 wgpu::surface
時需要 HTML 畫布元素。為此,請呼叫 instance.CreateSurface()
並指定 #canvas
選取器,以符合 Emscripten 產生的 HTML 頁面中的適當 HTML 畫布元素。
不要使用 With 迴圈,呼叫 emscripten_set_main_loop(Render)
來確保 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 建構應用程式時,唯一需要進行的變更,是在 cmake
指令前面加上 emcmake
殼層指令碼。這次請在 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 Wallez、Kai Ninomiya 和 Rachel Andrew 審查本文。
相片來源:Marc-Olivier Jodoin 於 Unsplash 網站上。