From f039d38bfad1d18605a7c8b330c131f11f089ce0 Mon Sep 17 00:00:00 2001 From: supersurviveur Date: Tue, 15 Sep 2026 21:22:39 +0200 Subject: [PATCH] feat: Add FPS counter --- src/app.rs | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/app.rs b/src/app.rs index e6f9652..05301f9 100644 --- a/src/app.rs +++ b/src/app.rs @@ -207,6 +207,14 @@ pub struct FractalApp { /// Time-based animation of colours / Julia c / Phoenix p / zoom. anim: AnimState, + /// Smoothed frames-per-second, recomputed each ~0.5 s window. Only advances + /// while the app is actually repainting (interaction / animation / export); + /// idle frames aren't forced, so a frozen value means "nothing to render". + fps: f32, + /// Frames counted in the current FPS window, and its start time (`i.time`). + fps_frames: u32, + fps_window_start: f64, + /// Reference orbit (`Z_n` as f32 pairs) for the current view. reference: Arc>, /// Bumped whenever `reference` is replaced, so the GPU re-uploads it. @@ -309,6 +317,9 @@ impl FractalApp { controls_open: true, fullscreen: false, anim: AnimState::default(), + fps: 0.0, + fps_frames: 0, + fps_window_start: 0.0, reference: Arc::new(Vec::new()), generation: 0, ref_center_re, @@ -904,6 +915,19 @@ impl FractalApp { self.fullscreen = !self.fullscreen; self.apply_fullscreen(ui.ctx()); } + // FPS readout. Monospace + fixed width so the number + // changing doesn't jitter the button row. + ui.add( + egui::Label::new( + egui::RichText::new(format!("{:>3.0} FPS", self.fps)) + .monospace(), + ) + .selectable(false), + ) + .on_hover_text( + "Frames per second while rendering (interaction, \ + animation, export). Frozen when idle.", + ); }); }); }); @@ -948,6 +972,27 @@ impl FractalApp { } } + /// Recompute the smoothed FPS. Counts frames over a ~0.5 s wall-clock window + /// (using egui's monotonic `i.time`, which works on native and web) and + /// divides once the window closes, so the readout is steady rather than + /// jittering every frame. Only advances when egui repaints — i.e. while the + /// app is doing work — so an idle app shows its last measured rate. + fn update_fps(&mut self, ui: &egui::Ui) { + let now = ui.input(|i| i.time); + // Reset the window if time went backwards or hasn't started yet. + if self.fps_window_start <= 0.0 || now < self.fps_window_start { + self.fps_window_start = now; + self.fps_frames = 0; + } + self.fps_frames += 1; + let elapsed = now - self.fps_window_start; + if elapsed >= 0.5 { + self.fps = (self.fps_frames as f64 / elapsed) as f32; + self.fps_frames = 0; + self.fps_window_start = now; + } + } + /// Advance any enabled animations by the frame's elapsed time, and request a /// repaint while active. Animations render at full resolution/AA (they do not /// trigger the interaction low-res pass). @@ -1418,6 +1463,7 @@ impl FractalApp { impl eframe::App for FractalApp { fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) { self.poll_export(ui.ctx()); + self.update_fps(ui); // Track the real fullscreen state (e.g. the user pressing Esc/F11 or the // browser leaving fullscreen) so the toggle button label stays correct. self.sync_fullscreen(ui.ctx());