pebble-engine 2.5.1

A modular, ECS-style graphics/app framework for Rust.
Documentation
# Apps and Plugins


`App` is the central object: it owns the ECS world, every resource, and every system, organized into stages. It's built by chaining methods that take `self` by value and return `Self`, so a typical setup reads as one expression:

```rust,ignore
use pebble::app::App;
use pebble::ecs::system::SystemStage;
use pebble::graphics::GraphicsPlugin;

App::new()
    .with_logging()
    .add_plugin(GraphicsPlugin::new())
    .add_system(SystemStage::Ready, setup)
    .add_system(SystemStage::Update, my_game_logic)
    .run();
```

`App::new()` gives you a completely empty app — no window, no GPU backend, no `Time`. Everything is opt-in via `.add_plugin(...)`.

## Plugins


A `Plugin` is a composable unit of app setup — it inserts resources, registers systems, or adds further plugins:

```rust,ignore
use pebble::app::App;
use pebble::ecs::plugin::Plugin;

struct MyPlugin;

impl Plugin for MyPlugin {
    fn build(self, app: App) -> App {
        app.insert_resource(MyResource::default())
            .add_system(SystemStage::Update, my_system)
    }
}
```

Any `FnOnce(App) -> App` also implements `Plugin`, so a plain closure works without a named type — handy for one-off setup you don't intend to reuse.

## Plugins composing other plugins


"Adds further plugins of its own" is how pebble's own `GraphicsPlugin` is actually built — it's not a special case, just three smaller plugins bundled behind one name, with a `DeviceFeatures` value threaded through to `BackendPlugin`:

```rust,ignore
pub struct GraphicsPlugin {
    features: DeviceFeatures,
}

impl GraphicsPlugin {
    pub fn new() -> Self {
        Self { features: DeviceFeatures::empty() }
    }

    pub fn with_features(features: DeviceFeatures) -> Self {
        Self { features }
    }
}

impl Plugin for GraphicsPlugin {
    fn build(self, app: App) -> App {
        app.add_plugin(WindowPlugin::default())
            .add_plugin(BackendPlugin::with_features(self.features))
            .add_plugin(BuiltinAssetsPlugin)
    }
}
```

Your own plugins can do the same — group a handful of related plugins/systems your project always wants together under one name, so call sites stay a one-liner instead of repeating the same five `.add_plugin(...)` calls in every example/binary.

A closure is the lighter-weight version of this for one-off, non-reusable setup — useful for something conditional you'd otherwise have to hand-roll a whole struct for:

```rust,ignore
fn debug_plugin(app: App) -> App {
    app.add_system(SystemStage::PostRender, print_frame_time)
}

let app = App::new().add_plugin(GraphicsPlugin::new());
let app = if cfg!(debug_assertions) { app.add_plugin(debug_plugin) } else { app };
```

## GPU device features


`DeviceFeatures` is a bitflag set of optional GPU capabilities (`ADDRESS_MODE_CLAMP_TO_BORDER`, `TIMESTAMP_QUERY`, `PIPELINE_STATISTICS_QUERY`, `INDIRECT_FIRST_INSTANCE`, `TEXTURE_COMPRESSION_BC`/`ETC2`/`ASTC`, `POLYGON_MODE_LINE`, `FLOAT32_FILTERABLE`, `SHADER_F16`, `SUBGROUP`, `RAY_QUERY`, `MESH_SHADER`) requested when the GPU device is created. `GraphicsPlugin::new()` requests none of them — combine flags with `|` and pass them to `with_features`:

```rust,ignore
App::new()
    .add_plugin(GraphicsPlugin::with_features(
        DeviceFeatures::SUBGROUP | DeviceFeatures::SHADER_F16,
    ))
    .run();
```

Only request a feature the adapter actually supports — `request_device` panics otherwise. Once the backend is up, `Read<Backend>::features()` reports what was actually granted:

```rust,ignore
fn check(backend: Read<Backend>) {
    if backend.features().contains(DeviceFeatures::SUBGROUP) {
        // safe to dispatch a compute shader using subgroup ops
    }
}
```

Because features default to none, anything built into pebble that depends on one — like the `NearestClampBorder` sampler needing `ADDRESS_MODE_CLAMP_TO_BORDER`, see [Samplers](./samplers.md) — has to handle the "not enabled" case itself rather than assuming it's there.

## GPU limits


`DeviceLimits` reports the numeric limits the device was actually granted — max texture size, bind groups, buffer sizes, vertex attributes, compute workgroup sizes, and so on. Unlike `DeviceFeatures`, there's nothing to request: limits aren't opt-in, so this is read-only, via `Read<Backend>::limits()`:

```rust,ignore
fn check(backend: Read<Backend>) {
    let limits = backend.limits();
    if width > limits.max_texture_dimension_2d {
        tracing::error!("texture too large for this device");
    }
}
```

Like `DeviceFeatures`, `DeviceLimits` mirrors a curated subset of `wgpu::Limits` — the fields most code actually reads (textures, bind groups, buffers, vertex layout, compute workgroups). It omits the mesh-shader/ray-tracing-specific limits that pair with `DeviceFeatures::MESH_SHADER`/`RAY_QUERY`.

## Key methods


- `insert_resource<T>`/`remove_resource<T>` — see [Resources]./resources.md.
- `add_plugin<P: Plugin>` — runs the plugin's `build`.
- `add_system(stage, system)` — see [Systems and Stages]./systems-and-stages.md.
- `add_event::<T>()` — see [Events]./events.md.
- `add_observer(fn)` — see [Observers]./observers.md.
- `set_runner(fn)` — overrides how the main loop is driven; `WindowPlugin` uses this to hand control to `winit`'s event loop instead of the default headless polling loop.
- `with_logging()` — initializes `tracing_subscriber` so `tracing::info!`/`warn!`/`error!` calls throughout the engine actually print.
- `run()` — consumes the app and starts it.