ウェブ デベロッパーにとって、WebGPU は GPU への統一された迅速なアクセスを提供するウェブ グラフィック API です。WebGPU は、Direct3D 12、Metal、Vulkan と同様に、最新のハードウェア機能を公開し、GPU 上でレンダリングと計算処理を行うことができます。
しかし、その話は完全とは言えません。WebGPU は、Apple、Google、Intel、Mozilla、Microsoft などの大手企業による共同の取り組みの成果です。中でも、WebGPU は JavaScript API ではなく、ウェブ以外のエコシステムのデベロッパーにとってクロス プラットフォームのグラフィック API になり得ると認識する方もいました。
主なユースケースに対応するため、Chrome 113 で JavaScript API が導入されました。ただし、これと並行して別の重要なプロジェクトである webgpu.h C API も開発されています。この C ヘッダー ファイルには、WebGPU で利用可能なすべてのプロシージャとデータ構造がリストされています。プラットフォームに依存しないハードウェア抽象化レイヤとして機能し、異なるプラットフォーム間で一貫したインターフェースを提供することで、プラットフォーム固有のアプリケーションを構築できます。
このドキュメントでは、WebGPU を使用して、ウェブと特定のプラットフォームの両方で動作する小規模な C++ アプリを作成する方法について説明します。今回はブラウザ ウィンドウとデスクトップ ウィンドウにそれぞれ赤い三角形が表示されますが、コードベースを少し調整するだけでこのようになります。
![WebGPU による赤い三角形の macOS のブラウザ ウィンドウとデスクトップ ウィンドウのスクリーンショット。](https://developer.chrome.google.cn/static/docs/web-platform/webgpu/build-app/image/screenshot-a-red-triangl-82836ca46abb9.png?authuser=6&hl=ja)
どのように機能するのか?
完成したアプリケーションを確認するには、WebGPU クロスプラットフォーム アプリ リポジトリをご覧ください。
このアプリは C++ のシンプルな例で、WebGPU を使用して単一のコードベースからデスクトップ アプリとウェブアプリを構築する方法を示しています。内部では、WebGPU の webgpu.h が、webgpu_cpp.h という C++ ラッパーを介してプラットフォームに依存しないハードウェア抽象化レイヤとして使用されます。
ウェブの場合、アプリは JavaScript API の上に webgpu.h を実装するバインディングを持つ Emscripten に対してビルドされます。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
アプリは実行されますが、画面に何かを描画する必要があるため、出力はまだありません。
夜明けを迎える
三角形を描画するには、Chromium のクロス プラットフォームの WebGPU 実装である Dawn を利用できます。これには、画面に描画するための GLFW C++ ライブラリが含まれます。Dawn をダウンロードする方法の一つは、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
に含まれているこのライブラリを使用すると、ウィンドウ管理に関してプラットフォームに依存しないコードを記述できます。
解像度 512x512 の「WebGPU window」という名前のウィンドウを開くには、以下のように 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 ウィンドウのスクリーンショット。](https://developer.chrome.google.cn/static/docs/web-platform/webgpu/build-app/image/screenshot-a-empty-macos-55308df210fa3.png?authuser=6&hl=ja)
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()
という 2 つのヘルパー関数を作成します。これらはそれぞれ 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
の 2 つの変数を宣言します。GetAdapter()
を呼び出すように main()
関数を更新し、結果のコールバックを adapter
に割り当ててから、GetDevice()
を呼び出して結果のコールバックを device
に代入してから、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();
});
});
}
三角形を描く
スワップ チェーンはブラウザが処理するため、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 ウィンドウの赤い三角形のスクリーンショット。](https://developer.chrome.google.cn/static/docs/web-platform/webgpu/build-app/image/screenshot-a-red-triangl-403ff28f43f9.png?authuser=6&hl=ja)
WebAssembly にコンパイルする
では、既存のコードベースを調整して、ブラウザ ウィンドウでこの赤い三角形を描画するために必要な最小限の変更を見てみましょう。ここでも、このアプリは C/C++ プログラムを WebAssembly にコンパイルするツールである Emscripten に対してビルドされています。Emscripten には JavaScript API の上に webgpu.h を実装するバインディングがあります。
CMake 設定を更新する
Emscripten をインストールしたら、次のように CMakeLists.txt
ビルドファイルを更新します。ハイライト表示されたコードのみ変更する必要があります。
set_target_properties
を使用すると、「html」ファイル拡張子がターゲット ファイルに自動的に追加されます。「app.html」ファイルを生成します。- Emscripten で WebGPU サポートを有効にするには、
USE_WEBGPU
アプリリンク オプションが必要です。この権限がないと、main.cpp
ファイルはwebgpu/webgpu_cpp.h
ファイルにアクセスできません。 - GLFW コードを再利用できるように、ここでは
USE_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()
を呼び出し、Emscripten によって生成された HTML ページの適切な HTML キャンバス要素と一致する #canvas
セレクタを指定します。
while ループを使用する代わりに 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
![ブラウザ ウィンドウの赤い三角形のスクリーンショット。](https://developer.chrome.google.cn/static/docs/web-platform/webgpu/build-app/image/screenshot-a-red-triangl-4d54843f1cde3.png?authuser=6&hl=ja)
次のステップ
今後の流れ:
- webgpu.h API と webgpu_cpp.h API の安定化を改善しました。
- Android と iOS で Dawn を初期サポート。
それまでは、提案や質問があれば、Emscripten の WebGPU に関する問題と Dawn に関する問題に報告してください。
リソース
このアプリのソースコードは自由にご確認ください。
WebGPU を使用して C++ ネイティブ 3D アプリケーションをゼロから作成する場合は、C++ 用 WebGPU について学ぶと Dawn Native WebGPU の例をご覧ください。
Rust に興味がある方は、WebGPU をベースにした wgpu グラフィック ライブラリもご覧ください。hello-triangle デモをご覧ください。
謝辞
この記事は、Corentin Wallez、Kai Ninomiya、Rachel Andrew によってレビューされました。
写真撮影: Marc-Olivier Jodoin(Unsplash)