From 0426cf8255f2a46dcb488a53714fcbc57d556ae9 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Thu, 12 Dec 2024 01:12:42 -0500 Subject: [PATCH 1/2] 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 2/2] 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, };