| // Copyright 2023 the Vello Authors | |
| // SPDX-License-Identifier: Apache-2.0 OR MIT | |
| //! A benchmark based on MotionMark 1.2's path benchmark. | |
| //! This is roughly comparable to: | |
| //! | |
| //! <https://browserbench.org/MotionMark1.2/developer.html?warmup-length=2000&warmup-frame-count=30&first-frame-minimum-length=0&test-interval=15&display=minimal&tiles=big&controller=adaptive&frame-rate=50&time-measurement=performance&suite-name=MotionMark&test-name=Paths&complexity=1> | |
| //! | |
| //! However, at this point it cannot be directly compared, as we don't accurately | |
| //! implement the stroke style parameters, and it has not been carefully validated. | |
| use std::cmp::Ordering; | |
| use rand::Rng; | |
| use rand::seq::IndexedRandom; | |
| use vello::Scene; | |
| use vello::kurbo::{Affine, BezPath, CubicBez, Line, ParamCurve, PathSeg, Point, QuadBez, Stroke}; | |
| use vello::peniko::Color; | |
| use crate::{SceneParams, TestScene}; | |
| 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) -> Self { | |
| let mut result = Self { elements: vec![] }; | |
| result.resize(n); | |
| result | |
| } | |
| fn resize(&mut self, n: usize) { | |
| let old_n = self.elements.len(); | |
| match n.cmp(&old_n) { | |
| Ordering::Less => self.elements.truncate(n), | |
| Ordering::Greater => { | |
| let mut last = self | |
| .elements | |
| .last() | |
| .map(|e| e.grid_point) | |
| .unwrap_or(GridPoint(GRID_WIDTH / 2, GRID_HEIGHT / 2)); | |
| self.elements.extend((old_n..n).map(|_| { | |
| let element = Element::new_rand(last); | |
| last = element.grid_point; | |
| element | |
| })); | |
| } | |
| _ => (), | |
| } | |
| } | |
| } | |
| impl TestScene for MMark { | |
| fn render(&mut self, scene: &mut Scene, params: &mut SceneParams<'_>) { | |
| let c = params.complexity; | |
| let n = if c < 10 { | |
| (c + 1) * 1000 | |
| } else { | |
| ((c - 8) * 10000).min(120_000) | |
| }; | |
| self.resize(n); | |
| let mut rng = rand::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. | |
| scene.stroke( | |
| &Stroke::new(element.width), | |
| Affine::IDENTITY, | |
| element.color, | |
| None, | |
| &path, | |
| ); | |
| path.truncate(0); // Should have clear method, to avoid allocations. | |
| } | |
| if rng.random::<f32>() > 0.995 { | |
| element.is_split ^= true; | |
| } | |
| } | |
| let label = format!("mmark test: {n} path elements (up/down to adjust)"); | |
| params.text.add( | |
| scene, | |
| None, | |
| 40.0, | |
| None, | |
| Affine::translate((100.0, 1100.0)), | |
| &label, | |
| ); | |
| } | |
| } | |
| const COLORS: &[Color] = &[ | |
| Color::from_rgb8(0x10, 0x10, 0x10), | |
| Color::from_rgb8(0x80, 0x80, 0x80), | |
| Color::from_rgb8(0xc0, 0xc0, 0xc0), | |
| Color::from_rgb8(0x10, 0x10, 0x10), | |
| Color::from_rgb8(0x80, 0x80, 0x80), | |
| Color::from_rgb8(0xc0, 0xc0, 0xc0), | |
| Color::from_rgb8(0xe0, 0x10, 0x40), | |
| ]; | |
| impl Element { | |
| fn new_rand(last: GridPoint) -> Self { | |
| let mut rng = rand::rng(); | |
| let seg_type = rng.random_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(); | |
| let width = rng.random::<f64>().powi(5) * 20.0 + 1.0; | |
| let is_split = rng.random(); | |
| Self { | |
| 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: Self) -> Self { | |
| let mut rng = rand::rng(); | |
| let offset = OFFSETS.choose(&mut rng).unwrap(); | |
| let mut x = last.0 + offset.0; | |
| if !(0..=GRID_WIDTH).contains(&x) { | |
| x -= offset.0 * 2; | |
| } | |
| let mut y = last.1 + offset.1; | |
| if !(0..=GRID_HEIGHT).contains(&y) { | |
| y -= offset.1 * 2; | |
| } | |
| Self(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 as f64 + 0.5) * scale_x, | |
| 100.0 + (self.1 as f64 + 0.5) * scale_y, | |
| ) | |
| } | |
| } |