Wado

WEP: wasi:webgpu Bindings

Context

Wado targets WASI P3 (Target WASI P3 Only), where a program reaches files, sockets, clocks and randomness but has no way to reach a GPU. wasi:webgpu is the WASI proposal for that: Phase 2, portable to Linux, Windows, macOS, Android and the Web, one WIT package at 0.3.0-rc.2.

The proposal is the WebGPU spec with its JS assumptions removed, generated from the WebGPU IDL. One file, one interface webgpu, and inside it 33 resources, 62 records, 34 enums, 12 variants, 5 flags, 13 typedefs, 8 async func, and a single free function, get-gpu. Presenting to a screen is a declared non-goal of the proposal, so what it covers is GPU compute and offscreen rendering.

Two things it needs from the toolchain are already there. wado-from-idl reads WIT and emits a wasi:* module, and a Wado module may bind a Component Model import in any namespace, with #[cm] naming the target (Declaration Identity).

Decision

The package is bundled, like every other wasi:*

wasi:webgpu appears in the binding table, so the namespace is reserved and a program imports it with no package of its own. What that costs is measured below, and it is small.

The module is generated from the proposal's WIT

wasi:* is generated, never hand-written (wado-from-idl/AGENTS.md), and this package is WIT like the rest, so whatever carries the module generates it. wasmtime does not ship this WIT, so it comes from a vendor/wasi-webgpu submodule, and mise run update-stdlib-wasi regenerates from there.

Run against 0.3.0-rc.2, the WIT frontend emits the whole interface as 2588 lines. It skips nothing and approximates nothing.

The interface is the effect, and each resource is one too

A WIT interface's free functions become a Wado interface, so webgpu yields Webgpu with get_gpu(), the one operation that hands out a handle. Effects and resources are unified, so a program names every resource it touches in its own with clause:

use { Webgpu, Gpu, GpuAdapter, GpuDevice, GpuBuffer } from "wasi:webgpu";

export fn run() with (Webgpu, Gpu, GpuAdapter, GpuDevice, GpuBuffer) {
    let gpu = Webgpu::get_gpu();
    let Some(adapter) = gpu.request_adapter(null).wait() else { return; };
    let Ok(device) = adapter.request_device(null).wait() else { return; };
    let buffer = device.create_buffer(GpuBufferDescriptor {
        size: 256,
        usage: GpuBufferUsage::Storage | GpuBufferUsage::CopySrc,
        mapped_at_creation: null,
        label: null,
    });
}

An async operation returns AsyncCall<T>

Eight operations are async func: request-adapter, request-device, map-async, the two create-*-pipeline-async, pop-error-scope, on-submitted-work-done and get-compilation-info. Each follows Generic AsyncCall<T>, so the call site .wait()s. gpu-device also hands out a future and a stream: lost() answers Future<GpuDeviceLostInfo> and on_uncaptured_error() a Stream<GpuError>.

The handles are affine CM resources

wado-lang:web collapses its handles into one unrestricted universal handle (Resource Inheritance). Nothing of the sort applies here: every wasi:webgpu resource is a CM resource, so Ownership Analysis governs it and the boundary carries own and borrow handles as it does for any other WASI package. The proposal borrows in 41 places, twelve of them record fields, so a descriptor is a struct with reference-typed fields:

pub struct GpuBindGroupDescriptor {
    pub layout: &GpuBindGroupLayout,
    pub entries: List<GpuBindGroupEntry>,
    pub label: Option<String>,
}

gpu-queue.submit and gpu-render-pass-encoder.execute-bundles take list<borrow<t>>, which is a List<&T>: the element is a borrow at the boundary and a reference on the Wado side. Every container reads the same way, a tuple, an option and a result alongside a list. Only a bare borrow<t> is the i32 handle the boundary passes.

What the boundary already carries

Compiled against the generated module, these shapes reach a valid component: the descriptor above, a variant payload holding a record that holds a borrow (GpuBindingResource::GpuBufferBinding), a List<&T> argument, an async operation through .wait(), and a flags value built with |.

They also run. wado-run-webgpu's own test compiles a Wado program that dispatches a WGSL compute shader and checks what the GPU wrote back, so the binding is covered end to end and not only to the point of compiling.

Roadmap

Known gaps