Code Style
Coding conventions for contributing to the Renzora engine workspace — formatting, lints, naming, and how crates are laid out.
Formatting
Renzora uses rustfmt defaults. There is no top-level rustfmt.toml (the only one in the tree belongs to a vendored crate, crates/bevy_silk), so the standard style applies everywhere. Format before you commit, running rustfmt through the toolchain container:
renzora shell -- cargo fmt --all
CI runs two jobs — tests and Clippy (
.github/workflows/test.yml); there is no separatecargo fmt --checkgate. Run rustfmt anyway so diffs stay clean and reviewable.
Lints and Clippy
Workspace lints
The workspace defines exactly one lint rule in the root Cargo.toml, and every first-party crate opts into it:
# root Cargo.toml
[workspace.lints.rust]
unexpected_cfgs = { level = "allow", check-cfg = ['cfg(feature, values("dlopen"))'] }
# each crate's Cargo.toml
[lints]
workspace = true
That single allow exists because renzora::add! gates its FFI exports behind #[cfg(feature = "dlopen")], and that cfg is evaluated in the calling crate. Workspace plugins that have no dlopen feature would otherwise warn on every add! invocation — the check-cfg entry tells the compiler the feature is expected-but-unset. Keep the [lints] workspace = true block in new crates so they inherit this.
Clippy
CI denies all warnings. Lint locally with renzora check, which runs the following inside the ghcr.io/renzora/linux container:
cargo clippy --workspace --no-deps \
--exclude bevy_gauge \
--exclude bevy_hanabi \
--exclude bevy_mod_outline \
--exclude bevy_silk \
--exclude vleue_navigator \
--exclude bevy_mod_openxr \
--exclude bevy_mod_xr \
--exclude bevy_xr_utils \
-- -D warnings \
-A clippy::too_many_arguments \
-A clippy::type_complexity
--no-depskeeps Clippy off the vendored third-party crates that leak in as path-dependencies; the--excludeflags drop the vendored Bevy-ecosystem crates from the lint scope (they re-test upstream code against our Bevy version — noise, not signal).clippy::too_many_argumentsandclippy::type_complexityare allowed because they are inherent to Bevy systems and queries (Bevy allows them too). Do not fight those two.- Everything else is
-D warnings. Don't paper over a real lint with#[allow(...)]— fix it, or add a one-line comment explaining why the allow is correct.
Tests
Tests run with the same first-party scope as Clippy, excluding the vendored crates. Run them with renzora test, which wraps:
cargo test --workspace \
--exclude bevy_gauge --exclude bevy_hanabi --exclude bevy_mod_outline \
--exclude bevy_silk --exclude vleue_navigator \
--exclude bevy_mod_openxr --exclude bevy_mod_xr --exclude bevy_xr_utils
New first-party crates are covered automatically because the workspace globs pick them up (see below) and --workspace runs their suites.
Naming conventions
Standard Rust naming, plus the renzora_ crate prefix:
| Item | Convention | Example |
|---|---|---|
| Functions, methods | snake_case | spawn_entity() |
| Variables, fields | snake_case | player_health |
| Types, traits, enums | PascalCase | PhysicsPlugin, ScriptComponent |
| Enum variants | PascalCase | PluginScope::Runtime |
| Constants, statics | SCREAMING_SNAKE_CASE | MAX_CLIENTS |
| Modules, files | snake_case | scene_io.rs |
| First-party crates | snake_case, renzora_ prefix | renzora_physics |
- Plugin types end in
Plugin(PhysicsPlugin,MarkupPlugin,LumenPlugin). - The editor half of a dual-mode crate is named
renzora_<name>_editorand its plugin is<Name>EditorPlugin(e.g.renzora_physics_editor/PhysicsEditorPlugin). - Vendored crates keep their upstream names (
bevy_hui,bevy_silk,vleue_navigator) — don't rename them to fit the prefix.
Workspace and crate layout
Crates live flat under crates/ — there is no crates/core/ (or any other) subgrouping. The root Cargo.toml auto-includes members via globs, so adding a crate never means editing workspace.members:
members = [
".",
"crates/renzora",
"crates/renzora_*", # every plugin crate
"crates/renzora_*/editor", # nested editor halves of dual-mode crates
"crates/dynamic_plugin_loader",
# vendored bevy_* crates listed explicitly (bevy_oxr is deliberately excluded)
]
Many features are split into a dual-mode pair:
crates/renzora_<name>/— the lean runtime crate (anrlib), no editor code.crates/renzora_<name>/editor/— the editor-only half, package namerenzora_<name>_editor, linked only by the editor bundle.
To add a crate, just create its directory; the glob picks it up. Check a single crate from the workspace root with:
renzora check -p renzora_<name>
See Project Structure for the full layout and Building From Source for the
renzoraCLI commands that drive editor/runtime builds.
Inside a crate
A typical crate keeps the plugin and public API in lib.rs and splits the rest by responsibility — one concept per file:
crates/renzora_physics/src/
├── lib.rs # Plugin struct, public API, re-exports
├── components.rs # Component definitions
├── systems.rs # System functions
├── resources.rs # Resource definitions
└── events.rs # Event definitions
lib.rsexports the public surface; internal helpers staypub(crate)or private.- Split large files by responsibility rather than letting one module sprawl.
- Order
useimportsstd→ external crates → internal modules.
Dependencies
Pull shared crates from the workspace and depend on the SDK with default features off:
[package]
name = "renzora_myplugin"
version = "0.1.0"
edition = "2021"
[dependencies]
bevy = { workspace = true } # one shared Bevy
renzora = { path = "../renzora", default-features = false } # SDK contracts
[lints]
workspace = true
bevy(andlog) come from[workspace.dependencies]— use{ workspace = true }, don't pin your own version.- All first-party crates are
edition = "2021". - Add
features = ["editor"]to therenzoradependency only when you deriveInspectable, use#[renzora::post_process(...)], or touch the inspector/toolbar/shortcut registries — those live behind the SDK'seditorfeature (default features are empty). - Import the SDK with
use renzora::*;(or pull individual items likeuse renzora::Inspectable;). There is norenzora::prelude— it does not exist and will not compile. For ECS types use Bevy's ownuse bevy::prelude::*;.
Registering a plugin
Wire a plugin in with a single macro — there is no central plugin list and no manual app.add_plugins(...) call:
use bevy::prelude::*;
#[derive(Default)]
pub struct MyPlugin;
impl Plugin for MyPlugin {
fn build(&self, app: &mut App) { /* ... */ }
}
renzora::add!(MyPlugin); // Runtime scope (default)
// renzora::add!(MyEditorTool, Editor);
// renzora::add!(MyFoundation, Runtime, priority = -100);
PluginScope is exactly { Editor, Runtime } with equality matching — there is no "both" scope. A feature that needs editor tooling on top of runtime behaviour ships two plugins (e.g. GameUiPlugin + GameUiEditorPlugin). See Building Plugins for the full model.
Error handling and panics
| Context | Approach |
|---|---|
| Library crates with a real error domain | A thiserror-derived error enum (renzora_engine, renzora_import, renzora_rmip do this) |
| Fallible functions | Return Result<_, _> and propagate with ? |
| Bevy systems | Log and continue — warn!() / error!(), never panic to unwind a frame |
| Genuinely infallible operations | .expect("clear reason"), never a bare .unwrap() in shipping code |
anyhowis not a workspace convention here — it appears in only one (vendored) crate. Prefer a typedthiserrorerror for libraries, or Bevy's ownResult/BevyErrorfor system-level fallibility.
// Good: log and continue inside a system
if let Err(e) = save_scene(&scene) {
error!("failed to save scene: {e}");
}
// Avoid: unwrap without context in non-test code
let file = std::fs::read_to_string(&path).unwrap(); // don't
⚠️ Don't panic in
Plugin::build. The editor bundle installs each plugin insidecatch_unwindand nothing unwinds across the FFI boundary — a panic there is caught and counted, but it silently drops your plugin. Surface recoverable problems through the engine'sruntime_warningsring buffer (inrenzora.dll) instead.
Unsafe code
- Avoid
unsafeunless it is genuinely necessary — the main legitimate place is the FFI theadd!/export_plugin_bundle!macros generate (e.g. theplugin_bevy_hashtransmute) and GPU interop. - Every
unsafeblock needs a// SAFETY:comment stating the invariant that makes it sound. - Prefer a safe abstraction over leaving
unsafeat the call site.
// SAFETY: TypeId is a #[repr(transparent)] wrapper around a 128-bit value;
// transmuting it to [u64; 2] for the FFI ABI guard preserves the bits exactly.
unsafe { std::mem::transmute(std::any::TypeId::of::<bevy::ecs::world::World>()) }
Documentation
///doc comments on public items; the first line is a one-line summary (shown in IDE hover).- Skip private/internal items unless the logic is non-obvious.
/// Synchronizes physics body transforms back onto Bevy `Transform`s.
///
/// Runs after the physics step; handles interpolation when the physics
/// tick rate differs from the frame rate.
fn sync_physics_transforms(mut query: Query<(&mut Transform, &RigidBody)>) { /* ... */ }
Commit messages
The repo uses <scope>: <summary>, where the scope is the crate or subsystem touched and the summary is a short, lower-case, imperative phrase:
shader: fix unnecessary_sort_by lint in MaterialPerf::snapshot
ember: guard gauge/chart/waveform at both add sites
editor: completely remove the renzora_gauges plugin
physics: split into lean runtime + editor crate
ci: re-enable renzora_shader in tests + clippy
docs: refresh to post-merge state
Common scopes: a crate short-name (physics, ember, shader, animation), or an area (editor, engine, scene, export, plugins, ci, docs, README). Keep one logical change per commit.
Pull requests
- One logical change per PR.
- Describe why, not just what.
- Add or update tests for new behaviour and bug fixes; make sure
renzora testandrenzora checkpass before requesting review. - Keep vendored
bevy_*/vleue_navigatorcrates out of scope — they are upstream copies, not first-party code.
What's next?
- Building From Source — the
renzoraCLI commands the workflow actually uses - Project Structure — how the workspace's ~187 crates are organized
- Building Plugins — the
renzora::add!plugin model in depth