From 0426cf8255f2a46dcb488a53714fcbc57d556ae9 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Thu, 12 Dec 2024 01:12:42 -0500 Subject: [PATCH 01/13] Image x/y extend, alpha, nearest neighbor sampling This adds support for image extend modes, alpha and nearest neighbor sampling to fine and plumbs support for all through the pipeline. --- Cargo.lock | 2 +- Cargo.toml | 2 +- examples/scenes/src/images.rs | 4 +- examples/scenes/src/test_scenes.rs | 41 ++++++++++--- vello/src/scene.rs | 6 +- vello_encoding/src/draw.rs | 4 +- vello_encoding/src/encoding.rs | 6 +- vello_shaders/shader/draw_leaf.wgsl | 1 + vello_shaders/shader/fine.wgsl | 77 +++++++++++++++++------- vello_shaders/shader/shared/drawtag.wgsl | 2 +- vello_shaders/shader/shared/ptcl.wgsl | 4 ++ vello_shaders/src/cpu/draw_leaf.rs | 1 + vello_tests/src/compare.rs | 6 +- vello_tests/src/lib.rs | 4 +- vello_tests/src/snapshot.rs | 4 +- vello_tests/tests/known_issues.rs | 4 +- vello_tests/tests/property.rs | 6 +- 17 files changed, 122 insertions(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fd93b92a3..78884ec07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1656,7 +1656,7 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "peniko" version = "0.2.0" -source = "git+https://github.com/linebender/peniko.git?rev=3462e19#3462e19c7ba152a1c3aaa883825073180864e3e4" +source = "git+https://github.com/linebender/peniko.git?rev=98b2b62#98b2b624849c75d600282e468d09aee9e5cb331b" dependencies = [ "color", "kurbo", diff --git a/Cargo.toml b/Cargo.toml index 66e62d80c..e0af0275f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -102,7 +102,7 @@ skrifa = "0.26.0" # The version of kurbo used below should be kept in sync # with the version of kurbo used by peniko. # peniko = "0.2.0" -peniko = { version = "0.2.0", git = "https://github.com/linebender/peniko.git", rev = "3462e19" } +peniko = { version = "0.2.0", git = "https://github.com/linebender/peniko.git", rev = "98b2b62" } # FIXME: This can be removed once peniko supports the schemars feature. kurbo = "0.11.1" futures-intrusive = "0.5.0" diff --git a/examples/scenes/src/images.rs b/examples/scenes/src/images.rs index 95b6a263a..f8046a053 100644 --- a/examples/scenes/src/images.rs +++ b/examples/scenes/src/images.rs @@ -5,7 +5,7 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::sync::Arc; -use vello::peniko::{Blob, Format, Image}; +use vello::peniko::{Blob, Image, ImageFormat}; /// Simple hack to support loading images for examples. #[derive(Default)] @@ -50,5 +50,5 @@ fn decode_image(data: &[u8]) -> anyhow::Result { let height = image.height(); let data = Arc::new(image.into_rgba8().into_vec()); let blob = Blob::new(data); - Ok(Image::new(blob, Format::Rgba8, width, height)) + Ok(Image::new(blob, ImageFormat::Rgba8, width, height)) } diff --git a/examples/scenes/src/test_scenes.rs b/examples/scenes/src/test_scenes.rs index 5fc93be25..89be70ea7 100644 --- a/examples/scenes/src/test_scenes.rs +++ b/examples/scenes/src/test_scenes.rs @@ -80,7 +80,8 @@ export_scenes!( many_draw_objects(many_draw_objects), blurred_rounded_rect(blurred_rounded_rect), image_sampling(image_sampling), - image_extend_modes(image_extend_modes) + image_extend_modes(image_extend_modes), + image_extend_modes_nearest_neighbor(image_extend_modes_nearest_neighbor), ); /// Implementations for the test scenes. @@ -659,7 +660,8 @@ mod impls { let piet_logo = params .images .from_bytes(FLOWER_IMAGE.as_ptr() as usize, FLOWER_IMAGE) - .unwrap(); + .unwrap() + .with_alpha(((params.time * 0.5 + 200.0).sin() as f32 + 1.0) * 0.5); use PathEl::*; let rect = Rect::from_origin_size(Point::new(0.0, 0.0), (1000.0, 1000.0)); @@ -766,7 +768,7 @@ mod impls { ); scene.draw_image( &piet_logo, - Affine::translate((800.0, 50.0)) * Affine::rotate(20f64.to_radians()), + Affine::translate((750.0, 70.0)) * Affine::rotate(20f64.to_radians()), ); } @@ -1779,7 +1781,7 @@ mod impls { blob.extend(c.premultiply().to_rgba8().to_u8_array()); }); let data = Blob::new(Arc::new(blob)); - let image = Image::new(data, Format::Rgba8, 2, 2); + let image = Image::new(data, ImageFormat::Rgba8, 2, 2); scene.draw_image( &image, @@ -1810,6 +1812,16 @@ mod impls { pub(super) fn image_extend_modes(scene: &mut Scene, params: &mut SceneParams) { params.resolution = Some(Vec2::new(1500., 1500.)); params.base_color = Some(palette::css::WHITE); + image_extend_modes_helper(scene, ImageQuality::Medium); + } + + pub(super) fn image_extend_modes_nearest_neighbor(scene: &mut Scene, params: &mut SceneParams) { + params.resolution = Some(Vec2::new(1500., 1500.)); + params.base_color = Some(palette::css::WHITE); + image_extend_modes_helper(scene, ImageQuality::Low); + } + + fn image_extend_modes_helper(scene: &mut Scene, quality: ImageQuality) { let mut blob: Vec = Vec::new(); [ palette::css::RED, @@ -1822,14 +1834,15 @@ mod impls { blob.extend(c.premultiply().to_rgba8().to_u8_array()); }); let data = Blob::new(Arc::new(blob)); - let image = Image::new(data, Format::Rgba8, 2, 2); - let image = image.with_extend(Extend::Pad); + let image = Image::new(data, ImageFormat::Rgba8, 2, 2).with_quality(quality); + let brush_offset = Some(Affine::translate((2., 2.))); // Pad extend mode + let image = image.with_extend(Extend::Pad); scene.fill( Fill::NonZero, Affine::scale(100.).then_translate((100., 100.).into()), &image, - Some(Affine::translate((2., 2.)).then_scale(100.)), + brush_offset, &Rect::new(0., 0., 6., 6.), ); let image = image.with_extend(Extend::Reflect); @@ -1837,7 +1850,7 @@ mod impls { Fill::NonZero, Affine::scale(100.).then_translate((100., 800.).into()), &image, - Some(Affine::translate((2., 2.))), + brush_offset, &Rect::new(0., 0., 6., 6.), ); let image = image.with_extend(Extend::Repeat); @@ -1845,7 +1858,17 @@ mod impls { Fill::NonZero, Affine::scale(100.).then_translate((800., 100.).into()), &image, - Some(Affine::translate((2., 2.))), + brush_offset, + &Rect::new(0., 0., 6., 6.), + ); + let image = image + .with_x_extend(Extend::Repeat) + .with_y_extend(Extend::Reflect); + scene.fill( + Fill::NonZero, + Affine::scale(100.).then_translate((800., 800.).into()), + &image, + brush_offset, &Rect::new(0., 0., 6., 6.), ); } diff --git a/vello/src/scene.rs b/vello/src/scene.rs index aba8ab683..847287310 100644 --- a/vello/src/scene.rs +++ b/vello/src/scene.rs @@ -554,7 +554,7 @@ impl<'a> DrawGlyphs<'a> { Image::new( // TODO: The design of the Blob type forces the double boxing Blob::new(Arc::new(data)), - peniko::Format::Rgba8, + peniko::ImageFormat::Rgba8, bitmap.width, bitmap.height, ) @@ -583,7 +583,7 @@ impl<'a> DrawGlyphs<'a> { Image::new( // TODO: The design of the Blob type forces the double boxing Blob::new(Arc::new(buf)), - peniko::Format::Rgba8, + peniko::ImageFormat::Rgba8, bitmap.width, bitmap.height, ) @@ -614,7 +614,7 @@ impl<'a> DrawGlyphs<'a> { Image::new( // TODO: The design of the Blob type forces the double boxing Blob::new(Arc::new(data)), - peniko::Format::Rgba8, + peniko::ImageFormat::Rgba8, bitmap.width, bitmap.height, ) diff --git a/vello_encoding/src/draw.rs b/vello_encoding/src/draw.rs index 90304ff94..2ed02bb1c 100644 --- a/vello_encoding/src/draw.rs +++ b/vello_encoding/src/draw.rs @@ -31,7 +31,7 @@ impl DrawTag { pub const SWEEP_GRADIENT: Self = Self(0x254); /// Image fill. - pub const IMAGE: Self = Self(0x248); + pub const IMAGE: Self = Self(0x28C); // info: 10, scene: 3 /// Blurred rounded rectangle. pub const BLUR_RECT: Self = Self(0x2d4); // info: 11, scene: 5 (DrawBlurRoundedRect) @@ -164,6 +164,8 @@ pub struct DrawImage { pub xy: u32, /// Packed image dimensions. pub width_height: u32, + /// Packed quality, extend mode and 8-bit alpha (bits `qqxxyyaaaaaaaa`). + pub sample_alpha: u32, } /// Draw data for a blurred rounded rectangle. diff --git a/vello_encoding/src/encoding.rs b/vello_encoding/src/encoding.rs index 9c1cf46d9..62834d4bf 100644 --- a/vello_encoding/src/encoding.rs +++ b/vello_encoding/src/encoding.rs @@ -402,7 +402,7 @@ impl Encoding { /// Encodes an image brush. pub fn encode_image(&mut self, image: &Image, alpha: f32) { - let _alpha = alpha * image.alpha; + let alpha = (alpha * image.alpha * 255.0).round() as u8; // TODO: feed the alpha multiplier through the full pipeline for consistency // with other brushes? // Tracked in https://github.com/linebender/vello/issues/692 @@ -415,6 +415,10 @@ impl Encoding { .extend_from_slice(bytemuck::bytes_of(&DrawImage { xy: 0, width_height: (image.width << 16) | (image.height & 0xFFFF), + sample_alpha: ((image.quality as u32) << 12) + | ((image.x_extend as u32) << 10) + | ((image.y_extend as u32) << 8) + | alpha as u32, })); } diff --git a/vello_shaders/shader/draw_leaf.wgsl b/vello_shaders/shader/draw_leaf.wgsl index e572a2763..415aa81f0 100644 --- a/vello_shaders/shader/draw_leaf.wgsl +++ b/vello_shaders/shader/draw_leaf.wgsl @@ -253,6 +253,7 @@ fn main( info[di + 6u] = bitcast(inv.translate.y); info[di + 7u] = scene[dd]; info[di + 8u] = scene[dd + 1u]; + info[di + 9u] = scene[dd + 2u]; } case DRAWTAG_BLURRED_ROUNDED_RECT: { info[di] = draw_flags; diff --git a/vello_shaders/shader/fine.wgsl b/vello_shaders/shader/fine.wgsl index e09e2ee7d..f092383b1 100644 --- a/vello_shaders/shader/fine.wgsl +++ b/vello_shaders/shader/fine.wgsl @@ -25,6 +25,10 @@ var segments: array; const GRADIENT_WIDTH = 512; +const IMAGE_QUALITY_LOW = 0u; +const IMAGE_QUALITY_MEDIUM = 1u; +const IMAGE_QUALITY_HIGH = 2u; + @group(0) @binding(2) var ptcl: array; @@ -805,12 +809,17 @@ fn read_image(cmd_ix: u32) -> CmdImage { let xlat = vec2(bitcast(info[info_offset + 4u]), bitcast(info[info_offset + 5u])); let xy = info[info_offset + 6u]; let width_height = info[info_offset + 7u]; + let sample_alpha = info[info_offset + 8u]; + let alpha = f32(sample_alpha & 0xFFu) / 255.0; + let quality = sample_alpha >> 12u; + let x_extend = (sample_alpha >> 10u) & 0x3u; + let y_extend = (sample_alpha >> 8u) & 0x3u; // The following are not intended to be bitcasts let x = f32(xy >> 16u); let y = f32(xy & 0xffffu); let width = f32(width_height >> 16u); let height = f32(width_height & 0xffffu); - return CmdImage(matrx, xlat, vec2(x, y), vec2(width, height)); + return CmdImage(matrx, xlat, vec2(x, y), vec2(width, height), x_extend, y_extend, quality, alpha); } fn read_end_clip(cmd_ix: u32) -> CmdEndClip { @@ -1146,26 +1155,52 @@ fn main( case CMD_IMAGE: { let image = read_image(cmd_ix); let atlas_max = image.atlas_offset + image.extents - vec2(1.0); - for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { - // We only need to load from the textures if the value will be used. - if area[i] != 0.0 { - let my_xy = vec2(xy.x + f32(i), xy.y); - let atlas_uv = image.matrx.xy * my_xy.x + image.matrx.zw * my_xy.y + image.xlat + image.atlas_offset - vec2(0.5); - // This currently only implements the Pad extend mode - // TODO: Support repeat and reflect - // TODO: If the image couldn't be added to the atlas (i.e. was too big), this isn't robust - let atlas_uv_clamped = clamp(atlas_uv, image.atlas_offset, atlas_max); - // We know that the floor and ceil are within the atlas area because atlas_max and - // atlas_offset are integers - let uv_quad = vec4(floor(atlas_uv_clamped), ceil(atlas_uv_clamped)); - let uv_frac = fract(atlas_uv); - let a = premul_alpha(textureLoad(image_atlas, vec2(uv_quad.xy), 0)); - let b = premul_alpha(textureLoad(image_atlas, vec2(uv_quad.xw), 0)); - let c = premul_alpha(textureLoad(image_atlas, vec2(uv_quad.zy), 0)); - let d = premul_alpha(textureLoad(image_atlas, vec2(uv_quad.zw), 0)); - let fg_rgba = mix(mix(a, b, uv_frac.y), mix(c, d, uv_frac.y), uv_frac.x); - let fg_i = fg_rgba * area[i]; - rgba[i] = rgba[i] * (1.0 - fg_i.a) + fg_i; + let extents_inv = vec2(1.0) / image.extents; + switch image.quality { + case IMAGE_QUALITY_LOW: { + for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + // We only need to load from the textures if the value will be used. + if area[i] != 0.0 { + let my_xy = vec2(xy.x + f32(i), xy.y); + var atlas_uv = image.matrx.xy * my_xy.x + image.matrx.zw * my_xy.y + image.xlat; + atlas_uv.x = extend_mode(atlas_uv.x * extents_inv.x, image.x_extend_mode) * image.extents.x; + atlas_uv.y = extend_mode(atlas_uv.y * extents_inv.y, image.y_extend_mode) * image.extents.y; + atlas_uv = atlas_uv + image.atlas_offset; + // TODO: If the image couldn't be added to the atlas (i.e. was too big), this isn't robust + let atlas_uv_clamped = clamp(atlas_uv, image.atlas_offset, atlas_max); + let fg_rgba = premul_alpha(textureLoad(image_atlas, vec2(atlas_uv_clamped), 0)); + let r = extend_mode(atlas_uv.x * extents_inv.x, image.x_extend_mode); + let g = extend_mode(atlas_uv.y * extents_inv.y, image.y_extend_mode); + let fg_rgba2 = vec4(r, g, 0.0, 1.0); + let fg_i = fg_rgba * area[i] * image.alpha; + rgba[i] = rgba[i] * (1.0 - fg_i.a) + fg_i; + } + } + } + case IMAGE_QUALITY_MEDIUM, default: { + for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + // We only need to load from the textures if the value will be used. + if area[i] != 0.0 { + let my_xy = vec2(xy.x + f32(i), xy.y); + var atlas_uv = image.matrx.xy * my_xy.x + image.matrx.zw * my_xy.y + image.xlat; + atlas_uv.x = extend_mode(atlas_uv.x * extents_inv.x, image.x_extend_mode) * image.extents.x; + atlas_uv.y = extend_mode(atlas_uv.y * extents_inv.y, image.y_extend_mode) * image.extents.y; + atlas_uv = atlas_uv + image.atlas_offset - vec2(0.5); + // TODO: If the image couldn't be added to the atlas (i.e. was too big), this isn't robust + let atlas_uv_clamped = clamp(atlas_uv, image.atlas_offset, atlas_max); + // We know that the floor and ceil are within the atlas area because atlas_max and + // atlas_offset are integers + let uv_quad = vec4(floor(atlas_uv_clamped), ceil(atlas_uv_clamped)); + let uv_frac = fract(atlas_uv); + let a = premul_alpha(textureLoad(image_atlas, vec2(uv_quad.xy), 0)); + let b = premul_alpha(textureLoad(image_atlas, vec2(uv_quad.xw), 0)); + let c = premul_alpha(textureLoad(image_atlas, vec2(uv_quad.zy), 0)); + let d = premul_alpha(textureLoad(image_atlas, vec2(uv_quad.zw), 0)); + let fg_rgba = mix(mix(a, b, uv_frac.y), mix(c, d, uv_frac.y), uv_frac.x); + let fg_i = fg_rgba * area[i] * image.alpha; + rgba[i] = rgba[i] * (1.0 - fg_i.a) + fg_i; + } + } } } cmd_ix += 2u; diff --git a/vello_shaders/shader/shared/drawtag.wgsl b/vello_shaders/shader/shared/drawtag.wgsl index 245594eb1..8a3bdda8a 100644 --- a/vello_shaders/shader/shared/drawtag.wgsl +++ b/vello_shaders/shader/shared/drawtag.wgsl @@ -21,7 +21,7 @@ const DRAWTAG_FILL_COLOR = 0x44u; const DRAWTAG_FILL_LIN_GRADIENT = 0x114u; const DRAWTAG_FILL_RAD_GRADIENT = 0x29cu; const DRAWTAG_FILL_SWEEP_GRADIENT = 0x254u; -const DRAWTAG_FILL_IMAGE = 0x248u; +const DRAWTAG_FILL_IMAGE = 0x28Cu; const DRAWTAG_BLURRED_ROUNDED_RECT = 0x2d4u; const DRAWTAG_BEGIN_CLIP = 0x9u; const DRAWTAG_END_CLIP = 0x21u; diff --git a/vello_shaders/shader/shared/ptcl.wgsl b/vello_shaders/shader/shared/ptcl.wgsl index 467739c8f..d0b41cbed 100644 --- a/vello_shaders/shader/shared/ptcl.wgsl +++ b/vello_shaders/shader/shared/ptcl.wgsl @@ -97,6 +97,10 @@ struct CmdImage { xlat: vec2, atlas_offset: vec2, extents: vec2, + x_extend_mode: u32, + y_extend_mode: u32, + quality: u32, + alpha: f32, } struct CmdEndClip { diff --git a/vello_shaders/src/cpu/draw_leaf.rs b/vello_shaders/src/cpu/draw_leaf.rs index 0fa5bb2c8..496ad58d0 100644 --- a/vello_shaders/src/cpu/draw_leaf.rs +++ b/vello_shaders/src/cpu/draw_leaf.rs @@ -175,6 +175,7 @@ fn draw_leaf_main( info[di + 6] = f32::to_bits(xform.0[5]); info[di + 7] = scene[dd as usize]; info[di + 8] = scene[dd as usize + 1]; + info[di + 9] = scene[dd as usize + 2]; } DrawTag::BLUR_RECT => { info[di] = draw_flags; diff --git a/vello_tests/src/compare.rs b/vello_tests/src/compare.rs index a87600859..4d29ccf07 100644 --- a/vello_tests/src/compare.rs +++ b/vello_tests/src/compare.rs @@ -10,7 +10,7 @@ use anyhow::{anyhow, bail, Result}; use image::DynamicImage; use nv_flip::FlipPool; use vello::{ - peniko::{Format, Image}, + peniko::{ImageFormat, Image}, Scene, }; @@ -105,8 +105,8 @@ pub async fn compare_gpu_cpu(scene: Scene, mut params: TestParams) -> Result Result Date: Thu, 12 Dec 2024 01:21:33 -0500 Subject: [PATCH 02/13] fmt --- vello_tests/src/compare.rs | 2 +- vello_tests/src/lib.rs | 2 +- vello_tests/src/snapshot.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/vello_tests/src/compare.rs b/vello_tests/src/compare.rs index 4d29ccf07..65524b65a 100644 --- a/vello_tests/src/compare.rs +++ b/vello_tests/src/compare.rs @@ -10,7 +10,7 @@ use anyhow::{anyhow, bail, Result}; use image::DynamicImage; use nv_flip::FlipPool; use vello::{ - peniko::{ImageFormat, Image}, + peniko::{Image, ImageFormat}, Scene, }; diff --git a/vello_tests/src/lib.rs b/vello_tests/src/lib.rs index 5b0b9f2ef..690807b5b 100644 --- a/vello_tests/src/lib.rs +++ b/vello_tests/src/lib.rs @@ -42,7 +42,7 @@ use std::sync::Arc; use anyhow::{anyhow, bail, Result}; use scenes::{ExampleScene, ImageCache, SceneParams, SimpleText}; use vello::kurbo::{Affine, Vec2}; -use vello::peniko::{color::palette, Blob, Color, ImageFormat, Image}; +use vello::peniko::{color::palette, Blob, Color, Image, ImageFormat}; use vello::wgpu::{ self, BufferDescriptor, BufferUsages, CommandEncoderDescriptor, Extent3d, ImageCopyBuffer, TextureDescriptor, TextureFormat, TextureUsages, diff --git a/vello_tests/src/snapshot.rs b/vello_tests/src/snapshot.rs index c1cda1fd5..4b3204cc2 100644 --- a/vello_tests/src/snapshot.rs +++ b/vello_tests/src/snapshot.rs @@ -11,7 +11,7 @@ use std::{ use image::{DynamicImage, ImageError}; use nv_flip::FlipPool; use vello::{ - peniko::{ImageFormat, Image}, + peniko::{Image, ImageFormat}, Scene, }; From 9dfc371d468116487de851a98efb290d52e018bc Mon Sep 17 00:00:00 2001 From: Bruce Mitchener Date: Sat, 4 Jan 2025 07:26:24 +0700 Subject: [PATCH 03/13] Remove extra whitespace. --- vello_shaders/shader/fine.wgsl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vello_shaders/shader/fine.wgsl b/vello_shaders/shader/fine.wgsl index f092383b1..24ecb5c7a 100644 --- a/vello_shaders/shader/fine.wgsl +++ b/vello_shaders/shader/fine.wgsl @@ -1175,7 +1175,7 @@ fn main( let fg_i = fg_rgba * area[i] * image.alpha; rgba[i] = rgba[i] * (1.0 - fg_i.a) + fg_i; } - } + } } case IMAGE_QUALITY_MEDIUM, default: { for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { @@ -1200,7 +1200,7 @@ fn main( let fg_i = fg_rgba * area[i] * image.alpha; rgba[i] = rgba[i] * (1.0 - fg_i.a) + fg_i; } - } + } } } cmd_ix += 2u; From 03a8dff218d565abff3b6960cd739d299b6297b7 Mon Sep 17 00:00:00 2001 From: Bruce Mitchener Date: Sat, 4 Jan 2025 07:26:55 +0700 Subject: [PATCH 04/13] Fix `elided-lifetimes-in-paths` lint --- examples/scenes/src/test_scenes.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/scenes/src/test_scenes.rs b/examples/scenes/src/test_scenes.rs index 5d26019ef..fb4b85621 100644 --- a/examples/scenes/src/test_scenes.rs +++ b/examples/scenes/src/test_scenes.rs @@ -1815,7 +1815,7 @@ mod impls { image_extend_modes_helper(scene, ImageQuality::Medium); } - pub(super) fn image_extend_modes_nearest_neighbor(scene: &mut Scene, params: &mut SceneParams) { + pub(super) fn image_extend_modes_nearest_neighbor(scene: &mut Scene, params: &mut SceneParams<'_>) { params.resolution = Some(Vec2::new(1500., 1500.)); params.base_color = Some(palette::css::WHITE); image_extend_modes_helper(scene, ImageQuality::Low); From 5a7ef517d71fbb116efb9fe3cc120f8fcde55c1b Mon Sep 17 00:00:00 2001 From: Bruce Mitchener Date: Sat, 4 Jan 2025 07:30:12 +0700 Subject: [PATCH 05/13] Update image_extend_modes test snapshot --- vello_tests/snapshots/image_extend_modes.png | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vello_tests/snapshots/image_extend_modes.png b/vello_tests/snapshots/image_extend_modes.png index 2bcfdde63..a53232e08 100644 --- a/vello_tests/snapshots/image_extend_modes.png +++ b/vello_tests/snapshots/image_extend_modes.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f046fa46e495ad62bada74cdfbec9501f29709b4e52c5ec655063a8ffcf7ea4 -size 26146 +oid sha256:a707994259c84dc4ae5f08977de58966c3976e59f03933fb216242b776e526e7 +size 107007 From ea95012c71e27bd4f9d73d00e88095f567092c53 Mon Sep 17 00:00:00 2001 From: Bruce Mitchener Date: Sat, 4 Jan 2025 07:32:05 +0700 Subject: [PATCH 06/13] Add snapshot test for image_extend_modes_nearest_neighbor --- examples/scenes/src/test_scenes.rs | 5 ++++- .../snapshots/image_extend_modes_nearest_neighbor.png | 3 +++ vello_tests/tests/snapshot_test_scenes.rs | 8 ++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 vello_tests/snapshots/image_extend_modes_nearest_neighbor.png diff --git a/examples/scenes/src/test_scenes.rs b/examples/scenes/src/test_scenes.rs index fb4b85621..00b649fd1 100644 --- a/examples/scenes/src/test_scenes.rs +++ b/examples/scenes/src/test_scenes.rs @@ -1815,7 +1815,10 @@ mod impls { image_extend_modes_helper(scene, ImageQuality::Medium); } - pub(super) fn image_extend_modes_nearest_neighbor(scene: &mut Scene, params: &mut SceneParams<'_>) { + pub(super) fn image_extend_modes_nearest_neighbor( + scene: &mut Scene, + params: &mut SceneParams<'_>, + ) { params.resolution = Some(Vec2::new(1500., 1500.)); params.base_color = Some(palette::css::WHITE); image_extend_modes_helper(scene, ImageQuality::Low); diff --git a/vello_tests/snapshots/image_extend_modes_nearest_neighbor.png b/vello_tests/snapshots/image_extend_modes_nearest_neighbor.png new file mode 100644 index 000000000..015c09c39 --- /dev/null +++ b/vello_tests/snapshots/image_extend_modes_nearest_neighbor.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e56f11690b5a99171cdb1b31f11cb5c293085c5e78d398c921525ba7be17612 +size 13764 diff --git a/vello_tests/tests/snapshot_test_scenes.rs b/vello_tests/tests/snapshot_test_scenes.rs index a40171d99..36db6eb93 100644 --- a/vello_tests/tests/snapshot_test_scenes.rs +++ b/vello_tests/tests/snapshot_test_scenes.rs @@ -129,3 +129,11 @@ fn snapshot_image_extend_modes() { let params = TestParams::new("image_extend_modes", 375, 375); snapshot_test_scene(test_scene, params); } + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn snapshot_image_extend_modes_nearest_neighbor() { + let test_scene = test_scenes::image_extend_modes_nearest_neighbor(); + let params = TestParams::new("image_extend_modes_nearest_neighbor", 375, 375); + snapshot_test_scene(test_scene, params); +} From 159293017ec8810b8410e8d34142a5dec51f143e Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Mon, 6 Jan 2025 11:02:06 -0500 Subject: [PATCH 07/13] fix tests --- vello_tests/snapshots/big_colr.png | 4 ++-- vello_tests/snapshots/image_extend_modes.png | 4 ++-- vello_tests/snapshots/image_extend_modes_nearest_neighbor.png | 4 ++-- vello_tests/tests/emoji.rs | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/vello_tests/snapshots/big_colr.png b/vello_tests/snapshots/big_colr.png index c7bc77824..8484a063e 100644 --- a/vello_tests/snapshots/big_colr.png +++ b/vello_tests/snapshots/big_colr.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c43e1738715b4b4359bdda7831f4bedbac3db182884c78bdcabd44fc49fffd55 -size 15735 +oid sha256:ab53a8a4e769ba509cc63c4cae01c09a464d0049d05e7fe51830a4533beca136 +size 15750 diff --git a/vello_tests/snapshots/image_extend_modes.png b/vello_tests/snapshots/image_extend_modes.png index a53232e08..a8a2043a7 100644 --- a/vello_tests/snapshots/image_extend_modes.png +++ b/vello_tests/snapshots/image_extend_modes.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a707994259c84dc4ae5f08977de58966c3976e59f03933fb216242b776e526e7 -size 107007 +oid sha256:1ecd8c9b3d796597f75f45013c2119668fac747e6c79af8e76681c1737d513bd +size 106425 diff --git a/vello_tests/snapshots/image_extend_modes_nearest_neighbor.png b/vello_tests/snapshots/image_extend_modes_nearest_neighbor.png index 015c09c39..81b0c287d 100644 --- a/vello_tests/snapshots/image_extend_modes_nearest_neighbor.png +++ b/vello_tests/snapshots/image_extend_modes_nearest_neighbor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e56f11690b5a99171cdb1b31f11cb5c293085c5e78d398c921525ba7be17612 -size 13764 +oid sha256:5b7e888402be9899a4f3495c8e0d3fb8a2935d1bb0e2dcc358fdd443ea51192a +size 13338 diff --git a/vello_tests/tests/emoji.rs b/vello_tests/tests/emoji.rs index 82258d269..df3582083 100644 --- a/vello_tests/tests/emoji.rs +++ b/vello_tests/tests/emoji.rs @@ -59,7 +59,7 @@ fn big_colr() { ); snapshot_test_sync(scene, ¶ms) .unwrap() - .assert_mean_less_than(0.001); + .assert_mean_less_than(0.002); } #[test] From cfa085a11465fbac052869fb75c55e6d4e5a42df Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Mon, 6 Jan 2025 11:03:24 -0500 Subject: [PATCH 08/13] remove texcoord debug code --- vello_shaders/shader/fine.wgsl | 3 --- 1 file changed, 3 deletions(-) diff --git a/vello_shaders/shader/fine.wgsl b/vello_shaders/shader/fine.wgsl index 24ecb5c7a..26ee559b1 100644 --- a/vello_shaders/shader/fine.wgsl +++ b/vello_shaders/shader/fine.wgsl @@ -1169,9 +1169,6 @@ fn main( // TODO: If the image couldn't be added to the atlas (i.e. was too big), this isn't robust let atlas_uv_clamped = clamp(atlas_uv, image.atlas_offset, atlas_max); let fg_rgba = premul_alpha(textureLoad(image_atlas, vec2(atlas_uv_clamped), 0)); - let r = extend_mode(atlas_uv.x * extents_inv.x, image.x_extend_mode); - let g = extend_mode(atlas_uv.y * extents_inv.y, image.y_extend_mode); - let fg_rgba2 = vec4(r, g, 0.0, 1.0); let fg_i = fg_rgba * area[i] * image.alpha; rgba[i] = rgba[i] * (1.0 - fg_i.a) + fg_i; } From 9b92b4c176c4fdb40e85a81c16cca00fadc813d5 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Mon, 6 Jan 2025 11:11:14 -0500 Subject: [PATCH 09/13] extend modes test scenes return impl fn --- examples/scenes/src/test_scenes.rs | 130 +++++++++++----------- vello_tests/tests/snapshot_test_scenes.rs | 2 +- 2 files changed, 63 insertions(+), 69 deletions(-) diff --git a/examples/scenes/src/test_scenes.rs b/examples/scenes/src/test_scenes.rs index 00b649fd1..22d32ec93 100644 --- a/examples/scenes/src/test_scenes.rs +++ b/examples/scenes/src/test_scenes.rs @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT use crate::{ExampleScene, SceneConfig, SceneSet}; -use vello::kurbo::{Affine, Cap}; +use vello::{ + kurbo::{Affine, Cap}, + peniko::ImageQuality, +}; /// All of the test scenes supported by Vello. pub fn test_scenes() -> SceneSet { @@ -80,8 +83,8 @@ export_scenes!( many_draw_objects(many_draw_objects), blurred_rounded_rect(blurred_rounded_rect), image_sampling(image_sampling), - image_extend_modes(image_extend_modes), - image_extend_modes_nearest_neighbor(image_extend_modes_nearest_neighbor), + image_extend_modes_bilinear(impls::image_extend_modes(ImageQuality::Medium), "image_extend_modes (bilinear)", false), + image_extend_modes_nearest_neighbor(impls::image_extend_modes(ImageQuality::Low), "image_extend_modes (nearest neighbor)", false), ); /// Implementations for the test scenes. @@ -1809,70 +1812,61 @@ mod impls { ); } - pub(super) fn image_extend_modes(scene: &mut Scene, params: &mut SceneParams<'_>) { - params.resolution = Some(Vec2::new(1500., 1500.)); - params.base_color = Some(palette::css::WHITE); - image_extend_modes_helper(scene, ImageQuality::Medium); - } - - pub(super) fn image_extend_modes_nearest_neighbor( - scene: &mut Scene, - params: &mut SceneParams<'_>, - ) { - params.resolution = Some(Vec2::new(1500., 1500.)); - params.base_color = Some(palette::css::WHITE); - image_extend_modes_helper(scene, ImageQuality::Low); - } - - fn image_extend_modes_helper(scene: &mut Scene, quality: ImageQuality) { - let mut blob: Vec = Vec::new(); - [ - palette::css::RED, - palette::css::BLUE, - palette::css::CYAN, - palette::css::MAGENTA, - ] - .iter() - .for_each(|c| { - blob.extend(c.premultiply().to_rgba8().to_u8_array()); - }); - let data = Blob::new(Arc::new(blob)); - let image = Image::new(data, ImageFormat::Rgba8, 2, 2).with_quality(quality); - let brush_offset = Some(Affine::translate((2., 2.))); - // Pad extend mode - let image = image.with_extend(Extend::Pad); - scene.fill( - Fill::NonZero, - Affine::scale(100.).then_translate((100., 100.).into()), - &image, - brush_offset, - &Rect::new(0., 0., 6., 6.), - ); - let image = image.with_extend(Extend::Reflect); - scene.fill( - Fill::NonZero, - Affine::scale(100.).then_translate((100., 800.).into()), - &image, - brush_offset, - &Rect::new(0., 0., 6., 6.), - ); - let image = image.with_extend(Extend::Repeat); - scene.fill( - Fill::NonZero, - Affine::scale(100.).then_translate((800., 100.).into()), - &image, - brush_offset, - &Rect::new(0., 0., 6., 6.), - ); - let image = image - .with_x_extend(Extend::Repeat) - .with_y_extend(Extend::Reflect); - scene.fill( - Fill::NonZero, - Affine::scale(100.).then_translate((800., 800.).into()), - &image, - brush_offset, - &Rect::new(0., 0., 6., 6.), - ); + pub(super) fn image_extend_modes( + quality: ImageQuality, + ) -> impl FnMut(&mut Scene, &mut SceneParams<'_>) { + move |scene, params| { + params.resolution = Some(Vec2::new(1500., 1500.)); + params.base_color = Some(palette::css::WHITE); + let mut blob: Vec = Vec::new(); + [ + palette::css::RED, + palette::css::BLUE, + palette::css::CYAN, + palette::css::MAGENTA, + ] + .iter() + .for_each(|c| { + blob.extend(c.premultiply().to_rgba8().to_u8_array()); + }); + let data = Blob::new(Arc::new(blob)); + let image = Image::new(data, ImageFormat::Rgba8, 2, 2).with_quality(quality); + let brush_offset = Some(Affine::translate((2., 2.))); + // Pad extend mode + let image = image.with_extend(Extend::Pad); + scene.fill( + Fill::NonZero, + Affine::scale(100.).then_translate((100., 100.).into()), + &image, + brush_offset, + &Rect::new(0., 0., 6., 6.), + ); + let image = image.with_extend(Extend::Reflect); + scene.fill( + Fill::NonZero, + Affine::scale(100.).then_translate((100., 800.).into()), + &image, + brush_offset, + &Rect::new(0., 0., 6., 6.), + ); + let image = image.with_extend(Extend::Repeat); + scene.fill( + Fill::NonZero, + Affine::scale(100.).then_translate((800., 100.).into()), + &image, + brush_offset, + &Rect::new(0., 0., 6., 6.), + ); + let image = image + .with_x_extend(Extend::Repeat) + .with_y_extend(Extend::Reflect); + scene.fill( + Fill::NonZero, + Affine::scale(100.).then_translate((800., 800.).into()), + &image, + brush_offset, + &Rect::new(0., 0., 6., 6.), + ); + } } } diff --git a/vello_tests/tests/snapshot_test_scenes.rs b/vello_tests/tests/snapshot_test_scenes.rs index 36db6eb93..19c64a04f 100644 --- a/vello_tests/tests/snapshot_test_scenes.rs +++ b/vello_tests/tests/snapshot_test_scenes.rs @@ -125,7 +125,7 @@ fn snapshot_image_sampling() { #[test] #[cfg_attr(skip_gpu_tests, ignore)] fn snapshot_image_extend_modes() { - let test_scene = test_scenes::image_extend_modes(); + let test_scene = test_scenes::image_extend_modes_bilinear(); let params = TestParams::new("image_extend_modes", 375, 375); snapshot_test_scene(test_scene, params); } From c3073cbd9d02c9cd6b6cf1249ba3890686c153ef Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Mon, 6 Jan 2025 11:13:27 -0500 Subject: [PATCH 10/13] add changelog entry --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74d7a3dd2..dc07717c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ This is the first step towards providing richer color functionality, better hand - Offset in image rendering, and sampling outside correct atlas area ([#722][] by [@dfrg]) - Inference conflict when using Kurbo's `schemars` feature ([#733][] by [@ratmice][]) - Detection of PNG format bitmap fonts, primarily for Apple systems ([#740][] by [@LaurenzV]) +- Support image extend modes, nearest-neighbor sampling and alpha ([#766][] by [@dfrg]) ## [0.3.0][] - 2024-10-04 @@ -215,6 +216,7 @@ This release has an [MSRV][] of 1.75. [#743]: https://github.com/linebender/vello/pull/743 [#754]: https://github.com/linebender/vello/pull/754 [#756]: https://github.com/linebender/vello/pull/756 +[#766]: https://github.com/linebender/vello/pull/766 [Unreleased]: https://github.com/linebender/vello/compare/v0.3.0...HEAD From c51dec287efaa2901a4614fe2a81392146f3523b Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Mon, 6 Jan 2025 11:15:58 -0500 Subject: [PATCH 11/13] improve comments per suggestions --- vello_encoding/src/draw.rs | 3 ++- vello_shaders/shader/fine.wgsl | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/vello_encoding/src/draw.rs b/vello_encoding/src/draw.rs index c527411b5..94db65c1b 100644 --- a/vello_encoding/src/draw.rs +++ b/vello_encoding/src/draw.rs @@ -164,7 +164,8 @@ pub struct DrawImage { pub xy: u32, /// Packed image dimensions. pub width_height: u32, - /// Packed quality, extend mode and 8-bit alpha (bits `qqxxyyaaaaaaaa`). + /// Packed quality, extend mode and 8-bit alpha (bits `qqxxyyaaaaaaaa`, + /// 18 unused prefix bits). pub sample_alpha: u32, } diff --git a/vello_shaders/shader/fine.wgsl b/vello_shaders/shader/fine.wgsl index 26ee559b1..f7fedef27 100644 --- a/vello_shaders/shader/fine.wgsl +++ b/vello_shaders/shader/fine.wgsl @@ -1168,6 +1168,7 @@ fn main( atlas_uv = atlas_uv + image.atlas_offset; // TODO: If the image couldn't be added to the atlas (i.e. was too big), this isn't robust let atlas_uv_clamped = clamp(atlas_uv, image.atlas_offset, atlas_max); + // Nearest neighbor sampling let fg_rgba = premul_alpha(textureLoad(image_atlas, vec2(atlas_uv_clamped), 0)); let fg_i = fg_rgba * area[i] * image.alpha; rgba[i] = rgba[i] * (1.0 - fg_i.a) + fg_i; @@ -1175,6 +1176,7 @@ fn main( } } case IMAGE_QUALITY_MEDIUM, default: { + // We don't have an implementation for `IMAGE_QUALITY_HIGH` yet, just use the same as medium for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { // We only need to load from the textures if the value will be used. if area[i] != 0.0 { @@ -1193,6 +1195,7 @@ fn main( let b = premul_alpha(textureLoad(image_atlas, vec2(uv_quad.xw), 0)); let c = premul_alpha(textureLoad(image_atlas, vec2(uv_quad.zy), 0)); let d = premul_alpha(textureLoad(image_atlas, vec2(uv_quad.zw), 0)); + // Bilinear sampling let fg_rgba = mix(mix(a, b, uv_frac.y), mix(c, d, uv_frac.y), uv_frac.x); let fg_i = fg_rgba * area[i] * image.alpha; rgba[i] = rgba[i] * (1.0 - fg_i.a) + fg_i; From ff50418183dbc9509fece32b6abf2239f354a73c Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Mon, 6 Jan 2025 11:21:21 -0500 Subject: [PATCH 12/13] add tests to ensure expected image variant discriminants --- vello_encoding/src/encoding.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/vello_encoding/src/encoding.rs b/vello_encoding/src/encoding.rs index dda2d5fa2..564bfe071 100644 --- a/vello_encoding/src/encoding.rs +++ b/vello_encoding/src/encoding.rs @@ -573,3 +573,30 @@ impl StreamOffsets { self.styles += other.styles; } } + +#[cfg(test)] +mod tests { + use peniko::{Extend, ImageQuality}; + + #[test] + fn ensure_image_quality_values() { + assert_eq!(ImageQuality::Low as u32, 0); + assert_eq!(ImageQuality::Medium as u32, 1); + assert_eq!(ImageQuality::High as u32, 2); + // exhaustive match to catch new variants + match ImageQuality::Low { + ImageQuality::Low | ImageQuality::Medium | ImageQuality::High => {} + } + } + + #[test] + fn ensure_extend_values() { + assert_eq!(Extend::Pad as u32, 0); + assert_eq!(Extend::Repeat as u32, 1); + assert_eq!(Extend::Reflect as u32, 2); + // exhaustive match to catch new variants + match Extend::Pad { + Extend::Pad | Extend::Repeat | Extend::Reflect => {} + } + } +} From 437133927adfccd867908127414c09e22d183af2 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Fri, 17 Jan 2025 08:09:57 -0500 Subject: [PATCH 13/13] remove extend modes snapshot tests --- vello_tests/snapshots/image_extend_modes.png | 3 --- .../image_extend_modes_nearest_neighbor.png | 3 --- vello_tests/tests/snapshot_test_scenes.rs | 16 ---------------- 3 files changed, 22 deletions(-) delete mode 100644 vello_tests/snapshots/image_extend_modes.png delete mode 100644 vello_tests/snapshots/image_extend_modes_nearest_neighbor.png diff --git a/vello_tests/snapshots/image_extend_modes.png b/vello_tests/snapshots/image_extend_modes.png deleted file mode 100644 index a8a2043a7..000000000 --- a/vello_tests/snapshots/image_extend_modes.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1ecd8c9b3d796597f75f45013c2119668fac747e6c79af8e76681c1737d513bd -size 106425 diff --git a/vello_tests/snapshots/image_extend_modes_nearest_neighbor.png b/vello_tests/snapshots/image_extend_modes_nearest_neighbor.png deleted file mode 100644 index 81b0c287d..000000000 --- a/vello_tests/snapshots/image_extend_modes_nearest_neighbor.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5b7e888402be9899a4f3495c8e0d3fb8a2935d1bb0e2dcc358fdd443ea51192a -size 13338 diff --git a/vello_tests/tests/snapshot_test_scenes.rs b/vello_tests/tests/snapshot_test_scenes.rs index 19c64a04f..e620335e2 100644 --- a/vello_tests/tests/snapshot_test_scenes.rs +++ b/vello_tests/tests/snapshot_test_scenes.rs @@ -121,19 +121,3 @@ fn snapshot_image_sampling() { let params = TestParams::new("image_sampling", 400, 400); snapshot_test_scene(test_scene, params); } - -#[test] -#[cfg_attr(skip_gpu_tests, ignore)] -fn snapshot_image_extend_modes() { - let test_scene = test_scenes::image_extend_modes_bilinear(); - let params = TestParams::new("image_extend_modes", 375, 375); - snapshot_test_scene(test_scene, params); -} - -#[test] -#[cfg_attr(skip_gpu_tests, ignore)] -fn snapshot_image_extend_modes_nearest_neighbor() { - let test_scene = test_scenes::image_extend_modes_nearest_neighbor(); - let params = TestParams::new("image_extend_modes_nearest_neighbor", 375, 375); - snapshot_test_scene(test_scene, params); -}