Merge pull request #314 from linebender/failure

A bit of polishing on the demo
This commit is contained in:
Raph Levien 2023-06-01 07:25:04 -07:00 committed by GitHub
commit 03545e5d9a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 324 additions and 35 deletions

View file

@ -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;

View file

@ -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

View file

@ -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<Vec2>,
pub base_color: Option<vello::peniko::Color>,
pub complexity: usize,
}
pub struct SceneConfig {
@ -34,11 +36,20 @@ pub struct SceneConfig {
}
pub struct ExampleScene {
#[allow(clippy::type_complexity)]
pub function: Box<dyn FnMut(&mut SceneBuilder, &mut SceneParams)>,
pub function: Box<dyn TestScene>,
pub config: SceneConfig,
}
pub trait TestScene {
fn render(&mut self, sb: &mut SceneBuilder, params: &mut SceneParams);
}
impl<F: FnMut(&mut SceneBuilder, &mut SceneParams)> TestScene for F {
fn render(&mut self, sb: &mut SceneBuilder, params: &mut SceneParams) {
self(sb, params);
}
}
pub struct SceneSet {
pub scenes: Vec<ExampleScene>,
}

View file

@ -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<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 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::<f32>() > 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::<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 + 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,
)
}
}

View file

@ -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);
}
}

View file

@ -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" ] }

View file

@ -117,6 +117,7 @@ fn run(
let mut prior_position: Option<Vec2> = 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#"<style>
p {
margin: 2em 10em;
font-family: sans-serif;
}
</style>
<p><a href="https://caniuse.com/webgpu">WebGPU</a>
is not enabled. Make sure your browser is updated to
<a href="https://chromiumdash.appspot.com/schedule">Chrome M113</a> or
another browser compatible with WebGPU.</p>"#,
);
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();
}
});
}
}

View file

@ -50,12 +50,20 @@ impl RenderContext {
}
/// Creates a new surface for the specified window and dimensions.
pub async fn create_surface<W>(&mut self, window: &W, width: u32, height: u32) -> RenderSurface
pub async fn create_surface<W>(
&mut self,
window: &W,
width: u32,
height: u32,
) -> Result<RenderSurface>
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.