use std::sync::Arc;
use std::sync::Weak;
use tokio::sync::RwLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DirtyRegion {
pub x: u16,
pub y: u16,
pub width: u16,
pub height: u16,
}
impl DirtyRegion {
#[must_use]
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
Self {
x,
y,
width,
height,
}
}
#[must_use]
pub fn merge(&self, other: &DirtyRegion) -> DirtyRegion {
let x1 = self.x.min(other.x);
let y1 = self.y.min(other.y);
let x2 = self
.x
.saturating_add(self.width)
.max(other.x.saturating_add(other.width));
let y2 = self
.y
.saturating_add(self.height)
.max(other.y.saturating_add(other.height));
DirtyRegion {
x: x1,
y: y1,
width: x2.saturating_sub(x1),
height: y2.saturating_sub(y1),
}
}
#[must_use]
pub fn intersects(&self, other: &DirtyRegion) -> bool {
let x1 = self.x.max(other.x);
let y1 = self.y.max(other.y);
let x2 = self
.x
.saturating_add(self.width)
.min(other.x.saturating_add(other.width));
let y2 = self
.y
.saturating_add(self.height)
.min(other.y.saturating_add(other.height));
x1 < x2 && y1 < y2
}
#[must_use]
pub fn intersect(&self, other: &DirtyRegion) -> Option<DirtyRegion> {
let x1 = self.x.max(other.x);
let y1 = self.y.max(other.y);
let x2 = self
.x
.saturating_add(self.width)
.min(other.x.saturating_add(other.width));
let y2 = self
.y
.saturating_add(self.height)
.min(other.y.saturating_add(other.height));
if x1 < x2 && y1 < y2 {
Some(DirtyRegion {
x: x1,
y: y1,
width: x2.saturating_sub(x1),
height: y2.saturating_sub(y1),
})
} else {
None
}
}
}
#[derive(Clone)]
pub struct DirtyRegionReceiver {
regions: Weak<RwLock<Vec<DirtyRegion>>>,
}
impl DirtyRegionReceiver {
#[must_use]
pub fn new(regions: Weak<RwLock<Vec<DirtyRegion>>>) -> Self {
Self { regions }
}
pub async fn add_dirty_region(&self, region: DirtyRegion) {
const MAX_REGIONS: usize = 10;
const MAX_TOTAL_PIXELS: usize = 1920 * 1080 * 2;
if let Some(regions_arc) = self.regions.upgrade() {
let mut regions = regions_arc.write().await;
let mut merged_region = region;
regions.retain(|existing| {
if existing.intersects(&merged_region) {
merged_region = existing.merge(&merged_region);
false } else {
true }
});
regions.push(merged_region);
let total_pixels: usize = regions
.iter()
.map(|r| (r.width as usize) * (r.height as usize))
.sum();
if regions.len() > MAX_REGIONS || total_pixels > MAX_TOTAL_PIXELS {
if let Some(first) = regions.first().copied() {
let merged = regions.iter().skip(1).fold(first, |acc, r| acc.merge(r));
regions.clear();
regions.push(merged);
}
}
}
}
}
use std::sync::atomic::{AtomicU16, Ordering as AtomicOrdering};
#[derive(Clone)]
pub struct Framebuffer {
width: Arc<AtomicU16>,
height: Arc<AtomicU16>,
data: Arc<RwLock<Vec<u8>>>,
receivers: Arc<RwLock<Vec<DirtyRegionReceiver>>>,
prev_data: Arc<RwLock<Vec<u8>>>,
}
impl Framebuffer {
#[must_use]
pub fn new(width: u16, height: u16) -> Self {
let size = (width as usize) * (height as usize) * 4; Self {
width: Arc::new(AtomicU16::new(width)),
height: Arc::new(AtomicU16::new(height)),
data: Arc::new(RwLock::new(vec![0; size])),
receivers: Arc::new(RwLock::new(Vec::new())),
prev_data: Arc::new(RwLock::new(vec![0; size])),
}
}
pub async fn register_receiver(&self, receiver: DirtyRegionReceiver) {
let mut receivers = self.receivers.write().await;
receivers.push(receiver);
}
async fn cleanup_receivers(&self) {
let mut receivers = self.receivers.write().await;
receivers.retain(|r| r.regions.strong_count() > 0);
}
pub async fn mark_dirty_region(&self, x: u16, y: u16, width: u16, height: u16) {
let region = DirtyRegion::new(x, y, width, height);
let receivers_copy = {
let receivers = self.receivers.read().await;
receivers.clone()
};
for receiver in &receivers_copy {
receiver.add_dirty_region(region).await;
}
self.cleanup_receivers().await;
}
#[must_use]
pub fn width(&self) -> u16 {
self.width.load(AtomicOrdering::Relaxed)
}
#[must_use]
pub fn height(&self) -> u16 {
self.height.load(AtomicOrdering::Relaxed)
}
#[allow(clippy::cast_possible_truncation)] pub async fn update_from_slice(&self, data: &[u8]) -> Result<(), String> {
let expected_size = (self.width() as usize) * (self.height() as usize) * 4;
if data.len() != expected_size {
return Err(format!(
"Invalid data size: expected {}, got {}",
expected_size,
data.len()
));
}
let mut fb = self.data.write().await;
let width_usize = self.width() as usize;
let row_bytes = width_usize * 4;
let mut changed = false;
let mut min_y = 0;
let mut max_y = 0;
let mut min_x = u16::MAX;
let mut max_x = 0;
if let Some(first_row) = (0..self.height() as usize).find(|&y| {
let offset = y * row_bytes;
fb[offset..offset + row_bytes] != data[offset..offset + row_bytes]
}) {
changed = true;
min_y = first_row as u16;
max_y = (min_y as usize..self.height() as usize)
.rev()
.find(|&y| {
let offset = y * row_bytes;
fb[offset..offset + row_bytes] != data[offset..offset + row_bytes]
})
.unwrap() as u16;
for y in min_y..=max_y {
for x in 0..self.width() {
let offset = ((y as usize) * width_usize + (x as usize)) * 4;
if fb[offset..offset + 4] != data[offset..offset + 4] {
min_x = min_x.min(x);
max_x = max_x.max(x);
}
}
}
}
if changed {
fb.copy_from_slice(data);
drop(fb);
self.save_state().await;
let width = max_x - min_x + 1;
let height = max_y - min_y + 1;
self.mark_dirty_region(min_x, min_y, width, height).await;
}
Ok(())
}
pub async fn get_rect(
&self,
x: u16,
y: u16,
width: u16,
height: u16,
) -> Result<Vec<u8>, String> {
if x.saturating_add(width) > self.width() || y.saturating_add(height) > self.height() {
return Err(format!(
"Rectangle out of bounds: ({}, {}, {}, {}) exceeds ({}, {})",
x,
y,
width,
height,
self.width(),
self.height()
));
}
let data = self.data.read().await;
let mut result = Vec::with_capacity((width as usize) * (height as usize) * 4);
for row in y..(y + height) {
let start = ((row as usize) * (self.width() as usize) + (x as usize)) * 4;
let end = start + (width as usize) * 4;
result.extend_from_slice(&data[start..end]);
}
Ok(result)
}
#[allow(dead_code)]
pub async fn get_full_data(&self) -> Vec<u8> {
self.data.read().await.clone()
}
pub async fn update_cropped(
&self,
data: &[u8],
crop_x: u16,
crop_y: u16,
crop_width: u16,
crop_height: u16,
) -> Result<(), String> {
if crop_x.saturating_add(crop_width) > self.width() {
return Err(format!(
"Crop region exceeds framebuffer width: {}+{} > {}",
crop_x,
crop_width,
self.width()
));
}
if crop_y.saturating_add(crop_height) > self.height() {
return Err(format!(
"Crop region exceeds framebuffer height: {}+{} > {}",
crop_y,
crop_height,
self.height()
));
}
let expected_size = (crop_width as usize) * (crop_height as usize) * 4;
if data.len() != expected_size {
return Err(format!(
"Invalid crop data size: expected {}, got {}",
expected_size,
data.len()
));
}
let mut fb = self.data.write().await;
let mut changed = false;
let mut min_x = u16::MAX;
let mut min_y = u16::MAX;
let mut max_x = 0u16;
let mut max_y = 0u16;
let crop_width_usize = crop_width as usize;
let frame_width_usize = self.width() as usize;
for y in 0..crop_height {
let src_offset = (y as usize) * crop_width_usize * 4;
let dst_offset = ((crop_y + y) as usize * frame_width_usize + crop_x as usize) * 4;
let src_row = &data[src_offset..src_offset + crop_width_usize * 4];
let dst_row = &fb[dst_offset..dst_offset + crop_width_usize * 4];
if src_row != dst_row {
let abs_y = crop_y + y;
min_y = min_y.min(abs_y);
max_y = max_y.max(abs_y);
for x in 0..crop_width {
let px_offset = x as usize * 4;
if src_row[px_offset..px_offset + 4] != dst_row[px_offset..px_offset + 4] {
let abs_x = crop_x + x;
min_x = min_x.min(abs_x);
max_x = max_x.max(abs_x);
}
}
fb[dst_offset..dst_offset + crop_width_usize * 4].copy_from_slice(src_row);
changed = true;
}
}
if changed {
let width = (max_x - min_x + 1).min(self.width() - min_x);
let height = (max_y - min_y + 1).min(self.height() - min_y);
drop(fb);
self.save_state().await;
self.mark_dirty_region(min_x, min_y, width, height).await;
}
Ok(())
}
#[allow(dead_code)]
#[allow(clippy::cast_possible_truncation)] #[allow(clippy::cast_sign_loss)] pub async fn detect_copy_rect(&self, region: &DirtyRegion) -> Option<(u16, u16)> {
const MIN_COPY_SIZE: u16 = 64;
if region.width < MIN_COPY_SIZE || region.height < MIN_COPY_SIZE {
return None;
}
let (current_region_data, prev_full_data) = {
let data = self.data.read().await;
let prev = self.prev_data.read().await;
if prev.len() != data.len() {
return None;
}
let mut current = Vec::new();
for row in region.y..(region.y + region.height) {
let start = ((row as usize) * (self.width() as usize) + (region.x as usize)) * 4;
let end = start + (region.width as usize) * 4;
current.extend_from_slice(&data[start..end]);
}
(current, prev.clone())
};
let search_offsets: Vec<(i32, i32)> = vec![
(0, -1),
(0, -2),
(0, -3),
(0, -5),
(0, -10),
(0, -20),
(0, 1),
(0, 2),
(0, 3),
(0, 5),
(0, 10),
(0, 20),
(-1, 0),
(-2, 0),
(-3, 0),
(-5, 0),
(-10, 0),
(-20, 0),
(1, 0),
(2, 0),
(3, 0),
(5, 0),
(10, 0),
(20, 0),
(-1, -1),
(1, 1),
(-1, 1),
(1, -1),
];
for (dx, dy) in search_offsets {
let src_x = i32::from(region.x) + dx;
let src_y = i32::from(region.y) + dy;
if src_x < 0
|| src_y < 0
|| (src_x as u16).saturating_add(region.width) > self.width()
|| (src_y as u16).saturating_add(region.height) > self.height()
{
continue;
}
let mut matches = true;
let sample_rows = (region.height / 4).max(1); let step_size =
((region.height as usize).max(1) / (sample_rows as usize).max(1)).max(1);
for row_idx in (0..region.height).step_by(step_size) {
let src_row = (src_y as u16) + row_idx;
let current_row_start = (row_idx as usize) * (region.width as usize) * 4;
let current_row_end = current_row_start + (region.width as usize) * 4;
let prev_row_start =
((src_row as usize) * (self.width() as usize) + (src_x as usize)) * 4;
let prev_row_end = prev_row_start + (region.width as usize) * 4;
if prev_row_end > prev_full_data.len() {
matches = false;
break;
}
if current_region_data[current_row_start..current_row_end]
!= prev_full_data[prev_row_start..prev_row_end]
{
matches = false;
break;
}
}
if matches {
return Some((src_x as u16, src_y as u16));
}
}
None
}
pub async fn save_state(&self) {
let data = self.data.read().await;
let mut prev = self.prev_data.write().await;
prev.copy_from_slice(&data);
}
pub async fn resize(&self, new_width: u16, new_height: u16) -> Result<(), String> {
const MAX_DIMENSION: u16 = 8192;
if new_width == 0 || new_height == 0 {
return Err("Framebuffer dimensions must be greater than zero".to_string());
}
if new_width > MAX_DIMENSION || new_height > MAX_DIMENSION {
return Err(format!(
"Framebuffer dimensions too large: {new_width}x{new_height} (max: {MAX_DIMENSION})"
));
}
let new_size = (new_width as usize) * (new_height as usize) * 4;
let old_width = self.width();
let old_height = self.height();
if new_width == old_width && new_height == old_height {
return Ok(());
}
let mut new_data = vec![0u8; new_size];
{
let old_data = self.data.read().await;
let copy_width = old_width.min(new_width) as usize;
let copy_height = old_height.min(new_height) as usize;
for y in 0..copy_height {
let old_offset = y * (old_width as usize) * 4;
let new_offset = y * (new_width as usize) * 4;
let len = copy_width * 4;
new_data[new_offset..new_offset + len]
.copy_from_slice(&old_data[old_offset..old_offset + len]);
}
}
{
let mut data = self.data.write().await;
*data = new_data;
}
self.width.store(new_width, AtomicOrdering::Release);
self.height.store(new_height, AtomicOrdering::Release);
{
let mut prev = self.prev_data.write().await;
*prev = vec![0u8; new_size];
}
self.mark_dirty_region(0, 0, new_width, new_height).await;
Ok(())
}
#[allow(clippy::cast_possible_truncation)] #[allow(clippy::cast_sign_loss)] pub async fn do_copy_region(
&self,
dest_x: u16,
dest_y: u16,
width: u16,
height: u16,
dx: i16,
dy: i16,
) -> Result<(), String> {
let src_x = (i32::from(dest_x) + i32::from(dx)) as u16;
let src_y = (i32::from(dest_y) + i32::from(dy)) as u16;
if dest_x.saturating_add(width) > self.width()
|| dest_y.saturating_add(height) > self.height()
{
return Err(format!(
"Destination rectangle out of bounds: ({}, {}, {}, {}) exceeds ({}, {})",
dest_x,
dest_y,
width,
height,
self.width(),
self.height()
));
}
if src_x.saturating_add(width) > self.width()
|| src_y.saturating_add(height) > self.height()
{
return Err(format!(
"Source rectangle out of bounds: ({}, {}, {}, {}) exceeds ({}, {})",
src_x,
src_y,
width,
height,
self.width(),
self.height()
));
}
let mut data = self.data.write().await;
let fb_width = self.width() as usize;
let row_bytes = width as usize * 4;
if dy < 0 {
for row in 0..height {
let src_offset = ((src_y + row) as usize * fb_width + src_x as usize) * 4;
let dest_offset = ((dest_y + row) as usize * fb_width + dest_x as usize) * 4;
data.copy_within(src_offset..src_offset + row_bytes, dest_offset);
}
} else {
for row in (0..height).rev() {
let src_offset = ((src_y + row) as usize * fb_width + src_x as usize) * 4;
let dest_offset = ((dest_y + row) as usize * fb_width + dest_x as usize) * 4;
data.copy_within(src_offset..src_offset + row_bytes, dest_offset);
}
}
drop(data);
self.save_state().await;
Ok(())
}
}