gfx-rs/wgpu
Design decisions
A non-exhaustive tour of the decisions that define wgpu's architecture. Where a decision is documented elsewhere (a README, a CHANGELOG note, a wiki article on the GitHub side), this page links there rather than repeating.
Three layers, not one
wgpu is split into wgpu (user-facing), wgpu-core (validation and FFI shape), and wgpu-hal (per-API backends). The split is what lets:
- Firefox embed
wgpu-coredirectly without dragging in the friendlierwgpuRust API or the C++/JS-shaped wrappers it would otherwise need to write. - Deno do the same via
deno_webgpu. - The native Rust application path stay idiomatic without polluting the FFI boundary.
The trade-off: every WebGPU method appears three times — once on wgpu::Device, once on wgpu_core::Global, once on hal::Device (or whichever HAL trait). When adding a method, you write it three times.
ID-based public surface in wgpu-core
wgpu-core exposes everything as Id<M>, never as a Rust reference. This makes the API natively FFI-safe (no lifetimes, no borrow checking across the boundary) and lets Firefox's IPC layer carry IDs across processes.
The cost is a registry layer (wgpu-core/src/{registry,storage,hub,id}.rs) that converts IDs to references on every entry point. That layer is hot enough that wgpu has invested in Storage's slot recycling, the pool deduplication cache, and several other optimizations.
wgpu-hal is unsafe by design
The HAL traits perform no validation. Errors are limited to "things the user can't anticipate" (OOM, lost device). Validation is expensive in graphics — duplicating Vulkan's validation layer would defeat the point.
This decision pushes the cost of safety onto wgpu-core. The benefit: the HAL is a thin shim over the underlying API, with overhead measured in tens of nanoseconds per call rather than microseconds.
Static dispatch, with a dynamic-dispatch path for wgpu-core
HAL traits are not object-safe. Each backend has its own concrete Api type and its own concrete Buffer, Texture, etc. types. wgpu-core would otherwise need to be parameterized over A: hal::Api, which would cripple its public surface.
The compromise: wgpu-hal/src/dynamic/ provides a parallel set of object-safe DynInstance, DynDevice, etc. traits with blanket impls. wgpu-core carries Vec<(Backend, Box<dyn DynInstance>)> and pays one indirection per method call. New methods on the HAL traits get a corresponding DynFoo method too.
wgpu uses dynamic dispatch the other way
wgpu's public types (Buffer, Texture, ...) hold Arc<dyn DispatchBuffer> from wgpu/src/dispatch.rs. This is so the same wgpu build can run against both the native (wgpu_core) and browser (webgpu) backends — the runtime can be wasm-with-WebGPU, wasm-with-WebGL2, or native, all in one binary.
naga is #![no_std] and #![forbid(unsafe_code)]
Shader translation happens on every shader compile, in every browser tab, in every Firefox content process. It must be portable, deterministic, and impossible to exploit. No allocations beyond alloc; no unsafe. The cost is some ergonomic loss (no std::collections, no Box<dyn Any>) and a slightly slower build.
Naga IR is arena-based
References inside shader IR are Handle<T> — a NonZeroU32 index into an Arena<T>. This:
- Avoids
&'alifetimes pervading the codebase, which would make it impossible to mutate the IR. - Makes the IR cheaply cloneable (clone the arenas) and serializable (with the
serializefeature). - Plays nicely with the validator and backends, which can store extra side-tables keyed by
Handle<T>.
The cost: you can't navigate the IR by following & references. Every walk uses arena indexing.
Static lock ranks in wgpu-core
Resource lifetime in WebGPU is complex (queue submissions, fence advancements, late-frees, mapping). Naive locking deadlocks immediately under that complexity.
The project's solution: every Mutex/RwLock in wgpu-core is created with a static rank (wgpu-core/src/lock/rank.rs). The lock::Mutex helper panics in debug builds if locks are taken out of rank order, catching ordering violations during testing. Recent commits show the rank graph is actively maintained as new locks are added.
Validation is not optional, but is layered
wgpu-core does the WebGPU-spec validation. Driver validation layers (Vulkan VVL, D3D12 debug layer, Metal validation layer) are also enabled by InstanceFlags::VALIDATION for caught cases that wgpu-core misses. The two layers are complementary; the project doesn't try to replace VVL.
CTS is the regression-test source of truth
The project relies heavily on the WebGPU CTS (gpuweb/cts) for regression coverage. cts_runner runs the CTS via Deno; cts_runner/test.lst lists what must pass. This is much higher-coverage than the in-tree tests could ever be on their own.
The trade-off: CTS expectations create churn (CTS bumps mean expectation updates), and CTS sometimes encodes behavior that's not in the spec. AGENTS.md is firm: the spec is the source of truth, not the CTS.
Trace and replay was important; now it isn't
The trace/replay path was originally a key debugging tool for Firefox crash reports. It's still in the codebase and still works for ad-hoc debugging, but docs/testing.md calls the test integration "soft-deprecated" — the trace capture system has bit-rotted and isn't actively maintained.
The project chose not to invest more in it because:
- The validation tests on the noop backend cover most of what trace tests covered.
- Real GPU regressions are caught by
tests/tests/wgpu-gpu/and the CTS. - Maintaining trace serialization across schema changes is costly.
Naga lives in the wgpu repo now
Until October 2023, naga was a separate repository (gfx-rs/naga). Co-evolution with wgpu-core was painful: naga changes that needed wgpu-core changes required two PRs and a coordinated bump. The 2023 merge folded naga's history into the wgpu repo and removed the friction.
The trade-off: naga is now coupled to wgpu's release cadence. Separate releases are still produced (wgpu-core-v29.0.1, naga-v27.0.3, etc.) but the development branches are unified.
Quarterly breaking releases
docs/release-checklist.md formalizes a roughly quarterly release cycle. Major versions (v22, v23, ..., v29) ship every ~3 months. This:
- Gives the WebGPU spec time to stabilize between cuts.
- Lets Firefox/Servo/Deno schedule their pickups.
- Concentrates breaking changes into known windows.
Version-bump churn elsewhere in the workspace is bounded by [workspace.package] having a single shared version value.
Where to read more
wgpu-hal/README.md— explicit list of HAL design choices.naga/README.md— naga's surface and conversion tool.CONTRIBUTING.md— including the "large pull requests are risky" doctrine.GOVERNANCE.md— project governance and decision-making.- The GitHub wiki at https://github.com/gfx-rs/wgpu/wiki covers patterns like ANGLE setup, debugging applications, architecture diagrams.
This wiki cross-references those documents rather than re-deriving their content.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.