Adaptation of MotionMark 1.2 path test

This is a quick and dirty adapation of the MotionMark 1.2 path test into
Rust. It is not at all a fair comparison, as the original is using miter
joins and butt caps, while this one is round/round (to use the SDF-based
stroke renderer). Nonetheless, it might be fun to play with.
diff --git a/piet-gpu-hal/src/vulkan.rs b/piet-gpu-hal/src/vulkan.rs
index 26e095f..e3f52d6 100644
--- a/piet-gpu-hal/src/vulkan.rs
+++ b/piet-gpu-hal/src/vulkan.rs
@@ -409,7 +409,7 @@
             .get_physical_device_surface_present_modes(device.physical_device, surface.surface)?;
 
         // Can change to MAILBOX to force high frame rates.
-        const PREFERRED_MODE: vk::PresentModeKHR = vk::PresentModeKHR::FIFO;
+        const PREFERRED_MODE: vk::PresentModeKHR = vk::PresentModeKHR::IMMEDIATE;
         let present_mode = present_modes
             .into_iter()
             .find(|mode| *mode == PREFERRED_MODE)
diff --git a/piet-gpu/bin/winit.rs b/piet-gpu/bin/winit.rs
index ef41b31..7e3d4c6 100644
--- a/piet-gpu/bin/winit.rs
+++ b/piet-gpu/bin/winit.rs
@@ -17,6 +17,8 @@
 const WIDTH: usize = 2048;
 const HEIGHT: usize = 1536;
 
+const MMARK_SIZE: usize = 60_000;
+
 fn main() -> Result<(), Error> {
     let matches = App::new("piet-gpu test")
         .arg(Arg::with_name("INPUT").index(1))
@@ -57,6 +59,8 @@
         let mut submitted: [Option<SubmittedCmdBuf>; NUM_FRAMES] = Default::default();
 
         let mut renderer = Renderer::new(&session, WIDTH, HEIGHT, NUM_FRAMES)?;
+        let mut mmark = piet_gpu::mmark::MMark::new(MMARK_SIZE);
+        let mut last_time = std::time::Instant::now();
 
         event_loop.run(move |event, _, control_flow| {
             *control_flow = ControlFlow::Poll; // `ControlFlow::Wait` if only re-render on event
@@ -75,12 +79,17 @@
                 }
                 Event::RedrawRequested(window_id) if window_id == window.id() => {
                     let frame_idx = current_frame % NUM_FRAMES;
+                    let now = std::time::Instant::now();
+                    let elapsed = now - last_time;
+                    last_time = now;
+                    let frame_s = format!("{:.1}ms", elapsed.as_micros() as f64 * 1e-3);
 
                     if let Some(submitted) = submitted[frame_idx].take() {
                         cmd_bufs[frame_idx] = submitted.wait().unwrap();
                         let ts = session.fetch_query_pool(&query_pools[frame_idx]).unwrap();
                         info_string = format!(
-                            "{:.3}ms :: e:{:.3}ms|alloc:{:.3}ms|cp:{:.3}ms|bd:{:.3}ms|bin:{:.3}ms|cr:{:.3}ms|r:{:.3}ms",
+                            "{} {:.3}ms :: e:{:.3}ms|alloc:{:.3}ms|cp:{:.3}ms|bd:{:.3}ms|bin:{:.3}ms|cr:{:.3}ms|r:{:.3}ms",
+                            frame_s,
                             ts[6] * 1e3,
                             ts[0] * 1e3,
                             (ts[1] - ts[0]) * 1e3,
@@ -103,7 +112,7 @@
                         }
                         test_scenes::render_svg(&mut ctx, input, scale);
                     } else {
-                        test_scenes::render_anim_frame(&mut ctx, current_frame);
+                        mmark.draw(&mut ctx);
                     }
                     render_info_string(&mut ctx, &info_string);
                     if let Err(e) = renderer.upload_render_ctx(&mut ctx, frame_idx) {
diff --git a/piet-gpu/src/lib.rs b/piet-gpu/src/lib.rs
index 30fcf8f..b445331 100644
--- a/piet-gpu/src/lib.rs
+++ b/piet-gpu/src/lib.rs
@@ -1,4 +1,5 @@
 mod gradient;
+pub mod mmark;
 mod pico_svg;
 mod render_ctx;
 pub mod test_scenes;
diff --git a/piet-gpu/src/mmark.rs b/piet-gpu/src/mmark.rs
new file mode 100644
index 0000000..8deda1b
--- /dev/null
+++ b/piet-gpu/src/mmark.rs
@@ -0,0 +1,121 @@
+//! A benchmark based on MotionMark 1.2's path benchmark.
+
+use rand::{Rng, seq::SliceRandom};
+use piet::{Color, RenderContext, kurbo::{BezPath, CubicBez, Line, ParamCurve, PathSeg, Point, QuadBez}};
+
+use crate::PietGpuRenderContext;
+
+const WIDTH: usize = 1600;
+const HEIGHT: usize = 900;
+
+const GRID_WIDTH: i64 = 80;
+const GRID_HEIGHT: i64 = 40;
+
+pub struct MMark {
+    elements: Vec<Element>,
+}
+
+struct Element {
+    seg: PathSeg,
+    color: Color,
+    width: f64,
+    is_split: bool,
+    grid_point: GridPoint,
+}
+
+#[derive(Clone, Copy)]
+struct GridPoint((i64, i64));
+
+impl MMark {
+    pub fn new(n: usize) -> MMark {
+        let mut last = GridPoint((GRID_WIDTH / 2, GRID_HEIGHT / 2));
+        let elements = (0..n).map(|_| {
+            let element = Element::new_rand(last);
+            last = element.grid_point;
+            element
+        }).collect();
+        MMark { elements }
+    }
+
+    pub fn draw(&mut self, ctx: &mut PietGpuRenderContext) {
+        let mut rng = rand::thread_rng();
+        let mut path = BezPath::new();
+        let len = self.elements.len();
+        for (i, element) in self.elements.iter_mut().enumerate() {
+            if path.is_empty() {
+                path.move_to(element.seg.start());
+            }
+            match element.seg {
+                PathSeg::Line(l) => path.line_to(l.p1),
+                PathSeg::Quad(q) => path.quad_to(q.p1, q.p2),
+                PathSeg::Cubic(c) => path.curve_to(c.p1, c.p2, c.p3),
+            }
+            if element.is_split || i == len {
+                // This gets color and width from the last element, original
+                // gets it from the first, but this should not matter.
+                ctx.stroke(&path, &element.color, element.width);
+                path = BezPath::new(); // Should have clear method, to avoid allocations.
+            }
+            if rng.gen::<f32>() > 0.995 {
+                element.is_split ^= true;
+            }
+        }
+    }
+}
+
+const COLORS: &[Color] = &[
+    Color::rgb8(0x10, 0x10, 0x10),
+    Color::rgb8(0x80, 0x80, 0x80),
+    Color::rgb8(0xc0, 0xc0, 0xc0),
+    Color::rgb8(0x10, 0x10, 0x10),
+    Color::rgb8(0x80, 0x80, 0x80),
+    Color::rgb8(0xc0, 0xc0, 0xc0),
+    Color::rgb8(0xe0, 0x10, 0x40),
+];
+
+impl Element {
+    fn new_rand(last: GridPoint) -> Element {
+        let mut rng = rand::thread_rng();
+        let seg_type = rng.gen_range(0, 4);
+        let next = GridPoint::random_point(last);
+        let (grid_point, seg) = if seg_type < 2 {
+            (next, PathSeg::Line(Line::new(last.coordinate(), next.coordinate())))
+        } else if seg_type < 3 {
+            let p2 = GridPoint::random_point(next);
+            (p2, PathSeg::Quad(QuadBez::new(last.coordinate(), next.coordinate(), p2.coordinate())))
+        } else {
+            let p2 = GridPoint::random_point(next);
+            let p3 = GridPoint::random_point(next);
+            (p3, PathSeg::Cubic(CubicBez::new(last.coordinate(), next.coordinate(), p2.coordinate(), p3.coordinate())))
+        };
+        let color = COLORS.choose(&mut rng).unwrap().clone();
+        let width = rng.gen::<f64>().powi(5) * 20.0 + 1.0;
+        let is_split = rng.gen();
+        Element { seg, color, width, is_split, grid_point }
+    }
+}
+
+const OFFSETS: &[(i64, i64)] = &[(-4, 0), (2, 0), (1, -2), (1, 2)];
+
+impl GridPoint {
+    fn random_point(last: GridPoint) -> GridPoint {
+        let mut rng = rand::thread_rng();
+
+        let offset = OFFSETS.choose(&mut rng).unwrap();
+        let mut x = last.0.0 + offset.0;
+        if x < 0 || x > GRID_WIDTH {
+            x -= offset.0 * 2;
+        }
+        let mut y = last.0.1 + offset.1;
+        if y < 0 || y > GRID_HEIGHT {
+            y -= offset.1 * 2;
+        }
+        GridPoint((x, y))
+    }
+
+    fn coordinate(&self) -> Point {
+        let scale_x = WIDTH as f64 / ((GRID_WIDTH + 1) as f64);
+        let scale_y = HEIGHT as f64 / ((GRID_HEIGHT + 1) as f64);
+        Point::new((self.0.0 as f64 + 0.5) * scale_x, 100.0 + (self.0.1 as f64 + 0.5) * scale_y)
+    }
+}