| // Copyright 2026 the Vello Authors | |
| // SPDX-License-Identifier: Apache-2.0 OR MIT | |
| mod tests { | |
| use std::sync::Arc; | |
| use vello_common::color; | |
| use vello_common::color::{AlphaColor, Srgb}; | |
| use vello_common::filter_effects::{EdgeMode, Filter, FilterPrimitive}; | |
| use vello_common::geometry::RectU16; | |
| use vello_common::kurbo::{Affine, Circle, Rect, Shape}; | |
| use vello_common::paint::{Image, ImageSource, Tint, TintMode}; | |
| use vello_common::peniko::{Color, Extend, ImageAlphaType, ImageQuality, ImageSampler}; | |
| use vello_common::pixmap::{PixelMetadata, Pixmap}; | |
| use vello_dev_macros::vello_test; | |
| use vello_gpu::TextureId; | |
| use crate::load_image; | |
| use crate::renderer::Renderer; | |
| const SPRITES: [RectU16; 4] = [ | |
| // Checkmark. | |
| RectU16::new(0, 0, 56, 56), | |
| // Eyes | |
| RectU16::new(56, 0, 112, 42), | |
| // Confetti | |
| RectU16::new(0, 56, 54, 112), | |
| // Cowboy Hat Face | |
| RectU16::new(56, 42, 115, 100), | |
| ]; | |
| fn solid_pixmap(r: u8, g: u8, b: u8, a: u8) -> Arc<Pixmap> { | |
| Arc::new(Pixmap::from_parts( | |
| vec![r, g, b, a], | |
| 1, | |
| 1, | |
| PixelMetadata::new(ImageAlphaType::AlphaPremultiplied, a != 255), | |
| )) | |
| } | |
| fn pad_sampler(quality: ImageQuality) -> ImageSampler { | |
| ImageSampler { | |
| x_extend: Extend::Pad, | |
| y_extend: Extend::Pad, | |
| quality, | |
| alpha: 1.0, | |
| } | |
| } | |
| fn fill_texture_rect( | |
| ctx: &mut impl Renderer, | |
| texture_id: TextureId, | |
| x: f64, | |
| y: f64, | |
| width: f64, | |
| height: f64, | |
| may_have_transparency: bool, | |
| ) { | |
| ctx.set_paint(Image { | |
| image: ImageSource::external_texture( | |
| texture_id, | |
| RectU16::new(0, 0, 1, 1), | |
| may_have_transparency, | |
| ), | |
| sampler: pad_sampler(ImageQuality::Low), | |
| }); | |
| ctx.fill_rect(&Rect::new(x, y, x + width, y + height)); | |
| } | |
| fn fill_sprite_rect( | |
| ctx: &mut impl Renderer, | |
| texture_id: TextureId, | |
| source_region: RectU16, | |
| x: f64, | |
| y: f64, | |
| quality: ImageQuality, | |
| ) { | |
| ctx.set_paint(Image { | |
| image: ImageSource::external_texture(texture_id, source_region, true), | |
| sampler: pad_sampler(quality), | |
| }); | |
| ctx.fill_rect(&Rect::new( | |
| x, | |
| y, | |
| x + f64::from(source_region.width()), | |
| y + f64::from(source_region.height()), | |
| )); | |
| } | |
| fn draw_atlas_rect(ctx: &mut impl Renderer, image: ImageSource, rect: Rect) { | |
| ctx.set_paint_transform(Affine::IDENTITY); | |
| ctx.set_paint(Image { | |
| image, | |
| sampler: ImageSampler { | |
| x_extend: Extend::Pad, | |
| y_extend: Extend::Pad, | |
| quality: ImageQuality::Low, | |
| alpha: 1.0, | |
| }, | |
| }); | |
| ctx.fill_rect(&rect); | |
| } | |
| fn texture_circle(ctx: &mut impl Renderer, texture_id: TextureId, circle: Circle) { | |
| let clip = circle.to_path(0.1); | |
| let rect = circle.bounding_box(); | |
| ctx.push_clip_path(&clip); | |
| ctx.set_paint(Image { | |
| image: ImageSource::external_texture(texture_id, RectU16::new(0, 0, 1, 1), true), | |
| sampler: pad_sampler(ImageQuality::Medium), | |
| }); | |
| ctx.fill_rect(&rect); | |
| ctx.pop_clip(); | |
| } | |
| fn painted_circle(ctx: &mut impl Renderer, color: AlphaColor<Srgb>, circle: Circle) { | |
| ctx.set_paint(color); | |
| ctx.fill_path(&circle.to_path(0.1)); | |
| } | |
| #[vello_test(width = 96, height = 96, gpu_only)] | |
| fn external_texture_composite(ctx: &mut impl Renderer) { | |
| let texture_id = ctx.register_external_texture(load_image!("glyphs_colr_noto")); | |
| ctx.set_paint_transform(Affine::translate((12., 15.))); | |
| fill_sprite_rect(ctx, texture_id, SPRITES[0], 12., 15., ImageQuality::Low); | |
| ctx.set_paint(color::palette::css::PALE_GOLDENROD.with_alpha(0.7)); | |
| ctx.fill_rect(&Rect::new(10., 7., 65., 55.)); | |
| ctx.set_paint_transform(Affine::translate((25., 25.))); | |
| fill_sprite_rect(ctx, texture_id, SPRITES[3], 25., 25., ImageQuality::Low); | |
| } | |
| #[vello_test(gpu_only)] | |
| fn external_texture_circle(ctx: &mut impl Renderer) { | |
| let texture_id = ctx.register_external_texture(load_image!("color_grid_16x16")); | |
| ctx.set_paint_transform(Affine::translate((10., 10.)) * Affine::scale(5.)); | |
| ctx.set_paint(Image { | |
| image: ImageSource::external_texture(texture_id, RectU16::new(0, 0, 16, 16), false), | |
| sampler: pad_sampler(ImageQuality::Medium), | |
| }); | |
| ctx.fill_path(&Circle::new((50., 50.), 40.).to_path(0.1)); | |
| } | |
| #[vello_test(gpu_only)] | |
| fn external_texture_repeat(ctx: &mut impl Renderer) { | |
| let texture_id = ctx.register_external_texture(load_image!("color_grid_16x16")); | |
| ctx.set_paint(Image { | |
| image: ImageSource::external_texture(texture_id, RectU16::new(0, 0, 16, 16), false), | |
| sampler: ImageSampler { | |
| x_extend: Extend::Repeat, | |
| y_extend: Extend::Repeat, | |
| quality: ImageQuality::Medium, | |
| alpha: 1.0, | |
| }, | |
| }); | |
| ctx.fill_rect(&Rect::new(5., 5., 95., 95.)); | |
| } | |
| #[vello_test(gpu_only)] | |
| fn external_texture_with_paint_transform(ctx: &mut impl Renderer) { | |
| let texture_id = ctx.register_external_texture(load_image!("color_grid_16x16")); | |
| ctx.set_paint_transform(Affine::rotate(0.35) * Affine::scale(2.)); | |
| ctx.set_paint(Image { | |
| image: ImageSource::external_texture(texture_id, RectU16::new(2, 2, 14, 14), false), | |
| sampler: ImageSampler { | |
| x_extend: Extend::Reflect, | |
| y_extend: Extend::Reflect, | |
| quality: ImageQuality::Medium, | |
| alpha: 1.0, | |
| }, | |
| }); | |
| ctx.fill_rect(&Rect::new(5., 5., 95., 95.)); | |
| } | |
| #[vello_test(gpu_only)] | |
| fn external_texture_with_scene_transform_2(ctx: &mut impl Renderer) { | |
| let texture_id = ctx.register_external_texture(load_image!("color_grid_16x16")); | |
| ctx.set_transform(Affine::translate((5., 5.)) * Affine::scale(5.625)); | |
| ctx.set_paint(Image { | |
| image: ImageSource::external_texture(texture_id, RectU16::new(0, 0, 16, 16), false), | |
| sampler: ImageSampler { | |
| x_extend: Extend::Repeat, | |
| y_extend: Extend::Repeat, | |
| quality: ImageQuality::Medium, | |
| alpha: 1.0, | |
| }, | |
| }); | |
| ctx.fill_rect(&Rect::new(0., 0., 16., 16.)); | |
| } | |
| #[vello_test(gpu_only)] | |
| fn external_texture_with_cropped_source(ctx: &mut impl Renderer) { | |
| let texture_id = ctx.register_external_texture(load_image!("color_grid_16x16")); | |
| ctx.set_paint_transform(Affine::translate((14.0, 14.0)) * Affine::scale(6.0)); | |
| ctx.set_paint(Image { | |
| image: ImageSource::external_texture(texture_id, RectU16::new(2, 2, 14, 14), false), | |
| sampler: ImageSampler { | |
| x_extend: Extend::Reflect, | |
| y_extend: Extend::Reflect, | |
| quality: ImageQuality::Medium, | |
| alpha: 1.0, | |
| }, | |
| }); | |
| ctx.fill_rect(&Rect::new(5., 5., 95., 95.)); | |
| } | |
| #[vello_test(width = 96, height = 96, gpu_only, gpu_no_depth)] | |
| fn external_texture_opaque_interleaving(ctx: &mut impl Renderer) { | |
| let texture_id = ctx.register_external_texture(solid_pixmap(192, 0, 0, 192)); | |
| ctx.set_paint(AlphaColor::from_rgba8(0, 0, 255, 255)); | |
| ctx.fill_rect(&Rect::new(8., 8., 64., 64.)); | |
| fill_texture_rect(ctx, texture_id, 28., 28., 40., 40., true); | |
| ctx.set_paint(AlphaColor::from_rgba8(0, 255, 0, 255)); | |
| ctx.fill_rect(&Rect::new(48., 16., 88., 56.)); | |
| } | |
| #[vello_test(width = 96, height = 96, gpu_only, gpu_no_depth)] | |
| fn external_texture_runs_with_opaque_prefix(ctx: &mut impl Renderer) { | |
| let red_texture = ctx.register_external_texture(solid_pixmap(255, 0, 0, 255)); | |
| let green_texture = ctx.register_external_texture(solid_pixmap(0, 255, 0, 255)); | |
| ctx.set_paint(AlphaColor::from_rgba8(32, 32, 32, 255)); | |
| ctx.fill_rect(&Rect::new(4., 4., 92., 92.)); | |
| fill_texture_rect(ctx, red_texture, 8., 8., 40., 40., true); | |
| ctx.set_paint(AlphaColor::from_rgba8(255, 255, 255, 128)); | |
| ctx.fill_rect(&Rect::new(24., 24., 72., 72.)); | |
| fill_texture_rect(ctx, green_texture, 28., 28., 40., 40., true); | |
| fill_texture_rect(ctx, red_texture, 48., 48., 40., 40., true); | |
| } | |
| #[vello_test(gpu_only, gpu_no_depth)] | |
| fn external_texture_root_painter_order(ctx: &mut impl Renderer) { | |
| let coral = ctx.register_external_texture(solid_pixmap(225, 87, 89, 255)); | |
| let teal = ctx.register_external_texture(solid_pixmap(42, 157, 143, 255)); | |
| let navy = ctx.register_external_texture(solid_pixmap(38, 70, 83, 255)); | |
| let gold = ctx.register_external_texture(solid_pixmap(153, 102, 61, 160)); | |
| let tint_source = ctx.register_external_texture(solid_pixmap(255, 255, 255, 255)); | |
| fill_texture_rect(ctx, coral, 6., 6., 88., 88., false); | |
| fill_texture_rect(ctx, gold, 11.5, 33.5, 77., 33., true); | |
| fill_texture_rect(ctx, gold, 33.5, 11.5, 33., 77., true); | |
| fill_texture_rect(ctx, teal, 21.5, 21.5, 57., 57., false); | |
| ctx.set_tint(Some(Tint { | |
| color: Color::from_rgba8(128, 102, 204, 160), | |
| mode: TintMode::Multiply, | |
| })); | |
| fill_texture_rect(ctx, tint_source, 16., 38., 68., 24., false); | |
| fill_texture_rect(ctx, tint_source, 38., 16., 24., 68., false); | |
| ctx.set_tint(None); | |
| fill_texture_rect(ctx, navy, 33.5, 33.5, 33., 33., false); | |
| fill_texture_rect(ctx, gold, 28., 44., 44., 12., true); | |
| fill_texture_rect(ctx, gold, 44., 28., 12., 44., true); | |
| fill_texture_rect(ctx, coral, 44., 44., 12., 12., false); | |
| } | |
| #[vello_test(gpu_only)] | |
| fn external_texture_atlas_interleaving(ctx: &mut impl Renderer) { | |
| let atlas_red = ctx.get_image_source(solid_pixmap(254, 0, 0, 254)); | |
| let external_green = ctx.register_external_texture(solid_pixmap(0, 254, 0, 254)); | |
| let atlas_blue = ctx.get_image_source(solid_pixmap(0, 0, 254, 254)); | |
| let external_yellow = ctx.register_external_texture(solid_pixmap(254, 254, 0, 254)); | |
| let atlas_magenta = ctx.get_image_source(solid_pixmap(254, 0, 254, 254)); | |
| draw_atlas_rect(ctx, atlas_red, Rect::new(10., 10., 34., 34.)); | |
| fill_texture_rect(ctx, external_green, 66., 10., 24., 24., true); | |
| draw_atlas_rect(ctx, atlas_blue, Rect::new(38., 38., 62., 62.)); | |
| fill_texture_rect(ctx, external_yellow, 10., 66., 24., 24., true); | |
| draw_atlas_rect(ctx, atlas_magenta, Rect::new(66., 66., 90., 90.)); | |
| } | |
| #[vello_test(gpu_only)] | |
| fn external_texture_layer_before_external(ctx: &mut impl Renderer) { | |
| let texture_id = ctx.register_external_texture(solid_pixmap(128, 0, 0, 128)); | |
| ctx.push_layer(None, None, None, None, None); | |
| ctx.set_paint(AlphaColor::from_rgba8(0, 255, 0, 255)); | |
| ctx.fill_path(&Rect::new(20., 20., 60., 60.).to_path(0.1)); | |
| ctx.pop_layer(); | |
| ctx.push_layer(None, None, None, None, None); | |
| fill_texture_rect(ctx, texture_id, 40., 40., 40., 40., true); | |
| ctx.pop_layer(); | |
| } | |
| #[vello_test(gpu_only)] | |
| fn external_texture_external_before_layer(ctx: &mut impl Renderer) { | |
| let texture_id = ctx.register_external_texture(solid_pixmap(0, 255, 0, 255)); | |
| ctx.push_layer(None, None, None, None, None); | |
| fill_texture_rect(ctx, texture_id, 20., 20., 40., 40., true); | |
| ctx.pop_layer(); | |
| ctx.push_layer(None, None, None, None, None); | |
| ctx.set_paint(AlphaColor::from_rgba8(255, 0, 0, 128)); | |
| ctx.fill_path(&Rect::new(40., 40., 80., 80.).to_path(0.1)); | |
| ctx.pop_layer(); | |
| } | |
| #[vello_test(gpu_only)] | |
| fn external_texture_layer_circle_orders(ctx: &mut impl Renderer) { | |
| let blue_texture = ctx.register_external_texture(solid_pixmap(0, 0, 255, 255)); | |
| let red_texture = ctx.register_external_texture(solid_pixmap(255, 0, 0, 255)); | |
| let green_texture = ctx.register_external_texture(solid_pixmap(0, 255, 0, 255)); | |
| let blue = AlphaColor::from_rgba8(0, 0, 255, 255); | |
| let green = AlphaColor::from_rgba8(0, 255, 0, 255); | |
| #[derive(Clone, Copy)] | |
| enum CirclePos { | |
| Blue, | |
| Red, | |
| Green, | |
| } | |
| #[derive(Clone, Copy)] | |
| enum CircleOp { | |
| Texture(CirclePos, TextureId), | |
| Paint(CirclePos, AlphaColor<Srgb>), | |
| } | |
| let circle = |(x, y): (f64, f64), pos| match pos { | |
| CirclePos::Blue => Circle::new((x + 25., y + 17.5), 12.5), | |
| CirclePos::Red => Circle::new((x + 17.5, y + 32.5), 12.5), | |
| CirclePos::Green => Circle::new((x + 32.5, y + 32.5), 12.5), | |
| }; | |
| let mut draw_panel = |origin, ops: &[CircleOp]| { | |
| ctx.push_layer(None, None, None, None, None); | |
| for op in ops { | |
| match *op { | |
| CircleOp::Texture(pos, texture_id) => { | |
| texture_circle(ctx, texture_id, circle(origin, pos)); | |
| } | |
| CircleOp::Paint(pos, color) => { | |
| painted_circle(ctx, color, circle(origin, pos)); | |
| } | |
| } | |
| } | |
| ctx.pop_layer(); | |
| }; | |
| draw_panel( | |
| (0., 0.), | |
| &[ | |
| CircleOp::Texture(CirclePos::Blue, blue_texture), | |
| CircleOp::Texture(CirclePos::Red, red_texture), | |
| CircleOp::Texture(CirclePos::Green, green_texture), | |
| ], | |
| ); | |
| draw_panel( | |
| (50., 0.), | |
| &[ | |
| CircleOp::Texture(CirclePos::Red, red_texture), | |
| CircleOp::Texture(CirclePos::Green, green_texture), | |
| CircleOp::Texture(CirclePos::Blue, blue_texture), | |
| ], | |
| ); | |
| draw_panel( | |
| (0., 50.), | |
| &[ | |
| CircleOp::Texture(CirclePos::Green, green_texture), | |
| CircleOp::Texture(CirclePos::Red, red_texture), | |
| CircleOp::Texture(CirclePos::Blue, blue_texture), | |
| ], | |
| ); | |
| draw_panel( | |
| (50., 50.), | |
| &[ | |
| CircleOp::Paint(CirclePos::Blue, blue), | |
| CircleOp::Texture(CirclePos::Red, red_texture), | |
| CircleOp::Paint(CirclePos::Green, green), | |
| ], | |
| ); | |
| } | |
| #[vello_test(width = 96, height = 96, gpu_only, gpu_tolerance = 1)] | |
| fn external_texture_skewed(ctx: &mut impl Renderer) { | |
| let texture_id = ctx.register_external_texture(load_image!("glyphs_colr_noto")); | |
| let source_region = SPRITES[0]; | |
| ctx.set_transform(Affine::translate((15., 15.)) * Affine::skew(0.2, 0.1)); | |
| ctx.set_paint_transform(Affine::IDENTITY); | |
| ctx.set_paint(Image { | |
| image: ImageSource::external_texture(texture_id, source_region, true), | |
| sampler: pad_sampler(ImageQuality::High), | |
| }); | |
| ctx.fill_rect(&Rect::new( | |
| 0., | |
| 0., | |
| f64::from(source_region.width()), | |
| f64::from(source_region.height()), | |
| )); | |
| } | |
| #[vello_test(width = 96, height = 96, gpu_only)] | |
| fn external_texture_clipped(ctx: &mut impl Renderer) { | |
| let texture_id = ctx.register_external_texture(load_image!("glyphs_colr_noto")); | |
| let clip = Circle::new((48., 48.), 24.).to_path(0.1); | |
| ctx.push_clip_layer(&clip); | |
| ctx.set_paint_transform(Affine::translate((18., 18.))); | |
| fill_sprite_rect(ctx, texture_id, SPRITES[1], 18., 18., ImageQuality::Medium); | |
| ctx.set_paint_transform(Affine::translate((34., 34.))); | |
| fill_sprite_rect(ctx, texture_id, SPRITES[3], 34., 34., ImageQuality::Medium); | |
| ctx.pop_layer(); | |
| } | |
| #[vello_test(width = 96, height = 96, gpu_only, gpu_tolerance = 2)] | |
| fn external_texture_blurred(ctx: &mut impl Renderer) { | |
| let texture_id = ctx.register_external_texture(load_image!("glyphs_colr_noto")); | |
| let blur = Filter::from_primitive(FilterPrimitive::GaussianBlur { | |
| std_deviation: 4.0, | |
| edge_mode: EdgeMode::None, | |
| }); | |
| ctx.push_filter_layer(blur); | |
| ctx.set_paint_transform(Affine::translate((20., 20.))); | |
| fill_sprite_rect(ctx, texture_id, SPRITES[2], 20., 20., ImageQuality::Low); | |
| ctx.pop_layer(); | |
| } | |
| #[vello_test(width = 192, height = 132, gpu_only)] | |
| fn external_texture_many_sprites(ctx: &mut impl Renderer) { | |
| let texture_id = ctx.register_external_texture(load_image!("glyphs_colr_noto")); | |
| let placements = [ | |
| (SPRITES[0], 8., 10.), | |
| (SPRITES[1], 34., 12.), | |
| (SPRITES[2], 56., 10.), | |
| (SPRITES[3], 80., 8.), | |
| (SPRITES[0], 106., 11.), | |
| (SPRITES[2], 130., 9.), | |
| (SPRITES[1], 10., 66.), | |
| (SPRITES[3], 34., 64.), | |
| (SPRITES[0], 58., 68.), | |
| (SPRITES[2], 84., 66.), | |
| (SPRITES[3], 110., 64.), | |
| (SPRITES[1], 134., 67.), | |
| ]; | |
| for (source_region, x, y) in placements { | |
| ctx.set_paint_transform(Affine::translate((x, y))); | |
| fill_sprite_rect(ctx, texture_id, source_region, x, y, ImageQuality::Low); | |
| } | |
| } | |
| #[vello_test(width = 96, height = 96, gpu_only, gpu_tolerance = 2)] | |
| fn external_texture_with_scene_transform(ctx: &mut impl Renderer) { | |
| let texture_id = ctx.register_external_texture(load_image!("glyphs_colr_noto")); | |
| let scene_transform = Affine::translate((20., 0.)) | |
| * Affine::rotate(0.35) | |
| * Affine::scale_non_uniform(0.85, 1.1); | |
| for (source_region, local_transform) in [ | |
| (SPRITES[0], Affine::translate((6., 8.))), | |
| ( | |
| SPRITES[3], | |
| Affine::translate((28., 5.)) * Affine::skew(0.18, -0.08), | |
| ), | |
| (SPRITES[2], Affine::translate((48., 6.))), | |
| ] { | |
| ctx.set_transform(scene_transform * local_transform); | |
| ctx.set_paint_transform(Affine::IDENTITY); | |
| ctx.set_paint(Image { | |
| image: ImageSource::external_texture(texture_id, source_region, true), | |
| sampler: pad_sampler(ImageQuality::Medium), | |
| }); | |
| ctx.fill_rect(&Rect::new( | |
| 0., | |
| 0., | |
| f64::from(source_region.width()), | |
| f64::from(source_region.height()), | |
| )); | |
| } | |
| } | |
| } |