From 1c126815b3a2a422549b35ca289369f623d14f27 Mon Sep 17 00:00:00 2001 From: "nico.burns" Date: Mon, 10 Aug 2026 23:04:38 +0000 Subject: [PATCH] Add Extend::None (decal) support for images across vello, vello_cpu and vello_hybrid --- Cargo.lock | 3 +- Cargo.toml | 2 +- sparse_strips/vello_common/src/encode.rs | 5 +- .../vello_cpu/src/fine/common/gradient/mod.rs | 3 +- .../vello_cpu/src/fine/common/image.rs | 169 ++++++++++++-- .../vello_cpu/src/fine/lowp/gradient.rs | 3 +- .../vello_cpu/src/fine/lowp/image.rs | 85 ++++++- .../vello_hybrid/src/render/webgl.rs | 3 +- sparse_strips/vello_hybrid/src/render/wgpu.rs | 3 +- .../vello_sparse_shaders/shaders/render.wesl | 212 +++++++++++++----- .../snapshots/image_none_x_none_y.png | 3 + .../snapshots/image_none_x_none_y_bicubic.png | 3 + .../image_none_x_none_y_bilinear.png | 3 + .../snapshots/image_none_x_repeat_y.png | 3 + .../snapshots/image_repeat_x_none_y.png | 3 + .../vello_sparse_tests/tests/image.rs | 42 ++++ vello_encoding/src/encoding.rs | 3 +- vello_shaders/shader/fine.wgsl | 121 +++++++--- 18 files changed, 530 insertions(+), 139 deletions(-) create mode 100644 sparse_strips/vello_sparse_tests/snapshots/image_none_x_none_y.png create mode 100644 sparse_strips/vello_sparse_tests/snapshots/image_none_x_none_y_bicubic.png create mode 100644 sparse_strips/vello_sparse_tests/snapshots/image_none_x_none_y_bilinear.png create mode 100644 sparse_strips/vello_sparse_tests/snapshots/image_none_x_repeat_y.png create mode 100644 sparse_strips/vello_sparse_tests/snapshots/image_repeat_x_none_y.png diff --git a/Cargo.lock b/Cargo.lock index 08cab1ab4e..399481a26b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3042,8 +3042,7 @@ dependencies = [ [[package]] name = "peniko" version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "839c8299360d2e998bdb106dc0a6cd71dcc5f4df51df1b620361bf50e283cca6" +source = "git+https://github.com/DioxusLabs/peniko?rev=44095211acdfb4024ecf0a4c620cd91e82440df3#44095211acdfb4024ecf0a4c620cd91e82440df3" dependencies = [ "bytemuck", "color", diff --git a/Cargo.toml b/Cargo.toml index 6aa907e6b4..9f97e05247 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -101,7 +101,7 @@ bytemuck = { version = "1.25.0", features = ["derive"] } skrifa = { version = "0.44.0", default-features = false, features = ["autohint_shaping"] } # The version of kurbo used below should be kept in sync # with the version of kurbo used by peniko. -peniko = { version = "0.6.1", default-features = false } +peniko = { git = "https://github.com/DioxusLabs/peniko", rev = "44095211acdfb4024ecf0a4c620cd91e82440df3", default-features = false } # FIXME: This can be removed once peniko supports the schemars feature. kurbo = { version = "0.13.1", default-features = false } futures-intrusive = "0.5.0" diff --git a/sparse_strips/vello_common/src/encode.rs b/sparse_strips/vello_common/src/encode.rs index 02f244a194..30ef840dbb 100644 --- a/sparse_strips/vello_common/src/encode.rs +++ b/sparse_strips/vello_common/src/encode.rs @@ -516,7 +516,10 @@ impl EncodeExt for Image { // even if the source pixels are all opaque. let has_opacity = tint.as_ref().is_some_and(|t| t.color.components[3] < 1.0) // Not supported yet, but just to future-proof. - || sampler.alpha != 1.0; + || sampler.alpha != 1.0 + // `Extend::None` produces transparent samples outside of the image. + || sampler.x_extend == Extend::None + || sampler.y_extend == Extend::None; let encoded = EncodedImage { may_have_transparency: self.image.may_have_transparency() || has_opacity, diff --git a/sparse_strips/vello_cpu/src/fine/common/gradient/mod.rs b/sparse_strips/vello_cpu/src/fine/common/gradient/mod.rs index 422aa6da13..3f2ea75b98 100644 --- a/sparse_strips/vello_cpu/src/fine/common/gradient/mod.rs +++ b/sparse_strips/vello_cpu/src/fine/common/gradient/mod.rs @@ -230,7 +230,8 @@ fn invalid_f32_mask(simd: S, indices: u32x4) -> mask32x16 { #[inline(always)] pub(crate) fn apply_extend(val: f32x8, extend: peniko::Extend) -> f32x8 { match extend { - peniko::Extend::Pad => val.max(0.0).min(1.0), + // `None` is currently only supported for images and falls back to `Pad` for gradients. + peniko::Extend::Pad | peniko::Extend::None => val.max(0.0).min(1.0), peniko::Extend::Repeat => (val - val.floor()).fract(), // See peniko::Extend::Reflect => ((val - 1.0) - 2.0 * ((val - 1.0) * 0.5).floor() - 1.0) diff --git a/sparse_strips/vello_cpu/src/fine/common/image.rs b/sparse_strips/vello_cpu/src/fine/common/image.rs index 58bb151bdc..0c0e33d7cf 100644 --- a/sparse_strips/vello_cpu/src/fine/common/image.rs +++ b/sparse_strips/vello_cpu/src/fine/common/image.rs @@ -5,7 +5,9 @@ use crate::fine::macros::{f32x16_painter, u8x16_painter}; use crate::fine::{PosExt, Splat4thExt, u8_to_f32}; use crate::kurbo::Point; use vello_common::encode::EncodedImage; -use vello_common::fearless_simd::{Bytes, Simd, SimdBase, SimdFloat, f32x4, f32x16, u8x16, u32x4}; +use vello_common::fearless_simd::{ + Bytes, Simd, SimdBase, SimdFloat, f32x4, f32x16, mask32x4, u8x16, u32x4, +}; use vello_common::pixmap::Pixmap; use vello_common::simd::element_wise_splat; @@ -14,6 +16,7 @@ use vello_common::simd::element_wise_splat; pub(crate) struct PlainNNImagePainter<'a, S: Simd> { data: ImagePainterData<'a, S>, y_positions: f32x4, + y_mask: mask32x4, cur_x_pos: f32x4, advance: f32, simd: S, @@ -32,14 +35,17 @@ impl<'a, S: Simd> PlainNNImagePainter<'a, S> { simd.vectorize( #[inline(always)] || { + let raw_y_positions = f32x4::splat_pos( + simd, + data.cur_pos.y as f32, + data.x_advances.1, + data.y_advances.1, + ); + let y_mask = + in_bounds_mask(simd, raw_y_positions, image.sampler.y_extend, data.height); let y_positions = extend( simd, - f32x4::splat_pos( - simd, - data.cur_pos.y as f32, - data.x_advances.1, - data.y_advances.1, - ), + raw_y_positions, image.sampler.y_extend, data.height, data.height_inv, @@ -56,6 +62,7 @@ impl<'a, S: Simd> PlainNNImagePainter<'a, S> { data, advance: image.x_advance.x as f32, y_positions, + y_mask, cur_x_pos, simd, } @@ -69,6 +76,12 @@ impl Iterator for PlainNNImagePainter<'_, S> { #[inline(always)] fn next(&mut self) -> Option { + let x_mask = in_bounds_mask( + self.simd, + self.cur_x_pos, + self.data.image.sampler.x_extend, + self.data.width, + ); let x_pos = extend( self.simd, self.cur_x_pos, @@ -78,6 +91,11 @@ impl Iterator for PlainNNImagePainter<'_, S> { ); let samples = sample(self.simd, &self.data, x_pos, self.y_positions); + let samples = apply_mask( + self.simd, + samples, + self.simd.and_mask32x4(x_mask, self.y_mask), + ); self.cur_x_pos += self.advance; @@ -113,14 +131,35 @@ impl Iterator for NNImagePainter<'_, S> { #[inline(always)] fn next(&mut self) -> Option { + let raw_x_positions = f32x4::splat_pos( + self.simd, + self.data.cur_pos.x as f32, + self.data.x_advances.0, + self.data.y_advances.0, + ); + let raw_y_positions = f32x4::splat_pos( + self.simd, + self.data.cur_pos.y as f32, + self.data.x_advances.1, + self.data.y_advances.1, + ); + + let x_mask = in_bounds_mask( + self.simd, + raw_x_positions, + self.data.image.sampler.x_extend, + self.data.width, + ); + let y_mask = in_bounds_mask( + self.simd, + raw_y_positions, + self.data.image.sampler.y_extend, + self.data.height, + ); + let x_positions = extend( self.simd, - f32x4::splat_pos( - self.simd, - self.data.cur_pos.x as f32, - self.data.x_advances.0, - self.data.y_advances.0, - ), + raw_x_positions, self.data.image.sampler.x_extend, self.data.width, self.data.width_inv, @@ -128,18 +167,14 @@ impl Iterator for NNImagePainter<'_, S> { let y_positions = extend( self.simd, - f32x4::splat_pos( - self.simd, - self.data.cur_pos.y as f32, - self.data.x_advances.1, - self.data.y_advances.1, - ), + raw_y_positions, self.data.image.sampler.y_extend, self.data.height, self.data.height_inv, ); let samples = sample(self.simd, &self.data, x_positions, y_positions); + let samples = apply_mask(self.simd, samples, self.simd.and_mask32x4(x_mask, y_mask)); self.data.cur_pos += self.data.image.x_advance; @@ -221,7 +256,7 @@ impl Iterator for FilteredImagePainter<'_, S, QUALIT extend( self.simd, x_positions + $offsets[$idx], - self.data.image.sampler.y_extend, + self.data.image.sampler.x_extend, self.data.width, self.data.width_inv, ) @@ -240,6 +275,34 @@ impl Iterator for FilteredImagePainter<'_, S, QUALIT }; } + macro_rules! mask_x { + ($idx:expr,$offsets:expr) => { + mask_to_weights( + self.simd, + in_bounds_mask( + self.simd, + x_positions + $offsets[$idx], + self.data.image.sampler.x_extend, + self.data.width, + ), + ) + }; + } + + macro_rules! mask_y { + ($idx:expr,$offsets:expr) => { + mask_to_weights( + self.simd, + in_bounds_mask( + self.simd, + y_positions + $offsets[$idx], + self.data.image.sampler.y_extend, + self.data.height, + ), + ) + }; + } + match QUALITY { // medium quality: bilinear 1 => { @@ -253,6 +316,9 @@ impl Iterator for FilteredImagePainter<'_, S, QUALIT const OFFSETS: [f32; 2] = [-0.5, 0.5]; + let x_masks = [mask_x!(0, OFFSETS), mask_x!(1, OFFSETS)]; + let y_masks = [mask_y!(0, OFFSETS), mask_y!(1, OFFSETS)]; + let x_positions = [extend_x!(0, OFFSETS), extend_x!(1, OFFSETS)]; let y_positions = [extend_y!(0, OFFSETS), extend_y!(1, OFFSETS)]; @@ -264,7 +330,10 @@ impl Iterator for FilteredImagePainter<'_, S, QUALIT for y_idx in 0..2 { let y_positions = y_positions[y_idx]; let color_sample = sample(x_positions, y_positions); - let w = element_wise_splat(self.simd, cx[x_idx] * cy[y_idx]); + let w = element_wise_splat( + self.simd, + cx[x_idx] * cy[y_idx] * x_masks[x_idx] * y_masks[y_idx], + ); interpolated_color = w.mul_add(color_sample, interpolated_color); } @@ -280,6 +349,19 @@ impl Iterator for FilteredImagePainter<'_, S, QUALIT const OFFSETS: [f32; 4] = [-1.5, -0.5, 0.5, 1.5]; + let x_masks = [ + mask_x!(0, OFFSETS), + mask_x!(1, OFFSETS), + mask_x!(2, OFFSETS), + mask_x!(3, OFFSETS), + ]; + let y_masks = [ + mask_y!(0, OFFSETS), + mask_y!(1, OFFSETS), + mask_y!(2, OFFSETS), + mask_y!(3, OFFSETS), + ]; + let x_positions = [ extend_x!(0, OFFSETS), extend_x!(1, OFFSETS), @@ -304,7 +386,10 @@ impl Iterator for FilteredImagePainter<'_, S, QUALIT let y_positions = y_positions[y_idx]; let color_sample = sample(x_positions, y_positions); - let w = element_wise_splat(self.simd, cx[x_idx] * cy[y_idx]); + let w = element_wise_splat( + self.simd, + cx[x_idx] * cy[y_idx] * x_masks[x_idx] * y_masks[y_idx], + ); interpolated_color = w.mul_add(color_sample, interpolated_color); } @@ -439,7 +524,12 @@ pub(crate) fn extend( // Note that max should be exclusive, so subtract one to enforce that. // Since the maximum image dimensions we support is u16::MAX, subtracting 1 in f32 // is enough to ensure that all numbers are subtracted correctly. - crate::peniko::Extend::Pad => val.min(max - 1.0).max(f32x4::splat(simd, 0.0)), + // + // For `None`, out-of-bounds positions are clamped like `Pad` so that they can be + // sampled safely; the resulting samples are zeroed via `in_bounds_mask`. + crate::peniko::Extend::Pad | crate::peniko::Extend::None => { + val.min(max - 1.0).max(f32x4::splat(simd, 0.0)) + } crate::peniko::Extend::Repeat => { // floor := (val * inv_max).floor() * max is the nearest multiple of `max` below val. max.mul_add(-(val * inv_max).floor(), val) @@ -468,6 +558,39 @@ pub(crate) fn extend( } } +/// A mask that is all-ones for lanes where `val` is inside `[0, max)` and all-zeroes +/// otherwise. For extend modes other than [`None`](crate::peniko::Extend::None), all +/// positions are considered in-bounds. +#[inline(always)] +pub(crate) fn in_bounds_mask( + simd: S, + val: f32x4, + extend: crate::peniko::Extend, + max: f32x4, +) -> mask32x4 { + match extend { + crate::peniko::Extend::None => simd.and_mask32x4( + simd.simd_ge_f32x4(val, f32x4::splat(simd, 0.0)), + simd.simd_lt_f32x4(val, max), + ), + _ => mask32x4::splat(simd, -1), + } +} + +/// Converts an in-bounds mask to per-lane multiplicative weights of `1.0`/`0.0`. +#[inline(always)] +pub(crate) fn mask_to_weights(simd: S, mask: mask32x4) -> f32x4 { + simd.select_f32x4(mask, f32x4::splat(simd, 1.0), f32x4::splat(simd, 0.0)) +} + +/// Zeroes out the pixels of `samples` for lanes that are masked out. +#[inline(always)] +pub(crate) fn apply_mask(simd: S, samples: u8x16, mask: mask32x4) -> u8x16 { + let as_u32 = u32x4::from_bytes(samples.to_bytes()); + simd.select_u32x4(mask, as_u32, u32x4::splat(simd, 0)) + .to_bytes() +} + /// Calculate the weights for a single fractional value. fn weights(simd: S, fract: f32x4) -> [f32x4; 4] { simd.vectorize( diff --git a/sparse_strips/vello_cpu/src/fine/lowp/gradient.rs b/sparse_strips/vello_cpu/src/fine/lowp/gradient.rs index 5a612d4347..affd5b22e9 100644 --- a/sparse_strips/vello_cpu/src/fine/lowp/gradient.rs +++ b/sparse_strips/vello_cpu/src/fine/lowp/gradient.rs @@ -78,7 +78,8 @@ impl crate::fine::Painter for GradientPainter<'_, S> { #[inline(always)] pub(crate) fn apply_extend(val: f32x16, extend: peniko::Extend) -> f32x16 { match extend { - peniko::Extend::Pad => val.max(0.0).min(1.0), + // `None` is currently only supported for images and falls back to `Pad` for gradients. + peniko::Extend::Pad | peniko::Extend::None => val.max(0.0).min(1.0), peniko::Extend::Repeat => (val - val.floor()).fract(), // See peniko::Extend::Reflect => ((val - 1.0) - 2.0 * ((val - 1.0) * 0.5).floor() - 1.0) diff --git a/sparse_strips/vello_cpu/src/fine/lowp/image.rs b/sparse_strips/vello_cpu/src/fine/lowp/image.rs index c5bbfc7349..b4dcc1bdf8 100644 --- a/sparse_strips/vello_cpu/src/fine/lowp/image.rs +++ b/sparse_strips/vello_cpu/src/fine/lowp/image.rs @@ -2,10 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT use crate::fine::PosExt; -use crate::fine::common::image::{ImagePainterData, extend, fract_floor, sample}; +use crate::fine::common::image::{ + ImagePainterData, apply_mask, extend, fract_floor, in_bounds_mask, sample, +}; use crate::fine::macros::u8x16_painter; use vello_common::encode::EncodedImage; -use vello_common::fearless_simd::{Simd, SimdBase, SimdFloat, f32x4, u8x16, u16x16}; +use vello_common::fearless_simd::{Simd, SimdBase, SimdFloat, f32x4, mask32x4, u8x16, u16x16}; use vello_common::pixmap::Pixmap; use vello_common::simd::element_wise_splat; use vello_common::util::{Div255Ext, f32_to_u8}; @@ -70,6 +72,24 @@ impl Iterator for BilinearImagePainter<'_, S> { ) }; + let mask_x = |x_pos: f32x4| { + in_bounds_mask( + self.simd, + x_pos, + self.data.image.sampler.x_extend, + self.data.width, + ) + }; + + let mask_y = |y_pos: f32x4| { + in_bounds_mask( + self.simd, + y_pos, + self.data.image.sampler.y_extend, + self.data.height, + ) + }; + let fx = f32_to_u8(element_wise_splat( self.simd, fract_floor(x_positions + 0.5).mul_add(255.0, 0.5), @@ -84,23 +104,36 @@ impl Iterator for BilinearImagePainter<'_, S> { let fx_inv = u16x16::splat(self.simd, 255) - fx; let fy_inv = u16x16::splat(self.simd, 255) - fy; + let x_mask1 = mask_x(x_positions - 0.5); + let x_mask2 = mask_x(x_positions + 0.5); + let y_mask1 = mask_y(y_positions - 0.5); + let y_mask2 = mask_y(y_positions + 0.5); + let x_pos1 = extend_x(x_positions - 0.5); let x_pos2 = extend_x(x_positions + 0.5); let y_pos1 = extend_y(y_positions - 0.5); let y_pos2 = extend_y(y_positions + 0.5); + let masked_sample = |x_pos: f32x4, y_pos: f32x4, x_mask, y_mask| { + apply_mask( + self.simd, + sample(self.simd, &self.data, x_pos, y_pos), + self.simd.and_mask32x4(x_mask, y_mask), + ) + }; + let p00 = self .simd - .widen_u8x16(sample(self.simd, &self.data, x_pos1, y_pos1)); + .widen_u8x16(masked_sample(x_pos1, y_pos1, x_mask1, y_mask1)); let p10 = self .simd - .widen_u8x16(sample(self.simd, &self.data, x_pos2, y_pos1)); + .widen_u8x16(masked_sample(x_pos2, y_pos1, x_mask2, y_mask1)); let p01 = self .simd - .widen_u8x16(sample(self.simd, &self.data, x_pos1, y_pos2)); + .widen_u8x16(masked_sample(x_pos1, y_pos2, x_mask1, y_mask2)); let p11 = self .simd - .widen_u8x16(sample(self.simd, &self.data, x_pos2, y_pos2)); + .widen_u8x16(masked_sample(x_pos2, y_pos2, x_mask2, y_mask2)); let ip1 = (p00 * fx_inv + p10 * fx).div_255(); let ip2 = (p01 * fx_inv + p11 * fx).div_255(); @@ -126,6 +159,10 @@ pub(crate) struct PlainBilinearImagePainter<'a, S: Simd> { y_pos1: f32x4, /// Pre-computed y sample positions (bottom row for bilinear grid) y_pos2: f32x4, + /// Pre-computed in-bounds mask for the top row + y_mask1: mask32x4, + /// Pre-computed in-bounds mask for the bottom row + y_mask2: mask32x4, /// Pre-computed y interpolation weight fy: u16x16, /// Pre-computed inverse y interpolation weight @@ -157,6 +194,12 @@ impl<'a, S: Simd> PlainBilinearImagePainter<'a, S> { data.y_advances.1, ); + // Pre-compute y in-bounds masks + let y_mask1 = + in_bounds_mask(simd, y_positions - 0.5, image.sampler.y_extend, data.height); + let y_mask2 = + in_bounds_mask(simd, y_positions + 0.5, image.sampler.y_extend, data.height); + // Pre-compute y extend positions let y_pos1 = extend( simd, @@ -192,6 +235,8 @@ impl<'a, S: Simd> PlainBilinearImagePainter<'a, S> { data, y_pos1, y_pos2, + y_mask1, + y_mask2, fy, fy_inv, cur_x_pos, @@ -212,6 +257,18 @@ impl Iterator for PlainBilinearImagePainter<'_, S> { let x_plus_half = self.cur_x_pos + 0.5; // Only x needs to be extended per-iteration + let x_mask1 = in_bounds_mask( + self.simd, + x_minus_half, + self.data.image.sampler.x_extend, + self.data.width, + ); + let x_mask2 = in_bounds_mask( + self.simd, + x_plus_half, + self.data.image.sampler.x_extend, + self.data.width, + ); let x_pos1 = extend( self.simd, x_minus_half, @@ -235,19 +292,27 @@ impl Iterator for PlainBilinearImagePainter<'_, S> { let fx = self.simd.widen_u8x16(fx); let fx_inv = u16x16::splat(self.simd, 255) - fx; + let masked_sample = |x_pos: f32x4, y_pos: f32x4, x_mask, y_mask| { + apply_mask( + self.simd, + sample(self.simd, &self.data, x_pos, y_pos), + self.simd.and_mask32x4(x_mask, y_mask), + ) + }; + // Sample the 4 corners using pre-computed y positions let p00 = self .simd - .widen_u8x16(sample(self.simd, &self.data, x_pos1, self.y_pos1)); + .widen_u8x16(masked_sample(x_pos1, self.y_pos1, x_mask1, self.y_mask1)); let p10 = self .simd - .widen_u8x16(sample(self.simd, &self.data, x_pos2, self.y_pos1)); + .widen_u8x16(masked_sample(x_pos2, self.y_pos1, x_mask2, self.y_mask1)); let p01 = self .simd - .widen_u8x16(sample(self.simd, &self.data, x_pos1, self.y_pos2)); + .widen_u8x16(masked_sample(x_pos1, self.y_pos2, x_mask1, self.y_mask2)); let p11 = self .simd - .widen_u8x16(sample(self.simd, &self.data, x_pos2, self.y_pos2)); + .widen_u8x16(masked_sample(x_pos2, self.y_pos2, x_mask2, self.y_mask2)); // Bilinear interpolation let ip1 = (p00 * fx_inv + p10 * fx).div_255(); diff --git a/sparse_strips/vello_hybrid/src/render/webgl.rs b/sparse_strips/vello_hybrid/src/render/webgl.rs index fee372c3ce..b3271ddcb4 100644 --- a/sparse_strips/vello_hybrid/src/render/webgl.rs +++ b/sparse_strips/vello_hybrid/src/render/webgl.rs @@ -828,7 +828,8 @@ impl WebGlRenderer { ) -> GpuEncodedPaint { let transform = gradient.transform.as_coeffs().map(|x| x as f32); let extend_mode = match gradient.extend { - peniko::Extend::Pad => 0, + // `None` is currently only supported for images and falls back to `Pad` for gradients. + peniko::Extend::Pad | peniko::Extend::None => 0, peniko::Extend::Repeat => 1, peniko::Extend::Reflect => 2, }; diff --git a/sparse_strips/vello_hybrid/src/render/wgpu.rs b/sparse_strips/vello_hybrid/src/render/wgpu.rs index ea9ba9aa5c..5f93fc7af0 100644 --- a/sparse_strips/vello_hybrid/src/render/wgpu.rs +++ b/sparse_strips/vello_hybrid/src/render/wgpu.rs @@ -791,7 +791,8 @@ impl Renderer { ) -> GpuEncodedPaint { let transform = gradient.transform.as_coeffs().map(|x| x as f32); let extend_mode = match gradient.extend { - peniko::Extend::Pad => 0, + // `None` is currently only supported for images and falls back to `Pad` for gradients. + peniko::Extend::Pad | peniko::Extend::None => 0, peniko::Extend::Repeat => 1, peniko::Extend::Reflect => 2, }; diff --git a/sparse_strips/vello_sparse_shaders/shaders/render.wesl b/sparse_strips/vello_sparse_shaders/shaders/render.wesl index bc38799568..7903a40618 100644 --- a/sparse_strips/vello_sparse_shaders/shaders/render.wesl +++ b/sparse_strips/vello_sparse_shaders/shaders/render.wesl @@ -388,21 +388,41 @@ fn fs_main( extend_mode(local_xy.x + offset, image_extend_modes.x, image_size.x), extend_mode(local_xy.y + offset, image_extend_modes.y, image_size.y) ); + // For filtered sampling, keep `EXTEND_NONE` axes unclamped so that + // out-of-bounds taps can be weighted by zero, fading the image edge. + let extended_xy_unclamped = vec2( + extend_mode_unclamped(local_xy.x + offset, image_extend_modes.x, image_size.x), + extend_mode_unclamped(local_xy.y + offset, image_extend_modes.y, image_size.y) + ); // TODO: add a fast path for images where we are using bilinear sampling and want transparent pixels, // using GPU-native bilinear sampling var sample_color: vec4; if image_source_kind == IMAGE_SOURCE_EXTERNAL { - let final_xy = image_offset + extended_xy; - sample_color = sample_external_image( - image_quality, - final_xy, - image_offset, - image_size, - ); + if image_quality == IMAGE_QUALITY_LOW { + let final_xy = image_offset + extended_xy; + sample_color = sample_external_image( + image_quality, + final_xy, + image_offset, + image_size, + image_extend_modes, + ); + sample_color *= tap_weight(local_xy.x + offset, image_extend_modes.x, 0.0, image_size.x) + * tap_weight(local_xy.y + offset, image_extend_modes.y, 0.0, image_size.y); + } else { + let final_xy = image_offset + extended_xy_unclamped; + sample_color = sample_external_image( + image_quality, + final_xy, + image_offset, + image_size, + image_extend_modes, + ); + } } else if image_quality == IMAGE_QUALITY_HIGH { - let final_xy = image_offset + extended_xy; + let final_xy = image_offset + extended_xy_unclamped; sample_color = bicubic_sample( atlas_texture_array, final_xy, @@ -413,7 +433,7 @@ fn fs_main( image_padding, ); } else if image_quality == IMAGE_QUALITY_MEDIUM { - let final_xy = image_offset + extended_xy - vec2(0.5); + let final_xy = image_offset + extended_xy_unclamped - vec2(0.5); sample_color = bilinear_sample( atlas_texture_array, final_xy, @@ -431,6 +451,8 @@ fn fs_main( i32(image_atlas_index), 0, ); + sample_color *= tap_weight(local_xy.x + offset, image_extend_modes.x, 0.0, image_size.x) + * tap_weight(local_xy.y + offset, image_extend_modes.y, 0.0, image_size.y); } final_color = alpha * select( @@ -629,9 +651,13 @@ fn unpack_alphas_from_channel(rgba: vec4, channel_index: u32) -> u32 { const EXTEND_PAD: u32 = 0u; const EXTEND_REPEAT: u32 = 1u; const EXTEND_REFLECT: u32 = 2u; +// Does not extend the image: samples outside the image are transparent. +const EXTEND_NONE: u32 = 3u; fn extend_mode(t: f32, mode: u32, max: f32) -> f32 { switch mode { - case EXTEND_PAD: { + // For `EXTEND_NONE`, out-of-bounds positions are clamped like `EXTEND_PAD` so + // that they can be sampled safely; the resulting samples are zeroed separately. + case EXTEND_PAD, EXTEND_NONE: { return clamp(t, 0.0, max - 1.0); } case EXTEND_REPEAT: { @@ -643,6 +669,21 @@ fn extend_mode(t: f32, mode: u32, max: f32) -> f32 { } } +// Like `extend_mode`, but leaves positions unmodified for `EXTEND_NONE` so that +// filtered sampling can weight out-of-bounds taps by zero. +fn extend_mode_unclamped(t: f32, mode: u32, max: f32) -> f32 { + if mode == EXTEND_NONE { + return t; + } + return extend_mode(t, mode, max); +} + +// The weight of a sample tap at position `t`: zero when the position is outside +// `[lo, hi)` and the extend mode is `EXTEND_NONE`, one otherwise. +fn tap_weight(t: f32, mode: u32, lo: f32, hi: f32) -> f32 { + return select(1.0, f32(t >= lo && t < hi), mode == EXTEND_NONE); +} + fn extend_mode_normalized(t: f32, mode: u32) -> f32 { switch mode { case EXTEND_PAD: { @@ -667,17 +708,24 @@ fn bilinear_sample( atlas_idx: i32, image_offset: vec2, image_size: vec2, - _extend_modes: vec2, + extend_modes: vec2, _image_padding: f32, ) -> vec4 { let atlas_max = image_offset + image_size - vec2(1.0); - let atlas_uv_clamped = clamp(coords, image_offset, atlas_max); - let uv_quad = vec4(floor(atlas_uv_clamped), ceil(atlas_uv_clamped)); + let image_end = image_offset + image_size; + let uv0 = floor(coords); + let uv1 = uv0 + vec2(1.0); + let wx0 = tap_weight(uv0.x, extend_modes.x, image_offset.x, image_end.x); + let wx1 = tap_weight(uv1.x, extend_modes.x, image_offset.x, image_end.x); + let wy0 = tap_weight(uv0.y, extend_modes.y, image_offset.y, image_end.y); + let wy1 = tap_weight(uv1.y, extend_modes.y, image_offset.y, image_end.y); + let cuv0 = clamp(uv0, image_offset, atlas_max); + let cuv1 = clamp(uv1, image_offset, atlas_max); let uv_frac = fract(coords); - let a = textureLoad(tex, vec2(uv_quad.xy), atlas_idx, 0); - let b = textureLoad(tex, vec2(uv_quad.xw), atlas_idx, 0); - let c = textureLoad(tex, vec2(uv_quad.zy), atlas_idx, 0); - let d = textureLoad(tex, vec2(uv_quad.zw), atlas_idx, 0); + let a = wx0 * wy0 * textureLoad(tex, vec2(cuv0), atlas_idx, 0); + let b = wx0 * wy1 * textureLoad(tex, vec2(vec2(cuv0.x, cuv1.y)), atlas_idx, 0); + let c = wx1 * wy0 * textureLoad(tex, vec2(vec2(cuv1.x, cuv0.y)), atlas_idx, 0); + let d = wx1 * wy1 * textureLoad(tex, vec2(cuv1), atlas_idx, 0); return mix(mix(a, b, uv_frac.y), mix(c, d, uv_frac.y), uv_frac.x); } @@ -686,15 +734,23 @@ fn external_bilinear_sample( coords: vec2, image_offset: vec2, image_size: vec2, + extend_modes: vec2, ) -> vec4 { let atlas_max = image_offset + image_size - vec2(1.0); - let atlas_uv_clamped = clamp(coords, image_offset, atlas_max); - let uv_quad = vec4(floor(atlas_uv_clamped), ceil(atlas_uv_clamped)); + let image_end = image_offset + image_size; + let uv0 = floor(coords); + let uv1 = uv0 + vec2(1.0); + let wx0 = tap_weight(uv0.x, extend_modes.x, image_offset.x, image_end.x); + let wx1 = tap_weight(uv1.x, extend_modes.x, image_offset.x, image_end.x); + let wy0 = tap_weight(uv0.y, extend_modes.y, image_offset.y, image_end.y); + let wy1 = tap_weight(uv1.y, extend_modes.y, image_offset.y, image_end.y); + let cuv0 = clamp(uv0, image_offset, atlas_max); + let cuv1 = clamp(uv1, image_offset, atlas_max); let uv_frac = fract(coords); - let a = textureLoad(external_texture, vec2(uv_quad.xy), 0); - let b = textureLoad(external_texture, vec2(uv_quad.xw), 0); - let c = textureLoad(external_texture, vec2(uv_quad.zy), 0); - let d = textureLoad(external_texture, vec2(uv_quad.zw), 0); + let a = wx0 * wy0 * textureLoad(external_texture, vec2(cuv0), 0); + let b = wx0 * wy1 * textureLoad(external_texture, vec2(vec2(cuv0.x, cuv1.y)), 0); + let c = wx1 * wy0 * textureLoad(external_texture, vec2(vec2(cuv1.x, cuv0.y)), 0); + let d = wx1 * wy1 * textureLoad(external_texture, vec2(cuv1), 0); return mix(mix(a, b, uv_frac.y), mix(c, d, uv_frac.y), uv_frac.x); } @@ -710,35 +766,50 @@ fn bicubic_sample( atlas_idx: i32, image_offset: vec2, image_size: vec2, - _extend_modes: vec2, + extend_modes: vec2, _image_padding: f32, ) -> vec4 { let atlas_max = image_offset + image_size - vec2(1.0); + let image_end = image_offset + image_size; let frac_coords = fract(coords + 0.5); // Get cubic weights for x and y directions let cx = cubic_weights(frac_coords.x); let cy = cubic_weights(frac_coords.y); - + + // Per-tap weights that zero out-of-bounds taps for `EXTEND_NONE`. + let wx = vec4( + tap_weight(coords.x - 1.5, extend_modes.x, image_offset.x, image_end.x), + tap_weight(coords.x - 0.5, extend_modes.x, image_offset.x, image_end.x), + tap_weight(coords.x + 0.5, extend_modes.x, image_offset.x, image_end.x), + tap_weight(coords.x + 1.5, extend_modes.x, image_offset.x, image_end.x), + ); + let wy = vec4( + tap_weight(coords.y - 1.5, extend_modes.y, image_offset.y, image_end.y), + tap_weight(coords.y - 0.5, extend_modes.y, image_offset.y, image_end.y), + tap_weight(coords.y + 0.5, extend_modes.y, image_offset.y, image_end.y), + tap_weight(coords.y + 1.5, extend_modes.y, image_offset.y, image_end.y), + ); + // Sample 4x4 grid around coords - let s00 = textureLoad(tex, vec2(clamp(coords + vec2(-1.5, -1.5), image_offset, atlas_max)), atlas_idx, 0); - let s10 = textureLoad(tex, vec2(clamp(coords + vec2(-0.5, -1.5), image_offset, atlas_max)), atlas_idx, 0); - let s20 = textureLoad(tex, vec2(clamp(coords + vec2(0.5, -1.5), image_offset, atlas_max)), atlas_idx, 0); - let s30 = textureLoad(tex, vec2(clamp(coords + vec2(1.5, -1.5), image_offset, atlas_max)), atlas_idx, 0); + let s00 = wx.x * wy.x * textureLoad(tex, vec2(clamp(coords + vec2(-1.5, -1.5), image_offset, atlas_max)), atlas_idx, 0); + let s10 = wx.y * wy.x * textureLoad(tex, vec2(clamp(coords + vec2(-0.5, -1.5), image_offset, atlas_max)), atlas_idx, 0); + let s20 = wx.z * wy.x * textureLoad(tex, vec2(clamp(coords + vec2(0.5, -1.5), image_offset, atlas_max)), atlas_idx, 0); + let s30 = wx.w * wy.x * textureLoad(tex, vec2(clamp(coords + vec2(1.5, -1.5), image_offset, atlas_max)), atlas_idx, 0); - let s01 = textureLoad(tex, vec2(clamp(coords + vec2(-1.5, -0.5), image_offset, atlas_max)), atlas_idx, 0); - let s11 = textureLoad(tex, vec2(clamp(coords + vec2(-0.5, -0.5), image_offset, atlas_max)), atlas_idx, 0); - let s21 = textureLoad(tex, vec2(clamp(coords + vec2(0.5, -0.5), image_offset, atlas_max)), atlas_idx, 0); - let s31 = textureLoad(tex, vec2(clamp(coords + vec2(1.5, -0.5), image_offset, atlas_max)), atlas_idx, 0); + let s01 = wx.x * wy.y * textureLoad(tex, vec2(clamp(coords + vec2(-1.5, -0.5), image_offset, atlas_max)), atlas_idx, 0); + let s11 = wx.y * wy.y * textureLoad(tex, vec2(clamp(coords + vec2(-0.5, -0.5), image_offset, atlas_max)), atlas_idx, 0); + let s21 = wx.z * wy.y * textureLoad(tex, vec2(clamp(coords + vec2(0.5, -0.5), image_offset, atlas_max)), atlas_idx, 0); + let s31 = wx.w * wy.y * textureLoad(tex, vec2(clamp(coords + vec2(1.5, -0.5), image_offset, atlas_max)), atlas_idx, 0); - let s02 = textureLoad(tex, vec2(clamp(coords + vec2(-1.5, 0.5), image_offset, atlas_max)), atlas_idx, 0); - let s12 = textureLoad(tex, vec2(clamp(coords + vec2(-0.5, 0.5), image_offset, atlas_max)), atlas_idx, 0); - let s22 = textureLoad(tex, vec2(clamp(coords + vec2(0.5, 0.5), image_offset, atlas_max)), atlas_idx, 0); - let s32 = textureLoad(tex, vec2(clamp(coords + vec2(1.5, 0.5), image_offset, atlas_max)), atlas_idx, 0); + let s02 = wx.x * wy.z * textureLoad(tex, vec2(clamp(coords + vec2(-1.5, 0.5), image_offset, atlas_max)), atlas_idx, 0); + let s12 = wx.y * wy.z * textureLoad(tex, vec2(clamp(coords + vec2(-0.5, 0.5), image_offset, atlas_max)), atlas_idx, 0); + let s22 = wx.z * wy.z * textureLoad(tex, vec2(clamp(coords + vec2(0.5, 0.5), image_offset, atlas_max)), atlas_idx, 0); + let s32 = wx.w * wy.z * textureLoad(tex, vec2(clamp(coords + vec2(1.5, 0.5), image_offset, atlas_max)), atlas_idx, 0); - let s03 = textureLoad(tex, vec2(clamp(coords + vec2(-1.5, 1.5), image_offset, atlas_max)), atlas_idx, 0); - let s13 = textureLoad(tex, vec2(clamp(coords + vec2(-0.5, 1.5), image_offset, atlas_max)), atlas_idx, 0); - let s23 = textureLoad(tex, vec2(clamp(coords + vec2(0.5, 1.5), image_offset, atlas_max)), atlas_idx, 0); - let s33 = textureLoad(tex, vec2(clamp(coords + vec2(1.5, 1.5), image_offset, atlas_max)), atlas_idx, 0); + let s03 = wx.x * wy.w * textureLoad(tex, vec2(clamp(coords + vec2(-1.5, 1.5), image_offset, atlas_max)), atlas_idx, 0); + let s13 = wx.y * wy.w * textureLoad(tex, vec2(clamp(coords + vec2(-0.5, 1.5), image_offset, atlas_max)), atlas_idx, 0); + let s23 = wx.z * wy.w * textureLoad(tex, vec2(clamp(coords + vec2(0.5, 1.5), image_offset, atlas_max)), atlas_idx, 0); + let s33 = wx.w * wy.w * textureLoad(tex, vec2(clamp(coords + vec2(1.5, 1.5), image_offset, atlas_max)), atlas_idx, 0); // Interpolate in x direction for each row let row0 = cx.x * s00 + cx.y * s10 + cx.z * s20 + cx.w * s30; @@ -758,33 +829,49 @@ fn external_bicubic_sample( coords: vec2, image_offset: vec2, image_size: vec2, + extend_modes: vec2, ) -> vec4 { let atlas_max = image_offset + image_size - vec2(1.0); + let image_end = image_offset + image_size; let frac_coords = fract(coords + 0.5); // Get cubic weights for x and y directions let cx = cubic_weights(frac_coords.x); let cy = cubic_weights(frac_coords.y); + // Per-tap weights that zero out-of-bounds taps for `EXTEND_NONE`. + let wx = vec4( + tap_weight(coords.x - 1.5, extend_modes.x, image_offset.x, image_end.x), + tap_weight(coords.x - 0.5, extend_modes.x, image_offset.x, image_end.x), + tap_weight(coords.x + 0.5, extend_modes.x, image_offset.x, image_end.x), + tap_weight(coords.x + 1.5, extend_modes.x, image_offset.x, image_end.x), + ); + let wy = vec4( + tap_weight(coords.y - 1.5, extend_modes.y, image_offset.y, image_end.y), + tap_weight(coords.y - 0.5, extend_modes.y, image_offset.y, image_end.y), + tap_weight(coords.y + 0.5, extend_modes.y, image_offset.y, image_end.y), + tap_weight(coords.y + 1.5, extend_modes.y, image_offset.y, image_end.y), + ); + // Sample 4x4 grid around coords - let s00 = textureLoad(external_texture, vec2(clamp(coords + vec2(-1.5, -1.5), image_offset, atlas_max)), 0); - let s10 = textureLoad(external_texture, vec2(clamp(coords + vec2(-0.5, -1.5), image_offset, atlas_max)), 0); - let s20 = textureLoad(external_texture, vec2(clamp(coords + vec2(0.5, -1.5), image_offset, atlas_max)), 0); - let s30 = textureLoad(external_texture, vec2(clamp(coords + vec2(1.5, -1.5), image_offset, atlas_max)), 0); - - let s01 = textureLoad(external_texture, vec2(clamp(coords + vec2(-1.5, -0.5), image_offset, atlas_max)), 0); - let s11 = textureLoad(external_texture, vec2(clamp(coords + vec2(-0.5, -0.5), image_offset, atlas_max)), 0); - let s21 = textureLoad(external_texture, vec2(clamp(coords + vec2(0.5, -0.5), image_offset, atlas_max)), 0); - let s31 = textureLoad(external_texture, vec2(clamp(coords + vec2(1.5, -0.5), image_offset, atlas_max)), 0); - - let s02 = textureLoad(external_texture, vec2(clamp(coords + vec2(-1.5, 0.5), image_offset, atlas_max)), 0); - let s12 = textureLoad(external_texture, vec2(clamp(coords + vec2(-0.5, 0.5), image_offset, atlas_max)), 0); - let s22 = textureLoad(external_texture, vec2(clamp(coords + vec2(0.5, 0.5), image_offset, atlas_max)), 0); - let s32 = textureLoad(external_texture, vec2(clamp(coords + vec2(1.5, 0.5), image_offset, atlas_max)), 0); - - let s03 = textureLoad(external_texture, vec2(clamp(coords + vec2(-1.5, 1.5), image_offset, atlas_max)), 0); - let s13 = textureLoad(external_texture, vec2(clamp(coords + vec2(-0.5, 1.5), image_offset, atlas_max)), 0); - let s23 = textureLoad(external_texture, vec2(clamp(coords + vec2(0.5, 1.5), image_offset, atlas_max)), 0); - let s33 = textureLoad(external_texture, vec2(clamp(coords + vec2(1.5, 1.5), image_offset, atlas_max)), 0); + let s00 = wx.x * wy.x * textureLoad(external_texture, vec2(clamp(coords + vec2(-1.5, -1.5), image_offset, atlas_max)), 0); + let s10 = wx.y * wy.x * textureLoad(external_texture, vec2(clamp(coords + vec2(-0.5, -1.5), image_offset, atlas_max)), 0); + let s20 = wx.z * wy.x * textureLoad(external_texture, vec2(clamp(coords + vec2(0.5, -1.5), image_offset, atlas_max)), 0); + let s30 = wx.w * wy.x * textureLoad(external_texture, vec2(clamp(coords + vec2(1.5, -1.5), image_offset, atlas_max)), 0); + + let s01 = wx.x * wy.y * textureLoad(external_texture, vec2(clamp(coords + vec2(-1.5, -0.5), image_offset, atlas_max)), 0); + let s11 = wx.y * wy.y * textureLoad(external_texture, vec2(clamp(coords + vec2(-0.5, -0.5), image_offset, atlas_max)), 0); + let s21 = wx.z * wy.y * textureLoad(external_texture, vec2(clamp(coords + vec2(0.5, -0.5), image_offset, atlas_max)), 0); + let s31 = wx.w * wy.y * textureLoad(external_texture, vec2(clamp(coords + vec2(1.5, -0.5), image_offset, atlas_max)), 0); + + let s02 = wx.x * wy.z * textureLoad(external_texture, vec2(clamp(coords + vec2(-1.5, 0.5), image_offset, atlas_max)), 0); + let s12 = wx.y * wy.z * textureLoad(external_texture, vec2(clamp(coords + vec2(-0.5, 0.5), image_offset, atlas_max)), 0); + let s22 = wx.z * wy.z * textureLoad(external_texture, vec2(clamp(coords + vec2(0.5, 0.5), image_offset, atlas_max)), 0); + let s32 = wx.w * wy.z * textureLoad(external_texture, vec2(clamp(coords + vec2(1.5, 0.5), image_offset, atlas_max)), 0); + + let s03 = wx.x * wy.w * textureLoad(external_texture, vec2(clamp(coords + vec2(-1.5, 1.5), image_offset, atlas_max)), 0); + let s13 = wx.y * wy.w * textureLoad(external_texture, vec2(clamp(coords + vec2(-0.5, 1.5), image_offset, atlas_max)), 0); + let s23 = wx.z * wy.w * textureLoad(external_texture, vec2(clamp(coords + vec2(0.5, 1.5), image_offset, atlas_max)), 0); + let s33 = wx.w * wy.w * textureLoad(external_texture, vec2(clamp(coords + vec2(1.5, 1.5), image_offset, atlas_max)), 0); // Interpolate in x direction for each row let row0 = cx.x * s00 + cx.y * s10 + cx.z * s20 + cx.w * s30; @@ -803,12 +890,13 @@ fn sample_external_image( coords: vec2, image_offset: vec2, image_size: vec2, + extend_modes: vec2, ) -> vec4 { if quality == IMAGE_QUALITY_HIGH { - return external_bicubic_sample(coords, image_offset, image_size); + return external_bicubic_sample(coords, image_offset, image_size, extend_modes); } if quality == IMAGE_QUALITY_MEDIUM { - return external_bilinear_sample(coords - vec2(0.5), image_offset, image_size); + return external_bilinear_sample(coords - vec2(0.5), image_offset, image_size, extend_modes); } return textureLoad(external_texture, vec2(coords), 0); } diff --git a/sparse_strips/vello_sparse_tests/snapshots/image_none_x_none_y.png b/sparse_strips/vello_sparse_tests/snapshots/image_none_x_none_y.png new file mode 100644 index 0000000000..1363c5784a --- /dev/null +++ b/sparse_strips/vello_sparse_tests/snapshots/image_none_x_none_y.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cee06af6883918cbdf131bcdda6455fd15c8f3ca9fb037b8b0fb23365ad930b +size 135 diff --git a/sparse_strips/vello_sparse_tests/snapshots/image_none_x_none_y_bicubic.png b/sparse_strips/vello_sparse_tests/snapshots/image_none_x_none_y_bicubic.png new file mode 100644 index 0000000000..305616b868 --- /dev/null +++ b/sparse_strips/vello_sparse_tests/snapshots/image_none_x_none_y_bicubic.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c06fd25894c7a5860468b606006b64983601c6aff40e4e01a763bcaf84385a19 +size 342 diff --git a/sparse_strips/vello_sparse_tests/snapshots/image_none_x_none_y_bilinear.png b/sparse_strips/vello_sparse_tests/snapshots/image_none_x_none_y_bilinear.png new file mode 100644 index 0000000000..1363c5784a --- /dev/null +++ b/sparse_strips/vello_sparse_tests/snapshots/image_none_x_none_y_bilinear.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cee06af6883918cbdf131bcdda6455fd15c8f3ca9fb037b8b0fb23365ad930b +size 135 diff --git a/sparse_strips/vello_sparse_tests/snapshots/image_none_x_repeat_y.png b/sparse_strips/vello_sparse_tests/snapshots/image_none_x_repeat_y.png new file mode 100644 index 0000000000..c9180a2319 --- /dev/null +++ b/sparse_strips/vello_sparse_tests/snapshots/image_none_x_repeat_y.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2f9cd69c0b5dbae9823cc033d395efa6285e5a920e33e36395a1f3d60fba52f0 +size 148 diff --git a/sparse_strips/vello_sparse_tests/snapshots/image_repeat_x_none_y.png b/sparse_strips/vello_sparse_tests/snapshots/image_repeat_x_none_y.png new file mode 100644 index 0000000000..6940885ca7 --- /dev/null +++ b/sparse_strips/vello_sparse_tests/snapshots/image_repeat_x_none_y.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6d1397c1c5b50d7dd707ccd04ebb4c83213f59f15ae75a0f05a4b2be9408724 +size 137 diff --git a/sparse_strips/vello_sparse_tests/tests/image.rs b/sparse_strips/vello_sparse_tests/tests/image.rs index f7694e0535..8be6186fab 100644 --- a/sparse_strips/vello_sparse_tests/tests/image.rs +++ b/sparse_strips/vello_sparse_tests/tests/image.rs @@ -89,6 +89,48 @@ fn image_pad_x_pad_y(ctx: &mut impl Renderer) { repeat(ctx, Extend::Pad, Extend::Pad); } +#[vello_test] +fn image_none_x_none_y(ctx: &mut impl Renderer) { + repeat(ctx, Extend::None, Extend::None); +} + +#[vello_test] +fn image_none_x_repeat_y(ctx: &mut impl Renderer) { + repeat(ctx, Extend::None, Extend::Repeat); +} + +#[vello_test] +fn image_repeat_x_none_y(ctx: &mut impl Renderer) { + repeat(ctx, Extend::Repeat, Extend::None); +} + +fn repeat_quality(ctx: &mut impl Renderer, quality: ImageQuality) { + let rect = Rect::new(10.0, 10.0, 90.0, 90.0); + let image_source = rgb_img_10x10(ctx); + + ctx.set_paint_transform(Affine::translate((45.0, 45.0))); + ctx.set_paint(Image { + image: image_source, + sampler: ImageSampler { + x_extend: Extend::None, + y_extend: Extend::None, + quality, + alpha: 1.0, + }, + }); + ctx.fill_rect(&rect); +} + +#[vello_test] +fn image_none_x_none_y_bilinear(ctx: &mut impl Renderer) { + repeat_quality(ctx, ImageQuality::Medium); +} + +#[vello_test] +fn image_none_x_none_y_bicubic(ctx: &mut impl Renderer) { + repeat_quality(ctx, ImageQuality::High); +} + fn transform(ctx: &mut impl Renderer, transform: Affine, l: f64, t: f64, r: f64, b: f64) { let rect = Rect::new(l, t, r, b); let image_source = rgb_img_10x10(ctx); diff --git a/vello_encoding/src/encoding.rs b/vello_encoding/src/encoding.rs index 78d37cd5b9..0ce4fdd2f5 100644 --- a/vello_encoding/src/encoding.rs +++ b/vello_encoding/src/encoding.rs @@ -635,9 +635,10 @@ mod tests { assert_eq!(Extend::Pad as u32, 0); assert_eq!(Extend::Repeat as u32, 1); assert_eq!(Extend::Reflect as u32, 2); + assert_eq!(Extend::None as u32, 3); // exhaustive match to catch new variants match Extend::Pad { - Extend::Pad | Extend::Repeat | Extend::Reflect => {} + Extend::Pad | Extend::Repeat | Extend::Reflect | Extend::None => {} } } } diff --git a/vello_shaders/shader/fine.wgsl b/vello_shaders/shader/fine.wgsl index 55f615de25..fb2ab642e7 100644 --- a/vello_shaders/shader/fine.wgsl +++ b/vello_shaders/shader/fine.wgsl @@ -864,6 +864,8 @@ fn maybe_premul_alpha(pixel: vec4f, alpha_type: u32) -> vec4f { const EXTEND_PAD: u32 = 0u; const EXTEND_REPEAT: u32 = 1u; const EXTEND_REFLECT: u32 = 2u; +// Does not extend the image: samples outside the image are transparent. +const EXTEND_NONE: u32 = 3u; fn extend_mode_normalized(t: f32, mode: u32) -> f32 { switch mode { case EXTEND_PAD: { @@ -880,7 +882,9 @@ fn extend_mode_normalized(t: f32, mode: u32) -> f32 { fn extend_mode(t: f32, mode: u32, max: f32) -> f32 { switch mode { - case EXTEND_PAD: { + // For `EXTEND_NONE`, out-of-bounds positions are clamped like `EXTEND_PAD` so + // that they can be sampled safely; the resulting samples are zeroed separately. + case EXTEND_PAD, EXTEND_NONE: { return clamp(t, 0.0, max); } case EXTEND_REPEAT: { @@ -892,6 +896,21 @@ fn extend_mode(t: f32, mode: u32, max: f32) -> f32 { } } +// Like `extend_mode`, but leaves positions unmodified for `EXTEND_NONE` so that +// filtered sampling can weight out-of-bounds taps by zero. +fn extend_mode_unclamped(t: f32, mode: u32, max: f32) -> f32 { + if mode == EXTEND_NONE { + return t; + } + return extend_mode(t, mode, max); +} + +// The weight of a sample tap at position `t`: zero when the position is outside +// `[lo, hi)` and the extend mode is `EXTEND_NONE`, one otherwise. +fn tap_weight(t: f32, mode: u32, lo: f32, hi: f32) -> f32 { + return select(1.0, f32(t >= lo && t < hi), mode == EXTEND_NONE); +} + // Cubic resampler logic borrowed from Skia (same as CPU cubic_resampler function) // Mitchell-Netravali cubic filter coefficients with parameters B=1/3 and C=1/3 const MF: array, 4> = array, 4>( @@ -952,32 +971,49 @@ fn bicubic_sample( atlas_offset: vec2, atlas_max: vec2, alpha_type: u32, + extend_modes: vec2, + extents: vec2, ) -> vec4 { + let atlas_end = atlas_offset + extents; let frac_coords = fract(coords + vec2(0.5)); // Get cubic weights for x and y directions let cx = cubic_weights(frac_coords.x); let cy = cubic_weights(frac_coords.y); + // Per-tap weights that zero out-of-bounds taps for `EXTEND_NONE`. + let wx = vec4( + tap_weight(coords.x - 1.5, extend_modes.x, atlas_offset.x, atlas_end.x), + tap_weight(coords.x - 0.5, extend_modes.x, atlas_offset.x, atlas_end.x), + tap_weight(coords.x + 0.5, extend_modes.x, atlas_offset.x, atlas_end.x), + tap_weight(coords.x + 1.5, extend_modes.x, atlas_offset.x, atlas_end.x), + ); + let wy = vec4( + tap_weight(coords.y - 1.5, extend_modes.y, atlas_offset.y, atlas_end.y), + tap_weight(coords.y - 0.5, extend_modes.y, atlas_offset.y, atlas_end.y), + tap_weight(coords.y + 0.5, extend_modes.y, atlas_offset.y, atlas_end.y), + tap_weight(coords.y + 1.5, extend_modes.y, atlas_offset.y, atlas_end.y), + ); + // Sample 4x4 grid around coords - let s00 = maybe_premul_alpha(textureLoad(image_atlas, vec2(clamp(coords + vec2(-1.5, -1.5), atlas_offset, atlas_max)), 0), alpha_type); - let s10 = maybe_premul_alpha(textureLoad(image_atlas, vec2(clamp(coords + vec2(-0.5, -1.5), atlas_offset, atlas_max)), 0), alpha_type); - let s20 = maybe_premul_alpha(textureLoad(image_atlas, vec2(clamp(coords + vec2(0.5, -1.5), atlas_offset, atlas_max)), 0), alpha_type); - let s30 = maybe_premul_alpha(textureLoad(image_atlas, vec2(clamp(coords + vec2(1.5, -1.5), atlas_offset, atlas_max)), 0), alpha_type); - - let s01 = maybe_premul_alpha(textureLoad(image_atlas, vec2(clamp(coords + vec2(-1.5, -0.5), atlas_offset, atlas_max)), 0), alpha_type); - let s11 = maybe_premul_alpha(textureLoad(image_atlas, vec2(clamp(coords + vec2(-0.5, -0.5), atlas_offset, atlas_max)), 0), alpha_type); - let s21 = maybe_premul_alpha(textureLoad(image_atlas, vec2(clamp(coords + vec2(0.5, -0.5), atlas_offset, atlas_max)), 0), alpha_type); - let s31 = maybe_premul_alpha(textureLoad(image_atlas, vec2(clamp(coords + vec2(1.5, -0.5), atlas_offset, atlas_max)), 0), alpha_type); - - let s02 = maybe_premul_alpha(textureLoad(image_atlas, vec2(clamp(coords + vec2(-1.5, 0.5), atlas_offset, atlas_max)), 0), alpha_type); - let s12 = maybe_premul_alpha(textureLoad(image_atlas, vec2(clamp(coords + vec2(-0.5, 0.5), atlas_offset, atlas_max)), 0), alpha_type); - let s22 = maybe_premul_alpha(textureLoad(image_atlas, vec2(clamp(coords + vec2(0.5, 0.5), atlas_offset, atlas_max)), 0), alpha_type); - let s32 = maybe_premul_alpha(textureLoad(image_atlas, vec2(clamp(coords + vec2(1.5, 0.5), atlas_offset, atlas_max)), 0), alpha_type); - - let s03 = maybe_premul_alpha(textureLoad(image_atlas, vec2(clamp(coords + vec2(-1.5, 1.5), atlas_offset, atlas_max)), 0), alpha_type); - let s13 = maybe_premul_alpha(textureLoad(image_atlas, vec2(clamp(coords + vec2(-0.5, 1.5), atlas_offset, atlas_max)), 0), alpha_type); - let s23 = maybe_premul_alpha(textureLoad(image_atlas, vec2(clamp(coords + vec2(0.5, 1.5), atlas_offset, atlas_max)), 0), alpha_type); - let s33 = maybe_premul_alpha(textureLoad(image_atlas, vec2(clamp(coords + vec2(1.5, 1.5), atlas_offset, atlas_max)), 0), alpha_type); + let s00 = wx.x * wy.x * maybe_premul_alpha(textureLoad(image_atlas, vec2(clamp(coords + vec2(-1.5, -1.5), atlas_offset, atlas_max)), 0), alpha_type); + let s10 = wx.y * wy.x * maybe_premul_alpha(textureLoad(image_atlas, vec2(clamp(coords + vec2(-0.5, -1.5), atlas_offset, atlas_max)), 0), alpha_type); + let s20 = wx.z * wy.x * maybe_premul_alpha(textureLoad(image_atlas, vec2(clamp(coords + vec2(0.5, -1.5), atlas_offset, atlas_max)), 0), alpha_type); + let s30 = wx.w * wy.x * maybe_premul_alpha(textureLoad(image_atlas, vec2(clamp(coords + vec2(1.5, -1.5), atlas_offset, atlas_max)), 0), alpha_type); + + let s01 = wx.x * wy.y * maybe_premul_alpha(textureLoad(image_atlas, vec2(clamp(coords + vec2(-1.5, -0.5), atlas_offset, atlas_max)), 0), alpha_type); + let s11 = wx.y * wy.y * maybe_premul_alpha(textureLoad(image_atlas, vec2(clamp(coords + vec2(-0.5, -0.5), atlas_offset, atlas_max)), 0), alpha_type); + let s21 = wx.z * wy.y * maybe_premul_alpha(textureLoad(image_atlas, vec2(clamp(coords + vec2(0.5, -0.5), atlas_offset, atlas_max)), 0), alpha_type); + let s31 = wx.w * wy.y * maybe_premul_alpha(textureLoad(image_atlas, vec2(clamp(coords + vec2(1.5, -0.5), atlas_offset, atlas_max)), 0), alpha_type); + + let s02 = wx.x * wy.z * maybe_premul_alpha(textureLoad(image_atlas, vec2(clamp(coords + vec2(-1.5, 0.5), atlas_offset, atlas_max)), 0), alpha_type); + let s12 = wx.y * wy.z * maybe_premul_alpha(textureLoad(image_atlas, vec2(clamp(coords + vec2(-0.5, 0.5), atlas_offset, atlas_max)), 0), alpha_type); + let s22 = wx.z * wy.z * maybe_premul_alpha(textureLoad(image_atlas, vec2(clamp(coords + vec2(0.5, 0.5), atlas_offset, atlas_max)), 0), alpha_type); + let s32 = wx.w * wy.z * maybe_premul_alpha(textureLoad(image_atlas, vec2(clamp(coords + vec2(1.5, 0.5), atlas_offset, atlas_max)), 0), alpha_type); + + let s03 = wx.x * wy.w * maybe_premul_alpha(textureLoad(image_atlas, vec2(clamp(coords + vec2(-1.5, 1.5), atlas_offset, atlas_max)), 0), alpha_type); + let s13 = wx.y * wy.w * maybe_premul_alpha(textureLoad(image_atlas, vec2(clamp(coords + vec2(-0.5, 1.5), atlas_offset, atlas_max)), 0), alpha_type); + let s23 = wx.z * wy.w * maybe_premul_alpha(textureLoad(image_atlas, vec2(clamp(coords + vec2(0.5, 1.5), atlas_offset, atlas_max)), 0), alpha_type); + let s33 = wx.w * wy.w * maybe_premul_alpha(textureLoad(image_atlas, vec2(clamp(coords + vec2(1.5, 1.5), atlas_offset, atlas_max)), 0), alpha_type); // Interpolate in x direction for each row let row0 = cx.x * s00 + cx.y * s10 + cx.z * s20 + cx.w * s30; @@ -1322,14 +1358,18 @@ fn main( if area[i] != 0.0 { // Use pixel centers (+0.5) rather than pixel corners for correct sampling let my_xy = vec2(xy.x + f32(i) + 0.5, xy.y + 0.5); - var atlas_uv = image.matrx.xy * my_xy.x + image.matrx.zw * my_xy.y + image.xlat; + let local_uv = image.matrx.xy * my_xy.x + image.matrx.zw * my_xy.y + image.xlat; + var atlas_uv = local_uv; atlas_uv.x = extend_mode(atlas_uv.x, image.x_extend_mode, image.extents.x); atlas_uv.y = extend_mode(atlas_uv.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); + // Zero out samples outside the image for `EXTEND_NONE`. + let decal_weight = tap_weight(local_uv.x, image.x_extend_mode, 0.0, image.extents.x) + * tap_weight(local_uv.y, image.y_extend_mode, 0.0, image.extents.y); // Nearest neighbor sampling - let fg_rgba = maybe_premul_alpha(textureLoad(image_atlas, vec2(atlas_uv_clamped), 0), image.alpha_type); + let fg_rgba = decal_weight * maybe_premul_alpha(textureLoad(image_atlas, vec2(atlas_uv_clamped), 0), image.alpha_type); let fg_i = pixel_format(fg_rgba * area[i] * image.alpha, image.format); rgba[i] = rgba[i] * (1.0 - fg_i.a) + fg_i; } @@ -1342,19 +1382,27 @@ fn main( // Use pixel centers (+0.5) rather than pixel corners for correct sampling let my_xy = vec2(xy.x + f32(i) + 0.5, xy.y + 0.5); 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, image.x_extend_mode, image.extents.x); - atlas_uv.y = extend_mode(atlas_uv.y, image.y_extend_mode, image.extents.y); + // Keep `EXTEND_NONE` axes unclamped so that out-of-bounds + // taps can be weighted by zero, fading the image edge. + atlas_uv.x = extend_mode_unclamped(atlas_uv.x, image.x_extend_mode, image.extents.x); + atlas_uv.y = extend_mode_unclamped(atlas_uv.y, image.y_extend_mode, image.extents.y); atlas_uv = atlas_uv + image.atlas_offset - vec2(0.5); + let atlas_end = image.atlas_offset + image.extents; + let uv0 = floor(atlas_uv); + let uv1 = uv0 + vec2(1.0); + // Per-tap weights that zero out-of-bounds taps for `EXTEND_NONE`. + let wx0 = tap_weight(uv0.x, image.x_extend_mode, image.atlas_offset.x, atlas_end.x); + let wx1 = tap_weight(uv1.x, image.x_extend_mode, image.atlas_offset.x, atlas_end.x); + let wy0 = tap_weight(uv0.y, image.y_extend_mode, image.atlas_offset.y, atlas_end.y); + let wy1 = tap_weight(uv1.y, image.y_extend_mode, image.atlas_offset.y, atlas_end.y); // 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 cuv0 = clamp(uv0, image.atlas_offset, atlas_max); + let cuv1 = clamp(uv1, image.atlas_offset, atlas_max); let uv_frac = fract(atlas_uv); - let a = maybe_premul_alpha(textureLoad(image_atlas, vec2(uv_quad.xy), 0), image.alpha_type); - let b = maybe_premul_alpha(textureLoad(image_atlas, vec2(uv_quad.xw), 0), image.alpha_type); - let c = maybe_premul_alpha(textureLoad(image_atlas, vec2(uv_quad.zy), 0), image.alpha_type); - let d = maybe_premul_alpha(textureLoad(image_atlas, vec2(uv_quad.zw), 0), image.alpha_type); + let a = wx0 * wy0 * maybe_premul_alpha(textureLoad(image_atlas, vec2(cuv0), 0), image.alpha_type); + let b = wx0 * wy1 * maybe_premul_alpha(textureLoad(image_atlas, vec2(vec2(cuv0.x, cuv1.y)), 0), image.alpha_type); + let c = wx1 * wy0 * maybe_premul_alpha(textureLoad(image_atlas, vec2(vec2(cuv1.x, cuv0.y)), 0), image.alpha_type); + let d = wx1 * wy1 * maybe_premul_alpha(textureLoad(image_atlas, vec2(cuv1), 0), image.alpha_type); // Bilinear sampling let fg_rgba = mix(mix(a, b, uv_frac.y), mix(c, d, uv_frac.y), uv_frac.x); let fg_i = pixel_format(fg_rgba * area[i] * image.alpha, image.format); @@ -1368,10 +1416,13 @@ fn main( // Use pixel centers (+0.5) rather than pixel corners for correct sampling let my_xy = vec2(xy.x + f32(i) + 0.5, xy.y + 0.5); 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, image.x_extend_mode, image.extents.x); - atlas_uv.y = extend_mode(atlas_uv.y, image.y_extend_mode, image.extents.y); + // Keep `EXTEND_NONE` axes unclamped so that out-of-bounds + // taps can be weighted by zero, fading the image edge. + atlas_uv.x = extend_mode_unclamped(atlas_uv.x, image.x_extend_mode, image.extents.x); + atlas_uv.y = extend_mode_unclamped(atlas_uv.y, image.y_extend_mode, image.extents.y); atlas_uv = atlas_uv + image.atlas_offset; - let fg_rgba = bicubic_sample(atlas_uv, image.atlas_offset, atlas_max, image.alpha_type); + let extend_modes = vec2(image.x_extend_mode, image.y_extend_mode); + let fg_rgba = bicubic_sample(atlas_uv, image.atlas_offset, atlas_max, image.alpha_type, extend_modes, image.extents); let fg_i = pixel_format(fg_rgba * area[i] * image.alpha, image.format); rgba[i] = rgba[i] * (1.0 - fg_i.a) + fg_i; }