use crate::compat::Vec;
use crate::core::Rect;
use crate::performance::DirtyRegionTracker;
pub const FULL_REPAINT_AREA_RATIO: f32 = 0.5;
pub fn render_dirty_regions(
tracker: &mut DirtyRegionTracker,
ctx: &mut crate::render::RenderContext,
mut render_all: impl FnMut(&mut crate::render::RenderContext),
) {
if tracker.over_capacity() {
paint_bounding_rect(tracker, ctx, &mut render_all);
return;
}
tracker.merge();
if tracker.is_empty() {
return;
}
let frame = ctx.size();
let frame_area = u64::from(frame.width) * u64::from(frame.height);
let covered: u64 = tracker
.regions()
.iter()
.map(|region| u64::from(region.rect.width) * u64::from(region.rect.height))
.sum();
let too_large =
frame_area == 0 || (covered as f32) >= (frame_area as f32) * FULL_REPAINT_AREA_RATIO;
if too_large {
paint_bounding_rect(tracker, ctx, &mut render_all);
return;
}
let regions: Vec<Rect> = tracker.regions().iter().map(|r| r.rect).collect();
for rect in regions {
ctx.push_clip(rect.x, rect.y, rect.width, rect.height);
render_all(ctx);
ctx.pop_clip();
}
tracker.clear();
}
fn paint_bounding_rect(
tracker: &mut DirtyRegionTracker,
ctx: &mut crate::render::RenderContext,
render_all: &mut impl FnMut(&mut crate::render::RenderContext),
) {
if let Some(bounding) = tracker.get_bounding_rect() {
ctx.push_clip(bounding.x, bounding.y, bounding.width, bounding.height);
render_all(ctx);
ctx.pop_clip();
}
tracker.clear();
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::{Color, Rect, Size};
use crate::render::RenderContext;
use crate::render::SoftwarePaintBackend;
use crate::render::PaintBackend as _;
fn paints_for(regions: &[Rect], frame: Size) -> usize {
let mut tracker = DirtyRegionTracker::new();
for rect in regions {
tracker.add(*rect);
}
let mut backend = SoftwarePaintBackend::new(frame, 1.0);
backend.begin_frame(Color::rgb(0, 0, 0));
let mut paints = 0usize;
{
let mut ctx = RenderContext::new(&mut backend);
render_dirty_regions(&mut tracker, &mut ctx, |_| {
paints += 1;
});
}
paints
}
#[test]
fn test_dirty_region_tracker() {
let mut tracker = DirtyRegionTracker::new();
tracker.add(Rect::new(0, 0, 100, 100));
tracker.add(Rect::new(50, 50, 100, 100));
tracker.merge();
assert_eq!(tracker.len(), 1);
assert!(!tracker.is_empty());
}
#[test]
fn no_damage_paints_nothing() {
assert_eq!(paints_for(&[], Size::new(800, 600)), 0);
}
#[test]
fn small_regions_in_a_large_frame_are_painted_separately() {
let regions =
[Rect::new(0, 0, 16, 16), Rect::new(100, 100, 16, 16), Rect::new(200, 200, 16, 16)];
assert_eq!(paints_for(®ions, Size::new(2000, 2000)), 3);
}
#[test]
fn damage_covering_the_frame_collapses_to_one_pass() {
let regions = [Rect::new(0, 0, 800, 400), Rect::new(0, 400, 800, 300)];
assert_eq!(
paints_for(®ions, Size::new(800, 700)),
1,
"two non-overlapping regions that together fill the frame must merge into one pass"
);
}
#[test]
fn the_fallback_applies_at_the_documented_ratio() {
let frame = Size::new(1000, 1000);
assert_eq!(
FULL_REPAINT_AREA_RATIO, 0.5,
"the fixture below steps the covered fraction by exactly 1/1000 of the frame; a different ratio needs a rebuilt fixture"
);
let exactly_half = [Rect::new(0, 0, 500, 500), Rect::new(500, 500, 500, 500)];
assert_eq!(
paints_for(&exactly_half, frame),
1,
"two disjoint regions covering exactly half the frame must collapse to the one-pass bounding fallback"
);
let just_under = [Rect::new(0, 0, 499, 500), Rect::new(500, 500, 500, 500)];
assert_eq!(
paints_for(&just_under, frame),
2,
"one pixel of area below the ratio must keep the two regions separate (one pass each)"
);
}
#[test]
fn many_small_regions_do_not_trigger_the_fallback() {
let regions: Vec<Rect> = (0..40).map(|i| Rect::new(i * 10, 0, 5, 5)).collect();
assert_eq!(
paints_for(®ions, Size::new(1000, 1000)),
regions.len(),
"forty regions covering 1000 px of a 1_000_000 px frame must not fall back"
);
}
#[test]
fn high_cardinality_damage_collapses_to_a_bounded_number_of_passes() {
let regions: Vec<Rect> = (0..500).map(|i| Rect::new(i, 0, 2, 2)).collect();
let paints = paints_for(®ions, Size::new(4000, 4000));
assert_eq!(
paints, 1,
"more regions than the tracker's capacity must paint once, not once per region"
);
}
#[test]
fn at_capacity_still_regions_separately() {
let regions: Vec<Rect> = (0..100).map(|i| Rect::new(i * 10, 0, 2, 2)).collect();
assert_eq!(paints_for(®ions, Size::new(4000, 4000)), 100);
}
#[test]
fn high_cardinality_damage_paints_every_damaged_pixel() {
let frame = Size::new(64, 64);
let clear = Color::rgb(0, 0, 0);
let ink = Color::rgb(255, 255, 255);
let damaged: Vec<Rect> = (0..300)
.map(|i| {
let x = i % 60;
let y = (i / 60) * 4;
Rect::new(x, y, 1, 1)
})
.collect();
let mut bounding = damaged[0];
for rect in &damaged[1..] {
bounding = bounding.union(rect);
}
let mut reference = SoftwarePaintBackend::new(frame, 1.0);
reference.begin_frame(clear);
let mut ref_ctx = RenderContext::new(&mut reference);
ref_ctx.fill_rect(bounding, ink);
drop(ref_ctx);
reference.end_frame();
let expected = reference.frame_rgba().to_vec();
let mut backend = SoftwarePaintBackend::new(frame, 1.0);
backend.begin_frame(clear);
let mut tracker = DirtyRegionTracker::new();
for rect in &damaged {
tracker.add(*rect);
}
let mut painted = 0usize;
{
let mut ctx = RenderContext::new(&mut backend);
render_dirty_regions(&mut tracker, &mut ctx, |ctx| {
painted += 1;
ctx.fill_rect(Rect::new(0, 0, frame.width, frame.height), ink);
});
}
backend.end_frame();
assert_eq!(painted, 1, "the bounded path paints once");
assert_eq!(
backend.frame_rgba(),
expected.as_slice(),
"every pixel inside the covering clip must match a full repaint: damage must not be dropped"
);
let stride = frame.width as usize * 4;
let got = backend.frame_rgba();
for rect in &damaged {
let at = rect.y as usize * stride + rect.x as usize * 4;
assert_eq!(
&got[at..at + 4],
&[255, 255, 255, 255],
"damage at {rect:?} was not repainted"
);
}
assert!(tracker.is_empty(), "the damage is consumed");
}
}