Skip to main content

concinnity_engine/ecs/
world_queries.rs

1// src/ecs/world_queries.rs
2//
3// Queries over a world that only a renderer-bearing runtime can answer. The
4// world itself is concinnity-core's and names no backend, no GPU profile, and
5// no streaming pool; each of these reads one of the resources this crate's
6// render band parks there, or the systems it built.
7
8use crate::app::budget::{MemoryBudget, ThreadBudget};
9use crate::app::mem_drift::MemoryDrift;
10use crate::ecs::{ActiveRenderBackend, World};
11use crate::gfx::backend::{GpuProfile, RenderBackend};
12use crate::gfx::streaming_system::{StreamingPressure, StreamingState, StreamingStats};
13
14/// Whether the world needs a renderer. True when it declares a
15/// `GraphicsConfig` (pre-`start`) or has a constructed `GraphicsSystem`
16/// (post-`start`, after the config component has been drained), so callers can
17/// decide on the render loop regardless of timing.
18pub fn renders(world: &World) -> bool {
19    world
20        .query::<crate::components::GraphicsConfig>()
21        .next()
22        .is_some()
23        || world.systems().iter().any(|s| {
24            s.downcast_ref::<crate::gfx::graphics_system::GraphicsSystem>()
25                .is_some()
26        })
27}
28
29/// Per-pool `(resident, pending, unloaded)` streaming counts from the parked
30/// `StreamingState` (StreamingSystem drives it against the backend each
31/// frame). `None` before graphics init parks it, and from inside a system
32/// step, which takes the state out. Read by the `cn debug` server's
33/// `streaming` command and the editor's Health panel.
34pub fn streaming_stats(world: &World) -> Option<StreamingStats> {
35    world
36        .resource::<StreamingState>()
37        .map(|s| s.streaming_stats())
38}
39
40/// Live process-RAM back-off pressure on streaming, published by
41/// StreamingSystem on its throttled RSS sample. `None` before the first sample
42/// or when no `MemoryBudget` / RSS is available (the valve is inert).
43pub fn streaming_pressure(world: &World) -> Option<StreamingPressure> {
44    world.resource::<StreamingPressure>().copied()
45}
46
47/// Long-session memory drift, folded from the same throttled sample as the
48/// back-off valve. `None` until the session settles enough for a baseline, and
49/// for the same reasons `streaming_pressure` is absent.
50pub fn memory_drift(world: &World) -> Option<MemoryDrift> {
51    world.resource::<MemoryDrift>().copied()
52}
53
54/// The detected GPU's capability + memory profile, published by graphics init.
55/// `None` before init runs, and `GpuProfile::UNKNOWN` when the backend could
56/// not classify the device.
57pub fn gpu_profile(world: &World) -> Option<GpuProfile> {
58    world.resource::<GpuProfile>().copied()
59}
60
61/// The process thread budget App published at start. `None` before `App::start`
62/// installs it. Read by the `cn debug` server's `budget` command.
63pub fn thread_budget(world: &World) -> Option<ThreadBudget> {
64    world.resource::<ThreadBudget>().copied()
65}
66
67/// The world's memory budget, once `start` has published one.
68pub fn memory_budget(world: &World) -> Option<MemoryBudget> {
69    world.resource::<MemoryBudget>().copied()
70}
71
72/// Take the live render backend out of the world's parked slot, leaving the
73/// world backend-less. The `cn editor` live SAVE swap transplants it into the
74/// rebuilt world (via a `PendingBackend` resource) so the edit applies without
75/// recreating the OS window / re-initialising the GPU device. `None` when the
76/// world never built a backend (or it was already yielded).
77pub fn take_render_backend(world: &mut World) -> Option<Box<dyn RenderBackend>> {
78    world
79        .resource_mut::<ActiveRenderBackend>()
80        .and_then(|slot| slot.0.take())
81}
82
83/// Disjoint mutable borrows of the system list and the parked render backend,
84/// for the `cn debug` hot-reload drive: it applies backend edits through a
85/// system's init-captured bookkeeping, so it needs both at once. The backend is
86/// `None` while a step has it taken (never the case between ticks, where the
87/// drive runs) or when no backend was built.
88pub fn systems_and_render_backend(
89    world: &mut World,
90) -> (
91    &mut [crate::ecs::BuiltSystem],
92    Option<&mut (dyn RenderBackend + 'static)>,
93) {
94    let (systems, resources) = world.systems_and_resources();
95    let backend = resources
96        .get_mut::<ActiveRenderBackend>()
97        .and_then(|slot| slot.0.as_deref_mut());
98    (systems, backend)
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    // A GraphicsConfig marks a rendering world. `renders` reports it before
106    // `start` (while the component is present), the pre-start signal callers
107    // use to choose the render loop. (The post-start GraphicsSystem path can't
108    // be unit-tested here: its `init` builds the GPU backend.)
109    #[test]
110    fn graphics_config_makes_world_render() {
111        let mut world = World::new();
112        assert!(!renders(&world));
113        world.add_component(crate::components::GraphicsConfig::default());
114        assert!(renders(&world));
115    }
116
117    // The streaming readouts are `None` until graphics init parks the state, so
118    // a world that never built a backend reports nothing rather than panicking.
119    #[test]
120    fn streaming_readouts_are_absent_before_graphics_init() {
121        let world = World::new();
122        assert!(streaming_stats(&world).is_none());
123        assert!(streaming_pressure(&world).is_none());
124    }
125
126    // A world that never built a backend has none to yield, and the disjoint
127    // borrow still hands back the (empty) system list.
128    #[test]
129    fn render_backend_accessors_without_a_backend() {
130        let mut world = World::new();
131        assert!(take_render_backend(&mut world).is_none());
132
133        let (systems, backend) = systems_and_render_backend(&mut world);
134        assert!(systems.is_empty());
135        assert!(backend.is_none());
136    }
137}