use crate::clip::ClipRect;
pub const DEFAULT_TILE_SIZE: u32 = 64;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Tile {
pub x: u32,
pub y: u32,
pub w: u32,
pub h: u32,
}
impl Tile {
pub fn clip_rect(&self) -> ClipRect {
ClipRect {
x0: self.x as i64,
y0: self.y as i64,
x1: (self.x + self.w) as i64,
y1: (self.y + self.h) as i64,
}
}
}
pub struct TileIter {
rect: ClipRect,
fb_w: u32,
fb_h: u32,
tile_size: u32,
cur_x: u32,
cur_y: u32,
done: bool,
}
impl TileIter {
fn new(rect: ClipRect, fb_w: u32, fb_h: u32, tile_size: u32) -> Self {
let ts = tile_size.max(1);
let rx0 = rect.x0.max(0) as u32;
let ry0 = rect.y0.max(0) as u32;
let clamped = ClipRect {
x0: rx0 as i64,
y0: ry0 as i64,
x1: (rect.x1 as u32).min(fb_w) as i64,
y1: (rect.y1 as u32).min(fb_h) as i64,
};
let done = clamped.is_empty();
Self {
rect: clamped,
fb_w,
fb_h,
tile_size: ts,
cur_x: rx0,
cur_y: ry0,
done,
}
}
}
impl Iterator for TileIter {
type Item = Tile;
fn next(&mut self) -> Option<Tile> {
if self.done {
return None;
}
let x0 = self.cur_x;
let y0 = self.cur_y;
if x0 >= self.fb_w || y0 >= self.fb_h {
return None;
}
if (x0 as i64) >= self.rect.x1 || (y0 as i64) >= self.rect.y1 {
return None;
}
let x_end = ((x0 + self.tile_size) as i64).min(self.rect.x1) as u32;
let y_end = ((y0 + self.tile_size) as i64).min(self.rect.y1) as u32;
let w = x_end.saturating_sub(x0);
let h = y_end.saturating_sub(y0);
if w == 0 || h == 0 {
return None;
}
let tile = Tile { x: x0, y: y0, w, h };
let next_x = x0 + self.tile_size;
if (next_x as i64) < self.rect.x1 {
self.cur_x = next_x;
} else {
self.cur_x = self.rect.x0 as u32;
self.cur_y = y0 + self.tile_size;
if (self.cur_y as i64) >= self.rect.y1 {
self.done = true;
}
}
Some(tile)
}
}
pub fn tiles_for(rect: ClipRect, fb_w: u32, fb_h: u32) -> TileIter {
TileIter::new(rect, fb_w, fb_h, DEFAULT_TILE_SIZE)
}
pub fn render_tiles<F>(rect: ClipRect, fb_w: u32, fb_h: u32, mut f: F)
where
F: FnMut(Tile),
{
for tile in tiles_for(rect, fb_w, fb_h) {
f(tile);
}
}
pub fn collect_tiles(rect: ClipRect, fb_w: u32, fb_h: u32) -> Vec<Tile> {
tiles_for(rect, fb_w, fb_h).collect()
}
#[cfg(feature = "parallel")]
pub fn render_parallel<F>(tiles: &[Tile], render_fn: F) -> Vec<(Tile, Vec<u32>)>
where
F: Fn(&Tile) -> Vec<u32> + Send + Sync,
{
use rayon::prelude::*;
tiles
.par_iter()
.map(|tile| (*tile, render_fn(tile)))
.collect()
}
#[derive(Debug, Clone)]
pub struct DirtyRegion {
tiles_wide: u32,
tiles_tall: u32,
dirty: Vec<bool>,
all_dirty: bool,
}
impl DirtyRegion {
pub fn new(fb_width: u32, fb_height: u32, tile_size: u32) -> Self {
let ts = tile_size.max(1);
let tw = fb_width.div_ceil(ts);
let th = fb_height.div_ceil(ts);
Self {
tiles_wide: tw,
tiles_tall: th,
dirty: vec![true; (tw * th) as usize],
all_dirty: true,
}
}
pub fn mark_rect(&mut self, x: u32, y: u32, width: u32, height: u32, tile_size: u32) {
if self.all_dirty {
return;
}
let ts = tile_size.max(1);
let tx_start = x / ts;
let ty_start = y / ts;
let tx_end = (x + width).div_ceil(ts);
let ty_end = (y + height).div_ceil(ts);
for ty in ty_start..ty_end.min(self.tiles_tall) {
for tx in tx_start..tx_end.min(self.tiles_wide) {
let idx = (ty * self.tiles_wide + tx) as usize;
if idx < self.dirty.len() {
self.dirty[idx] = true;
}
}
}
}
pub fn mark_tile(&mut self, tx: u32, ty: u32) {
if self.all_dirty {
return;
}
let idx = (ty * self.tiles_wide + tx) as usize;
if idx < self.dirty.len() {
self.dirty[idx] = true;
}
}
pub fn is_tile_dirty(&self, tx: u32, ty: u32) -> bool {
if self.all_dirty {
return true;
}
let idx = (ty * self.tiles_wide + tx) as usize;
self.dirty.get(idx).copied().unwrap_or(false)
}
pub fn clear_all(&mut self) {
self.dirty.fill(false);
self.all_dirty = false;
}
pub fn invalidate_all(&mut self) {
self.dirty.fill(true);
self.all_dirty = true;
}
pub fn dirty_tiles(&self) -> impl Iterator<Item = (u32, u32)> + '_ {
(0..self.tiles_tall).flat_map(move |ty| {
(0..self.tiles_wide).filter_map(move |tx| {
if self.is_tile_dirty(tx, ty) {
Some((tx, ty))
} else {
None
}
})
})
}
pub fn dirty_count(&self) -> usize {
if self.all_dirty {
(self.tiles_wide * self.tiles_tall) as usize
} else {
self.dirty.iter().filter(|&&d| d).count()
}
}
pub fn total_tiles(&self) -> usize {
(self.tiles_wide * self.tiles_tall) as usize
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tiles_cover_full_frame() {
let w = 200u32;
let h = 150u32;
let rect = ClipRect::full(w, h);
let tiles: Vec<Tile> = tiles_for(rect, w, h).collect();
let mut coverage = vec![0u32; (w * h) as usize];
for tile in &tiles {
for ty in tile.y..tile.y + tile.h {
for tx in tile.x..tile.x + tile.w {
coverage[(ty * w + tx) as usize] += 1;
}
}
}
for (i, &c) in coverage.iter().enumerate() {
assert_eq!(c, 1, "pixel {i} covered {c} times (expected exactly 1)");
}
}
#[test]
fn tile_covers_frame() {
let w = 64u32;
let h = 64u32;
let rect = ClipRect::full(w, h);
let tiles: Vec<Tile> = tiles_for(rect, w, h).collect();
assert_eq!(tiles.len(), 1);
assert_eq!(
tiles[0],
Tile {
x: 0,
y: 0,
w: 64,
h: 64
}
);
}
#[test]
fn tiles_non_overlapping() {
let w = 128u32;
let h = 128u32;
let rect = ClipRect::full(w, h);
let tiles: Vec<Tile> = tiles_for(rect, w, h).collect();
assert_eq!(tiles.len(), 4);
let mut coverage = vec![0u32; (w * h) as usize];
for tile in &tiles {
for ty in tile.y..tile.y + tile.h {
for tx in tile.x..tile.x + tile.w {
coverage[(ty * w + tx) as usize] += 1;
}
}
}
for &c in &coverage {
assert_eq!(c, 1, "every pixel must be covered exactly once");
}
}
#[test]
fn partial_boundary_tiles() {
let w = 70u32;
let h = 70u32;
let rect = ClipRect::full(w, h);
let tiles: Vec<Tile> = tiles_for(rect, w, h).collect();
assert_eq!(tiles.len(), 4);
let mut coverage = vec![0u32; (w * h) as usize];
for tile in &tiles {
for ty in tile.y..tile.y + tile.h {
for tx in tile.x..tile.x + tile.w {
coverage[(ty * w + tx) as usize] += 1;
}
}
}
for (i, &c) in coverage.iter().enumerate() {
assert_eq!(c, 1, "pixel {i} covered {c} times");
}
}
#[test]
fn tile_clip_rect_correct() {
let tile = Tile {
x: 64,
y: 128,
w: 32,
h: 16,
};
let clip = tile.clip_rect();
assert_eq!(clip.x0, 64);
assert_eq!(clip.y0, 128);
assert_eq!(clip.x1, 96);
assert_eq!(clip.y1, 144);
}
#[test]
fn render_tiles_visits_all() {
let w = 100u32;
let h = 100u32;
let rect = ClipRect::full(w, h);
let mut count = 0u32;
render_tiles(rect, w, h, |_tile| {
count += 1;
});
assert_eq!(count, 4);
}
#[test]
fn collect_tiles_returns_same_as_iterator() {
let w = 128u32;
let h = 128u32;
let rect = ClipRect::full(w, h);
let via_iter: Vec<Tile> = tiles_for(rect, w, h).collect();
let via_collect = collect_tiles(rect, w, h);
assert_eq!(via_iter, via_collect);
}
#[cfg(feature = "parallel")]
#[test]
fn parallel_output_matches_sequential() {
let w = 128u32;
let h = 128u32;
let rect = ClipRect::full(w, h);
let render_fn = |tile: &Tile| -> Vec<u32> {
let colour: u32 =
0xFF000000 | ((tile.x & 0xFF) << 16) | ((tile.y & 0xFF) << 8) | (tile.w & 0xFF);
vec![colour; (tile.w * tile.h) as usize]
};
let tiles = collect_tiles(rect, w, h);
let sequential: Vec<(Tile, Vec<u32>)> = tiles.iter().map(|t| (*t, render_fn(t))).collect();
let parallel = super::render_parallel(&tiles, render_fn);
let mut seq_sorted = sequential;
let mut par_sorted = parallel;
seq_sorted.sort_by_key(|(t, _)| (t.y, t.x));
par_sorted.sort_by_key(|(t, _)| (t.y, t.x));
assert_eq!(seq_sorted.len(), par_sorted.len(), "tile count mismatch");
for ((st, sp), (pt, pp)) in seq_sorted.iter().zip(par_sorted.iter()) {
assert_eq!(st, pt, "tile metadata mismatch");
assert_eq!(sp, pp, "pixel data mismatch for tile ({},{})", st.x, st.y);
}
}
}