Open-Source Wikis

/

wgpu

/

How to contribute

/

Patterns and conventions

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 in wgpu-core.
  • add validation to wgpu-hal. The HAL is unsafe by design; wgpu-core is the safe layer.
  • add platform-specific dependencies to wgpu-types. The types crate must build on every target.
  • depend on naga from wgpu-hal. Shader translation is wgpu-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; (and extern crate std; only behind a cfg).
  • import from core:: and alloc:: rather than std::. The lints clippy::std_instead_of_core and clippy::std_instead_of_alloc are warned at the crate level.
  • use hashbrown::HashMap (naga::FastHashMap and friends, see naga/src/lib.rs) rather than std::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::Error for typed errors. Almost every public module has a *Error enum.
  • Errors should carry enough context to be actionable. Many errors include the offending label (Label<'static>).
  • The clippy::large-error-threshold in clippy.toml keeps 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-core or wgpu propagate it. Do not panic. The crate-level #![warn(clippy::panic, clippy::todo, clippy::dbg_macro, clippy::print_stdout, clippy::print_stderr)] in naga/src/lib.rs enforces this.
  • When you do need to assert (impossible internal states), use assert! with a message; avoid unwrap() without context.

Logging

  • Use the log crate macros: log::trace!, log::debug!, log::info!, log::warn!, log::error!. There is no tracing dependency.
  • wgpu-core provides api_log!, api_log_debug!, and resource_log! macros (wgpu-core/src/lib.rs) that are gated by features and otherwise lower to trace!/debug!. Use them when you're logging API calls from inside Global methods.

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:

  1. Pick (or add) a rank in wgpu-core/src/lock/rank.rs.
  2. Make sure all call sites that take the new lock either already hold a lower-ranked lock or none.
  3. 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:

  • Device keeps a LifetimeTracker (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::poll advances 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::RwLock with a rank, not raw parking_lot types.

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/ and naga/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 fmt applies workspace-wide. The root rustfmt.toml is empty (i.e. defaults).
  • taplo fmt formats *.toml. CI checks this.
  • prettier is configured (.prettierrc.toml, .prettierignore) for non-Rust files.
  • The typos crate's config is typos.toml. CI runs typos.

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.md is 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.

Patterns and conventions – wgpu wiki | Factory