Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,165 changes: 988 additions & 1,177 deletions Cargo.lock

Large diffs are not rendered by default.

44 changes: 18 additions & 26 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,45 +1,37 @@
[package]
name = "bevy_config"
name = "bevy_settings_plus"
version = "0.1.0"
edition = "2024"
rust-version = "1.85"
license = "MIT OR Apache-2.0"
description = "Capability-aware configuration system for Bevy: adapter detection, platform defaults, user overrides, engine bindings."
repository = "https://github.com/pavlov-net/bevy_config"
description = "Capability-aware extension to bevy_settings: adapter detection, platform defaults, caps clamping, engine bindings."
repository = "https://github.com/pavlov-net/bevy_settings_plus"
readme = "README.md"
keywords = ["bevy", "config", "settings", "options", "graphics"]
keywords = ["bevy", "settings", "config", "graphics"]
categories = ["game-development", "config"]

[features]
default = ["ron"]
ron = ["dep:ron"]
default = []
dlss = ["bevy_anti_alias/dlss"]

[package.metadata.docs.rs]
# DLSS pulls dlss_wgpu which requires the NVIDIA SDK at build time, which
# docs.rs doesn't have — explicitly skip it. Add other features here if
# they don't pull external SDKs.
features = ["ron"]
features = []

[dependencies]
bevy_app = "0.18.1"
bevy_ecs = "0.18.1"
bevy_reflect = "0.18.1"
bevy_render = "0.18.1"
bevy_window = "0.18.1"
bevy_anti_alias = "0.18.1"
bevy_log = "0.18.1"
serde = { version = "1", features = ["derive"] }
ron = { version = "0.12", optional = true }
thiserror = "2"
wgpu-types = { version = "27", default-features = false }

[target.'cfg(not(target_family = "wasm"))'.dependencies]
directories = "6"

[target.'cfg(target_family = "wasm")'.dependencies]
web-sys = { version = "0.3", features = ["Window", "Storage"] }
serde_json = "1"
bevy_app = { git = "https://github.com/bevyengine/bevy" }
bevy_ecs = { git = "https://github.com/bevyengine/bevy" }
bevy_reflect = { git = "https://github.com/bevyengine/bevy" }
bevy_render = { git = "https://github.com/bevyengine/bevy" }
bevy_window = { git = "https://github.com/bevyengine/bevy" }
bevy_anti_alias = { git = "https://github.com/bevyengine/bevy" }
bevy_log = { git = "https://github.com/bevyengine/bevy" }
# Upstream renamed the package to `bevy-settings` (matches its crates.io name);
# the Rust import name stays `bevy_settings`.
bevy-settings = { git = "https://github.com/bevyengine/bevy" }
wgpu-types = { version = "29", default-features = false }

[dev-dependencies]
bevy = { version = "0.18.1", default-features = false, features = ["3d"] }
bevy = { git = "https://github.com/bevyengine/bevy", default-features = false, features = ["3d"] }
117 changes: 85 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,62 +1,115 @@
# bevy_config

Capability-aware configuration system for [Bevy](https://bevy.org).

The shape: **detect adapter caps → pick platform default → layer user overrides → bind to engine resources**.

A single hardcoded set of graphics defaults is wrong on at least one platform. Wasm/WebGPU first-time visitors should get conservative defaults. Desktop with an RT-capable GPU should get the full picture. Adapter features (ray query, DLSS support, vendor, backend) decide whether features are even *available* before user preference comes in.

`bevy_config` solves this with a small kernel:

- `AdapterCaps` — wgpu features and limits → stable, plain bools/ints/enums in the main world
- `PlatformTarget` — Wasm / Windows / Linux / macOS / Other
- `Config` trait — `platform_default(target)`, `merge(&overrides)`, `clamp_to(&caps)`, `current_overrides()`
- `ConfigBackend` trait — load/store; default impls for native RON files (atomic write) and wasm `localStorage`
- `ConfigPlugin<C>` — wires the lifecycle: load → merge → clamp → apply, on top of `Plugin::build`/`finish`

…plus an opinionated universal schema (`CommonConfig`) covering display, render, and accessibility, and engine bindings for the graphics axes (window mode, anti-alias, MSAA, DLSS — feature-gated).

Audio configuration is intentionally not part of `CommonConfig` because the bus convention is backend-specific (firewheel/seedling/kira/oddio all model differently). When the firewheel/seedling integration lands it will ship as its own `Config` type behind a cargo feature, registered alongside `CommonConfig` via a separate `ConfigPlugin`.

Game-specific quality dials use the same `Config` trait with a custom type and a separate `ConfigPlugin<MyGameConfig>` registration.
# bevy_settings_plus

Capability-aware extension to [`bevy_settings`](https://docs.rs/bevy_settings)
for [Bevy](https://bevy.org).

`bevy_settings` (in Bevy 0.19+) ships a polished settings persistence kernel:
TOML files, derive-driven `SettingsGroup` resources, change detection,
async + debounced saves. `bevy_settings_plus` layers the *graphics-aware* concerns
on top:

- **Adapter caps detection** — `AdapterCaps` is a stable, plain-data summary
of the live wgpu adapter (backend, vendor, ray-query support, DLSS
reachability, …). Detected once and inserted as a main-world resource.
- **Caps clamping** — settings types implement `CapsAware` and register
`#[reflect(CapsAware)]`. `CapsAwarePlugin` discovers them by reflection
(mirroring how `bevy_settings` discovers `SettingsGroup` types) and runs
each type's `clamp_to(caps)` after settings have been loaded from disk.
- **Opinionated graphics schema** — `DisplaySettings`, `RenderSettings`, and
`AccessibilitySettings` resources with platform-aware defaults and engine
bindings that drive the primary window and tagged cameras.

## Usage

```rust
use bevy::prelude::*;
use bevy_config::prelude::*;
use bevy_settings_plus::prelude::*;

fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(ConfigPlugin::<CommonConfig>::new(
FileBackend::<CommonConfig>::new("com", "example", "my_game"),
))
.add_plugins(CommonBindingsPlugin)
.add_plugins(SettingsPlusPlugins::new("net.pavlov.my_game"))
.add_systems(Startup, spawn_camera)
.run();
}

fn spawn_camera(mut commands: Commands) {
commands.spawn((Camera3d::default(), BevyConfigCamera));
commands.spawn((Camera3d::default(), SettingsCamera));
}
```

The camera marker tells the binding system which cameras to drive. Spawn it whenever — at `Startup`, on `OnEnter(GameState::Playing)`, after async asset load — the bindings apply on `Add<BevyConfigCamera>` so deferred spawns work without ceremony.
The camera marker tells the binding system which cameras to drive. Spawn it
whenever — at `Startup`, on `OnEnter(GameState::Playing)`, after async asset
load — the bindings apply on `Add<SettingsCamera>` so deferred spawns work
without ceremony.

To save a user's settings (e.g., from an "Apply" button in a menu):
To save user changes (e.g., from an "Apply" button or after dragging a
slider), use `bevy_settings`' commands directly:

```rust
fn save_button(mut commands: Commands) {
commands.queue(SaveConfig::<CommonConfig>::default());
// Debounced — coalesces bursts of changes into one write.
commands.queue(SavePreferencesDeferred::default());
}

fn save_now(mut commands: Commands) {
// Async, fire-once.
commands.queue(SavePreferences::IfChanged);
}
```

For a robust save-on-exit, the upstream pattern is to issue
`SavePreferencesDeferred` whenever the user changes something *and* a
synchronous `SavePreferencesSync::IfChanged` immediately before app exit.

## Extending — your own caps-aware game settings

```rust
use bevy::prelude::*;
use bevy_settings_plus::prelude::*;

#[derive(Resource, SettingsGroup, Reflect, Default)]
#[reflect(Resource, SettingsGroup, CapsAware, Default)]
#[settings_group(group = "my_game")]
struct MyGameQuality {
ray_tracing: bool,
shadow_distance: f32,
}

impl CapsAware for MyGameQuality {
fn clamp_to(&mut self, caps: AdapterCaps) {
if !caps.ray_query {
self.ray_tracing = false;
}
self.shadow_distance = self.shadow_distance.clamp(10.0, 1000.0);
}
}

// Then, in your plugin's `build`:
// app.register_type::<MyGameQuality>();
```

See `examples/basic.rs` for the minimal setup and `examples/deferred_camera.rs` for the post-`Startup` spawn pattern.
`bevy_settings::PreferencesPlugin` (added by `SettingsPlusPlugins`) discovers
the type from the registry and loads it from `<prefs_dir>/<app>/settings.toml`.
`CapsAwarePlugin` then clamps it after the wgpu adapter is known.

## Cargo features

- `dlss` — wires [DLSS](https://developer.nvidia.com/rtx/dlss) via
`bevy_anti_alias`. Requires the NVIDIA DLSS SDK at build time; only
enable for shipping NVIDIA-targeted builds.

## Status

`v0.1.x` targets **Bevy 0.18.1**. API may change between minor versions. A `0.19-dev` branch tracks Bevy `main` for the next-release line.
`0.19-dev` is the active branch and tracks Bevy `main` (currently
`0.19.0-dev`). API may change between minor versions.

This crate replaces `bevy_config` (the predecessor on the `main` branch,
which targeted Bevy 0.18.1 with its own persistence kernel before
`bevy_settings` landed upstream).

See `examples/basic.rs` for the minimal setup and
`examples/deferred_camera.rs` for the post-`Startup` spawn pattern.

## License

Expand Down
52 changes: 24 additions & 28 deletions examples/basic.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
//! Minimal `bevy_config` example.
//! Minimal `bevy_settings_plus` example.
//!
//! Adds the kernel + common bindings, observes `ConfigApplied`, and prints the
//! detected `AdapterCaps` and resolved `CommonConfig` once at startup.
//! Adds the plugin group, prints the detected `AdapterCaps` and resolved
//! per-axis settings once at startup, and spawns a `SettingsCamera` so
//! the render bindings have something to drive.
//!
//! Run with:
//!
Expand All @@ -10,26 +11,32 @@
//! ```

use bevy::prelude::*;
use bevy_config::prelude::*;
use bevy_settings_plus::prelude::*;

fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(ConfigPlugin::<CommonConfig>::new(
FileBackend::<CommonConfig>::new("net", "pavlov", "bevy_config_basic_example"),
.add_plugins(SettingsPlusPlugins::new(
"net.pavlov.bevy_settings_plus_basic_example",
))
.add_plugins(CommonBindingsPlugin)
.add_observer(on_config_applied)
.add_systems(Startup, spawn_camera)
.add_systems(Startup, (spawn_camera, log_resolved_settings))
.run();
}

fn on_config_applied(
_: On<ConfigApplied<CommonConfig>>,
config: Res<CommonConfig>,
fn spawn_camera(mut commands: Commands) {
commands.spawn((
Camera3d::default(),
Transform::from_xyz(0.0, 0.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
SettingsCamera,
));
}

fn log_resolved_settings(
caps: Option<Res<AdapterCaps>>,
display_cfg: Res<DisplaySettings>,
render_cfg: Res<RenderSettings>,
) {
info!("=== bevy_config: ConfigApplied ===");
info!("=== bevy_settings_plus: Startup ===");
if let Some(caps) = caps {
info!(
"AdapterCaps: backend={:?} vendor={:?} ray_query={} dlss_supported={}",
Expand All @@ -39,22 +46,11 @@ fn on_config_applied(
info!("AdapterCaps not yet inserted (running headless?).");
}
info!(
"Display: window_mode={:?} vsync={:?}",
config.display.window_mode, config.display.vsync
"DisplaySettings: window_mode={:?} vsync={:?}",
display_cfg.window_mode, display_cfg.vsync
);
info!(
"Render: anti_alias={:?} msaa={:?} upscaler={:?} ray_tracing={}",
config.render.anti_alias,
config.render.msaa,
config.render.upscaler,
config.render.ray_tracing
"RenderSettings: anti_alias={:?} msaa={:?} upscaler={:?} ray_tracing={}",
render_cfg.anti_alias, render_cfg.msaa, render_cfg.upscaler, render_cfg.ray_tracing
);
}

fn spawn_camera(mut commands: Commands) {
commands.spawn((
Camera3d::default(),
Transform::from_xyz(0.0, 0.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
BevyConfigCamera,
));
}
28 changes: 13 additions & 15 deletions examples/deferred_camera.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
//! Demonstrates deferred camera spawning.
//!
//! This is the realistic pattern for non-trivial games: the camera is
//! spawned after a state transition (e.g., `OnEnter(AppState::InGame)`)
//! or after async asset load — *not* at `Startup`. The
//! [`BevyConfigCamera`] marker still gets the right anti-alias / MSAA /
//! upscaler on its first frame, because [`CommonBindingsPlugin`]
//! registers an `On<Add, BevyConfigCamera>` observer.
//! The realistic pattern for non-trivial games: the camera is spawned after
//! a state transition (e.g., `OnEnter(AppState::InGame)`) or after async
//! asset load — *not* at `Startup`. The [`SettingsCamera`] marker still
//! gets the right anti-alias / MSAA / upscaler on its first frame, because
//! [`RenderSettingsPlugin`] registers an `On<Add, SettingsCamera>` observer.
//!
//! Run with:
//!
Expand All @@ -15,7 +14,7 @@

use bevy::prelude::*;
use bevy_anti_alias::taa::TemporalAntiAliasing;
use bevy_config::prelude::*;
use bevy_settings_plus::prelude::*;

#[derive(States, Default, Debug, Clone, Eq, PartialEq, Hash)]
enum AppState {
Expand All @@ -27,10 +26,9 @@ enum AppState {
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(ConfigPlugin::<CommonConfig>::new(
FileBackend::<CommonConfig>::new("net", "pavlov", "bevy_config_deferred_example"),
.add_plugins(SettingsPlusPlugins::new(
"net.pavlov.bevy_settings_plus_deferred_example",
))
.add_plugins(CommonBindingsPlugin)
.init_state::<AppState>()
.add_systems(Startup, kick_off_loading)
.add_systems(OnEnter(AppState::InGame), spawn_camera)
Expand All @@ -47,21 +45,21 @@ fn kick_off_loading(mut next: ResMut<NextState<AppState>>) {
}

fn spawn_camera(mut commands: Commands) {
info!("Entering InGame: spawning camera with BevyConfigCamera marker");
info!("Entering InGame: spawning camera with SettingsCamera marker");
commands.spawn((
Camera3d::default(),
Transform::from_xyz(0.0, 0.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
BevyConfigCamera,
SettingsCamera,
));
}

/// Confirm the binding observer attached `TemporalAntiAliasing` (the
/// desktop platform default) the moment the deferred camera was tagged
/// with `BevyConfigCamera`, despite the camera spawning *after* the
/// initial `ConfigApplied` pulse.
/// with `SettingsCamera`, despite the camera spawning *after* the
/// initial `RenderSettings`-changed pulse.
fn verify_taa_attached(
add: On<Add, TemporalAntiAliasing>,
cameras: Query<(), With<BevyConfigCamera>>,
cameras: Query<(), With<SettingsCamera>>,
) {
if cameras.contains(add.entity) {
info!(
Expand Down
Loading
Loading