use super::{CpuKernel, KernelContext, fwidth_fine, pixel_uv};
use crate::RenderSize;
use crate::builtins::{
DIRECTIONAL_BLUR_ID, DROP_SHADOW_ID, GAUSSIAN_BLUR_ID, GLOW_ID, TILT_SHIFT_ID, UNSHARP_MASK_ID,
};
use crate::cpu::curves;
use crate::cpu::image::{CpuImage, PixelBox};
mod blur;
mod color;
mod distort;
mod keying;
mod stylize;
pub(super) fn kernel(id: &str) -> Option<&'static dyn CpuKernel> {
match id {
GAUSSIAN_BLUR_ID | GLOW_ID | DIRECTIONAL_BLUR_ID | UNSHARP_MASK_ID | TILT_SHIFT_ID
| DROP_SHADOW_ID => Some(&Expanded),
_ => color::kernel(id)
.or_else(|| keying::kernel(id))
.or_else(|| blur::kernel(id))
.or_else(|| stylize::kernel(id))
.or_else(|| distort::kernel(id)),
}
}
#[inline]
pub(super) fn centre(image: &CpuImage, size: RenderSize, x: u32, y: u32) -> [f32; 4] {
if image.size() == size {
image.texel(x, y)
} else {
let uv = pixel_uv(size, x, y);
image.sample(uv[0], uv[1])
}
}
#[inline]
pub(super) fn effect_row(
context: &KernelContext<'_>,
y: u32,
x0: u32,
out: &mut [[f32; 4]],
effect: impl Fn(u32, [f32; 2], [f32; 4]) -> [f32; 4],
) {
for (offset, pixel) in out.iter_mut().enumerate() {
#[allow(clippy::cast_possible_truncation)]
let x = x0 + offset as u32;
let src = centre(context.source_a, context.size, x, y);
*pixel = effect(x, pixel_uv(context.size, x, y), src);
}
}
pub(super) fn grow(region: PixelBox, x: u32, y: u32, size: RenderSize) -> PixelBox {
if region.is_empty() {
return region;
}
let left = region.x.saturating_sub(x);
let top = region.y.saturating_sub(y);
let right = region.right().saturating_add(x).min(size.width);
let bottom = region.bottom().saturating_add(y).min(size.height);
PixelBox {
x: left,
y: top,
width: right.saturating_sub(left),
height: bottom.saturating_sub(top),
}
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub(super) fn margin(pixels: f32) -> u32 {
if pixels.is_finite() {
(pixels.abs().ceil().min(1.0e9) as u32).saturating_add(2)
} else {
u32::MAX
}
}
#[inline]
#[allow(clippy::manual_clamp)]
pub(super) fn saturate(value: f32) -> f32 {
value.max(0.0).min(1.0)
}
#[inline]
pub(super) fn luminance(rgb: [f32; 3]) -> f32 {
rgb[0] * 0.2126 + rgb[1] * 0.7152 + rgb[2] * 0.0722
}
#[inline]
pub(super) fn straight(color: [f32; 4], floor: f32) -> [f32; 3] {
if color[3] <= floor {
return [0.0; 3];
}
[
color[0] / color[3],
color[1] / color[3],
color[2] / color[3],
]
}
#[inline]
pub(super) fn premultiplied(rgb: [f32; 3], alpha: f32) -> [f32; 4] {
[rgb[0] * alpha, rgb[1] * alpha, rgb[2] * alpha, alpha]
}
#[inline]
pub(super) fn decode(encoded: f32) -> f32 {
if encoded <= 0.040_45 {
return encoded / 12.92;
}
if encoded < 1.0 {
return curves::srgb_to_linear(encoded);
}
libm::powf((encoded + 0.055) / 1.055, 2.4)
}
#[inline]
pub(super) fn encode(linear: f32) -> f32 {
if linear < 1.0 {
return curves::linear_to_srgb(linear);
}
1.055 * libm::powf(linear, 1.0 / 2.4) - 0.055
}
#[inline]
pub(super) fn decode3(rgb: [f32; 3]) -> [f32; 3] {
rgb.map(decode)
}
#[inline]
pub(super) fn output(rgb: [f32; 3], alpha: f32) -> [f32; 4] {
let alpha = saturate(alpha);
premultiplied(rgb.map(saturate), alpha)
}
#[inline]
pub(super) fn gray(rgb: [f32; 3]) -> f32 {
encode(saturate(luminance(decode3(rgb))))
}
#[inline]
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::manual_clamp
)]
pub(super) fn seed(value: f32) -> u32 {
value.round_ties_even().max(0.0).min(16_777_215.0) as u32
}
#[inline]
pub(super) fn aspect(context: &KernelContext<'_>) -> [f32; 2] {
let resolution = context.resolution();
[resolution[0] / resolution[1], 1.0]
}
#[inline]
pub(super) fn rotate(v: [f32; 2], radians: f32) -> [f32; 2] {
let c = libm::cosf(radians);
let s = libm::sinf(radians);
[c * v[0] - s * v[1], s * v[0] + c * v[1]]
}
#[inline]
pub(super) fn wrap(uv: [f32; 2], mode: u32) -> [f32; 2] {
match mode {
2 => uv.map(super::fract),
3 => uv.map(|value| 1.0 - (1.0 - 2.0 * super::fract(value * 0.5)).abs()),
_ => uv,
}
}
#[inline]
pub(super) fn edge_coverage(uv: [f32; 2], width: [f32; 2], mode: u32) -> f32 {
if mode != 0 {
return 1.0;
}
super::builtin_interval_coverage(uv[0], width[0])
* super::builtin_interval_coverage(uv[1], width[1])
}
#[inline]
pub(super) fn uv_width(
size: RenderSize,
x: u32,
y: u32,
source: impl Fn([f32; 2]) -> [f32; 2],
) -> [f32; 2] {
[
fwidth_fine(size, x, y, |uv| source(uv)[0]),
fwidth_fine(size, x, y, |uv| source(uv)[1]),
]
}
#[inline]
pub(super) fn sample_edge(
context: &KernelContext<'_>,
uv: [f32; 2],
width: [f32; 2],
mode: u32,
) -> [f32; 4] {
let coverage = edge_coverage(uv, width, mode);
context
.sample_src(wrap(uv, mode))
.map(|value| value * coverage)
}
struct Expanded;
impl CpuKernel for Expanded {
fn bounds(&self, context: &KernelContext<'_>) -> PixelBox {
context.source_a.bounds()
}
fn row(&self, context: &KernelContext<'_>, y: u32, x0: u32, out: &mut [[f32; 4]]) {
effect_row(context, y, x0, out, |_, _, src| src);
}
}
const POW_STEPS: usize = 1024;
const POW_SHIFT: u32 = 13;
pub(super) struct PowTables {
log2: Vec<f32>,
exp2: Vec<f32>,
}
#[allow(clippy::cast_precision_loss)]
pub(super) fn pow_tables() -> &'static PowTables {
static TABLES: std::sync::OnceLock<PowTables> = std::sync::OnceLock::new();
TABLES.get_or_init(|| PowTables {
log2: (0..=POW_STEPS)
.map(|step| libm::log2f(1.0 + step as f32 / POW_STEPS as f32))
.collect(),
exp2: (0..=POW_STEPS)
.map(|step| libm::exp2f(step as f32 / POW_STEPS as f32))
.collect(),
})
}
#[allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_possible_wrap
)]
pub(super) fn table_pow(tables: &PowTables, base: f32, exponent: f32) -> f32 {
let bits = base.to_bits();
let octave = ((bits >> 23) & 0xff) as i32 - 127;
let mantissa = bits & 0x007f_ffff;
let index = (mantissa >> POW_SHIFT) as usize;
let fraction = (mantissa & ((1 << POW_SHIFT) - 1)) as f32 * (1.0 / (1 << POW_SHIFT) as f32);
let low = tables.log2[index];
let log2 = octave as f32 + (low + (tables.log2[index + 1] - low) * fraction);
let power = exponent * log2;
if power.is_nan() || power <= -126.0 {
return 0.0;
}
if power >= 128.0 {
return f32::INFINITY;
}
let whole = power.floor();
let position = (power - whole) * POW_STEPS as f32;
let index = (position as usize).min(POW_STEPS - 1);
let fraction = position - index as f32;
let low = tables.exp2[index];
let scale = f32::from_bits(((whole as i32 + 127) as u32) << 23);
(low + (tables.exp2[index + 1] - low) * fraction) * scale
}
#[cfg(test)]
mod tests {
use super::{pow_tables, table_pow, wrap};
use crate::cpu::kernels::{builtin_hsv_to_rgb, builtin_rgb_to_hsv, builtin_value_noise};
#[test]
fn hsv_and_back_is_the_color_again() {
for r in 0..=8_u8 {
for g in 0..=8_u8 {
for b in 0..=8_u8 {
let rgb = [r, g, b].map(|value| f32::from(value) / 8.0);
let back = builtin_hsv_to_rgb(builtin_rgb_to_hsv(rgb));
for (channel, value) in back.iter().zip(rgb) {
assert!((channel - value).abs() < 1.0e-5, "{rgb:?} -> {back:?}");
}
}
}
}
}
#[test]
fn value_noise_stays_in_zero_to_one_on_both_sides_of_the_origin() {
let (mut low, mut high) = (1.0_f32, 0.0_f32);
for step in -400..400_i16 {
let p = [f32::from(step) * 0.173, f32::from(step) * -0.311 + 0.5];
let value = builtin_value_noise(p, 7);
low = low.min(value);
high = high.max(value);
}
assert!(low >= 0.0 && high <= 1.0, "{low}..{high}");
assert!(high - low > 0.5, "the noise varies: {low}..{high}");
}
#[test]
fn the_edge_modes_wrap_as_named() {
let near =
|a: [f32; 2], b: [f32; 2]| (a[0] - b[0]).abs() < 1.0e-6 && (a[1] - b[1]).abs() < 1.0e-6;
let outside = [-0.25, 1.25];
assert_eq!(wrap(outside, 0), outside);
assert_eq!(wrap(outside, 1), outside);
assert!(
near(wrap(outside, 2), [0.75, 0.25]),
"{:?}",
wrap(outside, 2)
);
assert!(
near(wrap(outside, 3), [0.25, 0.75]),
"{:?}",
wrap(outside, 3)
);
assert!(
near(wrap([0.4, 0.6], 3), [0.4, 0.6]),
"{:?}",
wrap([0.4, 0.6], 3)
);
}
#[test]
fn the_table_pow_stays_far_below_a_half_float_step() {
let tables = pow_tables();
let mut worst = 0.0_f64;
for exponent in [0.0_f32, 0.05, 0.3, 0.77, 1.0, 1.5, 2.0] {
let mut base = 1.0e-10_f32;
while base < 64.0 {
let expected = f64::from(base).powf(f64::from(exponent));
let got = f64::from(table_pow(tables, base, exponent));
worst = worst.max((got - expected).abs() / expected);
base *= 1.000_37;
}
}
assert!(worst < 1.0e-5, "relative error {worst}");
}
}