Skip to main content

mittens_engine/engine/ecs/system/
renderer_stats_system.rs

1use crate::engine::ecs::CommandQueue;
2use crate::engine::ecs::ComponentId;
3use crate::engine::ecs::SignalEmitter;
4use crate::engine::ecs::World;
5use crate::engine::ecs::component::{
6    ColorComponent, EmissiveComponent, RendererStatsComponent, TextComponent,
7};
8use crate::engine::graphics::{CameraTarget, VisualWorld};
9
10#[derive(Debug, Default)]
11pub struct RendererStatsSystem;
12
13impl RendererStatsSystem {
14    pub fn tick_with_queue(
15        &mut self,
16        world: &mut World,
17        visuals: &mut VisualWorld,
18        queue: &mut CommandQueue,
19        dt_sec: f32,
20    ) {
21        let stats_ids: Vec<ComponentId> = world
22            .all_components()
23            .filter(|&cid| {
24                world
25                    .get_component_by_id_as::<RendererStatsComponent>(cid)
26                    .is_some()
27            })
28            .collect();
29
30        for stats_id in stats_ids {
31            let (should_update, config, subtree_ids) = {
32                let Some(stats) =
33                    world.get_component_by_id_as_mut::<RendererStatsComponent>(stats_id)
34                else {
35                    continue;
36                };
37                if !stats.enabled {
38                    continue;
39                }
40                stats.accumulate_time(dt_sec);
41                (
42                    stats.should_update(),
43                    StatsConfig::from_component(stats),
44                    StatsSubtreeIds::from_component(stats),
45                )
46            };
47            if !should_update {
48                continue;
49            }
50
51            let (fps, dt_ms, label) = match config.target {
52                CameraTarget::Window => {
53                    let dt = visuals.window_frame_dt_sec();
54                    (visuals.window_frame_fps(), dt * 1000.0, "Window")
55                }
56                CameraTarget::Xr => {
57                    let dt = visuals.xr_frame_dt_sec().unwrap_or(0.0);
58                    let fps = visuals.xr_frame_fps().unwrap_or(0.0);
59                    (fps, dt * 1000.0, "XR")
60                }
61            };
62
63            let smoothed = {
64                let Some(stats) =
65                    world.get_component_by_id_as_mut::<RendererStatsComponent>(stats_id)
66                else {
67                    continue;
68                };
69                stats.smooth_fps(fps)
70            };
71
72            let new_text = if fps > 0.0 && dt_ms > 0.0 {
73                format!("{label}: {smoothed:.1} fps  ({dt_ms:.1} ms)")
74            } else {
75                format!("{label}: (no timing)")
76            };
77
78            let (text_id, updated_ids) =
79                ensure_stats_text_subtree(world, queue, stats_id, config, subtree_ids);
80
81            {
82                if let Some(stats) =
83                    world.get_component_by_id_as_mut::<RendererStatsComponent>(stats_id)
84                {
85                    updated_ids.write_back(stats);
86                    stats.reset_update_timer();
87                }
88            }
89
90            let Some(text_id) = text_id else {
91                continue;
92            };
93
94            // Avoid spamming SetText if it wouldn't change.
95            let needs_update = world
96                .get_component_by_id_as::<TextComponent>(text_id)
97                .map(|t| t.text != new_text)
98                .unwrap_or(true);
99
100            if needs_update {
101                queue.push_intent_now(
102                    stats_id,
103                    crate::engine::ecs::IntentValue::SetText {
104                        component_ids: vec![text_id],
105                        text: new_text,
106                    },
107                );
108            }
109        }
110    }
111}
112
113#[derive(Debug, Clone, Copy)]
114struct StatsConfig {
115    target: CameraTarget,
116    color: [f32; 4],
117    emissive: bool,
118}
119
120impl StatsConfig {
121    fn from_component(c: &RendererStatsComponent) -> Self {
122        Self {
123            target: c.target,
124            color: c.color,
125            emissive: c.emissive,
126        }
127    }
128}
129
130#[derive(Debug, Default, Clone, Copy)]
131struct StatsSubtreeIds {
132    text: Option<ComponentId>,
133    text_color: Option<ComponentId>,
134    text_emissive: Option<ComponentId>,
135}
136
137impl StatsSubtreeIds {
138    fn from_component(c: &mut RendererStatsComponent) -> Self {
139        let (t, c_id, e_id) = c.runtime_subtree_ids_mut();
140        Self {
141            text: *t,
142            text_color: *c_id,
143            text_emissive: *e_id,
144        }
145    }
146
147    fn write_back(self, c: &mut RendererStatsComponent) {
148        let (t, c_id, e_id) = c.runtime_subtree_ids_mut();
149        *t = self.text;
150        *c_id = self.text_color;
151        *e_id = self.text_emissive;
152    }
153}
154
155// Camera target selection is explicit via RendererStatsComponent.target.
156
157fn ensure_stats_text_subtree(
158    world: &mut World,
159    queue: &mut CommandQueue,
160    stats_id: ComponentId,
161    config: StatsConfig,
162    mut ids: StatsSubtreeIds,
163) -> (Option<ComponentId>, StatsSubtreeIds) {
164    // Text.
165    let text_id = match ids.text {
166        Some(cid) if world.get_component_by_id_as::<TextComponent>(cid).is_some() => cid,
167        _ => {
168            let cid = world.add_component(TextComponent::new(""));
169            let _ = world.add_child(stats_id, cid);
170            ids.text = Some(cid);
171            cid
172        }
173    };
174
175    // Styling: immediate children of TextComponent.
176    let has_color = match ids.text_color {
177        Some(cid) => world
178            .get_component_by_id_as::<ColorComponent>(cid)
179            .is_some(),
180        None => false,
181    };
182    if !has_color {
183        let cid = world.add_component(ColorComponent { rgba: config.color });
184        let _ = world.add_child(text_id, cid);
185        ids.text_color = Some(cid);
186    }
187    if let Some(cid) = ids.text_color {
188        if let Some(c) = world.get_component_by_id_as_mut::<ColorComponent>(cid) {
189            c.rgba = config.color;
190        }
191    }
192
193    let has_emissive = match ids.text_emissive {
194        Some(cid) => world
195            .get_component_by_id_as::<EmissiveComponent>(cid)
196            .is_some(),
197        None => false,
198    };
199    if !has_emissive {
200        let cid = world.add_component(EmissiveComponent::new(if config.emissive {
201            1.0
202        } else {
203            0.0
204        }));
205        let _ = world.add_child(text_id, cid);
206        ids.text_emissive = Some(cid);
207    }
208    if let Some(cid) = ids.text_emissive {
209        if let Some(e) = world.get_component_by_id_as_mut::<EmissiveComponent>(cid) {
210            e.intensity = if config.emissive { 1.0 } else { 0.0 };
211        }
212    }
213
214    // Initialize the whole subtree so the renderer/text system sees it.
215    world.init_component_tree(text_id, queue);
216
217    (Some(text_id), ids)
218}