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.)
The whole arrangement hangs on one line in the project manifest:
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:
// 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.
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.