From 874645ae86d7a95d3c6cfb5d0e74c06f90b76e47 Mon Sep 17 00:00:00 2001 From: sagudev <16504129+sagudev@users.noreply.github.com> Date: Sun, 3 Aug 2025 18:00:46 +0200 Subject: [PATCH] canvas: Do not use vello layers for opacity or default composition (#38440) In this PR we moved global alpha handling using temporary layers to mutation of paint in vello_cpu (we were already doing this in vello classic). This + not using temporary layer for SrcOver (default and most common composition operation) allows us to remove most temporary layers from `with_composition`. This slightly improves performance of vello backend, but drastically improves performance of vello_cpu. We are now able to render bunnymark (100 bunnies) with 60 FPS. In the future we could cache current layer and change it when compositing operation changes, although that would complicate clips, so improvement is questionable. Testing: Existing WPT tests for functionality, but we do not have any performance tests. Signed-off-by: sagudev <16504129+sagudev@users.noreply.github.com> --- components/canvas/vello_backend.rs | 29 +++++++--- components/canvas/vello_cpu_backend.rs | 78 +++++++++++++++++--------- 2 files changed, 73 insertions(+), 34 deletions(-) diff --git a/components/canvas/vello_backend.rs b/components/canvas/vello_backend.rs index 326539829bc..5c8f745c0b3 100644 --- a/components/canvas/vello_backend.rs +++ b/components/canvas/vello_backend.rs @@ -17,7 +17,8 @@ use std::num::NonZeroUsize; use std::rc::Rc; use canvas_traits::canvas::{ - CompositionOptions, FillOrStrokeStyle, FillRule, LineOptions, Path, ShadowOptions, + CompositionOptions, CompositionOrBlending, CompositionStyle, FillOrStrokeStyle, FillRule, + LineOptions, Path, ShadowOptions, }; use compositing_traits::SerializableImageData; use euclid::default::{Point2D, Rect, Size2D, Transform2D}; @@ -134,9 +135,19 @@ impl VelloDrawTarget { } } - fn with_draw_options(&mut self, draw_options: &CompositionOptions, f: F) { + fn with_composition( + &mut self, + composition_operation: CompositionOrBlending, + f: F, + ) { + // Fast-path for default and most common composition operation + if composition_operation == CompositionOrBlending::Composition(CompositionStyle::SourceOver) + { + f(self); + return; + } self.scene.push_layer( - draw_options.composition_operation.convert(), + composition_operation.convert(), 1.0, kurbo::Affine::IDENTITY, &kurbo::Rect::ZERO.with_size(self.size.cast()), @@ -304,7 +315,7 @@ impl GenericDrawTarget for VelloDrawTarget { self.ensure_drawing(); let scale_up = dest.size.width > source.size.width || dest.size.height > source.size.height; let shape: kurbo::Rect = dest.into(); - self.with_draw_options(&composition_options, move |self_| { + self.with_composition(composition_options.composition_operation, move |self_| { self_.scene.fill( peniko::Fill::NonZero, transform.cast().into(), @@ -359,7 +370,7 @@ impl GenericDrawTarget for VelloDrawTarget { transform: Transform2D, ) { self.ensure_drawing(); - self.with_draw_options(&composition_options, |self_| { + self.with_composition(composition_options.composition_operation, |self_| { self_.scene.fill( fill_rule.convert(), transform.cast().into(), @@ -381,7 +392,7 @@ impl GenericDrawTarget for VelloDrawTarget { self.ensure_drawing(); let pattern = convert_to_brush(style, composition_options); let transform = transform.cast().into(); - self.with_draw_options(&composition_options, |self_| { + self.with_composition(composition_options.composition_operation, |self_| { let mut advance = 0.; for run in text_runs.iter() { let glyphs = &run.glyphs; @@ -443,7 +454,7 @@ impl GenericDrawTarget for VelloDrawTarget { let pattern = convert_to_brush(style, composition_options); let transform = transform.cast().into(); let rect: kurbo::Rect = rect.cast().into(); - self.with_draw_options(&composition_options, |self_| { + self.with_composition(composition_options.composition_operation, |self_| { self_ .scene .fill(peniko::Fill::NonZero, transform, &pattern, None, &rect); @@ -489,7 +500,7 @@ impl GenericDrawTarget for VelloDrawTarget { transform: Transform2D, ) { self.ensure_drawing(); - self.with_draw_options(&composition_options, |self_| { + self.with_composition(composition_options.composition_operation, |self_| { self_.scene.stroke( &line_options.convert(), transform.cast().into(), @@ -510,7 +521,7 @@ impl GenericDrawTarget for VelloDrawTarget { ) { self.ensure_drawing(); let rect: kurbo::Rect = rect.cast().into(); - self.with_draw_options(&composition_options, |self_| { + self.with_composition(composition_options.composition_operation, |self_| { self_.scene.stroke( &line_options.convert(), transform.cast().into(), diff --git a/components/canvas/vello_cpu_backend.rs b/components/canvas/vello_cpu_backend.rs index f1f45389970..6074e85a935 100644 --- a/components/canvas/vello_cpu_backend.rs +++ b/components/canvas/vello_cpu_backend.rs @@ -7,7 +7,8 @@ use std::collections::HashMap; use std::sync::Arc; use canvas_traits::canvas::{ - CompositionOptions, FillOrStrokeStyle, FillRule, LineOptions, Path, ShadowOptions, + CompositionOptions, CompositionOrBlending, CompositionStyle, FillOrStrokeStyle, FillRule, + LineOptions, Path, ShadowOptions, }; use compositing_traits::SerializableImageData; use euclid::default::{Point2D, Rect, Size2D, Transform2D}; @@ -58,15 +59,16 @@ pub(crate) struct VelloCPUDrawTarget { impl VelloCPUDrawTarget { fn with_composition( &mut self, - composition_options: &CompositionOptions, + composition_operation: CompositionOrBlending, f: impl FnOnce(&mut Self), ) { - self.ctx.push_layer( - None, - Some(composition_options.composition_operation.convert()), - Some(composition_options.alpha as f32), - None, - ); + // Fast-path for default and most common composition operation + if composition_operation == CompositionOrBlending::Composition(CompositionStyle::SourceOver) + { + f(self); + return; + } + self.ctx.push_blend_layer(composition_operation.convert()); f(self); self.ctx.pop_layer(); } @@ -210,7 +212,7 @@ impl GenericDrawTarget for VelloCPUDrawTarget { fn draw_surface( &mut self, - surface: Self::SourceSurface, + mut surface: Self::SourceSurface, dest: Rect, source: Rect, filter: Filter, @@ -219,7 +221,12 @@ impl GenericDrawTarget for VelloCPUDrawTarget { ) { self.ensure_drawing(); let scale_up = dest.size.width > source.size.width || dest.size.height > source.size.height; - self.with_composition(&composition_options, move |self_| { + if composition_options.alpha != 1.0 { + Arc::get_mut(&mut surface) + .expect("surface should be owned") + .multiply_alpha((composition_options.alpha * 255.0) as u8); + } + self.with_composition(composition_options.composition_operation, move |self_| { self_.ctx.set_transform(transform.cast().into()); self_.ctx.set_paint(vello_cpu::Image { source: vello_cpu::ImageSource::Pixmap(surface), @@ -268,11 +275,10 @@ impl GenericDrawTarget for VelloCPUDrawTarget { transform: Transform2D, ) { self.ensure_drawing(); - let paint: vello_cpu::PaintType = style.convert(); - self.with_composition(&composition_options, |self_| { + self.with_composition(composition_options.composition_operation, |self_| { self_.ctx.set_transform(transform.cast().into()); self_.ctx.set_fill_rule(fill_rule.convert()); - self_.ctx.set_paint(paint); + self_.ctx.set_paint(paint(style, composition_options.alpha)); self_.ctx.fill_path(&path.0); }); self.ctx.set_fill_rule(peniko::Fill::NonZero); @@ -287,10 +293,9 @@ impl GenericDrawTarget for VelloCPUDrawTarget { transform: Transform2D, ) { self.ensure_drawing(); - let style: vello_cpu::PaintType = style.convert(); - self.ctx.set_paint(style); + self.ctx.set_paint(paint(style, composition_options.alpha)); self.ctx.set_transform(transform.cast().into()); - self.with_composition(&composition_options, |self_| { + self.with_composition(composition_options.composition_operation, |self_| { let mut advance = 0.; for run in text_runs.iter() { let glyphs = &run.glyphs; @@ -346,10 +351,9 @@ impl GenericDrawTarget for VelloCPUDrawTarget { transform: Transform2D, ) { self.ensure_drawing(); - let paint: vello_cpu::PaintType = style.convert(); - self.with_composition(&composition_options, |self_| { + self.with_composition(composition_options.composition_operation, |self_| { self_.ctx.set_transform(transform.cast().into()); - self_.ctx.set_paint(paint); + self_.ctx.set_paint(paint(style, composition_options.alpha)); self_.ctx.fill_rect(&rect.cast().into()); }) } @@ -395,10 +399,9 @@ impl GenericDrawTarget for VelloCPUDrawTarget { transform: Transform2D, ) { self.ensure_drawing(); - let paint: vello_cpu::PaintType = style.convert(); - self.with_composition(&composition_options, |self_| { + self.with_composition(composition_options.composition_operation, |self_| { self_.ctx.set_transform(transform.cast().into()); - self_.ctx.set_paint(paint); + self_.ctx.set_paint(paint(style, composition_options.alpha)); self_.ctx.set_stroke(line_options.convert()); self_.ctx.stroke_path(&path.0); }) @@ -413,10 +416,9 @@ impl GenericDrawTarget for VelloCPUDrawTarget { transform: Transform2D, ) { self.ensure_drawing(); - let paint: vello_cpu::PaintType = style.convert(); - self.with_composition(&composition_options, |self_| { + self.with_composition(composition_options.composition_operation, |self_| { self_.ctx.set_transform(transform.cast().into()); - self_.ctx.set_paint(paint); + self_.ctx.set_paint(paint(style, composition_options.alpha)); self_.ctx.set_stroke(line_options.convert()); self_.ctx.stroke_rect(&rect.cast().into()); }) @@ -520,3 +522,29 @@ impl Convert for FillOrStrokeStyle { } } } + +fn paint(style: FillOrStrokeStyle, alpha: f64) -> vello_cpu::PaintType { + assert!((0.0..=1.0).contains(&alpha)); + let paint = style.convert(); + if alpha == 1.0 { + paint + } else { + match paint { + vello_cpu::PaintType::Solid(alpha_color) => { + vello_cpu::PaintType::Solid(alpha_color.multiply_alpha(alpha as f32)) + }, + vello_cpu::PaintType::Gradient(gradient) => { + vello_cpu::PaintType::Gradient(gradient.multiply_alpha(alpha as f32)) + }, + vello_cpu::PaintType::Image(mut image) => { + match &mut image.source { + vello_cpu::ImageSource::Pixmap(pixmap) => Arc::get_mut(pixmap) + .expect("pixmap should not be shared with anyone at this point") + .multiply_alpha((alpha * 255.0) as u8), + vello_cpu::ImageSource::OpaqueId(_) => unimplemented!(), + }; + vello_cpu::PaintType::Image(image) + }, + } + } +}