#[derive(Debug)]
pub(super) struct DirtyRegions {
dirty: u64,
total_cells: usize,
}
impl DirtyRegions {
const CHUNK_SHIFT: u32 = 10; const CHUNK_SIZE: usize = 1 << Self::CHUNK_SHIFT;
pub(super) fn new(total_cells: usize) -> Self {
debug_assert!(total_cells > 0, "requires a non-zero sized terminal");
Self { dirty: 0, total_cells }
}
pub(super) fn is_clean(&self) -> bool {
self.dirty == 0
}
pub(super) fn mark(&mut self, cell_index: usize) {
self.dirty |= 1u64 << ((cell_index >> Self::CHUNK_SHIFT) & 0b0011_1111);
}
pub(super) fn mark_all(&mut self) {
self.dirty = u64::MAX;
}
fn active_mask(&self) -> u64 {
let active_chunks = self.total_cells.div_ceil(Self::CHUNK_SIZE);
if active_chunks >= 64 { u64::MAX } else { (1u64 << active_chunks) - 1 }
}
pub(super) fn is_all_active_dirty(&self) -> bool {
let mask = self.active_mask();
self.dirty & mask == mask
}
pub(super) fn clear(&mut self) {
self.dirty = 0;
}
pub(super) fn drain(&mut self) -> DirtyChunkIter {
let dirty = self.dirty;
self.dirty = 0;
let total_chunks = self.total_cells.div_ceil(Self::CHUNK_SIZE);
DirtyChunkIter {
dirty,
total_cells: self.total_cells,
current_chunk: 0,
total_chunks,
}
}
}
pub(super) struct DirtyChunkIter {
dirty: u64,
total_cells: usize,
current_chunk: usize,
total_chunks: usize,
}
impl Iterator for DirtyChunkIter {
type Item = (usize, usize);
fn next(&mut self) -> Option<Self::Item> {
while self.current_chunk < self.total_chunks {
if self.dirty & (1u64 << (self.current_chunk & 63)) != 0 {
let start_chunk = self.current_chunk;
self.current_chunk += 1;
while self.current_chunk < self.total_chunks
&& self.dirty & (1u64 << (self.current_chunk & 63)) != 0
{
self.current_chunk += 1;
}
let start = start_chunk * DirtyRegions::CHUNK_SIZE;
let end = (self.current_chunk * DirtyRegions::CHUNK_SIZE).min(self.total_cells);
return Some((start, end));
}
self.current_chunk += 1;
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn clean_on_creation() {
let dr = DirtyRegions::new(10_000);
assert!(dr.is_clean());
}
#[test]
fn mark_single_cell() {
let mut dr = DirtyRegions::new(10_000);
dr.mark(42);
assert!(!dr.is_clean());
let ranges: Vec<_> = dr.drain().collect();
assert_eq!(ranges, vec![(0, 1024)]);
}
#[test]
fn mark_adjacent_chunks_merge() {
let mut dr = DirtyRegions::new(10_000);
dr.mark(500); dr.mark(1500); dr.mark(2500); let ranges: Vec<_> = dr.drain().collect();
assert_eq!(ranges, vec![(0, 3072)]);
}
#[test]
fn mark_separated_chunks() {
let mut dr = DirtyRegions::new(10_000);
dr.mark(0); dr.mark(5000); let ranges: Vec<_> = dr.drain().collect();
assert_eq!(ranges, vec![(0, 1024), (4096, 5120)]);
}
#[test]
fn all_dirty_detection() {
let mut dr = DirtyRegions::new(10_000); dr.mark_all();
assert!(dr.is_all_active_dirty());
}
#[test]
fn drain_clamps_to_total_cells() {
let mut dr = DirtyRegions::new(1500); dr.mark(1400); let ranges: Vec<_> = dr.drain().collect();
assert_eq!(ranges, vec![(1024, 1500)]); }
#[test]
fn drain_resets_dirty_state() {
let mut dr = DirtyRegions::new(10_000);
dr.mark(42);
let _ = dr.drain().count();
assert!(dr.is_clean());
}
#[test]
fn all_dirty_single_range() {
let mut dr = DirtyRegions::new(2048); dr.mark_all();
let ranges: Vec<_> = dr.drain().collect();
assert_eq!(ranges, vec![(0, 2048)]);
}
#[test]
fn aliased_chunk_beyond_64k_is_uploaded() {
let mut dr = DirtyRegions::new(71_680);
dr.mark(66_000); let ranges: Vec<_> = dr.drain().collect();
assert!(
ranges
.iter()
.any(|(s, e)| *s <= 66_000 && *e > 66_000),
"expected a range covering cell 66000, got {ranges:?}"
);
}
#[test]
fn aliased_chunk_also_uploads_lower_alias() {
let mut dr = DirtyRegions::new(71_680);
dr.mark(66_000); let ranges: Vec<_> = dr.drain().collect();
assert!(
ranges.iter().any(|(s, _)| *s == 0),
"expected chunk 0 (aliased) to be uploaded, got {ranges:?}"
);
assert!(
ranges
.iter()
.any(|(s, e)| *s <= 65_536 && *e > 65_536),
"expected chunk 64 to be uploaded, got {ranges:?}"
);
}
#[test]
fn adjacent_aliased_chunks_merge() {
let mut dr = DirtyRegions::new(71_680);
dr.mark(65_536); dr.mark(66_560); let ranges: Vec<_> = dr.drain().collect();
assert!(
ranges.iter().any(|(s, e)| *s == 0 && *e >= 2048),
"expected chunks 0-1 merged, got {ranges:?}"
);
assert!(
ranges
.iter()
.any(|(s, e)| *s == 65_536 && *e >= 67_584),
"expected chunks 64-65 merged, got {ranges:?}"
);
}
}