mittens_engine/engine/ecs/system/
renderer_stats_system.rs1use 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 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
155fn 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 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 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 world.init_component_tree(text_id, queue);
216
217 (Some(text_id), ids)
218}