Align image rasterization more with Skia implementation (#1029)
On the one hand, to make it more SIMD-compatible, but on the other hand
for some nice performance improvements already!
```
Before:
fine/image/transform/none_u8: 893.79 ns
fine/image/transform/none_f32: 901.79 ns
fine/image/extend/pad_u8: 902.01 ns
fine/image/extend/pad_f32: 947.86 ns
fine/image/extend/repeat_u8: 2.2883 µs
fine/image/extend/repeat_f32: 2.3449 µs
fine/image/extend/reflect_u8: 2.3390 µs
fine/image/extend/reflect_f32: 2.4345 µs
After:
fine/image/transform/none_u8_scalar: 438.70 ns
fine/image/transform/none_f32_scalar: 626.66 ns
fine/image/extend/pad_u8_scalar: 464.97 ns
fine/image/extend/pad_f32_scalar: 801.11 ns
fine/image/extend/repeat_u8_scalar: 500.76 ns
fine/image/extend/repeat_f32_scalar: 682.00 ns
fine/image/extend/reflect_u8_scalar: 876.11 ns
fine/image/extend/reflect_f32_scalar: 1.1661 µs
```
diff --git a/sparse_strips/vello_bench/src/fine/image.rs b/sparse_strips/vello_bench/src/fine/image.rs
index 74b99d9..21fcca1 100644
--- a/sparse_strips/vello_bench/src/fine/image.rs
+++ b/sparse_strips/vello_bench/src/fine/image.rs
@@ -13,6 +13,7 @@
use vello_common::pixmap::Pixmap;
use vello_cpu::fine::{Fine, FineType};
+// TODO: Add benchmarks for images with transparency
pub fn image(c: &mut Criterion) {
transform::none(c);
transform::scale(c);
diff --git a/sparse_strips/vello_common/src/encode.rs b/sparse_strips/vello_common/src/encode.rs
index fd6ab7b..f842b0c 100644
--- a/sparse_strips/vello_common/src/encode.rs
+++ b/sparse_strips/vello_common/src/encode.rs
@@ -438,7 +438,9 @@
fn encode_into(&self, paints: &mut Vec<EncodedPaint>, transform: Affine) -> Paint {
let idx = paints.len();
- let transform = transform.inverse();
+ // Similarly to gradients, apply a 0.5 offset so we sample at the center of
+ // a pixel.
+ let transform = transform.inverse() * Affine::translate((0.5, 0.5));
// TODO: This is somewhat expensive for large images, maybe it's not worth optimizing
// non-opaque images in the first place..
let has_opacities = self.pixmap.data().iter().any(|pixel| pixel.a != 255);
diff --git a/sparse_strips/vello_cpu/src/fine/image.rs b/sparse_strips/vello_cpu/src/fine/image.rs
index a455d5e..0cd3964 100644
--- a/sparse_strips/vello_cpu/src/fine/image.rs
+++ b/sparse_strips/vello_cpu/src/fine/image.rs
@@ -11,39 +11,43 @@
use vello_common::kurbo::common::FloatFuncs as _;
#[cfg(feature = "std")]
-fn fract(val: f64) -> f64 {
- val.fract()
+fn floor(val: f32) -> f32 {
+ val.floor()
}
#[cfg(not(feature = "std"))]
-fn fract(val: f64) -> f64 {
+fn floor(val: f32) -> f32 {
#[cfg(feature = "libm")]
- return val - libm::trunc(val);
+ return libm::floorf(val);
#[cfg(not(feature = "libm"))]
compile_error!("vello_common requires either the `std` or `libm` feature");
}
-// Inlined version of f32::rem_euclid, to allow using abs.
-fn rem_euclid(lhs: f32, rhs: f32) -> f32 {
- let r = lhs % rhs;
- if r < 0.0 { r + rhs.abs() } else { r }
-}
-
#[derive(Debug)]
pub(crate) struct ImageFiller<'a> {
/// The current position that should be processed.
cur_pos: Point,
/// The underlying image.
image: &'a EncodedImage,
+ // Precomputed values reused in per-pixel calculations.
+ height: f32,
+ height_inv: f32,
+ width: f32,
+ width_inv: f32,
}
impl<'a> ImageFiller<'a> {
pub(crate) fn new(image: &'a EncodedImage, start_x: u16, start_y: u16) -> Self {
+ let width = image.pixmap.width() as f32;
+ let height = image.pixmap.height() as f32;
+
Self {
- // We want to sample values of the pixels at the center, so add an offset of 0.5.
- cur_pos: image.transform
- * Point::new(f64::from(start_x) + 0.5, f64::from(start_y) + 0.5),
+ cur_pos: image.transform * Point::new(f64::from(start_x), f64::from(start_y)),
image,
+ width,
+ height,
+ width_inv: 1.0 / width,
+ height_inv: 1.0 / height,
}
}
@@ -78,11 +82,10 @@
for (idx, pos) in y_positions.iter_mut().enumerate() {
*pos = extend(
- // Since we already added a 0.5 offset to sample at the center of the pixel,
- // we always floor to get the target pixel.
- (self.cur_pos.y + y_advance * idx as f64).floor() as f32,
+ (self.cur_pos.y + y_advance * idx as f64) as f32,
self.image.extends.1,
- f32::from(self.image.pixmap.height()),
+ self.height,
+ self.height_inv,
);
}
@@ -90,10 +93,10 @@
.chunks_exact_mut(TILE_HEIGHT_COMPONENTS)
.for_each(|column| {
let extended_x_pos = extend(
- // As above, always floor.
- x_pos.floor() as f32,
+ x_pos as f32,
self.image.extends.0,
- f32::from(self.image.pixmap.width()),
+ self.width,
+ self.width_inv,
);
self.run_simple_column(column, extended_x_pos, &y_positions);
x_pos += x_advance;
@@ -133,16 +136,17 @@
fn run_complex_column<F: FineType>(&mut self, col: &mut [F]) {
let extend_point = |mut point: Point| {
- // For the same reason as mentioned above, we always floor.
point.x = f64::from(extend(
- point.x.floor() as f32,
+ point.x as f32,
self.image.extends.0,
- f32::from(self.image.pixmap.width()),
+ self.width,
+ self.width_inv,
));
point.y = f64::from(extend(
- point.y.floor() as f32,
+ point.y as f32,
self.image.extends.1,
- f32::from(self.image.pixmap.height()),
+ self.height,
+ self.height_inv,
));
point
@@ -176,32 +180,12 @@
// center of the location we are sampling, and sample those points
// using a cubic filter to weight each location's contribution.
- let fract = |orig_val: f64| {
- // To give some intuition on why we need that shift, based on bilinear
- // filtering: If we sample at the position (0.5, 0.5), we are at the center
- // of the pixel and thus only want the color of the current pixel. Thus, we take
- // 1.0 * 1.0 from the top left pixel (which still lies on our pixel)
- // and 0.0 from all other corners (which lie at the start of other pixels).
- //
- // If we sample at the position (0.4, 0.4), we want 0.1 * 0.1 = 0.01 from
- // the top-left pixel, 0.1 * 0.9 = 0.09 from the bottom-left and top-right,
- // and finally 0.9 * 0.9 = 0.81 from the bottom right position (which still
- // lies on our pixel, and thus has intuitively should have the highest
- // contribution). Thus, we need to subtract 0.5 from the position to get
- // the correct fractional contribution.
- let start = orig_val - 0.5;
- let mut res = fract(start) as f32;
+ fn fract(val: f32) -> f32 {
+ val - floor(val)
+ }
- // In case we are in the negative we need to mirror the result.
- if res.is_sign_negative() {
- res += 1.0;
- }
-
- res
- };
-
- let x_fract = fract(pos.x);
- let y_fract = fract(pos.y);
+ let x_fract = fract(pos.x as f32 + 0.5);
+ let y_fract = fract(pos.y as f32 + 0.5);
let mut interpolated_color = [0.0_f32; 4];
@@ -213,6 +197,7 @@
};
if self.image.quality == ImageQuality::Medium {
+ // <https://github.com/google/skia/blob/220738774f7a0ce4a6c7bd17519a336e5e5dea5b/src/opts/SkRasterPipeline_opts.h#L5039-L5078>
let cx = [1.0 - x_fract, x_fract];
let cy = [1.0 - y_fract, y_fract];
@@ -234,7 +219,7 @@
}
}
} else {
- // Compare to https://github.com/google/skia/blob/84ff153b0093fc83f6c77cd10b025c06a12c5604/src/opts/SkRasterPipeline_opts.h#L5030-L5075.
+ // Compare to <https://github.com/google/skia/blob/84ff153b0093fc83f6c77cd10b025c06a12c5604/src/opts/SkRasterPipeline_opts.h#L5030-L5075>.
let cx = weights(x_fract);
let cy = weights(y_fract);
@@ -278,21 +263,31 @@
}
}
-fn extend(val: f32, extend: Extend, max: f32) -> f32 {
+#[inline(always)]
+fn extend(val: f32, extend: Extend, max: f32, inv_max: f32) -> f32 {
+ // We cannot chose f32::EPSILON here because for example 30.0 - f32::EPSILON is still 30.0.
+ // This bias should be large enough for all numbers that we support (i.e. <= u16::MAX).
+ const BIAS: f32 = 0.01;
+
match extend {
- Extend::Pad => val.clamp(0.0, max - 1.0),
- // TODO: We need to make repeat and reflect more efficient and branch-less.
- Extend::Repeat => rem_euclid(val, max),
+ // Note that max should be exclusive, so subtract a small bias to enforce that.
+ // Otherwise, we might sample out-of-bounds pixels.
+ Extend::Pad => val.clamp(0.0, max - BIAS),
+ Extend::Repeat => val - floor(val * inv_max) * max,
+ // <https://github.com/google/skia/blob/220738774f7a0ce4a6c7bd17519a336e5e5dea5b/src/opts/SkRasterPipeline_opts.h#L3274-L3290>
Extend::Reflect => {
- let period = 2.0 * max;
+ let u = val - floor(val * inv_max * 0.5) * 2.0 * max;
+ let s = floor(u * inv_max);
+ let m = u - 2.0 * s * (u - max);
- let val_mod = rem_euclid(val, period);
+ let bias_in_ulps = s.trunc();
- if val_mod < max {
- val_mod
- } else {
- (period - 1.0) - val_mod
- }
+ let m_bits = m.to_bits();
+ // This would yield NaN if `m` is 0 and `bias_in_ulps` > 0, but since
+ // our `max` is always an integer number, u and s must also be an integer number
+ // and thus `m_bits` must be 0.
+ let biased_bits = m_bits.wrapping_sub(bias_in_ulps as u32);
+ f32::from_bits(biased_bits)
}
}
}
diff --git a/sparse_strips/vello_sparse_tests/snapshots/glyphs_bitmap_apple.png b/sparse_strips/vello_sparse_tests/snapshots/glyphs_bitmap_apple.png
index 574d518..3346eba 100644
--- a/sparse_strips/vello_sparse_tests/snapshots/glyphs_bitmap_apple.png
+++ b/sparse_strips/vello_sparse_tests/snapshots/glyphs_bitmap_apple.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:99e38374f05358bef1fbd667d6804aec2cae31770d8597a027aaf75a444fcce5
-size 13994
+oid sha256:88b2d132ac8dfee79861b7078bef9678fcfd36b5cdb338e4b1342acfd73f78d7
+size 14012
diff --git a/sparse_strips/vello_sparse_tests/snapshots/image_bicubic_with_rotation.png b/sparse_strips/vello_sparse_tests/snapshots/image_bicubic_with_rotation.png
index dcb9c1b..05ec975 100644
--- a/sparse_strips/vello_sparse_tests/snapshots/image_bicubic_with_rotation.png
+++ b/sparse_strips/vello_sparse_tests/snapshots/image_bicubic_with_rotation.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f64982ff9b2b4668f4f11b851b7dd8f11d1ab994604cb90abe6620cb35083df5
-size 12719
+oid sha256:1fa5377b73650912f2653208cd49340705bc9a1b522915e7f0c4c44533052d06
+size 12716
diff --git a/sparse_strips/vello_sparse_tests/snapshots/image_bicubic_with_translation.png b/sparse_strips/vello_sparse_tests/snapshots/image_bicubic_with_translation.png
index dbddf1c..006c508 100644
--- a/sparse_strips/vello_sparse_tests/snapshots/image_bicubic_with_translation.png
+++ b/sparse_strips/vello_sparse_tests/snapshots/image_bicubic_with_translation.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1911f5291e5f90a032c50f7a7a4f687216b88aa65793dc12c170b8b7ab6be626
-size 648
+oid sha256:52e9abf295f3a8e083c685dc8a41f4f39623f6b8b69b9a1c5f6ba3ed9287032c
+size 644