use super::batch::BatchState;
use crate::compat::vec;
use crate::core::{Color, Font, Size};
use crate::render::pipeline::set_pixel;
use crate::render::{
BlendMode, RenderCommand, ShapedText, SoftwareRenderConfig, SoftwareSurface, TextMetrics,
};
pub trait PaintBackend {
fn begin_frame(&mut self, clear: Color);
fn end_frame(&mut self);
fn execute_command(&mut self, command: &RenderCommand);
fn size(&self) -> Size;
fn set_size(&mut self, size: Size);
fn dpi_scale(&self) -> f32;
fn set_dpi_scale(&mut self, dpi_scale: f32);
fn measure_text(&self, text: &str, font: &Font) -> TextMetrics;
fn shape_text(&self, text: &str, font: &Font) -> ShapedText;
fn frame_rgba(&self) -> &[u8];
fn apply_render_config(&mut self, _config: SoftwareRenderConfig) {}
fn render_config(&self) -> SoftwareRenderConfig {
SoftwareRenderConfig::default()
}
}
pub struct SoftwarePaintBackend {
pub(crate) surface: SoftwareSurface,
pub(crate) batch_state: BatchState,
pub(crate) current_blend_mode: BlendMode,
}
impl SoftwarePaintBackend {
pub fn new(size: Size, dpi_scale: f32) -> Self {
Self {
surface: SoftwareSurface::new(size, dpi_scale),
batch_state: BatchState::new(),
current_blend_mode: BlendMode::Normal,
}
}
pub fn surface(&self) -> &SoftwareSurface {
&self.surface
}
pub fn surface_mut(&mut self) -> &mut SoftwareSurface {
&mut self.surface
}
pub fn apply_render_config(&mut self, config: SoftwareRenderConfig) {
self.surface.apply_render_config(config);
}
pub fn render_config(&self) -> SoftwareRenderConfig {
self.surface.render_config()
}
pub fn seed_from(&mut self, frame: &[u8]) -> bool {
let target = &mut self.surface.buffer.back;
if frame.len() != target.len() {
return false;
}
target.copy_from_slice(frame);
true
}
}
impl PaintBackend for SoftwarePaintBackend {
fn begin_frame(&mut self, clear: Color) {
self.surface.begin_frame(clear);
}
fn end_frame(&mut self) {
self.surface.end_frame();
}
fn execute_command(&mut self, command: &RenderCommand) {
match command {
RenderCommand::FillRect { rect, color } => self.surface.fill_rect(*rect, *color),
RenderCommand::DrawRect { rect, color } => self.surface.draw_rect(*rect, *color),
RenderCommand::DrawRectStroke { rect, color, width } => {
self.surface.draw_rect_with_width(*rect, *color, *width)
}
RenderCommand::FillRoundedRect { rect, radius, color } => {
self.surface.fill_rounded_rect(*rect, *radius, *color)
}
RenderCommand::FillRoundedRectAA { rect, radius, color } => {
self.surface.fill_rounded_rect_aa(*rect, *radius, *color)
}
RenderCommand::DrawRoundedRectStroke { rect, radius, color, width } => {
self.surface.draw_rounded_rect_with_width(*rect, *radius, *color, *width)
}
RenderCommand::DrawRoundedRectStrokeAA { rect, radius, color, width } => {
self.surface.draw_rounded_rect_aa_with_width(*rect, *radius, *color, *width)
}
RenderCommand::DrawLine { from, to, color } => {
self.surface.draw_line(*from, *to, *color)
}
RenderCommand::DrawLineAA { from, to, color } => {
self.surface.draw_line_aa(*from, *to, *color)
}
RenderCommand::DrawLineStrokeAA { from, to, color, width } => {
self.surface.draw_line_aa_with_width(*from, *to, *color, *width)
}
RenderCommand::DrawLineStroke { from, to, color, width } => {
self.surface.draw_line_with_width(*from, *to, *color, *width)
}
RenderCommand::FillCircle { center, radius, color } => {
self.surface.fill_circle(*center, *radius, *color)
}
RenderCommand::FillCircleAA { center, radius, color } => {
self.surface.fill_circle_aa(*center, *radius, *color)
}
RenderCommand::DrawCircle { center, radius, color } => {
self.surface.draw_circle(*center, *radius, *color)
}
RenderCommand::DrawCircleStroke { center, radius, color, width } => {
self.surface.draw_circle_with_width(*center, *radius, *color, *width)
}
RenderCommand::DrawText { origin, text, font, color, alignment } => {
self.surface.draw_text(*origin, text, font, *color, *alignment)
}
RenderCommand::DrawImage { x, y, width, height, data } => {
self.surface.draw_image(*x, *y, *width, *height, data)
}
RenderCommand::PushClip { x, y, width, height } => {
self.surface.push_clip(*x, *y, *width, *height)
}
RenderCommand::PopClip => self.surface.pop_clip(),
RenderCommand::DrawGradient { rect, gradient } => {
self.surface.fill_rect_gradient(*rect, gradient);
}
RenderCommand::DrawArc { center, radius, start_angle, end_angle, color, filled } => {
self.surface.draw_arc(*center, *radius, *start_angle, *end_angle, *color, *filled);
}
RenderCommand::DrawPath { points, closed, color, filled, width } => {
self.surface.draw_path(points, *closed, *color, *filled, *width);
}
RenderCommand::BoxShadow { rect, color, offset_x, offset_y, blur_radius, spread } => {
let spread_rect = crate::core::Rect::new(
rect.x + offset_x - *spread,
rect.y + offset_y - *spread,
(rect.width as i32 + *spread * 2).max(0) as u32,
(rect.height as i32 + *spread * 2).max(0) as u32,
);
let shadow_color =
Color::rgba(color.r, color.g, color.b, (color.a as f32 * 0.5) as u8);
self.surface.fill_rect(spread_rect, shadow_color);
if *blur_radius > 0 {
let size = self.surface.size();
let w = size.width as usize;
let h = size.height as usize;
if w > 0 && h > 0 {
let back = self.surface.buffer.back_mut();
let radius = (*blur_radius).min(100) as usize;
let blur_x0 = spread_rect.x.max(0) as usize;
let blur_y0 = spread_rect.y.max(0) as usize;
let blur_w = ((spread_rect.x as usize + spread_rect.width as usize).min(w))
.saturating_sub(blur_x0);
let blur_h = ((spread_rect.y as usize + spread_rect.height as usize)
.min(h))
.saturating_sub(blur_y0);
box_blur_region(back, w, h, blur_x0, blur_y0, blur_w, blur_h, radius);
}
}
}
RenderCommand::Blur { radius } => {
let r = (*radius).min(100) as usize;
if r == 0 {
return;
}
let size = self.surface.size();
let w = size.width as usize;
let h = size.height as usize;
if w == 0 || h == 0 {
return;
}
let back = self.surface.buffer.back_mut();
box_blur_region(back, w, h, 0, 0, w, h, r);
}
RenderCommand::ClipPath { points } => {
if points.is_empty() {
return;
}
let min_x = points.iter().map(|p| p.x).min().unwrap();
let max_x = points.iter().map(|p| p.x).max().unwrap();
let min_y = points.iter().map(|p| p.y).min().unwrap();
let max_y = points.iter().map(|p| p.y).max().unwrap();
if min_x < max_x && min_y < max_y {
let cw = (max_x - min_x) as u32;
let ch = (max_y - min_y) as u32;
if cw > 0 && ch > 0 {
self.surface.push_clip(min_x, min_y, cw, ch);
}
}
}
RenderCommand::SetBlendMode { mode } => {
self.current_blend_mode = *mode;
}
RenderCommand::DrawConicGradient { center, start_angle, stops } => {
if stops.is_empty() {
return;
}
let size = self.surface.size();
let w = size.width as usize;
let h = size.height as usize;
if w == 0 || h == 0 {
return;
}
let back = self.surface.buffer.back_mut();
let cx = center.x as f32;
let cy = center.y as f32;
let angle_offset = *start_angle;
for py in 0..h {
for px in 0..w {
let dx = px as f32 - cx;
let dy = py as f32 - cy;
let mut t = dy.atan2(dx) + core::f32::consts::PI;
t = (t + angle_offset) % (2.0 * core::f32::consts::PI);
let pos = t / (2.0 * core::f32::consts::PI);
let color = if pos <= stops[0].0 {
stops[0].1
} else if pos >= stops.last().unwrap().0 {
stops.last().unwrap().1
} else {
let mut lo = 0usize;
let mut hi = stops.len() - 1;
while hi - lo > 1 {
let mid = (lo + hi) / 2;
if stops[mid].0 <= pos {
lo = mid;
} else {
hi = mid;
}
}
let t_local =
(pos - stops[lo].0) / (stops[hi].0 - stops[lo].0).max(0.0001);
let ca = stops[lo].1;
let cb = stops[hi].1;
Color::rgba(
(ca.r as f32 + (cb.r as f32 - ca.r as f32) * t_local) as u8,
(ca.g as f32 + (cb.g as f32 - ca.g as f32) * t_local) as u8,
(ca.b as f32 + (cb.b as f32 - ca.b as f32) * t_local) as u8,
(ca.a as f32 + (cb.a as f32 - ca.a as f32) * t_local) as u8,
)
};
set_pixel(back, w as u32, px as u32, py as u32, color);
}
}
}
}
}
fn size(&self) -> Size {
self.surface.size()
}
fn set_size(&mut self, size: Size) {
self.surface.resize(size);
}
fn dpi_scale(&self) -> f32 {
self.surface.dpi_scale()
}
fn set_dpi_scale(&mut self, dpi_scale: f32) {
self.surface.set_dpi_scale(dpi_scale);
}
fn measure_text(&self, text: &str, font: &Font) -> TextMetrics {
self.surface.measure_text(text, font)
}
fn shape_text(&self, text: &str, font: &Font) -> ShapedText {
self.surface.shape_text(text, font)
}
fn frame_rgba(&self) -> &[u8] {
self.surface.frame_rgba()
}
fn apply_render_config(&mut self, config: SoftwareRenderConfig) {
self.surface.apply_render_config(config);
}
fn render_config(&self) -> SoftwareRenderConfig {
self.surface.render_config()
}
}
fn box_blur_region(
back: &mut [u8],
w: usize,
h: usize,
region_x: usize,
region_y: usize,
region_w: usize,
region_h: usize,
radius: usize,
) {
if w == 0 || h == 0 || region_w == 0 || region_h == 0 || radius == 0 {
return;
}
let sigma = (radius as f32) / 2.0;
let (b0, b1, b2) = box_blur_widths(sigma);
let reach = b0.div_ceil(2) + b1.div_ceil(2) + b2.div_ceil(2);
let ex0 = region_x.saturating_sub(reach);
let ey0 = region_y.saturating_sub(reach);
let ex1 = (region_x + region_w + reach).min(w);
let ey1 = (region_y + region_h + reach).min(h);
let ew = ex1 - ex0;
let eh = ey1 - ey0;
if ew == 0 || eh == 0 {
return;
}
let mut row = vec![0u8; ew * 4];
let mut col = vec![0u8; eh * 4];
let mut scratch = vec![0u8; ew.max(eh) * 4];
let stride = w * 4;
for width in [b0, b1, b2] {
if width < 2 {
continue;
}
for y in ey0..ey1 {
let base = y * stride + ex0 * 4;
row.copy_from_slice(&back[base..base + ew * 4]);
scratch[..ew * 4].copy_from_slice(&row);
blur_row(&mut row, ew, width, &scratch[..ew * 4]);
back[base..base + ew * 4].copy_from_slice(&row);
}
for x in ex0..ex1 {
for y in 0..eh {
let si = (ey0 + y) * stride + x * 4;
col[y * 4..y * 4 + 4].copy_from_slice(&back[si..si + 4]);
}
scratch[..eh * 4].copy_from_slice(&col[..eh * 4]);
blur_row(&mut col, eh, width, &scratch[..eh * 4]);
for y in 0..eh {
let di = (ey0 + y) * stride + x * 4;
back[di..di + 4].copy_from_slice(&col[y * 4..y * 4 + 4]);
}
}
}
}
fn box_blur_widths(sigma: f32) -> (usize, usize, usize) {
if sigma <= 0.0 {
return (0, 0, 0);
}
const PASSES: f32 = 3.0;
let per_pass = sigma / PASSES.sqrt();
let ideal = (12.0 * per_pass * per_pass + 1.0).sqrt();
let wl = match ideal.floor() as usize {
w if w % 2 == 1 => w,
w => w.saturating_sub(1),
};
(wl, wl, wl + 2)
}
fn blur_row(pixels: &mut [u8], len: usize, width: usize, source: &[u8]) {
if len == 0 || width < 2 {
return;
}
let half = width / 2;
let lo_of = |x: usize| x.saturating_sub(half);
let hi_of = |x: usize| (x + half).min(len - 1);
let mut sums = [0i64; 4];
for i in 0..=hi_of(0) {
let idx = i * 4;
for c in 0..4 {
sums[c] += source[idx + c] as i64;
}
}
for x in 0..len {
let count = (hi_of(x) - lo_of(x) + 1) as i64;
let out = x * 4;
for c in 0..4 {
pixels[out + c] = (sums[c] / count) as u8;
}
if x + 1 == len {
break;
}
let mut delta = [0i64; 4];
if x + 1 + half < len {
let entering = (x + 1 + half) * 4;
for c in 0..4 {
delta[c] += source[entering + c] as i64;
}
}
if x >= half {
let leaving = lo_of(x) * 4;
for c in 0..4 {
delta[c] -= source[leaving + c] as i64;
}
}
for c in 0..4 {
sums[c] += delta[c];
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compat::MiniToString;
use crate::compat::Vec;
use crate::core::{Color, HorizontalAlignment, Point, Rect, Size};
#[test]
fn software_paint_backend_new_creates_surface() {
let size = Size::new(100, 100);
let backend = SoftwarePaintBackend::new(size, 1.0);
assert_eq!(backend.size(), size);
assert!((backend.dpi_scale() - 1.0).abs() < 1e-6);
}
#[test]
fn software_paint_backend_zero_size() {
let backend = SoftwarePaintBackend::new(Size::new(0, 0), 1.0);
assert_eq!(backend.size(), Size::new(0, 0));
let rgba = backend.frame_rgba();
assert!(rgba.is_empty());
}
#[test]
fn software_paint_backend_high_dpi() {
let backend = SoftwarePaintBackend::new(Size::new(50, 50), 2.0);
assert!((backend.dpi_scale() - 2.0).abs() < 1e-6);
}
#[test]
fn software_paint_backend_minimum_dpi_scale() {
let backend = SoftwarePaintBackend::new(Size::new(10, 10), 0.0);
assert!((backend.dpi_scale() - 0.1).abs() < 1e-6);
}
#[test]
fn software_paint_backend_surface_accessor() {
let backend = SoftwarePaintBackend::new(Size::new(30, 30), 1.0);
let surface = backend.surface();
assert_eq!(surface.size(), Size::new(30, 30));
}
#[test]
fn software_paint_backend_surface_mut_accessor() {
let mut backend = SoftwarePaintBackend::new(Size::new(40, 40), 1.0);
{
let surface = backend.surface_mut();
assert_eq!(surface.size(), Size::new(40, 40));
}
}
#[test]
fn paint_backend_begin_end_frame_clears() {
let size = Size::new(10, 10);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::RED);
backend.end_frame();
let rgba = backend.frame_rgba();
for chunk in rgba.chunks(4) {
assert_eq!(chunk[0], 255); assert_eq!(chunk[1], 0); assert_eq!(chunk[2], 0); assert_eq!(chunk[3], 255); }
}
#[test]
fn paint_backend_execute_fill_rect() {
let size = Size::new(20, 20);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::WHITE);
backend.execute_command(&RenderCommand::FillRect {
rect: Rect::new(2, 2, 10, 10),
color: Color::BLUE,
});
backend.end_frame();
let rgba = backend.frame_rgba();
let stride = 20 * 4;
let idx = 5 * stride + 5 * 4;
assert_eq!(rgba[idx], 0); assert_eq!(rgba[idx + 1], 0); assert_eq!(rgba[idx + 2], 255); }
#[test]
fn paint_backend_execute_draw_rect_stroke() {
let size = Size::new(20, 20);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::WHITE);
backend.execute_command(&RenderCommand::DrawRectStroke {
rect: Rect::new(0, 0, 20, 20),
color: Color::GREEN,
width: 1,
});
backend.end_frame();
let rgba = backend.frame_rgba();
assert_eq!(rgba[0], 0); assert_eq!(rgba[1], 255); assert_eq!(rgba[2], 0); }
#[test]
fn paint_backend_execute_draw_line() {
let size = Size::new(10, 10);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::WHITE);
backend.execute_command(&RenderCommand::DrawLine {
from: Point::new(0, 0),
to: Point::new(9, 9),
color: Color::RED,
});
backend.end_frame();
let rgba = backend.frame_rgba();
assert_eq!(rgba[0], 255); assert_eq!(rgba[3], 255); }
#[test]
fn paint_backend_execute_push_pop_clip() {
let size = Size::new(10, 10);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::WHITE);
backend.execute_command(&RenderCommand::PushClip { x: 0, y: 0, width: 5, height: 5 });
backend.execute_command(&RenderCommand::FillRect {
rect: Rect::new(0, 0, 10, 10),
color: Color::RED,
});
backend.execute_command(&RenderCommand::PopClip);
backend.end_frame();
let rgba = backend.frame_rgba();
let stride = 10 * 4;
let idx = 2 * stride + 2 * 4;
assert_eq!(rgba[idx], 255); assert_eq!(rgba[idx + 3], 255); }
#[test]
fn paint_backend_execute_fill_circle() {
let size = Size::new(20, 20);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::WHITE);
backend.execute_command(&RenderCommand::FillCircleAA {
center: Point::new(10, 10),
radius: 5,
color: Color::BLUE,
});
backend.end_frame();
let rgba = backend.frame_rgba();
let stride = 20 * 4;
let idx = 10 * stride + 10 * 4;
assert_eq!(rgba[idx], 0); assert_eq!(rgba[idx + 2], 255); }
#[test]
fn paint_backend_size_set_size() {
let size = Size::new(10, 10);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
assert_eq!(backend.size(), Size::new(10, 10));
let new_size = Size::new(50, 50);
backend.set_size(new_size);
assert_eq!(backend.size(), new_size);
}
#[test]
fn paint_backend_dpi_scale_set_dpi_scale() {
let mut backend = SoftwarePaintBackend::new(Size::new(10, 10), 1.0);
backend.set_dpi_scale(1.5);
assert!((backend.dpi_scale() - 1.5).abs() < 1e-6);
}
#[test]
fn paint_backend_render_config_default() {
let backend = SoftwarePaintBackend::new(Size::new(10, 10), 1.0);
let config = backend.render_config();
assert_eq!(config.aa_samples_per_axis, 4);
}
#[test]
fn paint_backend_apply_render_config() {
let mut backend = SoftwarePaintBackend::new(Size::new(10, 10), 1.0);
let config = SoftwareRenderConfig { aa_samples_per_axis: 2 };
backend.apply_render_config(config);
assert_eq!(backend.render_config().aa_samples_per_axis, 2);
}
#[test]
fn paint_backend_apply_render_config_clamps_to_normalized_range() {
let mut backend = SoftwarePaintBackend::new(Size::new(10, 10), 1.0);
let config = SoftwareRenderConfig { aa_samples_per_axis: 99 };
backend.apply_render_config(config);
assert_eq!(backend.render_config().aa_samples_per_axis, 8);
let config = SoftwareRenderConfig { aa_samples_per_axis: 0 };
backend.apply_render_config(config);
assert_eq!(backend.render_config().aa_samples_per_axis, 1);
}
#[test]
fn paint_backend_default_render_config() {
let config = <SoftwarePaintBackend as PaintBackend>::render_config(
&SoftwarePaintBackend::new(Size::new(1, 1), 1.0),
);
assert_eq!(config, SoftwareRenderConfig::default());
}
#[test]
fn paint_backend_execute_draw_text_does_not_panic() {
let size = Size::new(100, 100);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::WHITE);
let font = Font::simple("Arial", 12.0);
backend.execute_command(&RenderCommand::DrawText {
origin: Point::new(10, 20),
text: "Hello".to_string(),
font,
color: Color::BLACK,
alignment: HorizontalAlignment::Left,
});
backend.end_frame();
let rgba = backend.frame_rgba();
assert!(!rgba.is_empty());
}
#[test]
fn paint_backend_execute_draw_image() {
let size = Size::new(10, 10);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::WHITE);
let data = vec![
255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 0, 255, ];
backend.execute_command(&RenderCommand::DrawImage {
x: 0,
y: 0,
width: 2,
height: 2,
data,
});
backend.end_frame();
let rgba = backend.frame_rgba();
assert_eq!(rgba[0], 255); assert_eq!(rgba[1], 0); assert_eq!(rgba[2], 0); }
#[test]
fn paint_backend_execute_draw_rect() {
let size = Size::new(10, 10);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::WHITE);
backend.execute_command(&RenderCommand::DrawRect {
rect: Rect::new(0, 0, 10, 10),
color: Color::RED,
});
backend.end_frame();
let rgba = backend.frame_rgba();
assert_eq!(rgba[0], 255); assert_eq!(rgba[3], 255); }
#[test]
fn paint_backend_execute_fill_rounded_rect() {
let size = Size::new(20, 20);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::WHITE);
backend.execute_command(&RenderCommand::FillRoundedRect {
rect: Rect::new(2, 2, 16, 16),
radius: 4,
color: Color::GREEN,
});
backend.end_frame();
let rgba = backend.frame_rgba();
let stride = 20 * 4;
let idx = 10 * stride + 10 * 4;
assert_eq!(rgba[idx + 1], 255); }
#[test]
fn paint_backend_execute_draw_circle_stroke() {
let size = Size::new(20, 20);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::WHITE);
backend.execute_command(&RenderCommand::DrawCircleStroke {
center: Point::new(10, 10),
radius: 5,
color: Color::RED,
width: 2,
});
backend.end_frame();
let rgba = backend.frame_rgba();
assert!(!rgba.is_empty());
}
#[test]
fn paint_backend_measure_text_returns_metrics() {
let backend = SoftwarePaintBackend::new(Size::new(100, 100), 1.0);
let font = Font::simple("Arial", 12.0);
let metrics = backend.measure_text("Hello", &font);
assert!(metrics.width > 0);
assert!(metrics.height > 0);
}
#[test]
fn paint_backend_shape_text_returns_shaped() {
let backend = SoftwarePaintBackend::new(Size::new(100, 100), 1.0);
let font = Font::simple("Arial", 12.0);
let shaped = backend.shape_text("Hi", &font);
assert!(!shaped.clusters.is_empty());
}
#[test]
fn paint_backend_frame_rgba_after_clear() {
let size = Size::new(5, 5);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::rgb(128, 64, 32));
backend.end_frame();
let rgba = backend.frame_rgba();
let expected_len = 5 * 5 * 4;
assert_eq!(rgba.len(), expected_len);
assert_eq!(rgba[0], 128);
assert_eq!(rgba[1], 64);
assert_eq!(rgba[2], 32);
assert_eq!(rgba[3], 255);
}
#[test]
fn paint_backend_execute_fill_rect_out_of_bounds() {
let size = Size::new(10, 10);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::WHITE);
backend.execute_command(&RenderCommand::FillRect {
rect: Rect::new(100, 100, 50, 50),
color: Color::RED,
});
backend.end_frame();
let rgba = backend.frame_rgba();
for chunk in rgba.chunks(4) {
assert_eq!(chunk[0], 255);
assert_eq!(chunk[1], 255);
assert_eq!(chunk[2], 255);
}
}
#[test]
fn paint_backend_fill_rect_zero_size() {
let size = Size::new(10, 10);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::WHITE);
backend.execute_command(&RenderCommand::FillRect {
rect: Rect::new(0, 0, 0, 0),
color: Color::RED,
});
backend.end_frame();
let rgba = backend.frame_rgba();
for chunk in rgba.chunks(4) {
assert_eq!(chunk[0], 255);
}
}
#[test]
fn paint_backend_multiple_commands_in_frame() {
let size = Size::new(10, 10);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::WHITE);
backend.execute_command(&RenderCommand::FillRect {
rect: Rect::new(0, 0, 5, 10),
color: Color::RED,
});
backend.execute_command(&RenderCommand::FillRect {
rect: Rect::new(5, 0, 5, 10),
color: Color::BLUE,
});
backend.end_frame();
let rgba = backend.frame_rgba();
let stride = 10 * 4;
let left_idx = 2 * stride + 2 * 4;
assert_eq!(rgba[left_idx], 255); assert_eq!(rgba[left_idx + 2], 0);
let right_idx = 5 * stride + 7 * 4;
assert_eq!(rgba[right_idx], 0); assert_eq!(rgba[right_idx + 2], 255); }
#[test]
fn the_blur_cost_does_not_grow_with_the_radius() {
use std::time::Instant;
let (w, h) = (400usize, 400usize);
let budget = |radius: usize| {
let mut best = core::time::Duration::MAX;
for _ in 0..3 {
let mut buf: Vec<u8> =
(0..w * h * 4).map(|i| ((i as f32 * 0.37).sin().abs() * 255.0) as u8).collect();
let start = Instant::now();
box_blur_region(&mut buf, w, h, 0, 0, w, h, radius);
best = best.min(start.elapsed());
}
best
};
let small = budget(2);
let large = budget(40);
let ratio = large.as_secs_f64() / small.as_secs_f64().max(1e-9);
assert!(
ratio < 4.0,
"a 40 px blur took {ratio:.1}x a 2 px one ({large:?} vs {small:?}) — the cost is still \
growing with the radius, so the window is not sliding"
);
}
#[test]
fn the_sliding_window_agrees_with_a_direct_convolution() {
let len = 23usize;
let width = 5usize;
let half = width / 2;
let source: Vec<u8> =
(0..len * 4).map(|i| ((i as f32 * 37.0).sin().abs() * 200.0 + 20.0) as u8).collect();
let mut blurred = source.clone();
blur_row(&mut blurred, len, width, &source);
for x in 0..len {
for c in 0..4 {
let lo = x.saturating_sub(half);
let hi = (x + half).min(len - 1);
let sum: u32 = (lo..=hi).map(|k| source[k * 4 + c] as u32).sum();
let expected = (sum / (hi - lo + 1) as u32) as u8;
assert_eq!(
blurred[x * 4 + c],
expected,
"pixel {x} channel {c}: the window and the direct sum must agree"
);
}
}
}
#[test]
fn a_constant_field_survives_the_edge_clamping() {
let len = 16usize;
let mut row = vec![200u8; len * 4];
blur_row(&mut row, len, 7, &vec![200u8; len * 4]);
assert!(
row.iter().all(|&v| v == 200),
"a constant field must be unchanged by a blur, including at the edges"
);
}
#[test]
fn the_box_widths_are_odd_and_monotonic() {
let mut previous = 0usize;
for radius in [1usize, 2, 4, 8, 16, 32, 64] {
let (a, b, c) = box_blur_widths(radius as f32 / 2.0);
assert!(a % 2 == 1 && b % 2 == 1 && c % 2 == 1, "widths must be odd: {a} {b} {c}");
assert_eq!(a, b, "the pair must match, or the kernel is asymmetric");
let reach = a + c;
assert!(reach >= previous, "radius {radius} must not blur less than a smaller one");
previous = reach;
}
assert_eq!(box_blur_widths(0.0), (0, 0, 0), "a zero sigma is not a blur");
assert_eq!(box_blur_widths(-1.0), (0, 0, 0), "and neither is a negative one");
}
#[test]
fn the_three_boxes_realise_the_requested_sigma_not_three_times_it() {
let variance_of = |w: usize| {
if w == 0 {
0.0
} else {
(w * w - 1) as f32 / 12.0
}
};
for radius in [2usize, 3, 4, 6, 8, 12, 24, 48, 96] {
let sigma = radius as f32 / 2.0;
let (a, b, c) = box_blur_widths(sigma);
let realised = variance_of(a) + variance_of(b) + variance_of(c);
let target = sigma * sigma;
let ratio = realised / target;
assert!(
(0.6..=1.6).contains(&ratio),
"radius {radius}: the kernel realises variance {realised:.2} against a target of \
{target:.2} (ratio {ratio:.3}), i.e. a standard deviation {:.2}x the one asked for. \
Widths were ({a}, {b}, {c}). A ratio near 3 means the per-pass variance share was \
dropped again.",
ratio.sqrt()
);
}
}
#[test]
fn the_blur_is_separable() {
let (w, h) = (9usize, 7usize);
let source: Vec<u8> =
(0..w * h * 4).map(|i| ((i as f32 * 91.0).cos().abs() * 180.0 + 30.0) as u8).collect();
let mut horizontal_first = source.clone();
box_blur_region(&mut horizontal_first, w, h, 0, 0, w, h, 6);
let mut transposed = vec![0u8; w * h * 4];
for y in 0..h {
for x in 0..w {
transposed[(x * h + y) * 4..(x * h + y) * 4 + 4]
.copy_from_slice(&source[(y * w + x) * 4..(y * w + x) * 4 + 4]);
}
}
box_blur_region(&mut transposed, h, w, 0, 0, h, w, 6);
for y in 0..h {
for x in 0..w {
for c in 0..4 {
let a = horizontal_first[(y * w + x) * 4 + c] as i32;
let b = transposed[(x * h + y) * 4 + c] as i32;
assert!(
(a - b).abs() <= 1,
"({x},{y}) channel {c}: {a} vs {b} — the passes are not separable"
);
}
}
}
}
}