Integrate some tests and simplify
diff --git a/hacknight/src/main.rs b/hacknight/src/main.rs index 234573b..d7147c2 100644 --- a/hacknight/src/main.rs +++ b/hacknight/src/main.rs
@@ -14,21 +14,20 @@ // // Also licensed under MIT license, at your choice. -use instant::{Duration, Instant}; +use instant::Instant; use vello::kurbo::Circle; use anyhow::Result; -use vello::peniko::{Brush, Color}; +use vello::peniko::{Brush, Color, Style}; use vello::util::RenderSurface; +use vello::RendererOptions; use vello::{kurbo::Affine, util::RenderContext, AaConfig, Renderer, Scene, SceneBuilder}; -use vello::{BumpAllocators, RendererOptions}; use winit::{event_loop::EventLoop, window::Window}; use crate::simple_text::SimpleText; pub mod simple_text; -mod stats; struct RenderState { // SAFETY: We MUST drop the surface before the `window`, so the fields @@ -49,21 +48,11 @@ let mut scene = Scene::new(); let mut simple_text = SimpleText::new(); - let mut stats = stats::Stats::new(); - let mut stats_shown = false; - let mut scene_complexity: Option<BumpAllocators> = None; - let mut complexity_shown = false; let mut vsync_on = true; - let mut frame_start_time = Instant::now(); - #[allow(unused)] let start = Instant::now(); - let mut profile_stored = None; - let mut profile_taken = Instant::now(); - - let mut modifiers = ModifiersState::default(); // _event_loop is used on non-wasm platforms to create new windows event_loop.run(move |event, _event_loop, control_flow| match event { Event::WindowEvent { @@ -78,21 +67,10 @@ } match event { WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, - WindowEvent::ModifiersChanged(m) => modifiers = *m, + // WindowEvent::ModifiersChanged(m) => modifiers = *m, WindowEvent::KeyboardInput { input, .. } => { if input.state == ElementState::Pressed { match input.virtual_keycode { - Some(VirtualKeyCode::S) => { - // Toggle showing the stats screen - stats_shown = !stats_shown; - } - Some(VirtualKeyCode::D) => { - // Toggle showing the complexity menu - complexity_shown = !complexity_shown; - } - Some(VirtualKeyCode::C) => { - stats.clear_min_and_max(); - } Some(VirtualKeyCode::V) => { // Toggle vsync vsync_on = !vsync_on; @@ -131,7 +109,6 @@ 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 mut builder = SceneBuilder::for_scene(&mut scene); { @@ -142,6 +119,16 @@ &Brush::Solid(Color::rgb(255., 100., 0.)), None, &Circle::new((300., 300.), 200.), + ); + simple_text.add_run( + &mut builder, + None, + 24., + &Brush::Solid(Color::WHITE), + Affine::translate((100., 200.)), + None, + &Style::Fill(vello::peniko::Fill::EvenOdd), + "Hello Rustlab 2023 Hacknight!", ) } @@ -153,45 +140,13 @@ height, antialiasing_method: AaConfig::Area, }; - - if stats_shown { - snapshot.draw_layer( - &mut builder, - &mut simple_text, - width as f64, - height as f64, - stats.samples(), - complexity_shown.then_some(scene_complexity).flatten(), - vsync_on, - AaConfig::Area, - ); - if let Some(profiling_result) = renderers[render_state.surface.dev_id] - .as_mut() - .and_then(|it| it.profile_result.take()) - { - if profile_stored.is_none() || profile_taken.elapsed() > Duration::from_secs(1) - { - profile_stored = Some(profiling_result); - profile_taken = Instant::now(); - } - } - if let Some(profiling_result) = profile_stored.as_ref() { - stats::draw_gpu_profiling( - &mut builder, - &mut simple_text, - width as f64, - height as f64, - profiling_result, - ); - } - } let surface_texture = render_state .surface .surface .get_current_texture() .expect("failed to get surface texture"); - scene_complexity = vello::block_on_wgpu( + vello::block_on_wgpu( &device_handle.device, renderers[render_state.surface.dev_id] .as_mut() @@ -208,12 +163,6 @@ surface_texture.present(); device_handle.device.poll(wgpu::Maintain::Poll); - - let new_time = Instant::now(); - stats.add_sample(stats::Sample { - frame_time_us: (new_time - frame_start_time).as_micros() as u64, - }); - frame_start_time = new_time; } Event::Suspended => { // When we suspend, we need to remove the `wgpu` Surface
diff --git a/hacknight/src/stats.rs b/hacknight/src/stats.rs deleted file mode 100644 index 6a60407..0000000 --- a/hacknight/src/stats.rs +++ /dev/null
@@ -1,460 +0,0 @@ -// 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 crate::simple_text::SimpleText; -use std::{collections::VecDeque, time::Duration}; -use vello::{ - kurbo::{Affine, Line, PathEl, Rect, Stroke}, - peniko::{Brush, Color, Fill}, - AaConfig, BumpAllocators, SceneBuilder, -}; -use wgpu_profiler::GpuTimerScopeResult; - -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 { - #[allow(clippy::too_many_arguments)] - pub fn draw_layer<'a, T>( - &self, - sb: &mut SceneBuilder, - text: &mut SimpleText, - viewport_width: f64, - viewport_height: f64, - samples: T, - bump: Option<BumpAllocators>, - vsync: bool, - aa_config: AaConfig, - ) where - T: Iterator<Item = &'a u64>, - { - let width = (viewport_width * 0.4).max(200.).min(600.); - 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)); - - // Draw the background - sb.fill( - Fill::NonZero, - offset, - &Brush::Solid(Color::rgba8(0, 0, 0, 200)), - None, - &Rect::new(0., 0., width, height), - ); - - let mut labels = vec![ - 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!( - "AA method: {}", - match aa_config { - AaConfig::Area => "Analytic Area", - AaConfig::Msaa16 => "16xMSAA", - AaConfig::Msaa8 => "8xMSAA", - } - ), - format!("Resolution: {viewport_width}x{viewport_height}"), - ]; - if let Some(bump) = &bump { - if bump.failed >= 1 { - labels.push("Allocation Failed!".into()); - } - labels.push(format!("binning: {}", bump.binning)); - labels.push(format!("ptcl: {}", bump.ptcl)); - labels.push(format!("tile: {}", bump.tile)); - labels.push(format!("segments: {}", bump.segments)); - labels.push(format!("blend: {}", bump.blend)); - } - - // 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, - text_size, - Some(&Brush::Solid(Color::WHITE)), - offset * Affine::translate((width * 0.67, text_height)), - &format!("FPS: {:.2}", self.fps), - ); - - // 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 - 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.4; - let bar = [ - MoveTo((0., graph_max_height).into()), - LineTo((0., 0.).into()), - 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 sample_ms = ((*sample as f64) * 0.001).min(display_max); - let h = sample_ms / display_max; - let s = Affine::scale_non_uniform(1., -h); - #[allow(clippy::match_overlapping_arm)] - 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, - color, - 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 < display_max) { - let y = t / display_max; - 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), - offset * Affine::translate((left_margin_padding, (1. - y) * graph_max_height)), - Color::WHITE, - None, - &marker, - ); - } - } -} - -pub struct Sample { - pub frame_time_us: u64, -} - -pub struct Stats { - count: usize, - sum: u64, - min: u64, - max: u64, - samples: VecDeque<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 samples(&self) -> impl Iterator<Item = &u64> { - 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; - 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 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; - 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 { - self.sum -= oldest; - } - self.min = self.min.min(micros); - self.max = self.max.max(micros); - } -} - -fn round_up(n: usize, f: usize) -> usize { - n - 1 - (n - 1) % f + f -} - -const COLORS: &[Color] = &[ - Color::AQUA, - Color::RED, - Color::ALICE_BLUE, - Color::YELLOW, - Color::GREEN, - Color::BLUE, - Color::ORANGE, - Color::WHITE, -]; - -pub fn draw_gpu_profiling( - sb: &mut SceneBuilder, - text: &mut SimpleText, - viewport_width: f64, - viewport_height: f64, - profiles: &[GpuTimerScopeResult], -) { - if profiles.is_empty() { - return; - } - let width = (viewport_width * 0.3).clamp(150., 450.); - let height = width * 1.5; - let y_offset = viewport_height - height; - let offset = Affine::translate((0., y_offset)); - - // Draw the background - sb.fill( - Fill::NonZero, - offset, - &Brush::Solid(Color::rgba8(0, 0, 0, 200)), - None, - &Rect::new(0., 0., width, height), - ); - // Find the range of the samples, so we can normalise them - let mut min = f64::MAX; - let mut max = f64::MIN; - let mut max_depth = 0; - let mut depth = 0; - let mut count = 0; - traverse_profiling(profiles, &mut |profile, stage| { - match stage { - TraversalStage::Enter => { - count += 1; - min = min.min(profile.time.start); - max = max.max(profile.time.end); - max_depth = max_depth.max(depth); - // Apply a higher depth to the children - depth += 1; - } - TraversalStage::Leave => depth -= 1, - } - }); - let total_time = max - min; - { - let labels = [ - format!("GPU Time: {:.2?}", Duration::from_secs_f64(total_time)), - "Press P to save a trace".to_string(), - ]; - - // height / 5 is dedicated to the text labels and the rest is filled by the frame time. - let text_height = height * 0.2 / (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, - ); - } - - 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, - ); - } - } - let timeline_start_y = height * 0.21; - let timeline_range_y = height * 0.78; - let timeline_range_end = timeline_start_y + timeline_range_y; - - // Add 6 items worth of margin - let text_height = timeline_range_y / (6 + count) as f64; - let left_margin = width * 0.35; - let mut cur_text_y = timeline_start_y; - let mut cur_index = 0; - let mut depth = 0; - // Leave 1 bar's worth of margin - let depth_width = width * 0.28 / (max_depth + 1) as f64; - let depth_size = depth_width * 0.8; - traverse_profiling(profiles, &mut |profile, stage| { - if let TraversalStage::Enter = stage { - let start_normalised = - ((profile.time.start - min) / total_time) * timeline_range_y + timeline_start_y; - let end_normalised = - ((profile.time.end - min) / total_time) * timeline_range_y + timeline_start_y; - - let color = COLORS[cur_index % COLORS.len()]; - let x = width * 0.01 + (depth as f64 * depth_width); - sb.fill( - Fill::NonZero, - offset, - &Brush::Solid(color), - None, - &Rect::new(x, start_normalised, x + depth_size, end_normalised), - ); - - let mut text_start = start_normalised; - let nested = !profile.nested_scopes.is_empty(); - if nested { - // If we have children, leave some more space for them - text_start -= text_height * 0.7; - } - let this_time = profile.time.end - profile.time.start; - // Highlight as important if more than 10% of the total time, or more than 1ms - let slow = this_time * 20. >= total_time || this_time >= 0.001; - let text_y = text_start - // Ensure that we don't overlap the previous item - .max(cur_text_y) - // Ensure that all remaining items can fit - .min(timeline_range_end - (count - cur_index) as f64 * text_height); - let (text_height, text_color) = if slow { - (text_height, Color::WHITE) - } else { - (text_height * 0.6, Color::LIGHT_GRAY) - }; - let text_size = (text_height * 0.9) as f32; - // Text is specified by the baseline, but the y positions all refer to the top of the text - cur_text_y = text_y + text_height; - let label = format!( - "{:.2?} - {:.30}", - Duration::from_secs_f64(this_time), - profile.label - ); - sb.fill( - Fill::NonZero, - offset, - &Brush::Solid(color), - None, - &Rect::new( - width * 0.31, - cur_text_y - text_size as f64 * 0.7, - width * 0.34, - cur_text_y, - ), - ); - text.add( - sb, - None, - text_size, - Some(&Brush::Solid(text_color)), - offset * Affine::translate((left_margin, cur_text_y)), - &label, - ); - if !nested && slow { - sb.stroke( - &Stroke::new(2.), - offset, - &Brush::Solid(color), - None, - &Line::new( - (x + depth_size, (end_normalised + start_normalised) / 2.), - (width * 0.31, cur_text_y - text_size as f64 * 0.35), - ), - ); - } - cur_index += 1; - // Higher depth applies only to the children - depth += 1; - } else { - depth -= 1; - } - }); -} - -enum TraversalStage { - Enter, - Leave, -} - -fn traverse_profiling( - profiles: &[GpuTimerScopeResult], - callback: &mut impl FnMut(&GpuTimerScopeResult, TraversalStage), -) { - for profile in profiles { - callback(profile, TraversalStage::Enter); - traverse_profiling(&profile.nested_scopes, &mut *callback); - callback(profile, TraversalStage::Leave); - } -}
diff --git a/src/lib.rs b/src/lib.rs index b2554c2..09624a4 100644 --- a/src/lib.rs +++ b/src/lib.rs
@@ -100,6 +100,7 @@ /// Renders a scene into a texture or surface. #[cfg(feature = "wgpu")] pub struct Renderer { + #[cfg(feature = "hot_reload")] options: RendererOptions, engine: WgpuEngine, shaders: FullShaders, @@ -158,6 +159,7 @@ #[cfg(feature = "wgpu-profiler")] let timestamp_period = options.timestamp_period; Ok(Self { + #[cfg(feature = "hot_reload")] options, engine, shaders,