1use super::diagnostic_text_renderer::ShapedText;
2use super::DiagnosticTextRenderer;
3use crate::media::immutable::ImmutableSolidColorBrush;
4use crate::media::{BoxShadows, Brushes, Colors, IBrush};
5use crate::platform::IDrawingContextImpl;
6use crate::utilities::ByteSizeHelper;
7use crate::{Matrix, Rect};
8use std::cell::{Cell, RefCell};
9use std::rc::Rc;
10use std::time::Duration;
11
12const FRAME_DIGITS: i32 = 8;
13const FPS_DIGITS: i32 = 3;
14const VISUAL_DIGITS: i32 = 5;
15
16pub struct FpsCounter {
26 start: Duration,
27 text_renderer: Rc<DiagnosticTextRenderer>,
28 frame_label: Rc<ShapedText>,
29 fps_label: Rc<ShapedText>,
30 visited_label: Rc<ShapedText>,
31 rendered_label: Rc<ShapedText>,
32 digits_width: f64,
33 line_height: f64,
34
35 memory_text: RefCell<Option<ShapedText>>,
37 memory: Cell<Option<(u64, u64)>>,
38 text_width: Cell<f64>,
39
40 frames_this_second: Cell<i32>,
41 total_frames: Cell<i32>,
42 fps: Cell<i32>,
43 last_fps_update: Cell<Duration>,
44}
45
46impl FpsCounter {
47 pub const LABELS: [&'static str; 4] = ["Frame #", " FPS: ", " V:", " R:"];
53
54 pub fn new(text_renderer: Rc<DiagnosticTextRenderer>, now: Duration) -> Self {
57 let counter = Self {
58 start: now,
59 frame_label: text_renderer.shape_label(Self::LABELS[0]),
60 fps_label: text_renderer.shape_label(Self::LABELS[1]),
61 visited_label: text_renderer.shape_label(Self::LABELS[2]),
62 rendered_label: text_renderer.shape_label(Self::LABELS[3]),
63 digits_width: (FRAME_DIGITS + FPS_DIGITS + 2 * VISUAL_DIGITS) as f64 * text_renderer.digit_cell_width(),
64 line_height: text_renderer.get_max_height(),
65 text_renderer,
66 memory_text: RefCell::new(None),
67 memory: Cell::new(None),
68 text_width: Cell::new(0.0),
69 frames_this_second: Cell::new(0),
70 total_frames: Cell::new(0),
71 fps: Cell::new(0),
72 last_fps_update: Cell::new(Duration::ZERO),
73 };
74 counter.update_text_width();
75 counter
76 }
77
78 pub fn fps_tick(&self) {
79 self.frames_this_second.set(self.frames_this_second.get().wrapping_add(1));
80 }
81
82 #[allow(clippy::too_many_arguments)]
92 pub fn render_fps(
93 &self,
94 context: &mut dyn IDrawingContextImpl,
95 memory: Option<(u64, u64)>,
96 visited_visuals: i32,
97 rendered_visuals: i32,
98 has_layer: bool,
99 old_rect: Option<Rect>,
100 now: Duration,
101 ) -> Rect {
102 let now = now.saturating_sub(self.start);
103 let elapsed = now.saturating_sub(self.last_fps_update.get());
104
105 self.frames_this_second.set(self.frames_this_second.get().wrapping_add(1));
106 self.total_frames.set(self.total_frames.get().wrapping_add(1));
107
108 if elapsed.as_secs_f64() > 1.0 {
109 self.fps.set((self.frames_this_second.get() as f64 / elapsed.as_secs_f64()) as i32);
110 self.frames_this_second.set(0);
111 self.last_fps_update.set(now);
112 }
113
114 if memory != self.memory.get() {
115 self.memory.set(memory);
116 let memory_text = self.create_memory_text();
117 if let Some(old) = self.memory_text.replace(memory_text) {
118 old.dispose();
119 }
120 self.update_text_width();
121 }
122
123 let mut rect = Rect::new(0.0, 0.0, self.text_width.get() + 3.0, self.line_height + 3.0);
124 if let (true, Some(old_rect)) = (has_layer, old_rect) {
125 rect = rect.union(old_rect);
126 }
127
128 let layer_brush = ImmutableSolidColorBrush::with_opacity(Colors::BLACK, 0.5);
129 let black = Brushes::black();
130 let background: &dyn IBrush = if has_layer { &layer_brush } else { &*black };
131 context.draw_rectangle(Some(background), None, rect.into(), &BoxShadows::default());
132
133 let mut x = 0.0;
134 self.draw_label(context, &*self.frame_label, &mut x);
135 self.draw_number(context, self.total_frames.get(), FRAME_DIGITS, &mut x);
136 self.draw_label(context, &*self.fps_label, &mut x);
137 self.draw_number(context, self.fps.get(), FPS_DIGITS, &mut x);
138 if let Some(memory_text) = &*self.memory_text.borrow() {
139 self.draw_label(context, memory_text, &mut x);
140 }
141 self.draw_label(context, &*self.visited_label, &mut x);
142 self.draw_number(context, visited_visuals, VISUAL_DIGITS, &mut x);
143 self.draw_label(context, &*self.rendered_label, &mut x);
144 self.draw_number(context, rendered_visuals, VISUAL_DIGITS, &mut x);
145
146 rect
147 }
148
149 fn draw_label(&self, context: &mut dyn IDrawingContextImpl, text: &ShapedText, x: &mut f64) {
150 let old_transform = context.transform();
151 context.set_transform(Matrix::create_translation(*x, 0.0) * old_transform);
152 self.text_renderer.draw_shaped_text(context, text, &*Brushes::white());
153 context.set_transform(old_transform);
154
155 *x += text.width();
156 }
157
158 fn draw_number(&self, context: &mut dyn IDrawingContextImpl, value: i32, digit_count: i32, x: &mut f64) {
159 let old_transform = context.transform();
160 context.set_transform(Matrix::create_translation(*x, 0.0) * old_transform);
161 self.text_renderer.draw_digits(context, value, digit_count, &*Brushes::white());
162 context.set_transform(old_transform);
163
164 *x += digit_count as f64 * self.text_renderer.digit_cell_width();
165 }
166
167 fn create_memory_text(&self) -> Option<ShapedText> {
168 let (managed_bytes, native_bytes) = self.memory.get()?;
169 Some(self.text_renderer.shape(&format!(
170 " M:{} / N:{}",
171 ByteSizeHelper::to_string(managed_bytes, false),
172 ByteSizeHelper::to_string(native_bytes, false)
173 )))
174 }
175
176 fn update_text_width(&self) {
177 let memory_width = self.memory_text.borrow().as_ref().map_or(0.0, ShapedText::width);
178 self.text_width.set(
179 self.frame_label.width()
180 + self.fps_label.width()
181 + self.visited_label.width()
182 + self.rendered_label.width()
183 + memory_width
184 + self.digits_width,
185 );
186 }
187
188 pub fn reset(&self) {
189 self.frames_this_second.set(0);
190 self.total_frames.set(0);
191 self.fps.set(0);
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::super::diagnostic_text_renderer::tests::create_renderer;
198 use super::super::dirty_rects::test_mocks::MockDrawingContext;
199 use super::*;
200
201 fn ms(value: u64) -> Duration {
202 Duration::from_millis(value)
203 }
204
205 fn drawn_text(ctx: &MockDrawingContext) -> String {
208 ctx.log
209 .iter()
210 .filter_map(|l| {
211 if let Some(glyph) = l.strip_prefix("glyph ") {
212 let code = glyph.split(' ').next().unwrap().parse::<u32>().unwrap();
213 Some(char::from_u32(code).unwrap().to_string())
214 } else {
215 let text = l.strip_prefix("text ")?;
216 let at = text.rfind(" at ").unwrap();
217 Some(text[..at].rsplit_once(' ').unwrap().0.to_string())
218 }
219 })
220 .collect()
221 }
222
223 fn render(counter: &FpsCounter, visited: i32, rendered: i32, now: Duration) -> String {
224 let mut ctx = MockDrawingContext::default();
225 counter.render_fps(&mut ctx, None, visited, rendered, false, None, now);
226 drawn_text(&ctx)
227 }
228
229 #[test]
230 fn counts_frames_and_updates_fps_once_more_than_a_second_elapsed() {
231 let start = ms(5000);
233 let counter = FpsCounter::new(Rc::new(create_renderer()), start);
234
235 assert_eq!("Frame #00000001 FPS: 000 V:00003 R:00002", render(&counter, 3, 2, start + ms(100)));
236 assert_eq!("Frame #00000002 FPS: 000 V:00000 R:00000", render(&counter, 0, 0, start + ms(500)));
237 assert_eq!("Frame #00000003 FPS: 000 V:00000 R:00000", render(&counter, 0, 0, start + ms(1000)));
239 assert_eq!("Frame #00000004 FPS: 002 V:00000 R:00000", render(&counter, 0, 0, start + ms(2000)));
241 assert_eq!("Frame #00000005 FPS: 002 V:00000 R:00000", render(&counter, 0, 0, start + ms(2500)));
243 assert_eq!("Frame #00000006 FPS: 001 V:00000 R:00000", render(&counter, 0, 0, start + ms(3250)));
245 }
246
247 #[test]
248 fn fps_tick_counts_extra_frames() {
249 let counter = FpsCounter::new(Rc::new(create_renderer()), Duration::ZERO);
250 for _ in 0..119 {
251 counter.fps_tick();
252 }
253 assert_eq!("Frame #00000001 FPS: 060 V:00000 R:00000", render(&counter, 0, 0, ms(2000)));
254 }
255
256 #[test]
257 fn reset_clears_counters_but_not_the_update_time() {
258 let counter = FpsCounter::new(Rc::new(create_renderer()), Duration::ZERO);
259 render(&counter, 0, 0, ms(1500));
260 render(&counter, 0, 0, ms(1600));
261 counter.reset();
262 assert_eq!("Frame #00000001 FPS: 000 V:00000 R:00000", render(&counter, 0, 0, ms(1700)));
263 assert_eq!("Frame #00000002 FPS: 001 V:00000 R:00000", render(&counter, 0, 0, ms(3000)));
265 }
266
267 #[test]
268 fn numbers_are_clamped_to_their_digits() {
269 let counter = FpsCounter::new(Rc::new(create_renderer()), Duration::ZERO);
270 assert_eq!("Frame #00000001 FPS: 000 V:99999 R:00000", render(&counter, 123456, -1, ms(10)));
271 }
272
273 #[test]
274 fn the_memory_text_follows_the_frames_per_second() {
275 let counter = FpsCounter::new(Rc::new(create_renderer()), Duration::ZERO);
276 let mut ctx = MockDrawingContext::default();
277 counter.render_fps(&mut ctx, Some((1024, 2048)), 1, 2, false, None, ms(10));
278 let expected = format!(
279 "Frame #00000001 FPS: 000 M:{} / N:{} V:00001 R:00002",
280 ByteSizeHelper::to_string(1024, false),
281 ByteSizeHelper::to_string(2048, false)
282 );
283 assert_eq!(expected, drawn_text(&ctx));
284 }
285
286 #[test]
287 fn draws_background_then_text_and_returns_the_rect() {
288 let counter = FpsCounter::new(Rc::new(create_renderer()), Duration::ZERO);
289 let mut ctx = MockDrawingContext::default();
290 let rect = counter.render_fps(&mut ctx, None, 0, 0, false, Some(Rect::new(0.0, 0.0, 500.0, 500.0)), ms(10));
293 assert_eq!(Rect::new(0.0, 0.0, 243.0, 17.0), rect);
294 assert_eq!("rect #ff000000@1 0, 0, 243, 17", ctx.log[0]);
295 assert_eq!(26, ctx.log.len());
297 assert_eq!("text Frame # #ffffffff@1 at 0,0", ctx.log[1]);
298 assert_eq!("glyph 48 #ffffffff@1 at 42,0", ctx.log[2]);
299 }
300
301 #[test]
302 fn with_a_layer_the_old_rect_is_covered_with_a_translucent_background() {
303 let counter = FpsCounter::new(Rc::new(create_renderer()), Duration::ZERO);
304 let mut ctx = MockDrawingContext::default();
305 let rect = counter.render_fps(&mut ctx, None, 0, 0, true, Some(Rect::new(0.0, 0.0, 300.0, 10.0)), ms(10));
306 assert_eq!(Rect::new(0.0, 0.0, 300.0, 17.0), rect);
307 assert_eq!("rect #ff000000@0.5 0, 0, 300, 17", ctx.log[0]);
308
309 let mut ctx = MockDrawingContext::default();
310 let rect = counter.render_fps(&mut ctx, None, 0, 0, true, None, ms(20));
311 assert_eq!(Rect::new(0.0, 0.0, 243.0, 17.0), rect);
312 }
313}