canvas: Add vello_cpu backend (#38282)

vello_cpu does not have any tests timeouts, because we do not need
download stuff from GPU as all work happens on CPU. So performance wise
it's better then classic vello at least for our usecase. There are some
vello bugs, but I think we will be able to sort them out within
upstream, eventually. Interestingly enough there are no new PASS like
they were with classic vello.

Difference with raqote can be observed here:
https://github.com/sagudev/servo/actions/runs/16549241085/attempts/1#summary-46802486798

## Known vello problems:

- https://github.com/linebender/vello/issues/1119
- https://github.com/linebender/vello/issues/1056
-
`/html/canvas/element/fill-and-stroke-styles/2d.gradient.interpolate.coloralpha.html`
- `kurbo::Cap::Butt` is defect (only visible with big lineWidth)
https://github.com/linebender/vello/issues/1063
  - `/html/canvas/element/line-styles/2d.line.cross.html`
  - `/html/canvas/element/line-styles/2d.line.miter.acute.html`
- other lack of strong correct problems
(https://github.com/linebender/vello/issues/1063#issuecomment-2998084736):
  - `/html/canvas/element/path-objects/2d.path.rect.selfintersect.html`
- `putImageData(getImageData(...), ...)` is lossy (precision problems,
might be due to ImageData being unmultiplied)
-
`/html/canvas/element/pixel-manipulation/2d.imageData.put.unchanged.html`

Testing: Tested using vello_cpu_canvas subsuite

---------

Signed-off-by: sagudev <16504129+sagudev@users.noreply.github.com>
This commit is contained in:
sagudev 2025-07-27 21:56:38 +02:00 committed by GitHub
parent 15be75f955
commit 056b1538c0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 694 additions and 11 deletions

48
Cargo.lock generated
View file

@ -1087,6 +1087,7 @@ dependencies = [
"stylo",
"unicode-script",
"vello",
"vello_cpu",
"webrender_api",
]
@ -1335,6 +1336,9 @@ name = "color"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ae467d04a8a8aea5d9a49018a6ade2e4221d92968e8ce55a48c0b1164e5f698"
dependencies = [
"bytemuck",
]
[[package]]
name = "color_quant"
@ -1355,7 +1359,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.48.0",
"windows-sys 0.52.0",
]
[[package]]
@ -2464,6 +2468,14 @@ dependencies = [
"simd-adler32",
]
[[package]]
name = "fearless_simd"
version = "0.2.0"
source = "git+https://github.com/linebender/fearless_simd?rev=3d1a77c#3d1a77cfb4515c0da307d50dc782c08840b90c70"
dependencies = [
"bytemuck",
]
[[package]]
name = "filetime"
version = "0.2.25"
@ -4833,7 +4845,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
dependencies = [
"cfg-if",
"windows-targets 0.48.5",
"windows-targets 0.52.6",
]
[[package]]
@ -6220,6 +6232,7 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f9529efd019889b2a205193c14ffb6e2839b54ed9d2720674f10f4b04d87ac9"
dependencies = [
"bytemuck",
"color",
"kurbo",
"smallvec",
@ -9135,7 +9148,7 @@ checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "vello"
version = "0.5.0"
source = "git+https://github.com/linebender/vello?rev=ecf6b282dba01e5dc50e9463b87b6baeccdb3094#ecf6b282dba01e5dc50e9463b87b6baeccdb3094"
source = "git+https://github.com/linebender/vello?rev=daf940230a24cbb123a458b6de95721af47aef98#daf940230a24cbb123a458b6de95721af47aef98"
dependencies = [
"bytemuck",
"futures-intrusive",
@ -9150,10 +9163,33 @@ dependencies = [
"wgpu",
]
[[package]]
name = "vello_common"
version = "0.0.1"
source = "git+https://github.com/linebender/vello?rev=daf940230a24cbb123a458b6de95721af47aef98#daf940230a24cbb123a458b6de95721af47aef98"
dependencies = [
"bytemuck",
"fearless_simd",
"log",
"peniko",
"png",
"skrifa",
"smallvec",
]
[[package]]
name = "vello_cpu"
version = "0.0.1"
source = "git+https://github.com/linebender/vello?rev=daf940230a24cbb123a458b6de95721af47aef98#daf940230a24cbb123a458b6de95721af47aef98"
dependencies = [
"bytemuck",
"vello_common",
]
[[package]]
name = "vello_encoding"
version = "0.5.0"
source = "git+https://github.com/linebender/vello?rev=ecf6b282dba01e5dc50e9463b87b6baeccdb3094#ecf6b282dba01e5dc50e9463b87b6baeccdb3094"
source = "git+https://github.com/linebender/vello?rev=daf940230a24cbb123a458b6de95721af47aef98#daf940230a24cbb123a458b6de95721af47aef98"
dependencies = [
"bytemuck",
"guillotiere",
@ -9165,7 +9201,7 @@ dependencies = [
[[package]]
name = "vello_shaders"
version = "0.5.0"
source = "git+https://github.com/linebender/vello?rev=ecf6b282dba01e5dc50e9463b87b6baeccdb3094#ecf6b282dba01e5dc50e9463b87b6baeccdb3094"
source = "git+https://github.com/linebender/vello?rev=daf940230a24cbb123a458b6de95721af47aef98#daf940230a24cbb123a458b6de95721af47aef98"
dependencies = [
"bytemuck",
"log",
@ -9885,7 +9921,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.48.0",
"windows-sys 0.52.0",
]
[[package]]

View file

@ -168,7 +168,8 @@ unicode-segmentation = "1.12.0"
url = "2.5"
urlpattern = "0.3"
uuid = { version = "1.12.1", features = ["v4"] }
vello = { git = "https://github.com/linebender/vello", rev = "ecf6b282dba01e5dc50e9463b87b6baeccdb3094" }
vello = { git = "https://github.com/linebender/vello", rev = "daf940230a24cbb123a458b6de95721af47aef98" }
vello_cpu = { git = "https://github.com/linebender/vello", rev = "daf940230a24cbb123a458b6de95721af47aef98" }
webdriver = "0.53.0"
webgpu_traits = { path = "components/shared/webgpu" }
webpki-roots = "1.0"

View file

@ -13,6 +13,7 @@ path = "lib.rs"
[features]
vello = ["dep:vello", "dep:pollster", "dep:futures-intrusive", "dep:peniko"]
vello_cpu = ["dep:vello_cpu", "dep:peniko"]
[dependencies]
app_units = { workspace = true }
@ -38,5 +39,6 @@ unicode-script = { workspace = true }
webrender_api = { workspace = true }
servo_config = { path = "../config" }
vello = { workspace = true, optional = true }
vello_cpu = { workspace = true, optional = true }
pollster = { version = "0.4", optional = true }
futures-intrusive = { version = "0.5", optional = true }

View file

@ -296,6 +296,8 @@ enum Canvas {
Raqote(CanvasData<raqote::DrawTarget>),
#[cfg(feature = "vello")]
Vello(CanvasData<crate::vello_backend::VelloDrawTarget>),
#[cfg(feature = "vello_cpu")]
VelloCPU(CanvasData<crate::vello_cpu_backend::VelloCPUDrawTarget>),
}
impl Canvas {
@ -308,6 +310,10 @@ impl Canvas {
if servo_config::pref!(dom_canvas_vello_enabled) {
return Self::Vello(CanvasData::new(size, compositor_api, font_context));
}
#[cfg(feature = "vello_cpu")]
if servo_config::pref!(dom_canvas_vello_cpu_enabled) {
return Self::VelloCPU(CanvasData::new(size, compositor_api, font_context));
}
Self::Raqote(CanvasData::new(size, compositor_api, font_context))
}
@ -316,6 +322,8 @@ impl Canvas {
Canvas::Raqote(canvas_data) => canvas_data.image_key(),
#[cfg(feature = "vello")]
Canvas::Vello(canvas_data) => canvas_data.image_key(),
#[cfg(feature = "vello_cpu")]
Canvas::VelloCPU(canvas_data) => canvas_data.image_key(),
}
}
@ -324,6 +332,8 @@ impl Canvas {
Canvas::Raqote(canvas_data) => canvas_data.pop_clip(),
#[cfg(feature = "vello")]
Canvas::Vello(canvas_data) => canvas_data.pop_clip(),
#[cfg(feature = "vello_cpu")]
Canvas::VelloCPU(canvas_data) => canvas_data.pop_clip(),
}
}
@ -366,6 +376,19 @@ impl Canvas {
composition_options,
transform,
),
#[cfg(feature = "vello_cpu")]
Canvas::VelloCPU(canvas_data) => canvas_data.fill_text(
text,
x,
y,
max_width,
is_rtl,
style,
text_options,
shadow_options,
composition_options,
transform,
),
}
}
@ -385,6 +408,10 @@ impl Canvas {
Canvas::Vello(canvas_data) => {
canvas_data.fill_rect(rect, style, shadow_options, composition_options, transform)
},
#[cfg(feature = "vello_cpu")]
Canvas::VelloCPU(canvas_data) => {
canvas_data.fill_rect(rect, style, shadow_options, composition_options, transform)
},
}
}
@ -415,6 +442,15 @@ impl Canvas {
composition_options,
transform,
),
#[cfg(feature = "vello_cpu")]
Canvas::VelloCPU(canvas_data) => canvas_data.stroke_rect(
rect,
style,
line_options,
shadow_options,
composition_options,
transform,
),
}
}
@ -445,6 +481,15 @@ impl Canvas {
composition_options,
transform,
),
#[cfg(feature = "vello_cpu")]
Canvas::VelloCPU(canvas_data) => canvas_data.fill_path(
path,
fill_rule,
style,
shadow_options,
composition_options,
transform,
),
}
}
@ -475,6 +520,15 @@ impl Canvas {
composition_options,
transform,
),
#[cfg(feature = "vello_cpu")]
Canvas::VelloCPU(canvas_data) => canvas_data.stroke_path(
path,
style,
line_options,
shadow_options,
composition_options,
transform,
),
}
}
@ -483,6 +537,8 @@ impl Canvas {
Canvas::Raqote(canvas_data) => canvas_data.clear_rect(rect, transform),
#[cfg(feature = "vello")]
Canvas::Vello(canvas_data) => canvas_data.clear_rect(rect, transform),
#[cfg(feature = "vello_cpu")]
Canvas::VelloCPU(canvas_data) => canvas_data.clear_rect(rect, transform),
}
}
@ -516,6 +572,16 @@ impl Canvas {
composition_options,
transform,
),
#[cfg(feature = "vello_cpu")]
Canvas::VelloCPU(canvas_data) => canvas_data.draw_image(
snapshot,
dest_rect,
source_rect,
smoothing_enabled,
shadow_options,
composition_options,
transform,
),
}
}
@ -524,6 +590,8 @@ impl Canvas {
Canvas::Raqote(canvas_data) => canvas_data.read_pixels(read_rect),
#[cfg(feature = "vello")]
Canvas::Vello(canvas_data) => canvas_data.read_pixels(read_rect),
#[cfg(feature = "vello_cpu")]
Canvas::VelloCPU(canvas_data) => canvas_data.read_pixels(read_rect),
}
}
@ -532,6 +600,8 @@ impl Canvas {
Canvas::Raqote(canvas_data) => canvas_data.measure_text(text, text_options),
#[cfg(feature = "vello")]
Canvas::Vello(canvas_data) => canvas_data.measure_text(text, text_options),
#[cfg(feature = "vello_cpu")]
Canvas::VelloCPU(canvas_data) => canvas_data.measure_text(text, text_options),
}
}
@ -540,6 +610,8 @@ impl Canvas {
Canvas::Raqote(canvas_data) => canvas_data.clip_path(path, fill_rule, transform),
#[cfg(feature = "vello")]
Canvas::Vello(canvas_data) => canvas_data.clip_path(path, fill_rule, transform),
#[cfg(feature = "vello_cpu")]
Canvas::VelloCPU(canvas_data) => canvas_data.clip_path(path, fill_rule, transform),
}
}
@ -548,6 +620,8 @@ impl Canvas {
Canvas::Raqote(canvas_data) => canvas_data.put_image_data(snapshot, rect),
#[cfg(feature = "vello")]
Canvas::Vello(canvas_data) => canvas_data.put_image_data(snapshot, rect),
#[cfg(feature = "vello_cpu")]
Canvas::VelloCPU(canvas_data) => canvas_data.put_image_data(snapshot, rect),
}
}
@ -556,6 +630,8 @@ impl Canvas {
Canvas::Raqote(canvas_data) => canvas_data.update_image_rendering(),
#[cfg(feature = "vello")]
Canvas::Vello(canvas_data) => canvas_data.update_image_rendering(),
#[cfg(feature = "vello_cpu")]
Canvas::VelloCPU(canvas_data) => canvas_data.update_image_rendering(),
}
}
@ -564,6 +640,8 @@ impl Canvas {
Canvas::Raqote(canvas_data) => canvas_data.recreate(size),
#[cfg(feature = "vello")]
Canvas::Vello(canvas_data) => canvas_data.recreate(size),
#[cfg(feature = "vello_cpu")]
Canvas::VelloCPU(canvas_data) => canvas_data.recreate(size),
}
}
}

View file

@ -5,11 +5,13 @@
#![deny(unsafe_code)]
mod backend;
#[cfg(feature = "vello")]
#[cfg(any(feature = "vello", feature = "vello_cpu"))]
mod peniko_conversions;
mod raqote_backend;
#[cfg(feature = "vello")]
mod vello_backend;
#[cfg(feature = "vello_cpu")]
mod vello_cpu_backend;
pub mod canvas_data;
pub mod canvas_paint_thread;

View file

@ -0,0 +1,452 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::Arc;
use canvas_traits::canvas::{
CompositionOptions, FillOrStrokeStyle, FillRule, LineOptions, Path, ShadowOptions,
};
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;
use pixels::{Snapshot, SnapshotAlphaMode, SnapshotPixelFormat};
use range::Range;
use vello_cpu::{kurbo, peniko};
use webrender_api::{ImageDescriptor, ImageDescriptorFlags};
use crate::backend::{Convert, GenericDrawTarget};
use crate::canvas_data::{Filter, TextRun};
thread_local! {
/// The shared font cache used by all canvases that render on a thread. It would be nicer
/// to have a global cache, but it looks like font-kit uses a per-thread FreeType, so
/// in order to ensure that fonts are particular to a thread we have to make our own
/// cache thread local as well.
static SHARED_FONT_CACHE: RefCell<HashMap<FontIdentifier, peniko::Font>> = RefCell::default();
}
pub(crate) struct VelloCPUDrawTarget {
/// Because this is stateful context
/// caller cannot assume anything about transform, paint, stroke,
/// so it should provide it's own used by each command
/// but it can assume paint_transform to be identity
/// and fill rule to be `peniko::Fill::NonZero`
///
/// This is because paint_transform is rarely set,
/// so it's cheaper to always reset it after use.
ctx: vello_cpu::RenderContext,
pixmap: vello_cpu::Pixmap,
clips: Vec<Path>,
}
impl VelloCPUDrawTarget {
fn with_composition(
&mut self,
composition_options: &CompositionOptions,
f: impl FnOnce(&mut Self),
) {
self.ctx.push_layer(
None,
Some(composition_options.composition_operation.convert()),
Some(composition_options.alpha as f32),
None,
);
f(self);
self.ctx.pop_layer();
}
fn ignore_clips(&mut self, f: impl FnOnce(&mut Self)) {
// pop all clip layers
for _ in &self.clips {
self.ctx.pop_layer();
}
f(self);
// push all clip layers back
for path in &self.clips {
self.ctx.push_clip_layer(&path.0);
}
}
fn pixmap(&mut self) -> &[u8] {
self.ignore_clips(|self_| {
self_.ctx.flush();
self_
.ctx
.render_to_pixmap(&mut self_.pixmap, vello_cpu::RenderMode::OptimizeQuality)
});
self.pixmap.data_as_u8_slice()
}
fn size(&self) -> Size2D<u32> {
Size2D::new(self.ctx.width(), self.ctx.height()).cast()
}
}
impl GenericDrawTarget for VelloCPUDrawTarget {
type SourceSurface = Arc<vello_cpu::Pixmap>;
fn new(size: Size2D<u32>) -> Self {
let size = size.cast();
Self {
ctx: vello_cpu::RenderContext::new(size.width, size.height),
pixmap: vello_cpu::Pixmap::new(size.width, size.height),
clips: Vec::new(),
}
}
fn clear_rect(&mut self, rect: &Rect<f32>, transform: Transform2D<f32>) {
let rect: kurbo::Rect = rect.cast().into();
let mut clip_path = rect.to_path(0.1);
clip_path.apply_affine(transform.cast().into());
let blend_mode = peniko::Compose::Clear;
self.ctx.push_layer(
Some(&clip_path.to_path(0.1)),
Some(blend_mode.into()),
None,
None,
);
self.ctx.pop_layer();
}
fn copy_surface(
&mut self,
surface: Self::SourceSurface,
source: Rect<i32>,
destination: Point2D<i32>,
) {
let destination: kurbo::Point = destination.cast::<f64>().into();
let rect = kurbo::Rect::from_origin_size(destination, source.size.cast());
self.ctx.set_transform(kurbo::Affine::IDENTITY);
self.ignore_clips(|self_| {
// Clipped blending does not work correctly:
// https://github.com/linebender/vello/issues/1119
//self_.push_layer(Some(rect.to_path(0.1)), Some(peniko::Compose::Copy.into()), None, None);
self_.ctx.set_paint(vello_cpu::Image {
source: vello_cpu::ImageSource::Pixmap(surface),
x_extend: peniko::Extend::Pad,
y_extend: peniko::Extend::Pad,
quality: peniko::ImageQuality::Low,
});
self_.ctx.fill_rect(&rect);
//self_.ctx.pop_layer();
});
}
fn create_similar_draw_target(&self, size: &Size2D<i32>) -> Self {
Self::new(size.cast())
}
fn draw_surface(
&mut self,
surface: Self::SourceSurface,
dest: Rect<f64>,
source: Rect<f64>,
filter: Filter,
composition_options: CompositionOptions,
transform: Transform2D<f32>,
) {
let scale_up = dest.size.width > source.size.width || dest.size.height > source.size.height;
self.with_composition(&composition_options, move |self_| {
self_.ctx.set_transform(transform.cast().into());
self_.ctx.set_paint(vello_cpu::Image {
source: vello_cpu::ImageSource::Pixmap(surface),
x_extend: peniko::Extend::Pad,
y_extend: peniko::Extend::Pad,
// we should only do bicubic when scaling up
quality: if scale_up {
filter.convert()
} else {
peniko::ImageQuality::Low
},
});
self_.ctx.set_paint_transform(
kurbo::Affine::translate((dest.origin.x, dest.origin.y)).pre_scale_non_uniform(
dest.size.width / source.size.width,
dest.size.height / source.size.height,
),
);
self_.ctx.fill_rect(&dest.into());
self_.ctx.reset_paint_transform();
})
}
fn draw_surface_with_shadow(
&self,
_surface: Self::SourceSurface,
_dest: &Point2D<f32>,
_shadow_options: ShadowOptions,
_composition_options: CompositionOptions,
) {
log::warn!("no support for drawing shadows");
/*
We will need to do some changes to support drawing shadows with vello, as current abstraction is made for azure.
In vello we do not need new draw target (we will use layers) and we need to pass whole rect.
offsets will be applied to rect directly. shadow blur will be passed directly to let backend do transforms.
*/
//self_.scene.draw_blurred_rounded_rect(self_.transform, rect, color, 0.0, sigma);
}
fn fill(
&mut self,
path: &Path,
fill_rule: FillRule,
style: FillOrStrokeStyle,
composition_options: CompositionOptions,
transform: Transform2D<f32>,
) {
let paint: vello_cpu::PaintType = style.convert();
self.with_composition(&composition_options, |self_| {
self_.ctx.set_transform(transform.cast().into());
self_.ctx.set_fill_rule(fill_rule.convert());
self_.ctx.set_paint(paint);
self_.ctx.fill_path(&path.0);
});
self.ctx.set_fill_rule(peniko::Fill::NonZero);
}
fn fill_text(
&mut self,
text_runs: Vec<TextRun>,
start: Point2D<f32>,
style: FillOrStrokeStyle,
composition_options: CompositionOptions,
transform: Transform2D<f32>,
) {
let style: vello_cpu::PaintType = style.convert();
self.ctx.set_paint(style);
self.ctx.set_transform(transform.cast().into());
self.with_composition(&composition_options, |self_| {
let mut advance = 0.;
for run in text_runs.iter() {
let glyphs = &run.glyphs;
let template = &run.font.template;
SHARED_FONT_CACHE.with(|font_cache| {
let identifier = template.identifier();
if !font_cache.borrow().contains_key(&identifier) {
font_cache.borrow_mut().insert(
identifier.clone(),
peniko::Font::new(
peniko::Blob::from(run.font.data().as_ref().to_vec()),
identifier.index(),
),
);
}
let font_cache = font_cache.borrow();
let Some(font) = font_cache.get(&identifier) else {
return;
};
self_
.ctx
.glyph_run(font)
.font_size(run.font.descriptor.pt_size.to_f32_px())
.fill_glyphs(
glyphs
.iter_glyphs_for_byte_range(&Range::new(ByteIndex(0), glyphs.len()))
.map(|glyph| {
let glyph_offset = glyph.offset().unwrap_or(Point2D::zero());
let x = advance + start.x + glyph_offset.x.to_f32_px();
let y = start.y + glyph_offset.y.to_f32_px();
advance += glyph.advance().to_f32_px();
vello_cpu::Glyph {
id: glyph.id(),
x,
y,
}
}),
);
});
}
})
}
fn fill_rect(
&mut self,
rect: &Rect<f32>,
style: FillOrStrokeStyle,
composition_options: CompositionOptions,
transform: Transform2D<f32>,
) {
let paint: vello_cpu::PaintType = style.convert();
self.with_composition(&composition_options, |self_| {
self_.ctx.set_transform(transform.cast().into());
self_.ctx.set_paint(paint);
self_.ctx.fill_rect(&rect.cast().into());
})
}
fn get_size(&self) -> Size2D<i32> {
self.size().cast()
}
fn pop_clip(&mut self) {
if self.clips.pop().is_some() {
self.ctx.pop_layer();
}
}
fn push_clip(&mut self, path: &Path, fill_rule: FillRule, transform: Transform2D<f32>) {
self.ctx.set_transform(transform.cast().into());
let mut path = path.clone();
path.transform(transform.cast());
self.ctx.set_fill_rule(fill_rule.convert());
self.ctx.push_clip_layer(&path.0);
self.clips.push(path);
self.ctx.set_fill_rule(peniko::Fill::NonZero);
}
fn push_clip_rect(&mut self, rect: &Rect<i32>) {
let mut path = Path::new();
let rect = rect.cast();
path.rect(
rect.origin.x,
rect.origin.y,
rect.size.width,
rect.size.height,
);
self.push_clip(&path, FillRule::Nonzero, Transform2D::identity());
}
fn stroke(
&mut self,
path: &Path,
style: FillOrStrokeStyle,
line_options: LineOptions,
composition_options: CompositionOptions,
transform: Transform2D<f32>,
) {
let paint: vello_cpu::PaintType = style.convert();
self.with_composition(&composition_options, |self_| {
self_.ctx.set_transform(transform.cast().into());
self_.ctx.set_paint(paint);
self_.ctx.set_stroke(line_options.convert());
self_.ctx.stroke_path(&path.0);
})
}
fn stroke_rect(
&mut self,
rect: &Rect<f32>,
style: FillOrStrokeStyle,
line_options: LineOptions,
composition_options: CompositionOptions,
transform: Transform2D<f32>,
) {
let paint: vello_cpu::PaintType = style.convert();
self.with_composition(&composition_options, |self_| {
self_.ctx.set_transform(transform.cast().into());
self_.ctx.set_paint(paint);
self_.ctx.set_stroke(line_options.convert());
self_.ctx.stroke_rect(&rect.cast().into());
})
}
fn image_descriptor_and_serializable_data(
&mut self,
) -> (ImageDescriptor, SerializableImageData) {
let image_desc = ImageDescriptor {
format: webrender_api::ImageFormat::RGBA8,
size: self.size().cast().cast_unit(),
stride: None,
offset: 0,
flags: ImageDescriptorFlags::empty(),
};
let data = SerializableImageData::Raw(IpcSharedMemory::from_bytes(self.pixmap()));
(image_desc, data)
}
fn snapshot(&mut self) -> pixels::Snapshot {
Snapshot::from_vec(
self.size().cast(),
SnapshotPixelFormat::RGBA,
SnapshotAlphaMode::Transparent {
premultiplied: true,
},
self.pixmap().to_vec(),
)
}
fn surface(&mut self) -> Self::SourceSurface {
self.pixmap(); // sync pixmap
Arc::new(vello_cpu::Pixmap::from_parts(
self.pixmap.clone().take(),
self.pixmap.width(),
self.pixmap.height(),
))
}
fn create_source_surface_from_data(&self, data: Snapshot) -> Option<Self::SourceSurface> {
Some(snapshot_as_pixmap(data))
}
}
fn snapshot_as_pixmap(data: Snapshot) -> Arc<vello_cpu::Pixmap> {
let size = data.size().cast();
let (data, _, _) = data.to_vec(
Some(SnapshotAlphaMode::Transparent {
premultiplied: true,
}),
Some(SnapshotPixelFormat::RGBA),
);
Arc::new(vello_cpu::Pixmap::from_parts(
bytemuck::cast_vec(data),
size.width,
size.height,
))
}
impl Convert<vello_cpu::PaintType> for FillOrStrokeStyle {
fn convert(self) -> vello_cpu::PaintType {
use canvas_traits::canvas::FillOrStrokeStyle::*;
match self {
Color(absolute_color) => vello_cpu::PaintType::Solid(absolute_color.convert()),
LinearGradient(style) => {
let start = kurbo::Point::new(style.x0, style.y0);
let end = kurbo::Point::new(style.x1, style.y1);
let mut gradient = peniko::Gradient::new_linear(start, end);
gradient.stops = style.stops.convert();
vello_cpu::PaintType::Gradient(gradient)
},
RadialGradient(style) => {
let center1 = kurbo::Point::new(style.x0, style.y0);
let center2 = kurbo::Point::new(style.x1, style.y1);
let mut gradient = peniko::Gradient::new_two_point_radial(
center1,
style.r0 as f32,
center2,
style.r1 as f32,
);
gradient.stops = style.stops.convert();
vello_cpu::PaintType::Gradient(gradient)
},
Surface(surface_style) => {
let pixmap = snapshot_as_pixmap(surface_style.surface_data.to_owned());
vello_cpu::PaintType::Image(vello_cpu::Image {
source: vello_cpu::ImageSource::Pixmap(pixmap),
x_extend: if surface_style.repeat_x {
peniko::Extend::Repeat
} else {
peniko::Extend::Pad
},
y_extend: if surface_style.repeat_y {
peniko::Extend::Repeat
} else {
peniko::Extend::Pad
},
quality: peniko::ImageQuality::Low,
})
},
}
}
}

View file

@ -78,6 +78,8 @@ pub struct Preferences {
pub dom_canvas_text_enabled: bool,
/// Uses vello as canvas backend
pub dom_canvas_vello_enabled: bool,
/// Uses vello_cpu as canvas backend
pub dom_canvas_vello_cpu_enabled: bool,
pub dom_clipboardevent_enabled: bool,
pub dom_composition_event_enabled: bool,
pub dom_cookiestore_enabled: bool,
@ -258,6 +260,7 @@ impl Preferences {
dom_canvas_capture_enabled: false,
dom_canvas_text_enabled: true,
dom_canvas_vello_enabled: false,
dom_canvas_vello_cpu_enabled: false,
dom_clipboardevent_enabled: true,
dom_composition_event_enabled: false,
dom_cookiestore_enabled: false,

View file

@ -17,6 +17,7 @@ default = []
tracing = ["dep:tracing"]
webgpu = ["script_traits/webgpu"]
vello = ["canvas/vello"]
vello_cpu = ["canvas/vello_cpu"]
[dependencies]
background_hang_monitor = { path = "../background_hang_monitor" }

View file

@ -63,6 +63,7 @@ webgpu = [
"constellation_traits/webgpu",
]
vello = ["constellation/vello"]
vello_cpu = ["constellation/vello_cpu"]
[dependencies]
background_hang_monitor = { path = "../background_hang_monitor" }

View file

@ -53,6 +53,7 @@ webgl_backtrace = ["libservo/webgl_backtrace"]
webgpu = ["libservo/webgpu"]
webxr = ["libservo/webxr"]
vello = ["libservo/vello"]
vello_cpu = ["libservo/vello_cpu"]
[dependencies]
cfg-if = { workspace = true }

View file

@ -125,6 +125,19 @@ def handle_preset(s: str) -> Optional[JobConfig]:
unit_tests=False,
number_of_wpt_chunks=2,
)
elif any(word in s for word in ["vello-cpu", "vello_cpu"]):
return JobConfig(
"Vello-CPU WPT",
Workflow.LINUX,
wpt=True,
wpt_args=" ".join(
[
"--subsuite-file ./tests/wpt/vello_cpu_canvas_subsuite.json",
"--subsuite vello_cpu_canvas",
]
),
build_args="--features 'vello_cpu'",
)
elif any(word in s for word in ["vello"]):
return JobConfig(
"Vello WPT",

View file

@ -0,0 +1,4 @@
[2d.composite.clip.clear.html]
[fill() does not affect pixels outside the clip region.]
expected:
if subsuite == "vello_cpu_canvas": FAIL

View file

@ -0,0 +1,4 @@
[2d.composite.clip.copy.html]
[fill() does not affect pixels outside the clip region.]
expected:
if subsuite == "vello_cpu_canvas": FAIL

View file

@ -0,0 +1,4 @@
[2d.composite.clip.destination-atop.html]
[fill() does not affect pixels outside the clip region.]
expected:
if subsuite == "vello_cpu_canvas": FAIL

View file

@ -0,0 +1,4 @@
[2d.composite.clip.destination-in.html]
[fill() does not affect pixels outside the clip region.]
expected:
if subsuite == "vello_cpu_canvas": FAIL

View file

@ -0,0 +1,4 @@
[2d.composite.clip.source-in.html]
[fill() does not affect pixels outside the clip region.]
expected:
if subsuite == "vello_cpu_canvas": FAIL

View file

@ -0,0 +1,4 @@
[2d.composite.clip.source-out.html]
[fill() does not affect pixels outside the clip region.]
expected:
if subsuite == "vello_cpu_canvas": FAIL

View file

@ -0,0 +1,4 @@
[2d.composite.uncovered.pattern.copy.html]
[Pattern fill() draws pixels not covered by the source object as (0,0,0,0), and does not leave the pixels unchanged.]
expected:
if subsuite == "vello_cpu_canvas": FAIL

View file

@ -0,0 +1,4 @@
[2d.composite.uncovered.pattern.destination-atop.html]
[Pattern fill() draws pixels not covered by the source object as (0,0,0,0), and does not leave the pixels unchanged.]
expected:
if subsuite == "vello_cpu_canvas": FAIL

View file

@ -0,0 +1,4 @@
[2d.composite.uncovered.pattern.destination-in.html]
[Pattern fill() draws pixels not covered by the source object as (0,0,0,0), and does not leave the pixels unchanged.]
expected:
if subsuite == "vello_cpu_canvas": FAIL

View file

@ -0,0 +1,4 @@
[2d.composite.uncovered.pattern.source-in.html]
[Pattern fill() draws pixels not covered by the source object as (0,0,0,0), and does not leave the pixels unchanged.]
expected:
if subsuite == "vello_cpu_canvas": FAIL

View file

@ -0,0 +1,4 @@
[2d.composite.uncovered.pattern.source-out.html]
[Pattern fill() draws pixels not covered by the source object as (0,0,0,0), and does not leave the pixels unchanged.]
expected:
if subsuite == "vello_cpu_canvas": FAIL

View file

@ -0,0 +1,4 @@
[2d.clearRect.clip.html]
[clearRect is affected by clipping regions]
expected:
if subsuite == "vello_cpu_canvas": FAIL

View file

@ -0,0 +1,4 @@
[2d.clearRect.shadow.html]
[clearRect does not draw shadows]
expected:
if subsuite == "vello_cpu_canvas": FAIL

View file

@ -0,0 +1,4 @@
[2d.clearRect.zero.html]
[clearRect of zero pixels has no effect]
expected:
if subsuite == "vello_cpu_canvas": FAIL

View file

@ -3,3 +3,4 @@
bug: https://github.com/linebender/vello/issues/1056
expected:
if subsuite == "vello_canvas": FAIL
if subsuite == "vello_cpu_canvas": FAIL

View file

@ -0,0 +1,4 @@
[2d.gradient.radial.cone.front.html]
[Canvas test: 2d.gradient.radial.cone.front]
expected:
if subsuite == "vello_cpu_canvas": FAIL

View file

@ -0,0 +1,4 @@
[2d.gradient.radial.cone.top.html]
[Canvas test: 2d.gradient.radial.cone.top]
expected:
if subsuite == "vello_cpu_canvas": FAIL

View file

@ -0,0 +1,4 @@
[2d.gradient.radial.inside2.html]
[Canvas test: 2d.gradient.radial.inside2]
expected:
if subsuite == "vello_cpu_canvas": FAIL

View file

@ -0,0 +1,4 @@
[2d.gradient.radial.inside3.html]
[Canvas test: 2d.gradient.radial.inside3]
expected:
if subsuite == "vello_cpu_canvas": FAIL

View file

@ -0,0 +1,4 @@
[2d.gradient.radial.outside1.html]
[Canvas test: 2d.gradient.radial.outside1]
expected:
if subsuite == "vello_cpu_canvas": FAIL

View file

@ -2,3 +2,4 @@
[Canvas test: 2d.gradient.radial.outside3]
expected:
if subsuite == "": FAIL
if subsuite == "vello_cpu_canvas": FAIL

View file

@ -3,3 +3,4 @@
bug: https://github.com/linebender/vello/issues/1063
expected:
if subsuite == "vello_canvas": FAIL
if subsuite == "vello_cpu_canvas": FAIL

View file

@ -2,3 +2,4 @@
[Non-uniformly scaled arcs are the right shape]
expected:
if subsuite == "vello_canvas": FAIL
if subsuite == "vello_cpu_canvas": FAIL

View file

@ -1,5 +1,6 @@
[2d.path.rect.selfintersect.html]
[Canvas test: 2d.path.rect.selfintersect]
bug: https://github.com/linebender/vello/issues/1063#issuecomment-2998084736
bug: https://github.com/linebender/vello/issues/1063 #issuecomment-2998084736
expected:
if subsuite == "vello_canvas": FAIL
if subsuite == "vello_cpu_canvas": FAIL

View file

@ -2,3 +2,4 @@
[Stroke line widths are scaled by the current transformation matrix]
expected:
if subsuite == "vello_canvas": FAIL
if subsuite == "vello_cpu_canvas": FAIL

View file

@ -0,0 +1,4 @@
[2d.imageData.put.alpha.html]
[putImageData() puts non-solid image data correctly]
expected:
if subsuite == "vello_cpu_canvas": FAIL

View file

@ -1,4 +1,3 @@
[2d.imageData.put.unchanged.html]
[putImageData(getImageData(...), ...) has no effect]
expected:
if subsuite == "": FAIL
expected: FAIL

View file

@ -0,0 +1,8 @@
{
"vello_cpu_canvas": {
"config": {
"binary_args": ["--pref", "dom_canvas_vello_cpu_enabled"]
},
"include": ["/html/canvas/element"]
}
}