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,
render_all: impl FnMut(&mut crate::render::RenderContext),
) {
let mut regions = Vec::new();
render_dirty_regions_into(tracker, ctx, &mut regions, render_all);
}
pub fn render_dirty_regions_into(
tracker: &mut DirtyRegionTracker,
ctx: &mut crate::render::RenderContext,
regions: &mut Vec<Rect>,
mut render_all: impl FnMut(&mut crate::render::RenderContext),
) {
regions.clear();
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;
}
regions.extend(tracker.regions().iter().map(|region| region.rect));
ctx.begin_region_buckets(regions.as_slice());
render_all(ctx);
ctx.end_region_buckets();
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() {
let regions = [bounding];
ctx.begin_region_buckets(®ions);
render_all(ctx);
ctx.end_region_buckets();
}
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 _;
struct Passes {
traversals: usize,
region_clips: usize,
}
fn passes_for(regions: &[Rect], frame: Size) -> Passes {
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 traversals = 0usize;
let region_clips;
{
let mut ctx = RenderContext::new(&mut backend);
render_dirty_regions(&mut tracker, &mut ctx, |_| {
traversals += 1;
});
region_clips = ctx.region_bucket_passes();
}
Passes { traversals, region_clips }
}
#[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() {
let pass = passes_for(&[], Size::new(800, 600));
assert_eq!(pass.traversals, 0, "no damage means no traversal");
assert_eq!(pass.region_clips, 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)];
let pass = passes_for(®ions, Size::new(2000, 2000));
assert_eq!(pass.traversals, 1, "three regions must still mean one tree traversal");
assert_eq!(pass.region_clips, 3, "but each region keeps its own clip");
}
#[test]
fn damage_covering_the_frame_collapses_to_one_pass() {
let regions = [Rect::new(0, 0, 800, 400), Rect::new(0, 400, 800, 300)];
let pass = passes_for(®ions, Size::new(800, 700));
assert_eq!(pass.traversals, 1, "the covering repaint is still a single traversal");
assert_eq!(
pass.region_clips, 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)];
let pass = passes_for(&exactly_half, frame);
assert_eq!(pass.traversals, 1);
assert_eq!(
pass.region_clips, 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)];
let pass = passes_for(&just_under, frame);
assert_eq!(pass.traversals, 1);
assert_eq!(
pass.region_clips, 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();
let pass = passes_for(®ions, Size::new(1000, 1000));
assert_eq!(pass.traversals, 1);
assert_eq!(
pass.region_clips,
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 pass = passes_for(®ions, Size::new(4000, 4000));
assert_eq!(pass.traversals, 1, "the bound is one traversal, not one per region");
assert_eq!(
pass.region_clips, 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();
let pass = passes_for(®ions, Size::new(4000, 4000));
assert_eq!(pass.traversals, 1);
assert_eq!(pass.region_clips, 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");
}
#[test]
fn the_region_buffer_is_reused_and_never_left_stale() {
let mut tracker = DirtyRegionTracker::new();
tracker.add(Rect::new(0, 0, 10, 10));
tracker.add(Rect::new(200, 200, 10, 10));
let mut backend = SoftwarePaintBackend::new(Size::new(400, 400), 1.0);
backend.begin_frame(Color::rgb(0, 0, 0));
let mut regions = Vec::new();
{
let mut ctx = RenderContext::new(&mut backend);
render_dirty_regions_into(&mut tracker, &mut ctx, &mut regions, |_| {});
assert_eq!(ctx.region_bucket_passes(), 2, "both small regions are painted separately");
}
assert_eq!(regions.len(), 2, "the caller's buffer holds the rendered region rectangles");
let capacity = regions.capacity();
{
let mut ctx = RenderContext::new(&mut backend);
render_dirty_regions_into(&mut tracker, &mut ctx, &mut regions, |_| {});
assert_eq!(ctx.region_bucket_passes(), 0);
}
assert!(regions.is_empty(), "an empty tracker leaves the buffer empty");
assert_eq!(regions.capacity(), capacity, "the allocation survives across calls");
backend.end_frame();
}
}