use super::region::DirtyRegionTracker;
use crate::compat::{Instant, Vec};
use crate::core::Rect;
pub struct UpdateBatcher {
pending_updates: Vec<Rect>,
batch_timeout_ms: u64,
last_batch: Option<Instant>,
first_pending: Option<Instant>,
}
impl UpdateBatcher {
pub fn new(batch_timeout_ms: u64) -> Self {
Self {
pending_updates: Vec::new(),
batch_timeout_ms,
last_batch: None,
first_pending: None,
}
}
pub fn add(&mut self, rect: Rect) {
if self.pending_updates.is_empty() {
self.first_pending = Some(Instant::now());
}
self.pending_updates.push(rect);
}
pub fn should_flush(&self) -> bool {
if self.pending_updates.is_empty() {
return false;
}
if let Some(first) = self.first_pending {
if first.elapsed().as_millis() as u64 >= self.batch_timeout_ms {
return true;
}
}
if let Some(last) = self.last_batch {
if last.elapsed().as_millis() as u64 >= self.batch_timeout_ms {
return true;
}
}
self.pending_updates.len() >= 10
}
pub fn flush(&mut self) -> Vec<Rect> {
if self.pending_updates.is_empty() {
return Vec::new();
}
let mut tracker = DirtyRegionTracker::new();
for rect in self.pending_updates.drain(..) {
tracker.add(rect);
}
tracker.merge();
self.last_batch = Some(Instant::now());
self.first_pending = None;
tracker.regions.into_iter().map(|r| r.rect).collect()
}
pub fn flush_clipped(
&mut self,
ctx: &mut crate::render::RenderContext,
render_all: impl FnMut(&mut crate::render::RenderContext),
) {
let rects = self.flush();
if rects.is_empty() {
return;
}
let mut tracker = DirtyRegionTracker::new();
for rect in rects {
tracker.add(rect);
}
super::render_dirty_regions(&mut tracker, ctx, render_all);
}
pub fn clear(&mut self) {
self.pending_updates.clear();
self.first_pending = None;
}
pub fn is_empty(&self) -> bool {
self.pending_updates.is_empty()
}
pub fn len(&self) -> usize {
self.pending_updates.len()
}
}
impl Default for UpdateBatcher {
fn default() -> Self {
Self::new(16)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::{PaintBackend as _, RenderContext, SoftwarePaintBackend};
#[test]
fn an_empty_batch_never_flushes() {
let mut batcher = UpdateBatcher::new(0);
assert!(!batcher.should_flush(), "an empty batch is never ready");
assert!(batcher.is_empty());
assert_eq!(batcher.flush(), Vec::new(), "and flushing it yields nothing");
batcher.add(Rect::new(0, 0, 10, 10));
assert!(batcher.should_flush(), "the zero-timeout budget starts at the first rect");
}
#[test]
fn ten_rects_flush_a_batch_whose_timeout_has_not_elapsed() {
let mut batcher = UpdateBatcher::new(60_000);
for i in 0..9 {
batcher.add(Rect::new(i * 20, 0, 10, 10));
assert!(!batcher.should_flush(), "{} rects is below the threshold", i + 1);
}
batcher.add(Rect::new(200, 0, 10, 10));
assert!(batcher.should_flush(), "ten rects flush regardless of the timeout");
assert_eq!(batcher.len(), 10, "`len` counts raw adds, before merging");
}
#[test]
fn flush_merges_through_the_dirty_region_tracker() {
let mut batcher = UpdateBatcher::new(60_000);
batcher.add(Rect::new(0, 0, 100, 100));
batcher.add(Rect::new(50, 50, 100, 100));
batcher.add(Rect::new(400, 400, 10, 10));
let mut reference = DirtyRegionTracker::new();
for rect in
[Rect::new(0, 0, 100, 100), Rect::new(50, 50, 100, 100), Rect::new(400, 400, 10, 10)]
{
reference.add(rect);
}
reference.merge();
let expected: Vec<Rect> = reference.regions().iter().map(|r| r.rect).collect();
let flushed = batcher.flush();
assert_eq!(flushed, expected, "the batch merges exactly as the damage tracker does");
assert_eq!(flushed.len(), 2, "the two overlapping rects became one");
assert!(batcher.is_empty(), "a flush drains the batch");
assert!(batcher.flush().is_empty(), "and a second flush is empty");
}
#[test]
fn flush_and_clear_treat_their_timers_differently() {
let mut batcher = UpdateBatcher::new(60_000);
for i in 0..10 {
batcher.add(Rect::new(i * 20, 0, 10, 10));
}
assert!(batcher.should_flush());
let _ = batcher.flush();
batcher.add(Rect::new(0, 0, 10, 10));
assert!(!batcher.should_flush(), "the periodic cadence holds after a flush");
batcher.clear();
assert!(batcher.is_empty());
assert!(!batcher.should_flush(), "nothing pending, so nothing to flush");
batcher.add(Rect::new(0, 0, 10, 10));
assert_eq!(batcher.len(), 1, "clear only drops what was queued");
}
#[test]
fn flush_clipped_paints_only_what_was_queued() {
let frame = crate::core::Size::new(400, 300);
let mut backend = SoftwarePaintBackend::new(frame, 1.0);
backend.begin_frame(crate::core::Color::rgb(0, 0, 0));
let mut batcher = UpdateBatcher::new(0);
let mut paints = 0usize;
{
let mut ctx = RenderContext::new(&mut backend);
batcher.flush_clipped(&mut ctx, |_| paints += 1);
assert_eq!(paints, 0, "an empty batch paints nothing");
batcher.add(Rect::new(10, 10, 40, 40));
batcher.add(Rect::new(30, 30, 40, 40));
batcher.flush_clipped(&mut ctx, |_| paints += 1);
}
assert_eq!(paints, 1, "a merged batch is one clipped paint");
}
}