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>
This commit is contained in:
sagudev 2025-08-03 18:00:46 +02:00 committed by GitHub
parent fe9341dd46
commit 874645ae86
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 73 additions and 34 deletions

View file

@ -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<F: FnOnce(&mut Self)>(&mut self, draw_options: &CompositionOptions, f: F) {
fn with_composition<F: FnOnce(&mut Self)>(
&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<f32>,
) {
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<f32>,
) {
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(),

View file

@ -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<f64>,
source: Rect<f64>,
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<f32>,
) {
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<f32>,
) {
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<f32>,
) {
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<f32>,
) {
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<f32>,
) {
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<vello_cpu::PaintType> 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)
},
}
}
}