WebGPU でアプリをビルドする

François Beaufort
François Beaufort

公開日: 2023 年 7 月 20 日、最終更新日: 2025 年 6 月 17 日

ウェブ デベロッパーにとって、WebGPU は GPU への統合された高速アクセスを提供するウェブ グラフィック API です。WebGPU は最新のハードウェア機能を公開し、Direct3D 12、Metal、Vulkan と同様に GPU でのレンダリングと計算オペレーションを可能にします。

これは事実ですが、このストーリーは不完全です。WebGPU は、Apple、Google、Intel、Mozilla、Microsoft などの大手企業による共同作業の成果です。その中には、WebGPU が JavaScript API 以上のもので、ウェブ以外のエコシステム全体のデベロッパー向けのクロス プラットフォーム グラフィック API になり得ると気付いた人もいます。

主なユースケースを実現するため、Chrome 113 で JavaScript API が導入されました。ただし、これと並行して、もう 1 つの重要なプロジェクトである webgpu.h C API も開発されています。この C ヘッダー ファイルには、WebGPU で使用可能なすべてのプロシージャとデータ構造が一覧表示されます。これはプラットフォームに依存しないハードウェア抽象化レイヤとして機能し、さまざまなプラットフォームで一貫したインターフェースを提供することで、プラットフォーム固有のアプリケーションを構築できます。

このドキュメントでは、WebGPU を使用して、ウェブと特定のプラットフォームの両方で実行される小さな C++ アプリを作成する方法について説明します。コードベースを最小限に調整するだけで、ブラウザ ウィンドウとパソコンのウィンドウに同じ赤い三角形が表示されます。

macOS のブラウザ ウィンドウとデスクトップ ウィンドウに表示される、WebGPU による赤い三角形のスクリーンショット。
WebGPU を搭載した同じ三角形が、ブラウザ ウィンドウとデスクトップ ウィンドウで表示されます。

仕組み

完成したアプリケーションを確認するには、WebGPU クロスプラットフォーム アプリのリポジトリをご覧ください。

このアプリは、WebGPU を使用して 1 つのコードベースからデスクトップ アプリとウェブアプリを作成する方法を示す、ミニマルな C++ の例です。内部的には、webgpu_cpp.h という C++ ラッパーを介して、プラットフォームに依存しないハードウェア抽象化レイヤとして WebGPU の webgpu.h を使用します。

ウェブでは、アプリは emdawnwebgpu(Emscripten Dawn WebGPU)に対してビルドされます。これは、JavaScript API の上に webgpu.h を実装するバインディングを備えています。macOS や Windows などの特定のプラットフォームでは、このプロジェクトは Chromium のクロスプラットフォーム WebGPU 実装である Dawn に対してビルドできます。webgpu.h の Rust 実装である wgpu-native も存在しますが、このドキュメントでは使用しません。

始める

まず、クロスプラットフォーム ビルドを標準的な方法で処理するための 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 実装である Dawn を利用できます。これには、画面に描画するための 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 に含まれています。これにより、ウィンドウ管理にプラットフォームに依存しないコードを記述できます。

解像度が 512x512 の「WebGPU ウィンドウ」という名前のウィンドウを開くには、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 を宣言し、Init() 内で wgpu::CreateInstance() を呼び出します。

#include <webgpu/webgpu_cpp.h>


wgpu::Instance instance;


void Init() {
  wgpu::InstanceDescriptor instanceDesc{
      .capabilities = {.timedWaitAnyEnable = true}};
  instance = wgpu::CreateInstance(&instanceDesc);
}

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

main.cpp ファイルの上部で、2 つの変数 wgpu::Adapterwgpu::Device を宣言します。Init() 関数を更新して instance.RequestAdapter() を呼び出し、その結果コールバックを adapter に割り当て、adapter.RequestDevice() を呼び出してその結果コールバックを 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);
}

三角形を描画する

スワップチェーンはブラウザが処理するため、JavaScript API では公開されません。C++ では、手動で作成する必要があります。便宜上、main.cpp ファイルの上部に wgpu::Surface 変数を宣言します。Start() で GLFW ウィンドウを作成した直後に、便利な wgpu::glfw::CreateSurfaceForWindow() 関数を呼び出して wgpu::Surface(HTML キャンバスに似ている)を作成し、InitGraphics() で新しいヘルパー関数 ConfigureSurface() を呼び出して構成します。また、while ループで次のテクスチャを表示するために 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,
                                    .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();
  }
}

次に、以下のコードを使用してレンダリング パイプラインを作成します。アクセスを容易にするため、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::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();
}

最後に、フレームごとに呼び出される 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 ウィンドウに表示された赤い三角形のスクリーンショット。
PC のウィンドウに赤い三角形が表示されている。

WebAssembly にコンパイルする

ブラウザ ウィンドウにこの赤い三角形を描画するように既存のコードベースを調整するために必要な最小限の変更を見てみましょう。このアプリは emdawnwebgpu(Emscripten Dawn WebGPU)に対してビルドされています。このライブラリには、JavaScript API 上に webgpu.h を実装するバインディングが含まれています。C/C++ プログラムを WebAssembly にコンパイルするツールである Emscripten を使用します。

CMake 設定を更新する

Emscripten がインストールされたら、次のように CMakeLists.txt ビルドファイルを更新します。変更する必要があるのはハイライト表示されたコードのみです。

  • set_target_properties は、ターゲット ファイルに「html」ファイル拡張子を自動的に追加するために使用します。つまり、「app.html」ファイルを生成します。
  • emdawnwebgpu_cpp ターゲット リンク ライブラリにより、Emscripten で WebGPU のサポートが可能になります。このファイルがないと、main.cpp ファイルは webgpu/webgpu_cpp.h ファイルにアクセスできません。
  • ASYNCIFY=1 アプリリンク オプションを使用すると、同期 C++ コードが非同期 JavaScript とやり取りできます。
  • USE_GLFW=3 アプリリンク オプションは、GLFW 3 API の組み込み JavaScript 実装を使用するように Emscripten に指示します。
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()

コードを更新する

while ループを使用する代わりに emscripten_set_main_loop(Render) を呼び出して、Render() 関数がブラウザとモニタに適切に揃う適切なスムーズなレートで呼び出されるようにします。

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

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 API と webgpu_cpp.h API の安定性を改善しました。
  • Android と iOS での Dawn の初期サポート。

ご意見やご質問がある場合は、Emscripten の WebGPU に関する問題Dawn に関する問題を報告してください。

リソース

このアプリのソースコードを参照してください。

WebGPU を使用して C++ でネイティブ 3D アプリケーションをゼロから作成する方法について詳しくは、Learn WebGPU for C++ のドキュメントDawn Native WebGPU の例をご覧ください。

Rust に興味がある場合は、WebGPU ベースの wgpu グラフィック ライブラリもご覧ください。hello-triangle デモをご覧ください。

謝辞

この記事は、Corentin WallezKai NinomiyaRachel Andrew が確認しました。

写真: Marc-Olivier JodoinUnsplash より。