Merge pull request #297 from linebender/vello-fello

Replace font backend
diff --git a/examples/with_winit/README.md b/examples/with_winit/README.md
index c29fa1d..f1c0910 100644
--- a/examples/with_winit/README.md
+++ b/examples/with_winit/README.md
@@ -18,4 +18,7 @@
 - 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
+- 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 380d40b..2536e4c 100644
--- a/examples/with_winit/src/lib.rs
+++ b/examples/with_winit/src/lib.rs
@@ -37,6 +37,7 @@
 #[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,10 @@
     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_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();
@@ -142,6 +147,23 @@
                             Some(VirtualKeyCode::Space) => {
                                 transform = Affine::IDENTITY;
                             }
+                            Some(VirtualKeyCode::S) => {
+                                stats_shown = !stats_shown;
+                            }
+                            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::AutoVsync
+                                    } else {
+                                        wgpu::PresentMode::AutoNoVsync
+                                    },
+                                );
+                            }
                             Some(VirtualKeyCode::Escape) => {
                                 *control_flow = ControlFlow::Exit;
                             }
@@ -253,6 +275,7 @@
             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();
 
             // Allow looping forever
             scene_ix = scene_ix.rem_euclid(scenes.scenes.len() as i32);
@@ -296,6 +319,16 @@
                 transform = transform * Affine::scale(scale_factor);
             }
             builder.append(&fragment, Some(transform));
+            if stats_shown {
+                snapshot.draw_layer(
+                    &mut builder,
+                    &mut scene_params.text,
+                    width as f64,
+                    height as f64,
+                    stats.samples(),
+                    vsync_on,
+                );
+            }
             let surface_texture = render_state
                 .surface
                 .surface
@@ -334,6 +367,12 @@
                 .expect("failed to render to surface");
             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::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
new file mode 100644
index 0000000..a6531e6
--- /dev/null
+++ b/examples/with_winit/src/stats.rs
@@ -0,0 +1,236 @@
+// 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;
+use vello::{
+    kurbo::{Affine, PathEl, Rect},
+    peniko::{Brush, Color, Fill, Stroke},
+    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<'a, T>(
+        &self,
+        sb: &mut SceneBuilder,
+        text: &mut SimpleText,
+        viewport_width: f64,
+        viewport_height: f64,
+        samples: T,
+        vsync: bool,
+    ) 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 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,
+            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);
+            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) as f32),
+                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
+}
diff --git a/src/util.rs b/src/util.rs
index e33d940..87e2d35 100644
--- a/src/util.rs
+++ b/src/util.rs
@@ -70,7 +70,7 @@
             format,
             width,
             height,
-            present_mode: wgpu::PresentMode::Fifo,
+            present_mode: wgpu::PresentMode::AutoVsync,
             alpha_mode: wgpu::CompositeAlphaMode::Auto,
             view_formats: vec![],
         };
@@ -92,6 +92,13 @@
             .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<usize> {
         let compatible = match compatible_surface {