Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 97 additions & 8 deletions sparse_strips/vello_common/src/encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,20 @@ const DEGENERATE_THRESHOLD: f32 = 1.0e-6;
const NUDGE_VAL: f32 = 1.0e-7;
const PIXEL_CENTER_OFFSET: f64 = 0.5;

/// Sanitize a stop position so that it is finite and in the `[0.0, 1.0]` range.
///
/// This is intended as a last line of defence for inputs that bypass CSS-style validation or
/// encode semantics directly in terms of ranges.
fn sanitize_stop_position(pos: f32) -> f32 {
if pos.is_nan() {
0.0
} else if pos.is_infinite() {
if pos.is_sign_negative() { 0.0 } else { 1.0 }
} else {
pos.clamp(0.0, 1.0)
}
}

Comment on lines +44 to +53
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The following is a branchless way to do this.

Suggested change
fn sanitize_stop_position(pos: f32) -> f32 {
if pos.is_nan() {
0.0
} else if pos.is_infinite() {
if pos.is_sign_negative() { 0.0 } else { 1.0 }
} else {
pos.clamp(0.0, 1.0)
}
}
#[inline]
fn sanitize_stop_position(pos: f32) -> f32 {
pos.max(0.).min(1.)
}

f32:{max,min} don't propagate NaNs (so the NaN gets turned in to 0., if pos.max(0.) is first).

https://godbolt.org/z/nrzG75b1K

https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=3cd614c3df1900afdee1a4c481e5625a

#[cfg(feature = "std")]
fn exp(val: f32) -> f32 {
val.exp()
Expand Down Expand Up @@ -990,7 +1004,17 @@ impl<T: FromF32Color> GradientLut<T> {
let mut prev_idx = 0;

for range in ranges {
let max_idx = (range.x1 * lut_size as f32) as usize;
// Clamp the range end position to a finite value within \[0.0, 1.0\] so that the
// computed LUT index is always in-bounds and monotonic.
let pos = sanitize_stop_position(range.x1);
let scaled = (pos * lut_size as f32).floor() as usize;
let mut max_idx = scaled.min(lut_size);

// Ensure indices are monotonically increasing so we never create an empty or
// backwards range, even if the underlying stops were not sorted correctly.
if max_idx < prev_idx {
max_idx = prev_idx;
}

ramps.push((prev_idx..max_idx, range));
prev_idx = max_idx;
Expand Down Expand Up @@ -1107,16 +1131,18 @@ fn determine_lut_size(ranges: &[GradientRange]) -> usize {
};

// In case we have some tricky stops (for example 3 stops with 0.0, 0.001, 1.0), we might
// increase the resolution.
// increase the resolution. We also sanitize the positions here to defend against NaNs and
// infinities coming from upstream parsers.
let mut last_x1 = 0.0;
let mut min_size = 0;

for x1 in ranges.iter().map(|e| e.x1) {
for x1 in ranges.iter().map(|e| sanitize_stop_position(e.x1)) {
// For example, if the first stop is at 0.001, then we need a resolution of at least 1000
// so that we can still safely capture the first stop.
let res = ((1.0 / (x1 - last_x1)).ceil() as usize)
.min(MAX_GRADIENT_LUT_SIZE)
.next_power_of_two();
let delta = (x1 - last_x1).max(NUDGE_VAL);
let mut res = (1.0 / delta).ceil() as usize;
res = res.clamp(1, MAX_GRADIENT_LUT_SIZE);
let res = res.next_power_of_two();
min_size = min_size.max(res);
last_x1 = x1;
}
Expand All @@ -1134,15 +1160,17 @@ mod private {

#[cfg(test)]
mod tests {
use super::{EncodeExt, Gradient};
use super::{EncodeExt, Gradient, GradientLut, GradientRange, sanitize_stop_position};
use crate::color::DynamicColor;
use crate::color::palette::css::{BLACK, BLUE, GREEN};
use crate::kurbo::{Affine, Point};
use crate::peniko::{ColorStop, ColorStops};
use crate::peniko::{ColorStop, ColorStops, InterpolationAlphaSpace};
use alloc::vec;
use peniko::{LinearGradientPosition, RadialGradientPosition};
use smallvec::smallvec;

use crate::fearless_simd::Fallback;

#[test]
fn gradient_missing_stops() {
let mut buf = vec![];
Expand Down Expand Up @@ -1274,4 +1302,65 @@ mod tests {
GREEN.into()
);
}

#[test]
fn sanitize_stop_position_handles_non_finite_values() {
assert_eq!(sanitize_stop_position(f32::NEG_INFINITY), 0.0);
assert_eq!(sanitize_stop_position(f32::INFINITY), 1.0);
assert_eq!(sanitize_stop_position(f32::NAN), 0.0);
assert_eq!(sanitize_stop_position(-2.0), 0.0);
assert_eq!(sanitize_stop_position(2.0), 1.0);
}

#[test]
fn gradient_lut_handles_infinite_stop_offset() {
let simd = Fallback::new();

let ranges = vec![
GradientRange {
x1: 0.0,
bias: [0.0; 4],
scale: [0.0; 4],
interpolation_alpha_space: InterpolationAlphaSpace::Premultiplied,
},
GradientRange {
x1: f32::INFINITY,
bias: [0.0; 4],
scale: [0.0; 4],
interpolation_alpha_space: InterpolationAlphaSpace::Premultiplied,
},
];

let lut = GradientLut::<u8>::new(simd, &ranges, false);
assert!(lut.width() > 0);
}

#[test]
fn gradient_lut_handles_unsorted_and_out_of_range_offsets() {
let simd = Fallback::new();

let ranges = vec![
GradientRange {
x1: 0.75,
bias: [0.0; 4],
scale: [0.0; 4],
interpolation_alpha_space: InterpolationAlphaSpace::Premultiplied,
},
GradientRange {
x1: -1.0,
bias: [0.0; 4],
scale: [0.0; 4],
interpolation_alpha_space: InterpolationAlphaSpace::Premultiplied,
},
GradientRange {
x1: 2.0,
bias: [0.0; 4],
scale: [0.0; 4],
interpolation_alpha_space: InterpolationAlphaSpace::Premultiplied,
},
];

let lut = GradientLut::<f32>::new(simd, &ranges, false);
assert!(lut.width() > 0);
}
}