Skip to main content

oxiui_render_soft/
tile.rs

1//! 64×64 render-tile iterator.
2//!
3//! Splits the framebuffer into a grid of `TILE_SIZE × TILE_SIZE` tiles,
4//! clamped to the framebuffer boundary. Tiles are independent of each other
5//! and can be rendered in parallel (rayon-ready); this run uses a serial
6//! driver.
7
8use crate::clip::ClipRect;
9
10/// Default tile side length in pixels.
11pub const DEFAULT_TILE_SIZE: u32 = 64;
12
13/// A single render tile, identified by its top-left corner and its pixel
14/// extents (which may be smaller than `DEFAULT_TILE_SIZE` at boundaries).
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub struct Tile {
17    /// Left edge of the tile (column index, inclusive).
18    pub x: u32,
19    /// Top edge of the tile (row index, inclusive).
20    pub y: u32,
21    /// Width in pixels (1–`DEFAULT_TILE_SIZE`).
22    pub w: u32,
23    /// Height in pixels (1–`DEFAULT_TILE_SIZE`).
24    pub h: u32,
25}
26
27impl Tile {
28    /// Returns the `ClipRect` corresponding to this tile.
29    pub fn clip_rect(&self) -> ClipRect {
30        ClipRect {
31            x0: self.x as i64,
32            y0: self.y as i64,
33            x1: (self.x + self.w) as i64,
34            y1: (self.y + self.h) as i64,
35        }
36    }
37}
38
39/// Iterator over the tiles covering a [`ClipRect`], clamped to the
40/// framebuffer size.
41pub struct TileIter {
42    rect: ClipRect,
43    fb_w: u32,
44    fb_h: u32,
45    tile_size: u32,
46    cur_x: u32,
47    cur_y: u32,
48    done: bool,
49}
50
51impl TileIter {
52    fn new(rect: ClipRect, fb_w: u32, fb_h: u32, tile_size: u32) -> Self {
53        let ts = tile_size.max(1);
54        // Clamp the iteration rectangle to the framebuffer.
55        let rx0 = rect.x0.max(0) as u32;
56        let ry0 = rect.y0.max(0) as u32;
57        let clamped = ClipRect {
58            x0: rx0 as i64,
59            y0: ry0 as i64,
60            x1: (rect.x1 as u32).min(fb_w) as i64,
61            y1: (rect.y1 as u32).min(fb_h) as i64,
62        };
63        let done = clamped.is_empty();
64        Self {
65            rect: clamped,
66            fb_w,
67            fb_h,
68            tile_size: ts,
69            cur_x: rx0,
70            cur_y: ry0,
71            done,
72        }
73    }
74}
75
76impl Iterator for TileIter {
77    type Item = Tile;
78
79    fn next(&mut self) -> Option<Tile> {
80        if self.done {
81            return None;
82        }
83        let x0 = self.cur_x;
84        let y0 = self.cur_y;
85        if x0 >= self.fb_w || y0 >= self.fb_h {
86            return None;
87        }
88        if (x0 as i64) >= self.rect.x1 || (y0 as i64) >= self.rect.y1 {
89            return None;
90        }
91
92        let x_end = ((x0 + self.tile_size) as i64).min(self.rect.x1) as u32;
93        let y_end = ((y0 + self.tile_size) as i64).min(self.rect.y1) as u32;
94        let w = x_end.saturating_sub(x0);
95        let h = y_end.saturating_sub(y0);
96
97        if w == 0 || h == 0 {
98            return None;
99        }
100
101        let tile = Tile { x: x0, y: y0, w, h };
102
103        // Advance to the next position.
104        let next_x = x0 + self.tile_size;
105        if (next_x as i64) < self.rect.x1 {
106            self.cur_x = next_x;
107        } else {
108            // Move to the next row.
109            self.cur_x = self.rect.x0 as u32;
110            self.cur_y = y0 + self.tile_size;
111            if (self.cur_y as i64) >= self.rect.y1 {
112                self.done = true;
113            }
114        }
115
116        Some(tile)
117    }
118}
119
120/// Return an iterator over `DEFAULT_TILE_SIZE`-sized tiles covering `rect`,
121/// clamped to the framebuffer dimensions `fb_w × fb_h`.
122pub fn tiles_for(rect: ClipRect, fb_w: u32, fb_h: u32) -> TileIter {
123    TileIter::new(rect, fb_w, fb_h, DEFAULT_TILE_SIZE)
124}
125
126/// Apply closure `f` to each tile covering `rect`.
127///
128/// Currently serial. Designed to be swapped for a rayon parallel iterator
129/// when `rayon` becomes a workspace dependency.
130pub fn render_tiles<F>(rect: ClipRect, fb_w: u32, fb_h: u32, mut f: F)
131where
132    F: FnMut(Tile),
133{
134    for tile in tiles_for(rect, fb_w, fb_h) {
135        f(tile);
136    }
137}
138
139/// Collect all tiles covering `rect` into an owned `Vec<Tile>`.
140///
141/// Useful when you need to iterate multiple times or share the list across
142/// threads.
143pub fn collect_tiles(rect: ClipRect, fb_w: u32, fb_h: u32) -> Vec<Tile> {
144    tiles_for(rect, fb_w, fb_h).collect()
145}
146
147/// Render tiles in parallel using rayon, collecting per-tile pixel buffers.
148///
149/// Each tile is rendered independently into its own `Vec<u32>` scratch buffer
150/// by calling `render_fn(tile)`.  The results are returned in the same order
151/// as `tiles`, ready for the caller to merge back into the main framebuffer.
152///
153/// The merge pass is the caller's responsibility.  A typical merge:
154/// ```ignore
155/// for (tile, pixels) in results {
156///     for row in 0..tile.h {
157///         let src_start = (row * tile.w) as usize;
158///         let dst_start = ((tile.y + row) * fb_w + tile.x) as usize;
159///         fb_pixels[dst_start..dst_start + tile.w as usize]
160///             .copy_from_slice(&pixels[src_start..src_start + tile.w as usize]);
161///     }
162/// }
163/// ```
164///
165/// # Behaviour when `parallel` feature is disabled
166///
167/// This function is only available under `#[cfg(feature = "parallel")]`.
168/// The sequential equivalent is [`render_tiles`] / [`collect_tiles`].
169#[cfg(feature = "parallel")]
170pub fn render_parallel<F>(tiles: &[Tile], render_fn: F) -> Vec<(Tile, Vec<u32>)>
171where
172    F: Fn(&Tile) -> Vec<u32> + Send + Sync,
173{
174    use rayon::prelude::*;
175    tiles
176        .par_iter()
177        .map(|tile| (*tile, render_fn(tile)))
178        .collect()
179}
180
181// ---------------------------------------------------------------------------
182// DirtyRegion
183// ---------------------------------------------------------------------------
184
185/// Tracks which tiles of the framebuffer have been modified since the last
186/// clear, enabling callers to skip re-compositing unchanged tiles.
187///
188/// On construction every tile is marked dirty so the first repaint always
189/// paints the full surface. After [`DirtyRegion::clear_all`] only tiles
190/// touched by [`DirtyRegion::mark_rect`] or [`DirtyRegion::mark_tile`] are
191/// reported as dirty.
192#[derive(Debug, Clone)]
193pub struct DirtyRegion {
194    /// Width of the framebuffer in tiles.
195    tiles_wide: u32,
196    /// Height of the framebuffer in tiles.
197    tiles_tall: u32,
198    /// Bit set: `dirty[ty * tiles_wide + tx]` is `true` if that tile is dirty.
199    dirty: Vec<bool>,
200    /// `true` if every tile is dirty (avoids per-tile iteration on full invalidation).
201    all_dirty: bool,
202}
203
204impl DirtyRegion {
205    /// Create a new [`DirtyRegion`] for a framebuffer of `fb_width × fb_height`
206    /// pixels using `tile_size`-pixel tiles.
207    ///
208    /// All tiles start dirty so the first frame is always fully rendered.
209    pub fn new(fb_width: u32, fb_height: u32, tile_size: u32) -> Self {
210        let ts = tile_size.max(1);
211        let tw = fb_width.div_ceil(ts);
212        let th = fb_height.div_ceil(ts);
213        Self {
214            tiles_wide: tw,
215            tiles_tall: th,
216            dirty: vec![true; (tw * th) as usize],
217            all_dirty: true,
218        }
219    }
220
221    /// Mark the pixel rectangle `(x, y, width, height)` as dirty, dirtying
222    /// every tile that overlaps it.
223    ///
224    /// If all tiles are already dirty this is a no-op.
225    pub fn mark_rect(&mut self, x: u32, y: u32, width: u32, height: u32, tile_size: u32) {
226        if self.all_dirty {
227            return;
228        }
229        let ts = tile_size.max(1);
230        let tx_start = x / ts;
231        let ty_start = y / ts;
232        let tx_end = (x + width).div_ceil(ts);
233        let ty_end = (y + height).div_ceil(ts);
234        for ty in ty_start..ty_end.min(self.tiles_tall) {
235            for tx in tx_start..tx_end.min(self.tiles_wide) {
236                let idx = (ty * self.tiles_wide + tx) as usize;
237                if idx < self.dirty.len() {
238                    self.dirty[idx] = true;
239                }
240            }
241        }
242    }
243
244    /// Mark the single tile at `(tx, ty)` as dirty.
245    ///
246    /// If all tiles are already dirty this is a no-op.
247    pub fn mark_tile(&mut self, tx: u32, ty: u32) {
248        if self.all_dirty {
249            return;
250        }
251        let idx = (ty * self.tiles_wide + tx) as usize;
252        if idx < self.dirty.len() {
253            self.dirty[idx] = true;
254        }
255    }
256
257    /// Return `true` if the tile at `(tx, ty)` needs to be re-rendered.
258    pub fn is_tile_dirty(&self, tx: u32, ty: u32) -> bool {
259        if self.all_dirty {
260            return true;
261        }
262        let idx = (ty * self.tiles_wide + tx) as usize;
263        self.dirty.get(idx).copied().unwrap_or(false)
264    }
265
266    /// Clear all dirty flags after a completed repaint.
267    ///
268    /// After this call [`DirtyRegion::dirty_count`] returns `0` until a tile
269    /// is explicitly marked dirty again.
270    pub fn clear_all(&mut self) {
271        self.dirty.fill(false);
272        self.all_dirty = false;
273    }
274
275    /// Mark every tile as dirty.
276    ///
277    /// Use after a resize or any full framebuffer invalidation.
278    pub fn invalidate_all(&mut self) {
279        self.dirty.fill(true);
280        self.all_dirty = true;
281    }
282
283    /// Iterate over the `(tx, ty)` coordinates of all dirty tiles.
284    pub fn dirty_tiles(&self) -> impl Iterator<Item = (u32, u32)> + '_ {
285        (0..self.tiles_tall).flat_map(move |ty| {
286            (0..self.tiles_wide).filter_map(move |tx| {
287                if self.is_tile_dirty(tx, ty) {
288                    Some((tx, ty))
289                } else {
290                    None
291                }
292            })
293        })
294    }
295
296    /// Return the number of tiles currently marked dirty.
297    pub fn dirty_count(&self) -> usize {
298        if self.all_dirty {
299            (self.tiles_wide * self.tiles_tall) as usize
300        } else {
301            self.dirty.iter().filter(|&&d| d).count()
302        }
303    }
304
305    /// Return the total number of tiles in the framebuffer.
306    pub fn total_tiles(&self) -> usize {
307        (self.tiles_wide * self.tiles_tall) as usize
308    }
309}
310
311// ---------------------------------------------------------------------------
312// Tests
313// ---------------------------------------------------------------------------
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    #[test]
320    fn tiles_cover_full_frame() {
321        let w = 200u32;
322        let h = 150u32;
323        let rect = ClipRect::full(w, h);
324        let tiles: Vec<Tile> = tiles_for(rect, w, h).collect();
325
326        // All tiles combined must cover the full framebuffer with no overlaps.
327        let mut coverage = vec![0u32; (w * h) as usize];
328        for tile in &tiles {
329            for ty in tile.y..tile.y + tile.h {
330                for tx in tile.x..tile.x + tile.w {
331                    coverage[(ty * w + tx) as usize] += 1;
332                }
333            }
334        }
335        for (i, &c) in coverage.iter().enumerate() {
336            assert_eq!(c, 1, "pixel {i} covered {c} times (expected exactly 1)");
337        }
338    }
339
340    #[test]
341    fn tile_covers_frame() {
342        // The tiles for a full framebuffer should have no gaps and no overlaps.
343        let w = 64u32;
344        let h = 64u32;
345        let rect = ClipRect::full(w, h);
346        let tiles: Vec<Tile> = tiles_for(rect, w, h).collect();
347        // A 64x64 buffer with 64x64 tiles should yield exactly 1 tile.
348        assert_eq!(tiles.len(), 1);
349        assert_eq!(
350            tiles[0],
351            Tile {
352                x: 0,
353                y: 0,
354                w: 64,
355                h: 64
356            }
357        );
358    }
359
360    #[test]
361    fn tiles_non_overlapping() {
362        // 128x128 buffer → 4 tiles of 64x64.
363        let w = 128u32;
364        let h = 128u32;
365        let rect = ClipRect::full(w, h);
366        let tiles: Vec<Tile> = tiles_for(rect, w, h).collect();
367        assert_eq!(tiles.len(), 4);
368        // Verify no two tiles share a pixel.
369        let mut coverage = vec![0u32; (w * h) as usize];
370        for tile in &tiles {
371            for ty in tile.y..tile.y + tile.h {
372                for tx in tile.x..tile.x + tile.w {
373                    coverage[(ty * w + tx) as usize] += 1;
374                }
375            }
376        }
377        for &c in &coverage {
378            assert_eq!(c, 1, "every pixel must be covered exactly once");
379        }
380    }
381
382    #[test]
383    fn partial_boundary_tiles() {
384        // 70x70 buffer → boundary tiles are smaller than 64x64.
385        let w = 70u32;
386        let h = 70u32;
387        let rect = ClipRect::full(w, h);
388        let tiles: Vec<Tile> = tiles_for(rect, w, h).collect();
389        // Should have 4 tiles: 64x64, 6x64, 64x6, 6x6.
390        assert_eq!(tiles.len(), 4);
391        let mut coverage = vec![0u32; (w * h) as usize];
392        for tile in &tiles {
393            for ty in tile.y..tile.y + tile.h {
394                for tx in tile.x..tile.x + tile.w {
395                    coverage[(ty * w + tx) as usize] += 1;
396                }
397            }
398        }
399        for (i, &c) in coverage.iter().enumerate() {
400            assert_eq!(c, 1, "pixel {i} covered {c} times");
401        }
402    }
403
404    #[test]
405    fn tile_clip_rect_correct() {
406        let tile = Tile {
407            x: 64,
408            y: 128,
409            w: 32,
410            h: 16,
411        };
412        let clip = tile.clip_rect();
413        assert_eq!(clip.x0, 64);
414        assert_eq!(clip.y0, 128);
415        assert_eq!(clip.x1, 96);
416        assert_eq!(clip.y1, 144);
417    }
418
419    #[test]
420    fn render_tiles_visits_all() {
421        let w = 100u32;
422        let h = 100u32;
423        let rect = ClipRect::full(w, h);
424        let mut count = 0u32;
425        render_tiles(rect, w, h, |_tile| {
426            count += 1;
427        });
428        // 100x100 with 64x64 tiles: 2×2 = 4 tiles.
429        assert_eq!(count, 4);
430    }
431
432    #[test]
433    fn collect_tiles_returns_same_as_iterator() {
434        let w = 128u32;
435        let h = 128u32;
436        let rect = ClipRect::full(w, h);
437        let via_iter: Vec<Tile> = tiles_for(rect, w, h).collect();
438        let via_collect = collect_tiles(rect, w, h);
439        assert_eq!(via_iter, via_collect);
440    }
441
442    /// Parallel rendering produces the same output as sequential for a
443    /// deterministic render function (fill each tile with a constant colour).
444    #[cfg(feature = "parallel")]
445    #[test]
446    fn parallel_output_matches_sequential() {
447        let w = 128u32;
448        let h = 128u32;
449        let rect = ClipRect::full(w, h);
450
451        // A deterministic render function: fill with a per-tile constant
452        // derived from the tile's (x, y) position — no shared mutable state.
453        let render_fn = |tile: &Tile| -> Vec<u32> {
454            let colour: u32 =
455                0xFF000000 | ((tile.x & 0xFF) << 16) | ((tile.y & 0xFF) << 8) | (tile.w & 0xFF);
456            vec![colour; (tile.w * tile.h) as usize]
457        };
458
459        // Sequential baseline.
460        let tiles = collect_tiles(rect, w, h);
461        let sequential: Vec<(Tile, Vec<u32>)> = tiles.iter().map(|t| (*t, render_fn(t))).collect();
462
463        // Parallel run.
464        let parallel = super::render_parallel(&tiles, render_fn);
465
466        // Sort both by tile position to ensure order independence.
467        let mut seq_sorted = sequential;
468        let mut par_sorted = parallel;
469        seq_sorted.sort_by_key(|(t, _)| (t.y, t.x));
470        par_sorted.sort_by_key(|(t, _)| (t.y, t.x));
471
472        assert_eq!(seq_sorted.len(), par_sorted.len(), "tile count mismatch");
473        for ((st, sp), (pt, pp)) in seq_sorted.iter().zip(par_sorted.iter()) {
474            assert_eq!(st, pt, "tile metadata mismatch");
475            assert_eq!(sp, pp, "pixel data mismatch for tile ({},{})", st.x, st.y);
476        }
477    }
478}