concinnity-engine 0.19.119

Runtime engine for Concinnity: ECS schedule, graphics, spawn, streaming
//! Queries over a world that only a renderer-bearing runtime can answer. The
//! world itself is concinnity-core's and names no backend, no GPU profile, and
//! no streaming pool; each of these reads one of the resources this crate's
//! render band parks there, or the systems it built.

use concinnity_core::ecs::World;
use concinnity_core::render::backend::{GpuProfile, RenderBackend};
use concinnity_host::store::paths::StateTree;

use crate::animation::AnimationSystem;
use crate::app::budget::{MemoryBudget, ThreadBudget};
use crate::app::mem_drift::MemoryDrift;
use crate::ecs::ActiveRenderBackend;
use crate::gfx::streaming::system::{StreamingPressure, StreamingState, StreamingStats};

/// Whether the world runs with a renderer: the run mode `Runtime::start`
/// resolved and published, or a constructed `GraphicsSystem` for a world
/// started some other way.
///
/// Answers the same either side of `start`, so a caller choosing a loop does
/// not have to care about the timing. A runtime that has not resolved yet
/// should ask `Runtime::render_mode` instead, which resolves on demand.
pub fn renders(world: &World) -> bool {
    world
        .resource::<crate::ecs::render_mode::RenderMode>()
        .is_some_and(|m| m.renders())
        || world.systems().iter().any(|s| {
            s.downcast_ref::<crate::gfx::system::GraphicsSystem>()
                .is_some()
        })
}

/// Per-pool `(resident, pending, unloaded)` streaming counts from the parked
/// `StreamingState` (StreamingSystem drives it against the backend each
/// frame). `None` before graphics init parks it, and from inside a system
/// step, which takes the state out. Read by the `cn debug` server's
/// `streaming` command and the editor's Health panel.
pub fn streaming_stats(world: &World) -> Option<StreamingStats> {
    world
        .resource::<StreamingState>()
        .map(|s| s.streaming_stats())
}

/// Live process-RAM back-off pressure on streaming, published by
/// StreamingSystem on its throttled RSS sample. `None` before the first sample
/// or when no `MemoryBudget` / RSS is available (the valve is inert).
pub fn streaming_pressure(world: &World) -> Option<StreamingPressure> {
    world.resource::<StreamingPressure>().copied()
}

/// Long-session memory drift, folded from the same throttled sample as the
/// back-off valve. `None` until the session settles enough for a baseline, and
/// for the same reasons `streaming_pressure` is absent.
pub fn memory_drift(world: &World) -> Option<MemoryDrift> {
    world.resource::<MemoryDrift>().copied()
}

/// The detected GPU's capability + memory profile, published by graphics init.
/// `None` before init runs, and `GpuProfile::UNKNOWN` when the backend could
/// not classify the device.
pub fn gpu_profile(world: &World) -> Option<GpuProfile> {
    world.resource::<GpuProfile>().copied()
}

/// The state tree `Runtime::start` published: where this world reads and writes.
/// `None` for a world running against no tree, which is a world that touches no
/// disk. What every system reads instead of resolving a path of its own.
pub fn state_tree(world: &World) -> Option<&StateTree> {
    world.resource::<StateTree>()
}

/// The process thread budget the runtime published at start. `None` before
/// `Runtime::start`
/// installs it. Read by the `cn debug` server's `budget` command.
pub fn thread_budget(world: &World) -> Option<ThreadBudget> {
    world.resource::<ThreadBudget>().copied()
}

/// The world's memory budget, once `start` has published one.
pub fn memory_budget(world: &World) -> Option<MemoryBudget> {
    world.resource::<MemoryBudget>().copied()
}

/// Take the live render backend out of the world's parked slot, leaving the
/// world backend-less. A host hands it to a rebuilt world as a
/// [`PendingBackend`](crate::live_edit::PendingBackend), so the rebuild keeps
/// the OS window and GPU device. `None` when the world never built a backend
/// (or it was already yielded).
pub fn take_render_backend(world: &mut World) -> Option<Box<dyn RenderBackend>> {
    world
        .resource_mut::<ActiveRenderBackend>()
        .and_then(|slot| slot.0.take())
}

/// The world's AnimationSystem, when one was built.
pub fn animation_system_mut(world: &mut World) -> Option<&mut AnimationSystem> {
    world
        .systems_mut()
        .iter_mut()
        .find_map(|system| system.downcast_mut::<AnimationSystem>())
}

#[cfg(test)]
mod tests {
    use super::*;

    // The published run mode is what `renders` reports, which is the signal
    // callers use to choose the loop. (The post-start GraphicsSystem path
    // can't be unit-tested here: its `init` builds the GPU backend.)
    #[test]
    fn the_published_run_mode_is_what_renders_reports() {
        use crate::ecs::render_mode::RenderMode;

        let mut world = World::new();
        assert!(!renders(&world), "an unresolved world renders nothing");

        world.insert_resource(RenderMode::Headless);
        assert!(!renders(&world));

        world.insert_resource(RenderMode::Rendered);
        assert!(renders(&world));
    }

    // The streaming readouts are `None` until graphics init parks the state, so
    // a world that never built a backend reports nothing rather than panicking.
    #[test]
    fn streaming_readouts_are_absent_before_graphics_init() {
        let world = World::new();
        assert!(streaming_stats(&world).is_none());
        assert!(streaming_pressure(&world).is_none());
    }

    // A world that never built a backend has none to yield, and one that built
    // no AnimationSystem has none to lend.
    #[test]
    fn world_accessors_without_their_systems() {
        let mut world = World::new();
        assert!(take_render_backend(&mut world).is_none());
        assert!(animation_system_mut(&mut world).is_none());
    }
}