Rust

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へ振り分ける抽象化レイヤー。
  • 初期化の流れInstanceAdapterDeviceQueueSurfacerequest_adapterrequest_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_adapterrequest_deviceasyncなので.awaitが要ります。winitのイベントハンドラは非同期にできないため、ネイティブではpollster::block_on(...)で初期化をまとめて待つのが一般的です。PowerPreference::HighPerformanceは外部GPUを優先し、ノートPCのバッテリー重視ならLowPowerを選びます。

各フィールドは..Default::default()で省略しました。wgpuはメジャー更新でフィールドが増減するため、必須値だけ書いて残りをデフォルトに任せると、バージョン差分に強いコードになります。

画面をクリアする最小サンプル(描画ループ)

初期化ができたら、毎フレーム呼ぶ描画処理を書きます。流れは「Surfaceから今回のフレームを取り出す→RenderPassを開始して背景色でクリア→コマンドをsubmitpresentで表示」です。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()の中で生成します。生成したWindowArcで包んでcreate_surfaceclone()して渡すと、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_mainfs_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_adapterrequest_deviceはいずれもOptionではなくResultを返し、async.awaitが必要)。
  • トレースパス引数の廃止request_deviceの第2引数は消え、DeviceDescriptortraceフィールドに統合された。
  • 30の追加フィールドRequestAdapterOptionsapply_limit_bucketsSurfaceConfigurationcolor_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方式に合わせるのが前提で、古いクロージャ方式の記事のまま書くと詰まりやすい箇所です。

関連記事

資料請求

RELATED POSTS 関連記事