diff --git a/examples/headless/src/main.rs b/examples/headless/src/main.rs index a6ddb0f..5a49d05 100644 --- a/examples/headless/src/main.rs +++ b/examples/headless/src/main.rs @@ -104,8 +104,11 @@ async fn render(mut scenes: SceneSet, index: usize, args: &Args) -> Result<()> { resolution: None, base_color: None, interactive: false, + complexity: 0, }; - (example_scene.function)(&mut builder, &mut scene_params); + example_scene + .function + .render(&mut builder, &mut scene_params); let mut transform = Affine::IDENTITY; let (width, height) = if let Some(resolution) = scene_params.resolution { let ratio = resolution.x / resolution.y; diff --git a/examples/scenes/Cargo.toml b/examples/scenes/Cargo.toml index c68ffa3..fd5e641 100644 --- a/examples/scenes/Cargo.toml +++ b/examples/scenes/Cargo.toml @@ -16,6 +16,7 @@ vello_svg = { path = "../../integrations/vello_svg" } anyhow = { workspace = true } clap = { workspace = true, features = ["derive"] } image = "0.24.5" +rand = "0.8.5" instant = { workspace = true } # Used for the `download` command diff --git a/examples/scenes/src/lib.rs b/examples/scenes/src/lib.rs index 2201497..6cea26d 100644 --- a/examples/scenes/src/lib.rs +++ b/examples/scenes/src/lib.rs @@ -1,5 +1,6 @@ pub mod download; mod images; +mod mmark; mod simple_text; mod svg; mod test_scenes; @@ -25,6 +26,7 @@ pub struct SceneParams<'a> { pub images: &'a mut ImageCache, pub resolution: Option, pub base_color: Option, + pub complexity: usize, } pub struct SceneConfig { @@ -34,11 +36,20 @@ pub struct SceneConfig { } pub struct ExampleScene { - #[allow(clippy::type_complexity)] - pub function: Box, + pub function: Box, pub config: SceneConfig, } +pub trait TestScene { + fn render(&mut self, sb: &mut SceneBuilder, params: &mut SceneParams); +} + +impl TestScene for F { + fn render(&mut self, sb: &mut SceneBuilder, params: &mut SceneParams) { + self(sb, params); + } +} + pub struct SceneSet { pub scenes: Vec, } diff --git a/examples/scenes/src/mmark.rs b/examples/scenes/src/mmark.rs new file mode 100644 index 0000000..c7e439a --- /dev/null +++ b/examples/scenes/src/mmark.rs @@ -0,0 +1,201 @@ +//! 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::{seq::SliceRandom, Rng}; +use vello::peniko::Color; +use vello::{ + kurbo::{Affine, BezPath, CubicBez, Line, ParamCurve, PathSeg, Point, QuadBez}, + peniko::Stroke, + SceneBuilder, +}; + +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, +} + +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 result = MMark { 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, sb: &mut SceneBuilder, 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::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. + sb.stroke( + &Stroke::new(element.width as f32), + Affine::IDENTITY, + element.color, + None, + &path, + ); + path.truncate(0); // Should have clear method, to avoid allocations. + } + if rng.gen::() > 0.995 { + element.is_split ^= true; + } + } + let label = format!("mmark test: {} path elements (up/down to adjust)", n); + params.text.add( + sb, + None, + 40.0, + None, + Affine::translate((100.0, 1100.0)), + &label, + ); + } +} + +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(); + let width = rng.gen::().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 + 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; + } + 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 as f64 + 0.5) * scale_x, + 100.0 + (self.1 as f64 + 0.5) * scale_y, + ) + } +} diff --git a/examples/scenes/src/test_scenes.rs b/examples/scenes/src/test_scenes.rs index 040411c..ab3ee1e 100644 --- a/examples/scenes/src/test_scenes.rs +++ b/examples/scenes/src/test_scenes.rs @@ -24,9 +24,23 @@ macro_rules! scene { } pub fn test_scenes() -> SceneSet { - // For WASM below, must be mutable - #[allow(unused_mut)] - let mut scenes = vec![ + let splash_scene = ExampleScene { + config: SceneConfig { + animated: false, + name: "splash_with_tiger".to_owned(), + }, + function: Box::new(splash_with_tiger()), + }; + let mmark_scene = ExampleScene { + config: SceneConfig { + animated: false, + name: "mmark".to_owned(), + }, + function: Box::new(crate::mmark::MMark::new(80_000)), + }; + let scenes = vec![ + splash_scene, + mmark_scene, scene!(funky_paths), scene!(cardioid_and_friends), scene!(animated_text: animated), @@ -38,14 +52,6 @@ pub fn test_scenes() -> SceneSet { scene!(labyrinth), scene!(base_color_test: animated), ]; - #[cfg(any(target_arch = "wasm32", target_os = "android"))] - scenes.push(ExampleScene { - config: SceneConfig { - animated: false, - name: "included_tiger".to_owned(), - }, - function: Box::new(included_tiger()), - }); SceneSet { scenes } } @@ -491,15 +497,6 @@ fn blend_grid(sb: &mut SceneBuilder, _: &mut SceneParams) { } } -#[cfg(any(target_arch = "wasm32", target_os = "android"))] -fn included_tiger() -> impl FnMut(&mut SceneBuilder, &mut SceneParams) { - let contents = include_str!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/../assets/Ghostscript_Tiger.svg" - )); - crate::svg::svg_function_of("Ghostscript Tiger".to_string(), move || contents) -} - // Support functions fn render_cardioid(sb: &mut SceneBuilder) { @@ -841,3 +838,39 @@ fn make_diamond(cx: f64, cy: f64) -> [PathEl; 5] { PathEl::ClosePath, ] } + +fn splash_screen(sb: &mut SceneBuilder, params: &mut SceneParams) { + let strings = [ + "Vello test", + " Arrow keys: switch scenes", + " Space: reset transform", + " S: toggle stats", + " V: toggle vsync", + " Q, E: rotate", + ]; + // Tweak to make it fit with tiger + let a = Affine::scale(0.12) * Affine::translate((-90.0, -50.0)); + for (i, s) in strings.iter().enumerate() { + let text_size = if i == 0 { 60.0 } else { 40.0 }; + params.text.add( + sb, + None, + text_size, + None, + a * Affine::translate((100.0, 100.0 + 60.0 * i as f64)), + s, + ); + } +} + +fn splash_with_tiger() -> impl FnMut(&mut SceneBuilder, &mut SceneParams) { + let contents = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../assets/Ghostscript_Tiger.svg" + )); + let mut tiger = crate::svg::svg_function_of("Ghostscript Tiger".to_string(), move || contents); + move |sb, params| { + tiger(sb, params); + splash_screen(sb, params); + } +} diff --git a/examples/with_winit/Cargo.toml b/examples/with_winit/Cargo.toml index 3303388..24c157e 100644 --- a/examples/with_winit/Cargo.toml +++ b/examples/with_winit/Cargo.toml @@ -45,4 +45,4 @@ android_logger = "0.13.0" console_error_panic_hook = "0.1.7" console_log = "0.2" wasm-bindgen-futures = "0.4.33" -web-sys = "0.3.60" +web-sys = { version = "0.3.60", features = [ "HtmlCollection", "Text" ] } diff --git a/examples/with_winit/src/lib.rs b/examples/with_winit/src/lib.rs index 312c64f..8a5dacc 100644 --- a/examples/with_winit/src/lib.rs +++ b/examples/with_winit/src/lib.rs @@ -117,6 +117,7 @@ fn run( let mut prior_position: Option = None; // We allow looping left and right through the scenes, so use a signed index let mut scene_ix: i32 = 0; + let mut complexity: usize = 0; if let Some(set_scene) = args.scene { scene_ix = set_scene; } @@ -138,6 +139,8 @@ fn run( match input.virtual_keycode { Some(VirtualKeyCode::Left) => scene_ix = scene_ix.saturating_sub(1), Some(VirtualKeyCode::Right) => scene_ix = scene_ix.saturating_add(1), + Some(VirtualKeyCode::Up) => complexity += 1, + Some(VirtualKeyCode::Down) => complexity = complexity.saturating_sub(1), Some(key @ VirtualKeyCode::Q) | Some(key @ VirtualKeyCode::E) => { if let Some(prior_position) = prior_position { let is_clockwise = key == VirtualKeyCode::E; @@ -302,8 +305,11 @@ fn run( resolution: None, base_color: None, interactive: true, + complexity, }; - (example_scene.function)(&mut builder, &mut scene_params); + example_scene + .function + .render(&mut builder, &mut scene_params); // If the user specifies a base color in the CLI we use that. Otherwise we use any // color specified by the scene. The default is black. @@ -421,7 +427,7 @@ fn run( let size = window.inner_size(); let surface_future = render_cx.create_surface(&window, size.width, size.height); // We need to block here, in case a Suspended event appeared - let surface = pollster::block_on(surface_future); + let surface = pollster::block_on(surface_future).expect("Error creating surface"); render_state = { let render_state = RenderState { window, surface }; renderers.resize_with(render_cx.devices.len(), || None); @@ -461,6 +467,27 @@ enum UserEvent { HotReload, } +#[cfg(target_arch = "wasm32")] +fn display_error_message() -> Option<()> { + let window = web_sys::window()?; + let document = window.document()?; + let elements = document.get_elements_by_tag_name("body"); + let body = elements.item(0)?; + body.set_inner_html( + r#" +

WebGPU + is not enabled. Make sure your browser is updated to + Chrome M113 or + another browser compatible with WebGPU.

"#, + ); + Some(()) +} + pub fn main() -> Result<()> { // TODO: initializing both env_logger and console_logger fails on wasm. // Figure out a more principled approach. @@ -497,16 +524,21 @@ pub fn main() -> Result<()> { web_sys::window() .and_then(|win| win.document()) .and_then(|doc| doc.body()) - .and_then(|body| body.append_child(&web_sys::Element::from(canvas)).ok()) + .and_then(|body| body.append_child(canvas.as_ref()).ok()) .expect("couldn't append canvas to document body"); + _ = web_sys::HtmlElement::from(canvas).focus(); wasm_bindgen_futures::spawn_local(async move { let size = window.inner_size(); let surface = render_cx .create_surface(&window, size.width, size.height) .await; - let render_state = RenderState { window, surface }; - // No error handling here; if the event loop has finished, we don't need to send them the surface - run(event_loop, args, scenes, render_cx, render_state); + if let Ok(surface) = surface { + let render_state = RenderState { window, surface }; + // No error handling here; if the event loop has finished, we don't need to send them the surface + run(event_loop, args, scenes, render_cx, render_state); + } else { + _ = display_error_message(); + } }); } } diff --git a/src/util.rs b/src/util.rs index 87e2d35..add0ea2 100644 --- a/src/util.rs +++ b/src/util.rs @@ -50,12 +50,20 @@ impl RenderContext { } /// Creates a new surface for the specified window and dimensions. - pub async fn create_surface(&mut self, window: &W, width: u32, height: u32) -> RenderSurface + pub async fn create_surface( + &mut self, + window: &W, + width: u32, + height: u32, + ) -> Result where W: HasRawWindowHandle + HasRawDisplayHandle, { - let surface = unsafe { self.instance.create_surface(window) }.unwrap(); - let dev_id = self.device(Some(&surface)).await.unwrap(); + let surface = unsafe { self.instance.create_surface(window) }?; + let dev_id = self + .device(Some(&surface)) + .await + .ok_or("Error creating device")?; let device_handle = &self.devices[dev_id]; let capabilities = surface.get_capabilities(&device_handle.adapter); @@ -75,12 +83,12 @@ impl RenderContext { view_formats: vec![], }; surface.configure(&self.devices[dev_id].device, &config); - RenderSurface { + Ok(RenderSurface { surface, config, dev_id, format, - } + }) } /// Resizes the surface to the new dimensions.