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}