wgpuとは?RustでWebGPUを扱う仕組みと最小サンプル・主要API
wgpuは、次世代のグラフィックス標準であるWebGPU仕様をRustで実装した、クロスプラットフォームのグラフィックス/GPU計算ライブラリです。ブラウザ内だけでなくネイティブアプリでも同じコードが動き、内部ではVulkan・Metal・DirectX 12・OpenGLといった各OSのAPIへ振り分けられます。ゲームエンジンBevyやRust製ディープラーニングフレームワークBurnの描画・GPU計算基盤としても使われており、Rustでリアルタイム描画やGPGPUを扱う際の事実上の標準になっています。この記事では、wgpuとWebGPUの関係、Instance/Adapter/Device/Surfaceの初期化、画面クリアから三角形描画までの動く最小サンプル、WGSLシェーダー、ウィンドウ不要のGPU計算(Compute)、対応バックエンド、そして2026年最新のwgpu 30で入った破壊的変更までを、実際のコードで整理します。
まとめ:wgpu(Rust)の要点
- 正体:WebGPU仕様のRust実装。ブラウザ(wasm)とネイティブの両方で動き、内部でVulkan/Metal/DX12/GL/WebGPUへ振り分ける抽象化レイヤー。
- 初期化の流れ:
Instance→Adapter→Device/Queue→Surface。request_adapter・request_deviceは非同期(.await)で、いずれもResultを返す。 - APIはsnake_case:JavaScript版WebGPUの
requestAdapter等をそのまま書くとRustでは動かない。instance.request_adapter()のようにRustの命名へ読み替える。 - 描画:
Surfaceからフレームを取得→RenderPassでクリア/描画→queue.submit()→present()。三角形はWGSLの頂点・フラグメントシェーダーとRenderPipelineで描く。 - ウィンドウ不要のGPGPUもサポート(
create_compute_pipeline)。DLフレームワークBurnのwgpuバックエンドがこの仕組みを使う。 - バージョン注意:最新はwgpu 30.0.0(2026年7月1日)。約3か月ごとに破壊的変更が入るため、Cargo.tomlでメジャーを固定し、上げるときはCHANGELOGを確認する。
以下、それぞれを動くコードとともに具体的に見ていきます。
wgpuとは何か:WebGPU仕様のRust実装
wgpuを一言でいえば「WebGPUという標準仕様を、Rustで書かれたライブラリとして実装したもの」です。WebGPUはW3Cが策定する新しいグラフィックス/GPU計算のWeb標準で、旧来のWebGLよりも低レベルで、モダンなGPU(Vulkan/Metal/DirectX 12)の設計に沿っています。wgpuはこの仕様に準拠しつつ、ブラウザの外=ネイティブでも同じAPIを使えるようにした点が最大の特徴です。
WebGPU仕様・ブラウザ実装・wgpuの関係
「WebGPU」には3つの層があります。第一に、APIの振る舞いを定める仕様(W3C)。第二に、ブラウザに組み込まれた実装(ChromeやFirefoxのWebGPU)。第三に、その仕様をアプリから使うためのライブラリで、Rustのwgpuはここに当たります。実際、Firefoxのブラウザ内WebGPU実装はwgpuを土台にしています。つまりwgpuは「ブラウザに載る側」でもあり「アプリが呼ぶ側」でもある、両用のコードベースです。
JavaScriptのWebGPU(camelCase)とRust wgpu(snake_case)の対応
WebGPUを解説する日本語情報はJavaScript前提のものが多く、メソッド名がcamelCase(requestAdapterなど)で書かれています。Rustのwgpuでは命名規約が異なり、同じ操作をsnake_caseで呼びます。JS向けのコード片をそのままRustに貼ると必ずコンパイルエラーになるため、次の対応で読み替えてください。
| 操作 | JavaScript(WebGPU) | Rust(wgpu) |
|---|---|---|
| アダプタ取得 | navigator.gpu.requestAdapter() |
instance.request_adapter() |
| デバイス取得 | adapter.requestDevice() |
adapter.request_device() |
| Surface生成 | canvas.getContext("webgpu") |
instance.create_surface() |
| フレーム取得 | context.getCurrentTexture() |
surface.get_current_texture() |
| コマンド送出 | device.queue.submit() |
queue.submit() |
ポイントは、Rust側では「グローバルなnavigator.gpu」に相当するものがwgpu::Instanceという明示的なオブジェクトになっている点です。まずInstanceを作り、そこからAdapterとSurfaceを生やします。
なぜRustでGPUを扱うのか
GPUプログラミングは生ポインタ・手動のメモリ管理・ライフタイムの絡む世界で、C++では未定義動作の温床になりがちです。Rustはコンパイル時に所有権と借用を検査するため、この層のバグ(解放済みリソースの参照、データ競合)を型で防げます。加えてwgpuは同じコードがネイティブとブラウザ(wasm)で動くので、デスクトップ向けに書いた描画処理をWebへ持ち出す、あるいは逆、といった移植コストが小さいのも実務上の利点です。
導入:Cargo.tomlと対応バージョン(wgpu 30)
2026年7月時点の最新はwgpu 30.0.0です。ウィンドウ管理にはwinit、非同期関数を同期的に待つためにpollsterを併用するのが定番構成です。バージョンはメジャーで固定します(後述のとおり破壊的変更が頻繁なため)。
[dependencies]
wgpu = "30"
winit = "0.30"
pollster = "0.4" # request_adapter などの Future を同期的に待つ
winitは0.30からApplicationHandlerトレイトを実装する形に変わりました。古い記事にあるevent_loop.run(|event, _| ...)のクロージャ形式はもう使いません。この違いが最初のつまずきどころなので、後の描画セクションで具体的に触れます。
初期化の4オブジェクト:Instance→Adapter→Device/Queue→Surface
wgpuの初期化は、4つのオブジェクトを順に作る流れで固定されています。Instance(wgpuの入口)→Surface(描画先ウィンドウ)→Adapter(物理GPUの選択)→Device/Queue(論理デバイスとコマンド投入口)です。次のコードは、winitのウィンドウ(Arcで包む)を受け取り、描画準備までを行う非同期関数の中身です。
// window: Arc<winit::window::Window>(0.30ではArcで包むのが定石)
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::PRIMARY, // Vulkan / Metal / DX12 / WebGPU
..Default::default()
});
// Surface生成はResultを返す。Arc<Window>を渡すと'staticなSurfaceになる
let surface = instance.create_surface(window.clone()).unwrap();
// Adapter取得は非同期。旧版のOptionではなくResultを返す(24/25以降)
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: Some(&surface),
..Default::default()
})
.await
.unwrap();
// DeviceとQueueを取得。第2引数(旧トレースパス)は廃止されDescriptorに統合
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("main device"),
..Default::default()
})
.await
.unwrap();
// Surfaceを画面サイズで構成
let size = window.inner_size();
let config = surface
.get_default_config(&adapter, size.width, size.height)
.unwrap();
surface.configure(&device, &config);
request_adapterとrequest_deviceはasyncなので.awaitが要ります。winitのイベントハンドラは非同期にできないため、ネイティブではpollster::block_on(...)で初期化をまとめて待つのが一般的です。PowerPreference::HighPerformanceは外部GPUを優先し、ノートPCのバッテリー重視ならLowPowerを選びます。
各フィールドは..Default::default()で省略しました。wgpuはメジャー更新でフィールドが増減するため、必須値だけ書いて残りをデフォルトに任せると、バージョン差分に強いコードになります。
画面をクリアする最小サンプル(描画ループ)
初期化ができたら、毎フレーム呼ぶ描画処理を書きます。流れは「Surfaceから今回のフレームを取り出す→RenderPassを開始して背景色でクリア→コマンドをsubmit→presentで表示」です。wgpu 29以降、get_current_textureの戻り値がResultからCurrentSurfaceTextureという列挙型に変わった点に注意してください。?ではなくmatchで分岐します。
fn render(surface: &wgpu::Surface, device: &wgpu::Device,
queue: &wgpu::Queue, config: &wgpu::SurfaceConfiguration) {
// 29以降: Result ではなく CurrentSurfaceTexture 列挙で返る
let frame = match surface.get_current_texture() {
wgpu::CurrentSurfaceTexture::Success(f)
| wgpu::CurrentSurfaceTexture::Suboptimal(f) => f,
wgpu::CurrentSurfaceTexture::Outdated
| wgpu::CurrentSurfaceTexture::Lost => {
surface.configure(device, config); // 再構成して次フレームへ
return;
}
_ => return, // Timeout / Occluded など今回はスキップ
};
let view = frame
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder =
device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("encoder"),
});
{
let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("clear pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: 0.1, g: 0.2, b: 0.3, a: 1.0,
}),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
});
// ここに描画コマンドを書く(次章で pipeline を追加)
} // _pass が drop され、レンダリングパスが閉じる
queue.submit(std::iter::once(encoder.finish()));
frame.present();
}
Rustのwgpuには、JS版のpass.end()に当たる明示的な終了呼び出しはありません。RenderPassはエンコーダを可変借用しており、スコープを抜けてdropされた時点でパスが閉じます。上のように{ }でパスを囲むのはこのためで、囲まないとencoder.finish()が借用エラーになります。
winit 0.30のApplicationHandlerとArc<Window>で詰まらない
0.30ではApp構造体にApplicationHandlerを実装し、ウィンドウはresumed()の中で生成します。生成したWindowをArcで包んでcreate_surfaceへclone()して渡すと、Surfaceが'staticになり「Surfaceがウィンドウを借用してライフタイムが絡む」という初学者が必ず踏む借用エラーを回避できます。日本語のwgpu記事の多くはこの新方式より前の版で止まっているので、winitのバージョンを合わせて読むことが重要です。
レンダリングパイプラインとWGSLシェーダーで三角形を描く
色を塗るだけでなく図形を描くには、シェーダーを束ねたRenderPipelineが必要です。頂点シェーダー(頂点の位置を決める)とフラグメントシェーダー(各ピクセルの色を決める)をWGSLで書き、パイプラインに登録します。まずパイプラインの生成です。
// shader.wgsl を読み込む。include_wgsl! はコンパイル時に埋め込む
let shader = device.create_shader_module(wgpu::include_wgsl!("shader.wgsl"));
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("triangle"),
layout: None,
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"), // 22/24以降 Option<&str>
buffers: &[], // 今回は頂点バッファ無し
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: config.format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState::default(), // TriangleList など既定値
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
あとは前章のクリアパスの中に、pass.set_pipeline(&pipeline);とpass.draw(0..3, 0..1);の2行を足すだけで、頂点3つ=三角形が描かれます。0..3は頂点インデックス0〜2を意味し、頂点データを渡さずシェーダー側でインデックスから座標を計算します。
WGSLの基本構文と要点
WGSL(WebGPU Shading Language)はWebGPU標準のシェーダー言語で、GLSLやHLSLの代わりに使います。エントリポイントに@vertex/@fragment、組み込み入出力に@builtin(...)、可変の入出力に@location(n)を付けるのが基本です。次が上のパイプラインに対応する最小シェーダーです。
struct VsOut {
@builtin(position) pos: vec4<f32>,
};
@vertex
fn vs_main(@builtin(vertex_index) i: u32) -> VsOut {
var out: VsOut;
// 頂点インデックスから三角形の3頂点を計算
let x = f32(1 - i32(i)) * 0.5;
let y = f32(i32(i & 1u) * 2 - 1) * 0.5;
out.pos = vec4<f32>(x, y, 0.0, 1.0);
return out;
}
@fragment
fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
return vec4<f32>(0.9, 0.3, 0.2, 1.0); // オレンジ色
}
エントリポイント名(vs_main/fs_main)は任意ですが、パイプラインのentry_pointに書いた文字列と一致させる必要があります。なお古い解説にある[[stage(vertex)]]のような二重角括弧の記法は廃止済みで、現在は@属性が正です。
ウィンドウ不要のGPGPU:Compute Pipeline
wgpuは描画だけでなく、GPUを汎用計算に使うGPGPUにも対応します。この用途ではウィンドウもSurfaceも不要で、Instance→Adapter→Device/Queueまでは同じ流れ、以降をComputePipelineに差し替えます。物理シミュレーション、画像処理、そしてRust製DLフレームワークBurnのGPUバックエンドがこの仕組みで動いています。
// Surface無しでOK(headless)。compute.wgsl に @compute カーネルを書く
let module = device.create_shader_module(wgpu::include_wgsl!("compute.wgsl"));
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("doubler"),
layout: None,
module: &module,
entry_point: Some("main"),
compilation_options: Default::default(),
cache: None,
});
let mut encoder = device.create_command_encoder(&Default::default());
{
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("compute pass"),
timestamp_writes: None,
});
pass.set_pipeline(&pipeline);
pass.set_bind_group(0, &bind_group, &[]); // 入出力バッファは別途用意
pass.dispatch_workgroups(64, 1, 1); // 64ワークグループを起動
}
queue.submit(std::iter::once(encoder.finish()));
WGSL側は@compute @workgroup_size(64)のように並列度を指定したカーネル関数を書きます。結果はGPU上のバッファに書き込まれるため、CPUで読むには一度マッピング可能なバッファへコピーしてから読み出します(bind_groupやバッファ生成はここでは省略)。ディープラーニングの推論をブラウザやクロスプラットフォームで動かしたいときに、CUDA非依存の計算基盤としてwgpuが選ばれるのはこのためです。
クロスプラットフォーム対応とバックエンド
wgpuの価値は、1つのコードを各OSのネイティブGPU APIへ自動で振り分ける抽象化にあります。Backends::PRIMARYを指定すると、実行環境に応じて次のいずれかが選ばれます。
| バックエンド | 主な対応環境 | 備考 |
|---|---|---|
| Vulkan | Windows / Linux / Android | PRIMARYの主力 |
| Metal | macOS / iOS | Apple専用 |
| DX12 | Windows | DX11は廃止済み |
| OpenGL / GLES | 各OSのフォールバック | SECONDARY扱い |
| WebGPU | ブラウザ(wasm) | ブラウザ本来のAPI |
| WebGL2 | 旧ブラウザ(wasm) | GLES経由の後方互換 |
Windowsで長く使われたDX11バックエンドはすでに削除されており、現在Windowsのネイティブ描画はDX12かVulkanになります。ブラウザ向けにwasmへビルドすると、WebGPU対応ブラウザではWebGPUを、非対応環境ではWebGL2をGLES経由で使う形にフォールバックできます。同じ描画コードがデスクトップとWebで動くのは、この振り分け機構のおかげです。
バージョン差分でつまずかないための注意(wgpu 29/30の破壊的変更)
wgpuは約3か月ごとにメジャーを上げ、そのたびにAPIが変わります。日本語記事のコードが動かない原因の多くはこのバージョン差です。最近の主な変更点を押さえておくと、古いサンプルを読むときに自分で修正できます。
- フレーム取得の戻り値:
get_current_textureは29以降CurrentSurfaceTexture列挙を返す。旧Result<SurfaceTexture, SurfaceError>とSurfaceErrorは廃止。?ではなくmatchで分岐する。 - Adapter/Deviceの取得:
request_adapter・request_deviceはいずれもOptionではなくResultを返し、async(.awaitが必要)。 - トレースパス引数の廃止:
request_deviceの第2引数は消え、DeviceDescriptorのtraceフィールドに統合された。 - 30の追加フィールド:
RequestAdapterOptionsにapply_limit_buckets、SurfaceConfigurationにcolor_spaceが追加。手書きで全フィールドを埋めているコードはコンパイルが通らなくなるため、..Default::default()やget_default_configを使うと影響を受けにくい。 - WGSL属性:
[[stage(...)]]等の旧記法は廃止。@vertex/@builtin(...)/@location(n)が現行。
対策はシンプルで、Cargo.tomlでメジャーを固定し(例:wgpu = "30")、上げるときは公式のCHANGELOGの移行手順を1つずつ当てることです。学習には最新版に追随している英語のLearn Wgpuが実質的な標準リファレンスになります。
wgpu(Rust)のよくある質問
wgpuとWebGLやWebGPUの違いは何ですか?
WebGLは主にブラウザ内の3D描画向けの旧世代API、WebGPUはそれを置き換える新標準の仕様です。wgpuは「WebGPU仕様のRust実装」で、ブラウザの外(ネイティブ)でも同じAPIを使える点がWebGLと大きく異なります。低レベルで並列処理やGPU計算に強く、計算資源を多く使うアプリに向きます。
wgpuの最新バージョンはどれで、どれを使うべきですか?
2026年7月時点の最新はwgpu 30.0.0(2026年7月1日リリース)です。新規開発なら最新のメジャーに合わせ、Cargo.tomlでwgpu = "30"のように固定します。約3か月ごとに破壊的変更が入るため、途中でメジャーを上げる場合はCHANGELOGの移行手順を確認してください。
wgpuで書いたコードはブラウザで動きますか?
動きます。wasmへビルドすれば、WebGPU対応ブラウザではWebGPUバックエンド、非対応環境ではWebGL2(GLES経由)で描画されます。ただしブラウザでは非同期の扱いやwinitのイベントループの初期化がネイティブと少し異なるため、web向けの分岐(wasm-bindgen等)が必要になります。
RustのwgpuとJavaScriptのWebGPUは同じ書き方ですか?
概念は同じですが命名が違います。JS版はrequestAdapterのようなcamelCase、Rustのwgpuはrequest_adapterのようなsnake_caseです。JS向けのサンプルをそのままRustに貼るとコンパイルできないので、本文の対応表のとおり読み替えてください。Python版(wgpu-py)など他言語バインディングも同様に、各言語の命名規約に合わせて呼び出します。
create_surfaceでライフタイムや借用のエラーが出ます。
ウィンドウをArc<Window>で包み、instance.create_surface(window.clone())のようにcloneを渡してください。これでSurfaceが'staticになり、「Surfaceがウィンドウを借用している」ことに起因する借用エラーを避けられます。winitは0.30のApplicationHandler方式に合わせるのが前提で、古いクロージャ方式の記事のまま書くと詰まりやすい箇所です。