From 8bd3fca720d802c901a26d75480565061acbb61a Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Tue, 16 Sep 2025 11:43:58 +0200 Subject: [PATCH] Implement new clipping --- sparse_strips/vello_bench/src/glyph.rs | 1 + sparse_strips/vello_common/src/clip.rs | 717 ++++++++++++++++++ sparse_strips/vello_common/src/lib.rs | 1 + sparse_strips/vello_common/src/strip.rs | 9 +- .../vello_common/src/strip_generator.rs | 61 +- sparse_strips/vello_common/src/util.rs | 38 +- sparse_strips/vello_cpu/src/dispatch/mod.rs | 8 + .../vello_cpu/src/dispatch/multi_threaded.rs | 38 + .../src/dispatch/multi_threaded/worker.rs | 8 + .../vello_cpu/src/dispatch/single_threaded.rs | 28 + sparse_strips/vello_cpu/src/fine/lowp/mod.rs | 11 +- sparse_strips/vello_cpu/src/render.rs | 21 + sparse_strips/vello_cpu/src/util.rs | 30 +- .../vello_example_scenes/src/clip.rs | 106 ++- sparse_strips/vello_example_scenes/src/lib.rs | 20 + .../vello_hybrid/examples/winit/src/main.rs | 40 +- sparse_strips/vello_hybrid/src/scene.rs | 29 + ...lip_non_isolated_deeply_nested_circles.png | 3 + .../clip_non_isolated_outside_canvas.png | 3 + ...n_isolated_rectangle_with_star_evenodd.png | 3 + .../vello_sparse_tests/tests/clip.rs | 68 ++ .../vello_sparse_tests/tests/renderer.rs | 26 + 22 files changed, 1203 insertions(+), 66 deletions(-) create mode 100644 sparse_strips/vello_common/src/clip.rs create mode 100644 sparse_strips/vello_sparse_tests/snapshots/clip_non_isolated_deeply_nested_circles.png create mode 100644 sparse_strips/vello_sparse_tests/snapshots/clip_non_isolated_outside_canvas.png create mode 100644 sparse_strips/vello_sparse_tests/snapshots/clip_non_isolated_rectangle_with_star_evenodd.png diff --git a/sparse_strips/vello_bench/src/glyph.rs b/sparse_strips/vello_bench/src/glyph.rs index 32ff50996..6a2423a45 100644 --- a/sparse_strips/vello_bench/src/glyph.rs +++ b/sparse_strips/vello_bench/src/glyph.rs @@ -132,6 +132,7 @@ impl GlyphRenderer for GlyphBenchRenderer { glyph.transform, Some(128), &mut self.strip_storage, + None, ); } GlyphType::Bitmap(_) => {} diff --git a/sparse_strips/vello_common/src/clip.rs b/sparse_strips/vello_common/src/clip.rs new file mode 100644 index 000000000..2871b6275 --- /dev/null +++ b/sparse_strips/vello_common/src/clip.rs @@ -0,0 +1,717 @@ +// Copyright 2025 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Managing clipping state. + +use crate::kurbo::{Affine, BezPath}; +use crate::strip::Strip; +use crate::strip_generator::{GenerationMode, StripGenerator, StripStorage}; +use crate::tile::Tile; +use crate::util::normalized_mul_u8x16; +use alloc::vec; +use alloc::vec::Vec; +use fearless_simd::{Level, Simd, SimdBase, simd_dispatch, u8x16}; +use peniko::Fill; + +#[derive(Debug)] +struct ClipData { + alpha_start: u32, + strip_start: u32, +} + +impl ClipData { + fn to_path_data_ref<'a>(&self, storage: &'a StripStorage) -> PathDataRef<'a> { + PathDataRef { + strips: storage + .strips + .get(self.strip_start as usize..) + .unwrap_or(&[]), + alphas: storage + .alphas + .get(self.alpha_start as usize..) + .unwrap_or(&[]), + } + } +} + +/// A context for managing clip stacks. +#[derive(Debug)] +pub struct ClipContext { + storage: StripStorage, + temp_storage: StripStorage, + clip_stack: Vec, +} + +impl Default for ClipContext { + fn default() -> Self { + Self::new() + } +} + +impl ClipContext { + /// Create a new clip context. + #[inline] + pub fn new() -> Self { + let mut main_storage = StripStorage::default(); + main_storage.set_generation_mode(GenerationMode::Append); + Self { + storage: main_storage, + temp_storage: Default::default(), + clip_stack: vec![], + } + } + + /// Reset the clip context. + #[inline] + pub fn reset(&mut self) { + self.clip_stack.clear(); + self.storage.clear(); + self.temp_storage.clear(); + } + + /// Get the data of the current clip path. + #[inline] + pub fn get(&self) -> Option> { + self.clip_stack + .last() + .map(|c| c.to_path_data_ref(&self.storage)) + } + + /// Push a new clip path to the stack. + #[inline] + pub fn push_clip( + &mut self, + clip_path: &BezPath, + strip_generator: &mut StripGenerator, + fill_rule: Fill, + transform: Affine, + aliasing_threshold: Option, + ) { + self.temp_storage.clear(); + + let alpha_start = self.storage.alphas.len() as u32; + let strip_start = self.storage.strips.len() as u32; + + let clip_data = ClipData { + alpha_start, + strip_start, + }; + + let existing_clip = self + .clip_stack + .last() + .map(|c| c.to_path_data_ref(&self.storage)); + + strip_generator.generate_filled_path( + clip_path, + fill_rule, + transform, + aliasing_threshold, + &mut self.temp_storage, + existing_clip, + ); + + self.storage.extend(&self.temp_storage); + self.clip_stack.push(clip_data); + } + + /// Pop the least recent clip path. + #[inline] + pub fn pop_clip(&mut self) { + let data = self.clip_stack.pop().expect("clip stack underflowed"); + self.storage.strips.truncate(data.strip_start as usize); + self.storage.alphas.truncate(data.alpha_start as usize); + } +} + +/// Borrowed data of a stripped path. +#[derive(Clone, Copy, Debug)] +pub struct PathDataRef<'a> { + /// The strips. + pub strips: &'a [Strip], + /// The alpha buffer. + pub alphas: &'a [u8], +} + +/// Compute the sparse strips representation of a path that results +/// from intersecting the two input paths. This can be used to implement +/// clip paths. +pub fn intersect( + level: Level, + path_1: PathDataRef<'_>, + path_2: PathDataRef<'_>, + target: &mut StripStorage, +) { + intersect_dispatch(level, path_1, path_2, target); +} + +simd_dispatch!(fn intersect_dispatch( + level, + path_1: PathDataRef<'_>, + path_2: PathDataRef<'_>, + target: &mut StripStorage, +) = intersect_impl); + +/// The implementation of the clipping algorithm using sparse strips. Conceptually, it is relatively +/// simple: We iterate over each strip and fill region of the two paths in lock step and determine +/// all overlaps between the two. For each overlap, we proceed depending on what kind of region +/// we have in the first path and the second one. +/// - In case we have two fill regions, the overlap region will also be filled. +/// - In case we have one strip and one fill region, the overlap region will copy the alpha mask of the strip region. +/// - Finally, if we have two strip regions, we combine the alpha masks of both. +/// - All regions that are not filled in either path are simply ignored. +/// +/// This is all that this method does. It just looks more complicated as the logic for iterating +/// in lock step is a bit tricky. +fn intersect_impl( + simd: S, + path_1: PathDataRef<'_>, + path_2: PathDataRef<'_>, + target: &mut StripStorage, +) { + // In case either path is empty, the clip path should be empty. + if path_1.strips.is_empty() || path_2.strips.is_empty() { + return; + } + + // Ignore any y values that are outside the bounding box of either of the two paths, as + // those are guaranteed to have neither fill nor strip regions. + let mut cur_y = path_1.strips[0].strip_y().min(path_2.strips[0].strip_y()); + let end_y = path_1.strips[path_1.strips.len() - 1] + .strip_y() + .min(path_2.strips[path_2.strips.len() - 1].strip_y()); + + let mut path_1_idx = 0; + let mut path_2_idx = 0; + let mut strip_state = None; + + // Iterate over each strip row and handle them. + while cur_y <= end_y { + // For each row, we create two iterators that alternatingly yield the strips and fill + // regions in that row, until the last strip has been reached. + let mut p1_iter = RowIterator::new(path_1, &mut path_1_idx, cur_y); + let mut p2_iter = RowIterator::new(path_2, &mut path_2_idx, cur_y); + + let mut p1_region = p1_iter.next(); + let mut p2_region = p2_iter.next(); + + // If at least one region is none, it means that we reached the end of the row + // for that path, meaning that we exceeded the bounding box of that path and no + // additional strips should be generated for that row, even if the other path might + // still have more strips left. They will all be clipped away. So only consider it + // if both paths have a region left. + while let (Some(region_1), Some(region_2)) = (p1_region, p2_region) { + match region_1.overlap_relationship(®ion_2) { + // This means there is no overlap between the regions, so we need to advance + // the iterator of the region that is further behind. + OverlapRelationship::Advance(advance) => { + match advance { + Advance::Left => p1_region = p1_iter.next(), + Advance::Right => p2_region = p2_iter.next(), + }; + + continue; + } + // We have an overlap! + OverlapRelationship::Overlap(overlap) => { + match (region_1, region_2) { + // Both regions are a fill. Flush the current strip and start a new + // one at the end of the overlap region setting `fill_gap` to true, + // so that the whole area before that will be filled with a sparse + // fill. + (Region::Fill(_), Region::Fill(_)) => { + flush_strip(&mut strip_state, &mut target.strips, cur_y); + start_strip(&mut strip_state, &target.alphas, overlap.end, true); + } + // One fill one strip, so we simply use the alpha mask from the strip region. + (Region::Strip(s), Region::Fill(_)) + | (Region::Fill(_), Region::Strip(s)) => { + // If possible, don't create a new strip but just extend the current one. + if should_create_new_strip(&strip_state, &target.alphas, overlap.start) + { + flush_strip(&mut strip_state, &mut target.strips, cur_y); + start_strip(&mut strip_state, &target.alphas, overlap.start, false); + } + + let s_alphas = &s.alphas[(overlap.start - s.start) as usize * 4..] + [..overlap.width() as usize * 4]; + target.alphas.extend_from_slice(s_alphas); + } + // Two strips, we need to multiply the opacity masks from both paths. + (Region::Strip(s_region_1), Region::Strip(s_region_2)) => { + // Once again, only create a new strip if we can't extend the current one. + if should_create_new_strip(&strip_state, &target.alphas, overlap.start) + { + flush_strip(&mut strip_state, &mut target.strips, cur_y); + start_strip(&mut strip_state, &target.alphas, overlap.start, false); + } + + let num_blocks = overlap.width() / Tile::HEIGHT; + + // Get the right alpha values for the specific position. + let s1_alphas = s_region_1.alphas + [(overlap.start - s_region_1.start) as usize * 4..] + .chunks_exact(16) + .take(num_blocks as usize); + let s2_alphas = s_region_2.alphas + [(overlap.start - s_region_2.start) as usize * 4..] + .chunks_exact(16) + .take(num_blocks as usize); + + for (s1_alpha, s2_alpha) in s1_alphas.zip(s2_alphas) { + let s1 = u8x16::from_slice(simd, s1_alpha); + let s2 = u8x16::from_slice(simd, s2_alpha); + + // Combine them. + let res = simd.narrow_u16x16(normalized_mul_u8x16(s1, s2)); + target.alphas.extend(&res.val); + } + } + } + + // Advance the iterator of the path whose region's end is further behind. + match overlap.advance { + Advance::Left => p1_region = p1_iter.next(), + Advance::Right => p2_region = p2_iter.next(), + }; + } + } + } + + // Flush the strip before advancing to the next strip row. + flush_strip(&mut strip_state, &mut target.strips, cur_y); + cur_y += 1; + } + + // Push the sentinel strip. + target.strips.push(Strip::new( + u16::MAX, + end_y * Tile::HEIGHT, + target.alphas.len() as u32, + false, + )); +} + +/// An overlap between two regions. +struct Overlap { + /// The start x coordinate. + start: u16, + /// The end x coordinate. + end: u16, + /// Whether the left or right region iterator should be advanced next. + advance: Advance, +} + +impl Overlap { + fn width(&self) -> u16 { + self.end - self.start + } +} + +enum Advance { + Left, + Right, +} + +/// The relationship between two regions. +enum OverlapRelationship { + /// There is no overlap between the regions, advance the region iterator on the given side. + Advance(Advance), + /// There is an overlap between the regions. + Overlap(Overlap), +} + +#[derive(Debug, Clone, Copy)] +struct FillRegion { + start: u16, + width: u16, +} + +#[derive(Debug, Clone, Copy)] +struct StripRegion<'a> { + start: u16, + width: u16, + alphas: &'a [u8], +} + +#[derive(Debug, Clone, Copy)] +enum Region<'a> { + Fill(FillRegion), + Strip(StripRegion<'a>), +} + +impl Region<'_> { + fn start(&self) -> u16 { + match self { + Region::Fill(fill) => fill.start, + Region::Strip(strip) => strip.start, + } + } + + fn width(&self) -> u16 { + match self { + Region::Fill(fill) => fill.width, + Region::Strip(strip) => strip.width, + } + } + + fn end(&self) -> u16 { + self.start() + self.width() + } + + fn overlap_relationship(&self, other: &Region<'_>) -> OverlapRelationship { + if self.end() <= other.start() { + OverlapRelationship::Advance(Advance::Left) + } else if self.start() >= other.end() { + OverlapRelationship::Advance(Advance::Right) + } else { + let start = self.start().max(other.start()); + let end = self.end().min(other.end()); + + let shift = if self.end() <= other.end() { + Advance::Left + } else { + Advance::Right + }; + + OverlapRelationship::Overlap(Overlap { + advance: shift, + start, + end, + }) + } + } +} + +/// An iterator of strip and fill regions of a single strip row. +struct RowIterator<'a> { + /// The path in question. + input: PathDataRef<'a>, + /// The strip row we want to iterate over. + strip_y: u16, + /// The index of the current strip. + cur_idx: &'a mut usize, + /// Whether the iterator should yield a strip next or not. + /// When iterating over a row, we alternate between emitting strips and filled regions (unless + /// the region between two strips is not filled), so this flag acts as a toggle to store what + /// should be yielded next. + on_strip: bool, +} + +impl<'a> RowIterator<'a> { + fn new(input: PathDataRef<'a>, cur_idx: &'a mut usize, strip_y: u16) -> Self { + // Forward the index until we have found the right strip. + while input.strips[*cur_idx].strip_y() < strip_y { + *cur_idx += 1; + } + + Self { + input, + cur_idx, + strip_y, + on_strip: true, + } + } + + fn cur_strip(&self) -> &Strip { + &self.input.strips[*self.cur_idx] + } + + fn next_strip(&self) -> &Strip { + &self.input.strips[*self.cur_idx + 1] + } + + fn cur_strip_width(&self) -> u16 { + let cur = self.cur_strip(); + let next = self.next_strip(); + ((next.alpha_idx() - cur.alpha_idx()) / Tile::HEIGHT as u32) as u16 + } + + fn cur_strip_alphas(&self) -> &'a [u8] { + let cur = self.cur_strip(); + let next = self.next_strip(); + &self.input.alphas[cur.alpha_idx() as usize..next.alpha_idx() as usize] + } + + fn cur_strip_fill_area(&self) -> Option { + let cur = self.cur_strip(); + let next = self.next_strip(); + + // Note that if the next strip happens to be on the next line, it will always have + // zero winding so we don't need to special case this. + if next.fill_gap() { + let x = cur.x + self.cur_strip_width(); + let width = next.x - x; + + Some(FillRegion { start: x, width }) + } else { + None + } + } +} + +impl<'a> Iterator for RowIterator<'a> { + type Item = Region<'a>; + + #[inline(always)] + fn next(&mut self) -> Option { + // If we are currently not on a strip, we want to yield a filled region in case there is one. + if !self.on_strip { + // Flip boolean flag so we will yield a strip in the next iteration. + self.on_strip = true; + + // if we have a filled area, yield it and return. Otherwise, do nothing and we will + // instead yield the next strip below. In any case, we need to advance the current index + // so that we point to the next strip now. + if let Some(fill_area) = self.cur_strip_fill_area() { + *self.cur_idx += 1; + + return Some(Region::Fill(fill_area)); + } else { + *self.cur_idx += 1; + } + } + + // If we reached this point, we will yield a strip this iteration, so toggle the flag + // so that in the next iteration, we yield a filled region instead. + self.on_strip = false; + + // If the current strip is sentinel or not within our target row, terminate. + if self.cur_strip().is_sentinel() || self.cur_strip().strip_y() != self.strip_y { + return None; + } + + // Calculate the dimensions of the strip and yield it. + let x = self.cur_strip().x; + let width = self.cur_strip_width(); + let alphas = self.cur_strip_alphas(); + + Some(Region::Strip(StripRegion { + start: x, + width, + alphas, + })) + } +} + +/// The data of the current strip we are building. +struct StripState { + x: u16, + alpha_idx: u32, + fill_gap: bool, +} + +fn flush_strip(strip_state: &mut Option, strips: &mut Vec, cur_y: u16) { + if let Some(state) = core::mem::take(strip_state) { + strips.push(Strip::new( + state.x, + cur_y * Tile::HEIGHT, + state.alpha_idx, + state.fill_gap, + )); + } +} + +fn start_strip(strip_data: &mut Option, alphas: &[u8], x: u16, fill_gap: bool) { + *strip_data = Some(StripState { + x, + alpha_idx: alphas.len() as u32, + fill_gap, + }); +} + +fn should_create_new_strip( + strip_state: &Option, + alphas: &[u8], + overlap_start: u16, +) -> bool { + // Returns false in case we can append to the currently built strip. + strip_state.as_ref().is_none_or(|state| { + let width = ((alphas.len() as u32 - state.alpha_idx) / Tile::HEIGHT as u32) as u16; + let strip_end = state.x + width; + + strip_end < overlap_start - 1 + }) +} + +#[cfg(test)] +mod tests { + use crate::clip::{PathDataRef, RowIterator, intersect}; + use crate::strip::Strip; + use crate::strip_generator::StripStorage; + use crate::tile::Tile; + use fearless_simd::Level; + use std::vec; + + #[test] + fn intersect_partly_overlapping_strips() { + let path_1 = StripBuilder::new().add_strip(0, 0, 32, false).finish(); + + let path_2 = StripBuilder::new().add_strip(8, 0, 44, false).finish(); + + let expected = StripBuilder::new().add_strip(8, 0, 32, false).finish(); + + run_test(expected, path_1, path_2); + } + + #[test] + fn intersect_multiple_overlapping_strips() { + let path_1 = StripBuilder::new() + .add_strip(0, 1, 4, false) + .add_strip(12, 1, 20, true) + .add_strip(28, 1, 32, false) + .add_strip(44, 1, 52, true) + .finish(); + + let path_2 = StripBuilder::new() + .add_strip(4, 1, 8, false) + .add_strip(16, 1, 20, true) + .add_strip(24, 1, 28, false) + .add_strip(32, 1, 36, false) + .add_strip(44, 1, 48, true) + .finish(); + + let expected = StripBuilder::new() + .add_strip(4, 1, 8, false) + .add_strip(12, 1, 20, true) + .add_strip(32, 1, 36, false) + .add_strip(44, 1, 48, true) + .finish(); + + run_test(expected, path_1, path_2); + } + + #[test] + fn multiple_rows() { + let path_1 = StripBuilder::new() + .add_strip(0, 0, 4, false) + .add_strip(16, 0, 20, true) + .add_strip(4, 1, 8, false) + .add_strip(12, 1, 24, true) + .add_strip(4, 2, 8, false) + .add_strip(16, 2, 32, true) + .finish(); + + let path_2 = StripBuilder::new() + .add_strip(0, 2, 4, false) + .add_strip(16, 2, 24, true) + .add_strip(8, 3, 12, false) + .add_strip(16, 3, 28, true) + .finish(); + + let expected = StripBuilder::new() + .add_strip(4, 2, 8, false) + .add_strip(16, 2, 24, true) + .finish(); + + run_test(expected, path_1, path_2); + } + + #[test] + fn alpha_buffer_correct_width() { + let path_1 = StripBuilder::new() + .add_strip(0, 0, 4, false) + .add_strip(0, 1, 12, false) + .finish(); + + let path_2 = StripBuilder::new() + .add_strip(4, 0, 8, false) + .add_strip(0, 1, 4, false) + .add_strip(12, 1, 16, true) + .finish(); + + let expected = StripBuilder::new().add_strip(0, 1, 12, false).finish(); + + run_test(expected, path_1, path_2); + } + + #[test] + fn row_iterator_abort_next_line() { + let path_1 = StripBuilder::new() + .add_strip(0, 0, 4, false) + .add_strip(0, 1, 4, false) + .finish(); + + let path_ref = PathDataRef { + strips: &path_1.strips, + alphas: &path_1.alphas, + }; + + let mut idx = 0; + let mut iter = RowIterator::new(path_ref, &mut idx, 0); + + assert!(iter.next().is_some()); + assert!(iter.next().is_none()); + } + + fn run_test(expected: StripStorage, path_1: StripStorage, path_2: StripStorage) { + let mut write_target = StripStorage::default(); + + let path_1 = PathDataRef { + strips: &path_1.strips, + alphas: &path_1.alphas, + }; + + let path_2 = PathDataRef { + strips: &path_2.strips, + alphas: &path_2.alphas, + }; + + intersect(Level::new(), path_1, path_2, &mut write_target); + + assert_eq!(write_target, expected); + } + + struct StripBuilder { + storage: StripStorage, + } + + impl StripBuilder { + fn new() -> Self { + Self { + storage: StripStorage::default(), + } + } + + fn add_strip(self, x: u16, strip_y: u16, end: u16, fill_gap: bool) -> Self { + let width = end - x; + self.add_strip_with( + x, + strip_y, + end, + fill_gap, + &vec![0; (width * Tile::HEIGHT) as usize], + ) + } + + fn add_strip_with( + mut self, + x: u16, + strip_y: u16, + end: u16, + fill_gap: bool, + alphas: &[u8], + ) -> Self { + let width = end - x; + assert_eq!(alphas.len(), (width * Tile::HEIGHT) as usize); + let idx = self.storage.alphas.len(); + self.storage + .strips + .push(Strip::new(x, strip_y * Tile::HEIGHT, idx as u32, fill_gap)); + self.storage.alphas.extend_from_slice(alphas); + + self + } + + fn finish(mut self) -> StripStorage { + let last_y = self.storage.strips.last().unwrap().y; + let idx = self.storage.alphas.len(); + + self.storage + .strips + .push(Strip::new(u16::MAX, last_y, idx as u32, false)); + + self.storage + } + } +} diff --git a/sparse_strips/vello_common/src/lib.rs b/sparse_strips/vello_common/src/lib.rs index 0599c3884..2fe7ce0b3 100644 --- a/sparse_strips/vello_common/src/lib.rs +++ b/sparse_strips/vello_common/src/lib.rs @@ -62,6 +62,7 @@ extern crate alloc; extern crate std; pub mod blurred_rounded_rect; +pub mod clip; pub mod coarse; #[cfg(feature = "text")] pub mod colr; diff --git a/sparse_strips/vello_common/src/strip.rs b/sparse_strips/vello_common/src/strip.rs index 9a146de17..f7b71412d 100644 --- a/sparse_strips/vello_common/src/strip.rs +++ b/sparse_strips/vello_common/src/strip.rs @@ -11,7 +11,7 @@ use alloc::vec::Vec; use fearless_simd::*; /// A strip. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Strip { /// The x coordinate of the strip, in user coordinates. pub x: u16, @@ -44,7 +44,12 @@ impl Strip { } } - /// Returns the y coordinate of the strip, in strip units. + /// Return whether the strip is a sentinel strip. + pub fn is_sentinel(&self) -> bool { + self.x == u16::MAX + } + + /// Return the y coordinate of the strip, in strip units. pub fn strip_y(&self) -> u16 { self.y / Tile::HEIGHT } diff --git a/sparse_strips/vello_common/src/strip_generator.rs b/sparse_strips/vello_common/src/strip_generator.rs index e7bae82df..5ce8ef974 100644 --- a/sparse_strips/vello_common/src/strip_generator.rs +++ b/sparse_strips/vello_common/src/strip_generator.rs @@ -3,6 +3,7 @@ //! Abstraction for generating strips from paths. +use crate::clip::{PathDataRef, intersect}; use crate::fearless_simd::Level; use crate::flatten::{FlattenCtx, Line}; use crate::kurbo::{Affine, PathEl, Stroke}; @@ -14,7 +15,7 @@ use alloc::vec::Vec; use peniko::kurbo::StrokeCtx; /// A storage for storing strip-related data. -#[derive(Debug, Default)] +#[derive(Debug, Default, PartialEq, Eq)] pub struct StripStorage { /// The strips in the storage. pub strips: Vec, @@ -49,15 +50,22 @@ impl StripStorage { pub fn is_empty(&self) -> bool { self.strips.is_empty() && self.alphas.is_empty() } + + /// Extend the current strip storage with the data from another storage. + pub fn extend(&mut self, other: &Self) { + self.strips.extend(&other.strips); + self.alphas.extend(&other.alphas); + } } /// An object for easily generating strips for a filled/stroked path. #[derive(Debug)] pub struct StripGenerator { - level: Level, + pub(crate) level: Level, line_buf: Vec, flatten_ctx: FlattenCtx, stroke_ctx: StrokeCtx, + temp_storage: StripStorage, tiles: Tiles, width: u16, height: u16, @@ -72,6 +80,7 @@ impl StripGenerator { tiles: Tiles::new(level), flatten_ctx: FlattenCtx::default(), stroke_ctx: StrokeCtx::default(), + temp_storage: StripStorage::default(), width, height, } @@ -85,6 +94,7 @@ impl StripGenerator { transform: Affine, aliasing_threshold: Option, strip_storage: &mut StripStorage, + clip_path: Option>, ) { flatten::fill( self.level, @@ -93,7 +103,8 @@ impl StripGenerator { &mut self.line_buf, &mut self.flatten_ctx, ); - self.make_strips(strip_storage, fill_rule, aliasing_threshold); + + self.generate_with_clip(aliasing_threshold, strip_storage, fill_rule, clip_path); } /// Generate the strips for a stroked path. @@ -104,6 +115,7 @@ impl StripGenerator { transform: Affine, aliasing_threshold: Option, strip_storage: &mut StripStorage, + clip_path: Option>, ) { flatten::stroke( self.level, @@ -114,13 +126,38 @@ impl StripGenerator { &mut self.flatten_ctx, &mut self.stroke_ctx, ); - self.make_strips(strip_storage, Fill::NonZero, aliasing_threshold); + self.generate_with_clip(aliasing_threshold, strip_storage, Fill::NonZero, clip_path); + } + + fn generate_with_clip( + &mut self, + aliasing_threshold: Option, + strip_storage: &mut StripStorage, + fill_rule: Fill, + clip_path: Option>, + ) { + if strip_storage.generation_mode == GenerationMode::Replace { + strip_storage.strips.clear(); + } + + if let Some(clip_path) = clip_path { + self.make_strips(strip_storage, fill_rule, aliasing_threshold, true); + let path_data = PathDataRef { + strips: &self.temp_storage.strips, + alphas: &self.temp_storage.alphas, + }; + + intersect(self.level, clip_path, path_data, strip_storage); + } else { + self.make_strips(strip_storage, fill_rule, aliasing_threshold, false); + } } /// Reset the strip generator. pub fn reset(&mut self) { self.line_buf.clear(); self.tiles.reset(); + self.temp_storage.clear(); } fn make_strips( @@ -128,20 +165,25 @@ impl StripGenerator { strip_storage: &mut StripStorage, fill_rule: Fill, aliasing_threshold: Option, + temp: bool, ) { self.tiles .make_tiles(&self.line_buf, self.width, self.height); self.tiles.sort_tiles(); - if strip_storage.generation_mode == GenerationMode::Replace { - strip_storage.strips.clear(); - } + let storage = if temp { + self.temp_storage.clear(); + + &mut self.temp_storage + } else { + strip_storage + }; strip::render( self.level, &self.tiles, - &mut strip_storage.strips, - &mut strip_storage.alphas, + &mut storage.strips, + &mut storage.alphas, fill_rule, aliasing_threshold, &self.line_buf, @@ -168,6 +210,7 @@ mod tests { Affine::IDENTITY, None, &mut storage, + None, ); assert!(!generator.line_buf.is_empty()); diff --git a/sparse_strips/vello_common/src/util.rs b/sparse_strips/vello_common/src/util.rs index 51bbb0a1f..768ac698f 100644 --- a/sparse_strips/vello_common/src/util.rs +++ b/sparse_strips/vello_common/src/util.rs @@ -3,7 +3,7 @@ //! Utility functions. -use fearless_simd::{Simd, f32x16, u8x16}; +use fearless_simd::{Simd, SimdBase, f32x16, u8x16, u8x32, u16x16, u16x32}; /// Convert f32x16 to u8x16. #[inline(always)] @@ -19,3 +19,39 @@ pub fn f32_to_u8(val: f32x16) -> u8x16 { let uzp2 = simd.unzip_low_u8x16(p3, p4); simd.unzip_low_u8x16(uzp1, uzp2) } + +/// A trait for implementing a fast approximal division by 255 for integers. +pub trait Div255Ext { + /// Divide by 255. + fn div_255(self) -> Self; +} + +impl Div255Ext for u16x32 { + #[inline(always)] + fn div_255(self) -> Self { + let p1 = Self::splat(self.simd, 255); + let p2 = self + p1; + p2.shr(8) + } +} + +impl Div255Ext for u16x16 { + #[inline(always)] + fn div_255(self) -> Self { + let p1 = Self::splat(self.simd, 255); + let p2 = self + p1; + p2.shr(8) + } +} + +/// Perform a normalized multiplication for u8x32. +#[inline(always)] +pub fn normalized_mul_u8x32(a: u8x32, b: u8x32) -> u16x32 { + (S::widen_u8x32(a.simd, a) * S::widen_u8x32(b.simd, b)).div_255() +} + +/// Perform a normalized multiplication for u8x16. +#[inline(always)] +pub fn normalized_mul_u8x16(a: u8x16, b: u8x16) -> u16x16 { + (S::widen_u8x16(a.simd, a) * S::widen_u8x16(b.simd, b)).div_255() +} diff --git a/sparse_strips/vello_cpu/src/dispatch/mod.rs b/sparse_strips/vello_cpu/src/dispatch/mod.rs index 8c7dd49c3..0169f7a68 100644 --- a/sparse_strips/vello_cpu/src/dispatch/mod.rs +++ b/sparse_strips/vello_cpu/src/dispatch/mod.rs @@ -35,6 +35,14 @@ pub(crate) trait Dispatcher: Debug + Send + Sync { paint: Paint, aliasing_threshold: Option, ); + fn push_clip_path( + &mut self, + path: &BezPath, + fill_rule: Fill, + transform: Affine, + aliasing_threshold: Option, + ); + fn pop_clip_path(&mut self); fn push_layer( &mut self, clip_path: Option<&BezPath>, diff --git a/sparse_strips/vello_cpu/src/dispatch/multi_threaded.rs b/sparse_strips/vello_cpu/src/dispatch/multi_threaded.rs index 925ab07f1..eb4f2597e 100644 --- a/sparse_strips/vello_cpu/src/dispatch/multi_threaded.rs +++ b/sparse_strips/vello_cpu/src/dispatch/multi_threaded.rs @@ -21,6 +21,7 @@ use std::ops::Range; use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; use std::sync::{Barrier, Mutex}; use thread_local::ThreadLocal; +use vello_common::clip::ClipContext; use vello_common::coarse::{Cmd, MODE_CPU, Wide}; use vello_common::encode::EncodedPaint; use vello_common::fearless_simd::{Level, Simd, simd_dispatch}; @@ -49,6 +50,7 @@ type CoarseTaskReceiver = ordered_channel::Receiver; pub(crate) struct MultiThreadedDispatcher { /// The wide tile container. wide: Wide, + clip_context: ClipContext, /// The thread pool that is used for dispatching tasks. thread_pool: ThreadPool, allocation_group: AllocationGroup, @@ -141,6 +143,7 @@ impl MultiThreadedDispatcher { task_idx, flushed, workers, + clip_context: ClipContext::new(), task_sender: None, coarse_task_receiver: None, strip_generator: StripGenerator::new(width, height, level), @@ -248,8 +251,13 @@ impl MultiThreadedDispatcher { let allocation_group = std::mem::replace(&mut self.allocation_group, self.allocations.get()); let task_sender = self.task_sender.as_mut().unwrap(); + let clip_path = self.clip_context.get().map(|c| OwnedClip { + strips: c.strips.into(), + alphas: c.alphas.into(), + }); let task = RenderTask { idx: task_idx, + clip_path, allocation_group, }; task_sender.send(task).unwrap(); @@ -453,6 +461,7 @@ impl Dispatcher for MultiThreadedDispatcher { fn reset(&mut self) { self.wide.reset(); + self.clip_context.reset(); self.allocation_group.clear(); self.batch_cost = 0.0; self.task_idx = 0; @@ -544,6 +553,28 @@ impl Dispatcher for MultiThreadedDispatcher { fn strip_storage_mut(&mut self) -> &mut StripStorage { &mut self.strip_storage } + + fn push_clip_path( + &mut self, + path: &BezPath, + fill_rule: Fill, + transform: Affine, + aliasing_threshold: Option, + ) { + self.flush_tasks(); + self.clip_context.push_clip( + path, + &mut self.strip_generator, + fill_rule, + transform, + aliasing_threshold, + ); + } + + fn pop_clip_path(&mut self) { + self.flush_tasks(); + self.clip_context.pop_clip(); + } } simd_dispatch!( @@ -596,6 +627,12 @@ impl Debug for MultiThreadedDispatcher { } } +#[derive(Debug)] +pub(crate) struct OwnedClip { + strips: Box<[Strip]>, + alphas: Box<[u8]>, +} + /// A structure that allows storing and fetching existing allocations. struct AllocationManager { entries: Vec>, @@ -685,6 +722,7 @@ impl AllocationGroup { #[derive(Debug)] pub(crate) struct RenderTask { pub(crate) idx: u32, + pub(crate) clip_path: Option, pub(crate) allocation_group: AllocationGroup, } diff --git a/sparse_strips/vello_cpu/src/dispatch/multi_threaded/worker.rs b/sparse_strips/vello_cpu/src/dispatch/multi_threaded/worker.rs index 9d1ca1f71..3c598c6d8 100644 --- a/sparse_strips/vello_cpu/src/dispatch/multi_threaded/worker.rs +++ b/sparse_strips/vello_cpu/src/dispatch/multi_threaded/worker.rs @@ -6,6 +6,7 @@ use crate::dispatch::multi_threaded::{ CoarseTask, CoarseTaskSender, CoarseTaskType, RenderTask, RenderTaskType, }; use std::vec::Vec; +use vello_common::clip::PathDataRef; use vello_common::strip_generator::{GenerationMode, StripGenerator, StripStorage}; #[derive(Debug)] @@ -49,6 +50,10 @@ impl Worker { self.strip_storage .set_generation_mode(GenerationMode::Append); let task_idx = render_task.idx; + let path_clip = render_task.clip_path.as_ref().map(|c| PathDataRef { + strips: c.strips.as_ref(), + alphas: c.alphas.as_ref(), + }); for task in render_task .allocation_group @@ -73,6 +78,7 @@ impl Worker { transform, aliasing_threshold, &mut self.strip_storage, + path_clip, ); let end = self.strip_storage.strips.len() as u32; @@ -104,6 +110,7 @@ impl Worker { transform, aliasing_threshold, &mut self.strip_storage, + path_clip, ); let end = self.strip_storage.strips.len() as u32; @@ -137,6 +144,7 @@ impl Worker { transform, aliasing_threshold, &mut self.strip_storage, + path_clip, ); let end = self.strip_storage.strips.len() as u32; diff --git a/sparse_strips/vello_cpu/src/dispatch/single_threaded.rs b/sparse_strips/vello_cpu/src/dispatch/single_threaded.rs index 09b8cc6c6..5c433ae63 100644 --- a/sparse_strips/vello_cpu/src/dispatch/single_threaded.rs +++ b/sparse_strips/vello_cpu/src/dispatch/single_threaded.rs @@ -7,6 +7,7 @@ use crate::fine::{F32Kernel, Fine, FineKernel, U8Kernel}; use crate::kurbo::{Affine, BezPath, Stroke}; use crate::peniko::{BlendMode, Fill}; use crate::region::Regions; +use vello_common::clip::ClipContext; use vello_common::coarse::{MODE_CPU, Wide}; use vello_common::encode::EncodedPaint; use vello_common::fearless_simd::{Level, Simd, simd_dispatch}; @@ -18,6 +19,7 @@ use vello_common::strip_generator::{StripGenerator, StripStorage}; #[derive(Debug)] pub(crate) struct SingleThreadedDispatcher { wide: Wide, + clip_context: ClipContext, strip_generator: StripGenerator, strip_storage: StripStorage, level: Level, @@ -27,10 +29,12 @@ impl SingleThreadedDispatcher { pub(crate) fn new(width: u16, height: u16, level: Level) -> Self { let wide = Wide::::new(width, height); let strip_generator = StripGenerator::new(width, height, level); + let clip_context = ClipContext::new(); let strip_storage = StripStorage::default(); Self { wide, + clip_context, strip_generator, strip_storage, level, @@ -106,6 +110,7 @@ impl Dispatcher for SingleThreadedDispatcher { transform, aliasing_threshold, &mut self.strip_storage, + self.clip_context.get(), ); wide.generate(&self.strip_storage.strips, paint, 0); @@ -127,6 +132,7 @@ impl Dispatcher for SingleThreadedDispatcher { transform, aliasing_threshold, &mut self.strip_storage, + self.clip_context.get(), ); wide.generate(&self.strip_storage.strips, paint, 0); @@ -149,6 +155,7 @@ impl Dispatcher for SingleThreadedDispatcher { clip_transform, aliasing_threshold, &mut self.strip_storage, + self.clip_context.get(), ); Some(self.strip_storage.strips.as_slice()) @@ -165,6 +172,7 @@ impl Dispatcher for SingleThreadedDispatcher { fn reset(&mut self) { self.wide.reset(); + self.clip_context.reset(); self.strip_generator.reset(); self.strip_storage.clear(); } @@ -194,6 +202,26 @@ impl Dispatcher for SingleThreadedDispatcher { fn strip_storage_mut(&mut self) -> &mut StripStorage { &mut self.strip_storage } + + fn push_clip_path( + &mut self, + path: &BezPath, + fill_rule: Fill, + transform: Affine, + aliasing_threshold: Option, + ) { + self.clip_context.push_clip( + path, + &mut self.strip_generator, + fill_rule, + transform, + aliasing_threshold, + ); + } + + fn pop_clip_path(&mut self) { + self.clip_context.pop_clip(); + } } simd_dispatch!( diff --git a/sparse_strips/vello_cpu/src/fine/lowp/mod.rs b/sparse_strips/vello_cpu/src/fine/lowp/mod.rs index e4b74456c..a00e9505f 100644 --- a/sparse_strips/vello_cpu/src/fine/lowp/mod.rs +++ b/sparse_strips/vello_cpu/src/fine/lowp/mod.rs @@ -10,7 +10,6 @@ use crate::fine::{COLOR_COMPONENTS, Painter, SCRATCH_BUF_SIZE}; use crate::fine::{FineKernel, highp, u8_to_f32}; use crate::peniko::BlendMode; use crate::region::Region; -use crate::util::Div255Ext; use bytemuck::cast_slice; use vello_common::coarse::WideTile; use vello_common::encode::{EncodedGradient, EncodedImage}; @@ -18,7 +17,7 @@ use vello_common::fearless_simd::*; use vello_common::paint::PremulColor; use vello_common::pixmap::Pixmap; use vello_common::tile::Tile; -use vello_common::util::f32_to_u8; +use vello_common::util::{Div255Ext, f32_to_u8}; /// The kernel for doing rendering using u8/u16. #[derive(Clone, Copy, Debug)] @@ -163,8 +162,8 @@ mod fill { use crate::fine::lowp::compose::ComposeExt; use crate::fine::lowp::mix; use crate::peniko::{BlendMode, Mix}; - use crate::util::normalized_mul; use vello_common::fearless_simd::*; + use vello_common::util::normalized_mul_u8x32; pub(super) fn blend>>( simd: S, @@ -238,7 +237,7 @@ mod fill { src: u8x32, one_minus_alpha: u8x32, ) -> u8x32 { - s.narrow_u16x32(normalized_mul(bg, one_minus_alpha)) + src + s.narrow_u16x32(normalized_mul_u8x32(bg, one_minus_alpha)) + src } } @@ -247,8 +246,8 @@ mod alpha_fill { use crate::fine::lowp::compose::ComposeExt; use crate::fine::lowp::{extract_masks, mix}; use crate::peniko::{BlendMode, Mix}; - use crate::util::{Div255Ext, normalized_mul}; use vello_common::fearless_simd::*; + use vello_common::util::{Div255Ext, normalized_mul_u8x32}; pub(super) fn blend>>( simd: S, @@ -343,7 +342,7 @@ mod alpha_fill { let bg_v = u8x32::from_slice(s, dest); let mask_v = extract_masks(s, masks); - let inv_src_a_mask_a = one - s.narrow_u16x32(normalized_mul(src_a, mask_v)); + let inv_src_a_mask_a = one - s.narrow_u16x32(normalized_mul_u8x32(src_a, mask_v)); let p1 = s.widen_u8x32(bg_v) * s.widen_u8x32(inv_src_a_mask_a); let p2 = s.widen_u8x32(src_c) * s.widen_u8x32(mask_v); diff --git a/sparse_strips/vello_cpu/src/render.rs b/sparse_strips/vello_cpu/src/render.rs index ebcb5ef37..b2b7a91fe 100644 --- a/sparse_strips/vello_cpu/src/render.rs +++ b/sparse_strips/vello_cpu/src/render.rs @@ -428,6 +428,21 @@ impl RenderContext { self.glyph_caches.as_mut().unwrap().maintain(); } + /// Push a new clip path to the clip stack. + pub fn push_clip_path(&mut self, path: &BezPath) { + self.dispatcher.push_clip_path( + path, + self.fill_rule, + self.transform, + self.aliasing_threshold, + ); + } + + /// Pop a clip path from the clip stack. + pub fn pop_clip_path(&mut self) { + self.dispatcher.pop_clip_path(); + } + /// Flush any pending operations. /// /// This is a no-op when using the single-threaded render mode, and can be ignored. @@ -798,6 +813,7 @@ impl RenderContext { self.transform, self.aliasing_threshold, &mut strip_storage, + None, ); strip_start_indices.push(start_index); } @@ -808,6 +824,7 @@ impl RenderContext { self.transform, self.aliasing_threshold, &mut strip_storage, + None, ); strip_start_indices.push(start_index); } @@ -819,6 +836,7 @@ impl RenderContext { self.transform, self.aliasing_threshold, &mut strip_storage, + None, ); strip_start_indices.push(start_index); } @@ -830,6 +848,7 @@ impl RenderContext { self.transform, self.aliasing_threshold, &mut strip_storage, + None, ); strip_start_indices.push(start_index); } @@ -841,6 +860,7 @@ impl RenderContext { *glyph_transform, self.aliasing_threshold, &mut strip_storage, + None, ); strip_start_indices.push(start_index); } @@ -852,6 +872,7 @@ impl RenderContext { *glyph_transform, self.aliasing_threshold, &mut strip_storage, + None, ); strip_start_indices.push(start_index); } diff --git a/sparse_strips/vello_cpu/src/util.rs b/sparse_strips/vello_cpu/src/util.rs index 6acd24bde..1cc860f9e 100644 --- a/sparse_strips/vello_cpu/src/util.rs +++ b/sparse_strips/vello_cpu/src/util.rs @@ -3,8 +3,9 @@ use crate::peniko::{BlendMode, Compose, ImageQuality, Mix}; use vello_common::encode::EncodedImage; -use vello_common::fearless_simd::{Simd, SimdBase, f32x4, u8x32, u16x16, u16x32}; +use vello_common::fearless_simd::{Simd, SimdBase, f32x4, u8x32}; use vello_common::math::FloatExt; +use vello_common::util::Div255Ext; #[allow( dead_code, @@ -79,33 +80,6 @@ impl NormalizedMulExt for u8x32 { } } -pub(crate) trait Div255Ext { - fn div_255(self) -> Self; -} - -impl Div255Ext for u16x32 { - #[inline(always)] - fn div_255(self) -> Self { - let p1 = Self::splat(self.simd, 255); - let p2 = self + p1; - p2.shr(8) - } -} - -impl Div255Ext for u16x16 { - #[inline(always)] - fn div_255(self) -> Self { - let p1 = Self::splat(self.simd, 255); - let p2 = self + p1; - p2.shr(8) - } -} - -#[inline(always)] -pub(crate) fn normalized_mul(a: u8x32, b: u8x32) -> u16x32 { - (S::widen_u8x32(a.simd, a) * S::widen_u8x32(b.simd, b)).div_255() -} - pub(crate) trait BlendModeExt { fn is_default(&self) -> bool; } diff --git a/sparse_strips/vello_example_scenes/src/clip.rs b/sparse_strips/vello_example_scenes/src/clip.rs index 099b4fef9..95a600c70 100644 --- a/sparse_strips/vello_example_scenes/src/clip.rs +++ b/sparse_strips/vello_example_scenes/src/clip.rs @@ -18,18 +18,50 @@ use vello_common::peniko::Color; /// Clip scene state #[derive(Debug)] -pub struct ClipScene {} +pub struct ClipScene { + use_clip_path: bool, + num_circles: usize, +} impl ExampleScene for ClipScene { fn render(&mut self, ctx: &mut impl RenderingContext, root_transform: Affine) { - render(ctx, root_transform); + render(ctx, root_transform, self.use_clip_path, self.num_circles); + } + + fn handle_key(&mut self, key: &str) -> bool { + match key { + "c" | "C" => { + self.toggle_clip(); + true + } + "m" | "M" => { + self.add_circle(); + true + } + _ => false, + } } } impl ClipScene { /// Create a new `ClipScene` pub fn new() -> Self { - Self {} + Self { + use_clip_path: false, + num_circles: 1, + } + } + + /// Toggle using clip path + pub fn toggle_clip(&mut self) { + self.use_clip_path = !self.use_clip_path; + println!("Use clip path: {}", self.use_clip_path); + } + + /// Add another circle to the scene + pub fn add_circle(&mut self) { + self.num_circles += 1; + println!("Number of circles: {}", self.num_circles); } } @@ -47,7 +79,12 @@ fn draw_clipping_outline(ctx: &mut impl RenderingContext, path: &BezPath) { } /// Draws a deeply nested clip of circles. -pub fn render(ctx: &mut impl RenderingContext, root_transform: Affine) { +pub fn render( + ctx: &mut impl RenderingContext, + root_transform: Affine, + use_clip_path: bool, + num_circles: usize, +) { const INITIAL_RADIUS: f64 = 48.0; const RADIUS_DECREMENT: f64 = 2.5; const INNER_COUNT: usize = 10; @@ -67,26 +104,57 @@ pub fn render(ctx: &mut impl RenderingContext, root_transform: Affine) { DARK_GREEN, ]; - const COVER_RECT: Rect = Rect::new(0.0, 0.0, 100.0, 100.0); - const CENTER: Point = Point::new(50.0, 50.0); - let mut radius = INITIAL_RADIUS; + const SPACING: f64 = 120.0; + const BASE_X: f64 = 50.0; + const BASE_Y: f64 = 50.0; ctx.set_transform(root_transform); - for _ in 0..outer_count { - for color in COLORS.iter() { - let clip_circle = Circle::new(CENTER, radius).to_path(0.1); - draw_clipping_outline(ctx, &clip_circle); - ctx.push_clip_layer(&clip_circle); - ctx.set_paint(*color); - ctx.fill_rect(&COVER_RECT); + // Draw multiple circles in a checkerboard pattern + for circle_idx in 0..num_circles { + // Calculate checkerboard position + // Create a grid pattern where circles are placed in a checkerboard layout + let row = circle_idx / 4; + let col = circle_idx % 4; + + // Create checkerboard offset pattern + let offset_x = if (row + col) % 2 == 0 { + 0.0 + } else { + SPACING / 2.0 + }; + let x = BASE_X + col as f64 * SPACING + offset_x; + let y = BASE_Y + row as f64 * SPACING; + + let center = Point::new(x, y); + let cover_rect = Rect::new(x - 50.0, y - 50.0, x + 50.0, y + 50.0); + let mut radius = INITIAL_RADIUS; + + for _ in 0..outer_count { + for color in COLORS.iter() { + let clip_circle = Circle::new(center, radius).to_path(0.1); + draw_clipping_outline(ctx, &clip_circle); + if use_clip_path { + ctx.push_clip_path(&clip_circle); + } else { + ctx.push_clip_layer(&clip_circle); + } - radius -= RADIUS_DECREMENT; + ctx.set_paint(*color); + ctx.fill_rect(&cover_rect); + + radius -= RADIUS_DECREMENT; + } } - } - for _ in 0..outer_count { - for _ in COLORS.iter() { - ctx.pop_layer(); + + for _ in 0..outer_count { + for _ in COLORS.iter() { + if !use_clip_path { + ctx.pop_layer(); + } else { + ctx.pop_clip_path(); + } + } } } } diff --git a/sparse_strips/vello_example_scenes/src/lib.rs b/sparse_strips/vello_example_scenes/src/lib.rs index d1a3d8499..f96f1686a 100644 --- a/sparse_strips/vello_example_scenes/src/lib.rs +++ b/sparse_strips/vello_example_scenes/src/lib.rs @@ -49,6 +49,8 @@ pub trait RenderingContext: Sized { fn glyph_run(&mut self, font: &FontData) -> GlyphRunBuilder<'_, Self::GlyphRenderer>; /// Push a clip layer. fn push_clip_layer(&mut self, path: &BezPath); + /// Push a clip path. + fn push_clip_path(&mut self, path: &BezPath); /// Push a layer with blend mode, alpha, etc. fn push_layer( &mut self, @@ -59,6 +61,8 @@ pub trait RenderingContext: Sized { ); /// Pop the current layer. fn pop_layer(&mut self); + /// Pop the last clip path. + fn pop_clip_path(&mut self); /// Record rendering commands into a recording. fn record(&mut self, recording: &mut Recording, f: impl FnOnce(&mut Recorder<'_>)); /// Generate sparse strips for a recording. @@ -136,6 +140,14 @@ impl RenderingContext for RenderContext { fn execute_recording(&mut self, recording: &Recording) { Recordable::execute_recording(self, recording); } + + fn push_clip_path(&mut self, path: &BezPath) { + Self::push_clip_path(self, path); + } + + fn pop_clip_path(&mut self) { + Self::pop_clip_path(self); + } } impl RenderingContext for Scene { @@ -206,6 +218,14 @@ impl RenderingContext for Scene { fn execute_recording(&mut self, recording: &Recording) { Recordable::execute_recording(self, recording); } + + fn push_clip_path(&mut self, path: &BezPath) { + Self::push_clip_path(self, path); + } + + fn pop_clip_path(&mut self) { + Self::pop_clip_path(self); + } } /// Example scene that can maintain state between renders. diff --git a/sparse_strips/vello_hybrid/examples/winit/src/main.rs b/sparse_strips/vello_hybrid/examples/winit/src/main.rs index 5d429ff4b..a235f6fd6 100644 --- a/sparse_strips/vello_hybrid/examples/winit/src/main.rs +++ b/sparse_strips/vello_hybrid/examples/winit/src/main.rs @@ -8,6 +8,7 @@ use render_context::{RenderContext, RenderSurface, create_vello_renderer, create #[cfg(not(target_arch = "wasm32"))] use std::env; use std::sync::Arc; +use std::time::Instant; use vello_common::kurbo::{Affine, Point}; use vello_common::paint::ImageId; use vello_common::paint::ImageSource; @@ -34,6 +35,10 @@ struct App<'s> { transform: Affine, mouse_down: bool, last_cursor_position: Option, + last_frame_time: Option, + frame_count: u32, + fps_update_time: Instant, + accumulated_frame_time: f64, } fn main() { @@ -76,6 +81,7 @@ fn main() { 0, ); + let now = Instant::now(); let mut app = App { context: RenderContext::new(), renderers: vec![], @@ -86,6 +92,10 @@ fn main() { transform: Affine::IDENTITY, mouse_down: false, last_cursor_position: None, + last_frame_time: None, + frame_count: 0, + fps_update_time: now, + accumulated_frame_time: 0.0, }; let event_loop = EventLoop::new().unwrap(); @@ -129,7 +139,7 @@ impl ApplicationHandler for App<'_> { window.clone(), size.width, size.height, - wgpu::PresentMode::AutoVsync, + wgpu::PresentMode::Immediate, // Unlimited FPS mode wgpu::TextureFormat::Bgra8Unorm, )); @@ -267,6 +277,31 @@ impl ApplicationHandler for App<'_> { window.request_redraw(); } WindowEvent::RedrawRequested => { + // Measure frame time + let now = Instant::now(); + if let Some(last_time) = self.last_frame_time { + let frame_time = now.duration_since(last_time).as_secs_f64() * 1000.0; // Convert to milliseconds + self.accumulated_frame_time += frame_time; + self.frame_count += 1; + + // Update window title every second with average FPS + if now.duration_since(self.fps_update_time).as_secs_f64() >= 1.0 { + let avg_frame_time = self.accumulated_frame_time / self.frame_count as f64; + let avg_fps = 1000.0 / avg_frame_time; + println!("Average FPS: {avg_fps:.1}"); + window.set_title(&format!( + "Vello Hybrid - Scene {} - {:.1} FPS ({:.2}ms avg)", + self.current_scene, avg_fps, avg_frame_time + )); + + // Reset counters + self.frame_count = 0; + self.accumulated_frame_time = 0.0; + self.fps_update_time = now; + } + } + self.last_frame_time = Some(now); + self.scene.reset(); self.scene.set_transform(self.transform); @@ -310,6 +345,9 @@ impl ApplicationHandler for App<'_> { surface_texture.present(); device_handle.device.poll(wgpu::PollType::Poll).unwrap(); + + // Request continuous redraw for FPS measurement + window.request_redraw(); } _ => {} } diff --git a/sparse_strips/vello_hybrid/src/scene.rs b/sparse_strips/vello_hybrid/src/scene.rs index 40234d8c9..c8293c46c 100644 --- a/sparse_strips/vello_hybrid/src/scene.rs +++ b/sparse_strips/vello_hybrid/src/scene.rs @@ -5,6 +5,7 @@ use alloc::vec; use alloc::vec::Vec; +use vello_common::clip::ClipContext; use vello_common::coarse::{MODE_HYBRID, Wide}; use vello_common::encode::{EncodeExt, EncodedPaint}; use vello_common::fearless_simd::Level; @@ -73,6 +74,7 @@ pub struct Scene { pub(crate) width: u16, pub(crate) height: u16, pub(crate) wide: Wide, + clip_context: ClipContext, pub(crate) paint: PaintType, pub(crate) paint_transform: Affine, pub(crate) aliasing_threshold: Option, @@ -100,6 +102,7 @@ impl Scene { width, height, wide: Wide::::new(width, height), + clip_context: ClipContext::new(), aliasing_threshold: None, paint: render_state.paint, paint_transform: render_state.paint_transform, @@ -185,10 +188,27 @@ impl Scene { transform, aliasing_threshold, &mut self.strip_storage, + self.clip_context.get(), ); wide.generate(&self.strip_storage.strips, paint, 0); } + /// Push a new clip path to the clip stack. + pub fn push_clip_path(&mut self, path: &BezPath) { + self.clip_context.push_clip( + path, + &mut self.strip_generator, + self.fill_rule, + self.transform, + self.aliasing_threshold, + ); + } + + /// Pop a clip path from the clip stack. + pub fn pop_clip_path(&mut self) { + self.clip_context.pop_clip(); + } + /// Stroke a path with the current paint and stroke settings. pub fn stroke_path(&mut self, path: &BezPath) { if !self.paint_visible { @@ -215,6 +235,7 @@ impl Scene { transform, aliasing_threshold, &mut self.strip_storage, + self.clip_context.get(), ); wide.generate(&self.strip_storage.strips, paint, 0); @@ -267,6 +288,7 @@ impl Scene { self.transform, self.aliasing_threshold, &mut self.strip_storage, + self.clip_context.get(), ); Some(self.strip_storage.strips.as_slice()) @@ -353,6 +375,7 @@ impl Scene { pub fn reset(&mut self) { self.wide.reset(); self.strip_generator.reset(); + self.clip_context.reset(); self.strip_storage.clear(); self.encoded_paints.clear(); @@ -527,6 +550,7 @@ impl Scene { self.transform, self.aliasing_threshold, &mut strip_storage, + None, ); strip_start_indices.push(start_index); } @@ -537,6 +561,7 @@ impl Scene { self.transform, self.aliasing_threshold, &mut strip_storage, + None, ); strip_start_indices.push(start_index); } @@ -547,6 +572,7 @@ impl Scene { self.transform, self.aliasing_threshold, &mut strip_storage, + None, ); strip_start_indices.push(start_index); } @@ -557,6 +583,7 @@ impl Scene { self.transform, self.aliasing_threshold, &mut strip_storage, + None, ); strip_start_indices.push(start_index); } @@ -567,6 +594,7 @@ impl Scene { *glyph_transform, self.aliasing_threshold, &mut strip_storage, + None, ); strip_start_indices.push(start_index); } @@ -577,6 +605,7 @@ impl Scene { *glyph_transform, self.aliasing_threshold, &mut strip_storage, + None, ); strip_start_indices.push(start_index); } diff --git a/sparse_strips/vello_sparse_tests/snapshots/clip_non_isolated_deeply_nested_circles.png b/sparse_strips/vello_sparse_tests/snapshots/clip_non_isolated_deeply_nested_circles.png new file mode 100644 index 000000000..8cea7975d --- /dev/null +++ b/sparse_strips/vello_sparse_tests/snapshots/clip_non_isolated_deeply_nested_circles.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d62873acad1c121caa7b881ddc669cd4a2f9e272c76f3da24316774e9ebb097 +size 5720 diff --git a/sparse_strips/vello_sparse_tests/snapshots/clip_non_isolated_outside_canvas.png b/sparse_strips/vello_sparse_tests/snapshots/clip_non_isolated_outside_canvas.png new file mode 100644 index 000000000..1385b5b73 --- /dev/null +++ b/sparse_strips/vello_sparse_tests/snapshots/clip_non_isolated_outside_canvas.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:acc4f4602f91b71ca283af813d72694e7cca36b27690f0561e0be5f55638d746 +size 71 diff --git a/sparse_strips/vello_sparse_tests/snapshots/clip_non_isolated_rectangle_with_star_evenodd.png b/sparse_strips/vello_sparse_tests/snapshots/clip_non_isolated_rectangle_with_star_evenodd.png new file mode 100644 index 000000000..99c68ec33 --- /dev/null +++ b/sparse_strips/vello_sparse_tests/snapshots/clip_non_isolated_rectangle_with_star_evenodd.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6aef5976250fcb517718c06c045da9cd052b8a52c2902026a9217bfdc8ae18b9 +size 840 diff --git a/sparse_strips/vello_sparse_tests/tests/clip.rs b/sparse_strips/vello_sparse_tests/tests/clip.rs index ad6fb6dc1..11acb0fcc 100644 --- a/sparse_strips/vello_sparse_tests/tests/clip.rs +++ b/sparse_strips/vello_sparse_tests/tests/clip.rs @@ -326,3 +326,71 @@ fn clip_completely_in_out_of_bounds_wide_tile(ctx: &mut impl Renderer) { ctx.push_clip_layer(&Rect::new(300.0, 8.0, 350.0, 48.0).to_path(0.1)); ctx.pop_layer(); } + +#[vello_test(width = 16, height = 16)] +fn clip_non_isolated_outside_canvas(ctx: &mut impl Renderer) { + // Should be completely clipped. + let clip_rect = Rect::new(0.0, 0.0, 16.0, 16.0); + ctx.push_clip_path(&clip_rect.to_path(0.1)); + + let rect = Rect::new(16.0, -16.0, 32.0, 0.0); + ctx.set_paint(REBECCA_PURPLE); + ctx.fill_rect(&rect); + ctx.pop_clip_path(); +} + +#[vello_test] +fn clip_non_isolated_rectangle_with_star_evenodd(ctx: &mut impl Renderer) { + let rect = Rect::new(0.0, 0.0, 100.0, 100.0); + let star_path = crossed_line_star(); + + ctx.set_fill_rule(Fill::EvenOdd); + ctx.push_clip_path(&star_path); + ctx.set_paint(REBECCA_PURPLE); + ctx.fill_rect(&rect); + ctx.pop_clip_path(); +} + +#[vello_test(cpu_u8_tolerance = 1)] +fn clip_non_isolated_deeply_nested_circles(ctx: &mut impl Renderer) { + const INITIAL_RADIUS: f64 = 48.0; + const RADIUS_DECREMENT: f64 = 2.5; + const INNER_COUNT: usize = 10; + // `.ceil()` is not constant-evaluatable, so we have to do this at runtime. + let outer_count: usize = + (INITIAL_RADIUS / RADIUS_DECREMENT / INNER_COUNT as f64).ceil() as usize; + const COLORS: [Color; INNER_COUNT] = [ + RED, + DARK_BLUE, + DARK_GREEN, + REBECCA_PURPLE, + BLACK, + BLUE, + GREEN, + RED, + DARK_BLUE, + DARK_GREEN, + ]; + + const COVER_RECT: Rect = Rect::new(0.0, 0.0, 100.0, 100.0); + const CENTER: Point = Point::new(50.0, 50.0); + let mut radius = INITIAL_RADIUS; + + for _ in 0..outer_count { + for color in COLORS.iter() { + let clip_circle = Circle::new(CENTER, radius).to_path(0.1); + draw_clipping_outline(ctx, &clip_circle); + ctx.push_clip_path(&clip_circle); + + ctx.set_paint(*color); + ctx.fill_rect(&COVER_RECT); + + radius -= RADIUS_DECREMENT; + } + } + for _ in 0..outer_count { + for _ in COLORS.iter() { + ctx.pop_clip_path(); + } + } +} diff --git a/sparse_strips/vello_sparse_tests/tests/renderer.rs b/sparse_strips/vello_sparse_tests/tests/renderer.rs index e2ce9a185..b1dcc7f9c 100644 --- a/sparse_strips/vello_sparse_tests/tests/renderer.rs +++ b/sparse_strips/vello_sparse_tests/tests/renderer.rs @@ -41,10 +41,12 @@ pub(crate) trait Renderer: Sized { ); fn flush(&mut self); fn push_clip_layer(&mut self, path: &BezPath); + fn push_clip_path(&mut self, path: &BezPath); fn push_blend_layer(&mut self, blend_mode: BlendMode); fn push_opacity_layer(&mut self, opacity: f32); fn push_mask_layer(&mut self, mask: Mask); fn pop_layer(&mut self); + fn pop_clip_path(&mut self); fn set_stroke(&mut self, stroke: Stroke); fn set_paint(&mut self, paint: impl Into); fn set_paint_transform(&mut self, affine: Affine); @@ -121,6 +123,10 @@ impl Renderer for RenderContext { Self::push_clip_layer(self, path); } + fn push_clip_path(&mut self, path: &BezPath) { + Self::push_clip_path(self, path); + } + fn push_blend_layer(&mut self, blend_mode: BlendMode) { Self::push_blend_layer(self, blend_mode); } @@ -137,6 +143,10 @@ impl Renderer for RenderContext { Self::pop_layer(self); } + fn pop_clip_path(&mut self) { + Self::pop_clip_path(self); + } + fn set_stroke(&mut self, stroke: Stroke) { Self::set_stroke(self, stroke); } @@ -307,6 +317,10 @@ impl Renderer for HybridRenderer { self.scene.push_clip_layer(path); } + fn push_clip_path(&mut self, path: &BezPath) { + self.scene.push_clip_path(path); + } + fn push_blend_layer(&mut self, blend_mode: BlendMode) { self.scene.push_layer(None, Some(blend_mode), None, None); } @@ -323,6 +337,10 @@ impl Renderer for HybridRenderer { self.scene.pop_layer(); } + fn pop_clip_path(&mut self) { + self.scene.pop_clip_path(); + } + fn set_stroke(&mut self, stroke: Stroke) { self.scene.set_stroke(stroke); } @@ -575,6 +593,10 @@ impl Renderer for HybridRenderer { self.scene.glyph_run(font) } + fn push_clip_path(&mut self, path: &BezPath) { + self.scene.push_clip_path(path); + } + fn push_layer( &mut self, clip: Option<&BezPath>, @@ -607,6 +629,10 @@ impl Renderer for HybridRenderer { self.scene.pop_layer(); } + fn pop_clip_path(&mut self) { + self.scene.pop_clip_path(); + } + fn set_stroke(&mut self, stroke: Stroke) { self.scene.set_stroke(stroke); }