From 09f0e20e2998b5da39e55fd860ac1f64db3ad6c1 Mon Sep 17 00:00:00 2001 From: sagudev <16504129+sagudev@users.noreply.github.com> Date: Fri, 1 Aug 2025 07:05:15 +0200 Subject: [PATCH] canvas: Clear vello scene if possible (#38356) Vello scene only ever grows, so we need to clear it as soon as it's possible (in clear rect). This PR also adds ignore_clips to vello_backend (already exists in vello_cpu_backend). Testing: Behavior is verified by existing tests, as this is mainly a change for performance. There are currently no performance tests. This makes bunnymark actually playable (from 3 FPS to 20 FPS on vello_cpu). --------- Signed-off-by: sagudev <16504129+sagudev@users.noreply.github.com> --- components/canvas/vello_backend.rs | 96 +++++++++++++------ components/canvas/vello_cpu_backend.rs | 23 +++++ .../2d.path.clip.winding.evenodd.1.html.ini | 4 + .../2d.path.clip.winding.evenodd.2.html.ini | 4 + .../2d.imageData.put.clip.html.ini | 5 - .../2d.imageData.put.unchanged.html.ini | 4 +- 6 files changed, 102 insertions(+), 34 deletions(-) create mode 100644 tests/wpt/meta/html/canvas/element/path-objects/2d.path.clip.winding.evenodd.1.html.ini create mode 100644 tests/wpt/meta/html/canvas/element/path-objects/2d.path.clip.winding.evenodd.2.html.ini delete mode 100644 tests/wpt/meta/html/canvas/element/pixel-manipulation/2d.imageData.put.clip.html.ini diff --git a/components/canvas/vello_backend.rs b/components/canvas/vello_backend.rs index 46fced39281..f8901da673c 100644 --- a/components/canvas/vello_backend.rs +++ b/components/canvas/vello_backend.rs @@ -23,6 +23,7 @@ use compositing_traits::SerializableImageData; use euclid::default::{Point2D, Rect, Size2D, Transform2D}; use fonts::{ByteIndex, FontIdentifier, FontTemplateRefMethods as _}; use ipc_channel::ipc::IpcSharedMemory; +use kurbo::Shape as _; use pixels::{Snapshot, SnapshotAlphaMode, SnapshotPixelFormat}; use range::Range; use vello::wgpu::{ @@ -51,6 +52,7 @@ pub(crate) struct VelloDrawTarget { renderer: Rc>, scene: vello::Scene, size: Size2D, + clips: Vec, } fn options() -> vello::RendererOptions { @@ -152,6 +154,35 @@ impl VelloDrawTarget { buffer.unmap(); result } + + fn ignore_clips(&mut self, f: impl FnOnce(&mut Self)) { + // pop all clip layers + for _ in &self.clips { + self.scene.pop_layer(); + } + f(self); + // push all clip layers back + for path in &self.clips { + self.scene + .push_layer(peniko::Mix::Clip, 1.0, kurbo::Affine::IDENTITY, &path.0); + } + } + + fn is_viewport_cleared(&mut self, rect: &Rect, transform: Transform2D) -> bool { + let transformed_rect = transform.outer_transformed_rect(rect); + if transformed_rect.is_empty() { + return false; + } + let viewport: Rect = Rect::from_size(self.get_size().cast()); + let Some(clip) = self.clips.iter().try_fold(viewport, |acc, e| { + acc.intersection(&e.0.bounding_box().into()) + }) else { + // clip makes no visible side effects + return false; + }; + transformed_rect.cast().contains_rect(&viewport) && // whole viewport is cleared + clip.contains_rect(&viewport) // viewport is not clipped + } } impl GenericDrawTarget for VelloDrawTarget { @@ -188,10 +219,18 @@ impl GenericDrawTarget for VelloDrawTarget { renderer: Rc::new(RefCell::new(renderer)), scene, size, + clips: Vec::new(), } } fn clear_rect(&mut self, rect: &Rect, transform: Transform2D) { + // vello scene only ever grows, + // so we need to use every opportunity to shrink it + if self.is_viewport_cleared(rect, transform) { + self.scene.reset(); + self.clips.clear(); // no clips are affecting rendering + return; + } let rect: kurbo::Rect = rect.cast().into(); let transform = transform.cast().into(); self.scene @@ -210,35 +249,30 @@ impl GenericDrawTarget for VelloDrawTarget { let destination: kurbo::Point = destination.cast::().into(); let rect = kurbo::Rect::from_origin_size(destination, source.size.cast()); - // TODO: ignore clip from prev layers - // this will require creating a stacks of applicable clips - // that will be popped and reinserted after - // or we could impl this in vello directly + self.ignore_clips(|self_| { + self_ + .scene + .push_layer(peniko::Compose::Copy, 1.0, kurbo::Affine::IDENTITY, &rect); - // then there is also this nasty vello bug where clipping does not work correctly: - // https://xi.zulipchat.com/#narrow/channel/197075-vello/topic/Servo.202D.20canvas.20backend/near/525153593 + self_.scene.fill( + peniko::Fill::NonZero, + kurbo::Affine::IDENTITY, + &peniko::Image { + data: peniko::Blob::from(surface), + format: peniko::ImageFormat::Rgba8, + width: source.size.width as u32, + height: source.size.height as u32, + x_extend: peniko::Extend::Pad, + y_extend: peniko::Extend::Pad, + quality: peniko::ImageQuality::Low, + alpha: 1.0, + }, + Some(kurbo::Affine::translate(destination.to_vec2())), + &rect, + ); - self.scene - .push_layer(peniko::Compose::Copy, 1.0, kurbo::Affine::IDENTITY, &rect); - - self.scene.fill( - peniko::Fill::NonZero, - kurbo::Affine::IDENTITY, - &peniko::Image { - data: peniko::Blob::from(surface), - format: peniko::ImageFormat::Rgba8, - width: source.size.width as u32, - height: source.size.height as u32, - x_extend: peniko::Extend::Pad, - y_extend: peniko::Extend::Pad, - quality: peniko::ImageQuality::Low, - alpha: 1.0, - }, - Some(kurbo::Affine::translate(destination.to_vec2())), - &rect, - ); - - self.scene.pop_layer(); + self_.scene.pop_layer(); + }); } fn create_similar_draw_target(&self, size: &Size2D) -> Self { @@ -248,6 +282,7 @@ impl GenericDrawTarget for VelloDrawTarget { renderer: self.renderer.clone(), scene: vello::Scene::new(), size: size.cast(), + clips: Vec::new(), } } @@ -410,12 +445,17 @@ impl GenericDrawTarget for VelloDrawTarget { } fn pop_clip(&mut self) { - self.scene.pop_layer(); + if self.clips.pop().is_some() { + self.scene.pop_layer(); + } } fn push_clip(&mut self, path: &Path, _fill_rule: FillRule, transform: Transform2D) { self.scene .push_layer(peniko::Mix::Clip, 1.0, transform.cast().into(), &path.0); + let mut path = path.clone(); + path.transform(transform.cast()); + self.clips.push(path); } fn push_clip_rect(&mut self, rect: &Rect) { diff --git a/components/canvas/vello_cpu_backend.rs b/components/canvas/vello_cpu_backend.rs index d2fc3f36bae..78a9d0418cd 100644 --- a/components/canvas/vello_cpu_backend.rs +++ b/components/canvas/vello_cpu_backend.rs @@ -86,6 +86,22 @@ impl VelloCPUDrawTarget { fn size(&self) -> Size2D { Size2D::new(self.ctx.width(), self.ctx.height()).cast() } + + fn is_viewport_cleared(&mut self, rect: &Rect, transform: Transform2D) -> bool { + let transformed_rect = transform.outer_transformed_rect(rect); + if transformed_rect.is_empty() { + return false; + } + let viewport: Rect = Rect::from_size(self.get_size().cast()); + let Some(clip) = self.clips.iter().try_fold(viewport, |acc, e| { + acc.intersection(&e.0.bounding_box().into()) + }) else { + // clip makes no visible side effects + return false; + }; + transformed_rect.cast().contains_rect(&viewport) && // whole viewport is cleared + clip.contains_rect(&viewport) // viewport is not clipped + } } impl GenericDrawTarget for VelloCPUDrawTarget { @@ -101,6 +117,13 @@ impl GenericDrawTarget for VelloCPUDrawTarget { } fn clear_rect(&mut self, rect: &Rect, transform: Transform2D) { + // vello_cpu RenderingContext only ever grows, + // so we need to use every opportunity to shrink it + if self.is_viewport_cleared(rect, transform) { + self.ctx.reset(); + self.clips.clear(); // no clips are affecting rendering + return; + } let rect: kurbo::Rect = rect.cast().into(); let mut clip_path = rect.to_path(0.1); clip_path.apply_affine(transform.cast().into()); diff --git a/tests/wpt/meta/html/canvas/element/path-objects/2d.path.clip.winding.evenodd.1.html.ini b/tests/wpt/meta/html/canvas/element/path-objects/2d.path.clip.winding.evenodd.1.html.ini new file mode 100644 index 00000000000..62154a971da --- /dev/null +++ b/tests/wpt/meta/html/canvas/element/path-objects/2d.path.clip.winding.evenodd.1.html.ini @@ -0,0 +1,4 @@ +[2d.path.clip.winding.evenodd.1.html] + [evenodd winding number rule works in clip] + expected: + if subsuite == "vello_canvas": FAIL diff --git a/tests/wpt/meta/html/canvas/element/path-objects/2d.path.clip.winding.evenodd.2.html.ini b/tests/wpt/meta/html/canvas/element/path-objects/2d.path.clip.winding.evenodd.2.html.ini new file mode 100644 index 00000000000..c5b16f93111 --- /dev/null +++ b/tests/wpt/meta/html/canvas/element/path-objects/2d.path.clip.winding.evenodd.2.html.ini @@ -0,0 +1,4 @@ +[2d.path.clip.winding.evenodd.2.html] + [evenodd winding number rule works in clip] + expected: + if subsuite == "vello_canvas": FAIL diff --git a/tests/wpt/meta/html/canvas/element/pixel-manipulation/2d.imageData.put.clip.html.ini b/tests/wpt/meta/html/canvas/element/pixel-manipulation/2d.imageData.put.clip.html.ini deleted file mode 100644 index 2d7c071dd03..00000000000 --- a/tests/wpt/meta/html/canvas/element/pixel-manipulation/2d.imageData.put.clip.html.ini +++ /dev/null @@ -1,5 +0,0 @@ -[2d.imageData.put.clip.html] - [putImageData() is not affected by clipping regions] - bug: https://github.com/linebender/vello/issues/1088 - expected: - if subsuite == "vello_canvas": FAIL diff --git a/tests/wpt/meta/html/canvas/element/pixel-manipulation/2d.imageData.put.unchanged.html.ini b/tests/wpt/meta/html/canvas/element/pixel-manipulation/2d.imageData.put.unchanged.html.ini index 124b05bd597..eab58d20df9 100644 --- a/tests/wpt/meta/html/canvas/element/pixel-manipulation/2d.imageData.put.unchanged.html.ini +++ b/tests/wpt/meta/html/canvas/element/pixel-manipulation/2d.imageData.put.unchanged.html.ini @@ -1,3 +1,5 @@ [2d.imageData.put.unchanged.html] [putImageData(getImageData(...), ...) has no effect] - expected: FAIL + expected: + if subsuite == "": FAIL + if subsuite == "vello_cpu": FAIL