#![allow(clippy::chunks_exact_to_as_chunks)]
use super::batch::BatchState;
#[cfg(test)]
use crate::compat::vec;
use crate::compat::{Arc, Rc, Vec};
use crate::core::{Color, Font, Rect, Size};
use crate::render::pipeline::{
fill_conic_ramp, ramp_lut_for_stops, sat_add_i32, sat_mul_i32, sat_neg_i32,
u32_to_i32_saturating, ConicWrite,
};
use crate::render::{
blend_pixel_with_mode, BlendMode, MatteMode, RenderCommand, ShapedText, SoftwareRenderConfig,
SoftwareSurface, TextMetrics,
};
pub trait PaintBackend {
fn begin_frame(&mut self, clear: Color);
fn end_frame(&mut self);
fn mark_damage(&mut self, _rect: Rect) {}
fn damage_regions(&self) -> &[(i32, i32, u32, u32)] {
&[]
}
fn present_damage(&mut self) -> bool {
false
}
fn clear_layer_cache(&mut self) {}
#[cfg(test)]
fn cached_layer_count(&self) -> usize {
0
}
fn execute_command(&mut self, command: &RenderCommand);
fn draw_image(&mut self, x: i32, y: i32, width: u32, height: u32, data: &[u8]) {
self.execute_command(&RenderCommand::DrawImage {
x,
y,
width,
height,
data: Arc::from(data),
});
}
fn fill_path_non_zero(
&mut self,
points: &[crate::core::Point],
contours: &[(usize, usize)],
color: Color,
offset_x: i32,
offset_y: i32,
) -> Result<(), crate::render::CompoundPathError> {
let translated = points
.iter()
.map(|point| crate::core::Point::new(point.x + offset_x, point.y + offset_y))
.collect();
let path = crate::render::CompoundPath::new(translated, contours.to_vec())?;
self.execute_command(&RenderCommand::FillPathNonZero { path, color });
Ok(())
}
fn draw_text(
&mut self,
origin: crate::core::Point,
text: &str,
font: &Font,
color: Color,
alignment: crate::core::HorizontalAlignment,
) {
self.execute_command(&RenderCommand::DrawText {
origin,
text: crate::compat::Arc::from(text),
font: font.clone(),
color,
alignment,
});
}
fn fill_gradient(&mut self, rect: crate::core::Rect, gradient: &crate::style::Gradient) {
#[cfg(test)]
note_gradient_owned_command();
self.execute_command(&RenderCommand::DrawGradient {
rect,
gradient: Arc::new(gradient.clone()),
});
}
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) -> Rc<ShapedText>;
fn frame_rgba(&self) -> &[u8];
fn apply_render_config(&mut self, _config: SoftwareRenderConfig) {}
fn render_config(&self) -> SoftwareRenderConfig {
SoftwareRenderConfig::default()
}
}
#[cfg(test)]
thread_local! {
#[allow(clippy::missing_const_for_thread_local)]
static SEED_ADOPTIONS: core::cell::Cell<usize> = const { core::cell::Cell::new(0) };
}
pub struct SoftwarePaintBackend {
pub(crate) surface: SoftwareSurface,
pub(crate) batch_state: BatchState,
}
impl SoftwarePaintBackend {
pub fn new(size: Size, dpi_scale: f32) -> Self {
Self { surface: SoftwareSurface::new(size, dpi_scale), batch_state: BatchState::new() }
}
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
}
pub fn seed_from_owned(&mut self, frame: crate::compat::Vec<u8>) -> bool {
if frame.len() != self.surface.buffer.back.len() {
return false;
}
self.surface.buffer.back = frame;
#[cfg(test)]
SEED_ADOPTIONS.with(|count| count.set(count.get() + 1));
true
}
#[cfg(all(test, widgets_unstripped))]
pub(crate) fn take_seed_adoptions() -> usize {
SEED_ADOPTIONS.with(|count| count.replace(0))
}
pub fn clear_layer_cache(&mut self) {
self.surface.buffer.layer_cache.clear();
self.surface.buffer.layer_cache_cursor = 0;
}
#[cfg(test)]
pub(crate) fn cached_layer_count(&self) -> usize {
self.surface.buffer.layer_cache.len()
}
pub fn into_frame_rgba(self) -> crate::compat::Vec<u8> {
self.surface.into_frame_rgba()
}
}
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 mark_damage(&mut self, rect: Rect) {
self.surface.mark_damage(rect);
}
fn damage_regions(&self) -> &[(i32, i32, u32, u32)] {
self.surface.damage_regions()
}
fn present_damage(&mut self) -> bool {
self.surface.present_damage()
}
fn clear_layer_cache(&mut self) {
self.surface.buffer.layer_cache.clear();
self.surface.buffer.layer_cache_cursor = 0;
}
#[cfg(test)]
fn cached_layer_count(&self) -> usize {
self.surface.buffer.layer_cache.len()
}
fn execute_command(&mut self, command: &RenderCommand) {
route_command_to_surface(&mut self.surface, command);
}
fn draw_image(&mut self, x: i32, y: i32, width: u32, height: u32, data: &[u8]) {
#[cfg(test)]
note_image_bytes_drawn_without_copy(data.len());
self.surface.draw_image(x, y, width, height, data);
}
fn draw_text(
&mut self,
origin: crate::core::Point,
text: &str,
font: &Font,
color: Color,
alignment: crate::core::HorizontalAlignment,
) {
self.surface.draw_text(origin, text, font, color, alignment);
}
fn fill_gradient(&mut self, rect: crate::core::Rect, gradient: &crate::style::Gradient) {
self.surface.fill_rect_gradient(rect, gradient);
}
fn fill_path_non_zero(
&mut self,
points: &[crate::core::Point],
contours: &[(usize, usize)],
color: Color,
offset_x: i32,
offset_y: i32,
) -> Result<(), crate::render::CompoundPathError> {
self.surface.fill_path_non_zero_borrowed(points, contours, color, offset_x, offset_y)
}
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) -> Rc<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()
}
}
#[cfg(test)]
thread_local! {
#[allow(clippy::missing_const_for_thread_local)]
static IMAGE_BYTES_DRAWN_WITHOUT_COPY: core::cell::Cell<usize> =
const { core::cell::Cell::new(0) };
}
#[cfg(test)]
pub(crate) fn note_image_bytes_drawn_without_copy(bytes: usize) {
IMAGE_BYTES_DRAWN_WITHOUT_COPY.with(|cell| cell.set(cell.get() + bytes));
}
#[cfg(test)]
pub(crate) fn take_image_bytes_drawn_without_copy() -> usize {
IMAGE_BYTES_DRAWN_WITHOUT_COPY.with(|cell| cell.replace(0))
}
#[cfg(test)]
thread_local! {
#[allow(clippy::missing_const_for_thread_local)]
static GRADIENT_OWNED_COMMANDS: core::cell::Cell<usize> = const { core::cell::Cell::new(0) };
}
#[cfg(test)]
fn note_gradient_owned_command() {
GRADIENT_OWNED_COMMANDS.with(|cell| cell.set(cell.get() + 1));
}
#[cfg(test)]
fn take_gradient_owned_command_count() -> usize {
GRADIENT_OWNED_COMMANDS.with(|cell| cell.replace(0))
}
pub(super) fn route_command_to_surface(surface: &mut SoftwareSurface, command: &RenderCommand) {
if surface.buffer.suppress_depth > 0 {
match command {
RenderCommand::PushClip { .. }
| RenderCommand::PushLayer { .. }
| RenderCommand::PushLayerGroup { .. }
| RenderCommand::PushMatte { .. } => {
surface.buffer.suppress_depth += 1;
return;
}
RenderCommand::PopClip | RenderCommand::PopMatte => {
surface.buffer.suppress_depth -= 1;
return;
}
RenderCommand::PopLayer => {
surface.buffer.suppress_depth -= 1;
if surface.buffer.suppress_depth > 0 {
return;
}
}
_ => return,
}
}
match command {
RenderCommand::FillRect { rect, color } => surface.fill_rect(*rect, *color),
#[cfg(feature = "3d")]
RenderCommand::FillQuad { corners, color } => {
surface.draw_path(&corners[..], true, *color, true, 0);
}
#[cfg(feature = "3d")]
RenderCommand::FillQuadAA { corners, color } => surface.fill_quad_aa(*corners, *color),
#[cfg(feature = "3d")]
RenderCommand::DrawMesh3D { mesh, model, camera, viewport } => {
surface.draw_mesh_3d(mesh, model, camera, *viewport);
}
#[cfg(feature = "3d")]
RenderCommand::StrokeQuad { corners, color, width } => {
if *width > 0 {
for index in 0..4 {
surface.draw_line_with_width(
corners[index],
corners[(index + 1) % 4],
*color,
*width,
);
}
}
}
#[cfg(feature = "3d")]
RenderCommand::DrawImageQuad { corners, width, height, data } => {
surface.draw_image_quad(*corners, *width, *height, data);
}
RenderCommand::DrawRect { rect, color } => surface.draw_rect(*rect, *color),
RenderCommand::DrawRectStroke { rect, color, width } => {
surface.draw_rect_with_width(*rect, *color, *width)
}
RenderCommand::FillRoundedRect { rect, radii, color } => {
surface.fill_rounded_rect_radii(*rect, *radii, *color)
}
RenderCommand::FillRoundedRectAA { rect, radii, color } => {
surface.fill_rounded_rect_radii_aa(*rect, *radii, *color)
}
RenderCommand::DrawRoundedRectStroke { rect, radii, color, width } => {
surface.draw_rounded_rect_radii_with_width(*rect, *radii, *color, *width)
}
RenderCommand::DrawRoundedRectStrokeAA { rect, radii, color, width } => {
surface.draw_rounded_rect_radii_aa_with_width(*rect, *radii, *color, *width)
}
RenderCommand::DrawLine { from, to, color } => surface.draw_line(*from, *to, *color),
RenderCommand::DrawLineAA { from, to, color } => surface.draw_line_aa(*from, *to, *color),
RenderCommand::DrawLineStrokeAA { from, to, color, width } => {
surface.draw_line_aa_with_width(*from, *to, *color, *width)
}
RenderCommand::DrawLineStroke { from, to, color, width } => {
surface.draw_line_with_width(*from, *to, *color, *width)
}
RenderCommand::FillCircle { center, radius, color } => {
surface.fill_circle(*center, *radius, *color)
}
RenderCommand::FillCircleAA { center, radius, color } => {
surface.fill_circle_aa(*center, *radius, *color)
}
RenderCommand::DrawCircle { center, radius, color } => {
surface.draw_circle(*center, *radius, *color)
}
RenderCommand::DrawCircleStroke { center, radius, color, width } => {
surface.draw_circle_with_width(*center, *radius, *color, *width)
}
RenderCommand::DrawText { origin, text, font, color, alignment } => {
surface.draw_text(*origin, text, font, *color, *alignment)
}
RenderCommand::DrawImage { x, y, width, height, data } => {
surface.draw_image(*x, *y, *width, *height, data)
}
RenderCommand::PushClip { x, y, width, height } => {
surface.push_clip(*x, *y, *width, *height)
}
RenderCommand::PopClip => surface.pop_clip(),
RenderCommand::PushLayer { bounds, effect } => push_layer(surface, *bounds, *effect),
RenderCommand::PushLayerGroup { bounds, effect, opacity, cache_key } => {
push_layer_group(surface, *bounds, *effect, *opacity, *cache_key)
}
RenderCommand::PopLayer => pop_layer(surface),
RenderCommand::PushMatte { bounds, mode } => push_matte(surface, *bounds, *mode),
RenderCommand::PopMatte => pop_matte(surface),
RenderCommand::EndMatte => end_matte(surface),
RenderCommand::DrawGradient { rect, gradient } => {
surface.fill_rect_gradient(*rect, gradient);
}
RenderCommand::DrawArc { center, radius, start_angle, end_angle, color, filled } => {
surface.draw_arc(*center, *radius, *start_angle, *end_angle, *color, *filled);
}
RenderCommand::DrawPath { points, closed, color, filled, width } => {
surface.draw_path(points, *closed, *color, *filled, *width);
}
RenderCommand::FillPathNonZero { path, color } => {
surface.fill_path_non_zero(path, *color);
}
RenderCommand::BoxShadow { rect, color, offset_x, offset_y, blur_radius, spread } => {
let spread_rect = crate::core::Rect::new(
sat_add_i32(sat_add_i32(rect.x, *offset_x), sat_neg_i32(*spread)),
sat_add_i32(sat_add_i32(rect.y, *offset_y), sat_neg_i32(*spread)),
sat_mul_i32(*spread, 2).saturating_add(u32_to_i32_saturating(rect.width)).max(0)
as u32,
sat_mul_i32(*spread, 2).saturating_add(u32_to_i32_saturating(rect.height)).max(0)
as u32,
);
let shadow_color = *color;
surface.fill_rect(spread_rect, shadow_color);
if *blur_radius > 0 {
let size = surface.size();
let w = size.width as usize;
let h = size.height as usize;
if w > 0 && h > 0 {
let radius = (*blur_radius).min(100) as usize;
let region = surface.current_clip().unwrap_or((0, 0, w as u32, h as u32));
let blur_x0 = spread_rect.x.max(region.0).max(0) as usize;
let blur_y0 = spread_rect.y.max(region.1).max(0) as usize;
let clip_right = region.0 as i64 + i64::from(region.2);
let clip_bottom = region.1 as i64 + i64::from(region.3);
let blur_right = (spread_rect.x as i64 + i64::from(spread_rect.width))
.min(clip_right)
.min(w as i64)
.max(0);
let blur_bottom = (spread_rect.y as i64 + i64::from(spread_rect.height))
.min(clip_bottom)
.min(h as i64)
.max(0);
let blur_w = (blur_right - blur_x0 as i64).max(0) as usize;
let blur_h = (blur_bottom - blur_y0 as i64).max(0) as usize;
if blur_w > 0 && blur_h > 0 {
let mut scratch = surface.take_blur_scratch();
{
let back = surface.buffer.back_mut();
box_blur_region(
back,
w,
h,
(blur_x0, blur_y0, blur_w, blur_h),
radius,
&mut scratch,
);
}
surface.return_blur_scratch(scratch);
}
}
}
}
RenderCommand::Blur { radius } => {
let r = (*radius).min(100) as usize;
if r == 0 {
return;
}
let size = surface.size();
let w = size.width as usize;
let h = size.height as usize;
if w == 0 || h == 0 {
return;
}
let (bx, by, bw, bh) = match surface.current_clip() {
Some((clip_x, clip_y, clip_w, clip_h)) => {
let x0 = clip_x.max(0) as usize;
let y0 = clip_y.max(0) as usize;
let x1 = (clip_x as i64 + i64::from(clip_w)).clamp(0, w as i64) as usize;
let y1 = (clip_y as i64 + i64::from(clip_h)).clamp(0, h as i64) as usize;
(
x0.min(w),
y0.min(h),
x1.saturating_sub(x0.min(w)),
y1.saturating_sub(y0.min(h)),
)
}
None => (0, 0, w, h),
};
let mut scratch = surface.take_blur_scratch();
{
let back = surface.buffer.back_mut();
box_blur_region(back, w, h, (bx, by, bw, bh), r, &mut scratch);
}
surface.return_blur_scratch(scratch);
}
RenderCommand::ClipPath { points } => {
surface.push_path_clip(points);
}
RenderCommand::SetBlendMode { mode } => {
surface.set_blend_mode(*mode);
}
RenderCommand::DrawConicGradient { center, start_angle, stops } => {
if stops.is_empty() {
return;
}
let size = surface.size();
let w = size.width;
let h = size.height;
if w == 0 || h == 0 {
return;
}
let (x0, y0, x1, y1) = match surface.current_clip() {
Some((clip_x, clip_y, clip_w, clip_h)) => {
let x0 = clip_x.max(0) as u32;
let y0 = clip_y.max(0) as u32;
let x1 = (clip_x as i64 + i64::from(clip_w)).clamp(0, w as i64) as u32;
let y1 = (clip_y as i64 + i64::from(clip_h)).clamp(0, h as i64) as u32;
(x0.min(w), y0.min(h), x1, y1)
}
None => (0, 0, w, h),
};
if x1 <= x0 || y1 <= y0 {
return;
}
let lut = ramp_lut_for_stops(stops);
fill_conic_ramp(
surface.buffer.back_mut(),
size.width,
(x0, y0, x1, y1),
*center,
*start_angle,
&lut,
ConicWrite::Replace,
);
}
}
}
fn push_layer(
surface: &mut SoftwareSurface,
bounds: Rect,
effect: Option<crate::render::core::command::LayerEffect>,
) {
begin_layer(surface, bounds, effect, 1.0, None);
}
fn push_layer_group(
surface: &mut SoftwareSurface,
bounds: Rect,
effect: Option<crate::render::core::command::LayerEffect>,
opacity: f32,
cache_key: Option<u64>,
) {
begin_layer(surface, bounds, effect, opacity, cache_key);
}
fn begin_layer(
surface: &mut SoftwareSurface,
bounds: Rect,
effect: Option<crate::render::core::command::LayerEffect>,
opacity: f32,
cache_key: Option<u64>,
) {
let opacity = if opacity.is_finite() { opacity.clamp(0.0, 1.0) } else { 1.0 };
surface.push_clip(bounds.x, bounds.y, bounds.width, bounds.height);
let size = surface.size();
let Some(rect) = clipped_region(surface.current_clip(), size) else {
surface.buffer.layers.push(super::surface::LayerCapture {
rect: (0, 0, 0, 0),
effect,
backdrop: crate::compat::Vec::new(),
isolated: false,
opacity,
cache_key,
cached_foreground: None,
});
return;
};
let isolated = effect.is_some() || opacity < 1.0 || cache_key.is_some();
let cached_foreground = match cache_key {
Some(key) => {
let cursor = surface.buffer.layer_cache_cursor;
let cache = &mut surface.buffer.layer_cache;
let position = cache.iter().position(|(cached_key, entry)| {
*cached_key == key && entry.rect == rect && entry.opacity == opacity
});
position.map(|position| {
let newest = newest_cache_slot(cache.len(), cursor);
if position != newest {
cache.swap(position, newest);
}
Arc::clone(&cache[newest].1.foreground)
})
}
None => None,
};
let backdrop = if isolated {
let mut buffer = surface.take_layer_backdrop();
capture_and_clear_region_into(
surface.buffer.back_mut(),
size.width as usize,
rect,
&mut buffer,
);
buffer
} else {
crate::compat::Vec::new()
};
if cached_foreground.is_some() {
surface.buffer.suppress_depth += 1;
}
surface.buffer.layers.push(super::surface::LayerCapture {
rect,
effect,
backdrop,
isolated,
opacity,
cache_key,
cached_foreground,
});
}
fn push_matte(surface: &mut SoftwareSurface, bounds: Rect, mode: MatteMode) {
surface.push_clip(bounds.x, bounds.y, bounds.width, bounds.height);
let size = surface.size();
let Some(rect) = clipped_region(surface.current_clip(), size) else {
surface.buffer.mattes.push(super::surface::MatteCapture {
rect: (0, 0, 0, 0),
backdrop: Vec::new(),
mode,
mask: Vec::new(),
});
return;
};
let mut backdrop = surface.take_layer_backdrop();
capture_and_clear_region_into(
surface.buffer.back_mut(),
size.width as usize,
rect,
&mut backdrop,
);
surface.buffer.mattes.push(super::surface::MatteCapture {
rect,
backdrop,
mode,
mask: Vec::new(),
});
}
fn pop_matte(surface: &mut SoftwareSurface) {
let Some(mut capture) = surface.buffer.mattes.pop() else { return };
let (_, _, width, height) = capture.rect;
if width == 0 || height == 0 {
surface.buffer.mattes.push(capture);
return;
}
let surface_width = surface.size().width as usize;
let mut drawn = surface.take_layer_foreground_scratch();
capture_and_clear_region_into(
surface.buffer.back_mut(),
surface_width,
capture.rect,
&mut drawn,
);
capture.mask = matte_coverage(&drawn, capture.mode);
surface.return_layer_foreground_scratch(drawn);
surface.buffer.mattes.push(capture);
}
fn end_matte(surface: &mut SoftwareSurface) {
let Some(capture) = surface.buffer.mattes.pop() else { return };
surface.pop_clip();
let (_, _, width, height) = capture.rect;
if width == 0 || height == 0 {
surface.return_layer_backdrop(capture.backdrop);
return;
}
let pixel_count = width as usize * height as usize;
if capture.backdrop.len() != pixel_count * 4 || capture.mask.len() != pixel_count {
surface.return_layer_backdrop(capture.backdrop);
return;
}
let surface_width = surface.size().width as usize;
let mut matted = surface.take_layer_foreground_scratch();
capture_region_into(surface.buffer.back_mut(), surface_width, capture.rect, &mut matted);
for (index, pixel) in matted.chunks_exact_mut(4).enumerate() {
let coverage = capture.mask[index] as u32;
pixel[3] = ((pixel[3] as u32 * coverage) / 255) as u8;
}
composite_foreground(
surface.buffer.back_mut(),
surface_width,
capture.rect,
&capture.backdrop,
&matted,
1.0,
);
surface.return_layer_foreground_scratch(matted);
surface.return_layer_backdrop(capture.backdrop);
}
fn matte_coverage(source: &[u8], mode: MatteMode) -> Vec<u8> {
let mut mask = Vec::with_capacity(source.len() / 4);
for pixel in source.chunks_exact(4) {
let alpha = pixel[3] as u32;
let luma =
(2126 * pixel[0] as u32 + 7152 * pixel[1] as u32 + 722 * pixel[2] as u32) / 10_000;
let coverage = match mode {
MatteMode::Alpha => alpha,
MatteMode::AlphaInverted => 255 - alpha,
MatteMode::Luma => luma * alpha / 255,
MatteMode::LumaInverted => 255 - luma * alpha / 255,
};
mask.push(coverage.min(255) as u8);
}
mask
}
const LAYER_CACHE_CAPACITY: usize = 64;
fn pop_layer(surface: &mut SoftwareSurface) {
if surface.buffer.layers.is_empty() {
return;
}
let Some(layer) = surface.buffer.layers.pop() else {
return;
};
surface.pop_clip();
if !layer.isolated {
return;
}
let (_, _, width, height) = layer.rect;
if width == 0 || height == 0 {
return;
}
let expected = width as usize * height as usize * 4;
let surface_width = surface.size().width as usize;
let was_cached = layer.cached_foreground.is_some();
let mut cached = layer.cached_foreground.filter(|foreground| foreground.len() == expected);
let mut scratch = Vec::new();
let mut scratch_owned = false;
if cached.is_none() {
let mut buffer = surface.take_layer_foreground_scratch();
capture_region_into(surface.buffer.back_mut(), surface_width, layer.rect, &mut buffer);
if let (Some(key), false) = (layer.cache_key, was_cached) {
let shared = Arc::new(buffer);
store_cached_layer(surface, key, layer.rect, layer.opacity, Arc::clone(&shared));
cached = Some(shared);
} else {
scratch = buffer;
scratch_owned = true;
}
}
let foreground: &[u8] = match &cached {
Some(foreground) => foreground.as_slice(),
None => scratch.as_slice(),
};
let mut backdrop = layer.backdrop;
if layer.opacity <= 0.0 {
if backdrop.len() == expected {
restore_region(surface.buffer.back_mut(), surface_width, layer.rect, &backdrop);
}
surface.return_layer_backdrop(backdrop);
if scratch_owned {
surface.return_layer_foreground_scratch(scratch);
}
return;
}
if backdrop.len() != expected {
surface.return_layer_backdrop(backdrop);
if scratch_owned {
surface.return_layer_foreground_scratch(scratch);
}
return;
}
if let Some(effect) = layer.effect {
let radius = effect.radius().min(100) as usize;
if radius > 0 {
let mut blur = surface.take_blur_scratch();
box_blur_region(
&mut backdrop,
width as usize,
height as usize,
(0, 0, width as usize, height as usize),
radius,
&mut blur,
);
surface.return_blur_scratch(blur);
}
if let Some((tint, weight)) = effect.tint() {
tint_region(&mut backdrop, tint, weight);
}
}
composite_foreground(
surface.buffer.back_mut(),
surface_width,
layer.rect,
&backdrop,
foreground,
layer.opacity,
);
surface.return_layer_backdrop(backdrop);
if scratch_owned {
surface.return_layer_foreground_scratch(scratch);
}
}
fn newest_cache_slot(len: usize, cursor: usize) -> usize {
if len < LAYER_CACHE_CAPACITY {
len.saturating_sub(1)
} else {
(cursor + len - 1) % LAYER_CACHE_CAPACITY
}
}
fn store_cached_layer(
surface: &mut SoftwareSurface,
key: u64,
rect: (i32, i32, u32, u32),
opacity: f32,
foreground: Arc<Vec<u8>>,
) {
let capacity = LAYER_CACHE_CAPACITY;
let cursor = surface.buffer.layer_cache_cursor;
let entry = super::surface::CachedLayer { rect, opacity, foreground };
let next_cursor = {
let cache = &mut surface.buffer.layer_cache;
if let Some(position) = cache.iter().position(|(cached_key, _)| *cached_key == key) {
let newest = newest_cache_slot(cache.len(), cursor);
if position != newest {
cache.swap(position, newest);
}
cache[newest].1 = entry;
cursor
} else if cache.len() < capacity {
cache.push((key, entry));
cursor
} else {
cache[cursor] = (key, entry);
(cursor + 1) % capacity
}
};
surface.buffer.layer_cache_cursor = next_cursor;
}
fn clipped_region(clip: Option<(i32, i32, u32, u32)>, size: Size) -> Option<(i32, i32, u32, u32)> {
let (x, y, width, height) = clip?;
let x0 = x.max(0) as i64;
let y0 = y.max(0) as i64;
let x1 = (x as i64 + width as i64).min(size.width as i64);
let y1 = (y as i64 + height as i64).min(size.height as i64);
if x1 <= x0 || y1 <= y0 {
return None;
}
Some((x0 as i32, y0 as i32, (x1 - x0) as u32, (y1 - y0) as u32))
}
pub(crate) fn capture_region(
back: &[u8],
surface_width: usize,
rect: (i32, i32, u32, u32),
) -> crate::compat::Vec<u8> {
let mut out = crate::compat::Vec::new();
capture_region_into(back, surface_width, rect, &mut out);
out
}
fn capture_region_into(
back: &[u8],
surface_width: usize,
rect: (i32, i32, u32, u32),
out: &mut crate::compat::Vec<u8>,
) {
let (x, y, width, height) = rect;
let row_bytes = width as usize * 4;
out.resize(row_bytes * height as usize, 0);
for row in 0..height as usize {
let src = (y as usize + row) * surface_width * 4 + x as usize * 4;
let dst = row * row_bytes;
if let (Some(source), Some(target)) =
(back.get(src..src + row_bytes), out.get_mut(dst..dst + row_bytes))
{
target.copy_from_slice(source);
} else if let Some(target) = out.get_mut(dst..dst + row_bytes) {
target.fill(0);
}
}
}
fn capture_and_clear_region_into(
back: &mut [u8],
surface_width: usize,
rect: (i32, i32, u32, u32),
out: &mut crate::compat::Vec<u8>,
) {
let (x, y, width, height) = rect;
let row_bytes = width as usize * 4;
out.resize(row_bytes * height as usize, 0);
for row in 0..height as usize {
let src = (y as usize + row) * surface_width * 4 + x as usize * 4;
let dst = row * row_bytes;
if let Some(source) = back.get_mut(src..src + row_bytes) {
if let Some(target) = out.get_mut(dst..dst + row_bytes) {
target.copy_from_slice(source);
}
source.fill(0);
} else if let Some(target) = out.get_mut(dst..dst + row_bytes) {
target.fill(0);
}
}
}
fn restore_region(
back: &mut [u8],
surface_width: usize,
rect: (i32, i32, u32, u32),
source: &[u8],
) {
let (x, y, width, height) = rect;
let row_bytes = width as usize * 4;
for row in 0..height as usize {
let dst = (y as usize + row) * surface_width * 4 + x as usize * 4;
let src = row * row_bytes;
if let (Some(target), Some(pixels)) =
(back.get_mut(dst..dst + row_bytes), source.get(src..src + row_bytes))
{
target.copy_from_slice(pixels);
}
}
}
fn composite_foreground(
back: &mut [u8],
surface_width: usize,
rect: (i32, i32, u32, u32),
backdrop: &[u8],
foreground: &[u8],
opacity: f32,
) {
restore_region(back, surface_width, rect, backdrop);
let (x, y, width, height) = rect;
let opacity = if opacity.is_finite() { opacity.clamp(0.0, 1.0) } else { 1.0 };
for row in 0..height as usize {
for column in 0..width as usize {
let index = (row * width as usize + column) * 4;
let Some(pixel) = foreground.get(index..index + 4) else { continue };
if pixel[3] == 0 {
continue;
}
let alpha = (pixel[3] as f32 * opacity).round().clamp(0.0, 255.0) as u8;
if alpha == 0 {
continue;
}
let color = Color::rgba(pixel[0], pixel[1], pixel[2], alpha);
blend_pixel_with_mode(
back,
surface_width as u32,
x.max(0) as u32 + column as u32,
y.max(0) as u32 + row as u32,
color,
1.0,
BlendMode::Normal,
);
}
}
}
#[allow(clippy::chunks_exact_to_as_chunks)]
fn tint_region(region: &mut [u8], tint: Color, weight: f32) {
if !weight.is_finite() {
return;
}
let weight = weight.clamp(0.0, 1.0);
if weight <= 0.0 {
return;
}
let tint_alpha = (tint.a as f32 / 255.0) * weight;
if tint_alpha <= 0.0 {
return;
}
let tint_rgb = [tint.r as f32 / 255.0, tint.g as f32 / 255.0, tint.b as f32 / 255.0];
for pixel in region.chunks_exact_mut(4) {
let dst_alpha = pixel[3] as f32 / 255.0;
let out_alpha = tint_alpha + dst_alpha * (1.0 - tint_alpha);
if out_alpha <= f32::EPSILON {
pixel.fill(0);
continue;
}
for channel in 0..3 {
let dst = pixel[channel] as f32 / 255.0;
let blended =
(tint_rgb[channel] * tint_alpha + dst * dst_alpha * (1.0 - tint_alpha)) / out_alpha;
pixel[channel] = (blended * 255.0).round().clamp(0.0, 255.0) as u8;
}
pixel[3] = (out_alpha * 255.0).round().clamp(0.0, 255.0) as u8;
}
}
fn box_blur_region(
back: &mut [u8],
w: usize,
h: usize,
region: (usize, usize, usize, usize),
radius: usize,
scratch_buffers: &mut super::surface::BoxBlurScratch,
) {
let (region_x, region_y, region_w, region_h) = region;
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 row_len = ew * 4;
let col_len = eh * 4;
let copy_len = ew.max(eh) * 4;
scratch_buffers.row.resize(row_len, 0);
scratch_buffers.col.resize(col_len, 0);
scratch_buffers.copy.resize(copy_len, 0);
let row = &mut scratch_buffers.row[..row_len];
let col = &mut scratch_buffers.col[..col_len];
let copy = &mut scratch_buffers.copy[..copy_len];
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 + row_len]);
copy[..row_len].copy_from_slice(row);
blur_row(row, ew, width, ©[..row_len]);
back[base..base + row_len].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]);
}
copy[..col_len].copy_from_slice(&col[..col_len]);
blur_row(col, eh, width, ©[..col_len]);
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::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 the_software_backend_fills_a_gradient_without_building_the_owning_command() {
use crate::style::{Gradient, GradientStop};
let size = Size::new(16, 16);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::WHITE);
let _ = take_gradient_owned_command_count();
let gradient = Gradient::linear(Point::new(0, 0), Point::new(16, 0)).with_stops(vec![
GradientStop::new(0.0, Color::RED),
GradientStop::new(1.0, Color::BLUE),
]);
backend.fill_gradient(Rect::new(0, 0, 16, 16), &gradient);
backend.end_frame();
assert_eq!(
take_gradient_owned_command_count(),
0,
"the software backend must paint the gradient itself rather than copy it into a command"
);
let rgba = backend.frame_rgba();
let mut through_command = SoftwarePaintBackend::new(size, 1.0);
through_command.begin_frame(Color::WHITE);
through_command.execute_command(&RenderCommand::DrawGradient {
rect: Rect::new(0, 0, 16, 16),
gradient: Arc::new(gradient.clone()),
});
through_command.end_frame();
let mut untouched = SoftwarePaintBackend::new(size, 1.0);
untouched.begin_frame(Color::WHITE);
untouched.end_frame();
assert_eq!(
rgba,
through_command.frame_rgba(),
"the override must paint the command's pixels"
);
assert_ne!(rgba, untouched.frame_rgba(), "the gradient must actually paint");
}
#[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); }
#[cfg(feature = "3d")]
#[test]
fn paint_backend_execute_fill_quad_fills_the_quadrilateral() {
let size = Size::new(24, 24);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::WHITE);
backend.execute_command(&RenderCommand::FillQuad {
corners: [Point::new(12, 2), Point::new(22, 12), Point::new(12, 22), Point::new(2, 12)],
color: Color::RED,
});
backend.end_frame();
let rgba = backend.frame_rgba();
let stride = 24 * 4;
let center = 12 * stride + 12 * 4;
assert_eq!(&rgba[center..center + 4], &[255, 0, 0, 255], "the centre is inside");
let box_corner = 3 * stride + 3 * 4;
assert_eq!(rgba[box_corner], 255, "the bounding-box corner is outside the diamond");
assert_eq!(rgba[box_corner + 1], 255);
assert_eq!(rgba[box_corner + 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: Arc::from("Hello"),
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: data.into(),
});
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),
radii: [4, 4, 4, 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 core::time::Duration;
use std::time::Instant;
let (w, h) = (400usize, 400usize);
let mut buf: Vec<u8> =
(0..w * h * 4).map(|i| ((i as f32 * 0.37).sin().abs() * 255.0) as u8).collect();
let mut scratch = crate::render::backend::surface::BoxBlurScratch::default();
let mut blur = |radius: usize| {
let start = Instant::now();
box_blur_region(&mut buf, w, h, (0, 0, w, h), radius, &mut scratch);
start.elapsed()
};
const SAMPLES: usize = 5;
let mut small = Duration::MAX;
let mut large = Duration::MAX;
for _ in 0..SAMPLES {
small = small.min(blur(2));
large = large.min(blur(40));
}
let ratio = large.as_secs_f64() / small.as_secs_f64().max(1e-9);
assert!(
ratio < 6.0,
"a 40 px blur took {ratio:.1}x a 2 px one ({large:?} vs {small:?}, minimum of \
{SAMPLES} samples) — 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,
&mut crate::render::backend::surface::BoxBlurScratch::default(),
);
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,
&mut crate::render::backend::surface::BoxBlurScratch::default(),
);
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"
);
}
}
}
}
#[test]
fn a_backdrop_blur_layer_blurs_what_is_behind_it() {
use crate::render::core::command::LayerEffect;
let mut backend = SoftwarePaintBackend::new(Size::new(40, 20), 1.0);
backend.begin_frame(Color::BLACK);
backend.execute_command(&RenderCommand::FillRect {
rect: Rect::new(0, 0, 20, 20),
color: Color::rgb(255, 0, 0),
});
backend.execute_command(&RenderCommand::FillRect {
rect: Rect::new(20, 0, 20, 20),
color: Color::rgb(0, 0, 255),
});
backend.execute_command(&RenderCommand::PushLayer {
bounds: Rect::new(0, 0, 40, 20),
effect: Some(LayerEffect::BackdropBlur { radius: 8 }),
});
backend.execute_command(&RenderCommand::PopLayer);
backend.end_frame();
let frame = backend.frame_rgba();
let at = |x: usize, y: usize| -> [u8; 4] {
let index = (y * 40 + x) * 4;
[frame[index], frame[index + 1], frame[index + 2], frame[index + 3]]
};
assert_eq!(at(0, 10), [255, 0, 0, 255], "the untouched edge stays red");
assert_eq!(at(39, 10), [0, 0, 255, 255], "and the other edge stays blue");
let left = at(19, 10);
let right = at(20, 10);
assert!(left[2] > 0, "blue must bleed across the hard edge: {left:?}");
assert!(right[0] > 0, "red must bleed across the hard edge: {right:?}");
}
#[test]
fn a_layer_composites_its_foreground_over_the_tinted_backdrop() {
use crate::render::core::command::LayerEffect;
let mut backend = SoftwarePaintBackend::new(Size::new(20, 20), 1.0);
backend.begin_frame(Color::TRANSPARENT);
backend.execute_command(&RenderCommand::FillRect {
rect: Rect::new(0, 0, 20, 20),
color: Color::WHITE,
});
backend.execute_command(&RenderCommand::PushLayer {
bounds: Rect::new(0, 0, 20, 20),
effect: Some(LayerEffect::BackdropBlurTint {
radius: 0,
tint: Color::rgb(255, 0, 0),
tint_weight: 1.0,
}),
});
backend.execute_command(&RenderCommand::FillRect {
rect: Rect::new(4, 4, 4, 4),
color: Color::rgb(0, 255, 0),
});
backend.execute_command(&RenderCommand::PopLayer);
backend.end_frame();
let frame = backend.frame_rgba();
let at = |x: usize, y: usize| -> [u8; 4] {
let index = (y * 20 + x) * 4;
[frame[index], frame[index + 1], frame[index + 2], frame[index + 3]]
};
assert_eq!(at(10, 10), [255, 0, 0, 255], "the backdrop takes the tint");
assert_eq!(at(5, 5), [0, 255, 0, 255], "the foreground stays on top of it");
}
#[test]
fn a_layer_snapshot_is_the_same_with_and_without_the_3d_feature() {
use crate::render::core::command::LayerEffect;
let mut backend = SoftwarePaintBackend::new(Size::new(16, 16), 1.0);
backend.begin_frame(Color::rgb(0, 0, 0));
backend.execute_command(&RenderCommand::FillRect {
rect: Rect::new(0, 0, 8, 16),
color: Color::rgb(200, 40, 20),
});
backend.execute_command(&RenderCommand::FillRect {
rect: Rect::new(8, 0, 8, 16),
color: Color::rgb(20, 40, 200),
});
backend.execute_command(&RenderCommand::PushLayer {
bounds: Rect::new(0, 0, 16, 16),
effect: Some(LayerEffect::BackdropBlur { radius: 6 }),
});
backend.execute_command(&RenderCommand::PopLayer);
backend.end_frame();
let frame = backend.frame_rgba();
let index = (8 * 16 + 8) * 4;
let red = frame[index];
let blue = frame[index + 2];
assert!(
red > 0 && blue > 0,
"the seam must be a blend of both halves regardless of the 3d feature: {red}, {blue}"
);
}
#[test]
fn a_layer_group_applies_its_opacity_to_the_subtree() {
let mut backend = SoftwarePaintBackend::new(Size::new(4, 4), 1.0);
backend.begin_frame(Color::WHITE);
backend.execute_command(&RenderCommand::PushLayerGroup {
bounds: Rect::new(0, 0, 4, 4),
effect: None,
opacity: 0.5,
cache_key: None,
});
backend.execute_command(&RenderCommand::FillRect {
rect: Rect::new(0, 0, 4, 4),
color: Color::RED,
});
backend.execute_command(&RenderCommand::PopLayer);
backend.end_frame();
let frame = backend.frame_rgba();
assert_eq!(
&frame[..4],
&[255, 127, 127, 255],
"the group is halfway between red and white"
);
}
#[test]
fn a_fully_transparent_layer_keeps_its_backdrop() {
let mut backend = SoftwarePaintBackend::new(Size::new(4, 4), 1.0);
backend.begin_frame(Color::WHITE);
backend.execute_command(&RenderCommand::PushLayerGroup {
bounds: Rect::new(0, 0, 4, 4),
effect: None,
opacity: 0.0,
cache_key: None,
});
backend.execute_command(&RenderCommand::FillRect {
rect: Rect::new(0, 0, 4, 4),
color: Color::RED,
});
backend.execute_command(&RenderCommand::PopLayer);
backend.end_frame();
assert_eq!(
&backend.frame_rgba()[..4],
&[255, 255, 255, 255],
"an invisible group must leave the backdrop exactly as it was"
);
}
#[test]
fn a_cached_layer_reuses_its_foreground_until_the_key_changes() {
let mut backend = SoftwarePaintBackend::new(Size::new(4, 4), 1.0);
let group = |cache_key: u64| RenderCommand::PushLayerGroup {
bounds: Rect::new(0, 0, 4, 4),
effect: None,
opacity: 1.0,
cache_key: Some(cache_key),
};
backend.begin_frame(Color::WHITE);
backend.execute_command(&group(7));
backend.execute_command(&RenderCommand::FillRect {
rect: Rect::new(0, 0, 4, 4),
color: Color::RED,
});
backend.execute_command(&RenderCommand::PopLayer);
backend.end_frame();
assert_eq!(backend.cached_layer_count(), 1, "the first frame fills the cache");
backend.begin_frame(Color::WHITE);
backend.execute_command(&group(7));
backend.execute_command(&RenderCommand::FillRect {
rect: Rect::new(0, 0, 4, 4),
color: Color::BLUE,
});
backend.execute_command(&RenderCommand::PopLayer);
backend.end_frame();
assert_eq!(
&backend.frame_rgba()[..4],
&[255, 0, 0, 255],
"the cached red foreground must replace the suppressed blue fill"
);
backend.begin_frame(Color::WHITE);
backend.execute_command(&group(8));
backend.execute_command(&RenderCommand::FillRect {
rect: Rect::new(0, 0, 4, 4),
color: Color::BLUE,
});
backend.execute_command(&RenderCommand::PopLayer);
backend.end_frame();
assert_eq!(
&backend.frame_rgba()[..4],
&[0, 0, 255, 255],
"a new key must redraw the subtree"
);
}
#[test]
fn changing_a_cached_layer_opacity_redraws_it() {
let mut backend = SoftwarePaintBackend::new(Size::new(2, 2), 1.0);
let group = |opacity: f32| RenderCommand::PushLayerGroup {
bounds: Rect::new(0, 0, 2, 2),
effect: None,
opacity,
cache_key: Some(3),
};
backend.begin_frame(Color::WHITE);
backend.execute_command(&group(1.0));
backend.execute_command(&RenderCommand::FillRect {
rect: Rect::new(0, 0, 2, 2),
color: Color::RED,
});
backend.execute_command(&RenderCommand::PopLayer);
backend.end_frame();
backend.begin_frame(Color::WHITE);
backend.execute_command(&group(0.5));
backend.execute_command(&RenderCommand::FillRect {
rect: Rect::new(0, 0, 2, 2),
color: Color::BLUE,
});
backend.execute_command(&RenderCommand::PopLayer);
backend.end_frame();
assert_eq!(
&backend.frame_rgba()[..4],
&[127, 127, 255, 255],
"the new opacity must be applied to the newly drawn foreground"
);
}
#[test]
fn clearing_the_layer_cache_forces_a_redraw() {
let mut backend = SoftwarePaintBackend::new(Size::new(2, 2), 1.0);
let group = RenderCommand::PushLayerGroup {
bounds: Rect::new(0, 0, 2, 2),
effect: None,
opacity: 1.0,
cache_key: Some(11),
};
backend.begin_frame(Color::WHITE);
backend.execute_command(&group);
backend.execute_command(&RenderCommand::FillRect {
rect: Rect::new(0, 0, 2, 2),
color: Color::RED,
});
backend.execute_command(&RenderCommand::PopLayer);
backend.end_frame();
assert_eq!(backend.cached_layer_count(), 1);
backend.clear_layer_cache();
assert_eq!(backend.cached_layer_count(), 0);
}
}