diff --git a/examples/with_winit/README.md b/examples/with_winit/README.md index c29fa1d..49527b5 100644 --- a/examples/with_winit/README.md +++ b/examples/with_winit/README.md @@ -18,4 +18,5 @@ $ cargo run -p with_winit --release -- [SVG FILES] - Mouse scroll wheel will zoom. - Arrow keys switch between SVG images in the current set. - Space resets the position and zoom of the image. +- S toggles the frame statistics layer - Escape exits the program. diff --git a/examples/with_winit/src/lib.rs b/examples/with_winit/src/lib.rs index 380d40b..80e7112 100644 --- a/examples/with_winit/src/lib.rs +++ b/examples/with_winit/src/lib.rs @@ -37,6 +37,7 @@ use winit::{ #[cfg(not(any(target_arch = "wasm32", target_os = "android")))] mod hot_reload; mod multi_touch; +mod stats; #[derive(Parser, Debug)] #[command(about, long_about = None, bin_name="cargo run -p with_winit --")] @@ -97,6 +98,8 @@ fn run( let mut fragment = SceneFragment::new(); let mut simple_text = SimpleText::new(); let mut images = ImageCache::new(); + let mut stats = stats::Stats::new(); + let mut stats_toggle = false; let start = Instant::now(); let mut touch_state = multi_touch::TouchState::new(); @@ -142,6 +145,9 @@ fn run( Some(VirtualKeyCode::Space) => { transform = Affine::IDENTITY; } + Some(VirtualKeyCode::S) => { + stats_toggle = !stats_toggle; + } Some(VirtualKeyCode::Escape) => { *control_flow = ControlFlow::Exit; } @@ -253,6 +259,8 @@ fn run( let width = render_state.surface.config.width; let height = render_state.surface.config.height; let device_handle = &render_cx.devices[render_state.surface.dev_id]; + let snapshot = stats.snapshot(); + let _stats_frame = stats.frame_scope(); // Allow looping forever scene_ix = scene_ix.rem_euclid(scenes.scenes.len() as i32); @@ -296,6 +304,9 @@ fn run( transform = transform * Affine::scale(scale_factor); } builder.append(&fragment, Some(transform)); + if stats_toggle { + snapshot.draw_layer(&mut builder, &mut scene_params.text, width as f64); + } let surface_texture = render_state .surface .surface diff --git a/examples/with_winit/src/stats.rs b/examples/with_winit/src/stats.rs new file mode 100644 index 0000000..bf5fb1b --- /dev/null +++ b/examples/with_winit/src/stats.rs @@ -0,0 +1,148 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Also licensed under MIT license, at your choice. + +use scenes::SimpleText; +use std::{collections::VecDeque, time::Instant}; +use vello::{ + kurbo::{Affine, Rect}, + peniko::{Brush, Color, Fill}, + SceneBuilder, +}; + +const SLIDING_WINDOW_SIZE: usize = 100; + +#[derive(Debug)] +pub struct Snapshot { + pub fps: f64, + pub frame_time_ms: f64, + pub frame_time_min_ms: f64, + pub frame_time_max_ms: f64, +} + +impl Snapshot { + pub fn draw_layer(&self, sb: &mut SceneBuilder, text: &mut SimpleText, width: f64) { + let x_offset = width - 450.; + sb.fill( + Fill::NonZero, + Affine::IDENTITY, + &Brush::Solid(Color::rgba8(0, 0, 0, 200)), + None, + &Rect::new(x_offset, 0., width, 115.), + ); + text.add( + sb, + None, + 25., + Some(&Brush::Solid(Color::WHITE)), + Affine::translate((x_offset + 15., 30.)), + &format!("Frame Time: {:.2} ms", self.frame_time_ms), + ); + text.add( + sb, + None, + 25., + Some(&Brush::Solid(Color::WHITE)), + Affine::translate((x_offset + 15., 60.)), + &format!("Frame Time (min): {:.2} ms", self.frame_time_min_ms), + ); + text.add( + sb, + None, + 25., + Some(&Brush::Solid(Color::WHITE)), + Affine::translate((x_offset + 15., 90.)), + &format!("Frame Time (max): {:.2} ms", self.frame_time_max_ms), + ); + text.add( + sb, + None, + 25., + Some(&Brush::Solid(Color::WHITE)), + Affine::translate((x_offset + 300., 30.)), + &format!("FPS: {:.2}", self.fps), + ); + } +} + +pub struct Stats { + count: usize, + sum: u64, + min: u64, + max: u64, + samples: VecDeque, +} + +pub struct FrameScope<'a> { + stats: &'a mut Stats, + start: Instant, +} + +impl<'a> Drop for FrameScope<'a> { + fn drop(&mut self) { + self.stats + .add_sample(self.start.elapsed().as_micros() as u64); + } +} + +impl Stats { + pub fn new() -> Stats { + Stats { + count: 0, + sum: 0, + min: u64::MAX, + max: u64::MIN, + samples: VecDeque::with_capacity(SLIDING_WINDOW_SIZE), + } + } + + pub fn snapshot(&self) -> Snapshot { + let frame_time_ms = (self.sum as f64 / self.count as f64) * 0.001; + let fps = 1000. / frame_time_ms; + Snapshot { + fps, + frame_time_ms, + frame_time_min_ms: self.min as f64 * 0.001, + frame_time_max_ms: self.max as f64 * 0.001, + } + } + + pub fn frame_scope<'a>(&'a mut self) -> FrameScope<'a> { + FrameScope { + stats: self, + start: Instant::now(), + } + } + + fn add_sample(&mut self, micros: u64) { + let oldest = if self.count < SLIDING_WINDOW_SIZE { + self.count += 1; + None + } else { + self.samples.pop_front() + }; + self.sum += micros; + self.samples.push_back(micros); + if let Some(oldest) = oldest { + self.sum -= oldest; + } + if micros < self.min { + self.min = micros; + } + if micros > self.max { + self.max = micros; + } + } +}