Viewport frame-rate readout (Preferences ▸ Graphics) (#1580)
Thanks @sthifen.
This commit is contained in:
1 parent
904bf19e83
commit
e21c5f06ac
11 files changed
+125
-1
No files matched your search
@@ -0,0 +1,96 @@
|
||||
//! The viewport's frame-rate readout (Preferences ▸ Graphics ▸ "Frame rate").
|
||||
|
||||
use egui::{FontId, Rect, Stroke, pos2, vec2};
|
||||
|
||||
use crate::SolveApp;
|
||||
use crate::theme::Tokens;
|
||||
|
||||
/// Weight of the newest frame in the running mean (roughly the last 20 frames count).
|
||||
const NEWEST: f64 = 0.05;
|
||||
|
||||
/// Frames per second, averaged over recent frames.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct FrameRate {
|
||||
mean_dt: f64,
|
||||
}
|
||||
|
||||
impl FrameRate {
|
||||
/// Takes the time since the previous frame, in seconds. A gap over a second (the app idle
|
||||
/// before the readout was turned on, a window being moved) is not a frame of a running view
|
||||
/// and is left out, as is anything that isn't a positive finite time.
|
||||
pub fn record(&mut self, dt: f64) {
|
||||
if !dt.is_finite() || dt <= 0.0 || dt > 1.0 {
|
||||
return;
|
||||
}
|
||||
self.mean_dt = if self.mean_dt > 0.0 { self.mean_dt + NEWEST * (dt - self.mean_dt) } else { dt };
|
||||
}
|
||||
|
||||
/// The averaged rate; `None` before the first frame was recorded.
|
||||
pub fn fps(&self) -> Option<f64> {
|
||||
(self.mean_dt > 0.0).then(|| 1.0 / self.mean_dt)
|
||||
}
|
||||
|
||||
/// The readout's text: rate and the matching time per frame.
|
||||
pub fn label(&self) -> Option<String> {
|
||||
self.fps().map(|fps| format!("{fps:.0} FPS · {:.1} ms", 1000.0 * self.mean_dt))
|
||||
}
|
||||
}
|
||||
|
||||
/// Records this frame and paints the readout in the view's bottom-right corner. While it is on
|
||||
/// the view redraws every frame, so the rate is what the view can draw rather than how often
|
||||
/// something happened to change.
|
||||
pub(crate) fn paint(app: &mut SolveApp, ctx: &egui::Context, painter: &egui::Painter, rect: Rect) {
|
||||
if !app.preferences.show_frame_rate {
|
||||
app.frame_rate = FrameRate::default();
|
||||
return;
|
||||
}
|
||||
app.frame_rate.record(f64::from(ctx.input(|i| i.unstable_dt)));
|
||||
ctx.request_repaint();
|
||||
let Some(text) = app.frame_rate.label() else { return };
|
||||
let t = Tokens::get();
|
||||
let font = FontId::monospace(12.0);
|
||||
let galley = painter.layout_no_wrap(text, font, t.text);
|
||||
let size = galley.size() + vec2(16.0, 8.0);
|
||||
let r = Rect::from_min_size(pos2(rect.right() - 12.0 - size.x, rect.bottom() - 12.0 - size.y), size);
|
||||
painter.rect(r, 4.0, t.overlay, Stroke::new(1.0, t.border), egui::StrokeKind::Inside);
|
||||
crate::scenario::publish_handle("viewport:frame_rate", r.center());
|
||||
painter.galley(r.center() - galley.size() / 2.0, galley, t.text);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::FrameRate;
|
||||
|
||||
#[test]
|
||||
fn steady_frames_read_their_rate() {
|
||||
let mut f = FrameRate::default();
|
||||
assert_eq!(f.fps(), None);
|
||||
for _ in 0..200 {
|
||||
f.record(1.0 / 60.0);
|
||||
}
|
||||
assert!((f.fps().unwrap_or(0.0) - 60.0).abs() < 1e-6);
|
||||
assert_eq!(f.label().as_deref(), Some("60 FPS · 16.7 ms"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pauses_and_hostile_times_are_left_out() {
|
||||
let mut f = FrameRate::default();
|
||||
for dt in [0.0, -1.0, f64::NAN, f64::INFINITY, 5.0] {
|
||||
f.record(dt);
|
||||
}
|
||||
assert_eq!(f.fps(), None);
|
||||
f.record(0.01);
|
||||
f.record(3.0);
|
||||
assert!((f.fps().unwrap_or(0.0) - 100.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_mean_follows_a_slower_view() {
|
||||
let mut f = FrameRate::default();
|
||||
f.record(1.0 / 120.0);
|
||||
for _ in 0..300 {
|
||||
f.record(1.0 / 30.0);
|
||||
}
|
||||
assert!((f.fps().unwrap_or(0.0) - 30.0).abs() < 0.1);
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ pub mod drag_snap;
|
||||
pub mod edit_clipboard;
|
||||
pub mod fonts;
|
||||
pub mod frame;
|
||||
pub mod frame_rate;
|
||||
pub mod gizmo;
|
||||
pub mod gpu;
|
||||
pub mod help;
|
||||
@@ -353,6 +354,8 @@ pub struct SolveApp {
|
||||
/// [`SolveApp::set_wgpu`].
|
||||
pub cpu_meshing: bool,
|
||||
pub frame_ms: f64,
|
||||
/// The viewport's frame-rate readout ([`frame_rate`]).
|
||||
pub frame_rate: frame_rate::FrameRate,
|
||||
pub synthetic: Vec<egui::Event>,
|
||||
/// The modifiers of the synthetic press in progress (a Ctrl-drag keeps Ctrl while it moves).
|
||||
pub synthetic_mods: egui::Modifiers,
|
||||
@@ -446,6 +449,7 @@ impl SolveApp {
|
||||
integrated_titlebar: false,
|
||||
cpu_meshing: false,
|
||||
frame_ms: 0.0,
|
||||
frame_rate: frame_rate::FrameRate::default(),
|
||||
synthetic: Vec::new(),
|
||||
synthetic_mods: egui::Modifiers::default(),
|
||||
control_rx: None,
|
||||
|
||||
@@ -283,6 +283,8 @@ pub struct Prefs {
|
||||
pub supersampling: f32,
|
||||
/// Supersample only while the view holds still (full speed while orbiting).
|
||||
pub supersampling_adaptive: bool,
|
||||
/// Show the frame rate in the view's corner (the view then redraws every frame).
|
||||
pub show_frame_rate: bool,
|
||||
/// Show world X/Y/Z component dimensions beside the total distance in Measure.
|
||||
pub measure_axis_distances: bool,
|
||||
/// Decimals shown for lengths and angles.
|
||||
@@ -400,6 +402,7 @@ impl Default for Prefs {
|
||||
display_quality: "normal".into(),
|
||||
supersampling: 1.0,
|
||||
supersampling_adaptive: true,
|
||||
show_frame_rate: false,
|
||||
measure_axis_distances: true,
|
||||
length_decimals: 2,
|
||||
angle_decimals: 1,
|
||||
@@ -624,6 +627,7 @@ fn defaults(app: &mut SolveApp, page: usize) {
|
||||
p.display_quality = d.display_quality;
|
||||
p.supersampling = d.supersampling;
|
||||
p.supersampling_adaptive = d.supersampling_adaptive;
|
||||
p.show_frame_rate = d.show_frame_rate;
|
||||
p.measure_axis_distances = d.measure_axis_distances;
|
||||
p.color_vision = d.color_vision;
|
||||
app.ui.visual_style = 0;
|
||||
@@ -1310,6 +1314,13 @@ fn page(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) {
|
||||
colour_vision_sample(ui, t);
|
||||
ui.end_row();
|
||||
note(ui, app.tr("ui.colour_vision_note"));
|
||||
// Last, so the colour vision section above keeps its place on the page.
|
||||
ui.label("");
|
||||
ui.end_row();
|
||||
ui.label(app.tr("ui.frame_rate"));
|
||||
ui.checkbox(&mut app.preferences.show_frame_rate, "");
|
||||
ui.end_row();
|
||||
note(ui, app.tr("settings.frame_rate_note"));
|
||||
}
|
||||
5 => {
|
||||
ui.label(crate::i18n::tr_label("Project face edges into new sketches"));
|
||||
|
||||
@@ -2237,6 +2237,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
painter.add(egui_wgpu::Callback::new_paint_callback(rect, cb));
|
||||
}
|
||||
analysis_legend(app, &painter, rect);
|
||||
crate::frame_rate::paint(app, ui.ctx(), &painter, rect);
|
||||
|
||||
// ---- interaction ----
|
||||
let add = mods.shift || mods.command || mods.ctrl;
|
||||
|
||||
Reference in new issue
Block a user