use valo_geometry::{Matrix, Point, Rect};
use valo_dl::TileMode;
use super::filter_passes::FilterPasses;
use super::shading::DownsampleKernel;
use super::snapshot::{Placement, Snapshot};
const MAX_SIGMA: f32 = 500.0;
const EH_CLOSE_ENOUGH: f32 = 1e-3;
const KERNEL_RADIUS_PER_SIGMA: f32 = 1.732_050_8;
pub(crate) const MAX_KERNEL_SAMPLES: usize = 50;
const MAX_UNMERGED_SAMPLES: usize = MAX_KERNEL_SAMPLES * 2;
pub(super) fn scale_sigma(sigma: f32) -> f32 {
let clamped = sigma.min(MAX_SIGMA);
const A: f32 = 3.4e-6;
const B: f32 = -3.4e-3;
const C: f32 = 1.0;
let scalar = C + B * clamped + A * clamped * clamped;
clamped * scalar
}
pub(super) fn basis_of(mapping: &Matrix) -> [f32; 4] {
let [a, b, c, d, ..] = mapping.to_affine();
[a, b, c, d]
}
pub(super) fn extract_scale(transform: &Matrix) -> [f32; 2] {
let [a, b, c, d] = basis_of(transform);
[a.hypot(b), c.hypot(d)]
}
fn device_sigma(basis: [f32; 4], sigma_x: f32, sigma_y: f32) -> [f32; 2] {
let [a, b, c, d] = basis;
[
(sigma_x * a + sigma_y * c).abs(),
(sigma_x * b + sigma_y * d).abs(),
]
}
pub(super) fn skia_sigma(basis: [f32; 4], sigma_x: f32, sigma_y: f32) -> [f32; 2] {
let [a, b, c, d] = basis;
[sigma_x * a.hypot(b), sigma_y * c.hypot(d)]
}
pub(super) fn blur_source_coverage(
effect_transform: &Matrix,
sigma: [f32; 2],
output_limit: &Rect,
) -> Rect {
let radius = sigma.map(|sigma| blur_radius(scale_sigma(sigma)));
let radii = device_sigma(basis_of(effect_transform), radius[0], radius[1]);
expanded(output_limit, radii)
}
pub(super) fn blur_radius(sigma: f32) -> f32 {
if sigma > 0.5 {
(sigma - 0.5) * KERNEL_RADIUS_PER_SIGMA
} else {
0.0
}
}
fn calculate_scale(sigma: f32) -> f32 {
if sigma <= 4.0 {
return 1.0;
}
let raw_result = 4.0 / sigma;
let exponent = raw_result.log2().round().max(-4.0);
let rounded = exponent.exp2();
if rounded < 0.125 {
let rounded_plus = (exponent + 1.0).exp2();
let kernel_size_plus = scale_blur_radius(blur_radius(sigma), rounded_plus) * 2 + 1;
if kernel_size_plus <= 41 {
return rounded_plus;
}
}
rounded
}
fn scale_blur_radius(radius: f32, scalar: f32) -> i32 {
(radius * scalar).round() as i32
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(super) struct BlurInfo {
pub scaled_sigma: [f32; 2],
blur_radius: [f32; 2],
padding: [f32; 2],
local_padding: [f32; 2],
}
impl BlurInfo {
pub fn calculate(entity: &Matrix, effect_transform: &Matrix, sigma: [f32; 2]) -> Self {
let scalar = extract_scale(entity);
let source = [0, 1].map(|axis| scalar[axis] * scale_sigma(sigma[axis]));
let mut info = Self::of(device_sigma(
basis_of(effect_transform),
source[0],
source[1],
));
info.local_padding = [0, 1].map(|axis| (scalar[axis] * info.padding[axis]).abs());
info
}
pub fn of(scaled_sigma: [f32; 2]) -> Self {
let scaled_sigma = scaled_sigma.map(|sigma| sigma.clamp(0.0, MAX_SIGMA));
let blur_radius = scaled_sigma.map(blur_radius);
let padding = blur_radius.map(f32::ceil);
Self {
scaled_sigma,
blur_radius,
padding,
local_padding: padding,
}
}
pub fn in_texels(&self, texel_to_tree: &Matrix) -> Self {
let texel = extract_scale(texel_to_tree);
Self::of([0, 1].map(|axis| self.scaled_sigma[axis] / texel[axis]))
}
pub fn is_negligible(&self) -> bool {
self.scaled_sigma[0] < EH_CLOSE_ENOUGH && self.scaled_sigma[1] < EH_CLOSE_ENOUGH
}
pub fn radius(&self) -> [f32; 2] {
self.blur_radius
}
pub fn local_padding(&self) -> [f32; 2] {
self.local_padding
}
fn downsample_scale(&self) -> f32 {
calculate_scale(self.scaled_sigma[0]).min(calculate_scale(self.scaled_sigma[1]))
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
struct KernelSample {
uv_offset: [f32; 2],
coefficient: f32,
}
struct BlurParameters {
blur_uv_offset: [f32; 2],
blur_sigma: f32,
blur_radius: i32,
step_size: i32,
}
impl BlurParameters {
fn along(axis: usize, blur: &BlurInfo, args: &DownsamplePassArgs) -> Self {
let scalar = args.effective_scalar[axis];
let mut blur_uv_offset = [0.0; 2];
blur_uv_offset[axis] = 1.0 / args.subpass_size[axis] as f32;
Self {
blur_uv_offset,
blur_sigma: blur.scaled_sigma[axis] * scalar,
blur_radius: scale_blur_radius(blur.blur_radius[axis], scalar),
step_size: 1,
}
}
fn is_negligible(&self) -> bool {
self.blur_sigma < EH_CLOSE_ENOUGH
}
}
fn generate_blur_info(parameters: &BlurParameters) -> Vec<KernelSample> {
let mut sample_count = ((2 * parameters.blur_radius) / parameters.step_size) + 1;
let mut x_offset = 0;
if parameters.blur_radius >= 16 {
sample_count -= 2;
x_offset = 1;
}
let sample_count = (sample_count as usize).min(MAX_UNMERGED_SAMPLES);
let sigma = parameters.blur_sigma;
let mut samples: Vec<KernelSample> = (0..sample_count)
.map(|index| {
let x = x_offset + (index as i32 * parameters.step_size) - parameters.blur_radius;
let x = x as f32;
KernelSample {
uv_offset: parameters.blur_uv_offset.map(|offset| offset * x),
coefficient: (-0.5 * (x * x) / (sigma * sigma)).exp()
/ ((2.0 * std::f32::consts::PI).sqrt() * sigma),
}
})
.collect();
let tally: f32 = samples.iter().map(|sample| sample.coefficient).sum();
for sample in &mut samples {
sample.coefficient /= tally;
}
samples
}
fn lerp_hack_kernel_samples(samples: &[KernelSample]) -> Vec<KernelSample> {
let sample_count = ((samples.len() - 1) / 2) + 1;
let middle = sample_count / 2;
let mut merged = Vec::with_capacity(sample_count);
let mut j = 0;
for index in 0..sample_count {
if index == middle {
merged.push(samples[j]);
j += 1;
} else {
let (left, right) = (samples[j], samples[j + 1]);
let coefficient = left.coefficient + right.coefficient;
let uv_offset = [0, 1].map(|axis| {
(left.uv_offset[axis] * left.coefficient
+ right.uv_offset[axis] * right.coefficient)
/ coefficient
});
merged.push(KernelSample {
uv_offset,
coefficient,
});
j += 2;
}
}
merged
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum InputKind {
Cut,
Whole,
}
#[derive(Debug, PartialEq)]
pub(super) struct BlurPlan {
args: DownsamplePassArgs,
downsample: DownsampleTaps,
vertical: Option<BlurPass>,
horizontal: Option<BlurPass>,
output: Placement,
}
#[derive(Debug, PartialEq)]
pub(super) struct DownsampleTaps {
pub kernel: DownsampleKernel,
pub uv: [f32; 4],
pub texel: [f32; 2],
pub edges: Option<EdgeLines>,
pub tile_mode: TileMode,
}
#[derive(Debug, PartialEq)]
pub(super) struct BlurPass {
samples: Vec<KernelSample>,
pub divide_by_alpha: bool,
}
pub(super) struct BlurBounds<'a> {
pub rect: &'a Rect,
pub local_to_tree: &'a Matrix,
}
impl BlurBounds<'_> {
fn texel_quad(&self, input: &Placement) -> [Point; 4] {
let local_to_texels = input.texel_transform().then(self.local_to_tree);
self.rect
.corners()
.map(|corner| local_to_texels.map_point(corner))
}
}
impl BlurPlan {
pub fn new(
blur: &BlurInfo,
input: &Placement,
hint: Option<&Rect>,
tile_mode: TileMode,
bounds: Option<BlurBounds<'_>>,
) -> Option<Self> {
if blur.is_negligible() {
return None;
}
let blur = blur.in_texels(&input.transform);
let texels = input.size.map(|size| size as f32);
let (region, kind) = match cut_region(&input.coverage(), hint.filter(|_| input.placed)) {
Some(hint) => (input.texel_rect(hint), InputKind::Cut),
None => (Rect::new(0.0, 0.0, texels[0], texels[1]), InputKind::Whole),
};
let args = downsample_pass_args(&blur, texels, ®ion, kind);
let edges =
bounds.map(|bounds| EdgeLines::of(&bounds.texel_quad(input), texel_to_uv(texels)));
let bounded = edges.is_some();
Some(Self {
downsample: DownsampleTaps::of(&args, texels, edges, tile_mode),
vertical: BlurPass::along(1, &blur, &args, false),
horizontal: BlurPass::along(0, &blur, &args, bounded),
output: Placement {
size: args.subpass_size,
transform: blurred_transform(&input.transform, &args),
placed: false,
},
args,
})
}
}
impl DownsampleTaps {
fn of(
args: &DownsamplePassArgs,
texels: [f32; 2],
edges: Option<EdgeLines>,
tile_mode: TileMode,
) -> Self {
let work = args.subpass_size.map(|size| size as f32);
let [u0, v0, u1, v1] = args.uvs;
Self {
kernel: downsample_kernel(args.effective_scalar[0], edges.is_some()),
uv: [(u1 - u0) / work[0], (v1 - v0) / work[1], u0, v0],
texel: texels.map(|texels| 1.0 / texels),
edges,
tile_mode,
}
}
pub fn read_level(&self, mip_levels: u32) -> u32 {
let span = (self.uv[0] / self.texel[0]).max(self.uv[1] / self.texel[1]);
let nearest = span.log2().round().max(0.0) as u32;
nearest.min(mip_levels.saturating_sub(1))
}
}
impl BlurPass {
pub fn sample_count(&self) -> usize {
self.samples.len()
}
pub fn kernel_uniform(&self) -> [[f32; 4]; MAX_KERNEL_SAMPLES] {
let mut uniform = [[0.0; 4]; MAX_KERNEL_SAMPLES];
for (slot, sample) in uniform.iter_mut().zip(&self.samples) {
*slot = [
sample.uv_offset[0],
sample.uv_offset[1],
sample.coefficient,
0.0,
];
}
uniform
}
fn along(
axis: usize,
blur: &BlurInfo,
args: &DownsamplePassArgs,
divide_by_alpha: bool,
) -> Option<Self> {
let parameters = BlurParameters::along(axis, blur, args);
let samples = if !parameters.is_negligible() {
lerp_hack_kernel_samples(&generate_blur_info(¶meters))
} else if divide_by_alpha {
vec![KernelSample {
uv_offset: [0.0, 0.0],
coefficient: 1.0,
}]
} else {
return None;
};
Some(Self {
samples,
divide_by_alpha,
})
}
}
fn texel_to_uv(texels: [f32; 2]) -> [f32; 4] {
[1.0 / texels[0], 1.0 / texels[1], 0.0, 0.0]
}
fn cut_region<'h>(coverage: &Rect, input_hint: Option<&'h Rect>) -> Option<&'h Rect> {
input_hint.filter(|hint| contains(coverage, hint))
}
fn contains(outer: &Rect, inner: &Rect) -> bool {
!inner.is_empty()
&& inner.x >= outer.x
&& inner.y >= outer.y
&& inner.right() <= outer.right()
&& inner.bottom() <= outer.bottom()
}
#[derive(Clone, Copy, Debug, PartialEq)]
struct DownsamplePassArgs {
subpass_size: [u32; 2],
uvs: [f32; 4],
effective_scalar: [f32; 2],
covers: Rect,
}
fn downsample_pass_args(
blur: &BlurInfo,
texels: [f32; 2],
region: &Rect,
kind: InputKind,
) -> DownsamplePassArgs {
let scalar = blur.downsample_scale();
match kind {
InputKind::Cut => aligned_cut_args(scalar, texels, region),
InputKind::Whole => gutter_args(scalar, blur.padding, texels, region),
}
}
fn blurred_transform(input_transform: &Matrix, args: &DownsamplePassArgs) -> Matrix {
input_transform
.then(&Matrix::translation(args.covers.x, args.covers.y))
.then(&Matrix::scale(
1.0 / args.effective_scalar[0],
1.0 / args.effective_scalar[1],
))
}
fn aligned_cut_args(scalar: f32, texels: [f32; 2], region: &Rect) -> DownsamplePassArgs {
let divisor = (1.0 / scalar).round();
let left = floor_to_divisible(region.x, divisor);
let top = floor_to_divisible(region.y, divisor);
let aligned = Rect::new(
left,
top,
ceil_to_divisible(region.right() - left, divisor),
ceil_to_divisible(region.bottom() - top, divisor),
);
let source_size = [aligned.width.trunc(), aligned.height.trunc()];
let subpass_size = source_size.map(|size| (size * scalar) as u32);
let uvs = [
(aligned.x / texels[0]).clamp(0.0, 1.0),
(aligned.y / texels[1]).clamp(0.0, 1.0),
(aligned.right() / texels[0]).clamp(0.0, 1.0),
(aligned.bottom() / texels[1]).clamp(0.0, 1.0),
];
DownsamplePassArgs {
subpass_size,
uvs,
effective_scalar: [0, 1].map(|axis| subpass_size[axis] as f32 / source_size[axis]),
covers: Rect::new(aligned.x, aligned.y, source_size[0], source_size[1]),
}
}
fn gutter_args(
scalar: f32,
padding: [f32; 2],
texels: [f32; 2],
region: &Rect,
) -> DownsamplePassArgs {
let source_rect = *region;
let padded = expanded(&source_rect, padding);
let subpass_size = [
(padded.width * scalar).ceil() as u32,
(padded.height * scalar).ceil() as u32,
];
let divisible_size = [
ceil_to_divisible(padded.width, 1.0 / scalar),
ceil_to_divisible(padded.height, 1.0 / scalar),
];
let divisible_padding = [
divisible_gutter(padding[0], divisible_size[0], padded.width),
divisible_gutter(padding[1], divisible_size[1], padded.height),
];
let covers = expanded(&source_rect, divisible_padding);
DownsamplePassArgs {
subpass_size,
uvs: [
covers.x / texels[0],
covers.y / texels[1],
covers.right() / texels[0],
covers.bottom() / texels[1],
],
effective_scalar: [
subpass_size[0] as f32 / covers.width,
subpass_size[1] as f32 / covers.height,
],
covers,
}
}
pub(super) fn expanded(rect: &Rect, amount: [f32; 2]) -> Rect {
Rect::new(
rect.x - amount[0],
rect.y - amount[1],
rect.width + 2.0 * amount[0],
rect.height + 2.0 * amount[1],
)
}
fn divisible_gutter(padding: f32, divisible: f32, padded: f32) -> f32 {
if padding > 0.0 {
padding + (divisible - padded) / 2.0
} else {
0.0
}
}
fn ceil_to_divisible(value: f32, divisor: f32) -> f32 {
if divisor == 0.0 {
return value;
}
let remainder = value % divisor;
if remainder != 0.0 {
value + (divisor - remainder)
} else {
value
}
}
fn floor_to_divisible(value: f32, divisor: f32) -> f32 {
if divisor == 0.0 {
return value;
}
let remainder = value % divisor;
if remainder != 0.0 {
value - remainder
} else {
value
}
}
fn downsample_kernel(effective_scale: f32, bounded: bool) -> DownsampleKernel {
if !bounded && effective_scale >= 0.5 {
return DownsampleKernel::ONE_TAP;
}
let (edge, ratio) = if effective_scale <= 0.0625 {
(7.0, 1.0 / 64.0)
} else if effective_scale <= 0.125 {
(3.0, 1.0 / 16.0)
} else {
(1.0, 0.25)
};
DownsampleKernel { edge, ratio }
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(super) struct EdgeLines(pub [[f32; 4]; 4]);
impl EdgeLines {
pub fn of(quad: &[Point; 4], texel_to_uv: [f32; 4]) -> Self {
let [x, y, z, w] = texel_to_uv;
let corners = quad.map(|corner| Point::new(corner.x * x + z, corner.y * y + w));
let inward = if winding_area(&corners) < 0.0 {
-1.0
} else {
1.0
};
let edge = |from: Point, to: Point| {
let a = from.y - to.y;
let b = to.x - from.x;
let c = -(a * from.x + b * from.y);
[a * inward, b * inward, c * inward, 0.0]
};
Self([
edge(corners[0], corners[1]),
edge(corners[1], corners[2]),
edge(corners[2], corners[3]),
edge(corners[3], corners[0]),
])
}
}
fn winding_area(corners: &[Point; 4]) -> f32 {
(0..4)
.map(|index| {
let (from, to) = (corners[index], corners[(index + 1) % 4]);
from.x * to.y - to.x * from.y
})
.sum()
}
impl FilterPasses<'_, '_> {
pub fn push_blur(&mut self, plan: &BlurPlan, input: &wgpu::TextureView) -> Snapshot {
let size = plan.args.subpass_size;
let downsampled = self.push_downsample(&plan.downsample, size, input);
let vertical = plan
.vertical
.as_ref()
.map(|pass| self.push_blur_pass(pass, size, &downsampled, None));
let horizontal = plan.horizontal.as_ref().map(|pass| match &vertical {
Some(vertical) => self.push_blur_pass(pass, size, vertical, Some(downsampled.clone())),
None => self.push_blur_pass(pass, size, &downsampled, None),
});
Snapshot {
view: horizontal.or(vertical).unwrap_or(downsampled),
placement: plan.output,
}
}
fn push_downsample(
&mut self,
taps: &DownsampleTaps,
size: [u32; 2],
input: &wgpu::TextureView,
) -> wgpu::TextureView {
let target = self.pool.take_filter(size, self.format);
let shading = self.emit.downsample_shading(taps, input);
self.push_pass(
target.view.clone(),
self.format,
&whole(size),
size,
shading,
);
target.view
}
fn push_blur_pass(
&mut self,
pass: &BlurPass,
size: [u32; 2],
input: &wgpu::TextureView,
destination: Option<wgpu::TextureView>,
) -> wgpu::TextureView {
let target = destination.unwrap_or_else(|| self.pool.take_filter(size, self.format).view);
let shading = self.emit.blur_pass_shading(pass, input, size);
self.push_pass(target.clone(), self.format, &whole(size), size, shading);
target
}
}
fn whole(size: [u32; 2]) -> Rect {
Rect::new(0.0, 0.0, size[0] as f32, size[1] as f32)
}
#[cfg(test)]
pub(super) fn sigma_for_blur_radius(radius: f32, effect_transform: &Matrix) -> f32 {
let basis = basis_of(effect_transform);
let radius_of = |sigma: f32| {
let scaled = device_sigma(basis, scale_sigma(sigma), scale_sigma(sigma));
blur_radius(scaled[0]).max(blur_radius(scaled[1]))
};
let target = f64::from(radius);
let (delta, mut x) = (1e-6f64, 2.0f64);
loop {
let fx = f64::from(radius_of(x as f32)) - target;
let derivative =
(f64::from(radius_of((x + delta) as f32)) - f64::from(radius_of(x as f32))) / delta;
x -= fx / derivative;
if fx.abs() <= 0.001 && fx >= 0.0 {
return x as f32;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn picture(placed: bool) -> Placement {
Placement {
size: [100, 80],
transform: Matrix::translation(20.0, 10.0),
placed,
}
}
fn sigma(sigma: f32) -> BlurInfo {
BlurInfo::calculate(&Matrix::IDENTITY, &Matrix::IDENTITY, [sigma; 2])
}
fn contains_rect(outer: &Rect, inner: &Rect) -> bool {
inner.x >= outer.x
&& inner.y >= outer.y
&& inner.right() <= outer.right()
&& inner.bottom() <= outer.bottom()
}
#[test]
fn a_negligible_blur_plans_nothing() {
let plan = BlurPlan::new(&sigma(0.0), &picture(true), None, TileMode::Decal, None);
assert_eq!(plan, None);
}
#[test]
fn a_blur_cuts_a_placed_picture_to_its_hint() {
let hint = Rect::new(40.0, 30.0, 20.0, 20.0);
let cut = BlurPlan::new(
&sigma(2.0),
&picture(true),
Some(&hint),
TileMode::Decal,
None,
)
.expect("a blur");
assert!(contains_rect(&cut.output.coverage(), &hint));
assert!(cut.output.coverage().width < 30.0, "{:?}", cut.output);
let whole = BlurPlan::new(
&sigma(2.0),
&picture(false),
Some(&hint),
TileMode::Decal,
None,
)
.expect("a blur");
assert!(contains_rect(
&whole.output.coverage(),
&picture(false).coverage()
));
}
#[test]
fn a_hint_past_the_pictures_edge_reads_it_whole() {
let hint = Rect::new(0.0, 0.0, 50.0, 50.0);
let plan = BlurPlan::new(
&sigma(2.0),
&picture(true),
Some(&hint),
TileMode::Mirror,
None,
)
.expect("a blur");
assert!(contains_rect(
&plan.output.coverage(),
&picture(true).coverage()
));
assert_eq!(plan.downsample.tile_mode, TileMode::Mirror);
}
#[test]
fn a_bounded_blur_keeps_its_last_pass_to_divide_by_alpha() {
let vertical_only = BlurInfo::calculate(&Matrix::IDENTITY, &Matrix::IDENTITY, [0.0, 4.0]);
let unbounded = BlurPlan::new(&vertical_only, &picture(false), None, TileMode::Decal, None)
.expect("a blur");
assert!(unbounded.vertical.is_some());
assert_eq!(unbounded.horizontal, None);
let rect = Rect::new(30.0, 20.0, 50.0, 40.0);
let bounds = BlurBounds {
rect: &rect,
local_to_tree: &Matrix::IDENTITY,
};
let bounded = BlurPlan::new(
&vertical_only,
&picture(false),
None,
TileMode::Decal,
Some(bounds),
)
.expect("a blur");
let last = bounded.horizontal.expect("the division by alpha");
assert!(last.divide_by_alpha);
assert_eq!(last.samples.len(), 1);
assert!(bounded.downsample.edges.is_some());
}
#[test]
fn a_blur_over_wide_texels_blurs_fewer_of_them() {
let wide = Placement {
transform: Matrix::translation(20.0, 10.0).then(&Matrix::scale(2.0, 2.0)),
..picture(false)
};
let over_wide = BlurPlan::new(&BlurInfo::of([8.0; 2]), &wide, None, TileMode::Decal, None)
.expect("a blur");
let over_pixels = BlurPlan::new(
&BlurInfo::of([4.0; 2]),
&picture(false),
None,
TileMode::Decal,
None,
)
.expect("a blur");
assert_eq!(over_wide.args, over_pixels.args);
assert_eq!(over_wide.vertical, over_pixels.vertical);
assert_eq!(
over_wide.output.transform.to_affine()[0],
2.0 * over_pixels.output.transform.to_affine()[0]
);
}
#[test]
fn a_downsample_reads_the_mip_level_its_pixels_span() {
let read_level = |sigma: f32, mip_levels: u32| {
BlurPlan::new(
&BlurInfo::of([sigma; 2]),
&picture(false),
None,
TileMode::Decal,
None,
)
.expect("a blur")
.downsample
.read_level(mip_levels)
};
assert_eq!(read_level(8.0, 7), 1);
assert_eq!(read_level(16.0, 7), 2);
assert_eq!(read_level(2.0, 7), 0);
assert_eq!(read_level(8.0, 1), 0);
}
#[test]
fn calculate_sigma_values() {
assert_eq!(calculate_scale(1.0), 1.0);
assert_eq!(calculate_scale(2.0), 1.0);
assert_eq!(calculate_scale(3.0), 1.0);
assert_eq!(calculate_scale(4.0), 1.0);
assert_eq!(calculate_scale(16.0), 0.25);
assert_eq!(calculate_scale(95.0), 0.125);
assert_eq!(calculate_scale(96.0), 0.0625);
assert_eq!(calculate_scale(1024.0), 0.0625);
}
#[test]
fn calculate_sigma_for_blur_radius() {
let sigma = 1.0;
let radius = blur_radius(scale_sigma(sigma));
assert!((sigma - sigma_for_blur_radius(radius, &Matrix::IDENTITY)).abs() < 0.01);
}
#[test]
fn coverage_with_sigma() {
let sigma = sigma_for_blur_radius(1.0, &Matrix::IDENTITY);
let blur = BlurInfo::of([scale_sigma(sigma); 2]);
assert_eq!(blur.padding, [1.0, 1.0]);
}
fn rendered_coverage(texture_origin: Point, sigma: f32) -> (Rect, Rect) {
let texture = Rect::new(texture_origin.x, texture_origin.y, 100.0, 100.0);
let blur = BlurInfo::calculate(&Matrix::IDENTITY, &Matrix::IDENTITY, [sigma; 2]);
let args = downsample_pass_args(
&blur,
[100.0, 100.0],
&Rect::new(0.0, 0.0, 100.0, 100.0),
InputKind::Whole,
);
let transform = blurred_transform(
&Matrix::translation(texture_origin.x, texture_origin.y),
&args,
);
let [width, height] = args.subpass_size.map(|size| size as f32);
let rendered = transform.map_rect(&Rect::new(0.0, 0.0, width, height));
let tree = super::super::filter_tree::FilterInput::Source.with_image_filter(
Some(&valo_dl::ImageFilter::blur(sigma, sigma)),
TileMode::Decal,
);
(rendered, tree.coverage(&texture, &Matrix::IDENTITY))
}
fn assert_rect_near(actual: Rect, expected: Rect) {
let near = |a: f32, b: f32| (a - b).abs() < 1e-3;
assert!(
near(actual.x, expected.x)
&& near(actual.y, expected.y)
&& near(actual.right(), expected.right())
&& near(actual.bottom(), expected.bottom()),
"{actual:?} != {expected:?}"
);
}
#[test]
fn render_coverage_matches_get_coverage() {
let sigma = sigma_for_blur_radius(1.0, &Matrix::IDENTITY);
let (rendered, coverage) = rendered_coverage(Point::new(0.0, 0.0), sigma);
assert_rect_near(coverage, Rect::from_ltrb(-1.0, -1.0, 101.0, 101.0));
assert_rect_near(rendered, Rect::from_ltrb(-1.0, -1.0, 101.0, 101.0));
}
#[test]
fn render_coverage_matches_get_coverage_translate() {
let sigma = sigma_for_blur_radius(1.0, &Matrix::IDENTITY);
let (rendered, coverage) = rendered_coverage(Point::new(100.0, 200.0), sigma);
assert_rect_near(coverage, Rect::from_ltrb(99.0, 199.0, 201.0, 301.0));
assert_rect_near(rendered, Rect::from_ltrb(99.0, 199.0, 201.0, 301.0));
}
#[test]
fn a_downsampled_blur_covers_its_halo() {
let (rendered, coverage) = rendered_coverage(Point::new(10.0, 20.0), 20.0);
assert!(rendered.x <= coverage.x && rendered.right() >= coverage.right());
assert!(rendered.y <= coverage.y && rendered.bottom() >= coverage.bottom());
assert!(
(rendered.width - coverage.width).abs() < 8.0,
"{rendered:?} vs {coverage:?}"
);
}
#[test]
fn the_downsample_cuts_a_placed_input_that_holds_its_hint() {
let parent = Rect::new(0.0, 0.0, 100.0, 80.0);
let inside = Rect::new(10.0, 10.0, 40.0, 40.0);
let past_the_top = Rect::new(10.0, -6.0, 40.0, 40.0);
assert_eq!(cut_region(&parent, Some(&inside)), Some(&inside));
assert_eq!(cut_region(&parent, Some(&past_the_top)), None);
assert_eq!(cut_region(&parent, None), None);
}
#[test]
fn coefficients() {
let samples = generate_blur_info(&BlurParameters {
blur_uv_offset: [1.0, 0.0],
blur_sigma: 1.0,
blur_radius: 5,
step_size: 1,
});
assert_eq!(samples.len(), 11);
let tally: f32 = samples.iter().map(|sample| sample.coefficient).sum();
assert!((tally - 1.0).abs() < 1e-6);
for index in 0..4 {
assert_eq!(samples[index].coefficient, samples[10 - index].coefficient);
assert!(samples[index + 1].coefficient > samples[index].coefficient);
}
}
fn sample(x: f32, coefficient: f32) -> KernelSample {
KernelSample {
uv_offset: [x, 0.0],
coefficient,
}
}
#[test]
fn lerp_hack_kernel_samples_simple() {
let samples = [
sample(-2.0, 0.1),
sample(-1.0, 0.2),
sample(0.0, 0.4),
sample(1.0, 0.2),
sample(2.0, 0.1),
];
let merged = lerp_hack_kernel_samples(&samples);
assert_eq!(merged.len(), 3);
assert!((merged[0].uv_offset[0] + 1.333_333_3).abs() < 1e-5);
assert!((merged[0].coefficient - 0.3).abs() < 1e-6);
assert_eq!(merged[1].uv_offset, [0.0, 0.0]);
assert!((merged[1].coefficient - 0.4).abs() < 1e-6);
assert!((merged[2].uv_offset[0] - 1.333_333).abs() < 1e-5);
assert!((merged[2].coefficient - 0.3).abs() < 1e-6);
let data = [0.25, 0.5, 0.5, 1.0, 0.2];
let original: f32 = samples
.iter()
.zip(data)
.map(|(sample, value)| sample.coefficient * value)
.sum();
let lerp = |x: f32, left: f32, right: f32| {
let fract = x.fract().abs();
if x < 0.0 {
left * fract + right * (1.0 - fract)
} else {
left * (1.0 - fract) + right * fract
}
};
let fast = lerp(merged[0].uv_offset[0], data[0], data[1]) * merged[0].coefficient
+ data[2] * merged[1].coefficient
+ lerp(merged[2].uv_offset[0], data[3], data[4]) * merged[2].coefficient;
assert!((original - fast).abs() < 0.01);
}
#[test]
fn lerp_hack_kernel_samples_complex() {
let sigma = 10.0;
let radius = blur_radius(sigma).ceil() as i32;
let samples = generate_blur_info(&BlurParameters {
blur_uv_offset: [1.0, 0.0],
blur_sigma: sigma,
blur_radius: radius,
step_size: 1,
});
assert_eq!(samples.len(), 33);
let merged = lerp_hack_kernel_samples(&samples);
assert_eq!(merged.len(), 17);
let data: Vec<f32> = (0..33u32)
.map(|index| ((index.wrapping_mul(2_654_435_761) >> 8) % 256) as f32)
.collect();
let sampler = |x: f32| {
let fract = x.fract().abs();
if fract == 0.0 {
data[(x as i32 + 16) as usize]
} else {
let (left, right) = (
data[(x.floor() as i32 + 16) as usize],
data[(x.ceil() as i32 + 16) as usize],
);
if x < 0.0 {
fract * left + (1.0 - fract) * right
} else {
(1.0 - fract) * left + fract * right
}
}
};
let output: f32 = samples
.iter()
.map(|sample| sample.coefficient * sampler(sample.uv_offset[0]))
.sum();
let fast: f32 = merged
.iter()
.map(|sample| sample.coefficient * sampler(sample.uv_offset[0]))
.sum();
assert!((output - fast).abs() < 0.1, "{output} vs {fast}");
}
#[test]
fn chop_huge_blurs() {
let sigma = 30.5;
let samples = generate_blur_info(&BlurParameters {
blur_uv_offset: [1.0, 0.0],
blur_sigma: sigma,
blur_radius: blur_radius(sigma).ceil() as i32,
step_size: 1,
});
assert!(lerp_hack_kernel_samples(&samples).len() <= MAX_KERNEL_SAMPLES);
}
#[test]
fn edge_lines_hold_the_inside_of_a_rect_in_uv() {
let quad = Rect::new(20.0, 30.0, 40.0, 60.0).corners();
let edges = EdgeLines::of(&quad, [0.01, 0.01, 0.0, 0.0]);
assert!(inside(&edges, Point::new(0.4, 0.6)));
for outside in [(0.1, 0.6), (0.7, 0.6), (0.4, 0.2), (0.4, 0.95)] {
assert!(
!inside(&edges, Point::new(outside.0, outside.1)),
"{outside:?}"
);
}
}
#[test]
fn edge_lines_follow_a_rotated_quad() {
let quarter_turn = valo_geometry::Matrix::rotation(std::f32::consts::FRAC_PI_2);
let quad = Rect::new(0.0, 0.0, 40.0, 10.0)
.corners()
.map(|corner| quarter_turn.map_point(corner));
let edges = EdgeLines::of(&quad, [1.0, 1.0, 0.0, 0.0]);
assert!(inside(&edges, Point::new(-5.0, 20.0)));
assert!(!inside(&edges, Point::new(20.0, 5.0)));
}
#[test]
fn edge_lines_face_inward_under_a_mirror() {
let mirror = valo_geometry::Matrix::translation(100.0, 0.0)
.then(&valo_geometry::Matrix::scale(-1.0, 1.0));
let quad = Rect::new(20.0, 30.0, 40.0, 60.0)
.corners()
.map(|corner| mirror.map_point(corner));
let edges = EdgeLines::of(&quad, [1.0, 1.0, 0.0, 0.0]);
assert!(inside(&edges, Point::new(60.0, 50.0)));
assert!(!inside(&edges, Point::new(30.0, 50.0)));
}
fn inside(edges: &EdgeLines, point: Point) -> bool {
edges
.0
.iter()
.all(|[a, b, c, _]| a * point.x + b * point.y + c >= 0.0)
}
fn assert_sigma(actual: [f32; 2], expected: [f32; 2]) {
assert!(
(actual[0] - expected[0]).abs() < 1e-4 && (actual[1] - expected[1]).abs() < 1e-4,
"sigma {actual:?} != {expected:?}"
);
}
#[test]
fn skia_sigma_keeps_a_rotated_blur_round() {
let basis = basis_of(&Matrix::rotation(std::f32::consts::FRAC_PI_4));
assert_sigma(skia_sigma(basis, 10.0, 10.0), [10.0, 10.0]);
assert_sigma(device_sigma(basis, 10.0, 10.0), [0.0, 14.142136]);
}
#[test]
fn skia_sigma_still_scales_each_axis() {
let basis = basis_of(&Matrix::scale(2.0, 3.0));
assert_sigma(skia_sigma(basis, 4.0, 5.0), [8.0, 15.0]);
}
#[test]
fn device_sigma_scales_each_axis() {
let basis = basis_of(&Matrix::scale(2.0, 3.0));
assert_sigma(device_sigma(basis, 4.0, 5.0), [8.0, 15.0]);
}
#[test]
fn device_sigma_swaps_axes_under_a_quarter_turn() {
let basis = basis_of(&Matrix::rotation(std::f32::consts::FRAC_PI_2));
assert_sigma(device_sigma(basis, 12.0, 3.0), [3.0, 12.0]);
}
#[test]
fn device_sigma_is_sign_agnostic() {
let basis = basis_of(&Matrix::rotation(std::f32::consts::PI));
assert_sigma(device_sigma(basis, 7.0, 2.0), [7.0, 2.0]);
}
#[test]
fn device_sigma_mixes_axes_under_rotation_and_scale() {
let matrix = Matrix::scale(2.0, 3.0).then(&Matrix::rotation(std::f32::consts::FRAC_PI_4));
let [a, b, c, d, ..] = matrix.to_affine();
let expected = [(6.0 * a + 4.0 * c).abs(), (6.0 * b + 4.0 * d).abs()];
assert_sigma(device_sigma(basis_of(&matrix), 6.0, 4.0), expected);
assert!(expected[0] > 1.0 && expected[1] > 1.0);
}
#[test]
fn device_sigma_ignores_a_mirror() {
let mirrored = basis_of(&Matrix::scale(-2.0, 3.0));
assert_sigma(device_sigma(mirrored, 4.0, 5.0), [8.0, 15.0]);
}
#[test]
fn calculate_scales_sigma_by_the_entitys_axes() {
let scaled = scale_sigma(10.0);
let blur = BlurInfo::calculate(&Matrix::scale(2.0, 3.0), &Matrix::IDENTITY, [10.0; 2]);
assert_sigma(blur.scaled_sigma, [2.0 * scaled, 3.0 * scaled]);
}
#[test]
fn calculate_leaves_the_entitys_rotation_out() {
let entity = Matrix::rotation(std::f32::consts::FRAC_PI_4).then(&Matrix::scale(2.0, 2.0));
let blur = BlurInfo::calculate(&entity, &Matrix::IDENTITY, [10.0, 0.0]);
assert_sigma(blur.scaled_sigma, [2.0 * scale_sigma(10.0), 0.0]);
}
#[test]
fn calculate_takes_sigma_through_the_effect_transform_as_a_vector() {
let effect = Matrix::rotation(std::f32::consts::FRAC_PI_2);
let blur = BlurInfo::calculate(&Matrix::IDENTITY, &effect, [12.0, 3.0]);
assert_sigma(blur.scaled_sigma, [scale_sigma(3.0), scale_sigma(12.0)]);
}
#[test]
fn local_padding_is_the_padding_scaled_by_the_entity() {
let blur = BlurInfo::calculate(&Matrix::scale(2.0, 2.0), &Matrix::IDENTITY, [4.0; 2]);
assert_eq!(
blur.local_padding(),
blur.padding.map(|padding| 2.0 * padding)
);
}
#[test]
fn a_blur_over_scaled_texels_keeps_its_reach_in_pixels() {
let blur = BlurInfo::of([8.0, 8.0]);
let texels = Matrix::translation(30.0, 40.0).then(&Matrix::scale(2.0, 0.5));
assert_eq!(blur.in_texels(&texels).scaled_sigma, [4.0, 16.0]);
assert_eq!(blur.in_texels(&Matrix::translation(30.0, 40.0)), blur);
}
}