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