gfx-rs/wgpu
Patterns and conventions
This page captures the conventions a new contributor needs in order to write code that fits the rest of the repository. The full list of project rules is in AGENTS.md and CONTRIBUTING.md; this page focuses on conventions you'll see in source files.
Crate boundaries are real
Every layer in wgpu is a separately published crate, and the boundary between them is intentional. Do not:
- add validation to
wgpu(the user-facing crate). Validation belongs inwgpu-core. - add validation to
wgpu-hal. The HAL is unsafe by design;wgpu-coreis the safe layer. - add platform-specific dependencies to
wgpu-types. The types crate must build on every target. - depend on
nagafromwgpu-hal. Shader translation iswgpu-core's responsibility.
The wgpu-naga-bridge crate exists specifically to keep naga and wgpu-core decoupled.
The no_std boundary
wgpu, wgpu-core, and naga are all #![no_std] crates with optional std features. New code should:
extern crate alloc;(andextern crate std;only behind acfg).- import from
core::andalloc::rather thanstd::. The lintsclippy::std_instead_of_coreandclippy::std_instead_of_allocare warned at the crate level. - use
hashbrown::HashMap(naga::FastHashMapand friends, seenaga/src/lib.rs) rather thanstd::collections::HashMap.
naga is also #![forbid(unsafe_code)]. Don't add unsafe to it.
Static dispatch first, dynamic dispatch where required
wgpu-hal uses static dispatch through associated-type traits. Each backend defines a marker type (vulkan::Api, metal::Api, ...) and the rest is generic. The dynamic-dispatch wrappers in wgpu-hal/src/dynamic/ exist solely so wgpu-core can hold a Vec<Box<dyn DynInstance>>. New code should default to the static path; touch dynamic/ only when adding a new HAL trait method.
The wgpu crate uses dynamic dispatch in the other direction: handles store Arc<dyn DispatchFoo> (wgpu/src/dispatch.rs) so the same handle type works on the wgpu_core, webgpu, and custom backends.
ID-based public API in wgpu-core
wgpu-core is shaped for FFI. Public functions take Ids, not Rust references. New methods on Global follow the existing pattern: take an Id, look up the resource through Hub/Registry, validate, call into HAL, return either an error or a new Id. See wgpu-core/src/device/global.rs for the canonical examples.
Error handling
- Use
thiserror::Errorfor typed errors. Almost every public module has a*Errorenum. - Errors should carry enough context to be actionable. Many errors include the offending label (
Label<'static>). - The
clippy::large-error-thresholdinclippy.tomlkeeps error enums from growing too big; if you trip it, box the offending variant. - For invalid input from the user, return an error and let
wgpu-coreorwgpupropagate it. Do not panic. The crate-level#![warn(clippy::panic, clippy::todo, clippy::dbg_macro, clippy::print_stdout, clippy::print_stderr)]innaga/src/lib.rsenforces this. - When you do need to assert (impossible internal states), use
assert!with a message; avoidunwrap()without context.
Logging
- Use the
logcrate macros:log::trace!,log::debug!,log::info!,log::warn!,log::error!. There is notracingdependency. wgpu-coreprovidesapi_log!,api_log_debug!, andresource_log!macros (wgpu-core/src/lib.rs) that are gated by features and otherwise lower totrace!/debug!. Use them when you're logging API calls from insideGlobalmethods.
Locking
wgpu-core enforces a static lock ordering through wgpu-core/src/lock/. Every Mutex/RwLock is created with a rank (rank::QUEUE_PENDING_WRITES, rank::DEVICE_FENCE, ...). Locks must be acquired in increasing rank order. The lock graph is documented inline in wgpu-core/src/lock/. When adding a new lock:
- Pick (or add) a rank in
wgpu-core/src/lock/rank.rs. - Make sure all call sites that take the new lock either already hold a lower-ranked lock or none.
- Run
cargo nextest run -p wgpu-core— the lock helpers panic in debug builds on rank violations.
The repo also has a lock-analyzer crate (lock-analyzer/) for offline analysis.
Resource lifetime
Resources are reference-counted (Arc<...>). When a resource's last public reference is dropped, it isn't freed immediately — it has to wait for any in-flight GPU work. The flow is:
Devicekeeps aLifetimeTracker(wgpu-core/src/device/life.rs) of resources scheduled for cleanup.- Each submission carries a
SubmissionIndex(a fence value). Resources are tagged with the index of the submission that uses them. - When
Queue::poll/Device::polladvances the fence, resources whose tag is now safe are dropped.
Do not bypass this: if you need to free GPU memory, schedule it through the lifetime tracker.
Mutable state in Device
Device is large (wgpu-core/src/device/resource.rs is ~210 KB). Most state is wrapped in fine-grained locks rather than a single big mutex. New mutable fields should:
- get their own lock when they're independent.
- live under an existing lock if access patterns demand it.
- always be added through
lock::Mutex/lock::RwLockwith a rank, not rawparking_lottypes.
Naga IR mutation
naga's arenas are append-only at the public API. To modify an existing module:
- build a new arena, copy what you need, and emit indices.
- or use
compact(naga/src/compact/), which produces a new module with reachable items only.
When adding a new IR variant or field, update:
naga/src/ir/- the validator in
naga/src/valid/ - every relevant frontend in
naga/src/front/ - every relevant backend in
naga/src/back/ - snapshot tests under
naga/tests/in/andnaga/tests/out/
The "butterfly" snapshot pattern (WGSL → all backends; GLSL/SPIR-V → WGSL) is documented in docs/testing.md.
Comments
AGENTS.md is explicit: keep comments to a strict minimum. Add them on non-trivial code, on function signatures whose arguments aren't self-explanatory, and on type definitions and their members. Don't narrate what the code does line-by-line. Don't use emoji.
Formatting
cargo fmtapplies workspace-wide. The rootrustfmt.tomlis empty (i.e. defaults).taplo fmtformats*.toml. CI checks this.prettieris configured (.prettierrc.toml,.prettierignore) for non-Rust files.- The
typoscrate's config istypos.toml. CI runstypos.
Changelog
User-visible changes (changes to documented public APIs, significant bugfixes, new functionality) belong in CHANGELOG.md under the current Unreleased section. The format is enforced by cargo xtask changelog (see xtask/src/changelog.rs).
CTS lists
Three files in cts_runner/ track WebGPU CTS expectations:
test.lst— selectors that must pass. CI fails if anything in here fails.fail.lst— selectors with known failures. Add a// xx%comment with the pass rate when partial.skip.lst— selectors that are ≥ 90 % skips.
Use the broadest wildcard that exclusively matches the right file. AGENTS.md describes the rules in detail.
Don't speculate from the CTS
A recurring rule from AGENTS.md: a test in the CTS expecting a behavior is not proof that the behavior is correct. Verify in the WebGPU or WGSL specification.
What doesn't go in CHANGELOG
- Internal refactors with no user-visible effect.
- Comment-only changes.
- Test-only changes (the test taxonomy in
docs/testing.mdis the right place to discuss those).
When in doubt, ask in the PR.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.