diff --git a/imaging/src/diagnostics.rs b/imaging/src/diagnostics.rs index df8bf0e..d9f5ee1 100644 --- a/imaging/src/diagnostics.rs +++ b/imaging/src/diagnostics.rs @@ -12,11 +12,9 @@ use alloc::vec::Vec; -use peniko::BrushRef; - use crate::{ - BlurredRoundedRect, ClipRef, Composite, ContextRef, FillRef, GlyphRunRef, GroupRef, PaintSink, - SourceLocationRef, StrokeRef, + BlurredRoundedRect, BrushRef, ClipRef, Composite, ContextRef, FillRef, GlyphRunRef, GroupRef, + PaintSink, SourceLocationRef, StrokeRef, record::{ContextNote, ResolvedSourceLocation, Scene}, }; @@ -451,15 +449,13 @@ mod tests { use alloc::vec; use kurbo::{Affine, BezPath, Rect, Stroke}; - use peniko::{Brush, Color, Fill}; + use peniko::{Color, Fill}; use super::*; - use crate::Composite; - use crate::MaskMode; - use crate::Painter; use crate::record::{ AppliedMask, Clip, ContextId, Draw, Geometry, Group, Mask, ResolvedSourceLocation, }; + use crate::{Brush, Composite, MaskMode, Painter}; #[test] fn diagnose_reports_empty_scopes_and_transparent_draws() { diff --git a/imaging/src/image.rs b/imaging/src/image.rs new file mode 100644 index 0000000..b196153 --- /dev/null +++ b/imaging/src/image.rs @@ -0,0 +1,606 @@ +// Copyright 2026 the Imaging Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Shared image and brush types for `imaging`. + +use alloc::sync::{Arc, Weak}; +use core::{ + ops::{Deref, DerefMut}, + sync::atomic::{AtomicU64, Ordering}, +}; +use kurbo::Rect; + +use crate::record; + +static NEXT_SCENE_IMAGE_ID: AtomicU64 = AtomicU64::new(1); +static NEXT_SCENE_PICTURE_ID: AtomicU64 = AtomicU64::new(1); + +/// Image payload accepted by `imaging` brushes. +#[derive(Clone, Debug, PartialEq)] +pub enum Image { + /// Raster image data. + Raster(peniko::ImageData), + /// Retained scene content with an explicit natural size. + Scene(SceneImage), +} + +impl Image { + /// Return the natural width of the image in pixels. + #[must_use] + pub fn width(&self) -> u32 { + match self { + Self::Raster(image) => image.width, + Self::Scene(scene) => scene.width(), + } + } + + /// Return the natural height of the image in pixels. + #[must_use] + pub fn height(&self) -> u32 { + match self { + Self::Raster(image) => image.height, + Self::Scene(scene) => scene.height(), + } + } + + /// Borrow this image payload. + #[must_use] + pub fn as_ref(&self) -> ImageRef<'_> { + match self { + Self::Raster(image) => ImageRef::Raster(image), + Self::Scene(scene) => ImageRef::Scene(scene), + } + } +} + +impl From for Image { + fn from(value: peniko::ImageData) -> Self { + Self::Raster(value) + } +} + +impl From for Image { + fn from(value: SceneImage) -> Self { + Self::Scene(value) + } +} + +/// Borrowed image payload. +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum ImageRef<'a> { + /// Borrowed raster image data. + Raster(&'a peniko::ImageData), + /// Borrowed retained scene image. + Scene(&'a SceneImage), +} + +impl ImageRef<'_> { + /// Return the natural width of the image in pixels. + #[must_use] + pub fn width(self) -> u32 { + match self { + Self::Raster(image) => image.width, + Self::Scene(scene) => scene.width(), + } + } + + /// Return the natural height of the image in pixels. + #[must_use] + pub fn height(self) -> u32 { + match self { + Self::Raster(image) => image.height, + Self::Scene(scene) => scene.height(), + } + } + + /// Convert to an owned image payload. + #[must_use] + pub fn to_owned(self) -> Image { + match self { + Self::Raster(image) => Image::Raster(image.clone()), + Self::Scene(scene) => Image::Scene(scene.clone()), + } + } +} + +impl<'a> From<&'a Image> for ImageRef<'a> { + fn from(value: &'a Image) -> Self { + value.as_ref() + } +} + +impl<'a> From<&'a peniko::ImageData> for ImageRef<'a> { + fn from(value: &'a peniko::ImageData) -> Self { + Self::Raster(value) + } +} + +impl<'a> From<&'a SceneImage> for ImageRef<'a> { + fn from(value: &'a SceneImage) -> Self { + Self::Scene(value) + } +} + +/// A retained scene recording. +#[derive(Clone, Debug, PartialEq)] +pub struct ScenePicture(Arc); + +#[derive(Debug, PartialEq)] +struct ScenePictureInner { + /// Stable identity for cache lookups. + id: u64, + /// Scene content. + scene: record::Scene, + /// Conservative cull bounds for the retained recording. + bounds: Rect, +} + +/// Weak retained handle to a scene picture. +#[derive(Clone, Debug)] +pub struct ScenePictureWeak(Weak); + +impl ScenePictureWeak { + /// Upgrade the weak handle if the picture is still alive. + #[must_use] + pub fn upgrade(&self) -> Option { + self.0.upgrade().map(ScenePicture) + } +} + +impl ScenePicture { + /// Create a retained scene picture. + #[must_use] + pub fn new(scene: record::Scene, bounds: Rect) -> Self { + Self(Arc::new(ScenePictureInner { + id: NEXT_SCENE_PICTURE_ID.fetch_add(1, Ordering::Relaxed), + scene, + bounds, + })) + } + + /// Return the stable identity of this scene picture. + #[must_use] + pub fn id(&self) -> u64 { + self.0.id + } + + /// Borrow the retained scene. + #[must_use] + pub fn scene(&self) -> &record::Scene { + &self.0.scene + } + + /// Return the conservative cull bounds for this retained scene picture. + #[must_use] + pub fn bounds(&self) -> Rect { + self.0.bounds + } + + /// Downgrade this retained scene picture to a weak handle for cache entries. + #[must_use] + pub fn downgrade(&self) -> ScenePictureWeak { + ScenePictureWeak(Arc::downgrade(&self.0)) + } +} + +/// A retained scene used as an image brush source. +#[derive(Clone, Debug, PartialEq)] +pub struct SceneImage(Arc); + +#[derive(Debug, PartialEq)] +struct SceneImageInner { + /// Stable identity for cache lookups. + id: u64, + /// Retained scene picture. + picture: ScenePicture, + /// Natural width in pixels. + width: u32, + /// Natural height in pixels. + height: u32, +} + +/// Weak retained handle to a scene-backed image source. +#[derive(Clone, Debug)] +pub struct SceneImageWeak(Weak); + +impl SceneImageWeak { + /// Upgrade the weak handle if the source is still alive. + #[must_use] + pub fn upgrade(&self) -> Option { + self.0.upgrade().map(SceneImage) + } +} + +impl SceneImage { + /// Create a scene-backed image with an explicit natural size. + #[must_use] + pub fn new(scene: record::Scene, width: u32, height: u32) -> Self { + Self::from_picture( + ScenePicture::new( + scene, + Rect::new(0.0, 0.0, f64::from(width), f64::from(height)), + ), + width, + height, + ) + } + + /// Create a scene-backed image from an existing retained scene picture. + #[must_use] + pub fn from_picture(picture: ScenePicture, width: u32, height: u32) -> Self { + Self(Arc::new(SceneImageInner { + id: NEXT_SCENE_IMAGE_ID.fetch_add(1, Ordering::Relaxed), + picture, + width, + height, + })) + } + + /// Return the stable identity of this scene-backed image. + #[must_use] + pub fn id(&self) -> u64 { + self.0.id + } + + /// Return the natural width of the scene image in pixels. + #[must_use] + pub fn width(&self) -> u32 { + self.0.width + } + + /// Return the natural height of the scene image in pixels. + #[must_use] + pub fn height(&self) -> u32 { + self.0.height + } + + /// Borrow the retained scene. + #[must_use] + pub fn scene(&self) -> &record::Scene { + self.0.picture.scene() + } + + /// Borrow the retained picture underlying this scene-backed image. + #[must_use] + pub fn picture(&self) -> &ScenePicture { + &self.0.picture + } + + /// Downgrade this retained scene image to a weak handle for cache entries. + #[must_use] + pub fn downgrade(&self) -> SceneImageWeak { + SceneImageWeak(Arc::downgrade(&self.0)) + } +} + +/// Imaging-owned image brush. +#[derive(Copy, Clone, Debug, PartialEq)] +#[repr(transparent)] +pub struct ImageBrush(pub peniko::ImageBrush); + +impl Deref for ImageBrush { + type Target = peniko::ImageBrush; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for ImageBrush { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl ImageBrush { + /// Builder method for setting the image extend mode in both directions. + #[must_use] + pub fn with_extend(mut self, mode: peniko::Extend) -> Self { + self.0 = self.0.with_extend(mode); + self + } + + /// Builder method for setting the image extend mode in the horizontal direction. + #[must_use] + pub fn with_x_extend(mut self, mode: peniko::Extend) -> Self { + self.0 = self.0.with_x_extend(mode); + self + } + + /// Builder method for setting the image extend mode in the vertical direction. + #[must_use] + pub fn with_y_extend(mut self, mode: peniko::Extend) -> Self { + self.0 = self.0.with_y_extend(mode); + self + } + + /// Builder method for setting the desired image quality hint. + #[must_use] + pub fn with_quality(mut self, quality: peniko::ImageQuality) -> Self { + self.0 = self.0.with_quality(quality); + self + } + + /// Return the image with the alpha multiplier set to `alpha`. + #[must_use] + #[track_caller] + pub fn with_alpha(mut self, alpha: f32) -> Self { + self.0 = self.0.with_alpha(alpha); + self + } + + /// Return the image with its alpha multiplier multiplied by `alpha`. + #[must_use] + #[track_caller] + pub fn multiply_alpha(mut self, alpha: f32) -> Self { + self.0 = self.0.multiply_alpha(alpha); + self + } +} + +impl ImageBrush { + /// Create a new image brush with default sampling. + #[must_use] + pub fn new(image: impl Into) -> Self { + Self(peniko::ImageBrush { + image: image.into(), + sampler: peniko::ImageSampler::default(), + }) + } + + /// Borrow this image brush. + #[must_use] + pub fn as_ref(&self) -> ImageBrushRef<'_> { + ImageBrush(peniko::ImageBrush { + image: self.image.as_ref(), + sampler: self.sampler, + }) + } +} + +impl From for ImageBrush { + fn from(image: Image) -> Self { + Self(peniko::ImageBrush { + image, + sampler: peniko::ImageSampler::default(), + }) + } +} + +impl From for ImageBrush { + fn from(image: peniko::ImageData) -> Self { + Image::from(image).into() + } +} + +impl From for ImageBrush { + fn from(image: SceneImage) -> Self { + Image::from(image).into() + } +} + +impl From for ImageBrush { + fn from(image: peniko::ImageBrush) -> Self { + Self(peniko::ImageBrush { + image: Image::Raster(image.image), + sampler: image.sampler, + }) + } +} + +/// Borrowed image brush. +pub type ImageBrushRef<'a> = ImageBrush>; + +fn image_brush_as_ref(image: &ImageBrush) -> ImageBrushRef<'_> { + image.as_ref() +} + +fn image_brush_ref_to_owned(image: &ImageBrushRef<'_>) -> ImageBrush { + ImageBrush(peniko::ImageBrush { + image: image.image.to_owned(), + sampler: image.sampler, + }) +} + +impl<'a> From<&'a ImageBrush> for ImageBrushRef<'a> { + fn from(value: &'a ImageBrush) -> Self { + image_brush_as_ref(value) + } +} + +impl<'a> From<&'a peniko::ImageBrush> for ImageBrushRef<'a> { + fn from(value: &'a peniko::ImageBrush) -> Self { + Self(peniko::ImageBrush { + image: ImageRef::Raster(&value.image), + sampler: value.sampler, + }) + } +} + +impl<'a> From<&'a peniko::ImageData> for ImageBrushRef<'a> { + fn from(image: &'a peniko::ImageData) -> Self { + Self(peniko::ImageBrush { + image: image.into(), + sampler: peniko::ImageSampler::default(), + }) + } +} + +impl<'a> From<&'a SceneImage> for ImageBrushRef<'a> { + fn from(image: &'a SceneImage) -> Self { + Self(peniko::ImageBrush { + image: image.into(), + sampler: peniko::ImageSampler::default(), + }) + } +} + +/// Imaging-owned brush. +#[derive(Clone, Debug, PartialEq)] +pub enum Brush { + /// Solid color brush. + Solid(peniko::Color), + /// Gradient brush. + Gradient(peniko::Gradient), + /// Image brush. + Image(ImageBrush), +} + +impl Brush { + /// Return the brush with the alpha component set to `alpha`. + #[must_use] + pub fn with_alpha(self, alpha: f32) -> Self { + match self { + Self::Solid(color) => Self::Solid(color.with_alpha(alpha)), + Self::Gradient(gradient) => Self::Gradient(gradient.with_alpha(alpha)), + Self::Image(image) => Self::Image(image.with_alpha(alpha)), + } + } + + /// Return the brush with the alpha component multiplied by `alpha`. + #[must_use] + #[track_caller] + pub fn multiply_alpha(self, alpha: f32) -> Self { + debug_assert!( + alpha.is_finite() && alpha >= 0.0, + "A non-finite or negative alpha ({alpha}) is meaningless." + ); + if alpha == 1.0 { + self + } else { + match self { + Self::Solid(color) => Self::Solid(color.multiply_alpha(alpha)), + Self::Gradient(gradient) => Self::Gradient(gradient.multiply_alpha(alpha)), + Self::Image(image) => Self::Image(image.multiply_alpha(alpha)), + } + } + } +} + +impl Default for Brush { + fn default() -> Self { + Self::Solid(peniko::Color::TRANSPARENT) + } +} + +impl From for Brush { + fn from(value: peniko::Color) -> Self { + Self::Solid(value) + } +} + +impl From<&peniko::Color> for Brush { + fn from(value: &peniko::Color) -> Self { + Self::Solid(*value) + } +} + +impl From for Brush { + fn from(value: peniko::Gradient) -> Self { + Self::Gradient(value) + } +} + +impl From for Brush { + fn from(value: ImageBrush) -> Self { + Self::Image(value) + } +} + +impl From for Brush { + fn from(value: peniko::Brush) -> Self { + match value { + peniko::Brush::Solid(color) => Self::Solid(color), + peniko::Brush::Gradient(gradient) => Self::Gradient(gradient), + peniko::Brush::Image(image) => Self::Image(image.into()), + } + } +} + +/// Borrowed imaging brush. +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum BrushRef<'a> { + /// Solid color brush. + Solid(peniko::Color), + /// Gradient brush. + Gradient(&'a peniko::Gradient), + /// Image brush. + Image(ImageBrushRef<'a>), +} + +impl BrushRef<'_> { + /// Convert the borrowed brush into an owned brush. + #[must_use] + pub fn to_owned(&self) -> Brush { + match self { + Self::Solid(color) => Brush::Solid(*color), + Self::Gradient(gradient) => Brush::Gradient((*gradient).clone()), + Self::Image(image) => Brush::Image(image_brush_ref_to_owned(image)), + } + } +} + +impl<'a> From for BrushRef<'a> { + fn from(value: peniko::Color) -> Self { + Self::Solid(value) + } +} + +impl<'a> From<&'a peniko::Color> for BrushRef<'a> { + fn from(value: &'a peniko::Color) -> Self { + Self::Solid(*value) + } +} + +impl<'a> From<&'a peniko::Gradient> for BrushRef<'a> { + fn from(value: &'a peniko::Gradient) -> Self { + Self::Gradient(value) + } +} + +impl<'a> From<&'a ImageBrush> for BrushRef<'a> { + fn from(value: &'a ImageBrush) -> Self { + Self::Image(image_brush_as_ref(value)) + } +} + +impl<'a> From> for BrushRef<'a> { + fn from(value: ImageBrushRef<'a>) -> Self { + Self::Image(value) + } +} + +impl<'a> From<&'a peniko::ImageData> for BrushRef<'a> { + fn from(value: &'a peniko::ImageData) -> Self { + Self::Image(value.into()) + } +} + +impl<'a> From<&'a SceneImage> for BrushRef<'a> { + fn from(value: &'a SceneImage) -> Self { + Self::Image(value.into()) + } +} + +impl<'a> From<&'a Brush> for BrushRef<'a> { + fn from(value: &'a Brush) -> Self { + match value { + Brush::Solid(color) => Self::Solid(*color), + Brush::Gradient(gradient) => Self::Gradient(gradient), + Brush::Image(image) => Self::Image(image_brush_as_ref(image)), + } + } +} + +impl<'a> From<&'a peniko::Brush> for BrushRef<'a> { + fn from(value: &'a peniko::Brush) -> Self { + match value { + peniko::Brush::Solid(color) => Self::Solid(*color), + peniko::Brush::Gradient(gradient) => Self::Gradient(gradient), + peniko::Brush::Image(image) => Self::Image(ImageBrush(peniko::ImageBrush { + image: ImageRef::Raster(&image.image), + sampler: image.sampler, + })), + } + } +} diff --git a/imaging/src/lib.rs b/imaging/src/lib.rs index 277f3e5..68374f4 100644 --- a/imaging/src/lib.rs +++ b/imaging/src/lib.rs @@ -103,6 +103,44 @@ //! Low-level retained payloads like [`record::Draw`], [`record::Clip`], and [`record::Group`] are //! also public under [`record`] when you need exact control over the recorded representation. //! +//! # Scene-Backed Image Brushes +//! +//! [`SceneImage`] lets you use a retained [`record::Scene`] as the source for an [`ImageBrush`]. +//! This is useful when you want image-brush sampling semantics like `Pad`, `Repeat`, or +//! `Reflect`, but the source content is authored as vector drawing commands instead of raster +//! pixels. +//! +//! ```rust +//! use imaging::{Brush, ImageBrush, Painter, SceneImage, record::Scene}; +//! use kurbo::Rect; +//! use peniko::{Color, Extend}; +//! +//! let mut source = Scene::new(); +//! { +//! let mut painter = Painter::new(&mut source); +//! painter.fill_rect(Rect::new(0.0, 0.0, 1.0, 1.0), Color::from_rgb8(0xff, 0x00, 0x00)); +//! painter.fill_rect(Rect::new(1.0, 0.0, 2.0, 1.0), Color::from_rgb8(0x00, 0xff, 0x00)); +//! } +//! +//! let brush = Brush::Image(ImageBrush::from(SceneImage::new(source, 2, 1)).with_extend( +//! Extend::Reflect, +//! )); +//! +//! let mut scene = Scene::new(); +//! { +//! let mut painter = Painter::new(&mut scene); +//! painter.fill(Rect::new(0.0, 0.0, 4.0, 1.0), &brush).draw(); +//! } +//! ``` +//! +//! Backend support is renderer-specific: +//! - `imaging_skia` supports scene-backed image brushes natively. +//! - `imaging_tiny_skia` and `imaging_vello_cpu` support them by rasterizing the source scene and +//! then sampling the realized image. +//! - `imaging_vello_hybrid` supports them by rasterizing the source scene, then uploading the +//! realized image into its hybrid atlas cache. +//! - `imaging_vello` intentionally rejects them. +//! //! The API is intentionally small and experimental; expect breaking changes while we iterate. #![no_std] @@ -114,12 +152,17 @@ use kurbo::{Affine, Rect}; use peniko::BlendMode; pub mod diagnostics; +mod image; mod paint; mod painter; pub mod record; pub mod render; pub mod validation; +pub use image::{ + Brush, BrushRef, Image, ImageBrush, ImageBrushRef, ImageRef, SceneImage, SceneImageWeak, + ScenePicture, ScenePictureWeak, +}; pub use paint::{ AppliedMaskRef, ClipRef, ContextRef, DrawRef, FillRef, GeometryRef, GlyphRunRef, GroupRef, MaskRef, PaintSink, SourceLocationRef, StrokeRef, diff --git a/imaging/src/paint.rs b/imaging/src/paint.rs index b04baa2..7fa8d3e 100644 --- a/imaging/src/paint.rs +++ b/imaging/src/paint.rs @@ -8,10 +8,10 @@ //! semantic recording format. use kurbo::{Affine, BezPath, Rect, RoundedRect, Shape as _, Stroke, Vec2}; -use peniko::{BrushRef, Fill, Style}; +use peniko::{Fill, Style}; use crate::{ - BlurredRoundedRect, Composite, Filter, MaskMode, NormalizedCoord, + BlurredRoundedRect, BrushRef, Composite, Filter, MaskMode, NormalizedCoord, ScenePicture, record::{ AppliedMask, Clip, ClipId, Command, Draw, DrawId, Geometry, Glyph, GlyphRun, Group, GroupId, Mask, MaskId, Scene, @@ -654,6 +654,13 @@ pub enum DrawRef<'a> { GlyphRun(GlyphRunRef<'a>), /// Draw a solid-color rounded rectangle blurred with a gaussian filter. BlurredRoundedRect(BlurredRoundedRect), + /// Replay a retained scene picture. + ScenePicture { + /// Transform applied while replaying the retained picture. + transform: Affine, + /// Retained scene picture to replay. + picture: &'a ScenePicture, + }, } /// Borrowed source location carried by a context annotation. @@ -701,6 +708,10 @@ impl<'a> DrawRef<'a> { Self::Stroke(draw) => draw.to_owned(), Self::GlyphRun(draw) => Draw::GlyphRun(draw.to_owned(glyphs)), Self::BlurredRoundedRect(draw) => Draw::BlurredRoundedRect(draw), + Self::ScenePicture { transform, picture } => Draw::ScenePicture { + transform, + picture: picture.clone(), + }, } } } @@ -734,6 +745,10 @@ pub trait PaintSink { fn glyph_run(&mut self, draw: GlyphRunRef<'_>, glyphs: &mut dyn Iterator); /// Emit a blurred rounded rect draw. fn blurred_rounded_rect(&mut self, draw: BlurredRoundedRect); + /// Replay a retained scene picture. + fn scene_picture(&mut self, picture: &ScenePicture, transform: Affine) { + replay_transformed(picture.scene(), self, transform); + } } impl Geometry { @@ -869,6 +884,10 @@ impl Draw { }), Self::GlyphRun(glyph_run) => DrawRef::GlyphRun(glyph_run.as_ref()), Self::BlurredRoundedRect(draw) => DrawRef::BlurredRoundedRect(*draw), + Self::ScenePicture { transform, picture } => DrawRef::ScenePicture { + transform: *transform, + picture, + }, } } } @@ -924,6 +943,11 @@ where self.inner .blurred_rounded_rect(draw.prepend_transform(self.transform)); } + + fn scene_picture(&mut self, picture: &ScenePicture, transform: Affine) { + self.inner + .scene_picture(picture, self.transform * transform); + } } fn replay_clip(scene: &Scene, id: ClipId, sink: &mut S) @@ -949,10 +973,14 @@ where let mut glyphs = glyph_run.glyphs.iter().copied(); sink.glyph_run(glyph_run.as_ref(), &mut glyphs); } + Draw::ScenePicture { transform, picture } => sink.scene_picture(picture, *transform), draw => match draw.as_ref() { DrawRef::Fill(draw) => sink.fill(draw), DrawRef::Stroke(draw) => sink.stroke(draw), DrawRef::BlurredRoundedRect(draw) => sink.blurred_rounded_rect(draw), + DrawRef::ScenePicture { .. } => { + unreachable!("scene-picture draws are handled using the retained picture") + } DrawRef::GlyphRun(_) => { unreachable!("glyph runs are handled using the owned glyph slice") } @@ -996,8 +1024,8 @@ mod tests { use alloc::vec; use super::*; - use crate::{Composite, record::Geometry}; - use peniko::{Brush, FontData}; + use crate::{Brush, Composite, ScenePicture, record::Geometry}; + use peniko::FontData; #[test] fn clip_ref_prepend_transform_prefixes_clip_transform() { @@ -1053,12 +1081,9 @@ mod tests { #[test] fn fill_ref_prepend_transform_prefixes_draw_transform_only() { - let draw = FillRef::new( - Rect::new(0.0, 0.0, 3.0, 4.0), - Brush::Solid(peniko::Color::WHITE), - ) - .transform(Affine::translate((1.0, 2.0))) - .brush_transform(Some(Affine::translate((3.0, 4.0)))); + let draw = FillRef::new(Rect::new(0.0, 0.0, 3.0, 4.0), peniko::Color::WHITE) + .transform(Affine::translate((1.0, 2.0))) + .brush_transform(Some(Affine::translate((3.0, 4.0)))); assert_eq!( draw.prepend_transform(Affine::translate((5.0, 6.0))), @@ -1075,10 +1100,7 @@ mod tests { #[test] fn fill_ref_prepend_transform_preserves_missing_brush_transform() { - let draw = FillRef::new( - Rect::new(0.0, 0.0, 3.0, 4.0), - Brush::Solid(peniko::Color::WHITE), - ); + let draw = FillRef::new(Rect::new(0.0, 0.0, 3.0, 4.0), peniko::Color::WHITE); assert_eq!( draw.prepend_transform(Affine::translate((5.0, 6.0))) @@ -1091,7 +1113,7 @@ mod tests { fn glyph_run_ref_prepend_transform_only_prefixes_run_transform() { let font = FontData::new(peniko::Blob::new(Arc::new([0_u8, 1_u8, 2_u8, 3_u8])), 0); let style = Style::Fill(Fill::NonZero); - let draw = GlyphRunRef::new(&font, &style, Brush::Solid(peniko::Color::BLACK)); + let draw = GlyphRunRef::new(&font, &style, peniko::Color::BLACK); let draw = GlyphRunRef { transform: Affine::translate((1.0, 2.0)), glyph_transform: Some(Affine::translate((3.0, 4.0))), @@ -1168,6 +1190,11 @@ mod tests { std_dev: 2.0, composite: Composite::default(), })); + let picture = ScenePicture::new(Scene::new(), Rect::new(25.0, 26.0, 31.0, 32.0)); + let picture_id = source.draw(Draw::ScenePicture { + transform: Affine::translate((27.0, 28.0)), + picture: picture.clone(), + }); source.pop_group(); source.pop_clip(); @@ -1244,5 +1271,12 @@ mod tests { composite: Composite::default(), }) ); + assert_eq!( + replayed.draw_op(picture_id), + &Draw::ScenePicture { + transform: transform * Affine::translate((27.0, 28.0)), + picture, + } + ); } } diff --git a/imaging/src/painter.rs b/imaging/src/painter.rs index 1c17c67..e3cbd24 100644 --- a/imaging/src/painter.rs +++ b/imaging/src/painter.rs @@ -6,11 +6,12 @@ use core::borrow::Borrow; use kurbo::{Affine, BezPath, CubicBez, Line, QuadBez, Rect, RoundedRect, Stroke, Vec2}; -use peniko::{BrushRef, ImageBrushRef, Style}; +use peniko::Style; use crate::{ - BlurredRoundedRect, ClipRef, Composite, ContextRef, FillRef, GeometryRef, GlyphRunRef, - GroupRef, MaskMode, NormalizedCoord, PaintSink, SourceLocationRef, StrokeRef, record, + BlurredRoundedRect, BrushRef, ClipRef, Composite, ContextRef, FillRef, GeometryRef, + GlyphRunRef, GroupRef, ImageBrushRef, MaskMode, NormalizedCoord, PaintSink, ScenePicture, + SourceLocationRef, StrokeRef, record, }; const DEFAULT_SHAPE_TOLERANCE: f64 = 0.1; @@ -244,12 +245,17 @@ where let rect = Rect::new( 0.0, 0.0, - image.image.width as f64, - image.image.height as f64, + image.image.width() as f64, + image.image.height() as f64, ); self.fill(rect, image).transform(transform).draw(); } + /// Replay a retained scene picture with the given transform. + pub fn draw_scene_picture(&mut self, picture: &ScenePicture, transform: Affine) { + self.sink.scene_picture(picture, transform); + } + /// Push a context annotation onto the context stack. /// /// This is recorded by sinks that support retained context metadata and ignored by sinks that @@ -494,7 +500,7 @@ mod tests { use kurbo::{Circle, Point, Shape as _, Vec2}; use peniko::Fill; - use crate::{BlurredRoundedRect, GroupRef}; + use crate::{BlurredRoundedRect, GroupRef, ScenePicture}; #[derive(Default)] struct RecordingSink { @@ -602,7 +608,7 @@ mod tests { &record::Draw::Fill { transform, fill_rule: Fill::NonZero, - brush: peniko::Brush::Image(peniko::ImageBrush::new(image)), + brush: crate::Brush::Image(crate::ImageBrush::from(image)), brush_transform: None, shape: record::Geometry::Rect(Rect::new(0.0, 0.0, 2.0, 2.0)), composite: Composite::default(), @@ -610,13 +616,41 @@ mod tests { ); } + #[test] + fn draw_scene_picture_records_retained_picture_draw() { + let mut source = record::Scene::new(); + source.draw(record::Draw::Fill { + transform: Affine::IDENTITY, + fill_rule: Fill::NonZero, + brush: crate::Brush::Solid(peniko::Color::WHITE), + brush_transform: None, + shape: record::Geometry::Rect(Rect::new(0.0, 0.0, 2.0, 3.0)), + composite: Composite::default(), + }); + let picture = ScenePicture::new(source, Rect::new(4.0, 5.0, 14.0, 16.0)); + + let mut scene = record::Scene::new(); + let mut painter = Painter::new(&mut scene); + let transform = Affine::translate((8.0, 9.0)); + + painter.draw_scene_picture(&picture, transform); + + assert_eq!( + scene.draw_op(record::DrawId(0)), + &record::Draw::ScenePicture { + transform, + picture: picture.clone(), + } + ); + } + #[test] fn replay_forwards_recorded_scene_into_wrapped_sink() { let mut source = record::Scene::new(); source.draw(record::Draw::Fill { transform: Affine::IDENTITY, fill_rule: Fill::NonZero, - brush: peniko::Brush::Solid(peniko::Color::WHITE), + brush: crate::Brush::Solid(peniko::Color::WHITE), brush_transform: None, shape: record::Geometry::Rect(Rect::new(0.0, 0.0, 2.0, 3.0)), composite: Composite::default(), diff --git a/imaging/src/record.rs b/imaging/src/record.rs index ee56c5b..41eda00 100644 --- a/imaging/src/record.rs +++ b/imaging/src/record.rs @@ -9,11 +9,11 @@ use alloc::{boxed::Box, string::String, vec::Vec}; use kurbo::{Affine, BezPath, Rect, RoundedRect, Shape as _, Stroke, Vec2}; -use peniko::{Brush, Fill, FontData, Style}; +use peniko::{Fill, FontData, Style}; use crate::{ - BlurredRoundedRect, ClipRef, Composite, ContextRef, FillRef, GlyphRunRef, GroupRef, MaskMode, - NormalizedCoord, PaintSink, SourceLocationRef, StrokeRef, + BlurredRoundedRect, Brush, ClipRef, Composite, ContextRef, FillRef, GlyphRunRef, GroupRef, + MaskMode, NormalizedCoord, PaintSink, ScenePicture, SourceLocationRef, StrokeRef, }; /// A geometry payload stored in a recording. @@ -329,6 +329,13 @@ pub enum Draw { GlyphRun(GlyphRun), /// Draw a solid-color rounded rectangle blurred with a gaussian filter. BlurredRoundedRect(BlurredRoundedRect), + /// Replay a retained scene picture. + ScenePicture { + /// Transform applied while replaying the retained picture. + transform: Affine, + /// Retained scene picture to replay. + picture: ScenePicture, + }, } /// A single command in a [`Scene`]. @@ -733,6 +740,17 @@ impl PaintSink for Scene { fn blurred_rounded_rect(&mut self, draw: BlurredRoundedRect) { let _ = Self::draw(self, Draw::BlurredRoundedRect(draw)); } + + #[inline] + fn scene_picture(&mut self, picture: &ScenePicture, transform: Affine) { + let _ = Self::draw( + self, + Draw::ScenePicture { + transform, + picture: picture.clone(), + }, + ); + } } /// Replay a recorded [`Scene`] into a [`crate::PaintSink`]. diff --git a/imaging/src/validation.rs b/imaging/src/validation.rs index 89ad4ab..785238f 100644 --- a/imaging/src/validation.rs +++ b/imaging/src/validation.rs @@ -71,12 +71,11 @@ //! ``` use crate::{ - AppliedMaskRef, BlurredRoundedRect, ClipRef, Composite, FillRef, Filter, GlyphRunRef, GroupRef, - PaintSink, StrokeRef, + AppliedMaskRef, BlurredRoundedRect, BrushRef, ClipRef, Composite, FillRef, Filter, GlyphRunRef, + GroupRef, ImageRef, PaintSink, StrokeRef, record::{self, Geometry, Glyph}, }; use kurbo::{Affine, BezPath, Rect, RoundedRect, Stroke}; -use peniko::BrushRef; /// Decision returned by a [`ValidatingSink`] violation hook. #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -447,25 +446,32 @@ where } } - fn validate_image_brush( - &mut self, - image_brush: peniko::ImageBrush<&peniko::ImageData>, - ) -> bool { + fn validate_image_brush(&mut self, image_brush: crate::ImageBrushRef<'_>) -> bool { if !(image_brush.sampler.alpha.is_finite() && image_brush.sampler.alpha >= 0.0) { return !self.violate(ValidationError::InvalidBrush { what: "Brush::Image::alpha", }); } - let image = image_brush.image; - if image - .format - .size_in_bytes(image.width, image.height) - .is_none_or(|expected| expected != image.data.len()) - { - return !self.violate(ValidationError::InvalidBrush { - what: "Brush::Image::data_len", - }); + match image_brush.image { + ImageRef::Raster(image) => { + if image + .format + .size_in_bytes(image.width, image.height) + .is_none_or(|expected| expected != image.data.len()) + { + return !self.violate(ValidationError::InvalidBrush { + what: "Brush::Image::data_len", + }); + } + } + ImageRef::Scene(scene) => { + if scene.scene().validate().is_err() { + return !self.violate(ValidationError::InvalidBrush { + what: "Brush::Image::scene", + }); + } + } } true @@ -689,6 +695,18 @@ where } self.inner.blurred_rounded_rect(draw); } + + fn scene_picture(&mut self, picture: &crate::ScenePicture, transform: Affine) { + if self.aborted { + return; + } + if !self.validate_affine("Draw::ScenePicture::transform", &transform) + || !self.validate_recorded_scene_stream(picture.scene()) + { + return; + } + self.inner.scene_picture(picture, transform); + } } #[cfg(test)] @@ -853,7 +871,7 @@ mod tests { fn image_brushes_validate_byte_length() { let inner = Scene::new(); let mut sink = ValidatingSink::new(inner); - let paint = Brush::Image(ImageBrush::new(ImageData { + let paint = Brush::Image(ImageBrush::from(ImageData { data: Blob::from(vec![0_u8; 3]), format: ImageFormat::Rgba8, alpha_type: ImageAlphaType::Alpha, diff --git a/imaging_skia/src/lib.rs b/imaging_skia/src/lib.rs index 5ed75ac..9ac3ce2 100644 --- a/imaging_skia/src/lib.rs +++ b/imaging_skia/src/lib.rs @@ -11,6 +11,10 @@ //! This crate provides a CPU raster renderer that consumes `imaging::record::Scene` or native //! Skia draw targets and produces an RGBA8 image buffer using Skia. //! +//! `imaging_skia` supports scene-backed [`imaging::SceneImage`] brushes natively by lowering the +//! retained subscene to Skia picture/image-shader machinery, so `Pad`, `Repeat`, and `Reflect` +//! operate on scene content without first rasterizing through another backend. +//! //! # Render A Recorded Scene //! //! Record commands into [`imaging::record::Scene`], then hand the scene to [`SkiaCpuRenderer`]. @@ -168,7 +172,8 @@ mod vulkan; #[cfg(all(feature = "gpu", any(target_os = "macos", target_os = "ios")))] use foreign_types_shared as _; use imaging::{ - Filter, GeometryRef, GlyphRunRef, RgbaImage, + BrushRef, Filter, GeometryRef, GlyphRunRef, ImageRef, RgbaImage, ScenePicture, + ScenePictureWeak, record::{Scene, ValidateError, replay}, render::{ ImageBufferFormat, ImageBufferTarget, ImageRenderer, ImageRendererError, ImageTargetError, @@ -177,9 +182,7 @@ use imaging::{ }; use kurbo::{Affine, Shape as _}; use peniko::color::{ColorSpaceTag, HueDirection}; -use peniko::{ - BrushRef, ImageAlphaType, ImageData, ImageFormat, ImageQuality, InterpolationAlphaSpace, -}; +use peniko::{ImageAlphaType, ImageData, ImageFormat, ImageQuality, InterpolationAlphaSpace}; use skia_safe as sk; use std::{ cell::{RefCell, RefMut}, @@ -307,6 +310,13 @@ struct CachedImage { bytes: usize, } +#[derive(Clone, Debug)] +struct CachedPicture { + picture_id: u64, + scene_picture: ScenePictureWeak, + picture: sk::Picture, +} + #[derive(Debug)] struct ImageCache { bytes_used: usize, @@ -386,6 +396,52 @@ impl Default for ImageCache { } } +#[derive(Debug, Default)] +struct PictureCache { + entries: VecDeque, +} + +impl PictureCache { + fn clear(&mut self) { + self.entries.clear(); + } + + fn touch(&mut self, index: usize) { + if index + 1 == self.entries.len() { + return; + } + if let Some(entry) = self.entries.remove(index) { + self.entries.push_back(entry); + } + } + + fn prune_dead(&mut self) { + self.entries + .retain(|entry| entry.scene_picture.upgrade().is_some()); + } + + fn get_or_create(&mut self, scene_picture: &ScenePicture) -> Option { + self.prune_dead(); + if let Some(index) = self + .entries + .iter() + .position(|entry| entry.picture_id == scene_picture.id()) + { + let cached = self.entries.get(index)?.picture.clone(); + self.touch(index); + return Some(cached); + } + + let picture = make_skia_picture_from_scene(scene_picture)?; + self.entries.push_back(CachedPicture { + picture_id: scene_picture.id(), + scene_picture: scene_picture.downgrade(), + picture: picture.clone(), + }); + Some(picture) + } +} + /// Shared cache bundle for Skia renderers. /// /// This exists so renderer construction does not grow a new constructor every time more shareable @@ -394,6 +450,7 @@ impl Default for ImageCache { pub struct SkiaCaches { font_cache: SkiaFontCache, image_cache: Rc>, + picture_cache: Rc>, mask_cache: Rc>, } @@ -404,6 +461,7 @@ impl SkiaCaches { Self { font_cache: SkiaFontCache::new(), image_cache: Rc::new(RefCell::new(ImageCache::default())), + picture_cache: Rc::new(RefCell::new(PictureCache::default())), mask_cache: Rc::new(RefCell::new(MaskCache::default())), } } @@ -422,6 +480,9 @@ impl SkiaCaches { fn image_cache(&self) -> Rc> { Rc::clone(&self.image_cache) } + fn picture_cache(&self) -> Rc> { + Rc::clone(&self.picture_cache) + } fn mask_cache(&self) -> Rc> { Rc::clone(&self.mask_cache) } @@ -429,6 +490,7 @@ impl SkiaCaches { fn clear(&self) { self.font_cache.clear(); self.image_cache.borrow_mut().clear(); + self.picture_cache.borrow_mut().clear(); self.mask_cache.borrow_mut().clear(); } @@ -689,6 +751,7 @@ impl SkiaCpuRenderer { let mut sink = SkCanvasSink::new_with_caches( self.surface.canvas(), Some(self.caches.image_cache()), + self.caches.picture_cache(), self.caches.mask_cache(), self.caches.font_cache(), ); @@ -762,6 +825,7 @@ impl ImageRenderer for SkiaCpuRenderer { let mut sink = SkCanvasSink::new_with_caches( self.surface.canvas(), Some(self.caches.image_cache()), + self.caches.picture_cache(), self.caches.mask_cache(), self.caches.font_cache(), ); @@ -780,11 +844,17 @@ fn encode_source_to_picture( height: u32, tolerance: f64, image_cache: Rc>, + picture_cache: Rc>, font_cache: SkiaFontCache, ) -> Result { source.validate().map_err(Error::InvalidScene)?; let bounds = kurbo::Rect::new(0.0, 0.0, f64::from(width), f64::from(height)); - let mut sink = SkPictureRecorderSink::new_with_caches(bounds, Some(image_cache), font_cache); + let mut sink = SkPictureRecorderSink::new_with_caches( + bounds, + Some(image_cache), + picture_cache, + font_cache, + ); sink.set_tolerance(tolerance); source.paint_into(&mut sink); sink.finish_picture() @@ -873,6 +943,7 @@ impl SkiaGpuRendererState { let mut sink = SkCanvasSink::new_with_caches( surface.canvas(), Some(self.caches.image_cache()), + self.caches.picture_cache(), self.caches.mask_cache(), self.caches.font_cache(), ); @@ -961,6 +1032,7 @@ impl SkiaRenderer { height, self.state.tolerance, self.state.caches.image_cache(), + self.state.caches.picture_cache(), self.state.caches.font_cache(), ) } @@ -1125,6 +1197,7 @@ impl ImageRenderer for SkiaRenderer { target.height, self.state.tolerance, self.state.caches.image_cache(), + self.state.caches.picture_cache(), self.state.caches.font_cache(), ) .map_err(map_image_renderer_error)?; @@ -1763,6 +1836,7 @@ fn brush_to_paint( opacity: f32, paint_xf: Affine, image_cache: Option<&Rc>>, + picture_cache: Option<&Rc>>, ) -> Option { let mut paint = sk::Paint::default(); paint.set_anti_alias(true); @@ -1883,15 +1957,31 @@ fn brush_to_paint( } } BrushRef::Image(image_brush) => { - let image = skia_image_from_peniko(image_brush.image, image_cache)?; - let shader = image.to_shader( - Some(( - tile_mode_from_extend(image_brush.sampler.x_extend), - tile_mode_from_extend(image_brush.sampler.y_extend), - )), - sampling_options_from_quality(image_brush.sampler.quality), - Some(&affine_to_matrix(paint_xf)), - )?; + let tile_modes = Some(( + tile_mode_from_extend(image_brush.sampler.x_extend), + tile_mode_from_extend(image_brush.sampler.y_extend), + )); + let shader = match image_brush.image { + ImageRef::Raster(image) => skia_image_from_peniko(image, image_cache)?.to_shader( + tile_modes, + sampling_options_from_quality(image_brush.sampler.quality), + Some(&affine_to_matrix(paint_xf)), + )?, + ImageRef::Scene(scene) => { + let picture = skia_picture_from_scene(scene.picture(), picture_cache)?; + picture.to_shader( + tile_modes, + filter_mode_from_quality(image_brush.sampler.quality), + Some(&affine_to_matrix(paint_xf)), + Some(&sk::Rect::new( + 0.0, + 0.0, + scene.width() as f32, + scene.height() as f32, + )), + ) + } + }; paint.set_shader(shader); paint.set_alpha_f((image_brush.sampler.alpha * alpha_scale).clamp(0.0, 1.0)); } @@ -1910,6 +2000,22 @@ fn skia_image_from_peniko( } } +fn skia_picture_from_scene( + scene_picture: &ScenePicture, + picture_cache: Option<&Rc>>, +) -> Option { + match picture_cache { + Some(picture_cache) => picture_cache.borrow_mut().get_or_create(scene_picture), + None => make_skia_picture_from_scene(scene_picture), + } +} + +fn make_skia_picture_from_scene(scene_picture: &ScenePicture) -> Option { + let mut sink = SkPictureRecorderSink::new(scene_picture.bounds()); + replay(scene_picture.scene(), &mut sink); + sink.finish_picture().ok() +} + fn make_skia_image_from_peniko(image: &ImageData) -> Option { let color_type = match image.format { ImageFormat::Rgba8 => sk::ColorType::RGBA8888, @@ -1941,6 +2047,13 @@ fn sampling_options_from_quality(quality: ImageQuality) -> sk::SamplingOptions { } } +fn filter_mode_from_quality(quality: ImageQuality) -> sk::FilterMode { + match quality { + ImageQuality::Low => sk::FilterMode::Nearest, + ImageQuality::Medium | ImageQuality::High => sk::FilterMode::Linear, + } +} + fn apply_stroke_style(paint: &mut sk::Paint, style: &kurbo::Stroke) { paint.set_style(sk::PaintStyle::Stroke); paint.set_stroke_width(f64_to_f32(style.width)); @@ -2043,13 +2156,13 @@ fn build_filter_chain(filters: &[Filter]) -> Option { mod tests { use super::*; use imaging::{ - GroupRef, MaskMode, Painter, + Brush, GroupRef, ImageBrush, MaskMode, Painter, SceneImage, record::Glyph, render::{ImageBufferTarget, ImageTargetError}, }; use kurbo::Rect; use peniko::{ - Blob, Brush, Color, Fill, FontData, ImageAlphaType, ImageData, ImageFormat, Style, + Blob, Color, Extend, Fill, FontData, ImageAlphaType, ImageData, ImageFormat, Style, }; use std::sync::{Arc, OnceLock}; #[cfg(feature = "gpu")] @@ -2109,7 +2222,7 @@ mod tests { } fn image_scene() -> Scene { - let brush = Brush::Image(peniko::ImageBrush::new(test_image())); + let brush = Brush::Image(ImageBrush::from(test_image())); let mut scene = Scene::new(); { let mut painter = Painter::new(&mut scene); @@ -2186,6 +2299,45 @@ mod tests { assert_eq!(renderer.caches.image_cache().borrow().len(), 0); } + #[test] + fn scene_image_brush_reflects_in_skia() { + let mut source = Scene::new(); + { + let mut painter = Painter::new(&mut source); + painter.fill_rect( + Rect::new(0.0, 0.0, 1.0, 1.0), + Color::from_rgb8(0xff, 0x00, 0x00), + ); + painter.fill_rect( + Rect::new(1.0, 0.0, 2.0, 1.0), + Color::from_rgb8(0x00, 0xff, 0x00), + ); + } + + let scene_image = SceneImage::new(source, 2, 1); + let brush = Brush::Image( + ImageBrush::from(scene_image) + .with_extend(Extend::Reflect) + .with_quality(ImageQuality::Low), + ); + + let mut scene = Scene::new(); + { + let mut painter = Painter::new(&mut scene); + painter.fill(Rect::new(0.0, 0.0, 4.0, 1.0), &brush).draw(); + } + + let mut renderer = SkiaCpuRenderer::new(); + let image = renderer.render_scene(&scene, 4, 1).unwrap(); + assert_eq!( + &image.data[..16], + &[ + 0xff, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0xff, 0x00, + 0x00, 0xff + ] + ); + } + #[test] fn changing_tolerance_clears_cached_masks() { let scene = masked_scene(MaskMode::Alpha); diff --git a/imaging_skia/src/sinks.rs b/imaging_skia/src/sinks.rs index 50ceb34..0928bab 100644 --- a/imaging_skia/src/sinks.rs +++ b/imaging_skia/src/sinks.rs @@ -2,9 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT use super::{ - Error, ImageCache, SkiaFontCache, affine_to_matrix, apply_stroke_style, bez_to_sk_path, - brush_to_paint, build_filter_chain, f64_to_f32, geometry_to_bez_path, geometry_to_sk_path, - map_blend_mode, path_with_fill_rule, skia_font_from_glyph_run, + Error, ImageCache, PictureCache, SkiaFontCache, affine_to_matrix, apply_stroke_style, + bez_to_sk_path, brush_to_paint, build_filter_chain, f64_to_f32, geometry_to_bez_path, + geometry_to_sk_path, map_blend_mode, path_with_fill_rule, skia_font_from_glyph_run, }; use imaging::{ BlurredRoundedRect, ClipRef, FillRef, GeometryRef, GlyphRunRef, GroupRef, MaskMode, PaintSink, @@ -303,6 +303,7 @@ fn draw_glyph_run( canvas: &sk::Canvas, state: &mut StreamState, image_cache: Option<&Rc>>, + picture_cache: Option<&Rc>>, font_cache: Option<&SkiaFontCache>, glyph_run: GlyphRunRef<'_>, glyphs: &mut dyn Iterator, @@ -319,6 +320,7 @@ fn draw_glyph_run( glyph_run.composite.alpha, Affine::IDENTITY, image_cache, + picture_cache, ) else { state.set_error_once(Error::Internal("invalid image brush")); return; @@ -449,6 +451,7 @@ fn draw_masked_group( state: &mut StreamState, masked: MaskedGroupFrame, image_cache: Option<&Rc>>, + picture_cache: Option<&Rc>>, mask_cache: Option<&Rc>>, font_cache: Option<&SkiaFontCache>, ) { @@ -481,6 +484,9 @@ fn draw_masked_group( (Some(cache), Some(font_cache)) => SkCanvasSink::new_with_caches( mask_surface.canvas(), image_cache.cloned(), + picture_cache + .cloned() + .expect("picture cache should accompany image cache"), cache.clone(), font_cache.clone(), ), @@ -527,6 +533,9 @@ fn draw_masked_group( (Some(cache), Some(font_cache)) => SkCanvasSink::new_with_caches( content_surface.canvas(), image_cache.cloned(), + picture_cache + .cloned() + .expect("picture cache should accompany image cache"), cache.clone(), font_cache.clone(), ), @@ -656,6 +665,7 @@ fn paint_sink_pop_group( canvas: &sk::Canvas, state: &mut StreamState, image_cache: Option<&Rc>>, + picture_cache: Option<&Rc>>, mask_cache: Option<&Rc>>, font_cache: Option<&SkiaFontCache>, ) { @@ -679,7 +689,15 @@ fn paint_sink_pop_group( state.group_stack.push(GroupFrame::Masked(frame)); return; } - draw_masked_group(canvas, state, *frame, image_cache, mask_cache, font_cache); + draw_masked_group( + canvas, + state, + *frame, + image_cache, + picture_cache, + mask_cache, + font_cache, + ); } } } @@ -688,6 +706,7 @@ fn paint_sink_fill( canvas: &sk::Canvas, state: &mut StreamState, image_cache: Option<&Rc>>, + picture_cache: Option<&Rc>>, draw: FillRef<'_>, ) { if state.error.is_some() { @@ -704,6 +723,7 @@ fn paint_sink_fill( draw.composite.alpha, draw.brush_transform.unwrap_or(Affine::IDENTITY), image_cache, + picture_cache, ) else { state.set_error_once(Error::Internal("invalid image brush")); return; @@ -744,6 +764,7 @@ fn paint_sink_stroke( canvas: &sk::Canvas, state: &mut StreamState, image_cache: Option<&Rc>>, + picture_cache: Option<&Rc>>, draw: StrokeRef<'_>, ) { if state.error.is_some() { @@ -760,6 +781,7 @@ fn paint_sink_stroke( draw.composite.alpha, draw.brush_transform.unwrap_or(Affine::IDENTITY), image_cache, + picture_cache, ) else { state.set_error_once(Error::Internal("invalid image brush")); return; @@ -797,6 +819,7 @@ fn paint_sink_stroke( pub struct SkCanvasSink<'a> { canvas: &'a sk::Canvas, image_cache: Option>>, + picture_cache: Option>>, mask_cache: Option>>, font_cache: Option, state: StreamState, @@ -819,6 +842,7 @@ impl<'a> SkCanvasSink<'a> { Self { canvas, image_cache: None, + picture_cache: None, mask_cache: None, font_cache: None, state: StreamState::new(), @@ -828,12 +852,14 @@ impl<'a> SkCanvasSink<'a> { pub(crate) fn new_with_caches( canvas: &'a sk::Canvas, image_cache: Option>>, + picture_cache: Rc>, mask_cache: Rc>, font_cache: SkiaFontCache, ) -> Self { Self { canvas, image_cache, + picture_cache: Some(picture_cache), mask_cache: Some(mask_cache), font_cache: Some(font_cache), state: StreamState::new(), @@ -869,6 +895,7 @@ impl PaintSink for SkCanvasSink<'_> { self.canvas, &mut self.state, self.image_cache.as_ref(), + self.picture_cache.as_ref(), self.mask_cache.as_ref(), self.font_cache.as_ref(), ); @@ -879,6 +906,7 @@ impl PaintSink for SkCanvasSink<'_> { self.canvas, &mut self.state, self.image_cache.as_ref(), + self.picture_cache.as_ref(), draw, ); } @@ -888,6 +916,7 @@ impl PaintSink for SkCanvasSink<'_> { self.canvas, &mut self.state, self.image_cache.as_ref(), + self.picture_cache.as_ref(), draw, ); } @@ -908,6 +937,7 @@ impl PaintSink for SkCanvasSink<'_> { self.canvas, &mut self.state, self.image_cache.as_ref(), + self.picture_cache.as_ref(), self.font_cache.as_ref(), draw, glyphs, @@ -930,6 +960,7 @@ impl PaintSink for SkCanvasSink<'_> { pub struct SkPictureRecorderSink { recorder: sk::PictureRecorder, image_cache: Option>>, + picture_cache: Option>>, font_cache: Option, state: StreamState, } @@ -955,20 +986,28 @@ impl SkPictureRecorderSink { pub(crate) fn new_with_caches( bounds: Rect, image_cache: Option>>, + picture_cache: Rc>, font_cache: SkiaFontCache, ) -> Self { - Self::new_with_options(bounds, false, image_cache, Some(font_cache)) + Self::new_with_options( + bounds, + false, + image_cache, + Some(picture_cache), + Some(font_cache), + ) } /// Start recording a Skia picture with optional bounding-box hierarchy acceleration. pub fn new_with_bbh(bounds: Rect, use_bbh: bool) -> Self { - Self::new_with_options(bounds, use_bbh, None, None) + Self::new_with_options(bounds, use_bbh, None, None, None) } fn new_with_options( bounds: Rect, use_bbh: bool, image_cache: Option>>, + picture_cache: Option>>, font_cache: Option, ) -> Self { let mut recorder = sk::PictureRecorder::new(); @@ -982,6 +1021,7 @@ impl SkPictureRecorderSink { Self { recorder, image_cache, + picture_cache, font_cache, state: StreamState::new(), } @@ -1043,6 +1083,7 @@ impl PaintSink for SkPictureRecorderSink { canvas, state, self.image_cache.as_ref(), + self.picture_cache.as_ref(), None, self.font_cache.as_ref(), ); @@ -1055,7 +1096,13 @@ impl PaintSink for SkPictureRecorderSink { state.set_error_once(Error::Internal("picture recorder not recording")); return; }; - paint_sink_fill(canvas, state, self.image_cache.as_ref(), draw); + paint_sink_fill( + canvas, + state, + self.image_cache.as_ref(), + self.picture_cache.as_ref(), + draw, + ); } fn stroke(&mut self, draw: StrokeRef<'_>) { @@ -1065,7 +1112,13 @@ impl PaintSink for SkPictureRecorderSink { state.set_error_once(Error::Internal("picture recorder not recording")); return; }; - paint_sink_stroke(canvas, state, self.image_cache.as_ref(), draw); + paint_sink_stroke( + canvas, + state, + self.image_cache.as_ref(), + self.picture_cache.as_ref(), + draw, + ); } fn glyph_run( @@ -1090,6 +1143,7 @@ impl PaintSink for SkPictureRecorderSink { canvas, state, self.image_cache.as_ref(), + self.picture_cache.as_ref(), self.font_cache.as_ref(), draw, glyphs, diff --git a/imaging_snapshot_tests/src/cases/images.rs b/imaging_snapshot_tests/src/cases/images.rs index 4cbe6f9..0c36b18 100644 --- a/imaging_snapshot_tests/src/cases/images.rs +++ b/imaging_snapshot_tests/src/cases/images.rs @@ -26,7 +26,7 @@ impl SnapshotCase for GmImageBrushes { let mut painter = Painter::new(sink); let left = Brush::Image( - ImageBrush::new(test_image()) + ImageBrush::from(test_image()) .with_extend(Extend::Pad) .with_quality(ImageQuality::Medium), ); @@ -45,7 +45,7 @@ impl SnapshotCase for GmImageBrushes { .draw(); let diamond_brush = Brush::Image( - ImageBrush::new(test_image()) + ImageBrush::from(test_image()) .with_x_extend(Extend::Reflect) .with_y_extend(Extend::Pad) .with_quality(ImageQuality::Medium), @@ -66,7 +66,7 @@ impl SnapshotCase for GmImageBrushes { let frame_stroke = Stroke::new(20.0); let frame_brush = Brush::Image( - ImageBrush::new(test_image()) + ImageBrush::from(test_image()) .with_extend(Extend::Reflect) .with_quality(ImageQuality::Low), ); diff --git a/imaging_snapshot_tests/tests/vello_hybrid_snapshots.rs b/imaging_snapshot_tests/tests/vello_hybrid_snapshots.rs index 8927076..7669337 100644 --- a/imaging_snapshot_tests/tests/vello_hybrid_snapshots.rs +++ b/imaging_snapshot_tests/tests/vello_hybrid_snapshots.rs @@ -96,7 +96,7 @@ fn native_scene_sink_supports_image_brushes_with_renderer() { let mut scene = vello_hybrid::Scene::new(32, 32); scene.reset(); { - let brush = Brush::Image(ImageBrush::new(ImageData { + let brush = Brush::Image(ImageBrush::from(ImageData { data: Blob::new(Arc::new([ 0xff, 0x20, 0x20, 0xff, 0x20, 0xff, 0x20, 0xff, 0x20, 0x20, 0xff, 0xff, 0xff, 0xff, 0x20, 0xff, diff --git a/imaging_tiny_skia/src/lib.rs b/imaging_tiny_skia/src/lib.rs index 2bb7c37..77f2f66 100644 --- a/imaging_tiny_skia/src/lib.rs +++ b/imaging_tiny_skia/src/lib.rs @@ -11,13 +11,16 @@ //! //! The implementation was integrated from Floem's tiny-skia renderer and adapted to match the //! public renderer shape used by the other `imaging_*` backends. +//! +//! `imaging_tiny_skia` supports scene-backed [`imaging::SceneImage`] brushes by rasterizing the +//! retained subscene to a cached pixmap and then applying the normal image-brush sampling path. #![deny(unsafe_code)] #![cfg_attr(not(test), warn(unused_crate_dependencies))] use imaging::{ - BlurredRoundedRect, ClipRef, Composite, FillRef, Filter, GlyphRunRef, GroupRef, MaskMode, - PaintSink, RgbaImage, StrokeRef, + BlurredRoundedRect, BrushRef, ClipRef, Composite, FillRef, Filter, GlyphRunRef, GroupRef, + ImageRef, MaskMode, PaintSink, RgbaImage, SceneImageWeak, StrokeRef, record::{Scene, ValidateError}, render::{ ImageBufferFormat, ImageBufferTarget, ImageRenderer, ImageRendererError, ImageTargetError, @@ -26,14 +29,13 @@ use imaging::{ }; use kurbo::{Affine, BezPath, Cap, Join, Point, Rect, Shape, Stroke as KurboStroke, Vec2}; use peniko::{ - BlendMode, BrushRef, Color, Compose, Extend, Gradient, GradientKind, ImageData, ImageQuality, - Mix, RadialGradientPosition, + BlendMode, Color, Compose, Extend, Gradient, GradientKind, ImageData, ImageQuality, Mix, + RadialGradientPosition, color::{self, ColorSpaceTag, DynamicColor, HueDirection, Srgb}, kurbo::{PathEl, Size}, }; use rustc_hash::FxHashMap; use std::{ - borrow::Borrow, collections::VecDeque, mem::Discriminant, sync::Arc, @@ -216,13 +218,15 @@ impl GlyphCacheKey { } } -type ImageCacheMap = FxHashMap)>; +type ImageCacheMap = FxHashMap)>; +type ScenePixmapCacheMap = FxHashMap; type ScaledImageCacheMap = FxHashMap)>; type BlurredRRectCacheMap = FxHashMap)>; type GlyphCacheMap = FxHashMap<(GlyphCacheKey, u32), GlyphCacheEntry>; struct RendererCaches { image_cache: ImageCacheMap, + scene_pixmap_cache: ScenePixmapCacheMap, scaled_image_cache: ScaledImageCacheMap, blurred_rrect_cache: BlurredRRectCacheMap, // The `u32` is a color encoded as a u32 so that it is hashable and eq. @@ -234,6 +238,7 @@ impl RendererCaches { fn new() -> Self { Self { image_cache: FxHashMap::default(), + scene_pixmap_cache: FxHashMap::default(), scaled_image_cache: FxHashMap::default(), blurred_rrect_cache: FxHashMap::default(), glyph_cache: FxHashMap::default(), @@ -242,6 +247,12 @@ impl RendererCaches { } } +struct CachedScenePixmap { + cache_color: CacheColor, + scene_image: SceneImageWeak, + pixmap: Arc, +} + const GLYPH_FILTER_PAD: u32 = 1; struct GlyphRasterRequest<'a> { @@ -412,9 +423,21 @@ fn should_retain_glyph_entry( entry.cache_color == cache_color || now.duration_since(entry.last_touched) < GLYPH_CACHE_MIN_TTL } +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] +enum ImageCacheKey { + Raster(u64), + Scene(u64), +} + +#[derive(Clone, Copy)] +struct ResolvedImageBrush { + key: ImageCacheKey, + sampler: peniko::ImageSampler, +} + #[derive(Hash, PartialEq, Eq)] struct ScaledImageCacheKey { - image_id: u64, + image_key: ImageCacheKey, width: u32, height: u32, quality: Discriminant, @@ -1338,18 +1361,15 @@ impl<'a> Layer<'a> { ); } - fn try_fill_image_fallback( + fn try_fill_image_fallback( &mut self, shape: &impl Shape, image_pixmap: &Pixmap, - image: &peniko::ImageBrush, + image: &ResolvedImageBrush, brush_transform: Option, opacity: f32, blend_mode: TinyBlendMode, - ) -> bool - where - T: Borrow, - { + ) -> bool { if !image_brush_has_mixed_extend(image) { return false; } @@ -1583,17 +1603,15 @@ impl<'a> Layer<'a> { true } - fn fill_image_with_pixmap_and_mode( + fn fill_image_with_pixmap_and_mode( &mut self, shape: &impl Shape, image_pixmap: &Pixmap, - image: &peniko::ImageBrush, + image: &ResolvedImageBrush, brush_transform: Option, opacity: f32, blend_mode: TinyBlendMode, - ) where - T: Borrow, - { + ) { if self.try_fill_image_fallback( shape, image_pixmap, @@ -1639,18 +1657,16 @@ impl<'a> Layer<'a> { } } - fn stroke_image_with_pixmap_and_mode( + fn stroke_image_with_pixmap_and_mode( &mut self, shape: &impl Shape, image_pixmap: &Pixmap, - image: &peniko::ImageBrush, + image: &ResolvedImageBrush, stroke: &KurboStroke, brush_transform: Option, opacity: f32, blend_mode: TinyBlendMode, - ) where - T: Borrow, - { + ) { if image_brush_has_mixed_extend(image) { return; } @@ -1695,12 +1711,16 @@ impl Layer<'_> { } fn stroke_with_brush_transform<'b, 's>( &mut self, + caches: &mut RendererCaches, + cache_color: CacheColor, shape: &impl Shape, brush: impl Into>, stroke: &'s KurboStroke, brush_transform: Option, ) { self.stroke_with_brush_transform_and_mode( + caches, + cache_color, shape, brush, stroke, @@ -1712,6 +1732,8 @@ impl Layer<'_> { fn stroke_with_brush_transform_and_mode<'b, 's>( &mut self, + caches: &mut RendererCaches, + cache_color: CacheColor, shape: &impl Shape, brush: impl Into>, stroke: &'s KurboStroke, @@ -1722,7 +1744,7 @@ impl Layer<'_> { let path = try_ret!(shape_to_path(shape)); let brush = brush.into(); if let BrushRef::Image(image) = brush { - let image_pixmap = try_ret!(image_brush_pixmap(&image)); + let (image, image_pixmap) = try_ret!(resolve_image_brush(caches, cache_color, image)); self.stroke_image_with_pixmap_and_mode( shape, &image_pixmap, @@ -1745,16 +1767,21 @@ impl Layer<'_> { } fn fill<'b>(&mut self, shape: &impl Shape, brush: impl Into>) { - self.fill_with_brush_transform(shape, brush, None); + let mut caches = RendererCaches::new(); + self.fill_with_brush_transform(&mut caches, CacheColor(false), shape, brush, None); } fn fill_with_brush_transform<'b>( &mut self, + caches: &mut RendererCaches, + cache_color: CacheColor, shape: &impl Shape, brush: impl Into>, brush_transform: Option, ) { self.fill_with_brush_transform_and_mode( + caches, + cache_color, shape, brush, brush_transform, @@ -1765,6 +1792,8 @@ impl Layer<'_> { fn fill_with_brush_transform_and_mode<'b>( &mut self, + caches: &mut RendererCaches, + cache_color: CacheColor, shape: &impl Shape, brush: impl Into>, brush_transform: Option, @@ -1773,7 +1802,7 @@ impl Layer<'_> { ) { let brush = brush.into(); if let BrushRef::Image(image) = brush { - let image_pixmap = try_ret!(image_brush_pixmap(&image)); + let (image, image_pixmap) = try_ret!(resolve_image_brush(caches, cache_color, image)); self.fill_image_with_pixmap_and_mode( shape, &image_pixmap, @@ -2261,7 +2290,7 @@ impl<'renderer, 'target> TinySkiaRender<'renderer, 'target> { match brush.into() { BrushRef::Solid(color) => Some(peniko::Brush::Solid(color)), BrushRef::Gradient(gradient) => Some(peniko::Brush::Gradient(gradient.clone())), - BrushRef::Image(image) => Some(peniko::Brush::Image(image.to_owned())), + BrushRef::Image(_) => None, } } @@ -2304,14 +2333,14 @@ impl<'renderer, 'target> TinySkiaRender<'renderer, 'target> { fn try_fill_cached_image_rect( &mut self, - image: &peniko::ImageBrush, + image: &ResolvedImageBrush, + image_pixmap: &Pixmap, rect: Rect, draw: &FillRef<'_>, ) -> bool { if let BlendStrategy::SinglePass(blend_mode) = determine_blend_strategy(&draw.composite.blend) { - let cache_color = self.cache_color; let transform = draw.transform; let brush_transform = draw.brush_transform; let (caches, layers) = (&mut self.caches, &mut self.layers); @@ -2321,9 +2350,10 @@ impl<'renderer, 'target> TinySkiaRender<'renderer, 'target> { layer.transform = transform; return render_cached_image_rect( caches, - cache_color, + self.cache_color, layer, image, + image_pixmap, rect, transform, brush_transform, @@ -2340,6 +2370,7 @@ impl<'renderer, 'target> TinySkiaRender<'renderer, 'target> { self.cache_color, &mut child, image, + image_pixmap, rect, draw.transform, draw.brush_transform, @@ -2355,6 +2386,8 @@ impl<'renderer, 'target> TinySkiaRender<'renderer, 'target> { } fn fill_geometry<'b>( + caches: &mut RendererCaches, + cache_color: CacheColor, layer: &mut Layer<'_>, shape: &imaging::GeometryRef<'_>, brush: impl Into>, @@ -2362,21 +2395,23 @@ impl<'renderer, 'target> TinySkiaRender<'renderer, 'target> { ) { match shape { imaging::GeometryRef::Rect(rect) => { - layer.fill_with_brush_transform(rect, brush, brush_transform); + layer.fill_with_brush_transform(caches, cache_color, rect, brush, brush_transform); } imaging::GeometryRef::RoundedRect(rect) => { - layer.fill_with_brush_transform(rect, brush, brush_transform); + layer.fill_with_brush_transform(caches, cache_color, rect, brush, brush_transform); } imaging::GeometryRef::Path(path) => { - layer.fill_with_brush_transform(path, brush, brush_transform); + layer.fill_with_brush_transform(caches, cache_color, path, brush, brush_transform); } imaging::GeometryRef::OwnedPath(path) => { - layer.fill_with_brush_transform(path, brush, brush_transform); + layer.fill_with_brush_transform(caches, cache_color, path, brush, brush_transform); } } } fn fill_geometry_with_mode<'b>( + caches: &mut RendererCaches, + cache_color: CacheColor, layer: &mut Layer<'_>, shape: &imaging::GeometryRef<'_>, brush: impl Into>, @@ -2386,6 +2421,8 @@ impl<'renderer, 'target> TinySkiaRender<'renderer, 'target> { ) { match shape { imaging::GeometryRef::Rect(rect) => layer.fill_with_brush_transform_and_mode( + caches, + cache_color, rect, brush, brush_transform, @@ -2393,6 +2430,8 @@ impl<'renderer, 'target> TinySkiaRender<'renderer, 'target> { blend_mode, ), imaging::GeometryRef::RoundedRect(rect) => layer.fill_with_brush_transform_and_mode( + caches, + cache_color, rect, brush, brush_transform, @@ -2400,6 +2439,8 @@ impl<'renderer, 'target> TinySkiaRender<'renderer, 'target> { blend_mode, ), imaging::GeometryRef::Path(path) => layer.fill_with_brush_transform_and_mode( + caches, + cache_color, path, brush, brush_transform, @@ -2407,6 +2448,8 @@ impl<'renderer, 'target> TinySkiaRender<'renderer, 'target> { blend_mode, ), imaging::GeometryRef::OwnedPath(path) => layer.fill_with_brush_transform_and_mode( + caches, + cache_color, path, brush, brush_transform, @@ -2420,7 +2463,7 @@ impl<'renderer, 'target> TinySkiaRender<'renderer, 'target> { layer: &mut Layer<'_>, shape: &imaging::GeometryRef<'_>, image_pixmap: &Pixmap, - image: &peniko::ImageBrush, + image: &ResolvedImageBrush, brush_transform: Option, opacity: f32, blend_mode: TinyBlendMode, @@ -2462,6 +2505,8 @@ impl<'renderer, 'target> TinySkiaRender<'renderer, 'target> { } fn stroke_geometry<'b>( + caches: &mut RendererCaches, + cache_color: CacheColor, layer: &mut Layer<'_>, shape: &imaging::GeometryRef<'_>, brush: impl Into>, @@ -2470,21 +2515,51 @@ impl<'renderer, 'target> TinySkiaRender<'renderer, 'target> { ) { match shape { imaging::GeometryRef::Rect(rect) => { - layer.stroke_with_brush_transform(rect, brush, stroke, brush_transform); + layer.stroke_with_brush_transform( + caches, + cache_color, + rect, + brush, + stroke, + brush_transform, + ); } imaging::GeometryRef::RoundedRect(rect) => { - layer.stroke_with_brush_transform(rect, brush, stroke, brush_transform); + layer.stroke_with_brush_transform( + caches, + cache_color, + rect, + brush, + stroke, + brush_transform, + ); } imaging::GeometryRef::Path(path) => { - layer.stroke_with_brush_transform(path, brush, stroke, brush_transform); + layer.stroke_with_brush_transform( + caches, + cache_color, + path, + brush, + stroke, + brush_transform, + ); } imaging::GeometryRef::OwnedPath(path) => { - layer.stroke_with_brush_transform(path, brush, stroke, brush_transform); + layer.stroke_with_brush_transform( + caches, + cache_color, + path, + brush, + stroke, + brush_transform, + ); } } } fn stroke_geometry_with_mode<'b>( + caches: &mut RendererCaches, + cache_color: CacheColor, layer: &mut Layer<'_>, shape: &imaging::GeometryRef<'_>, brush: impl Into>, @@ -2495,6 +2570,8 @@ impl<'renderer, 'target> TinySkiaRender<'renderer, 'target> { ) { match shape { imaging::GeometryRef::Rect(rect) => layer.stroke_with_brush_transform_and_mode( + caches, + cache_color, rect, brush, stroke, @@ -2503,6 +2580,8 @@ impl<'renderer, 'target> TinySkiaRender<'renderer, 'target> { blend_mode, ), imaging::GeometryRef::RoundedRect(rect) => layer.stroke_with_brush_transform_and_mode( + caches, + cache_color, rect, brush, stroke, @@ -2511,6 +2590,8 @@ impl<'renderer, 'target> TinySkiaRender<'renderer, 'target> { blend_mode, ), imaging::GeometryRef::Path(path) => layer.stroke_with_brush_transform_and_mode( + caches, + cache_color, path, brush, stroke, @@ -2519,6 +2600,8 @@ impl<'renderer, 'target> TinySkiaRender<'renderer, 'target> { blend_mode, ), imaging::GeometryRef::OwnedPath(path) => layer.stroke_with_brush_transform_and_mode( + caches, + cache_color, path, brush, stroke, @@ -2533,7 +2616,7 @@ impl<'renderer, 'target> TinySkiaRender<'renderer, 'target> { layer: &mut Layer<'_>, shape: &imaging::GeometryRef<'_>, image_pixmap: &Pixmap, - image: &peniko::ImageBrush, + image: &ResolvedImageBrush, stroke: &KurboStroke, brush_transform: Option, opacity: f32, @@ -2641,6 +2724,62 @@ fn rasterize_scene_mask(scene: &Scene, bounds: IntRect, transform: Affine) -> Op Some(pixmap) } +fn cache_scene_image_pixmap( + caches: &mut RendererCaches, + cache_color: CacheColor, + image: &imaging::SceneImage, +) -> Option> { + caches + .scene_pixmap_cache + .retain(|_, entry| entry.scene_image.upgrade().is_some()); + + if let Some(entry) = caches.scene_pixmap_cache.get_mut(&image.id()) { + entry.cache_color = cache_color; + return Some(entry.pixmap.clone()); + } + + let pixmap = rasterize_scene_pixmap( + image.scene(), + image.width(), + image.height(), + Affine::IDENTITY, + )?; + caches.scene_pixmap_cache.insert( + image.id(), + CachedScenePixmap { + cache_color, + scene_image: image.downgrade(), + pixmap: pixmap.clone(), + }, + ); + Some(pixmap) +} + +fn resolve_image_brush( + caches: &mut RendererCaches, + cache_color: CacheColor, + image: imaging::ImageBrushRef<'_>, +) -> Option<(ResolvedImageBrush, Arc)> { + match image.image { + ImageRef::Raster(image_data) => { + let resolved = ResolvedImageBrush { + key: ImageCacheKey::Raster(image_data.data.id()), + sampler: image.sampler, + }; + let pixmap = cache_image_pixmap(caches, cache_color, image_data)?; + Some((resolved, pixmap)) + } + ImageRef::Scene(scene) => { + let resolved = ResolvedImageBrush { + key: ImageCacheKey::Scene(scene.id()), + sampler: image.sampler, + }; + let pixmap = cache_scene_image_pixmap(caches, cache_color, scene)?; + Some((resolved, pixmap)) + } + } +} + impl PaintSink for TinySkiaRender<'_, '_> { fn push_clip(&mut self, clip: ClipRef<'_>) { let clip_path = self.clip_path_for_clip(clip); @@ -2704,16 +2843,14 @@ impl PaintSink for TinySkiaRender<'_, '_> { } fn fill(&mut self, draw: FillRef<'_>) { - let Some(brush) = self.brush_to_owned(draw.brush) else { - return; - }; - if let peniko::Brush::Image(image) = &brush { - let Some(image_pixmap) = cache_image_pixmap(self.caches, self.cache_color, image) + if let BrushRef::Image(image) = draw.brush { + let Some((image, image_pixmap)) = + resolve_image_brush(self.caches, self.cache_color, image) else { return; }; if let imaging::GeometryRef::Rect(rect) = draw.shape - && self.try_fill_cached_image_rect(image, rect, &draw) + && self.try_fill_cached_image_rect(&image, &image_pixmap, rect, &draw) { return; } @@ -2727,7 +2864,7 @@ impl PaintSink for TinySkiaRender<'_, '_> { layer, &draw.shape, &image_pixmap, - image, + &image, draw.brush_transform, draw.composite.alpha, blend_mode, @@ -2743,7 +2880,7 @@ impl PaintSink for TinySkiaRender<'_, '_> { &mut child, &draw.shape, &image_pixmap, - image, + &image, draw.brush_transform, 1.0, TinyBlendMode::SourceOver, @@ -2753,12 +2890,21 @@ impl PaintSink for TinySkiaRender<'_, '_> { return; } + let Some(brush) = self.brush_to_owned(draw.brush) else { + return; + }; + if let BlendStrategy::SinglePass(blend_mode) = determine_blend_strategy(&draw.composite.blend) { - let layer = self.current_layer_mut(); + let (caches, layers) = (&mut self.caches, &mut self.layers); + let layer = layers + .last_mut() + .expect("TinySkiaRender always has a root layer"); layer.transform = draw.transform; Self::fill_geometry_with_mode( + caches, + self.cache_color, layer, &draw.shape, &brush, @@ -2771,18 +2917,23 @@ impl PaintSink for TinySkiaRender<'_, '_> { let transform = draw.transform; let brush_transform = draw.brush_transform; - self.draw_with_composite(draw.composite, |layer| { + self.draw_with_composite(draw.composite, |caches, cache_color, layer| { layer.transform = transform; - Self::fill_geometry(layer, &draw.shape, &brush, brush_transform); + Self::fill_geometry( + caches, + cache_color, + layer, + &draw.shape, + &brush, + brush_transform, + ); }); } fn stroke(&mut self, draw: StrokeRef<'_>) { - let Some(brush) = self.brush_to_owned(draw.brush) else { - return; - }; - if let peniko::Brush::Image(image) = &brush { - let Some(image_pixmap) = cache_image_pixmap(self.caches, self.cache_color, image) + if let BrushRef::Image(image) = draw.brush { + let Some((image, image_pixmap)) = + resolve_image_brush(self.caches, self.cache_color, image) else { return; }; @@ -2795,7 +2946,7 @@ impl PaintSink for TinySkiaRender<'_, '_> { layer, &draw.shape, &image_pixmap, - image, + &image, draw.stroke, draw.brush_transform, draw.composite.alpha, @@ -2812,7 +2963,7 @@ impl PaintSink for TinySkiaRender<'_, '_> { &mut child, &draw.shape, &image_pixmap, - image, + &image, draw.stroke, draw.brush_transform, 1.0, @@ -2823,12 +2974,21 @@ impl PaintSink for TinySkiaRender<'_, '_> { return; } + let Some(brush) = self.brush_to_owned(draw.brush) else { + return; + }; + if let BlendStrategy::SinglePass(blend_mode) = determine_blend_strategy(&draw.composite.blend) { - let layer = self.current_layer_mut(); + let (caches, layers) = (&mut self.caches, &mut self.layers); + let layer = layers + .last_mut() + .expect("TinySkiaRender always has a root layer"); layer.transform = draw.transform; Self::stroke_geometry_with_mode( + caches, + self.cache_color, layer, &draw.shape, &brush, @@ -2842,9 +3002,18 @@ impl PaintSink for TinySkiaRender<'_, '_> { let transform = draw.transform; let brush_transform = draw.brush_transform; - self.draw_with_composite(draw.composite, |layer| { + let stroke = draw.stroke; + self.draw_with_composite(draw.composite, |caches, cache_color, layer| { layer.transform = transform; - Self::stroke_geometry(layer, &draw.shape, &brush, draw.stroke, brush_transform); + Self::stroke_geometry( + caches, + cache_color, + layer, + &draw.shape, + &brush, + stroke, + brush_transform, + ); }); } @@ -3597,6 +3766,8 @@ fn draw_brushed_glyphs_into_layer<'a>( ), ); content_layer.fill_with_brush_transform_and_mode( + caches, + cache_color, &canvas_rect, run.brush, Some(Affine::translate(( @@ -3668,19 +3839,27 @@ impl<'renderer, 'target> TinySkiaRender<'renderer, 'target> { ) } - fn draw_with_composite(&mut self, composite: Composite, draw: impl FnOnce(&mut Layer<'_>)) { + fn draw_with_composite( + &mut self, + composite: Composite, + draw: impl FnOnce(&mut RendererCaches, CacheColor, &mut Layer<'_>), + ) { if composite == Composite::default() { let transform = self.transform; - let layer = self.current_layer_mut(); + let cache_color = self.cache_color; + let (caches, layers) = (&mut self.caches, &mut self.layers); + let layer = layers + .last_mut() + .expect("TinySkiaRender always has a root layer"); layer.transform = transform; - draw(layer); + draw(caches, cache_color, layer); return; } let Some(mut child) = self.new_composite_child_layer(composite, self.transform) else { return; }; - draw(&mut child); + draw(self.caches, self.cache_color, &mut child); let parent = self.current_layer_mut(); apply_layer(&child, parent); @@ -3702,6 +3881,9 @@ impl<'renderer, 'target> TinySkiaRender<'renderer, 'target> { self.caches .image_cache .retain(|_, (c, _)| *c == self.cache_color); + self.caches.scene_pixmap_cache.retain(|_, entry| { + entry.cache_color == self.cache_color && entry.scene_image.upgrade().is_some() + }); self.caches .scaled_image_cache .retain(|_, (c, _)| *c == self.cache_color); @@ -3829,25 +4011,21 @@ fn realize_image_pixmap(image_data: &ImageData) -> Option { Some(pixmap) } -fn cache_image_pixmap( +fn cache_image_pixmap( caches: &mut RendererCaches, cache_color: CacheColor, - image: &peniko::ImageBrush, -) -> Option> -where - T: Borrow, -{ - let image_data = image.image.borrow(); - let image_id = image_data.data.id(); - if let Some((entry_color, pixmap)) = caches.image_cache.get_mut(&image_id) { + image: &ImageData, +) -> Option> { + let image_key = ImageCacheKey::Raster(image.data.id()); + if let Some((entry_color, pixmap)) = caches.image_cache.get_mut(&image_key) { *entry_color = cache_color; return Some(pixmap.clone()); } - let pixmap = Arc::new(realize_image_pixmap(image_data)?); + let pixmap = Arc::new(realize_image_pixmap(image)?); caches .image_cache - .insert(image_id, (cache_color, pixmap.clone())); + .insert(image_key, (cache_color, pixmap.clone())); Some(pixmap) } @@ -3871,19 +4049,16 @@ fn resize_pixmap( Some(scaled) } -fn cache_scaled_image_pixmap( +fn cache_scaled_image_pixmap( caches: &mut RendererCaches, cache_color: CacheColor, - image: &peniko::ImageBrush, + image: &ResolvedImageBrush, + source: &Pixmap, width: u32, height: u32, -) -> Option> -where - T: Borrow, -{ - let image_id = image.image.borrow().data.id(); +) -> Option> { let key = ScaledImageCacheKey { - image_id, + image_key: image.key, width, height, quality: std::mem::discriminant(&image.sampler.quality), @@ -3893,27 +4068,18 @@ where return Some(pixmap.clone()); } - let source = cache_image_pixmap(caches, cache_color, image)?; - let scaled = Arc::new(resize_pixmap( - &source, - width, - height, - image.sampler.quality, - )?); + let scaled = Arc::new(resize_pixmap(source, width, height, image.sampler.quality)?); caches .scaled_image_cache .insert(key, (cache_color, scaled.clone())); Some(scaled) } -fn cached_image_rect_size( - image: &peniko::ImageBrush, +fn cached_image_rect_size( + image: &ResolvedImageBrush, rect: Rect, brush_transform: Option, -) -> Option<(u32, u32)> -where - T: Borrow, -{ +) -> Option<(u32, u32)> { (brush_transform.is_none() && image.sampler.x_extend == Extend::Pad && image.sampler.y_extend == Extend::Pad) @@ -3926,24 +4092,24 @@ where )) } -fn render_cached_image_rect( +fn render_cached_image_rect( caches: &mut RendererCaches, cache_color: CacheColor, layer: &mut Layer<'_>, - image: &peniko::ImageBrush, + image: &ResolvedImageBrush, + image_pixmap: &Pixmap, rect: Rect, transform: Affine, brush_transform: Option, opacity: f32, blend_mode: TinyBlendMode, -) -> bool -where - T: Borrow, -{ +) -> bool { let Some((width, height)) = cached_image_rect_size(image, rect, brush_transform) else { return false; }; - let Some(pixmap) = cache_scaled_image_pixmap(caches, cache_color, image, width, height) else { + let Some(pixmap) = + cache_scaled_image_pixmap(caches, cache_color, image, image_pixmap, width, height) + else { return false; }; layer.draw_pixmap_rect( @@ -4125,18 +4291,11 @@ fn brush_to_paint<'b>( }) } -fn image_brush_pixmap(image: &peniko::ImageBrush) -> Option -where - T: Borrow, -{ - realize_image_pixmap(image.image.borrow()) -} - -fn image_brush_has_mixed_extend(image: &peniko::ImageBrush) -> bool { +fn image_brush_has_mixed_extend(image: &ResolvedImageBrush) -> bool { image.sampler.x_extend != image.sampler.y_extend } -fn image_brush_spread_mode(image: &peniko::ImageBrush) -> SpreadMode { +fn image_brush_spread_mode(image: &ResolvedImageBrush) -> SpreadMode { debug_assert!( !image_brush_has_mixed_extend(image), "mixed-axis image brushes must use the custom fallback path" @@ -4254,14 +4413,11 @@ fn pixmap_pixel_or_transparent( } } -fn sample_image_brush_at( +fn sample_image_brush_at( pixmap: &Pixmap, - image: &peniko::ImageBrush, + image: &ResolvedImageBrush, point: Point, -) -> PremultipliedColorU8 -where - T: Borrow, -{ +) -> PremultipliedColorU8 { let quality = image_quality_to_filter_quality(image.sampler.quality); let opacity = opacity_to_u8(image.sampler.alpha); let width = pixmap.width(); @@ -5271,9 +5427,11 @@ fn skia_transform(affine: Affine) -> Transform { #[cfg(test)] mod tests { use super::*; - use imaging::{GroupRef, MaskMode, Painter, record::Scene}; + use imaging::{ + Brush, GroupRef, ImageBrush, ImageBrushRef, MaskMode, Painter, SceneImage, record::Scene, + }; use peniko::color::{ColorSpaceTag, HueDirection, palette::css}; - use peniko::{Blob, ImageAlphaType, ImageData, ImageFormat}; + use peniko::{Blob, Extend, ImageAlphaType, ImageData, ImageFormat}; /// Creates a `Layer` directly without a window, for offscreen rendering. fn make_layer(width: u32, height: u32) -> Layer<'static> { @@ -5438,23 +5596,115 @@ mod tests { width: 2, height: 2, }; - let brush = peniko::ImageBrush::new(image) + let brush = peniko::ImageBrush::new(image.clone()) .with_extend(Extend::Pad) .with_quality(ImageQuality::Medium); let mut caches = RendererCaches::new(); - let first = cache_scaled_image_pixmap(&mut caches, CacheColor(false), &brush, 8, 8) - .expect("scale image"); + let source = cache_image_pixmap(&mut caches, CacheColor(false), &image).expect("source"); + let resolved = ResolvedImageBrush { + key: ImageCacheKey::Raster(image.data.id()), + sampler: brush.sampler, + }; + let first = + cache_scaled_image_pixmap(&mut caches, CacheColor(false), &resolved, &source, 8, 8) + .expect("scale image"); assert_eq!(caches.image_cache.len(), 1); assert_eq!(caches.scaled_image_cache.len(), 1); - let second = cache_scaled_image_pixmap(&mut caches, CacheColor(true), &brush, 8, 8) - .expect("reuse scaled image"); + let second = + cache_scaled_image_pixmap(&mut caches, CacheColor(true), &resolved, &source, 8, 8) + .expect("reuse scaled image"); assert!(Arc::ptr_eq(&first, &second)); assert_eq!(caches.image_cache.len(), 1); assert_eq!(caches.scaled_image_cache.len(), 1); } + #[test] + fn scene_image_brush_reflects_in_tiny_skia() { + let mut source = Scene::new(); + { + let mut painter = Painter::new(&mut source); + painter.fill_rect( + Rect::new(0.0, 0.0, 1.0, 1.0), + Color::from_rgb8(0xff, 0x00, 0x00), + ); + painter.fill_rect( + Rect::new(1.0, 0.0, 2.0, 1.0), + Color::from_rgb8(0x00, 0xff, 0x00), + ); + } + + let scene_image = SceneImage::new(source, 2, 1); + let brush = Brush::Image( + ImageBrush::from(scene_image) + .with_extend(Extend::Reflect) + .with_quality(ImageQuality::Low), + ); + + let mut scene = Scene::new(); + { + let mut painter = Painter::new(&mut scene); + painter.fill(Rect::new(0.0, 0.0, 4.0, 1.0), &brush).draw(); + } + + let mut renderer = TinySkiaRenderer::new(); + let image = renderer + .render_scene(&scene, 4, 1) + .expect("render reflected scene image"); + assert_eq!( + &image.data[..16], + &[ + 0xff, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0xff, 0x00, + 0x00, 0xff, + ] + ); + } + + #[test] + fn scene_image_cache_is_used_for_identical_scene_brushes() { + let mut source = Scene::new(); + { + let mut painter = Painter::new(&mut source); + painter.fill_rect( + Rect::new(0.0, 0.0, 2.0, 2.0), + Color::from_rgb8(0x2a, 0x6f, 0xdb), + ); + } + + let scene_image = SceneImage::new(source, 2, 2); + let scene_image_id = scene_image.id(); + let brush = ImageBrush::from(scene_image); + let mut caches = RendererCaches::new(); + + let (first_brush, first) = + resolve_image_brush(&mut caches, CacheColor(false), ImageBrushRef::from(&brush)) + .expect("realize"); + assert_eq!(first_brush.key, ImageCacheKey::Scene(scene_image_id)); + assert_eq!(caches.scene_pixmap_cache.len(), 1); + + let (_, second) = + resolve_image_brush(&mut caches, CacheColor(true), ImageBrushRef::from(&brush)) + .expect("reuse"); + assert!(Arc::ptr_eq(&first, &second)); + assert_eq!(caches.scene_pixmap_cache.len(), 1); + } + + #[test] + fn scene_image_cache_drops_released_sources() { + let mut caches = RendererCaches::new(); + + let first = SceneImage::new(Scene::new(), 1, 1); + cache_scene_image_pixmap(&mut caches, CacheColor(false), &first).expect("cache first"); + assert_eq!(caches.scene_pixmap_cache.len(), 1); + drop(first); + + let second = SceneImage::new(Scene::new(), 1, 1); + cache_scene_image_pixmap(&mut caches, CacheColor(false), &second).expect("cache second"); + assert_eq!(caches.scene_pixmap_cache.len(), 1); + assert!(caches.scene_pixmap_cache.contains_key(&second.id())); + } + #[test] fn blurred_rrect_cache_is_used_for_translation_only_draws() { let mut caches = RendererCaches::new(); @@ -5513,7 +5763,7 @@ mod tests { width: 2, height: 2, }; - let brush = peniko::ImageBrush::new(image) + let brush = ImageBrush::from(image) .with_quality(ImageQuality::Low) .with_x_extend(Extend::Repeat) .with_y_extend(Extend::Pad); @@ -5672,7 +5922,10 @@ mod tests { plain.fill(&Rect::new(0.0, 0.0, 8.0, 2.0), &gradient); let mut transformed = make_layer(8, 2); + let mut caches = RendererCaches::new(); transformed.fill_with_brush_transform( + &mut caches, + CacheColor(false), &Rect::new(0.0, 0.0, 8.0, 2.0), &gradient, Some(Affine::translate((2.0, 0.0))), diff --git a/imaging_vello/src/lib.rs b/imaging_vello/src/lib.rs index e789785..803c5fd 100644 --- a/imaging_vello/src/lib.rs +++ b/imaging_vello/src/lib.rs @@ -9,6 +9,10 @@ //! Semantic [`imaging::record::Scene`] values can be lowered to native Vello scenes through //! [`VelloRenderer::encode_scene`]. //! +//! Scene-backed [`imaging::SceneImage`] brushes are intentionally unsupported here. Vello does not +//! expose a native "scene as image shader" primitive comparable to Skia's retained picture/image +//! path, and this backend does not silently fall back to offscreen rasterization for them. +//! //! In UI integrations, the host application should usually own the `wgpu` device, queue, and //! presentation targets, then pass those handles into [`VelloRenderer`]. //! diff --git a/imaging_vello/src/scene_sink.rs b/imaging_vello/src/scene_sink.rs index 91c5285..72aeca9 100644 --- a/imaging_vello/src/scene_sink.rs +++ b/imaging_vello/src/scene_sink.rs @@ -4,12 +4,12 @@ use super::Error; use crate::vello::{self, Glyph as VelloGlyph}; use imaging::{ - BlurredRoundedRect, ClipRef, Composite, FillRef, GeometryRef, GlyphRunRef, GroupRef, MaskMode, - PaintSink, StrokeRef, + BlurredRoundedRect, Brush as ImagingBrush, BrushRef, ClipRef, Composite, FillRef, GeometryRef, + GlyphRunRef, GroupRef, ImageRef, MaskMode, PaintSink, StrokeRef, record::{Scene, replay_transformed}, }; use kurbo::{Affine, Rect}; -use peniko::{Brush, BrushRef, Fill}; +use peniko::{Brush as PenikoBrush, Fill}; use std::boxed::Box; /// Borrowed adapter that streams `imaging` commands into an existing [`crate::vello::Scene`]. @@ -78,10 +78,25 @@ impl<'a> VelloSceneSink<'a> { } } - fn brush_to_brush(&mut self, brush: BrushRef<'_>, composite: Composite) -> Option { + fn brush_to_brush(&mut self, brush: BrushRef<'_>, composite: Composite) -> Option { let brush = brush.to_owned().multiply_alpha(composite.alpha); match brush { - Brush::Solid(_) | Brush::Gradient(_) | Brush::Image(_) => Some(brush), + ImagingBrush::Image(image) if !matches!(image.image.as_ref(), ImageRef::Raster(_)) => { + self.set_error_once(Error::UnsupportedImageBrush); + None + } + ImagingBrush::Solid(color) => Some(PenikoBrush::Solid(color)), + ImagingBrush::Gradient(gradient) => Some(PenikoBrush::Gradient(gradient)), + ImagingBrush::Image(image) => { + let ImageRef::Raster(raster) = image.image.as_ref() else { + self.set_error_once(Error::UnsupportedImageBrush); + return None; + }; + Some(PenikoBrush::Image(peniko::ImageBrush { + image: raster.clone(), + sampler: image.sampler, + })) + } } } @@ -348,9 +363,9 @@ impl PaintSink for VelloSceneSink<'_> { }; let (blend, paint) = match (&paint, draw.composite.blend.compose) { - (Brush::Solid(c), peniko::Compose::Copy) if c.components[3] == 0.0 => ( + (PenikoBrush::Solid(c), peniko::Compose::Copy) if c.components[3] == 0.0 => ( peniko::BlendMode::new(peniko::Mix::Normal, peniko::Compose::DestOut), - Brush::Solid(peniko::Color::from_rgba8(0, 0, 0, 255)), + PenikoBrush::Solid(peniko::Color::from_rgba8(0, 0, 0, 255)), ), _ => (draw.composite.blend, paint), }; @@ -434,9 +449,9 @@ impl PaintSink for VelloSceneSink<'_> { }; let (blend, paint) = match (&paint, draw.composite.blend.compose) { - (Brush::Solid(c), peniko::Compose::Copy) if c.components[3] == 0.0 => ( + (PenikoBrush::Solid(c), peniko::Compose::Copy) if c.components[3] == 0.0 => ( peniko::BlendMode::new(peniko::Mix::Normal, peniko::Compose::DestOut), - Brush::Solid(peniko::Color::from_rgba8(0, 0, 0, 255)), + PenikoBrush::Solid(peniko::Color::from_rgba8(0, 0, 0, 255)), ), _ => (draw.composite.blend, paint), }; diff --git a/imaging_vello_cpu/src/lib.rs b/imaging_vello_cpu/src/lib.rs index 7effaeb..436ceaa 100644 --- a/imaging_vello_cpu/src/lib.rs +++ b/imaging_vello_cpu/src/lib.rs @@ -6,6 +6,10 @@ //! This crate provides a CPU renderer that consumes `imaging::record::Scene` (or accepts commands //! directly via `imaging::PaintSink`) and produces an RGBA8 image buffer using `vello_cpu`. //! +//! `imaging_vello_cpu` supports scene-backed [`imaging::SceneImage`] brushes by rendering the +//! retained subscene to a cached raster image and then sampling that image through the existing +//! image-brush path. +//! //! # Render A Recorded Scene //! //! Record commands into [`imaging::record::Scene`], then render them with [`VelloCpuRenderer`]. @@ -68,8 +72,9 @@ use alloc::sync::Arc; use alloc::vec; use alloc::vec::Vec; use imaging::{ - BlurredRoundedRect, ClipRef, Composite, FillRef, Filter, GeometryRef, GlyphRunRef, GroupRef, - MaskMode, PaintSink, RgbaImage, StrokeRef, + BlurredRoundedRect, Brush as ImagingBrush, BrushRef, ClipRef, Composite, FillRef, Filter, + GeometryRef, GlyphRunRef, GroupRef, ImageRef, MaskMode, PaintSink, RgbaImage, SceneImage, + SceneImageWeak, StrokeRef, record::{Scene, ValidateError, replay, replay_transformed}, render::{ ImageBufferFormat, ImageBufferTarget, ImageRenderer, ImageRendererError, ImageTargetError, @@ -77,7 +82,9 @@ use imaging::{ }, }; use kurbo::{Affine, Rect, Shape as _}; -use peniko::{BlendMode, Brush, BrushRef, Fill, Style}; +use peniko::{ + BlendMode, Blob, Brush as PenikoBrush, Fill, ImageAlphaType, ImageData, ImageFormat, Style, +}; use vello_common::filter_effects::{EdgeMode, Filter as VelloFilter, FilterGraph, FilterPrimitive}; use vello_common::glyph::Glyph as VelloGlyph; use vello_common::paint::{Image as VelloImage, ImageSource}; @@ -116,6 +123,7 @@ pub struct VelloCpuRenderer { clip_depth: u32, group_depth: u32, mask_cache: VecDeque, + scene_image_cache: VecDeque, } #[derive(Clone, Debug)] @@ -126,6 +134,16 @@ struct CachedMask { mask: vello_cpu::Mask, } +#[derive(Clone, Debug)] +struct CachedSceneImage { + scene_image: SceneImageWeak, + scene_image_id: u64, + width: u32, + height: u32, + tolerance: f64, + image: ImageData, +} + impl VelloCpuRenderer { fn checked_size(width: u32, height: u32) -> Result<(u16, u16), Error> { let width = u16::try_from(width).map_err(|_| Error::Internal("render width too large"))?; @@ -156,6 +174,7 @@ impl VelloCpuRenderer { clip_depth: 0, group_depth: 0, mask_cache: VecDeque::new(), + scene_image_cache: VecDeque::new(), } } @@ -164,6 +183,7 @@ impl VelloCpuRenderer { if self.tolerance != tolerance { self.tolerance = tolerance; self.clear_cached_masks(); + self.clear_cached_scene_images(); } } @@ -184,6 +204,16 @@ impl VelloCpuRenderer { self.mask_cache.clear(); } + /// Drop any realized scene-image artifacts cached by the renderer. + pub fn clear_cached_scene_images(&mut self) { + self.scene_image_cache.clear(); + } + + fn prune_cached_scene_images(&mut self) { + self.scene_image_cache + .retain(|entry| entry.scene_image.upgrade().is_some()); + } + fn resize(&mut self, width: u16, height: u16) { if self.width == width && self.height == height { return; @@ -193,6 +223,7 @@ impl VelloCpuRenderer { self.width = width; self.height = height; self.clear_cached_masks(); + self.clear_cached_scene_images(); self.error = None; self.clip_depth = 0; self.group_depth = 0; @@ -317,12 +348,22 @@ impl VelloCpuRenderer { ) -> Option { let brush = brush.to_owned().multiply_alpha(composite.alpha); let paint: vello_cpu::PaintType = match brush { - Brush::Solid(c) => Brush::Solid(c), - Brush::Gradient(g) => Brush::Gradient(g), - Brush::Image(image) => Brush::Image(VelloImage { - image: ImageSource::from_peniko_image_data(&image.image), - sampler: image.sampler, - }), + ImagingBrush::Solid(c) => PenikoBrush::Solid(c), + ImagingBrush::Gradient(g) => PenikoBrush::Gradient(g), + ImagingBrush::Image(image) => { + let sampler = image.sampler; + let image_source = match image.image.as_ref() { + ImageRef::Raster(image) => ImageSource::from_peniko_image_data(image), + ImageRef::Scene(scene) => { + let image = self.realize_scene_image(scene)?; + ImageSource::from_peniko_image_data(&image) + } + }; + PenikoBrush::Image(VelloImage { + image: image_source, + sampler, + }) + } }; Some(paint) } @@ -535,6 +576,51 @@ impl VelloCpuRenderer { mask, }); } + + fn realize_scene_image(&mut self, scene_image: &SceneImage) -> Option { + self.prune_cached_scene_images(); + if let Some(entry) = self.scene_image_cache.iter().find(|entry| { + entry.tolerance == self.tolerance + && entry.scene_image_id == scene_image.id() + && entry.width == scene_image.width() + && entry.height == scene_image.height() + }) { + return Some(entry.image.clone()); + } + + let (width, height) = match Self::checked_size(scene_image.width(), scene_image.height()) { + Ok(size) => size, + Err(err) => { + self.set_error_once(err); + return None; + } + }; + let mut renderer = Self::new(width, height); + renderer.set_tolerance(self.tolerance); + let image = match renderer.render_scene(scene_image.scene(), width, height) { + Ok(image) => image, + Err(err) => { + self.set_error_once(err); + return None; + } + }; + let image = ImageData { + data: Blob::new(Arc::new(image.data)), + format: ImageFormat::Rgba8, + alpha_type: ImageAlphaType::Alpha, + width: scene_image.width(), + height: scene_image.height(), + }; + self.scene_image_cache.push_back(CachedSceneImage { + scene_image: scene_image.downgrade(), + scene_image_id: scene_image.id(), + width: scene_image.width(), + height: scene_image.height(), + tolerance: self.tolerance, + image: image.clone(), + }); + Some(image) + } } impl ImageRenderer for VelloCpuRenderer { @@ -774,11 +860,11 @@ impl PaintSink for VelloCpuRenderer { mod tests { use super::*; use imaging::{ - Painter, + Brush, ImageBrush, Painter, SceneImage, render::{ImageBufferTarget, ImageTargetError}, }; use kurbo::Rect; - use peniko::Color; + use peniko::{Color, Extend}; fn masked_scene(mode: MaskMode) -> Scene { let mut mask = Scene::new(); @@ -911,6 +997,85 @@ mod tests { assert_eq!(image.height, 48); } + #[test] + fn scene_image_brush_reflects_in_vello_cpu() { + let mut source = Scene::new(); + { + let mut painter = Painter::new(&mut source); + painter.fill_rect( + Rect::new(0.0, 0.0, 1.0, 1.0), + Color::from_rgb8(0xff, 0x00, 0x00), + ); + painter.fill_rect( + Rect::new(1.0, 0.0, 2.0, 1.0), + Color::from_rgb8(0x00, 0xff, 0x00), + ); + } + + let scene_image = SceneImage::new(source, 2, 1); + let brush = Brush::Image(ImageBrush::from(scene_image).with_extend(Extend::Reflect)); + + let mut scene = Scene::new(); + { + let mut painter = Painter::new(&mut scene); + painter.fill(Rect::new(0.0, 0.0, 4.0, 1.0), &brush).draw(); + } + + let mut renderer = VelloCpuRenderer::new(4, 1); + let image = renderer.render_scene(&scene, 4, 1).unwrap(); + assert_eq!( + &image.data[..16], + &[ + 0xff, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0xff, 0x00, + 0x00, 0xff, + ] + ); + } + + #[test] + fn render_scene_reuses_cached_scene_images_for_identical_scenes() { + let mut source = Scene::new(); + { + let mut painter = Painter::new(&mut source); + painter.fill_rect( + Rect::new(0.0, 0.0, 2.0, 2.0), + Color::from_rgb8(0x2a, 0x6f, 0xdb), + ); + } + let brush = Brush::Image(ImageBrush::from(SceneImage::new(source, 2, 2))); + let mut scene = Scene::new(); + { + let mut painter = Painter::new(&mut scene); + painter.fill(Rect::new(0.0, 0.0, 4.0, 4.0), &brush).draw(); + } + + let mut renderer = VelloCpuRenderer::new(4, 4); + renderer.render_scene(&scene, 4, 4).unwrap(); + assert_eq!(renderer.scene_image_cache.len(), 1); + + renderer.render_scene(&scene, 4, 4).unwrap(); + assert_eq!(renderer.scene_image_cache.len(), 1); + } + + #[test] + fn cached_scene_images_are_dropped_after_source_is_released() { + let mut renderer = VelloCpuRenderer::new(1, 1); + + let first = SceneImage::new(Scene::new(), 1, 1); + renderer + .realize_scene_image(&first) + .expect("realize first scene image"); + assert_eq!(renderer.scene_image_cache.len(), 1); + drop(first); + + let second = SceneImage::new(Scene::new(), 1, 1); + renderer + .realize_scene_image(&second) + .expect("realize second scene image"); + assert_eq!(renderer.scene_image_cache.len(), 1); + assert_eq!(renderer.scene_image_cache[0].scene_image_id, second.id()); + } + #[test] fn render_source_into_rejects_short_row_stride_as_target_error() { let mut renderer = VelloCpuRenderer::new(4, 4); diff --git a/imaging_vello_hybrid/README.md b/imaging_vello_hybrid/README.md index c9769a8..e37919d 100644 --- a/imaging_vello_hybrid/README.md +++ b/imaging_vello_hybrid/README.md @@ -10,9 +10,10 @@ This backend supports both headless image rendering and host-owned `wgpu` textur themselves; test code in this repository uses local helper functions rather than a public bootstrap API. - Recorded `imaging::record::Scene` values can use inline image brushes; the renderer uploads and - caches them behind the scenes. Direct native-scene recording can use image brushes too via - `VelloHybridSceneSink::with_renderer`; the plain `VelloHybridSceneSink::new` constructor stays - limited to non-image brushes. + caches them behind the scenes. This includes `imaging::SceneImage`, which is rasterized once + per retained-image identity and then reused through the same hybrid cache. Direct native-scene + recording can use image brushes too via `VelloHybridSceneSink::with_renderer`; the plain + `VelloHybridSceneSink::new` constructor stays limited to non-image brushes. - Use `VelloHybridSceneSink::new` for solid/gradient-only native scene recording. - Use `VelloHybridSceneSink::with_renderer` for native scene recording that needs image brushes. - Group-level filters are currently not supported by `vello_hybrid`; `imaging_vello_hybrid` diff --git a/imaging_vello_hybrid/src/image_registry.rs b/imaging_vello_hybrid/src/image_registry.rs index e29681e..2015328 100644 --- a/imaging_vello_hybrid/src/image_registry.rs +++ b/imaging_vello_hybrid/src/image_registry.rs @@ -2,14 +2,17 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT use crate::Error; -use peniko::{ImageBrush, ImageData}; +use imaging::{ImageBrush, ImageRef, SceneImage, SceneImageWeak}; +use peniko::{Blob, ImageAlphaType, ImageData, ImageFormat}; use std::collections::VecDeque; use std::hash::{DefaultHasher, Hash, Hasher}; +use std::sync::Arc; use vello_common::paint::{Image as VelloImage, ImageId, ImageSource}; #[derive(Debug)] pub(crate) struct HybridImageRegistry { live: VecDeque, + scene_images: VecDeque, bytes_used: usize, max_bytes: usize, } @@ -24,6 +27,7 @@ impl HybridImageRegistry { pub(crate) fn new(max_bytes: usize) -> Self { Self { live: VecDeque::new(), + scene_images: VecDeque::new(), bytes_used: 0, max_bytes, } @@ -34,6 +38,7 @@ impl HybridImageRegistry { renderer: &'a mut vello_hybrid::Renderer, device: &'a wgpu::Device, queue: &'a wgpu::Queue, + tolerance: f64, mut encoder: wgpu::CommandEncoder, ) -> HybridImageUploadSession<'a> { // We evict excess images at the start of the session, @@ -49,6 +54,7 @@ impl HybridImageRegistry { renderer, device, queue, + tolerance, encoder: Some(encoder), pending: Vec::new(), } @@ -101,20 +107,79 @@ pub(crate) struct HybridImageUploadSession<'a> { renderer: &'a mut vello_hybrid::Renderer, device: &'a wgpu::Device, queue: &'a wgpu::Queue, + tolerance: f64, encoder: Option, pending: Vec, } impl HybridImageUploadSession<'_> { + pub(crate) fn realize_scene_image( + &mut self, + scene_image: &SceneImage, + ) -> Result { + self.registry + .scene_images + .retain(|entry| entry.scene_image.upgrade().is_some()); + + if let Some(entry) = self.registry.scene_images.iter().find(|entry| { + entry.scene_image_id == scene_image.id() + && entry.width == scene_image.width() + && entry.height == scene_image.height() + && entry.tolerance == self.tolerance + }) { + return Ok(entry.image.clone()); + } + + let (width, height) = crate::VelloHybridRendererState::checked_size( + scene_image.width(), + scene_image.height(), + )?; + let mut renderer = crate::VelloHybridRenderer::new(self.device.clone(), self.queue.clone()); + renderer.set_tolerance(self.tolerance); + let native = renderer.encode_scene(scene_image.scene(), width, height)?; + let image = renderer.render(&native, width, height)?; + let image = ImageData { + data: Blob::new(Arc::new(image.data)), + format: ImageFormat::Rgba8, + alpha_type: ImageAlphaType::Alpha, + width: scene_image.width(), + height: scene_image.height(), + }; + self.registry.scene_images.push_back(CachedSceneImage { + scene_image: scene_image.downgrade(), + scene_image_id: scene_image.id(), + width: scene_image.width(), + height: scene_image.height(), + tolerance: self.tolerance, + image: image.clone(), + }); + Ok(image) + } pub(crate) fn resolve_image_brush(&mut self, brush: &ImageBrush) -> Result { - let key = ImageKey::derive(&brush.image); + let key = match brush.image.as_ref() { + ImageRef::Raster(image) => ImageKey::Raster(ImageDataKey::derive(image)), + ImageRef::Scene(scene_image) => ImageKey::Scene(SceneImageKey { + scene_image_id: scene_image.id(), + width: scene_image.width(), + height: scene_image.height(), + tolerance: self.tolerance.to_bits(), + }), + }; let image = if let Some(image) = self.pending.iter().find(|ri| ri.key == key).copied() { image } else if let Some(index) = self.registry.live.iter().position(|ri| ri.key == key) { let index = self.registry.touch(index); self.registry.live.get(index).copied().unwrap() } else { - let image_source = ImageSource::from_peniko_image_data(&brush.image); + let realized_image; + let image = match brush.image.as_ref() { + ImageRef::Raster(image) => image, + ImageRef::Scene(scene_image) => { + realized_image = self.realize_scene_image(scene_image)?; + &realized_image + } + }; + let image_source = ImageSource::from_peniko_image_data(image); let ImageSource::Pixmap(pixmap) = image_source else { return Err(Error::Internal( "peniko image conversion did not produce a pixmap", @@ -132,11 +197,10 @@ impl HybridImageUploadSession<'_> { key, id, may_have_opacities: pixmap.may_have_opacities(), - bytes: brush - .image + bytes: image .format - .size_in_bytes(brush.image.width, brush.image.height) - .unwrap_or_else(|| brush.image.data.data().len()), + .size_in_bytes(image.width, image.height) + .unwrap_or_else(|| image.data.data().len()), }; self.pending.push(image); image @@ -182,15 +246,21 @@ struct RegisteredImage { } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -struct ImageKey { - format: core::mem::Discriminant, - alpha_type: core::mem::Discriminant, +enum ImageKey { + Raster(ImageDataKey), + Scene(SceneImageKey), +} + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +struct ImageDataKey { + format: core::mem::Discriminant, + alpha_type: core::mem::Discriminant, width: u32, height: u32, data_hash: u64, } -impl ImageKey { +impl ImageDataKey { fn derive(image: &ImageData) -> Self { let mut hasher = DefaultHasher::new(); image.data.data().hash(&mut hasher); @@ -204,11 +274,29 @@ impl ImageKey { } } +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +struct SceneImageKey { + scene_image_id: u64, + width: u32, + height: u32, + tolerance: u64, +} + +#[derive(Clone, Debug)] +struct CachedSceneImage { + scene_image: SceneImageWeak, + scene_image_id: u64, + width: u32, + height: u32, + tolerance: f64, + image: ImageData, +} + #[cfg(test)] mod tests { use crate::image_registry::HybridImageRegistry; - use super::{ImageKey, RegisteredImage}; + use super::{ImageDataKey, ImageKey, RegisteredImage, SceneImageKey}; use peniko::{Blob, ImageAlphaType, ImageData, ImageFormat}; use std::collections::VecDeque; use std::sync::Arc; @@ -228,7 +316,7 @@ mod tests { fn image_key_dedupes_equivalent_image_contents() { let a = image([1, 2, 3, 4, 9, 8, 7, 6, 5, 4, 3, 2, 10, 11, 12, 13]); let b = image([1, 2, 3, 4, 9, 8, 7, 6, 5, 4, 3, 2, 10, 11, 12, 13]); - assert_eq!(ImageKey::derive(&a), ImageKey::derive(&b)); + assert_eq!(ImageDataKey::derive(&a), ImageDataKey::derive(&b)); } #[test] @@ -236,10 +324,25 @@ mod tests { let mut a = image([1, 2, 3, 4, 9, 8, 7, 6, 5, 4, 3, 2, 10, 11, 12, 13]); let mut b = a.clone(); b.alpha_type = ImageAlphaType::AlphaPremultiplied; - assert_ne!(ImageKey::derive(&a), ImageKey::derive(&b)); + assert_ne!(ImageDataKey::derive(&a), ImageDataKey::derive(&b)); a.format = ImageFormat::Bgra8; - assert_ne!(ImageKey::derive(&a), ImageKey::derive(&b)); + assert_ne!(ImageDataKey::derive(&a), ImageDataKey::derive(&b)); + } + + #[test] + fn scene_image_key_distinguishes_scene_identity() { + let a = SceneImageKey { + scene_image_id: 1, + width: 2, + height: 3, + tolerance: 0.1_f64.to_bits(), + }; + let b = SceneImageKey { + scene_image_id: 2, + ..a + }; + assert_ne!(a, b); } #[test] @@ -249,8 +352,8 @@ mod tests { let bytes_used = a.data.len() + b.data.len(); - let a_key = ImageKey::derive(&a); - let b_key = ImageKey::derive(&b); + let a_key = ImageKey::Raster(ImageDataKey::derive(&a)); + let b_key = ImageKey::Raster(ImageDataKey::derive(&b)); let a_ri = RegisteredImage { key: a_key, @@ -271,6 +374,7 @@ mod tests { let mut registry = HybridImageRegistry { live, + scene_images: VecDeque::new(), max_bytes: 1000 * 1000 * 1000, bytes_used, }; diff --git a/imaging_vello_hybrid/src/lib.rs b/imaging_vello_hybrid/src/lib.rs index bb5b90d..625fbaf 100644 --- a/imaging_vello_hybrid/src/lib.rs +++ b/imaging_vello_hybrid/src/lib.rs @@ -13,6 +13,9 @@ //! In UI integrations, the host application should usually own the `wgpu` device, queue, and //! presentation targets, then pass those handles into [`VelloHybridRenderer`]. //! +//! Scene-backed [`imaging::SceneImage`] brushes are supported by rasterizing the retained scene to +//! a cached image, then uploading that image into the hybrid atlas. +//! //! Recorded scenes with inline image brushes are uploaded through a renderer-scoped image registry //! and translated to backend-managed opaque image ids. Use [`VelloHybridSceneSink::with_renderer`] //! when recording directly into a native [`vello_hybrid::Scene`] and you want the same image @@ -102,7 +105,7 @@ //! width: 2, //! height: 2, //! }; -//! let brush = Brush::Image(ImageBrush::new(image)); +//! let brush = Brush::Image(ImageBrush::from(image)); //! //! # let device: imaging_vello_hybrid::wgpu::Device = todo!(); //! # let queue: imaging_vello_hybrid::wgpu::Queue = todo!(); @@ -264,6 +267,7 @@ impl VelloHybridRendererState { &mut self.renderer, &self.device, &self.queue, + self.tolerance, encoder, ) } @@ -640,7 +644,10 @@ fn map_readback_image_error(error: ReadbackError) -> ImageRendererError { #[cfg(test)] mod tests { use super::*; - use imaging::{Painter, record::Scene, render::ImageTargetError}; + use imaging::{ + Brush as ImagingBrush, ImageBrush as ImagingImageBrush, Painter, SceneImage, ScenePicture, + record::Scene, render::ImageTargetError, + }; use kurbo::Rect; use peniko::{Blob, Brush, Color, ImageAlphaType, ImageBrush, ImageData, ImageFormat}; use pollster::block_on; @@ -905,7 +912,7 @@ mod tests { width: 2, height: 2, }; - let brush = Brush::Image(ImageBrush::new(image)); + let brush = Brush::Image(ImageBrush::from(image)); let mut scene = Scene::new(); { @@ -921,4 +928,49 @@ mod tests { assert_eq!(image.width, 20); assert_eq!(image.height, 20); } + + #[test] + fn scene_image_brush_renders() { + let Ok((device, queue)) = try_init_device_and_queue() else { + return; + }; + let mut renderer = VelloHybridRenderer::new(device, queue); + + let source = solid_scene(Color::from_rgb8(0x12, 0x34, 0x56), 2.0, 2.0); + let brush = ImagingBrush::Image(ImagingImageBrush::from(SceneImage::new(source, 2, 2))); + + let mut scene = Scene::new(); + { + let mut painter = Painter::new(&mut scene); + painter.fill(Rect::new(0.0, 0.0, 20.0, 20.0), &brush).draw(); + } + + let native = renderer.encode_scene(&scene, 20, 20).unwrap(); + let image = renderer.render(&native, 20, 20).unwrap(); + assert_eq!(image.width, 20); + assert_eq!(image.height, 20); + } + + #[test] + fn scene_picture_draw_renders() { + let Ok((device, queue)) = try_init_device_and_queue() else { + return; + }; + let mut renderer = VelloHybridRenderer::new(device, queue); + + let picture = ScenePicture::new( + solid_scene(Color::from_rgb8(0xaa, 0x44, 0x22), 8.0, 8.0), + Rect::new(0.0, 0.0, 8.0, 8.0), + ); + let mut scene = Scene::new(); + { + let mut painter = Painter::new(&mut scene); + painter.draw_scene_picture(&picture, kurbo::Affine::IDENTITY); + } + + let native = renderer.encode_scene(&scene, 8, 8).unwrap(); + let image = renderer.render(&native, 8, 8).unwrap(); + assert_eq!(image.width, 8); + assert_eq!(image.height, 8); + } } diff --git a/imaging_vello_hybrid/src/scene_sink.rs b/imaging_vello_hybrid/src/scene_sink.rs index 04998d7..27d837d 100644 --- a/imaging_vello_hybrid/src/scene_sink.rs +++ b/imaging_vello_hybrid/src/scene_sink.rs @@ -4,11 +4,11 @@ use super::Error; use crate::{VelloHybridRenderer, image_registry::HybridImageUploadSession}; use imaging::{ - BlurredRoundedRect, ClipRef, Composite, FillRef, GeometryRef, GlyphRunRef, GroupRef, PaintSink, - StrokeRef, + BlurredRoundedRect, Brush as ImagingBrush, BrushRef, ClipRef, Composite, FillRef, GeometryRef, + GlyphRunRef, GroupRef, PaintSink, StrokeRef, }; use kurbo::{Affine, Shape as _}; -use peniko::{Brush, BrushRef, ImageBrush, Style}; +use peniko::{Brush as PenikoBrush, Style}; use vello_common::glyph::Glyph as VelloGlyph; /// Borrowed adapter that streams `imaging` commands into an existing [`vello_hybrid::Scene`]. @@ -103,13 +103,16 @@ impl<'a> VelloHybridSceneSink<'a> { ) -> Option { let brush = brush.to_owned().multiply_alpha(composite.alpha); match brush { - Brush::Solid(c) => Some(Brush::Solid(c)), - Brush::Gradient(g) => Some(Brush::Gradient(g)), - Brush::Image(image) => self.resolve_image_brush(&image).map(Brush::Image), + ImagingBrush::Solid(c) => Some(PenikoBrush::Solid(c)), + ImagingBrush::Gradient(g) => Some(PenikoBrush::Gradient(g)), + ImagingBrush::Image(image) => self.resolve_image_brush(&image).map(PenikoBrush::Image), } } - fn resolve_image_brush(&mut self, image: &ImageBrush) -> Option { + fn resolve_image_brush( + &mut self, + image: &imaging::ImageBrush, + ) -> Option { let Some(image_upload) = self.image_upload.as_mut() else { self.set_error_once(Error::UnsupportedImageBrush); return None; @@ -293,9 +296,9 @@ impl PaintSink for VelloHybridSceneSink<'_> { .set_paint_transform(draw.brush_transform.unwrap_or(Affine::IDENTITY)); let (blend, paint) = match (&paint, draw.composite.blend.compose) { - (Brush::Solid(c), peniko::Compose::Copy) if c.components[3] == 0.0 => ( + (PenikoBrush::Solid(c), peniko::Compose::Copy) if c.components[3] == 0.0 => ( peniko::BlendMode::new(peniko::Mix::Normal, peniko::Compose::Clear), - Brush::Solid(peniko::Color::from_rgba8(0, 0, 0, 255)), + PenikoBrush::Solid(peniko::Color::from_rgba8(0, 0, 0, 255)), ), _ => (draw.composite.blend, paint), }; @@ -328,9 +331,9 @@ impl PaintSink for VelloHybridSceneSink<'_> { .set_paint_transform(draw.brush_transform.unwrap_or(Affine::IDENTITY)); let (blend, paint) = match (&paint, draw.composite.blend.compose) { - (Brush::Solid(c), peniko::Compose::Copy) if c.components[3] == 0.0 => ( + (PenikoBrush::Solid(c), peniko::Compose::Copy) if c.components[3] == 0.0 => ( peniko::BlendMode::new(peniko::Mix::Normal, peniko::Compose::Clear), - Brush::Solid(peniko::Color::from_rgba8(0, 0, 0, 255)), + PenikoBrush::Solid(peniko::Color::from_rgba8(0, 0, 0, 255)), ), _ => (draw.composite.blend, paint), }; @@ -367,12 +370,11 @@ impl PaintSink for VelloHybridSceneSink<'_> { self.draw_blurred_rounded_rect(draw); } } - #[cfg(test)] mod tests { use super::*; use imaging::{Filter, MaskMode, MaskRef, record}; - use peniko::{Blob, ImageAlphaType, ImageData, ImageFormat}; + use peniko::{Blob, Brush, ImageAlphaType, ImageBrush, ImageData, ImageFormat}; use std::sync::Arc; #[test] @@ -401,7 +403,7 @@ mod tests { let mut scene = vello_hybrid::Scene::new(32, 32); scene.reset(); let mut sink = VelloHybridSceneSink::new(&mut scene); - let image = Brush::Image(ImageBrush::new(ImageData { + let image = Brush::Image(ImageBrush::from(ImageData { data: Blob::new(Arc::new([255_u8; 16])), format: ImageFormat::Rgba8, alpha_type: ImageAlphaType::Alpha, diff --git a/svg_imaging/src/document.rs b/svg_imaging/src/document.rs index 22f7143..70bc4df 100644 --- a/svg_imaging/src/document.rs +++ b/svg_imaging/src/document.rs @@ -80,8 +80,7 @@ mod tests { use alloc::{borrow::ToOwned, boxed::Box, vec, vec::Vec}; use std::sync::Arc; - use imaging::{MaskMode, Painter, record}; - use peniko::Brush; + use imaging::{Brush, MaskMode, Painter, record}; use super::{ParseOptions, RenderOptions, SvgDocument}; @@ -538,8 +537,8 @@ mod tests { *shape, record::Geometry::Rect(kurbo::Rect::new(0.0, 0.0, 1.0, 1.0)) ); - assert_eq!(image.image.width, 1); - assert_eq!(image.image.height, 1); + assert_eq!(image.image.width(), 1); + assert_eq!(image.image.height(), 1); } other => panic!("expected image fill draw, got {other:?}"), }