154 lines
4.3 KiB
Rust
154 lines
4.3 KiB
Rust
use std::ops::Range;
|
|
|
|
#[derive(Default)]
|
|
pub struct Dirty {
|
|
words: Vec<u64>,
|
|
/// Everything is dirty regardless of the bits -- the state after a
|
|
/// buffer reallocation, whose contents are undefined, and the state a
|
|
/// freshly built arena starts in. Kept as a flag rather than by
|
|
/// setting every bit so that it costs nothing to say and cannot be
|
|
/// half-applied as the array grows.
|
|
all: bool,
|
|
}
|
|
|
|
impl Dirty {
|
|
pub fn new_all() -> Self {
|
|
Self {
|
|
words: Vec::new(),
|
|
all: true,
|
|
}
|
|
}
|
|
|
|
pub fn mark(&mut self, i: usize) {
|
|
if self.all {
|
|
return;
|
|
}
|
|
let word = i / 64;
|
|
if word >= self.words.len() {
|
|
self.words.resize(word + 1, 0);
|
|
}
|
|
self.words[word] |= 1 << (i % 64);
|
|
}
|
|
|
|
pub fn contains(&self, i: usize) -> bool {
|
|
self.all
|
|
|| self
|
|
.words
|
|
.get(i / 64)
|
|
.is_some_and(|word| word & (1 << (i % 64)) != 0)
|
|
}
|
|
|
|
/// Clear one entry that was restored to the value already on the GPU.
|
|
/// `all` has no per-entry representation and is used only when every
|
|
/// byte must be uploaded regardless of later writes, so it stays set.
|
|
pub fn unmark(&mut self, i: usize) {
|
|
if self.all {
|
|
return;
|
|
}
|
|
if let Some(word) = self.words.get_mut(i / 64) {
|
|
*word &= !(1 << (i % 64));
|
|
}
|
|
}
|
|
|
|
/// Everything must be written: the buffer was reallocated (its
|
|
/// contents are undefined), or the array was cleared.
|
|
pub fn mark_all(&mut self) {
|
|
self.all = true;
|
|
self.words.clear();
|
|
}
|
|
|
|
pub fn is_clean(&self) -> bool {
|
|
!self.all && self.words.iter().all(|w| *w == 0)
|
|
}
|
|
|
|
pub fn ranges(&self, len: usize, gap: usize) -> Vec<Range<usize>> {
|
|
if self.all {
|
|
return Vec::from_iter((len > 0).then_some(0..len));
|
|
}
|
|
let mut ranges: Vec<Range<usize>> = Vec::new();
|
|
for (w, word) in self.words.iter().enumerate() {
|
|
let mut bits = *word;
|
|
while bits != 0 {
|
|
let start = w * 64 + bits.trailing_zeros() as usize;
|
|
let run = (bits >> (start - w * 64)).trailing_ones() as usize;
|
|
let end = (start + run).min(len);
|
|
if start >= len {
|
|
break;
|
|
}
|
|
match ranges.last_mut() {
|
|
Some(last) if start - last.end <= gap => last.end = end,
|
|
_ => ranges.push(start..end),
|
|
}
|
|
bits &= !(((1u128 << run) - 1) as u64) << (start - w * 64);
|
|
}
|
|
}
|
|
ranges
|
|
}
|
|
|
|
pub fn clear(&mut self) {
|
|
self.all = false;
|
|
self.words.clear();
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn marked(indices: &[usize], len: usize, gap: usize) -> Vec<Range<usize>> {
|
|
let mut d = Dirty::default();
|
|
for &i in indices {
|
|
d.mark(i);
|
|
}
|
|
d.ranges(len, gap)
|
|
}
|
|
|
|
#[test]
|
|
fn adjacent_entries_are_one_range() {
|
|
assert_eq!(marked(&[3, 4, 5], 64, 0), vec![3..6]);
|
|
}
|
|
|
|
#[test]
|
|
fn a_restored_entry_can_be_unmarked() {
|
|
let mut dirty = Dirty::default();
|
|
dirty.mark(3);
|
|
dirty.mark(5);
|
|
assert!(dirty.contains(3));
|
|
dirty.unmark(3);
|
|
assert!(!dirty.contains(3));
|
|
assert_eq!(dirty.ranges(8, 0), vec![5..6]);
|
|
}
|
|
|
|
#[test]
|
|
fn a_run_that_crosses_a_word_boundary_is_one_range() {
|
|
assert_eq!(marked(&[62, 63, 64, 65], 128, 0), vec![62..66]);
|
|
}
|
|
|
|
#[test]
|
|
fn a_gap_wider_than_the_threshold_stays_two_ranges() {
|
|
assert_eq!(marked(&[0, 10], 64, 4), vec![0..1, 10..11]);
|
|
assert_eq!(marked(&[0, 10], 64, 16), vec![0..11]);
|
|
}
|
|
|
|
#[test]
|
|
fn ranges_stop_at_the_length() {
|
|
assert_eq!(marked(&[1, 2, 40], 3, 0), vec![1..3]);
|
|
}
|
|
|
|
#[test]
|
|
fn mark_all_covers_everything_and_survives_later_marks() {
|
|
let mut d = Dirty::new_all();
|
|
d.mark(2);
|
|
assert_eq!(d.ranges(9, 0), vec![0..9]);
|
|
assert!(!d.is_clean());
|
|
d.clear();
|
|
assert!(d.is_clean());
|
|
assert!(d.ranges(9, 0).is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn an_empty_array_has_nothing_to_upload_even_when_all_is_set() {
|
|
assert!(Dirty::new_all().ranges(0, 0).is_empty());
|
|
}
|
|
}
|