Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 65 additions & 17 deletions imaging_vello_hybrid/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ use imaging::RgbaImage;
use imaging::record::{Scene, ValidateError, replay};
use imaging::render::{
GpuReadbackError, ImageBufferFormat, ImageBufferTarget, ImageRenderer, ImageRendererError,
ImageTargetError, RenderContentError, RenderSource, RenderUnsupportedError,
ImageTargetError, RenderContentError, RenderSource,
};
pub use imaging_wgpu::wgpu;
use imaging_wgpu::{TextureRenderer, TextureRendererError, TextureTargetError, TextureViewTarget};
Expand All @@ -187,12 +187,8 @@ pub enum Error {
InvalidScene(ValidateError),
/// An image brush was encountered on a sink path that has no renderer-backed image resolver.
UnsupportedImageBrush,
/// A filter configuration could not be translated.
UnsupportedFilter,
/// Masks are not supported by this backend yet.
UnsupportedMask,
/// Blurred rounded rect draws are not supported by this backend yet.
UnsupportedBlurredRoundedRect,
/// Vello hybrid returned a render error.
Render(RenderError),
/// An internal invariant was violated.
Expand Down Expand Up @@ -570,16 +566,6 @@ fn map_texture_renderer_error(error: Error) -> TextureRendererError {
Error::InvalidScene(error) => {
TextureRendererError::Content(RenderContentError::InvalidScene(error))
}
Error::UnsupportedImageBrush => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why remove this? The error type still exists...

TextureRendererError::Unsupported(RenderUnsupportedError::ImageBrush)
}
Error::UnsupportedFilter => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should still be used for filters with more than one primitive which aren't supported.

TextureRendererError::Unsupported(RenderUnsupportedError::Filter)
}
Error::UnsupportedMask => TextureRendererError::Unsupported(RenderUnsupportedError::Mask),
Error::UnsupportedBlurredRoundedRect => {
TextureRendererError::Unsupported(RenderUnsupportedError::BlurredRoundedRect)
}
Error::Internal("render width too large" | "render height too large") => {
TextureRendererError::Target(TextureTargetError::DimensionsTooLarge)
}
Expand Down Expand Up @@ -640,8 +626,10 @@ fn map_readback_image_error(error: ReadbackError) -> ImageRendererError {
#[cfg(test)]
mod tests {
use super::*;
use imaging::{Painter, record::Scene, render::ImageTargetError};
use kurbo::Rect;
use imaging::{
BlurredRoundedRect, Composite, Filter, Painter, record::Scene, render::ImageTargetError,
};
use kurbo::{Affine, Rect};
use peniko::{Blob, Brush, Color, ImageAlphaType, ImageBrush, ImageData, ImageFormat};
use pollster::block_on;
use std::sync::Arc;
Expand Down Expand Up @@ -921,4 +909,64 @@ mod tests {
assert_eq!(image.width, 20);
assert_eq!(image.height, 20);
}
#[test]
fn blurred_rounded_rect_blurs_beyond_source_rect() {
let Ok((device, queue)) = try_init_device_and_queue() else {
return;
};
let mut renderer = VelloHybridRenderer::new(device, queue);

let mut scene = Scene::new();
{
let mut painter = Painter::new(&mut scene);
painter.blurred_rounded_rect(BlurredRoundedRect {
transform: Affine::IDENTITY,
rect: Rect::new(8.0, 8.0, 24.0, 24.0),
color: Color::from_rgba8(0x40, 0x80, 0xc0, 0xff),
radius: 6.0,
std_dev: 3.0,
composite: Composite::default(),
});
}

let native = renderer.encode_scene(&scene, 32, 32).unwrap();
let image = renderer.render(&native, 32, 32).unwrap();

let alpha_at = |x: usize, y: usize| -> u8 { image.data[(y * 32 + x) * 4 + 3] };
assert_eq!(alpha_at(2, 2), 0);
assert!(alpha_at(6, 16) > 0);
assert!(alpha_at(16, 16) > alpha_at(6, 16));
}

#[test]
fn filtered_group_blurs_beyond_source_rect() {
let Ok((device, queue)) = try_init_device_and_queue() else {
return;
};
let mut renderer = VelloHybridRenderer::new(device, queue);

let mut scene = Scene::new();
{
let mut painter = Painter::new(&mut scene);
painter.with_group(
imaging::GroupRef::new().with_filters(&[Filter::blur(3.0)]),
|group| {
group
.fill(
Rect::new(8.0, 8.0, 24.0, 24.0),
Color::from_rgb8(0x20, 0x90, 0x60),
)
.draw();
},
);
}

let native = renderer.encode_scene(&scene, 32, 32).unwrap();
let image = renderer.render(&native, 32, 32).unwrap();

let alpha_at = |x: usize, y: usize| -> u8 { image.data[(y * 32 + x) * 4 + 3] };
assert_eq!(alpha_at(2, 2), 0);
assert!(alpha_at(6, 16) > 0);
assert!(alpha_at(16, 16) > alpha_at(6, 16));
}
}
83 changes: 74 additions & 9 deletions imaging_vello_hybrid/src/scene_sink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ use imaging::{
};
use kurbo::{Affine, Shape as _};
use peniko::{Brush, BrushRef, ImageBrush, Style};
use std::sync::Arc;
use vello_common::filter_effects::{EdgeMode, Filter as VelloFilter, FilterGraph, FilterPrimitive};
use vello_common::glyph::Glyph as VelloGlyph;

/// Borrowed adapter that streams `imaging` commands into an existing [`vello_hybrid::Scene`].
Expand Down Expand Up @@ -212,8 +214,73 @@ impl<'a> VelloHybridSceneSink<'a> {
}
}

fn draw_blurred_rounded_rect(&mut self, _draw: BlurredRoundedRect) {
self.set_error_once(Error::UnsupportedBlurredRoundedRect);
fn draw_blurred_rounded_rect(&mut self, draw: BlurredRoundedRect) {
#[expect(
clippy::cast_possible_truncation,
reason = "filter sigma is stored as f32"
)]
let blur_sigma = draw.std_dev as f32;
self.scene.set_transform(draw.transform);
self.scene.set_fill_rule(peniko::Fill::NonZero);
self.scene.push_layer(
None,
Some(draw.composite.blend),
Some(draw.composite.alpha),
None,
self.filters_to_vello(&[imaging::Filter::blur(blur_sigma)]),
);
self.scene.set_blend_mode(Composite::default().blend);
self.scene.set_paint(Brush::Solid(
draw.color.multiply_alpha(Composite::default().alpha),
));
let path = draw
.rect
.to_rounded_rect(draw.radius)
.to_path(self.tolerance);
self.scene.fill_path(&path);
self.scene.pop_layer();
}

fn filters_to_vello(&self, filters: &[imaging::Filter]) -> Option<VelloFilter> {
if filters.is_empty() {
return None;
}

let mut graph = FilterGraph::new();
let mut last = None;
for filter in filters {
let primitive = match *filter {
imaging::Filter::Flood { color } => FilterPrimitive::Flood { color },
imaging::Filter::Blur {
std_deviation_x,
std_deviation_y,
} => FilterPrimitive::GaussianBlur {
std_deviation: std_deviation_x.max(std_deviation_y),
edge_mode: EdgeMode::None,
},
imaging::Filter::DropShadow {
dx,
dy,
std_deviation_x,
std_deviation_y,
color,
} => FilterPrimitive::DropShadow {
dx,
dy,
std_deviation: std_deviation_x.max(std_deviation_y),
color,
edge_mode: EdgeMode::None,
},
imaging::Filter::Offset { dx, dy } => FilterPrimitive::Offset { dx, dy },
};
last = Some(graph.add(primitive, None));
}
if let Some(output) = last {
graph.set_output(output);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You dropped some logic here that was in the vello_cpu adapter code...

Some(VelloFilter {
graph: Arc::new(graph),
})
}
}

Expand Down Expand Up @@ -249,10 +316,6 @@ impl PaintSink for VelloHybridSceneSink<'_> {
self.set_error_once(Error::UnsupportedMask);
return;
}
if !group.filters.is_empty() {
self.set_error_once(Error::UnsupportedFilter);
return;
}
let clip_path = group.clip.map(|clip| {
let (xf, path, fill_rule) = self.clip_to_path(clip);
self.scene.set_transform(xf);
Expand All @@ -262,8 +325,9 @@ impl PaintSink for VelloHybridSceneSink<'_> {

let blend = Some(group.composite.blend);
let opacity = Some(group.composite.alpha);
let filter = self.filters_to_vello(group.filters);
self.scene
.push_layer(clip_path.as_ref(), blend, opacity, None, None);
.push_layer(clip_path.as_ref(), blend, opacity, None, filter);
self.group_depth += 1;
}

Expand Down Expand Up @@ -388,12 +452,13 @@ mod tests {
}

#[test]
fn hybrid_scene_sink_rejects_filters() {
fn hybrid_scene_sink_supports_filters() {
let mut scene = vello_hybrid::Scene::new(32, 32);
scene.reset();
let mut sink = VelloHybridSceneSink::new(&mut scene);
sink.push_group(GroupRef::new().with_filters(&[Filter::blur(2.0)]));
assert!(matches!(sink.finish(), Err(Error::UnsupportedFilter)));
sink.pop_group();
assert!(matches!(sink.finish(), Ok(())));
}

#[test]
Expand Down
Loading