From 89fb1b89daeff0f67f0058895b9e285462263af3 Mon Sep 17 00:00:00 2001 From: Arman Uguray Date: Sat, 4 Mar 2023 01:49:09 -0800 Subject: [PATCH 01/13] [frame_stats] Add frame statistics UI to with_winit example Added a module for frame time statistics and UI layer that displays the average, minimum, and maximum frame time alongside FPS. The UI can be toggled by pressing the `S` key. --- examples/with_winit/README.md | 1 + examples/with_winit/src/lib.rs | 11 +++ examples/with_winit/src/stats.rs | 148 +++++++++++++++++++++++++++++++ 3 files changed, 160 insertions(+) create mode 100644 examples/with_winit/src/stats.rs 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; + } + } +} From d7bacbcc3f5a3de4d6042dbb30e56ad3972b4b69 Mon Sep 17 00:00:00 2001 From: Arman Uguray Date: Sun, 5 Mar 2023 11:26:58 -0800 Subject: [PATCH 02/13] [frame_stats] Build stats layer layout based on viewport dimensions --- examples/with_winit/src/lib.rs | 7 +++++- examples/with_winit/src/stats.rs | 37 +++++++++++++++++++++----------- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/examples/with_winit/src/lib.rs b/examples/with_winit/src/lib.rs index 80e7112..e0281e2 100644 --- a/examples/with_winit/src/lib.rs +++ b/examples/with_winit/src/lib.rs @@ -305,7 +305,12 @@ fn run( } builder.append(&fragment, Some(transform)); if stats_toggle { - snapshot.draw_layer(&mut builder, &mut scene_params.text, width as f64); + snapshot.draw_layer( + &mut builder, + &mut scene_params.text, + width as f64, + height as f64, + ); } let surface_texture = render_state .surface diff --git a/examples/with_winit/src/stats.rs b/examples/with_winit/src/stats.rs index bf5fb1b..9a3a1c9 100644 --- a/examples/with_winit/src/stats.rs +++ b/examples/with_winit/src/stats.rs @@ -33,45 +33,58 @@ pub struct Snapshot { } impl Snapshot { - pub fn draw_layer(&self, sb: &mut SceneBuilder, text: &mut SimpleText, width: f64) { - let x_offset = width - 450.; + pub fn draw_layer( + &self, + sb: &mut SceneBuilder, + text: &mut SimpleText, + viewport_width: f64, + viewport_height: f64, + ) { + let width = (viewport_width * 0.4).max(200.).min(400.); + let height = width * 0.3; + let x_offset = viewport_width - width; + let y_offset = viewport_height - height; + let offset = Affine::translate((x_offset, y_offset)); + let text_height = height * 0.2; + let left_margin = width * 0.03; + let text_size = (text_height * 0.9) as f32; sb.fill( Fill::NonZero, - Affine::IDENTITY, + offset, &Brush::Solid(Color::rgba8(0, 0, 0, 200)), None, - &Rect::new(x_offset, 0., width, 115.), + &Rect::new(0., 0., width, height), ); text.add( sb, None, - 25., + text_size, Some(&Brush::Solid(Color::WHITE)), - Affine::translate((x_offset + 15., 30.)), + offset * Affine::translate((left_margin, text_height)), &format!("Frame Time: {:.2} ms", self.frame_time_ms), ); text.add( sb, None, - 25., + text_size, Some(&Brush::Solid(Color::WHITE)), - Affine::translate((x_offset + 15., 60.)), + offset * Affine::translate((left_margin, 2. * text_height)), &format!("Frame Time (min): {:.2} ms", self.frame_time_min_ms), ); text.add( sb, None, - 25., + text_size, Some(&Brush::Solid(Color::WHITE)), - Affine::translate((x_offset + 15., 90.)), + offset * Affine::translate((left_margin, 3. * text_height)), &format!("Frame Time (max): {:.2} ms", self.frame_time_max_ms), ); text.add( sb, None, - 25., + text_size, Some(&Brush::Solid(Color::WHITE)), - Affine::translate((x_offset + 300., 30.)), + offset * Affine::translate((width * 0.67, text_height)), &format!("FPS: {:.2}", self.fps), ); } From bacaeebcb6281b7718ca88d56ee0dbe1f00fe136 Mon Sep 17 00:00:00 2001 From: Arman Uguray Date: Sun, 5 Mar 2023 14:39:02 -0800 Subject: [PATCH 03/13] [frame_stats] Show viewport resolution on the stats layer Also set the stats layer toggle to be on by default until we add some UI to toggle it on mobile. --- examples/with_winit/src/lib.rs | 2 +- examples/with_winit/src/stats.rs | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/examples/with_winit/src/lib.rs b/examples/with_winit/src/lib.rs index e0281e2..76a1100 100644 --- a/examples/with_winit/src/lib.rs +++ b/examples/with_winit/src/lib.rs @@ -99,7 +99,7 @@ fn run( let mut simple_text = SimpleText::new(); let mut images = ImageCache::new(); let mut stats = stats::Stats::new(); - let mut stats_toggle = false; + let mut stats_toggle = true; let start = Instant::now(); let mut touch_state = multi_touch::TouchState::new(); diff --git a/examples/with_winit/src/stats.rs b/examples/with_winit/src/stats.rs index 9a3a1c9..344fc9e 100644 --- a/examples/with_winit/src/stats.rs +++ b/examples/with_winit/src/stats.rs @@ -79,6 +79,14 @@ impl Snapshot { offset * Affine::translate((left_margin, 3. * text_height)), &format!("Frame Time (max): {:.2} ms", self.frame_time_max_ms), ); + text.add( + sb, + None, + text_size, + Some(&Brush::Solid(Color::WHITE)), + offset * Affine::translate((left_margin, 4. * text_height)), + &format!("Resolution: {viewport_width}x{viewport_height}"), + ); text.add( sb, None, From 1ac4a4f1a88ef946187877e87264757d5f2be78e Mon Sep 17 00:00:00 2001 From: Arman Uguray Date: Thu, 16 Mar 2023 00:15:47 -0700 Subject: [PATCH 04/13] [frame_stats] Draw a live plot of frame time samples Also abandoned the FrameScope idea and revised the `Stats::add_sample` to accept a struct to accept a variety of future measurements. --- examples/with_winit/src/lib.rs | 7 ++- examples/with_winit/src/stats.rs | 78 ++++++++++++++++++++------------ 2 files changed, 56 insertions(+), 29 deletions(-) diff --git a/examples/with_winit/src/lib.rs b/examples/with_winit/src/lib.rs index 76a1100..7943d6f 100644 --- a/examples/with_winit/src/lib.rs +++ b/examples/with_winit/src/lib.rs @@ -260,7 +260,7 @@ fn run( 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(); + let frame_start_time = Instant::now(); // Allow looping forever scene_ix = scene_ix.rem_euclid(scenes.scenes.len() as i32); @@ -310,6 +310,7 @@ fn run( &mut scene_params.text, width as f64, height as f64, + stats.samples(), ); } let surface_texture = render_state @@ -350,6 +351,10 @@ fn run( .expect("failed to render to surface"); surface_texture.present(); device_handle.device.poll(wgpu::Maintain::Poll); + + stats.add_sample(stats::Sample { + frame_time_us: frame_start_time.elapsed().as_micros() as u64, + }); } Event::UserEvent(event) => match event { #[cfg(not(any(target_arch = "wasm32", target_os = "android")))] diff --git a/examples/with_winit/src/stats.rs b/examples/with_winit/src/stats.rs index 344fc9e..e0b0b28 100644 --- a/examples/with_winit/src/stats.rs +++ b/examples/with_winit/src/stats.rs @@ -15,9 +15,9 @@ // Also licensed under MIT license, at your choice. use scenes::SimpleText; -use std::{collections::VecDeque, time::Instant}; +use std::collections::VecDeque; use vello::{ - kurbo::{Affine, Rect}, + kurbo::{Affine, PathEl, Rect}, peniko::{Brush, Color, Fill}, SceneBuilder, }; @@ -33,20 +33,23 @@ pub struct Snapshot { } impl Snapshot { - pub fn draw_layer( + pub fn draw_layer<'a, T>( &self, sb: &mut SceneBuilder, text: &mut SimpleText, viewport_width: f64, viewport_height: f64, - ) { - let width = (viewport_width * 0.4).max(200.).min(400.); - let height = width * 0.3; + samples: T, + ) where + T: Iterator, + { + let width = (viewport_width * 0.4).max(200.).min(600.); + let height = width * 0.6; let x_offset = viewport_width - width; let y_offset = viewport_height - height; let offset = Affine::translate((x_offset, y_offset)); - let text_height = height * 0.2; - let left_margin = width * 0.03; + let text_height = height * 0.1; + let left_margin = width * 0.01; let text_size = (text_height * 0.9) as f32; sb.fill( Fill::NonZero, @@ -95,9 +98,42 @@ impl Snapshot { offset * Affine::translate((width * 0.67, text_height)), &format!("FPS: {:.2}", self.fps), ); + + // Plot the samples with a bar graph + use PathEl::*; + let graph_max_height = height * 0.5; + let graph_max_width = width - 2. * left_margin; + let bar_extent = graph_max_width / (SLIDING_WINDOW_SIZE as f64); + let bar_width = bar_extent * 0.3; + let bar = [ + MoveTo((0., graph_max_height).into()), + LineTo((0., 0.).into()), + LineTo((bar_width, 0.).into()), + LineTo((bar_width, graph_max_height).into()), + ]; + for (i, sample) in samples.enumerate() { + let t = offset * Affine::translate(((i as f64) * bar_extent, graph_max_height)); + // The height of each sample is based on its ratio to the maximum observed frame time. + // Currently this maximum scale is sticky and a high temporary spike will permanently + // shrink the draw size of the overall average sample, so scale the size non-linearly to + // emphasize smaller samples. + let h = (*sample as f64) * 0.001 / self.frame_time_max_ms; + let s = Affine::scale_non_uniform(1., -h.sqrt()); + sb.fill( + Fill::NonZero, + t * Affine::translate((left_margin, 5. * text_height)) * s, + Color::rgb8(0, 240, 0), + None, + &bar, + ); + } } } +pub struct Sample { + pub frame_time_us: u64, +} + pub struct Stats { count: usize, sum: u64, @@ -106,18 +142,6 @@ pub struct Stats { 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 { @@ -129,6 +153,10 @@ impl Stats { } } + pub fn samples(&self) -> impl Iterator { + self.samples.iter() + } + 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; @@ -140,20 +168,14 @@ impl Stats { } } - pub fn frame_scope<'a>(&'a mut self) -> FrameScope<'a> { - FrameScope { - stats: self, - start: Instant::now(), - } - } - - fn add_sample(&mut self, micros: u64) { + pub fn add_sample(&mut self, sample: Sample) { let oldest = if self.count < SLIDING_WINDOW_SIZE { self.count += 1; None } else { self.samples.pop_front() }; + let micros = sample.frame_time_us; self.sum += micros; self.samples.push_back(micros); if let Some(oldest) = oldest { From 306aeab6dfb6673dcedae18a3bb26f0f9a5311de Mon Sep 17 00:00:00 2001 From: Arman Uguray Date: Thu, 16 Mar 2023 00:30:51 -0700 Subject: [PATCH 05/13] [frame_stats] Key binding to clear min/max frame time; address review comments --- examples/with_winit/README.md | 1 + examples/with_winit/src/lib.rs | 9 ++++++--- examples/with_winit/src/stats.rs | 13 +++++++------ 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/examples/with_winit/README.md b/examples/with_winit/README.md index 49527b5..8dc1a8e 100644 --- a/examples/with_winit/README.md +++ b/examples/with_winit/README.md @@ -19,4 +19,5 @@ $ cargo run -p with_winit --release -- [SVG FILES] - 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 +- C resets the min/max frame time tracked by statistics - Escape exits the program. diff --git a/examples/with_winit/src/lib.rs b/examples/with_winit/src/lib.rs index 7943d6f..760ec99 100644 --- a/examples/with_winit/src/lib.rs +++ b/examples/with_winit/src/lib.rs @@ -99,7 +99,7 @@ fn run( let mut simple_text = SimpleText::new(); let mut images = ImageCache::new(); let mut stats = stats::Stats::new(); - let mut stats_toggle = true; + let mut stats_shown = true; let start = Instant::now(); let mut touch_state = multi_touch::TouchState::new(); @@ -146,7 +146,10 @@ fn run( transform = Affine::IDENTITY; } Some(VirtualKeyCode::S) => { - stats_toggle = !stats_toggle; + stats_shown = !stats_shown; + } + Some(VirtualKeyCode::C) => { + stats.clear_min_and_max(); } Some(VirtualKeyCode::Escape) => { *control_flow = ControlFlow::Exit; @@ -304,7 +307,7 @@ fn run( transform = transform * Affine::scale(scale_factor); } builder.append(&fragment, Some(transform)); - if stats_toggle { + if stats_shown { snapshot.draw_layer( &mut builder, &mut scene_params.text, diff --git a/examples/with_winit/src/stats.rs b/examples/with_winit/src/stats.rs index e0b0b28..0e68e63 100644 --- a/examples/with_winit/src/stats.rs +++ b/examples/with_winit/src/stats.rs @@ -168,6 +168,11 @@ impl Stats { } } + pub fn clear_min_and_max(&mut self) { + self.min = u64::MAX; + self.max = u64::MIN; + } + pub fn add_sample(&mut self, sample: Sample) { let oldest = if self.count < SLIDING_WINDOW_SIZE { self.count += 1; @@ -181,11 +186,7 @@ impl Stats { if let Some(oldest) = oldest { self.sum -= oldest; } - if micros < self.min { - self.min = micros; - } - if micros > self.max { - self.max = micros; - } + self.min = self.min.min(micros); + self.max = self.max.max(micros); } } From 1250cdcf868e34b1dcc733768f84447f0398e389 Mon Sep 17 00:00:00 2001 From: Arman Uguray Date: Thu, 16 Mar 2023 01:02:49 -0700 Subject: [PATCH 06/13] [frame_stats] Add a VSync toggle key; show the current VSync state in stats UI --- examples/with_winit/README.md | 1 + examples/with_winit/src/lib.rs | 13 +++++++ examples/with_winit/src/stats.rs | 67 ++++++++++++++------------------ src/util.rs | 7 ++++ 4 files changed, 50 insertions(+), 38 deletions(-) diff --git a/examples/with_winit/README.md b/examples/with_winit/README.md index 8dc1a8e..f1c0910 100644 --- a/examples/with_winit/README.md +++ b/examples/with_winit/README.md @@ -20,4 +20,5 @@ $ cargo run -p with_winit --release -- [SVG FILES] - Space resets the position and zoom of the image. - S toggles the frame statistics layer - C resets the min/max frame time tracked by statistics +- V toggles VSync on/off (default: on) - Escape exits the program. diff --git a/examples/with_winit/src/lib.rs b/examples/with_winit/src/lib.rs index 760ec99..34553f2 100644 --- a/examples/with_winit/src/lib.rs +++ b/examples/with_winit/src/lib.rs @@ -100,6 +100,7 @@ fn run( let mut images = ImageCache::new(); let mut stats = stats::Stats::new(); let mut stats_shown = true; + let mut vsync_on = true; let start = Instant::now(); let mut touch_state = multi_touch::TouchState::new(); @@ -151,6 +152,17 @@ fn run( Some(VirtualKeyCode::C) => { stats.clear_min_and_max(); } + Some(VirtualKeyCode::V) => { + vsync_on = !vsync_on; + render_cx.set_present_mode( + &mut render_state.surface, + if vsync_on { + wgpu::PresentMode::Fifo + } else { + wgpu::PresentMode::Immediate + }, + ); + } Some(VirtualKeyCode::Escape) => { *control_flow = ControlFlow::Exit; } @@ -314,6 +326,7 @@ fn run( width as f64, height as f64, stats.samples(), + vsync_on, ); } let surface_texture = render_state diff --git a/examples/with_winit/src/stats.rs b/examples/with_winit/src/stats.rs index 0e68e63..76c3e04 100644 --- a/examples/with_winit/src/stats.rs +++ b/examples/with_winit/src/stats.rs @@ -40,17 +40,17 @@ impl Snapshot { viewport_width: f64, viewport_height: f64, samples: T, + vsync: bool, ) where T: Iterator, { let width = (viewport_width * 0.4).max(200.).min(600.); - let height = width * 0.6; + let height = width * 0.7; let x_offset = viewport_width - width; let y_offset = viewport_height - height; let offset = Affine::translate((x_offset, y_offset)); - let text_height = height * 0.1; - let left_margin = width * 0.01; - let text_size = (text_height * 0.9) as f32; + + // Draw the background sb.fill( Fill::NonZero, offset, @@ -58,38 +58,29 @@ impl Snapshot { None, &Rect::new(0., 0., width, height), ); - text.add( - sb, - None, - text_size, - Some(&Brush::Solid(Color::WHITE)), - offset * Affine::translate((left_margin, text_height)), - &format!("Frame Time: {:.2} ms", self.frame_time_ms), - ); - text.add( - sb, - None, - text_size, - Some(&Brush::Solid(Color::WHITE)), - offset * Affine::translate((left_margin, 2. * text_height)), - &format!("Frame Time (min): {:.2} ms", self.frame_time_min_ms), - ); - text.add( - sb, - None, - text_size, - Some(&Brush::Solid(Color::WHITE)), - offset * Affine::translate((left_margin, 3. * text_height)), - &format!("Frame Time (max): {:.2} ms", self.frame_time_max_ms), - ); - text.add( - sb, - None, - text_size, - Some(&Brush::Solid(Color::WHITE)), - offset * Affine::translate((left_margin, 4. * text_height)), - &format!("Resolution: {viewport_width}x{viewport_height}"), - ); + + let labels = [ + format!("Frame Time: {:.2} ms", self.frame_time_ms), + format!("Frame Time (min): {:.2} ms", self.frame_time_min_ms), + format!("Frame Time (max): {:.2} ms", self.frame_time_max_ms), + format!("VSync: {}", if vsync { "on" } else { "off" }), + format!("Resolution: {viewport_width}x{viewport_height}"), + ]; + + // height / 2 is dedicated to the text labels and the rest is filled by the bar graph. + let text_height = height * 0.5 / (1 + labels.len()) as f64; + let left_margin = width * 0.01; + let text_size = (text_height * 0.9) as f32; + for (i, label) in labels.iter().enumerate() { + text.add( + sb, + None, + text_size, + Some(&Brush::Solid(Color::WHITE)), + offset * Affine::translate((left_margin, (i + 1) as f64 * text_height)), + &label, + ); + } text.add( sb, None, @@ -112,7 +103,7 @@ impl Snapshot { LineTo((bar_width, graph_max_height).into()), ]; for (i, sample) in samples.enumerate() { - let t = offset * Affine::translate(((i as f64) * bar_extent, graph_max_height)); + let t = offset * Affine::translate((i as f64 * bar_extent, graph_max_height)); // The height of each sample is based on its ratio to the maximum observed frame time. // Currently this maximum scale is sticky and a high temporary spike will permanently // shrink the draw size of the overall average sample, so scale the size non-linearly to @@ -121,7 +112,7 @@ impl Snapshot { let s = Affine::scale_non_uniform(1., -h.sqrt()); sb.fill( Fill::NonZero, - t * Affine::translate((left_margin, 5. * text_height)) * s, + t * Affine::translate((left_margin, (1 + labels.len()) as f64 * text_height)) * s, Color::rgb8(0, 240, 0), None, &bar, diff --git a/src/util.rs b/src/util.rs index e33d940..d73778a 100644 --- a/src/util.rs +++ b/src/util.rs @@ -92,6 +92,13 @@ impl RenderContext { .configure(&self.devices[surface.dev_id].device, &surface.config); } + pub fn set_present_mode(&self, surface: &mut RenderSurface, present_mode: wgpu::PresentMode) { + surface.config.present_mode = present_mode; + surface + .surface + .configure(&self.devices[surface.dev_id].device, &surface.config); + } + /// Finds or creates a compatible device handle id. pub async fn device(&mut self, compatible_surface: Option<&Surface>) -> Option { let compatible = match compatible_surface { From a1c0df1058897b421fa420e636c8b8ed9591d03e Mon Sep 17 00:00:00 2001 From: Arman Uguray Date: Thu, 16 Mar 2023 01:08:36 -0700 Subject: [PATCH 07/13] [frame_stats] Track frame time more tightly We now track the frame time from snapshot to snapshot corresponding to the exact presentation time. --- examples/with_winit/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/with_winit/src/lib.rs b/examples/with_winit/src/lib.rs index 34553f2..79601b9 100644 --- a/examples/with_winit/src/lib.rs +++ b/examples/with_winit/src/lib.rs @@ -101,6 +101,7 @@ fn run( let mut stats = stats::Stats::new(); let mut stats_shown = true; let mut vsync_on = true; + let mut frame_start_time = Instant::now(); let start = Instant::now(); let mut touch_state = multi_touch::TouchState::new(); @@ -275,7 +276,6 @@ fn run( let height = render_state.surface.config.height; let device_handle = &render_cx.devices[render_state.surface.dev_id]; let snapshot = stats.snapshot(); - let frame_start_time = Instant::now(); // Allow looping forever scene_ix = scene_ix.rem_euclid(scenes.scenes.len() as i32); @@ -371,6 +371,7 @@ fn run( stats.add_sample(stats::Sample { frame_time_us: frame_start_time.elapsed().as_micros() as u64, }); + frame_start_time = Instant::now(); } Event::UserEvent(event) => match event { #[cfg(not(any(target_arch = "wasm32", target_os = "android")))] From 02a222f435e6a97253522fb0f9f5e0e888842f69 Mon Sep 17 00:00:00 2001 From: Arman Uguray Date: Thu, 16 Mar 2023 09:27:30 -0700 Subject: [PATCH 08/13] [frame_stats] Reduce stats time delta between elapsed() and next frame start time This does not account for the time spent in processing `Stats::add_sample` but it should be very close. --- examples/with_winit/src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/with_winit/src/lib.rs b/examples/with_winit/src/lib.rs index 79601b9..5e023e6 100644 --- a/examples/with_winit/src/lib.rs +++ b/examples/with_winit/src/lib.rs @@ -368,10 +368,11 @@ fn run( surface_texture.present(); device_handle.device.poll(wgpu::Maintain::Poll); + let new_time = Instant::now(); stats.add_sample(stats::Sample { - frame_time_us: frame_start_time.elapsed().as_micros() as u64, + frame_time_us: (new_time - frame_start_time).as_micros() as u64, }); - frame_start_time = Instant::now(); + frame_start_time = new_time; } Event::UserEvent(event) => match event { #[cfg(not(any(target_arch = "wasm32", target_os = "android")))] From 8bd1bdfaa862af383c4134137df9e4ec7874c838 Mon Sep 17 00:00:00 2001 From: Arman Uguray Date: Thu, 16 Mar 2023 09:28:42 -0700 Subject: [PATCH 09/13] [frame_stats] Use wgpu::PresentMode::AutoVsync/AutoNoVsync for VSync mode The Auto* modes should have wider compatibility as they implement fallback behavior based on what the platform supports. This also means that on web "vsync off" will likely be incorrect. --- examples/with_winit/src/lib.rs | 4 ++-- src/util.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/with_winit/src/lib.rs b/examples/with_winit/src/lib.rs index 5e023e6..2536e4c 100644 --- a/examples/with_winit/src/lib.rs +++ b/examples/with_winit/src/lib.rs @@ -158,9 +158,9 @@ fn run( render_cx.set_present_mode( &mut render_state.surface, if vsync_on { - wgpu::PresentMode::Fifo + wgpu::PresentMode::AutoVsync } else { - wgpu::PresentMode::Immediate + wgpu::PresentMode::AutoNoVsync }, ); } diff --git a/src/util.rs b/src/util.rs index d73778a..87e2d35 100644 --- a/src/util.rs +++ b/src/util.rs @@ -70,7 +70,7 @@ impl RenderContext { format, width, height, - present_mode: wgpu::PresentMode::Fifo, + present_mode: wgpu::PresentMode::AutoVsync, alpha_mode: wgpu::CompositeAlphaMode::Auto, view_formats: vec![], }; From 5ba28264604d337405bbb00909d80a4849ce1e7e Mon Sep 17 00:00:00 2001 From: Arman Uguray Date: Tue, 21 Mar 2023 10:30:02 -0700 Subject: [PATCH 10/13] [frame_stats] Scale the frame time graph linearly The sqrt scale doesn't add much value any more since the max frame time can be reset with a keypress. --- examples/with_winit/src/stats.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/examples/with_winit/src/stats.rs b/examples/with_winit/src/stats.rs index 76c3e04..e9f1bf3 100644 --- a/examples/with_winit/src/stats.rs +++ b/examples/with_winit/src/stats.rs @@ -105,11 +105,8 @@ impl Snapshot { for (i, sample) in samples.enumerate() { let t = offset * Affine::translate((i as f64 * bar_extent, graph_max_height)); // The height of each sample is based on its ratio to the maximum observed frame time. - // Currently this maximum scale is sticky and a high temporary spike will permanently - // shrink the draw size of the overall average sample, so scale the size non-linearly to - // emphasize smaller samples. let h = (*sample as f64) * 0.001 / self.frame_time_max_ms; - let s = Affine::scale_non_uniform(1., -h.sqrt()); + let s = Affine::scale_non_uniform(1., -h); sb.fill( Fill::NonZero, t * Affine::translate((left_margin, (1 + labels.len()) as f64 * text_height)) * s, From e6f4f6de5700f29b0952277a1c77c539c523337c Mon Sep 17 00:00:00 2001 From: Arman Uguray Date: Tue, 21 Mar 2023 11:31:25 -0700 Subject: [PATCH 11/13] [frame_stats] Draw thresholds for 16.66ms, 33.33ms, and 8.33 timings Also added color coding for the bar graph based on these thresholds. --- examples/with_winit/src/stats.rs | 51 ++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/examples/with_winit/src/stats.rs b/examples/with_winit/src/stats.rs index e9f1bf3..7d6f76c 100644 --- a/examples/with_winit/src/stats.rs +++ b/examples/with_winit/src/stats.rs @@ -18,7 +18,7 @@ use scenes::SimpleText; use std::collections::VecDeque; use vello::{ kurbo::{Affine, PathEl, Rect}, - peniko::{Brush, Color, Fill}, + peniko::{Brush, Color, Fill, Stroke}, SceneBuilder, }; @@ -92,10 +92,12 @@ impl Snapshot { // Plot the samples with a bar graph use PathEl::*; + let left_padding = width * 0.05; // Left padding for the frame time marker text. let graph_max_height = height * 0.5; - let graph_max_width = width - 2. * left_margin; + let graph_max_width = width - 2. * left_margin - left_padding; + let left_margin_padding = left_margin + left_padding; let bar_extent = graph_max_width / (SLIDING_WINDOW_SIZE as f64); - let bar_width = bar_extent * 0.3; + let bar_width = bar_extent * 0.4; let bar = [ MoveTo((0., graph_max_height).into()), LineTo((0., 0.).into()), @@ -109,12 +111,51 @@ impl Snapshot { let s = Affine::scale_non_uniform(1., -h); sb.fill( Fill::NonZero, - t * Affine::translate((left_margin, (1 + labels.len()) as f64 * text_height)) * s, - Color::rgb8(0, 240, 0), + t * Affine::translate(( + left_margin_padding, + (1 + labels.len()) as f64 * text_height, + )) * s, + if *sample < 16667 { + Color::rgb8(100, 143, 255) + } else if *sample < 33334 { + Color::rgb8(255, 176, 0) + } else { + Color::rgb8(220, 38, 127) + }, None, &bar, ); } + // Draw horizontal lines to mark 8.33ms, 16.33ms, and 33.33ms + let marker = [ + MoveTo((0., graph_max_height).into()), + LineTo((graph_max_width, graph_max_height).into()), + ]; + let thresholds = [8.33, 16.66, 33.33]; + let thres_text_height = graph_max_height * 0.05; + let thres_text_height_2 = thres_text_height * 0.5; + for t in thresholds.iter().filter(|&&t| t < self.frame_time_max_ms) { + let y = t / self.frame_time_max_ms; + text.add( + sb, + None, + thres_text_height as f32, + Some(&Brush::Solid(Color::WHITE)), + offset + * Affine::translate(( + left_margin, + (2. - y) * graph_max_height + thres_text_height_2, + )), + &format!("{}", t), + ); + sb.stroke( + &Stroke::new((graph_max_height * 0.01) as f32), + offset * Affine::translate((left_margin_padding, (1. - y) * graph_max_height)), + Color::WHITE, + None, + &marker, + ); + } } } From 6f3051837f178d92f103bc40d0207b760e4ec7b2 Mon Sep 17 00:00:00 2001 From: Arman Uguray Date: Tue, 21 Mar 2023 15:16:45 -0700 Subject: [PATCH 12/13] [frame_stats] Adapt the graph scale based on the current mean frame time This allows the graph to display at a reasonable scale in the face of fluctuations and a max recorded sample that is much larger than the current average. --- examples/with_winit/src/stats.rs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/examples/with_winit/src/stats.rs b/examples/with_winit/src/stats.rs index 7d6f76c..85f4dd7 100644 --- a/examples/with_winit/src/stats.rs +++ b/examples/with_winit/src/stats.rs @@ -104,10 +104,22 @@ impl Snapshot { LineTo((bar_width, 0.).into()), LineTo((bar_width, graph_max_height).into()), ]; + // We determine the scale of the graph based on the maximum sampled frame time unless it's + // greater than 3x the current average. In that case we cap the max scale at 4/3 * the + // current average (rounded up to the nearest multiple of 5ms). This allows the scale to + // adapt to the most recent sample set as relying on the maximum alone can make the + // displayed samples to look too small in the presence of spikes/fluctuation without + // manually resetting the max sample. + let display_max = if self.frame_time_max_ms > 3. * self.frame_time_ms { + round_up((1.33334 * self.frame_time_ms) as usize, 5) as f64 + } else { + self.frame_time_max_ms + }; for (i, sample) in samples.enumerate() { let t = offset * Affine::translate((i as f64 * bar_extent, graph_max_height)); // The height of each sample is based on its ratio to the maximum observed frame time. - let h = (*sample as f64) * 0.001 / self.frame_time_max_ms; + let sample_ms = ((*sample as f64) * 0.001).min(display_max); + let h = sample_ms / display_max; let s = Affine::scale_non_uniform(1., -h); sb.fill( Fill::NonZero, @@ -134,8 +146,8 @@ impl Snapshot { let thresholds = [8.33, 16.66, 33.33]; let thres_text_height = graph_max_height * 0.05; let thres_text_height_2 = thres_text_height * 0.5; - for t in thresholds.iter().filter(|&&t| t < self.frame_time_max_ms) { - let y = t / self.frame_time_max_ms; + for t in thresholds.iter().filter(|&&t| t < display_max) { + let y = t / display_max; text.add( sb, None, @@ -219,3 +231,7 @@ impl Stats { self.max = self.max.max(micros); } } + +fn round_up(n: usize, f: usize) -> usize { + n - 1 - (n - 1) % f + f +} From 703d22f4b7a90b4c4cc8d2433e7a284d8b3cbfb5 Mon Sep 17 00:00:00 2001 From: Arman Uguray Date: Tue, 21 Mar 2023 15:29:16 -0700 Subject: [PATCH 13/13] [frame_stats] Use match statement for color coding graph --- examples/with_winit/src/stats.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/examples/with_winit/src/stats.rs b/examples/with_winit/src/stats.rs index 85f4dd7..a6531e6 100644 --- a/examples/with_winit/src/stats.rs +++ b/examples/with_winit/src/stats.rs @@ -121,19 +121,18 @@ impl Snapshot { let sample_ms = ((*sample as f64) * 0.001).min(display_max); let h = sample_ms / display_max; let s = Affine::scale_non_uniform(1., -h); + let color = match *sample { + ..=16_667 => Color::rgb8(100, 143, 255), + ..=33_334 => Color::rgb8(255, 176, 0), + _ => Color::rgb8(220, 38, 127), + }; sb.fill( Fill::NonZero, t * Affine::translate(( left_margin_padding, (1 + labels.len()) as f64 * text_height, )) * s, - if *sample < 16667 { - Color::rgb8(100, 143, 255) - } else if *sample < 33334 { - Color::rgb8(255, 176, 0) - } else { - Color::rgb8(220, 38, 127) - }, + color, None, &bar, );