---
title: "wgpuとは？Rustで三角形描画とGPU計算を動かす手順【wgpu 30対応】"
url: "https://www.issoh.co.jp/tech/details/4094/"
published: 2024-11-08
updated: 2026-10-20
categories: ["Rust"]
publisher: "株式会社一創"
---

# wgpuとは？Rustで三角形描画とGPU計算を動かす手順【wgpu 30対応】

wgpuは、WebGPUのAPIを土台にしたRust製のGPUライブラリです。Windows・macOS・Linux・Android・iOSのネイティブ環境と、WebAssembly経由のブラウザに対応し、内部ではVulkan・Metal・DirectX 12・OpenGL・WebGPUを呼び分けます。この記事では、wgpuが内部で何をしているのか、GPU計算（Compute）とウィンドウへの三角形描画の完全なコード、nagaによるシェーダーの検証と変換、バックエンドの選ばれ方、28〜30系の破壊的変更の直し方までを扱います。掲載したコードはすべて、2026年10月20日に wgpu 30.0.1・winit 0.30.13・rustc 1.99.1（macOS 26.6.2、AMD Radeon Pro 5500M／Metal）でビルドして実行し、出力をそのまま載せています。

## まとめ：wgpu(Rust)の要点

- **正体**：WebGPU APIをRustの型で書けるようにしたGPUライブラリ。内部で各OSのネイティブAPIを呼び分け、WGSLシェーダーはnagaがSPIR-V・MSL・HLSL・GLSLへ変換する。FirefoxのWebGPU実装の中核でもある。
- **最新版**：wgpu 30.0.1（2026年8月）。メジャーはほぼ3か月ごとに上がり、毎回APIが変わる。`Cargo.toml` でメジャーを固定し、周辺クレートと同じメジャーにそろえる。
- **古いサンプルが動かない主因**：`InstanceDescriptor::default()` の削除（v29）、`frame.present()` から `queue.present(frame)` への変更（v30）、`get_current_texture` の戻り値の列挙型化（v29）。
- **GPU計算だけならウィンドウは不要**：Instance→Adapter→Device／Queueまで作れば、Compute Shaderを実行して結果をCPUへ読み戻せる。
- **macOSの落とし穴**：ウィンドウが隠れると `Occluded` が返る。そのまま再描画を要求し続けると、手元では6秒で20万回を超える空回りになった。
- **ブラウザ**：WebGPU対応ブラウザで動くが、掲載したネイティブ向けコードは移植が要る（`pollster` での同期待ちと `device.poll` による完了待ちがブラウザでは使えない）。WebGL2へのフォールバックには `webgl` featureが要り、WebGL2ではstorage bufferを使うCompute Shaderが動かない。

## wgpuとは：WebGPU APIを土台にしたRustのGPUライブラリ

[公式リポジトリのREADME](https://github.com/gfx-rs/wgpu)は、wgpuをクロスプラットフォームで安全な純Rust製グラフィックスAPIと説明しています。ネイティブではVulkan・Metal・D3D12・OpenGLの上で動き、wasmではWebGL2とWebGPUの上で動きます。APIはWebGPU標準に基づきますが、ブラウザの機能を呼ぶラッパーではなく、それ自体がネイティブのRustライブラリです。同じREADMEには、FirefoxのほかServoとDenoでもWebGPU統合の中核になっていると書かれています。

### wgpuが実際にやっていること：API呼び分けとシェーダー変換

GPUを動かすには、OSごとに違うAPIを呼ぶ必要があります。WindowsならDirectX 12かVulkan、macOSとiOSならMetal、LinuxとAndroidならVulkanかOpenGL ESです。wgpuは、アプリが呼ぶ1つのAPI（バッファやテクスチャの確保、パイプラインの作成、コマンドの送信）を、実行環境のネイティブAPIの呼び出しに置き換えます。

シェーダーも同様です。ネイティブAPIはそれぞれ受け付けるシェーダー形式が違うため、wgpuはWGSLで書かれたシェーダーを、同梱のコンパイラnagaでVulkan向けのSPIR-V、Metal向けのMSL、DirectX向けのHLSL、OpenGL向けのGLSLへ変換してから渡します。この変換は後の「nagaによるWGSLの検証とシェーダー変換」の章で、実際のコマンド出力で確認します。

### WebGPU・wgpu・wgpu-native・wgpu-pyの関係

名前が似たものが多いので、役割を分けておきます。

| 名前          | 何か                        | 使う言語            |
| ----------- | ------------------------- | --------------- |
| WebGPU      | W3Cの標準仕様（ブラウザのAPI）        | JavaScript／WGSL |
| wgpu        | WebGPUに基づくRustクレート        | Rust            |
| wgpu-native | wgpuのC言語バインディング           | C／C++ほか         |
| wgpu-py     | wgpu-nativeを包むPythonパッケージ | Python          |

C言語や他言語から使う場合は、READMEが案内する[wgpu-native](https://github.com/gfx-rs/wgpu-native)を経由します。Pythonのwgpu-py 0.32.0が読み込んでいたのはwgpu-native 29.0.1.1で、Rustのwgpu 30より1つ前のメジャーに当たります。

### JavaScriptのWebGPUとRustのwgpuの対応

WebGPUの日本語情報はJavaScript前提のものが多く、メソッド名がcamelCaseです。Rustのwgpuでは同じ操作をsnake\_caseで呼び、いくつかの操作は書き方そのものが違います。

| 操作     | JavaScript（WebGPU）            | Rust（wgpu 30）                        |
| ------ | ----------------------------- | ------------------------------------ |
| 入口     | `navigator.gpu`               | `wgpu::Instance::new(...)`           |
| アダプタ取得 | `requestAdapter()`            | `instance.request_adapter(..).await` |
| デバイス取得 | `adapter.requestDevice()`     | `adapter.request_device(..).await`   |
| 描画先    | `canvas.getContext("webgpu")` | `instance.create_surface(window)`    |
| パスの終了  | `pass.end()`                  | RenderPassのdrop（スコープを抜ける）            |
| 画面へ表示  | 自動（ブラウザの描画更新時）                | `queue.present(frame)`               |

JavaScriptではキャンバスのテクスチャが自動で表示されますが、Rustでは表示を明示的に呼びます。この呼び方はv30で `frame.present()` から `queue.present(frame)` に変わったので、2026年7月より前のサンプルはここでビルドが止まります。

## wgpuとOpenGL・Vulkanの違いと選ぶ基準

wgpuは、OpenGLやVulkanと同じ層の選択肢ではなく、それらの上に載る抽象化レイヤーです。比べると次のようになります。

| 項目        | OpenGL   | Vulkan   | wgpu            |
| --------- | -------- | -------- | --------------- |
| 位置づけ      | ネイティブAPI | ネイティブAPI | ネイティブAPIの上の層    |
| シェーダー     | GLSL     | SPIR-V   | WGSL（変換して各APIへ） |
| ブラウザ      | WebGL2   | 不可       | WebGPU／WebGL2   |
| リソース誤用の検出 | ドライバ任せ   | 検証レイヤー任せ | wgpu自身が検証       |

wgpuを選ぶべきなのは、1つのコードでデスクトップ・モバイル・ブラウザをまとめて扱いたいとき、そしてCUDAに依存しないGPU計算が欲しいときです。逆に、特定のネイティブAPIでしか使えない機能を突き詰めたい場合は向きません。wgpuの機能はWebGPUの範囲が基本で、それを超える機能は `Features` で個別に要求し、対応するバックエンドでしか動きません。たとえばv30で入ったWGSLの16bit整数（`Features::SHADER_I16`）は、CHANGELOGによればVulkan・Metal・DX12（SM 6.2以上）のみの対応です。OpenGL自体の現状は[OpenGLとは？最新4.6の現状とWindows 11での入手・確認方法、Vulkanとの違い](/tech/details/7473/)で扱っています。

## プロジェクトの作成：Cargo.tomlと版の固定

検証に使った依存関係です。ウィンドウ管理にwinit、`async` な初期化をmain関数から待つためにpollster、バッファとバイト列の変換にbytemuckを使います。

```
[dependencies]
wgpu = "30"
winit = "0.30"
pollster = "1"
bytemuck = "1"
env_logger = "0.11"
```

wgpu 30のMSRV（必要な最低Rustバージョン）は1.87です（[README のMSRV policy](https://github.com/gfx-rs/wgpu#msrv-policy)）。既定のfeatureには `vulkan`・`metal`・`dx12`・`gles`・`wgsl`・`webgpu` が入っていますが、`webgl` は入っていません。GLSLやSPIR-Vのシェーダーをそのまま読み込みたい場合も、`glsl`・`spirv` のfeatureを足します。

### 周辺クレートとwgpuのメジャーバージョンの整合

wgpuのリリースは、26.0.0（2025年7月）、27.0.0（2025年10月）、28.0.0（2025年12月）、29.0.0（2026年3月）、30.0.0（2026年7月）とほぼ3か月ごとです。eguiやBevyと組み合わせるときは、そのクレートが依存するwgpuのメジャーに自分の `Cargo.toml` を合わせます。メジャーがずれると2つのwgpuが同時にビルドされ、片方の `wgpu::Device` をもう片方へ渡せずコンパイルエラーになります。2026年10月20日にcrates.ioで確認した依存は次のとおりです。

| クレート                 | 最新版    | 依存するwgpu |
| -------------------- | ------ | -------- |
| bevy\_render（Bevy）   | 0.20.0 | ^30      |
| egui-wgpu            | 0.37.0 | ^30.0    |
| vello（2Dベクター描画）      | 0.11.0 | ^30.0.0  |
| glyphon（テキスト描画）      | 0.12.0 | ^30.0.0  |
| cubecl-wgpu（Burnが使う） | 0.11.0 | ^30.0.0  |
| iced\_wgpu（iced）     | 0.14.0 | ^27.0    |

icedだけは27系に依存しているので、icedと一緒に使う場合はwgpuを27に下げます。この記事のコードは30用なので、そのままでは動きません。BevyのECSは[Bevy ECSとは？Rust製ゲームエンジンの仕組みと書き方を0.19で実行検証](/tech/details/7874/)、BurnのGPUバックエンドは[Burn 0.21とは？Rust製ディープラーニングフレームワークのバックエンド選定と実装手順](/tech/details/8307/)で扱っています。

## Compute Shaderによるウィンドウ不要のGPU計算

まずは表示を伴わないGPU計算です。1000個の `f32` をGPUに送り、Compute Shaderで2倍にしてCPUへ読み戻します。ファイルは `src/bin/compute.rs` とし、`cargo run --release --bin compute` で実行します。

```
use wgpu::util::DeviceExt;

const SHADER: &str = r#"
@group(0) @binding(0) var<storage, read_write> data: array<f32>;

@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
    let i = id.x;
    if (i < arrayLength(&data)) {
        data[i] = data[i] * 2.0;
    }
}
"#;

fn main() {
    env_logger::init();
    pollster::block_on(run());
}

async fn run() {
    let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
    let adapter = instance
        .request_adapter(&wgpu::RequestAdapterOptions::default())
        .await
        .expect("GPUアダプタが見つからない");
    let info = adapter.get_info();
    println!("adapter: {} / backend: {:?}", info.name, info.backend);

    let (device, queue) = adapter
        .request_device(&wgpu::DeviceDescriptor::default())
        .await
        .expect("デバイス取得に失敗");

    let input: Vec<f32> = (0..1000).map(|x| x as f32).collect();
    let size = (input.len() * std::mem::size_of::<f32>()) as u64;

    // GPU側で読み書きするバッファ（STORAGE）
    let storage = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
        label: Some("storage"),
        contents: bytemuck::cast_slice(&input),
        usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
    });
    // CPUへ読み戻すためのバッファ（MAP_READ）
    let readback = device.create_buffer(&wgpu::BufferDescriptor {
        label: Some("readback"),
        size,
        usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
        mapped_at_creation: false,
    });

    let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
        label: Some("doubler"),
        source: wgpu::ShaderSource::Wgsl(SHADER.into()),
    });
    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 bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
        label: None,
        layout: &pipeline.get_bind_group_layout(0),
        entries: &[wgpu::BindGroupEntry {
            binding: 0,
            resource: storage.as_entire_binding(),
        }],
    });

    let mut encoder = device.create_command_encoder(&Default::default());
    {
        let mut pass = encoder.begin_compute_pass(&Default::default());
        pass.set_pipeline(&pipeline);
        pass.set_bind_group(0, &bind_group, &[]);
        // 1000要素 / 64スレッド = 16ワークグループ（切り上げ）
        pass.dispatch_workgroups((input.len() as u32).div_ceil(64), 1, 1);
    }
    encoder.copy_buffer_to_buffer(&storage, 0, &readback, 0, size);
    queue.submit([encoder.finish()]);

    let slice = readback.slice(..);
    slice.map_async(wgpu::MapMode::Read, |r| r.expect("map失敗"));
    device.poll(wgpu::PollType::wait_indefinitely()).unwrap();

    let view = slice.get_mapped_range().expect("mapped range");
    let out: &[f32] = bytemuck::cast_slice(&view);
    println!("先頭5件: {:?}", &out[..5]);
    println!("末尾: {}", out[out.len() - 1]);
    let ok = out.iter().enumerate().all(|(i, &v)| v == i as f32 * 2.0);
    println!("全{}件が2倍か: {}", out.len(), ok);
}
```

実行結果です。ディスクリートGPUのRadeon Pro 5500MがMetalバックエンドで選ばれ、1000件すべてが元の値の2倍になっています。

```
adapter: AMD Radeon Pro 5500M / backend: Metal
先頭5件: [0.0, 2.0, 4.0, 6.0, 8.0]
末尾: 1998
全1000件が2倍か: true
```

押さえるべき点は3つあります。1つ目は `InstanceDescriptor::new_without_display_handle()` です。v29で `InstanceDescriptor` の `Default` 実装が削除されたため、`..Default::default()` で埋める古い書き方はコンパイルできません。ウィンドウを使わない計算ではこの関数で作ります。2つ目は読み戻しの手順で、GPUが書くバッファ（`STORAGE`）から `MAP_READ` 付きのバッファへコピーし、`map_async` を呼んでから `device.poll` で完了を待ちます。3つ目は `get_mapped_range()` の戻り値で、v30からは失敗時にパニックせず `Result` を返すため、`expect` などで取り出します。

なお `DeviceDescriptor::default()` が要求する上限はWebGPUの既定値で、1つのバッファは256MiB、storage bufferの1バインディングは128MiBまでです。手元のRadeon Pro 5500Mは `adapter.limits()` で約3.2GBのバッファを報告しましたが、既定のままでは256MiBを超えるバッファは作れません。大きなデータを扱うときは `required_limits: adapter.limits()` のように必要な上限を明示して要求します。

## 三角形の描画：winit 0.30で動く完全なmain.rs

次はウィンドウへの描画です。Rust側の処理は1つの `main.rs` にまとめ、シェーダーだけを別ファイルにしています。シェーダーは頂点番号から3頂点の座標と色を決めるので、頂点バッファは使いません。`src/shader.wgsl` に置きます。

```
struct VsOut {
    @builtin(position) pos: vec4<f32>,
    @location(0) color: vec3<f32>,
};

@vertex
fn vs_main(@builtin(vertex_index) i: u32) -> VsOut {
    var positions = array<vec2<f32>, 3>(
        vec2<f32>(0.0, 0.5),
        vec2<f32>(-0.5, -0.5),
        vec2<f32>(0.5, -0.5),
    );
    var colors = array<vec3<f32>, 3>(
        vec3<f32>(1.0, 0.0, 0.0),
        vec3<f32>(0.0, 1.0, 0.0),
        vec3<f32>(0.0, 0.0, 1.0),
    );
    var out: VsOut;
    out.pos = vec4<f32>(positions[i], 0.0, 1.0);
    out.color = colors[i];
    return out;
}

@fragment
fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
    return vec4<f32>(in.color, 1.0);
}
```

`src/main.rs` です。winit 0.30の `ApplicationHandler` を実装し、ウィンドウは `resumed` で作ります。

```
use std::sync::Arc;
use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::window::{Window, WindowId};

struct Gpu {
    instance: wgpu::Instance,
    window: Arc<Window>,
    surface: wgpu::Surface<'static>,
    device: wgpu::Device,
    queue: wgpu::Queue,
    config: wgpu::SurfaceConfiguration,
    pipeline: wgpu::RenderPipeline,
}

impl Gpu {
    async fn new(instance: &wgpu::Instance, window: Arc<Window>) -> Self {
        // Arc<Window> を渡すと Surface<'static> になり、借用が絡まない
        let surface = instance.create_surface(window.clone()).unwrap();
        let adapter = instance
            .request_adapter(&wgpu::RequestAdapterOptions {
                compatible_surface: Some(&surface),
                ..Default::default()
            })
            .await
            .unwrap();
        let (device, queue) = adapter
            .request_device(&wgpu::DeviceDescriptor::default())
            .await
            .unwrap();

        let size = window.inner_size();
        let config = surface
            .get_default_config(&adapter, size.width.max(1), size.height.max(1))
            .unwrap();
        surface.configure(&device, &config);

        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"),
                buffers: &[],
                compilation_options: Default::default(),
            },
            fragment: Some(wgpu::FragmentState {
                module: &shader,
                entry_point: Some("fs_main"),
                targets: &[Some(config.format.into())],
                compilation_options: Default::default(),
            }),
            primitive: wgpu::PrimitiveState::default(),
            depth_stencil: None,
            multisample: wgpu::MultisampleState::default(),
            multiview_mask: None,
            cache: None,
        });

        Self { instance: instance.clone(), window, surface, device, queue, config, pipeline }
    }

    fn resize(&mut self, width: u32, height: u32) {
        if width > 0 && height > 0 {
            self.config.width = width;
            self.config.height = height;
            self.surface.configure(&self.device, &self.config);
        }
    }

    /// 描画を続けてよいなら true（隠れている間は false）
    fn render(&mut self) -> bool {
        let frame = match self.surface.get_current_texture() {
            wgpu::CurrentSurfaceTexture::Success(f) => f,
            wgpu::CurrentSurfaceTexture::Suboptimal(f) => {
                // SurfaceTexture を持ったまま configure するとパニックするので先に捨てる
                drop(f);
                self.surface.configure(&self.device, &self.config);
                return true;
            }
            wgpu::CurrentSurfaceTexture::Outdated => {
                self.surface.configure(&self.device, &self.config);
                return true;
            }
            wgpu::CurrentSurfaceTexture::Lost => {
                // Surface を作り直してから構成し直す
                self.surface = self.instance.create_surface(self.window.clone()).unwrap();
                self.surface.configure(&self.device, &self.config);
                return true;
            }
            // ウィンドウが隠れている。再描画を止め、Occluded(false) で再開する
            wgpu::CurrentSurfaceTexture::Occluded => return false,
            // Timeout / Validation はこのフレームだけ飛ばす
            _ => return true,
        };
        let view = frame.texture.create_view(&wgpu::TextureViewDescriptor::default());
        let mut encoder = self.device.create_command_encoder(&Default::default());
        {
            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("main 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,
                multiview_mask: None,
            });
            pass.set_pipeline(&self.pipeline);
            pass.draw(0..3, 0..1);
        } // pass が drop されてレンダーパスが閉じる
        self.queue.submit([encoder.finish()]);
        self.queue.present(frame); // v30〜: frame.present() ではない
        true
    }
}

struct App {
    instance: wgpu::Instance,
    gpu: Option<Gpu>,
}

impl ApplicationHandler for App {
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        if self.gpu.is_none() {
            let window = Arc::new(
                event_loop
                    .create_window(Window::default_attributes().with_title("wgpu 30"))
                    .unwrap(),
            );
            self.gpu = Some(pollster::block_on(Gpu::new(&self.instance, window)));
        }
    }

    fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
        let Some(gpu) = self.gpu.as_mut() else { return };
        match event {
            WindowEvent::CloseRequested => event_loop.exit(),
            WindowEvent::Resized(size) => gpu.resize(size.width, size.height),
            WindowEvent::Occluded(false) => gpu.window.request_redraw(),
            WindowEvent::RedrawRequested => {
                if gpu.render() {
                    gpu.window.request_redraw();
                }
            }
            _ => {}
        }
    }
}

fn main() {
    env_logger::init();
    let event_loop = EventLoop::new().unwrap();
    // v29〜: InstanceDescriptor に Default は無い。winit の display handle を渡す
    let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_with_display_handle(
        Box::new(event_loop.owned_display_handle()),
    ));
    let mut app = App { instance, gpu: None };
    event_loop.run_app(&mut app).unwrap();
}
```

### 初期化の流れとdisplay handle

初期化は Instance→Surface→Adapter→Device／Queue の順です。ウィンドウ付きのアプリでは、Instanceを作るときに `event_loop.owned_display_handle()` を渡します。v29のCHANGELOGは、LinuxのWaylandでEGL（OpenGL ES）を正しく初期化するためにdisplay handleを `InstanceDescriptor` へ渡すよう求めています。macOSやWindowsでは無くても動きますが、同じコードをLinuxへ持っていくなら最初から渡しておきます。

ウィンドウは `Arc<Window>` で包み、`create_surface(window.clone())` に渡します。こうすると `Surface<'static>` になり、Surfaceがウィンドウを借用することで起きるライフタイムのエラーを避けられます。`request_adapter` と `request_device` はどちらも `async` で `Result` を返します（`request_adapter` はv25で `Option` から `Result` に変わりました）。winitのイベントハンドラは `async` にできないので、`pollster::block_on` で待ちます。

### フレーム取得の分岐とmacOSのOccluded

`get_current_texture()` はv29から `CurrentSurfaceTexture` という列挙型を返します。値は `Success`・`Suboptimal`・`Timeout`・`Occluded`・`Outdated`・`Lost`・`Validation` の7種類です。`Lost` の扱いは、v29のCHANGELOGの例が「Surfaceの再構成」なのに対し、wgpu 30.0.1のソースのコメントは「`create_surface` でSurfaceを作り直してから再構成」と案内しており、記述がそろっていません。上のコードは後者に合わせ、Instanceを保持しておいてSurfaceを作り直しています。デバイスごと失われた場合はDeviceと全リソースの作り直しが必要で、その検知には `device.set_device_lost_callback` を使います。

`Suboptimal` ではフレームは取れていますが、`Surface::configure` のドキュメントには、古い `SurfaceTexture` が残ったまま呼ぶとパニックすると書かれています。そのため取れたフレームを先に `drop` してから再構成し、そのフレームの描画は飛ばしています。

注意したいのは `Occluded` です。wgpu-hal 30.0.1のソースを見ると、この値を返すのはmacOSのMetalバックエンドだけで、ウィンドウが完全に隠れているときにフレームの取得を飛ばしています。今回の検証環境ではウィンドウが隠れた状態で起動したため、最初のフレームから `Occluded` が返りました。そこで `Occluded` でも毎回 `request_redraw()` を呼ぶ版を6秒動かしたところ、`Occluded` が20万回を超え、CPUが空回りしました。上のコードは `Occluded` のときに再描画の要求を止め、winitの `WindowEvent::Occluded(false)` を受けたら再開します。winit 0.30.13のドキュメントでは、この `Occluded` イベントはAndroid・Wayland・Windows・Orbitalで未対応ですが、wgpuが `Occluded` を返すのもmacOSだけなので、組み合わせとして矛盾はありません。

レンダーパスに明示的な終了呼び出しはありません。`RenderPass` はエンコーダを借用しており、`{ }` のスコープを抜けてdropされた時点でパスが閉じます。囲まずに書くと、`encoder.finish()` が借用エラーになります。`RenderPassDescriptor` の `multiview_mask: None` は、v28でマルチビュー（VR向けに複数レイヤーへ同時描画する機能）が作り直されたときに増えた項目で、これが無いと28以降ではビルドできません。

## 描画結果の確認：オフスクリーン描画とピクセルの読み戻し

ウィンドウが隠れていたため、描画の正しさはオフスクリーン描画で確かめました。同じシェーダーで256×256のテクスチャに描き、バッファへコピーしてピクセルを読みます。要点の部分は次のとおりです（全体は約120行）。

```
// 1行のバイト数は 256 の倍数にそろえる必要がある（COPY_BYTES_PER_ROW_ALIGNMENT）
let bytes_per_row = (W * 4).div_ceil(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT) * wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
let readback = device.create_buffer(&wgpu::BufferDescriptor {
    label: Some("readback"),
    size: (bytes_per_row * H) as u64,
    usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
    mapped_at_creation: false,
});

let mut encoder = device.create_command_encoder(&Default::default());
encoder.push_debug_group("draw triangle");
{
    let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
        label: Some("offscreen pass"),
        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
            view: &view,
            resolve_target: None,
            depth_slice: None,
            ops: wgpu::Operations {
                load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
                store: wgpu::StoreOp::Store,
            },
        })],
        depth_stencil_attachment: None,
        occlusion_query_set: None,
        timestamp_writes: None,
        multiview_mask: None,
    });
    pass.set_pipeline(&pipeline);
    pass.draw(0..3, 0..1);
}
encoder.pop_debug_group();
encoder.copy_texture_to_buffer(
    texture.as_image_copy(),
    wgpu::TexelCopyBufferInfo {
        buffer: &readback,
        layout: wgpu::TexelCopyBufferLayout {
            offset: 0,
            bytes_per_row: Some(bytes_per_row),
            rows_per_image: Some(H),
        },
    },
    wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
);
queue.submit([encoder.finish()]);
```

`Rgba8UnormSrgb` 形式のテクスチャから読み戻したRGBAの値です。RGBはsRGBに符号化されているため、シェーダー内の線形値を単純に255倍した値とは一致しません。3頂点の付近がそれぞれ赤・緑・青、中心が3色の補間、三角形の外側がクリア色の黒になっています。

```
bytes_per_row=1024 (W*4=1024)
中心(128,128) = [187, 137, 139, 255]
上頂点付近(128,70) = [249, 40, 48, 255]
左下頂点付近(70,188) = [46, 248, 54, 255]
右下頂点付近(186,188) = [46, 48, 249, 255]
隅(5,5) = [0, 0, 0, 255]
```

テクスチャからバッファへのコピーでは、1行のバイト数を `COPY_BYTES_PER_ROW_ALIGNMENT`（256）の倍数にする必要があります。幅256ピクセルのRGBAならちょうど1024バイトで端数は出ませんが、幅100ピクセルなら400バイトを512バイトに切り上げ、読むときに行末の余白を飛ばします。スクリーンショット機能や画像の書き出しで画像が斜めにずれる場合は、まずここを疑います。

`push_debug_group` と `pop_debug_group` は、コマンドに名前付きの区切りを入れるAPIです。描画結果には影響せず、XcodeやRenderDocなどのGPUデバッガでキャプチャしたときにコマンドの塊が名前で表示されます。同じメソッドは `CommandEncoder`・`RenderPass`・`ComputePass` にあり、1点だけ印を付けるなら `insert_debug_marker` を使います。

## nagaによるWGSLの検証とシェーダー変換

nagaはコマンドラインツール（naga-cli）としても配布されています。`cargo install naga-cli` で入れると、WGSLの検証と他言語への変換をGPU無しで試せます。上の `shader.wgsl` を渡した結果です。

```
$ naga --version
30.0.1
$ naga src/shader.wgsl
Validation successful
$ naga src/shader.wgsl out.metal
$ head -3 out.metal
// language: metal1.0
#include <metal_stdlib>
#include <simd/simd.h>
$ naga src/shader.wgsl out.vert --entry-point vs_main --profile es310
$ head -1 out.vert
#version 310 es
```

出力先の拡張子で変換先が決まり、`.metal` ならMSL、`.spv` ならSPIR-V、`.hlsl` ならHLSL、`.vert` ならGLSLになります。wgpuがMetal上で動くときは、内部でこれと同じ変換をしてからMetalに渡しています。

v30では、整数型のシェーダー入出力に `@interpolate(flat)` を自動で補わなくなりました。`@location(0) index: i32` のように書いた古いシェーダーは、次のエラーで止まります。

```
$ naga bad_int_io.wgsl
error: Entry point vs_main at Vertex is invalid
  = `@interpolate(flat) must be explicitly specified for integer I/O
```

直し方は `@location(0) @interpolate(flat) index: i32` と明示することです。古い解説にある `[[stage(vertex)]]` のような二重角括弧の属性も現在は使えず、`@vertex`・`@builtin(...)`・`@location(n)` が正しい書き方です。シェーダーを変えたら、Rustをビルドする前に `naga` で検証すると原因の切り分けが早くなります。

## バックエンドの選ばれ方と環境変数

READMEの対応表を要約すると、Vulkanは「Windows・Linux・Android」、Metalは「macOS・iOS」、DX12は「Windows」がFirst Class（第一級）の対応です。OpenGLはWindowsでGL 3.3以上、Linux／AndroidでGL ES 3.0以上が「Downlevel／Best Effort」で、macOS／iOSではANGLE（`angle` feature）が要ります。macOSでVulkanを使うにはMoltenVKが要ります。`Backends::PRIMARY` はVulkan・Metal・DX12・ブラウザのWebGPUの4つ、`Backends::SECONDARY` はGLです。

どのGPUが使えるかは、アダプタを列挙すれば確かめられます。

```
// 使えるアダプタを列挙し、環境変数で選ぶ
fn main() {
    pollster::block_on(async {
        // *_from_env を使ったときだけ WGPU_BACKEND などの環境変数が効く
        let instance =
            wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle_from_env());
        for a in instance.enumerate_adapters(wgpu::Backends::all()).await {
            let i = a.get_info();
            println!("{:?} / {} / {:?}", i.backend, i.name, i.device_type);
        }
        let adapter = wgpu::util::initialize_adapter_from_env_or_default(&instance, None)
            .await
            .expect("アダプタが無い");
        println!("選ばれた: {}", adapter.get_info().name);
        let l = adapter.limits();
        println!("max_buffer_size={} max_compute_workgroup_size_x={}", l.max_buffer_size, l.max_compute_workgroup_size_x);
    });
}
```

実行結果です（長い行は一部省略）。

```
$ cargo run --release --bin adapters
Metal / AMD Radeon Pro 5500M / DiscreteGpu
Metal / Intel(R) UHD Graphics 630 / IntegratedGpu
選ばれた: AMD Radeon Pro 5500M
max_buffer_size=3208642560 max_compute_workgroup_size_x=1024

$ WGPU_ADAPTER_NAME=intel cargo run --release --bin adapters
選ばれた: Intel(R) UHD Graphics 630

$ WGPU_BACKEND=vulkan cargo run --release --bin adapters
アダプタが無い: NotFound { ... requested_backends: Backends(VULKAN), supported_backends: Backends(METAL), ... }
```

既定のfeatureでビルドしたmacOS版は、対応バックエンドがMetalだけでした。Vulkanを要求するとエラーメッセージの `supported_backends` にその理由が出ます。`WGPU_ADAPTER_NAME` はアダプタ名の部分一致で選ぶ変数で、この検証ではディスクリートGPUのRadeon Pro 5500Mと、CPU内蔵のIntel UHD Graphics 630を切り替えられました。ただし、`WGPU_BACKEND` などの環境変数が効くのは、`new_without_display_handle_from_env()` のような `*_from_env` 系で作ったときだけです。前章の `main.rs` のように `new_with_display_handle()` で作ったInstanceは環境変数を読まないので、切り替えたいなら `new_with_display_handle_from_env()` に替えます。

### ブラウザ（wasm）で動かすときの条件

`wasm32-unknown-unknown` 向けにビルドすると、WebGPUに対応したブラウザではブラウザのWebGPUの上で動きます。ただし、この記事のコードはネイティブ向けなので、そのままでは動きません。`pollster::block_on` による同期待ちはブラウザでは使えず、非同期の初期化をブラウザのイベントループに任せる形へ書き換えます。読み戻しの完了待ちも同様で、`PollType` のドキュメントのとおり `device.poll` はWebGPUでは効果が無く、`map_async` のコールバックはブラウザのイベントループから呼ばれます。描画先もウィンドウではなくHTMLのcanvasです。

WebGPUが無いブラウザでWebGL2に落とすには、`wgpu = { version = "30", features = ["webgl"] }` とfeatureを足し、`required_limits` に `Limits::downlevel_webgl2_defaults()` などWebGL2で満たせる上限を明示して要求します。`DeviceDescriptor::default()` のままではWebGPUの既定上限を要求するため、デバイスの取得に失敗し得ます。この上限ではシェーダー段ごとのstorage bufferの数が0です。この記事のCompute Shaderのようにstorage bufferを使う処理はWebGL2では動かないので、GPU計算をブラウザで使うならWebGPU対応ブラウザを前提にします。JavaScript側との連携は[WebAssembly(Wasm)とは？JavaScript連携とMemory・Table操作の実装](/tech/details/4250/)、WebGL自体の仕組みは[WebGLとは？仕組み・対応ブラウザの確認・動くコードとUnity WebGLビルドの手順【2026年】](/tech/details/4884/)で解説しています。

## wgpu 28〜30の破壊的変更と古いサンプルの直し方

古いwgpuのサンプルを新しいメジャーでビルドすると、API変更によるコンパイルエラーが起こります。[CHANGELOG](https://github.com/gfx-rs/wgpu/blob/trunk/CHANGELOG.md)から、入門コードに出てくる箇所だけを抜き出しました。本記事の2026年7月版のコードも、wgpu 30.0.1で実際にビルドしたところ、表の1・3・4・6行目に当たる4か所でコンパイルが通りませんでした。

| 旧コード                            | 現行の書き方                           | 変更版 |
| ------------------------------- | -------------------------------- | --- |
| `InstanceDescriptor::default()` | `new_with_display_handle(..)` など | v29 |
| `get_current_texture()?`        | `match CurrentSurfaceTexture`    | v29 |
| `frame.present()`               | `queue.present(frame)`           | v30 |
| RenderPassDescriptorに項目なし       | `multiview_mask: None`           | v28 |
| `multiview: None`（パイプライン）       | `multiview_mask: None`           | v28 |
| `get_mapped_range(..)` で値       | `Result` を返す                     | v30 |
| `buffers: &[layout]`            | `buffers: &[Some(layout)]`       | v30 |
| `bind_group_layouts: &[&bgl]`   | `&[Some(&bgl)]`                  | v29 |
| 整数の `@location` のみ              | `@interpolate(flat)` を明示         | v30 |

v29ではマップしたバッファへの書き込みも変わり、`MapMode::Write` の `get_mapped_range_mut()` は `&mut [u8]` として扱えなくなりました。`copy_from_slice` で丸ごと書く使い方は大きく変わりませんが（v30では先に `Result` を取り出す）、`view[start..end]` のような添字アクセスは `view.slice(start..end)` に書き換えます。

版を上げるときは、CHANGELOGの「Major changes」に載っているdiffを1つずつ当て、Learn Wgpuのように最新版へ追従している教材と照らし合わせるのが確実です。2026年10月時点で[Learn Wgpu](https://sotrh.github.io/learn-wgpu/)のリポジトリは `wgpu = "30.0"` に更新されています。

## エラーの捕捉とデバッグの手順

wgpuは、APIの誤用（サイズの合わないバッファ、シェーダーの検証エラーなど）を自分で検出します。既定では、検出したエラーは「wgpu error: Validation Error」としてパニックします。パニックさせずに受け取りたい処理は、エラースコープで囲みます。v30の `push_error_scope` はガードを返し、`pop()` でエラーを受け取ります。次のRustコードを `src/bin/error_scope.rs` に置き、わざと検証エラーになるシェーダーを `src/bad_int_io.wgsl` として用意します。シェーダーは前章のnagaのエラー例と同じものです。

```
struct VsOut {
    @builtin(position) pos: vec4<f32>,
    @location(0) index: i32,
};
@vertex
fn vs_main(@builtin(vertex_index) i: u32) -> VsOut {
    var out: VsOut;
    out.pos = vec4<f32>(0.0, 0.0, 0.0, 1.0);
    out.index = i32(i);
    return out;
}
```

```
// バリデーションエラーをパニックさせずに受け取る
fn main() {
    pollster::block_on(async {
        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
        let adapter = instance.request_adapter(&Default::default()).await.unwrap();
        let (device, _queue) = adapter.request_device(&Default::default()).await.unwrap();

        let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
        let _module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
            label: Some("bad shader"),
            source: wgpu::ShaderSource::Wgsl(include_str!("../bad_int_io.wgsl").into()),
        });
        if let Some(err) = scope.pop().await {
            println!("捕捉したエラー:\n{err}");
        }

        // スコープ外のエラーは既定でパニックになる
        println!("--- スコープ無し ---");
        let _module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
            label: Some("bad shader 2"),
            source: wgpu::ShaderSource::Wgsl(include_str!("../bad_int_io.wgsl").into()),
        });
        println!("ここまで到達");
    });
}
```

```
捕捉したエラー:
Validation Error

Caused by:
  In Device::create_shader_module, label = 'bad shader'

Shader validation error: Entry point vs_main at Vertex is invalid
...
--- スコープ無し ---

thread 'main' panicked at .../wgpu-30.0.1/src/backend/wgpu_core.rs:1084:30:
wgpu error: Validation Error
```

エラーメッセージに `label = 'bad shader'` と出ているとおり、各Descriptorの `label` に付けた名前はエラーとGPUデバッガの両方に表示されます。`None` のままにせず名前を付けておくと、どのリソースで失敗したかがすぐ分かります。アプリ全体でスコープ外のエラーを拾うなら `device.on_uncaptured_error` にハンドラを登録します。ログは `env_logger::init()` を呼んだうえで `RUST_LOG=wgpu_core=warn,wgpu_hal=warn` のように出力レベルを絞ると読みやすくなります。検証の強さは `InstanceFlags` で決まり、`*_from_env` 系で作ったInstanceなら `WGPU_VALIDATION`・`WGPU_DEBUG`・`WGPU_GPU_BASED_VALIDATION` などの環境変数で切り替えられます。

## wgpu(Rust)のよくある質問

### wgpuの最新バージョンはいくつですか？

2026年10月20日時点では30.0.1（2026年8月）です。メジャーはほぼ3か月ごとに上がるので、`wgpu = "30"` のようにメジャーで固定し、組み合わせるクレートとメジャーをそろえてください。

### wgpuはPythonから使えますか？

使えます。wgpu-py（`pip install wgpu`、Python 3.11以上）を使います。0.32.0で「値を2倍にする」同じCompute Shaderを書くと、Radeon Pro 5500MのMetalで動き `[0. 2. 4. 6. 8.] 1998.0` を返しました。

### wgpu-rsとwgpuは同じものですか？

同じ系統です。以前はRust向けAPIが gfx-rs/wgpu-rs という別リポジトリで開発されていましたが、このリポジトリはアーカイブ済み（最終更新2021年6月）です。現在のRust APIは gfx-rs/wgpu リポジトリの `wgpu` クレートで、crates.ioのクレート名も `wgpu` です。

### BevyやeguiでもwgpuのAPIを直接書く必要がありますか？

通常は不要です。Bevy 0.20は描画をwgpu 30の上に実装しており、eguiも描画をegui-wgpuに任せられます。自前のシェーダーや独自の描画パスを足すときに、この記事の初期化とパイプラインの知識が要ります。

### wgpuのコードはブラウザで動きますか？

ブラウザ向けに移植すれば動きます。この記事のコードはネイティブ向けなので、非同期の初期化、canvasとの接続、読み戻しの完了待ちを書き換える必要があります。WebGL2にも対応させるなら `webgl` featureを足し、WebGL2用の上限を明示して要求します。WebGL2ではstorage bufferを使うGPU計算は動きません。

## 関連記事

- [Bevy ECSとは？Rust製ゲームエンジンの仕組みと書き方を0.19で実行検証](/tech/details/7874/)
- [Burn 0.21とは？Rust製ディープラーニングフレームワークのバックエンド選定と実装手順](/tech/details/8307/)
- [cuda-oxideのDisjointSliceとは｜RustでGPU並列書き込みを型安全にする仕組み](/tech/details/12684/)
- [WebGLとは？仕組み・対応ブラウザの確認・動くコードとUnity WebGLビルドの手順【2026年】](/tech/details/4884/)
- [OpenGLとは？最新4.6の現状とWindows 11での入手・確認方法、Vulkanとの違い](/tech/details/7473/)

---

出典: [wgpuとは？Rustで三角形描画とGPU計算を動かす手順【wgpu 30対応】](<https://www.issoh.co.jp/tech/details/4094/>)（株式会社一創）
