diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b632ab..9b311f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,27 @@ Subheadings to categorize changes are `added, changed, deprecated, removed, fixe ## Unreleased +## 0.3.0 + +### Added + +- Added `vello_svg::Error`, which is returned by new functions that read text into a `usvg::Tree`. +- Added `vello_svg::render`, which takes an svg string and renders to a new vello scene. +- Added `vello_svg::append`, which takes an svg string and renders to a provided vello scene. +- Added `vello_svg::append_with`, which takes an svg string and renders to a provided vello scene with and error handler. +- Added `vello_svg::render_tree`, which takes a usvg::Tree and renders to a provided vello scene with and error handler. + +### Changed + +- Updated to vello 0.2 +- Updated to usvg 0.42 +- Renamed `render_tree` to `append_tree` +- Renamed `render_tree_with` to `append_tree_with` and removed the `Result<(), E>` return type for the error handler. + +### Removed + +- All code and related profiling (`wgpu_profiler`) used in examples. + ## 0.2.0 ### Added diff --git a/Cargo.toml b/Cargo.toml index 4796115..828c5e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,13 +4,13 @@ members = ["examples/with_winit", "examples/run_wasm", "examples/scenes"] [workspace.package] edition = "2021" -version = "0.2.0" +version = "0.3.0" license = "Apache-2.0 OR MIT" repository = "https://github.com/linebender/vello_svg" [workspace.dependencies] # NOTE: Make sure to keep this in sync with the version badge in README.md -vello = { version = "0.1.0", default-features = false } +vello = { version = "0.2.0", default-features = false } [package] name = "vello_svg" @@ -24,14 +24,14 @@ repository.workspace = true [dependencies] vello = { workspace = true } -usvg = "0.41.0" +thiserror = "1.0.61" +usvg = "0.42.0" image = { version = "0.25.0", default-features = false, features = [ "png", "jpeg", "gif", ] } - [target.'cfg(target_arch = "wasm32")'.dev-dependencies] wasm-bindgen-test = "0.3.42" diff --git a/README.md b/README.md index efa1adf..40c4294 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,7 @@ [![Linebender Zulip](https://img.shields.io/badge/Linebender-%23gpu-blue?logo=Zulip)](https://xi.zulipchat.com/#narrow/stream/197075-gpu) [![dependency status](https://deps.rs/repo/github/linebender/vello_svg/status.svg)](https://deps.rs/repo/github/linebender/vello_svg) [![MIT/Apache 2.0](https://img.shields.io/badge/license-MIT%2FApache-blue.svg)](#license) -[![vello version](https://img.shields.io/badge/vello-v0.1.0-purple.svg)](https://crates.io/crates/vello) - +[![vello version](https://img.shields.io/badge/vello-v0.2.0-purple.svg)](https://crates.io/crates/vello)\ [![Crates.io](https://img.shields.io/crates/v/vello_svg.svg)](https://crates.io/crates/vello_svg) [![Docs](https://docs.rs/vello_svg/badge.svg)](https://docs.rs/vello_svg) [![Build status](https://github.com/linebender/vello_svg/workflows/CI/badge.svg)](https://github.com/linebender/vello_svg/actions) diff --git a/examples/run_wasm/Cargo.toml b/examples/run_wasm/Cargo.toml index 76068dd..6a8b1ee 100644 --- a/examples/run_wasm/Cargo.toml +++ b/examples/run_wasm/Cargo.toml @@ -6,4 +6,4 @@ repository.workspace = true publish = false [dependencies] -cargo-run-wasm = "0.3.2" +cargo-run-wasm = "0.4.0" diff --git a/examples/run_wasm/src/main.rs b/examples/run_wasm/src/main.rs index 41d8eb0..cfa6c23 100644 --- a/examples/run_wasm/src/main.rs +++ b/examples/run_wasm/src/main.rs @@ -13,5 +13,5 @@ /// ``` fn main() { - cargo_run_wasm::run_wasm_with_css("body { margin: 0px; }"); + cargo_run_wasm::run_wasm_cli_with_css("body { margin: 0px; }"); } diff --git a/examples/scenes/src/svg.rs b/examples/scenes/src/svg.rs index 3bb1967..ad87422 100644 --- a/examples/scenes/src/svg.rs +++ b/examples/scenes/src/svg.rs @@ -93,16 +93,14 @@ pub fn svg_function_of>( ) -> impl FnMut(&mut Scene, &mut SceneParams) { fn render_svg_contents(name: &str, contents: &str) -> (Scene, Vec2) { let start = Instant::now(); - let fontdb = usvg::fontdb::Database::new(); - let svg = usvg::Tree::from_str(contents, &usvg::Options::default(), &fontdb) + let svg = usvg::Tree::from_str(contents, &usvg::Options::default()) .unwrap_or_else(|e| panic!("failed to parse svg file {name}: {e}")); eprintln!("Parsed svg {name} in {:?}", start.elapsed()); let start = Instant::now(); - let mut new_scene = Scene::new(); - vello_svg::render_tree(&mut new_scene, &svg); + let scene = vello_svg::render_tree(&svg); let resolution = Vec2::new(svg.size().width() as f64, svg.size().height() as f64); eprintln!("Encoded svg {name} in {:?}", start.elapsed()); - (new_scene, resolution) + (scene, resolution) } let mut cached_scene = None; #[cfg(not(target_arch = "wasm32"))] diff --git a/examples/with_winit/Cargo.toml b/examples/with_winit/Cargo.toml index c73e9aa..4cc638d 100644 --- a/examples/with_winit/Cargo.toml +++ b/examples/with_winit/Cargo.toml @@ -17,20 +17,18 @@ name = "with_winit_bin" path = "src/main.rs" [dependencies] -vello = { workspace = true, features = ["buffer_labels", "wgpu", "wgpu-profiler"] } +vello = { workspace = true, features = ["buffer_labels", "wgpu"] } scenes = { path = "../scenes" } anyhow = "1" clap = { version = "4.5.1", features = ["derive"] } instant = { version = "0.1.12", features = ["wasm-bindgen"] } pollster = "0.3" -wgpu-profiler = "0.16" -wgpu = "0.19.3" winit = "0.29.12" env_logger = "0.11.2" log = "0.4.21" [target.'cfg(not(any(target_arch = "wasm32", target_os = "android")))'.dependencies] -vello = { workspace = true, features = ["hot_reload", "wgpu", "wgpu-profiler"] } +vello = { workspace = true, features = ["hot_reload", "wgpu"] } notify-debouncer-mini = "0.3" [target.'cfg(target_os = "android")'.dependencies] diff --git a/examples/with_winit/src/lib.rs b/examples/with_winit/src/lib.rs index 534b584..f80accf 100644 --- a/examples/with_winit/src/lib.rs +++ b/examples/with_winit/src/lib.rs @@ -1,7 +1,7 @@ // Copyright 2022 the Vello Authors // SPDX-License-Identifier: Apache-2.0 OR MIT -use instant::{Duration, Instant}; +use instant::Instant; use std::collections::HashSet; use std::num::NonZeroUsize; use std::sync::Arc; @@ -12,7 +12,7 @@ use scenes::{RobotoText, SceneParams, SceneSet}; use vello::kurbo::{Affine, Vec2}; use vello::peniko::Color; use vello::util::{RenderContext, RenderSurface}; -use vello::{AaConfig, BumpAllocators, Renderer, RendererOptions, Scene}; +use vello::{wgpu, AaConfig, BumpAllocators, Renderer, RendererOptions, Scene}; use winit::event_loop::{EventLoop, EventLoopBuilder}; use winit::window::Window; @@ -83,7 +83,7 @@ fn run( let mut render_state = { renderers.resize_with(render_cx.devices.len(), || None); let id = render_state.surface.dev_id; - let mut renderer = Renderer::new( + let renderer = Renderer::new( &render_cx.devices[id].device, RendererOptions { surface_format: Some(render_state.surface.format), @@ -95,14 +95,6 @@ fn run( }, ) .expect("Could create renderer"); - renderer - .profiler - .change_settings(wgpu_profiler::GpuProfilerSettings { - enable_timer_queries: false, - enable_debug_groups: false, - ..Default::default() - }) - .expect("Not setting max_num_pending_frames"); renderers[id] = Some(renderer); Some(render_state) }; @@ -145,9 +137,7 @@ fn run( if let Some(set_scene) = args.scene { scene_ix = set_scene; } - let mut profile_stored = None; let mut prev_scene_ix = scene_ix - 1; - let mut profile_taken = Instant::now(); let mut modifiers = ModifiersState::default(); event_loop .run(move |event, event_loop| match event { @@ -211,32 +201,6 @@ fn run( aa_config_ix.saturating_add(1) }; } - "p" => { - if let Some(renderer) = &renderers[render_state.surface.dev_id] - { - if let Some(profile_result) = &renderer - .profile_result - .as_ref() - .or(profile_stored.as_ref()) - { - // There can be empty results if the required features aren't supported - if !profile_result.is_empty() { - let path = std::path::Path::new("trace.json"); - match wgpu_profiler::chrometrace::write_chrometrace( - path, - profile_result, - ) { - Ok(()) => { - println!("Wrote trace to path {path:?}"); - } - Err(e) => { - eprintln!("Failed to write trace {e}") - } - } - } - } - } - } "v" => { vsync_on = !vsync_on; render_cx.set_present_mode( @@ -409,26 +373,6 @@ fn run( vsync_on, antialiasing_method, ); - if let Some(profiling_result) = renderers[render_state.surface.dev_id] - .as_mut() - .and_then(|it| it.profile_result.take()) - { - if profile_stored.is_none() - || profile_taken.elapsed() > Duration::from_secs(1) - { - profile_stored = Some(profiling_result); - profile_taken = Instant::now(); - } - } - if let Some(profiling_result) = profile_stored.as_ref() { - stats::draw_gpu_profiling( - &mut scene, - scene_params.text, - width as f64, - height as f64, - profiling_result, - ); - } } let surface_texture = render_state .surface @@ -534,7 +478,12 @@ fn run( .take() .unwrap_or_else(|| create_window(event_loop)); let size = window.inner_size(); - let surface_future = render_cx.create_surface(window.clone(), size.width, size.height, wgpu::PresentMode::AutoVsync); + let surface_future = render_cx.create_surface( + window.clone(), + size.width, + size.height, + wgpu::PresentMode::AutoVsync, + ); // We need to block here, in case a Suspended event appeared let surface = pollster::block_on(surface_future).expect("Error creating surface"); @@ -550,7 +499,7 @@ fn run( surface_format: Some(render_state.surface.format), use_cpu, antialiasing_support: vello::AaSupport::all(), - num_init_threads: NonZeroUsize::new(args.num_init_threads) + num_init_threads: NonZeroUsize::new(args.num_init_threads), }, ) .expect("Could create renderer"); @@ -617,7 +566,7 @@ pub fn main() -> Result<()> { if let Some(scenes) = scenes { let event_loop = EventLoopBuilder::::with_user_event().build()?; #[allow(unused_mut)] - let mut render_cx = RenderContext::new().unwrap(); + let mut render_cx = RenderContext::new(); #[cfg(not(target_arch = "wasm32"))] { #[cfg(not(target_os = "android"))] @@ -701,7 +650,7 @@ fn android_main(app: AndroidApp) { .select_scene_set(|| Args::command()) .unwrap() .unwrap(); - let render_cx = RenderContext::new().unwrap(); + let render_cx = RenderContext::new(); run(event_loop, args, scenes, render_cx); } diff --git a/examples/with_winit/src/multi_touch.rs b/examples/with_winit/src/multi_touch.rs index 29fb966..25b8a58 100644 --- a/examples/with_winit/src/multi_touch.rs +++ b/examples/with_winit/src/multi_touch.rs @@ -1,7 +1,7 @@ // Copyright 2021 the egui Authors and the Vello Authors // SPDX-License-Identifier: Apache-2.0 OR MIT -/// Adapted from https://github.com/emilk/egui/blob/212656f3fc6b931b21eaad401e5cec2b0da93baa/crates/egui/src/input_state/touch_state.rs +/// Adapted from use std::{collections::BTreeMap, fmt::Debug}; use vello::kurbo::{Point, Vec2}; @@ -10,8 +10,8 @@ use winit::event::{Touch, TouchPhase}; /// All you probably need to know about a multi-touch gesture. #[derive(Clone, Copy, Debug, PartialEq)] pub struct MultiTouchInfo { - /// Number of touches (fingers) on the surface. Value is ≥ 2 since for a - /// single touch no [`MultiTouchInfo`] is created. + /// Number of touches (fingers) on the surface. Value is ≥ 2 since for a single touch no + /// [`MultiTouchInfo`] is created. pub num_touches: usize, /// Proportional zoom factor (pinch gesture). @@ -31,42 +31,36 @@ pub struct MultiTouchInfo { /// * `zoom > 1`: pinch spread pub zoom_delta_2d: Vec2, - /// Rotation in radians. Moving fingers around each other will change this - /// value. This is a relative value, comparing the orientation of - /// fingers in the current frame with the previous frame. If all - /// fingers are resting, this value is `0.0`. + /// Rotation in radians. Moving fingers around each other will change this value. This is a + /// relative value, comparing the orientation of fingers in the current frame with the previous + /// frame. If all fingers are resting, this value is `0.0`. pub rotation_delta: f64, - /// Relative movement (comparing previous frame and current frame) of the - /// average position of all touch points. Without movement this value - /// is `Vec2::ZERO`. + /// Relative movement (comparing previous frame and current frame) of the average position of + /// all touch points. Without movement this value is `Vec2::ZERO`. /// - /// Note that this may not necessarily be measured in screen points - /// (although it _will_ be for most mobile devices). In general - /// (depending on the touch device), touch coordinates cannot - /// be directly mapped to the screen. A touch always is considered to start - /// at the position of the pointer, but touch movement is always - /// measured in the units delivered by the device, and may depend on - /// hardware and system settings. + /// Note that this may not necessarily be measured in screen points (although it _will_ be for + /// most mobile devices). In general (depending on the touch device), touch coordinates cannot + /// be directly mapped to the screen. A touch always is considered to start at the position of + /// the pointer, but touch movement is always measured in the units delivered by the device, + /// and may depend on hardware and system settings. pub translation_delta: Vec2, pub zoom_centre: Point, } -/// The current state (for a specific touch device) of touch events and -/// gestures. +/// The current state (for a specific touch device) of touch events and gestures. #[derive(Clone)] pub(crate) struct TouchState { /// Active touches, if any. /// - /// TouchId is the unique identifier of the touch. It is valid as long as - /// the finger/pen touches the surface. The next touch will receive a - /// new unique ID. + /// Touch id is the unique identifier of the touch. It is valid as long as the finger/pen + /// touches the surface. The next touch will receive a new unique id. /// /// Refer to [`ActiveTouch`]. active_touches: BTreeMap, - /// If a gesture has been recognized (i.e. when exactly two fingers touch - /// the surface), this holds state information + /// If a gesture has been recognized (i.e. when exactly two fingers touch the surface), this + /// holds state information gesture_state: Option, added_or_removed_touches: bool, @@ -90,12 +84,11 @@ struct DynGestureState { heading: f64, } -/// Describes an individual touch (finger or digitizer) on the touch surface. -/// Instances exist as long as the finger/pen touches the surface. +/// Describes an individual touch (finger or digitizer) on the touch surface. Instances exist as +/// long as the finger/pen touches the surface. #[derive(Clone, Copy, Debug)] struct ActiveTouch { - /// Current position of this touch, in device coordinates (not necessarily - /// screen position) + /// Current position of this touch, in device coordinates (not necessarily screen position) pos: Point, } @@ -128,15 +121,13 @@ impl TouchState { } pub fn end_frame(&mut self) { - // This needs to be called each frame, even if there are no new touch - // events. Otherwise, we would send the same old delta - // information multiple times: + // This needs to be called each frame, even if there are no new touch events. + // Otherwise, we would send the same old delta information multiple times: self.update_gesture(); if self.added_or_removed_touches { - // Adding or removing fingers makes the average values "jump". We - // better forget about the previous values, and don't - // create delta information for this frame: + // Adding or removing fingers makes the average values "jump". We better forget + // about the previous values, and don't create delta information for this frame: if let Some(ref mut state) = &mut self.gesture_state { state.previous = None; } @@ -146,10 +137,9 @@ impl TouchState { pub fn info(&self) -> Option { self.gesture_state.as_ref().map(|state| { - // state.previous can be `None` when the number of simultaneous - // touches has just changed. In this case, we take - // `current` as `previous`, pretending that there was no - // change for the current frame. + // state.previous can be `None` when the number of simultaneous touches has just + // changed. In this case, we take `current` as `previous`, pretending that there + // was no change for the current frame. let state_previous = state.previous.unwrap_or(state.current); let zoom_delta = if self.active_touches.len() > 1 { @@ -237,18 +227,15 @@ impl TouchState { state.avg_abs_distance2 *= num_touches_recip; // Calculate the direction from the first touch to the center position. - // This is not the perfect way of calculating the direction if more than - // two fingers are involved, but as long as all fingers rotate - // more or less at the same angular velocity, the shortcomings - // of this method will not be noticed. One can see the - // issues though, when touching with three or more fingers, and moving - // only one of them (it takes two hands to do this in a - // controlled manner). A better technique would be to store the - // current and previous directions (with reference to the center) for - // each touch individually, and then calculate the average of - // all individual changes in direction. But this approach cannot - // be implemented locally in this method, making everything a - // bit more complicated. + // This is not the perfect way of calculating the direction if more than two fingers + // are involved, but as long as all fingers rotate more or less at the same angular + // velocity, the shortcomings of this method will not be noticed. One can see the + // issues though, when touching with three or more fingers, and moving only one of them + // (it takes two hands to do this in a controlled manner). A better technique would be + // to store the current and previous directions (with reference to the center) for each + // touch individually, and then calculate the average of all individual changes in + // direction. But this approach cannot be implemented locally in this method, making + // everything a bit more complicated. let first_touch = self.active_touches.values().next().unwrap(); state.heading = (state.avg_pos - first_touch.pos).atan2(); @@ -277,12 +264,11 @@ enum PinchType { impl PinchType { fn classify(touches: &BTreeMap) -> Self { // For non-proportional 2d zooming: - // If the user is pinching with two fingers that have roughly the same Y - // coord, then the Y zoom is unstable and should be 1. + // If the user is pinching with two fingers that have roughly the same Y coord, + // then the Y zoom is unstable and should be 1. // Similarly, if the fingers are directly above/below each other, // we should only zoom on the Y axis. - // If the fingers are roughly on a diagonal, we revert to the - // proportional zooming. + // If the fingers are roughly on a diagonal, we revert to the proportional zooming. if touches.len() == 2 { let mut touches = touches.values(); diff --git a/examples/with_winit/src/stats.rs b/examples/with_winit/src/stats.rs index 54215de..ade200f 100644 --- a/examples/with_winit/src/stats.rs +++ b/examples/with_winit/src/stats.rs @@ -3,11 +3,9 @@ use scenes::RobotoText; use std::collections::VecDeque; -use std::time::Duration; -use vello::kurbo::{Affine, Line, PathEl, Rect, Stroke}; +use vello::kurbo::{Affine, PathEl, Rect, Stroke}; use vello::peniko::{Brush, Color, Fill}; use vello::{AaConfig, BumpAllocators, Scene}; -use wgpu_profiler::GpuTimerQueryResult; const SLIDING_WINDOW_SIZE: usize = 100; @@ -34,7 +32,7 @@ impl Snapshot { ) where T: Iterator, { - let width = (viewport_width * 0.4).max(200.).min(600.); + let width = (viewport_width * 0.4).clamp(200., 600.); let height = width * 0.7; let x_offset = viewport_width - width; let y_offset = viewport_height - height; @@ -75,8 +73,7 @@ impl Snapshot { labels.push(format!("blend: {}", bump.blend)); } - // height / 2 is dedicated to the text labels and the rest is filled by - // the bar graph. + // 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; @@ -113,13 +110,11 @@ impl Snapshot { 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 + // 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 @@ -128,8 +123,7 @@ impl Snapshot { }; 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. + // 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); @@ -247,207 +241,3 @@ impl Stats { fn round_up(n: usize, f: usize) -> usize { n - 1 - (n - 1) % f + f } - -const COLORS: &[Color] = &[ - Color::AQUA, - Color::RED, - Color::ALICE_BLUE, - Color::YELLOW, - Color::GREEN, - Color::BLUE, - Color::ORANGE, - Color::WHITE, -]; - -pub fn draw_gpu_profiling( - scene: &mut Scene, - text: &mut RobotoText, - viewport_width: f64, - viewport_height: f64, - profiles: &[GpuTimerQueryResult], -) { - if profiles.is_empty() { - return; - } - let width = (viewport_width * 0.3).clamp(150., 450.); - let height = width * 1.5; - let y_offset = viewport_height - height; - let offset = Affine::translate((0., y_offset)); - - // Draw the background - scene.fill( - Fill::NonZero, - offset, - &Brush::Solid(Color::rgba8(0, 0, 0, 200)), - None, - &Rect::new(0., 0., width, height), - ); - // Find the range of the samples, so we can normalise them - let mut min = f64::MAX; - let mut max = f64::MIN; - let mut max_depth = 0; - let mut depth = 0; - let mut count = 0; - traverse_profiling(profiles, &mut |profile, stage| { - match stage { - TraversalStage::Enter => { - count += 1; - min = min.min(profile.time.start); - max = max.max(profile.time.end); - max_depth = max_depth.max(depth); - // Apply a higher depth to the children - depth += 1; - } - TraversalStage::Leave => depth -= 1, - } - }); - let total_time = max - min; - { - let labels = [ - format!("GPU Time: {:.2?}", Duration::from_secs_f64(total_time)), - "Press P to save a trace".to_string(), - ]; - - // height / 5 is dedicated to the text labels and the rest is filled by - // the frame time. - let text_height = height * 0.2 / (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( - scene, - None, - text_size, - Some(&Brush::Solid(Color::WHITE)), - offset * Affine::translate((left_margin, (i + 1) as f64 * text_height)), - label, - ); - } - - let text_size = (text_height * 0.9) as f32; - for (i, label) in labels.iter().enumerate() { - text.add( - scene, - None, - text_size, - Some(&Brush::Solid(Color::WHITE)), - offset * Affine::translate((left_margin, (i + 1) as f64 * text_height)), - label, - ); - } - } - let timeline_start_y = height * 0.21; - let timeline_range_y = height * 0.78; - let timeline_range_end = timeline_start_y + timeline_range_y; - - // Add 6 items worth of margin - let text_height = timeline_range_y / (6 + count) as f64; - let left_margin = width * 0.35; - let mut cur_text_y = timeline_start_y; - let mut cur_index = 0; - let mut depth = 0; - // Leave 1 bar's worth of margin - let depth_width = width * 0.28 / (max_depth + 1) as f64; - let depth_size = depth_width * 0.8; - traverse_profiling(profiles, &mut |profile, stage| { - if let TraversalStage::Enter = stage { - let start_normalised = - ((profile.time.start - min) / total_time) * timeline_range_y + timeline_start_y; - let end_normalised = - ((profile.time.end - min) / total_time) * timeline_range_y + timeline_start_y; - - let color = COLORS[cur_index % COLORS.len()]; - let x = width * 0.01 + (depth as f64 * depth_width); - scene.fill( - Fill::NonZero, - offset, - &Brush::Solid(color), - None, - &Rect::new(x, start_normalised, x + depth_size, end_normalised), - ); - - let mut text_start = start_normalised; - let nested = !profile.nested_queries.is_empty(); - if nested { - // If we have children, leave some more space for them - text_start -= text_height * 0.7; - } - let this_time = profile.time.end - profile.time.start; - // Highlight as important if more than 10% of the total time, or - // more than 1ms - let slow = this_time * 20. >= total_time || this_time >= 0.001; - let text_y = text_start - // Ensure that we don't overlap the previous item - .max(cur_text_y) - // Ensure that all remaining items can fit - .min(timeline_range_end - (count - cur_index) as f64 * text_height); - let (text_height, text_color) = if slow { - (text_height, Color::WHITE) - } else { - (text_height * 0.6, Color::LIGHT_GRAY) - }; - let text_size = (text_height * 0.9) as f32; - // Text is specified by the baseline, but the y positions all refer - // to the top of the text - cur_text_y = text_y + text_height; - let label = format!( - "{:.2?} - {:.30}", - Duration::from_secs_f64(this_time), - profile.label - ); - scene.fill( - Fill::NonZero, - offset, - &Brush::Solid(color), - None, - &Rect::new( - width * 0.31, - cur_text_y - text_size as f64 * 0.7, - width * 0.34, - cur_text_y, - ), - ); - text.add( - scene, - None, - text_size, - Some(&Brush::Solid(text_color)), - offset * Affine::translate((left_margin, cur_text_y)), - &label, - ); - if !nested && slow { - scene.stroke( - &Stroke::new(2.), - offset, - &Brush::Solid(color), - None, - &Line::new( - (x + depth_size, (end_normalised + start_normalised) / 2.), - (width * 0.31, cur_text_y - text_size as f64 * 0.35), - ), - ); - } - cur_index += 1; - // Higher depth applies only to the children - depth += 1; - } else { - depth -= 1; - } - }); -} - -enum TraversalStage { - Enter, - Leave, -} - -fn traverse_profiling( - profiles: &[GpuTimerQueryResult], - callback: &mut impl FnMut(&GpuTimerQueryResult, TraversalStage), -) { - for profile in profiles { - callback(profile, TraversalStage::Enter); - traverse_profiling(&profile.nested_queries, &mut *callback); - callback(profile, TraversalStage::Leave); - } -} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..162d4fd --- /dev/null +++ b/src/error.rs @@ -0,0 +1,12 @@ +// Copyright 2023 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use thiserror::Error; + +/// Triggered when there is an issue parsing user input. +#[derive(Error, Debug)] +#[non_exhaustive] +pub enum Error { + #[error("Error parsing svg: {0}")] + Svg(#[from] usvg::Error), +} diff --git a/src/lib.rs b/src/lib.rs index 44be1ed..650b612 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,23 +1,14 @@ // Copyright 2023 the Vello Authors // SPDX-License-Identifier: Apache-2.0 OR MIT -//! Render a [`usvg::Tree`] to a Vello [`Scene`]. +//! Render an SVG document to a Vello [`Scene`]. //! //! This currently lacks support for a [number of important](crate#unsupported-features) SVG features. -//! This is because this integration was developed for examples, which only need to support enough SVG -//! to demonstrate Vello. //! -//! However, this is also intended to be the preferred integration between Vello and [usvg], so [consider +//! This is also intended to be the preferred integration between Vello and [usvg], so [consider //! contributing](https://github.com/linebender/vello_svg) if you need a feature which is missing. //! -//! [`render_tree_with`] is the primary entry point function, which supports choosing the behaviour -//! when [unsupported features](crate#unsupported-features) are detected. In a future release where there are -//! no unsupported features, this may be phased out -//! -//! [`render_tree`] is a convenience wrapper around [`render_tree_with`] which renders an indicator around not -//! yet supported features -//! -//! This crate also re-exports [`usvg`], to make handling dependency versions easier +//! This crate also re-exports [`usvg`] and [`vello`], so you can easily use the specific versions that are compatible with Vello SVG. //! //! # Unsupported features //! @@ -32,11 +23,12 @@ //! - path shape-rendering //! - patterns -pub mod util; +mod render; -use std::convert::Infallible; -use vello::peniko::{BlendMode, Fill}; -use vello::Scene; +mod error; +pub use error::Error; + +pub mod util; /// Re-export vello. pub use vello; @@ -44,226 +36,64 @@ pub use vello; /// Re-export usvg. pub use usvg; -/// Append a [`usvg::Tree`] into a Vello [`Scene`], with default error handling -/// This will draw a red box over (some) unsupported elements -/// -/// Calls [`render_tree_with`] with an error handler implementing the above. +/// Render a [`Scene`] from an SVG string, with default error handling. /// -/// See the [module level documentation](crate#unsupported-features) for a list of some unsupported svg features -pub fn render_tree(scene: &mut Scene, svg: &usvg::Tree) { - render_tree_with::<_, Infallible>(scene, svg, &mut util::default_error_handler) - .unwrap_or_else(|e| match e {}); +/// This will draw a red box over (some) unsupported elements. +pub fn render(svg: &str) -> Result { + let opt = usvg::Options::default(); + let tree = usvg::Tree::from_str(svg, &opt)?; + let mut scene = vello::Scene::new(); + append_tree(&mut scene, &tree); + Ok(scene) } -/// Append a [`usvg::Tree`] into a Vello [`Scene`]. +/// Append an SVG to a vello [`Scene`], with default error handling. /// -/// Calls [`render_tree_with`] with [`util::default_error_handler`]. -/// This will draw a red box over unsupported element types. +/// This will draw a red box over (some) unsupported elements. +pub fn append(scene: &mut vello::Scene, svg: &str) -> Result<(), Error> { + let opt = usvg::Options::default(); + let tree = usvg::Tree::from_str(svg, &opt)?; + append_tree(scene, &tree); + Ok(()) +} + +/// Append an SVG to a vello [`Scene`], with user-provided error handling logic. /// /// See the [module level documentation](crate#unsupported-features) for a list of some unsupported svg features -pub fn render_tree_with Result<(), E>, E>( - scene: &mut Scene, - svg: &usvg::Tree, +pub fn append_with( + scene: &mut vello::Scene, + svg: &str, error_handler: &mut F, -) -> Result<(), E> { - render_tree_impl( - scene, - svg, - &svg.view_box(), - &usvg::Transform::identity(), - error_handler, - ) +) -> Result<(), Error> { + let opt = usvg::Options::default(); + let tree = usvg::Tree::from_str(svg, &opt)?; + append_tree_with(scene, &tree, error_handler); + Ok(()) } -/// A helper function to render a tree with a given transform. -fn render_tree_impl Result<(), E>, E>( - scene: &mut Scene, - tree: &usvg::Tree, - view_box: &usvg::ViewBox, - ts: &usvg::Transform, - error_handler: &mut F, -) -> Result<(), E> { - let ts = &ts.pre_concat(view_box.to_transform(tree.size())); - let transform = util::to_affine(ts); - scene.push_layer( - BlendMode { - mix: vello::peniko::Mix::Clip, - compose: vello::peniko::Compose::SrcOver, - }, - 1.0, - transform, - &vello::kurbo::Rect::new( - view_box.rect.left().into(), - view_box.rect.top().into(), - view_box.rect.right().into(), - view_box.rect.bottom().into(), - ), - ); - render_group( - scene, - tree.root(), - &ts.pre_concat(tree.root().transform()), - error_handler, - )?; - scene.pop_layer(); +/// Render a [`Scene`] from a [`usvg::Tree`], with default error handling. +/// +/// This will draw a red box over (some) unsupported elements. +pub fn render_tree(svg: &usvg::Tree) -> vello::Scene { + let mut scene = vello::Scene::new(); + append_tree(&mut scene, svg); + scene +} - Ok(()) +/// Append an [`usvg::Tree`] to a vello [`Scene`], with default error handling. +/// +/// This will draw a red box over (some) unsupported elements. +pub fn append_tree(scene: &mut vello::Scene, svg: &usvg::Tree) { + append_tree_with(scene, svg, &mut util::default_error_handler); } -fn render_group Result<(), E>, E>( - scene: &mut Scene, - group: &usvg::Group, - ts: &usvg::Transform, +/// Append an [`usvg::Tree`] to a vello [`Scene`], with user-provided error handling logic. +/// +/// See the [module level documentation](crate#unsupported-features) for a list of some unsupported svg features +pub fn append_tree_with( + scene: &mut vello::Scene, + svg: &usvg::Tree, error_handler: &mut F, -) -> Result<(), E> { - for node in group.children() { - let transform = util::to_affine(ts); - match node { - usvg::Node::Group(g) => { - let mut pushed_clip = false; - if let Some(clip_path) = g.clip_path() { - if let Some(usvg::Node::Path(clip_path)) = clip_path.root().children().first() { - // support clip-path with a single path - let local_path = util::to_bez_path(clip_path); - scene.push_layer( - BlendMode { - mix: vello::peniko::Mix::Clip, - compose: vello::peniko::Compose::SrcOver, - }, - 1.0, - transform, - &local_path, - ); - pushed_clip = true; - } - } - - render_group(scene, g, &ts.pre_concat(g.transform()), error_handler)?; - - if pushed_clip { - scene.pop_layer(); - } - } - usvg::Node::Path(path) => { - if path.visibility() != usvg::Visibility::Visible { - continue; - } - let local_path = util::to_bez_path(path); - - let do_fill = |scene: &mut Scene, error_handler: &mut F| { - if let Some(fill) = &path.fill() { - if let Some((brush, brush_transform)) = - util::to_brush(fill.paint(), fill.opacity()) - { - scene.fill( - match fill.rule() { - usvg::FillRule::NonZero => Fill::NonZero, - usvg::FillRule::EvenOdd => Fill::EvenOdd, - }, - transform, - &brush, - Some(brush_transform), - &local_path, - ); - } else { - return error_handler(scene, node); - } - } - Ok(()) - }; - let do_stroke = |scene: &mut Scene, error_handler: &mut F| { - if let Some(stroke) = &path.stroke() { - if let Some((brush, brush_transform)) = - util::to_brush(stroke.paint(), stroke.opacity()) - { - let conv_stroke = util::to_stroke(stroke); - scene.stroke( - &conv_stroke, - transform, - &brush, - Some(brush_transform), - &local_path, - ); - } else { - return error_handler(scene, node); - } - } - Ok(()) - }; - match path.paint_order() { - usvg::PaintOrder::FillAndStroke => { - do_fill(scene, error_handler)?; - do_stroke(scene, error_handler)?; - } - usvg::PaintOrder::StrokeAndFill => { - do_stroke(scene, error_handler)?; - do_fill(scene, error_handler)?; - } - } - } - usvg::Node::Image(img) => { - if img.visibility() != usvg::Visibility::Visible { - continue; - } - match img.kind() { - usvg::ImageKind::JPEG(_) - | usvg::ImageKind::PNG(_) - | usvg::ImageKind::GIF(_) => { - let Ok(decoded_image) = util::decode_raw_raster_image(img.kind()) else { - error_handler(scene, node)?; - continue; - }; - let image = util::into_image(decoded_image); - let Some(size) = - usvg::Size::from_wh(image.width as f32, image.height as f32) - else { - error_handler(scene, node)?; - continue; - }; - let view_box = img.view_box(); - let new_size = view_box.rect.size(); - let (tx, ty) = usvg::utils::aligned_pos( - view_box.aspect.align, - view_box.rect.x(), - view_box.rect.y(), - view_box.rect.width() - new_size.width(), - view_box.rect.height() - new_size.height(), - ); - let (sx, sy) = ( - new_size.width() / size.width(), - new_size.height() / size.height(), - ); - let view_box_transform = - usvg::Transform::from_row(sx, 0.0, 0.0, sy, tx, ty); - scene.push_layer( - BlendMode { - mix: vello::peniko::Mix::Clip, - compose: vello::peniko::Compose::SrcOver, - }, - 1.0, - transform, - &vello::kurbo::Rect::new( - view_box.rect.left().into(), - view_box.rect.top().into(), - view_box.rect.right().into(), - view_box.rect.bottom().into(), - ), - ); - let image_ts = util::to_affine(&ts.pre_concat(view_box_transform)); - scene.draw_image(&image, image_ts); - - scene.pop_layer(); - } - usvg::ImageKind::SVG(svg) => { - render_tree_impl(scene, svg, &img.view_box(), ts, error_handler)?; - } - } - } - usvg::Node::Text(_) => { - error_handler(scene, node)?; - } - } - } - - Ok(()) +) { + render::render_group(scene, svg.root(), error_handler) } diff --git a/src/render.rs b/src/render.rs new file mode 100644 index 0000000..ec8a245 --- /dev/null +++ b/src/render.rs @@ -0,0 +1,122 @@ +// Copyright 2024 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use crate::util; +use vello::peniko::{BlendMode, Fill}; +use vello::Scene; + +pub(crate) fn render_group( + scene: &mut Scene, + group: &usvg::Group, + error_handler: &mut F, +) { + for node in group.children() { + let transform = util::to_affine(&node.abs_transform()); + match node { + usvg::Node::Group(g) => { + let mut pushed_clip = false; + if let Some(clip_path) = g.clip_path() { + if let Some(usvg::Node::Path(clip_path)) = clip_path.root().children().first() { + // support clip-path with a single path + let local_path = util::to_bez_path(clip_path); + scene.push_layer( + BlendMode { + mix: vello::peniko::Mix::Clip, + compose: vello::peniko::Compose::SrcOver, + }, + 1.0, + transform, + &local_path, + ); + pushed_clip = true; + } + } + + render_group(scene, g, error_handler); + + if pushed_clip { + scene.pop_layer(); + } + } + usvg::Node::Path(path) => { + if !path.is_visible() { + continue; + } + let local_path = util::to_bez_path(path); + + let do_fill = |scene: &mut Scene, error_handler: &mut F| { + if let Some(fill) = &path.fill() { + if let Some((brush, brush_transform)) = + util::to_brush(fill.paint(), fill.opacity()) + { + scene.fill( + match fill.rule() { + usvg::FillRule::NonZero => Fill::NonZero, + usvg::FillRule::EvenOdd => Fill::EvenOdd, + }, + transform, + &brush, + Some(brush_transform), + &local_path, + ); + } else { + error_handler(scene, node) + } + } + }; + let do_stroke = |scene: &mut Scene, error_handler: &mut F| { + if let Some(stroke) = &path.stroke() { + if let Some((brush, brush_transform)) = + util::to_brush(stroke.paint(), stroke.opacity()) + { + let conv_stroke = util::to_stroke(stroke); + scene.stroke( + &conv_stroke, + transform, + &brush, + Some(brush_transform), + &local_path, + ); + } else { + error_handler(scene, node) + } + } + }; + match path.paint_order() { + usvg::PaintOrder::FillAndStroke => { + do_fill(scene, error_handler); + do_stroke(scene, error_handler); + } + usvg::PaintOrder::StrokeAndFill => { + do_stroke(scene, error_handler); + do_fill(scene, error_handler); + } + } + } + usvg::Node::Image(img) => { + if !img.is_visible() { + continue; + } + match img.kind() { + usvg::ImageKind::JPEG(_) + | usvg::ImageKind::PNG(_) + | usvg::ImageKind::GIF(_) => { + let Ok(decoded_image) = util::decode_raw_raster_image(img.kind()) else { + error_handler(scene, node); + continue; + }; + let image = util::into_image(decoded_image); + let image_ts = util::to_affine(&img.abs_transform()); + scene.draw_image(&image, image_ts); + } + usvg::ImageKind::SVG(svg) => { + render_group(scene, svg.root(), error_handler); + } + } + } + usvg::Node::Text(_) => { + error_handler(scene, node); + } + } + } +} diff --git a/src/util.rs b/src/util.rs index e659253..9647695 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,7 +1,6 @@ // Copyright 2023 the Vello Authors // SPDX-License-Identifier: Apache-2.0 OR MIT -use std::convert::Infallible; use vello::kurbo::{Affine, BezPath, Point, Rect, Stroke}; use vello::peniko::{Blob, Brush, Color, Fill, Image}; use vello::Scene; @@ -186,7 +185,7 @@ pub fn to_brush(paint: &usvg::Paint, opacity: usvg::Opacity) -> Option<(Brush, A /// Error handler function for [`super::render_tree_with`] which draws a transparent red box /// instead of unsupported SVG features -pub fn default_error_handler(scene: &mut Scene, node: &usvg::Node) -> Result<(), Infallible> { +pub fn default_error_handler(scene: &mut Scene, node: &usvg::Node) { let bb = node.bounding_box(); let rect = Rect { x0: bb.left() as f64, @@ -201,8 +200,6 @@ pub fn default_error_handler(scene: &mut Scene, node: &usvg::Node) -> Result<(), None, &rect, ); - - Ok(()) } pub fn decode_raw_raster_image(