Concept · Platform

WebGPU & wgpu

Every triangle FloraForge draws — on a Mac, on a Linux box, or inside a browser tab — goes through a single graphics API: WebGPU. This page explains what WebGPU is, why it exists, and how the wgpu library turns one Rust renderer into a program that runs natively on every desktop GPU stack and on the web.

Why WebGL needed a successor

For over a decade, 3D graphics on the web meant WebGL — a JavaScript binding of OpenGL ES, an API whose design dates back to the 1990s. It worked, but it carried OpenGL's baggage: a hidden global state machine the driver has to second-guess, validation work repeated on every draw call, and no support at all for general-purpose GPU computation. Meanwhile the native world had moved on to a new generation of explicit APIs — Apple's Metal, Khronos's Vulkan, Microsoft's Direct3D 12 — where the application states its intentions up front and the driver stops guessing.

WebGPU is the web's version of that generation, designed from scratch by the browser vendors themselves — Apple, Google, Mozilla, and Microsoft working jointly in the W3C's "GPU for the Web" group — so that one standard could be implemented efficiently on top of all three native APIs. Three design choices define it:

  • Everything is declared up front. Pipelines, resource layouts, and render passes are built once as immutable objects, so the expensive validation happens at creation time, not on every draw. (The resource-layout half of that story has its own page: Bind Groups.)
  • Compute is a first-class citizen. WebGL could only draw; WebGPU exposes the GPU as a general parallel processor. FloraForge leans on this hard — its terrain meshes are built by a compute shader, not by the CPU.
  • It has its own shading language. WGSL was standardised alongside the API, so the same shader source is valid everywhere WebGPU runs — see Shaders & WGSL.

Despite the "Web" in the name, WebGPU is not only a browser API. Because it was specified as a clean, portable layer over Metal, Vulkan, and D3D12, it turned out to be an excellent way to write native graphics code too. That is exactly how FloraForge uses it.

The stack: one renderer, four destinations

FloraForge's renderer never talks to Metal or Vulkan directly. It talks to wgpu, a Rust library that implements the WebGPU API and translates it to whatever the machine underneath actually speaks. (wgpu is no toy shim — it is the same engine that powers WebGPU inside Firefox.)

FloraForge renderer src/renderer_wgpu/ — passes, materials, WGSL shaders one Rust codebase, written once calls the WebGPU API wgpu v27 Rust implementation of the WebGPU standard translates every command and shader per platform Metal macOS · iOS Vulkan Linux · Android Direct3D 12 Windows Browser WebGPU Chrome · Firefox · Safari desktop build — cargo run --release browser build — wasm
The portability stack. The renderer calls WebGPU through wgpu; wgpu translates to the native API of whatever machine it finds itself on — or, in the browser build, passes calls straight through to the browser's own WebGPU implementation.

The whole arrangement hangs on one line in the project manifest:

Cargo.toml — the dependency that is the renderer's whole platform layer
wgpu = "27"
winit = "0.29"   # windowing — gives wgpu a surface to draw into

On desktop, wgpu picks the best native backend at runtime — Metal on a Mac, Vulkan on Linux, Direct3D 12 on Windows — and translates both the API calls and the WGSL shaders into that backend's dialect. In the browser build, the same Rust code compiles to WebAssembly and wgpu becomes a thin wrapper over the browser's own navigator.gpu: no translation at all, because the browser speaks WebGPU natively. And that is no longer an experimental claim — as of 2026, every major browser ships WebGPU on by default: Chrome and Edge have since 2023, Safari 26 brought it to macOS, iOS, and iPadOS in 2025, and Firefox shipped it the same year (with Linux support still rolling out).

Talking to the GPU: instance, adapter, device

WebGPU's explicitness shows in how a program starts up. Nothing is implicit; you walk down a ladder of objects, each more concrete than the last. This is FloraForge's actual startup sequence, from src/renderer_wgpu/gpu_context.rs:

src/renderer_wgpu/gpu_context.rs — acquiring the GPU (error handling trimmed)
// 1. The instance: the entry point. The browser build may only use
//    the browser's WebGPU; native may use any backend wgpu supports.
let backends = if cfg!(target_arch = "wasm32") {
    wgpu::Backends::BROWSER_WEBGPU
} else {
    wgpu::Backends::all()
};
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { backends, ..Default::default() });
let surface = instance.create_surface(window)?;

// 2. The adapter: a specific physical GPU, asked for by preference.
let adapter = instance
    .request_adapter(&wgpu::RequestAdapterOptions {
        power_preference: wgpu::PowerPreference::HighPerformance,
        compatible_surface: Some(&surface),
        force_fallback_adapter: false,
    })
    .await?;

// 3. Limits: on the web, take what the browser grants; on native,
//    the conservative defaults are plenty for this engine.
let required_limits = if cfg!(target_arch = "wasm32") {
    adapter.limits()
} else {
    wgpu::Limits::default()
};

// 4. The device and queue: the logical connection the renderer keeps.
let (device, queue) = adapter
    .request_device(&wgpu::DeviceDescriptor {
        label: Some("world-gen-device"),
        required_features: wgpu::Features::empty(),
        required_limits,
        ..Default::default()
    })
    .await?;

Each rung has a job. The instance is the library itself — the set of backends you're willing to use. The adapter is one physical GPU; asking for HighPerformance matters on laptops with both an integrated and a discrete chip. The device is the logical connection the rest of the renderer holds, created with an explicit contract of features and limits — maximum texture sizes, buffer sizes, bind-group counts. FloraForge requests no optional features at all, which is part of why it runs everywhere. Finally the queue is where finished command buffers are submitted each frame, and the surface is the window (or canvas) being drawn into — whose image-swapping machinery is its own story, told in The Swapchain.

One subtlety in the snippet rewards a second look: on the web, the engine asks for adapter.limits() — whatever the browser is willing to grant — while on native it takes WebGPU's defaults. Browsers deliberately quantise the limits they report so that pages can't fingerprint the exact GPU model; negotiating limits explicitly at startup is the price of an API that is safe to expose to any web page.

In the engine
The payoff of this stack is that FloraForge has exactly one renderer. src/renderer_wgpu/ contains no platform-specific drawing code at all — the only platform branch in the GPU setup is the Backends::BROWSER_WEBGPU selection shown above. The terrain compute pass, the instanced vegetation, the sky and water shaders: all of it is written once against wgpu 27 and runs unchanged on Metal, Vulkan, Direct3D 12, and in a browser tab.