1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272
//! This crate defines a framebuffer compositor.
//!
//! A framebuffer compositor composites a list of framebuffers into a single destination framebuffer.
//! The coordinate system within a framebuffer is expressed relative to its origin, i.e., the top-left point.
//!
//! # Cache
//! The compositor caches groups of framebuffer rows for better performance.
//!
//! First, it divides each framebuffer into ranges of rows called "blocks" which are `CACHE_BLOCK_HEIGHT` rows in height,
//! and deals with these row ranges one by one.
//! The pixels in each block's row range are a contiguous array of length `CACHE_BLOCK_HEIGHT * framebuffer_width`,
//! and the cache key is the hash value of that pixel array.
//!
//! In the next step, for every `CACHE_BLOCK_HEIGHT` rows, the compositor checks if the pixel array is are already cached.
//! It ignores row ranges that do not overlap with the given bounding box to be updated.
//! If a pixel array is not cached, the compositor will refresh the pixels within the bounding box and cache those `CACHE_BLOCK_HEIGHT` rows.
//!
//! In order to cache a range of rows from the source framebuffer, the compositor needs to cache its contents, its location in the destination framebuffer, and its size.
//! The cache is basically a rectangular region in the destination framebuffer, and we define the structure `CacheBlock` to represent that cached region.
#![no_std]
extern crate alloc;
extern crate compositor;
extern crate framebuffer;
extern crate spin;
extern crate hashbrown;
extern crate shapes;
use alloc::collections::BTreeMap;
use alloc::vec::{Vec};
use core::hash::{Hash, BuildHasher};
use hashbrown::hash_map::{DefaultHashBuilder};
use compositor::{Compositor, FramebufferUpdates, CompositableRegion};
use framebuffer::{Framebuffer, Pixel};
use shapes::{Coord, Rectangle};
use spin::Mutex;
use core::ops::Range;
/// The height of a cache block. In every iteration the compositor will deal with groups of 16 rows and cache them.
pub const CACHE_BLOCK_HEIGHT: usize = 16;
/// The instance of the framebuffer compositor.
pub static FRAME_COMPOSITOR: Mutex<FrameCompositor> = Mutex::new(
FrameCompositor{
caches: BTreeMap::new()
}
);
/// A `CacheBlock` represents the cached (previously-composited) content of a range of rows in the source framebuffer.
/// It specifies the rectangular region in the destination framebuffer and the hash.
/// Once cached, a `CacheBlock` block is independent of the source framebuffer it came from.
/// `content_hash` is the hash value of the actual pixel contents in the cached block. A cache block is identical to some new framebuffer rows to be updated if they share the same `content_hash`, location and width.
pub struct CacheBlock {
/// The rectanglular region in the destination framebuffer occupied by the cached rows in the source framebuffer.
/// We need this information because if an old cache block overlaps with some new framebuffer rows to be updated,
/// the compositor should remove the old one since part of that region will change.
block: Rectangle,
/// The hash value of the actual pixel contents in the cached block.
content_hash: u64,
}
impl CacheBlock {
/// Checks if a cache block overlaps with another one
pub fn overlaps_with(&self, cache: &CacheBlock) -> bool {
self.contains_corner(cache) || cache.contains_corner(self)
}
/// checks if the coordinate is within the block
fn contains(&self, coordinate: Coord) -> bool {
coordinate.x >= self.block.top_left.x
&& coordinate.x < self.block.bottom_right.x
&& coordinate.y >= self.block.top_left.y
&& coordinate.y < self.block.bottom_right.y
}
/// checks if this block contains any of the four corners of another cache block.
fn contains_corner(&self, cache: &CacheBlock) -> bool {
self.contains(cache.block.top_left)
|| self.contains(cache.block.top_left + (cache.block.bottom_right.x - cache.block.top_left.x - 1, 0))
|| self.contains(cache.block.top_left + (0, cache.block.bottom_right.y - cache.block.top_left.y - 1))
|| self.contains(cache.block.bottom_right - (1, 1))
}
}
/// The framebuffer compositor structure.
/// It caches framebuffer rows since last update as soft states for better performance.
pub struct FrameCompositor {
// Cache of updated framebuffers before
caches: BTreeMap<Coord, CacheBlock>,
}
impl FrameCompositor {
/// Checks if some rows of a framebuffer are cached.
/// # Arguments
/// * `row_pixels`: the continuous pixels in the rows.
/// * `dest_coord`: the location of the first pixel in the destination framebuffer.
/// * `width`: the width of the rows
///
fn is_cached<P: Pixel>(&self, row_pixels: &[P], dest_coord: &Coord, width: usize) -> bool {
match self.caches.get(dest_coord) {
Some(cache) => {
// The same hash and width means the cache block is identical to the row pixels.
// We do not check the height because if the hashes are the same, the number of pixels, namely `width * height` must be the same.
cache.content_hash == hash(row_pixels) && (cache.block.bottom_right.x - cache.block.top_left.x) as usize == width
}
None => false
}
}
/// This function will return true if several continuous rows in the framebuffer are cached.
/// If false, i.e. the given `row_range` is not in the cache, this function will remove
/// the old cached blocks that overlap with the rows in the given `src_fb_row_range` and cache those rows as a new cache block.
/// # Arguments
/// * `src_fb`: the updated source framebuffer.
/// * `dest_coord`: the position of the source framebuffer (its top-left corner) relative to the destination framebuffer's top-left corner.
/// * `src_fb_row_range`: the range of rows in the source framebuffer to check and cache.
fn check_and_cache<P: Pixel>(
&mut self,
src_fb: &Framebuffer<P>,
dest_coord: Coord,
src_fb_row_range: &Range<usize>,
) -> Result<bool, &'static str> {
let (src_width, src_height) = src_fb.get_size();
let src_buffer_len = src_width * src_height;
// The start pixel of the rows
let start_index = src_width * src_fb_row_range.start;
let coordinate_start = dest_coord + (0, src_fb_row_range.start as isize);
// The end pixel of the rows
let end_index = src_width * src_fb_row_range.end;
let pixel_slice = &src_fb.buffer()[start_index..core::cmp::min(end_index, src_buffer_len)];
// Skip if the rows are already cached
if self.is_cached(pixel_slice, &coordinate_start, src_width) {
return Ok(true);
}
// remove overlapped caches
let new_cache = CacheBlock {
block: Rectangle {
top_left: coordinate_start,
bottom_right: coordinate_start + (src_width as isize, (pixel_slice.len() / src_width) as isize)
},
content_hash: hash(pixel_slice),
};
let keys: Vec<_> = self.caches.keys().cloned().collect();
for key in keys {
if let Some(cache) = self.caches.get_mut(&key) {
if cache.overlaps_with(&new_cache) {
self.caches.remove(&key);
}
};
}
self.caches.insert(coordinate_start, new_cache);
Ok(false)
}
/// Returns the range of rows in the source framebuffer that were (1) previously cached as cache blocks
/// and (2) overlap with the given `dest_bounding_box`.
/// This methods extends the row range of the given bounding box because the compositor deals with chunks of `CACHE_BLOCK_HEIGHT` rows.
/// # Arguments
/// * `dest_coord`: the position in the destination framebuffer (relative to its top-left corner)
/// to where the source framebuffer will be composited.
/// * `dest_bounding_box`: the region of the destination framebuffer that should be composited.
/// * `src_fb_height`: the height of the source framebuffer.
fn get_cache_row_range<B: CompositableRegion>(
&self,
dest_coord: Coord,
dest_bounding_box: &B,
src_fb_height: usize,
) -> Range<usize> {
let abs_row_range = dest_bounding_box.row_range();
let mut relative_row_start = abs_row_range.start - dest_coord.y;
let mut relative_row_end = abs_row_range.end - dest_coord.y;
relative_row_start = core::cmp::max(relative_row_start, 0);
relative_row_end = core::cmp::min(relative_row_end, src_fb_height as isize);
if relative_row_start >= relative_row_end {
return 0..0;
}
let cache_row_start = relative_row_start as usize / CACHE_BLOCK_HEIGHT * CACHE_BLOCK_HEIGHT;
let mut cache_row_end = ((relative_row_end - 1) as usize / CACHE_BLOCK_HEIGHT + 1) * CACHE_BLOCK_HEIGHT;
cache_row_end = core::cmp::min(cache_row_end, src_fb_height);
cache_row_start..cache_row_end
}
}
impl Compositor for FrameCompositor {
fn composite<'a, B: CompositableRegion + Clone, P: 'a + Pixel>(
&mut self,
src_fbs: impl IntoIterator<Item = FramebufferUpdates<'a, P>>,
dest_fb: &mut Framebuffer<P>,
dest_bounding_boxes: impl IntoIterator<Item = B> + Clone,
) -> Result<(), &'static str> {
let mut box_iter = dest_bounding_boxes.clone().into_iter();
if box_iter.next().is_none() {
for framebuffer_updates in src_fbs.into_iter() {
let src_fb = framebuffer_updates.src_framebuffer;
let coordinate = framebuffer_updates.coordinate_in_dest_framebuffer;
// Update the whole screen if the caller does not specify the blocks
let (src_width, src_height) = framebuffer_updates.src_framebuffer.get_size();
// let block_number = (src_height - 1) / CACHE_BLOCK_HEIGHT + 1;
let area = Rectangle {
top_left: coordinate,
bottom_right: coordinate + (src_width as isize, src_height as isize)
};
let mut row_start = 0;
loop {
if row_start >= src_height {
break;
}
let cache_range = row_start..(row_start + CACHE_BLOCK_HEIGHT);
if !self.check_and_cache(src_fb, coordinate, &cache_range)? {
area.blend_buffers(
src_fb,
dest_fb,
coordinate,
cache_range,
)?;
}
row_start += CACHE_BLOCK_HEIGHT;
}
}
} else {
for framebuffer_updates in src_fbs.into_iter() {
//let mut updated_blocks = Vec::new();
for bounding_box in dest_bounding_boxes.clone() {
let src_fb = framebuffer_updates.src_framebuffer;
let coordinate = framebuffer_updates.coordinate_in_dest_framebuffer;
let (_, height) = src_fb.get_size();
let mut row_range = self.get_cache_row_range(coordinate, &bounding_box, height);
// let cache_block_size = CACHE_BLOCK_HEIGHT * width;
loop {
if row_range.start >= row_range.end {
break;
}
let cache_range = row_range.start..(row_range.start + CACHE_BLOCK_HEIGHT);
// check cache if the bounding box is not a single pixel
if bounding_box.size() > 1 && self.check_and_cache(src_fb, coordinate, &cache_range)? {
row_range.start += CACHE_BLOCK_HEIGHT;
continue;
};
bounding_box.blend_buffers(
src_fb,
dest_fb,
coordinate,
cache_range,
)?;
row_range.start += CACHE_BLOCK_HEIGHT;
}
}
}
}
Ok(())
}
}
/// Gets the hash of an item
fn hash<T: Hash>(item: T) -> u64 {
DefaultHashBuilder::default().hash_one(&item)
}