Skip to content
Closed
Show file tree
Hide file tree
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
2 changes: 1 addition & 1 deletion sparse_strips/vello_bench/src/fine/rounded_blurred_rect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ fn base<S: Simd, N: FineKernel<S>>(b: &mut Bencher<'_>, fine: &mut Fine<S, N>, t
let rect = BlurredRoundedRectangle {
rect: Rect::new(0.0, 0.0, BENCH_WIDTH as f64, Tile::HEIGHT as f64),
color: GREEN,
radius: 30.0,
radii: 30.0.into(),
std_dev: 10.0,
invert: false,
};
Expand Down
9 changes: 6 additions & 3 deletions sparse_strips/vello_common/src/blurred_rounded_rect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

//! Blurred, rounded rectangles.
use crate::color::{AlphaColor, Srgb};
use crate::kurbo::Rect;
use crate::kurbo::{Rect, RoundedRectRadii};

/// A blurred, rounded rectangle.
#[derive(Debug)]
Expand All @@ -12,8 +12,11 @@ pub struct BlurredRoundedRectangle {
pub rect: Rect,
/// The color of the blurred rectangle.
pub color: AlphaColor<Srgb>,
/// The radius of the rounded rectangle's corners.
pub radius: f32,
/// The radii of the rounded rectangle's corners.
///
/// Each corner may have a different radius. Note that only scalar (circular) corner
/// radii are supported; elliptical radii are not.
pub radii: RoundedRectRadii,
/// The standard deviation of the blur effect.
pub std_dev: f32,
/// Whether to paint the inverse (`1 - alpha`) of the blur coverage.
Expand Down
39 changes: 27 additions & 12 deletions sparse_strips/vello_common/src/encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -856,12 +856,14 @@ pub struct GradientRange {
}

/// An encoded blurred, rounded rectangle.
///
/// Per-corner values are stored in the order `[top-left, top-right, bottom-left, bottom-right]`.
#[derive(Debug)]
pub struct EncodedBlurredRoundedRectangle {
/// An component for computing the blur effect.
pub exponent: f32,
/// An component for computing the blur effect.
pub recip_exponent: f32,
/// A per-corner component for computing the blur effect.
pub exponent: [f32; 4],
/// A per-corner component for computing the blur effect.
pub recip_exponent: [f32; 4],
/// An component for computing the blur effect.
pub scale: f32,
/// An component for computing the blur effect.
Expand All @@ -876,8 +878,8 @@ pub struct EncodedBlurredRoundedRectangle {
pub width: f32,
/// An component for computing the blur effect.
pub height: f32,
/// An component for computing the blur effect.
pub r1: f32,
/// A per-corner component for computing the blur effect.
pub r1: [f32; 4],
/// Whether to paint the inverse (`1 - alpha`) of the blur coverage.
///
/// When `true`, the paint is fully opaque outside the blurred rectangle and fades to
Expand Down Expand Up @@ -923,17 +925,30 @@ impl EncodeExt for BlurredRoundedRectangle {

let width = rect.width() as f32;
let height = rect.height() as f32;
let radius = self.radius.min(0.5 * width.min(height));

// To avoid divide by 0; potentially should be a bigger number for antialiasing.
let std_dev = self.std_dev.max(1e-6);

let min_edge = width.min(height);
let rmax = 0.5 * min_edge;
let r0 = radius.hypot(std_dev * 1.15).min(rmax);
let r1 = radius.hypot(std_dev * 2.0).min(rmax);

let exponent = 2.0 * r1 / r0;
// Per-corner radii, in the order [top-left, top-right, bottom-left, bottom-right].
let radii = [
self.radii.top_left,
self.radii.top_right,
self.radii.bottom_left,
self.radii.bottom_right,
]
.map(|r| (r as f32).clamp(0.0, rmax));

let r1 = radii.map(|radius| radius.hypot(std_dev * 2.0).min(rmax));
let mut exponent = [0.0; 4];
let mut recip_exponent = [0.0; 4];
for i in 0..4 {
let r0 = radii[i].hypot(std_dev * 1.15).min(rmax);
exponent[i] = 2.0 * r1[i] / r0;
recip_exponent[i] = exponent[i].recip();
}

let std_dev_inv = std_dev.recip();

Expand All @@ -945,8 +960,8 @@ impl EncodeExt for BlurredRoundedRectangle {
let w = width + delta.min(0.0);
let h = height - delta.max(0.0);

let recip_exponent = exponent.recip();
let scale = 0.5 * compute_erf7(std_dev_inv * 0.5 * (w.max(h) - 0.5 * radius));
let mean_radius = 0.25 * (radii[0] + radii[1] + radii[2] + radii[3]);
let scale = 0.5 * compute_erf7(std_dev_inv * 0.5 * (w.max(h) - 0.5 * mean_radius));

let encoded = EncodedBlurredRoundedRectangle {
exponent,
Expand Down
4 changes: 4 additions & 0 deletions sparse_strips/vello_cpu/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ Subheadings to categorize changes are `added, changed, deprecated, removed, fixe

This release has an [MSRV][] of 1.88.

### Changed

- Breaking change: `RenderContext::fill_blurred_rounded_rect` now takes a `RoundedRectRadii`, allowing a different radius per corner. A uniform radius can be passed as `radius.into()`.

## [0.1.0][] - 2026-07-29

This release has an [MSRV][] of 1.88.
Expand Down
63 changes: 40 additions & 23 deletions sparse_strips/vello_cpu/src/fine/common/rounded_blurred_rect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,17 +169,34 @@ impl<S: Simd> Iterator for AlphaCalculator<S> {
let r = &self.r;

let y = j - r.v1 * r.height;
// Equivalent to r.r1 + y.abs() - (r.h * r.v1)
let y0 = r.r1 - r.h.mul_sub(r.v1, y.abs());
let x = i - r.v1 * r.width;

// Select the parameters of the corner in whose quadrant the pixel lies.
// Per-corner values are stored as [top-left, top-right, bottom-left, bottom-right].
let simd = self.simd;
let right = simd.simd_ge_f32x8(x, r.v0);
let bottom = simd.simd_ge_f32x8(y, r.v0);
let select_corner = |v: &[f32x8<S>; 4]| {
simd.select_f32x8(
bottom,
simd.select_f32x8(right, v[3], v[2]),
simd.select_f32x8(right, v[1], v[0]),
)
};
let r1 = select_corner(&r.r1);
let exponent = select_corner(&r.exponent);
let recip_exponent = select_corner(&r.recip_exponent);

// Equivalent to r1 + y.abs() - (r.h * r.v1)
let y0 = r1 - r.h.mul_sub(r.v1, y.abs());
let y1 = y0.max(r.v0);

let x = i - r.v1 * r.width;
// Equivalent to r.r1 + x.abs() - (r.w * r.v1)
let x0 = r.r1 - r.w.mul_sub(r.v1, x.abs());
// Equivalent to r1 + x.abs() - (r.w * r.v1)
let x0 = r1 - r.w.mul_sub(r.v1, x.abs());
let x1 = x0.max(r.v0);
let d_pos = (x1.powf(r.exponent) + y1.powf(r.exponent)).powf(r.recip_exponent);
let d_pos = (x1.powf(exponent) + y1.powf(exponent)).powf(recip_exponent);
let d_neg = x0.max(y0).min(r.v0);
let d = d_pos + d_neg - r.r1;
let d = d_pos + d_neg - r1;
let z = r.scale
* (f32x8::compute_erf7(self.simd, r.std_dev_inv * (r.min_edge + d))
- f32x8::compute_erf7(self.simd, r.std_dev_inv * d));
Expand All @@ -192,16 +209,16 @@ impl<S: Simd> Iterator for AlphaCalculator<S> {

#[derive(Debug)]
struct SimdRoundedBlurredRect<S: Simd> {
pub exponent: f32,
pub recip_exponent: f32,
pub exponent: [f32x8<S>; 4],
pub recip_exponent: [f32x8<S>; 4],
pub scale: f32x8<S>,
pub std_dev_inv: f32x8<S>,
pub min_edge: f32x8<S>,
pub w: f32x8<S>,
pub h: f32x8<S>,
pub width: f32x8<S>,
pub height: f32x8<S>,
pub r1: f32x8<S>,
pub r1: [f32x8<S>; 4],
pub v0: f32x8<S>,
pub v1: f32x8<S>,
}
Expand All @@ -215,9 +232,9 @@ impl<S: Simd> SimdRoundedBlurredRect<S> {
let w = f32x8::splat(s, encoded.w);
let width = f32x8::splat(s, encoded.width);
let height = f32x8::splat(s, encoded.height);
let r1 = f32x8::splat(s, encoded.r1);
let exponent = encoded.exponent;
let recip_exponent = encoded.recip_exponent;
let r1 = encoded.r1.map(|v| f32x8::splat(s, v));
let exponent = encoded.exponent.map(|v| f32x8::splat(s, v));
let recip_exponent = encoded.recip_exponent.map(|v| f32x8::splat(s, v));
let scale = f32x8::splat(s, encoded.scale);
let min_edge = f32x8::splat(s, encoded.min_edge);
let std_dev_inv = f32x8::splat(s, encoded.std_dev_inv);
Expand Down Expand Up @@ -248,7 +265,7 @@ trait FloatExt<S: Simd> {
// explanation of this approximation to the erf function.
/// Approximate the erf function.
fn compute_erf7(simd: S, x: Self) -> Self;
fn powf(self, x: f32) -> Self;
fn powf(self, x: Self) -> Self;
}

impl<S: Simd> FloatExt<S> for f32x8<S> {
Expand All @@ -269,16 +286,16 @@ impl<S: Simd> FloatExt<S> for f32x8<S> {
}

#[inline]
fn powf(mut self, x: f32) -> Self {
fn powf(mut self, x: Self) -> Self {
// TODO: SIMD
self[0] = self[0].powf(x);
self[1] = self[1].powf(x);
self[2] = self[2].powf(x);
self[3] = self[3].powf(x);
self[4] = self[4].powf(x);
self[5] = self[5].powf(x);
self[6] = self[6].powf(x);
self[7] = self[7].powf(x);
self[0] = self[0].powf(x[0]);
self[1] = self[1].powf(x[1]);
self[2] = self[2].powf(x[2]);
self[3] = self[3].powf(x[3]);
self[4] = self[4].powf(x[4]);
self[5] = self[5].powf(x[5]);
self[6] = self[6].powf(x[6]);
self[7] = self[7].powf(x[7]);

self
}
Expand Down
11 changes: 7 additions & 4 deletions sparse_strips/vello_cpu/src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use vello_common::encode::{EncodeExt, EncodedPaint};
use vello_common::fearless_simd::Level;
use vello_common::filter::FilterData;
use vello_common::filter_effects::Filter;
use vello_common::kurbo::{Affine, BezPath, Rect, Stroke};
use vello_common::kurbo::{Affine, BezPath, Rect, RoundedRectRadii, Stroke};
use vello_common::mask::Mask;
use vello_common::paint::{ImageId, ImageResolver, Paint, PaintType, Tint};
use vello_common::peniko::color::palette::css::BLACK;
Expand Down Expand Up @@ -383,7 +383,10 @@ impl RenderContext {
self.temp_path.push(PathEl::ClosePath);
}

/// Fill a blurred rectangle with the given corner radius and standard deviation.
/// Fill a blurred rectangle with the given corner radii and standard deviation.
///
/// Each corner may have a different (scalar) radius; elliptical corner radii are not
/// supported.
///
/// When `invert` is `true`, the inverse (`1 - alpha`) of the blur coverage is painted: the
/// paint is fully opaque outside the blurred rectangle and fades to transparent inside it. This
Expand All @@ -394,7 +397,7 @@ impl RenderContext {
pub fn fill_blurred_rounded_rect(
&mut self,
rect: &Rect,
radius: f32,
radii: RoundedRectRadii,
std_dev: f32,
invert: bool,
) {
Expand All @@ -408,7 +411,7 @@ impl RenderContext {
let blurred_rect = BlurredRoundedRectangle {
rect,
color,
radius,
radii,
std_dev,
invert,
};
Expand Down
26 changes: 18 additions & 8 deletions sparse_strips/vello_example_scenes/src/blurred_rounded_rect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

//! Scene demonstrating blurred rounded rectangle rendering.

use vello_common::kurbo::{Affine, Rect};
use vello_common::kurbo::{Affine, Rect, RoundedRectRadii};
use vello_common::peniko::color::palette;
use vello_common::peniko::color::{AlphaColor, Srgb};

Expand Down Expand Up @@ -53,7 +53,7 @@ pub fn render(ctx: &mut impl RenderingContext, root_transform: Affine) {
scene_transform * Affine::translate((300.0, 300.0)),
rect,
palette::css::BLUE,
50.0,
50.0.into(),
45.0,
);

Expand All @@ -64,7 +64,7 @@ pub fn render(ctx: &mut impl RenderingContext, root_transform: Affine) {
* Affine::skew(20_f64.to_radians().tan(), 0.0),
rect,
palette::css::BLACK,
50.0,
50.0.into(),
45.0,
);

Expand All @@ -73,7 +73,7 @@ pub fn render(ctx: &mut impl RenderingContext, root_transform: Affine) {
scene_transform,
Rect::new(100.0, 800.0, 400.0, 1100.0),
palette::css::BLACK,
150.0,
150.0.into(),
45.0,
);

Expand All @@ -82,7 +82,17 @@ pub fn render(ctx: &mut impl RenderingContext, root_transform: Affine) {
scene_transform,
Rect::new(600.0, 800.0, 900.0, 900.0),
palette::css::BLACK,
150.0,
150.0.into(),
45.0,
);

// Non-uniform corner radii.
draw_blurred_rect(
ctx,
scene_transform * Affine::translate((900.0, 1000.0)),
rect,
palette::css::BLACK,
RoundedRectRadii::new(120.0, 0.0, 60.0, 20.0),
45.0,
);

Expand All @@ -91,7 +101,7 @@ pub fn render(ctx: &mut impl RenderingContext, root_transform: Affine) {
scene_transform * Affine::translate((600.0, 600.0)) * Affine::scale_non_uniform(2.2, 0.9),
rect,
palette::css::BLACK,
50.0,
50.0.into(),
30.0,
);
}
Expand All @@ -101,10 +111,10 @@ fn draw_blurred_rect(
transform: Affine,
rect: Rect,
color: AlphaColor<Srgb>,
radius: f32,
radii: RoundedRectRadii,
std_dev: f32,
) {
ctx.set_transform(transform);
ctx.set_paint(color);
ctx.fill_blurred_rounded_rect(&rect, radius, std_dev);
ctx.fill_blurred_rounded_rect(&rect, radii, std_dev);
}
12 changes: 6 additions & 6 deletions sparse_strips/vello_example_scenes/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pub mod text;
use glifo::GlyphRunBackend;
use vello_common::filter_effects::Filter;
use vello_common::kurbo::Affine;
pub use vello_common::kurbo::{BezPath, Rect, Shape, Stroke};
pub use vello_common::kurbo::{BezPath, Rect, RoundedRectRadii, Shape, Stroke};
pub use vello_common::mask::Mask;
use vello_common::paint::ImageSource;
pub use vello_common::paint::{Paint, PaintType};
Expand Down Expand Up @@ -81,7 +81,7 @@ pub trait RenderingContext: Sized {
/// Fill a rectangle with the current paint.
fn fill_rect(&mut self, rect: &Rect);
/// Fill a blurred rounded rectangle with the current solid paint.
fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radius: f32, std_dev: f32);
fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radii: RoundedRectRadii, std_dev: f32);
/// Create a glyph run builder for text rendering.
fn glyph_run<'a>(
&'a mut self,
Expand Down Expand Up @@ -172,8 +172,8 @@ impl RenderingContext for RenderContext {
self.fill_rect(rect);
}

fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radius: f32, std_dev: f32) {
self.fill_blurred_rounded_rect(rect, radius, std_dev, false);
fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radii: RoundedRectRadii, std_dev: f32) {
self.fill_blurred_rounded_rect(rect, radii, std_dev, false);
}

fn glyph_run<'a>(
Expand Down Expand Up @@ -277,8 +277,8 @@ impl RenderingContext for Scene {
self.fill_rect(rect);
}

fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radius: f32, std_dev: f32) {
self.fill_blurred_rounded_rect(rect, radius, std_dev, false);
fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radii: RoundedRectRadii, std_dev: f32) {
self.fill_blurred_rounded_rect(rect, radii, std_dev, false);
}

fn glyph_run<'a>(
Expand Down
1 change: 1 addition & 0 deletions sparse_strips/vello_hybrid/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ This release has an [MSRV][] of 1.88.
### Changed

- Breaking change: `WebGlRenderer::render` and `WebGlRenderer::render_to_atlas` now take a `WebGlTextureBindings`, providing the external textures referenced by the scene. Pass `&WebGlTextureBindings::new()` if the scene does not use any.
- Breaking change: `Scene::fill_blurred_rounded_rect` now takes a `RoundedRectRadii`, allowing a different radius per corner. A uniform radius can be passed as `radius.into()`.

## [0.1.0][] - 2026-07-29

Expand Down
Loading
Loading