use crate::render::Rgba;
use crate::render_config::RenderConfig;
pub const TILE_SIZE: u32 = 8;
pub const BYTES_PER_PIXEL: usize = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DirtyRegion {
pub x: i32,
pub y: i32,
pub width: u32,
pub height: u32,
pub present: bool,
}
impl DirtyRegion {
pub fn empty() -> Self {
Self { x: 0, y: 0, width: 0, height: 0, present: false }
}
pub fn full(width: u32, height: u32) -> Self {
Self { x: 0, y: 0, width, height, present: true }
}
pub fn new(x: i32, y: i32, width: u32, height: u32) -> Self {
Self { x, y, width, height, present: true }
}
pub fn union(&self, other: &DirtyRegion) -> DirtyRegion {
if !self.present {
return *other;
}
if !other.present {
return *self;
}
let x1 = self.x.min(other.x);
let y1 = self.y.min(other.y);
let x2 = (self.x + self.width as i32).max(other.x + other.width as i32);
let y2 = (self.y + self.height as i32).max(other.y + other.height as i32);
DirtyRegion::new(x1, y1, (x2 - x1).max(0) as u32, (y2 - y1).max(0) as u32)
}
#[inline]
pub fn contains_pixel(&self, px: u32, py: u32) -> bool {
if !self.present {
return false;
}
px as i32 >= self.x
&& (px as i32) < self.x + self.width as i32
&& py as i32 >= self.y
&& (py as i32) < self.y + self.height as i32
}
pub fn to_tile_rect(&self, tile_size: u32) -> (i32, i32, u32, u32) {
if !self.present {
return (0, 0, 0, 0);
}
let ts = tile_size as i32;
let tx = if self.x >= 0 { self.x / ts } else { (self.x - ts + 1) / ts };
let ty = if self.y >= 0 { self.y / ts } else { (self.y - ts + 1) / ts };
let right = self.x + self.width as i32;
let bottom = self.y + self.height as i32;
let tw = ((right + ts - 1) / ts - tx).max(0) as u32;
let th = ((bottom + ts - 1) / ts - ty).max(0) as u32;
(tx, ty, tw, th)
}
}
impl Default for DirtyRegion {
fn default() -> Self {
Self::empty()
}
}
#[derive(Debug, Clone)]
pub struct FrameBuffer {
pub data: Vec<u8>,
pub width: u32,
pub height: u32,
pub dirty_region: DirtyRegion,
}
impl FrameBuffer {
pub fn new(config: RenderConfig, clear_color: Rgba) -> Self {
let fb_size = (config.screen_width as usize)
* (config.screen_height as usize)
* BYTES_PER_PIXEL;
let mut fb = Self {
data: vec![0; fb_size],
width: config.screen_width,
height: config.screen_height,
dirty_region: DirtyRegion::full(config.screen_width, config.screen_height),
};
fb.clear(clear_color);
fb
}
pub fn mark_all_dirty(&mut self) {
self.dirty_region = DirtyRegion::full(self.width, self.height);
}
pub fn mark_dirty_rect(&mut self, x: i32, y: i32, w: u32, h: u32) {
let r = DirtyRegion::new(x, y, w, h);
self.dirty_region = self.dirty_region.union(&r);
}
pub fn mark_dirty_tile(&mut self, tx: i32, ty: i32) {
let tile_size = TILE_SIZE as i32;
self.mark_dirty_rect(tx * tile_size, ty * tile_size, TILE_SIZE, TILE_SIZE);
}
pub fn clear_dirty(&mut self) {
self.dirty_region = DirtyRegion::empty();
}
#[inline]
pub fn is_dirty_pixel(&self, x: u32, y: u32) -> bool {
self.dirty_region.contains_pixel(x, y)
}
#[inline]
pub fn width(&self) -> u32 {
self.width
}
#[inline]
pub fn height(&self) -> u32 {
self.height
}
pub fn clear(&mut self, color: Rgba) {
let rgba = color.to_array();
for pixel in self.data.chunks_exact_mut(BYTES_PER_PIXEL) {
pixel.copy_from_slice(&rgba);
}
}
pub fn set_pixel(&mut self, x: u32, y: u32, color: Rgba) -> bool {
if x >= self.width || y >= self.height {
return false;
}
let offset = ((y as usize) * (self.width as usize) + (x as usize)) * BYTES_PER_PIXEL;
self.data[offset..offset + BYTES_PER_PIXEL].copy_from_slice(&color.to_array());
true
}
pub fn get_pixel(&self, x: u32, y: u32) -> Option<Rgba> {
if x >= self.width || y >= self.height {
return None;
}
let offset = ((y as usize) * (self.width as usize) + (x as usize)) * BYTES_PER_PIXEL;
let mut c = [0u8; 4];
c.copy_from_slice(&self.data[offset..offset + BYTES_PER_PIXEL]);
Some(Rgba::from(c))
}
pub fn fill_rect(&mut self, x: u32, y: u32, rect_width: u32, rect_height: u32, color: Rgba) {
let x_start = x.min(self.width);
let y_start = y.min(self.height);
let x_end = (x + rect_width).min(self.width);
let y_end = (y + rect_height).min(self.height);
let rgba = color.to_array();
for row in y_start..y_end {
let row_offset = (row as usize) * (self.width as usize) * BYTES_PER_PIXEL;
for col in x_start..x_end {
let offset = row_offset + (col as usize) * BYTES_PER_PIXEL;
self.data[offset..offset + BYTES_PER_PIXEL].copy_from_slice(&rgba);
}
}
}
pub fn row_slice(&self, y: u32) -> Option<&[u8]> {
if y >= self.height {
return None;
}
let start = (y as usize) * (self.width as usize) * BYTES_PER_PIXEL;
let end = start + (self.width as usize) * BYTES_PER_PIXEL;
Some(&self.data[start..end])
}
pub fn row_slice_mut(&mut self, y: u32) -> Option<&mut [u8]> {
if y >= self.height {
return None;
}
let start = (y as usize) * (self.width as usize) * BYTES_PER_PIXEL;
let end = start + (self.width as usize) * BYTES_PER_PIXEL;
Some(&mut self.data[start..end])
}
pub fn blit_row(&mut self, x: u32, y: u32, src: &[u8], count: u32) -> bool {
if y >= self.height || x >= self.width {
return false;
}
let actual_count = count.min(self.width - x) as usize;
let src_bytes = actual_count * BYTES_PER_PIXEL;
if src.len() < src_bytes {
return false;
}
let offset = ((y as usize) * (self.width as usize) + (x as usize)) * BYTES_PER_PIXEL;
self.data[offset..offset + src_bytes].copy_from_slice(&src[..src_bytes]);
true
}
pub fn save_png(&self, path: &std::path::Path) -> std::io::Result<()> {
use image::{ImageBuffer, Rgba as ImgRgba};
let img: ImageBuffer<ImgRgba<u8>, _> =
ImageBuffer::from_raw(self.width, self.height, self.data.clone())
.expect("FrameBuffer data size mismatch");
img.save(path)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
}
}