This commit is contained in:
2026-07-09 02:59:53 +03:00
commit 7ace048d94
817 changed files with 234289 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "hcie-tile"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
[lib]
crate-type = ["rlib"]
[dev-dependencies]
rstest = "0.23"
proptest = "1.5"
approx = "0.5"
serde_json = "1.0"
bincode = "1.3"
+361
View File
@@ -0,0 +1,361 @@
//! # hcie-tile
//!
//! ## Purpose
//! Sparse tile-based layer storage designed to optimize memory usage and processing speed
//! on large/high-resolution canvases (e.g., 4K or larger). Instead of maintaining flat, dense
//! buffers of raw pixel data (which can exceed 33MB per layer), this crate chunks layer data
//! into small 256x256 pixel tiles and only allocates memory for tiles that contain active pixels.
//!
//! ## Logic & Workflow
//! - The canvas is logically subdivided into a grid of 256x256 tiles (`TILE_SIZE`).
//! - Each tile holds a fixed-size `[u8; TILE_BYTES]` byte array for RGBA pixels.
//! - A hash map is used to map 2D grid coordinates (`TileKey = (u32, u32)`) to active `Tile` objects.
//! - Transparent tiles (where all alpha bytes are 0) are automatically pruned or omitted,
//! dramatically saving memory and skipping blending iterations in renderer components.
//! - Updates can be performed within specific sub-regions, culling the coordinate space to
//! only update affected tiles.
use std::collections::HashMap;
pub const TILE_SIZE: u32 = 256;
pub const TILE_BYTES: usize = (TILE_SIZE * TILE_SIZE * 4) as usize; // 262144
pub type TileKey = (u32, u32);
/// Represents a single 256x256 pixel tile holding raw RGBA pixel data.
#[derive(Clone)]
pub struct Tile {
pub pixels: [u8; TILE_BYTES],
}
impl Tile {
/// Creates a new transparent tile (all channels set to 0).
pub fn new_transparent() -> Self {
Self { pixels: [0u8; TILE_BYTES] }
}
/// Creates a new solid white tile (RGB=255, Alpha=255).
pub fn new_blank() -> Self {
Self { pixels: [255u8; TILE_BYTES] }
}
}
impl serde::Serialize for Tile {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_bytes(&self.pixels)
}
}
impl<'de> serde::Deserialize<'de> for Tile {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct TileVisitor;
impl<'de> serde::de::Visitor<'de> for TileVisitor {
type Value = Tile;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "a byte sequence of exactly {} bytes representing tile pixel data", TILE_BYTES)
}
fn visit_bytes<E: serde::de::Error>(self, v: &[u8]) -> Result<Tile, E> {
if v.len() != TILE_BYTES {
return Err(E::custom(format!(
"expected {} bytes, got {}", TILE_BYTES, v.len()
)));
}
let mut tile = Tile::new_transparent();
tile.pixels.copy_from_slice(v);
Ok(tile)
}
fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut seq: A) -> Result<Tile, A::Error> {
let mut tile = Tile::new_transparent();
let mut i = 0;
while let Some(byte) = seq.next_element()? {
if i < TILE_BYTES { tile.pixels[i] = byte; }
i += 1;
}
if i != TILE_BYTES {
use serde::de::Error;
return Err(A::Error::custom(format!(
"expected {} bytes, got {}", TILE_BYTES, i
)));
}
Ok(tile)
}
}
deserializer.deserialize_bytes(TileVisitor)
}
}
/// Sparse tile-based storage layer. Only allocates 256x256 tiles for regions with non-transparent content.
#[derive(Clone)]
pub struct TiledLayer {
tiles: HashMap<TileKey, Tile>,
width: u32,
height: u32,
}
impl TiledLayer {
/// Creates an empty tiled layer with the specified dimensions.
pub fn new(width: u32, height: u32) -> Self {
Self {
tiles: HashMap::new(),
width,
height,
}
}
/// Builds a sparse tiled layer from a dense, flat RGBA pixel buffer.
///
/// # Arguments
/// * `pixels` - Flat raw RGBA pixel slice.
/// * `width` - Layer width.
/// * `height` - Layer height.
pub fn from_dense(pixels: &[u8], width: u32, height: u32) -> Self {
let mut tl = Self::new(width, height);
if width == 0 || height == 0 { return tl; }
let tiles_x = div_ceil(width, TILE_SIZE);
let tiles_y = div_ceil(height, TILE_SIZE);
for ty in 0..tiles_y {
for tx in 0..tiles_x {
let key = (tx, ty);
let mut tile = Tile::new_transparent();
let mut has_content = false;
for y in 0..TILE_SIZE {
let gy = ty * TILE_SIZE + y;
if gy >= height { break; }
for x in 0..TILE_SIZE {
let gx = tx * TILE_SIZE + x;
if gx >= width { break; }
let src_idx = ((gy * width + gx) as usize) * 4;
let dst_idx = ((y * TILE_SIZE + x) as usize) * 4;
tile.pixels[dst_idx..dst_idx + 4].copy_from_slice(&pixels[src_idx..src_idx + 4]);
if pixels[src_idx + 3] != 0 {
has_content = true;
}
}
}
if has_content {
tl.tiles.insert(key, tile);
}
}
}
tl
}
/// Update only tiles overlapping the specified rectangular region [x0, y0) -> (x1, y1).
/// Tiles outside this region are left untouched. On large canvases, this culls the scanned
/// pixel space to avoid costly full-buffer sweeps.
///
/// When a tile already exists, only the pixels within the update region are
/// overwritten, preserving untouched pixels and avoiding a full tile
/// allocation/copy.
pub fn update_tiles_in_region(
&mut self,
pixels: &[u8],
layer_width: u32,
x0: u32,
y0: u32,
mut x1: u32,
mut y1: u32,
) {
if layer_width == 0 || self.height == 0 { return; }
x1 = x1.min(layer_width).min(self.width);
y1 = y1.min(self.height);
if x0 >= x1 || y0 >= y1 { return; }
let tile_start_x = x0 / TILE_SIZE;
let tile_start_y = y0 / TILE_SIZE;
let tile_end_x = div_ceil(x1, TILE_SIZE);
let tile_end_y = div_ceil(y1, TILE_SIZE);
for ty in tile_start_y..tile_end_y {
for tx in tile_start_x..tile_end_x {
let key = (tx, ty);
let tile_ox = tx * TILE_SIZE;
let tile_oy = ty * TILE_SIZE;
// Determine the sub-rectangle of this tile that intersects the
// update region and the layer bounds.
let x_start_in_tile = x0.saturating_sub(tile_ox).min(TILE_SIZE);
let y_start_in_tile = y0.saturating_sub(tile_oy).min(TILE_SIZE);
let x_end_in_tile = (x1 - tile_ox).min(TILE_SIZE);
let y_end_in_tile = (y1 - tile_oy).min(TILE_SIZE);
if x_start_in_tile >= x_end_in_tile || y_start_in_tile >= y_end_in_tile {
continue;
}
let mut has_content = false;
let entry = self.tiles.entry(key);
match entry {
std::collections::hash_map::Entry::Occupied(mut occupied) => {
let tile = occupied.get_mut();
for y in y_start_in_tile..y_end_in_tile {
let gy = tile_oy + y;
if gy >= self.height { break; }
for x in x_start_in_tile..x_end_in_tile {
let gx = tile_ox + x;
if gx >= layer_width || gx >= self.width { break; }
let src_idx = ((gy * layer_width + gx) as usize) * 4;
let dst_idx = ((y * TILE_SIZE + x) as usize) * 4;
tile.pixels[dst_idx..dst_idx + 4].copy_from_slice(&pixels[src_idx..src_idx + 4]);
if pixels[src_idx + 3] != 0 {
has_content = true;
}
}
}
if !has_content {
// Scan the rest of the tile for any surviving content
// before deciding to drop it.
has_content = tile_has_any_content(tile);
}
if !has_content {
occupied.remove();
}
}
std::collections::hash_map::Entry::Vacant(vacant) => {
let mut tile = Tile::new_transparent();
for y in y_start_in_tile..y_end_in_tile {
let gy = tile_oy + y;
if gy >= self.height { break; }
for x in x_start_in_tile..x_end_in_tile {
let gx = tile_ox + x;
if gx >= layer_width || gx >= self.width { break; }
let src_idx = ((gy * layer_width + gx) as usize) * 4;
let dst_idx = ((y * TILE_SIZE + x) as usize) * 4;
tile.pixels[dst_idx..dst_idx + 4].copy_from_slice(&pixels[src_idx..src_idx + 4]);
if pixels[src_idx + 3] != 0 {
has_content = true;
}
}
}
if has_content {
vacant.insert(tile);
}
}
}
}
}
}
/// Returns the number of allocated tiles.
pub fn tile_count(&self) -> usize {
self.tiles.len()
}
/// Returns the layer width.
pub fn width(&self) -> u32 { self.width }
/// Returns the layer height.
pub fn height(&self) -> u32 { self.height }
/// Returns a reference to the active tiles map.
pub fn tiles(&self) -> &HashMap<TileKey, Tile> { &self.tiles }
/// Gets a reference to a specific tile at coordinates (tx, ty), if it exists.
pub fn get_tile(&self, tx: u32, ty: u32) -> Option<&Tile> {
self.tiles.get(&(tx, ty))
}
/// Computes the tile key (tx, ty) corresponding to the absolute pixel coordinate (x, y).
pub fn tile_key(x: u32, y: u32) -> TileKey {
(x / TILE_SIZE, y / TILE_SIZE)
}
/// Gets the pixel at coordinates (x, y). Returns transparent black if the tile does not exist.
pub fn get_pixel(&self, x: u32, y: u32) -> [u8; 4] {
let key = Self::tile_key(x, y);
match self.tiles.get(&key) {
Some(tile) => {
let lx = (x % TILE_SIZE) as usize;
let ly = (y % TILE_SIZE) as usize;
let idx = (ly * TILE_SIZE as usize + lx) * 4;
[tile.pixels[idx], tile.pixels[idx + 1], tile.pixels[idx + 2], tile.pixels[idx + 3]]
}
None => [0u8; 4],
}
}
/// Sets the pixel at coordinates (x, y), dynamically allocating a tile if it doesn't already exist.
pub fn set_pixel(&mut self, x: u32, y: u32, rgba: [u8; 4]) {
if x >= self.width || y >= self.height { return; }
let key = Self::tile_key(x, y);
let tile = self.tiles.entry(key).or_insert_with(Tile::new_transparent);
let lx = (x % TILE_SIZE) as usize;
let ly = (y % TILE_SIZE) as usize;
let idx = (ly * TILE_SIZE as usize + lx) * 4;
tile.pixels[idx..idx + 4].copy_from_slice(&rgba);
}
/// Expands the sparse tile storage into a flat, dense RGBA pixel buffer.
pub fn to_dense(&self) -> Vec<u8> {
let size = (self.width * self.height * 4) as usize;
let mut out = vec![0u8; size];
for ((tx, ty), tile) in &self.tiles {
for y in 0..TILE_SIZE {
let gy = ty * TILE_SIZE + y;
if gy >= self.height { break; }
for x in 0..TILE_SIZE {
let gx = tx * TILE_SIZE + x;
if gx >= self.width { break; }
let src_idx = ((y * TILE_SIZE + x) as usize) * 4;
let dst_idx = ((gy * self.width + gx) as usize) * 4;
out[dst_idx..dst_idx + 4].copy_from_slice(&tile.pixels[src_idx..src_idx + 4]);
}
}
}
out
}
/// Fast composite helper that copies active tile contents directly into an output buffer
/// within a specified sub-region.
pub fn composite_into(
&self,
output: &mut [u8],
canvas_w: u32,
canvas_h: u32,
region_x0: u32, region_y0: u32, region_x1: u32, region_y1: u32,
) {
if self.tiles.is_empty() { return; }
let tile_start_x = region_x0 / TILE_SIZE;
let tile_start_y = region_y0 / TILE_SIZE;
let tile_end_x = div_ceil(region_x1, TILE_SIZE);
let tile_end_y = div_ceil(region_y1, TILE_SIZE);
for ty in tile_start_y..tile_end_y {
for tx in tile_start_x..tile_end_x {
let key = (tx, ty);
if let Some(tile) = self.tiles.get(&key) {
let tile_ox = tx * TILE_SIZE;
let tile_oy = ty * TILE_SIZE;
for y in 0..TILE_SIZE {
let gy = tile_oy + y;
if gy >= self.height || gy >= canvas_h { break; }
if gy < region_y0 || gy >= region_y1 { continue; }
for x in 0..TILE_SIZE {
let gx = tile_ox + x;
if gx >= self.width || gx >= canvas_w { break; }
if gx < region_x0 || gx >= region_x1 { continue; }
let dst_idx = ((gy * canvas_w + gx) as usize) * 4;
let src_idx = ((y * TILE_SIZE + x) as usize) * 4;
output[dst_idx..dst_idx + 4].copy_from_slice(&tile.pixels[src_idx..src_idx + 4]);
}
}
}
}
}
}
}
/// Returns true if any pixel in the tile has non-zero alpha.
#[inline]
fn tile_has_any_content(tile: &Tile) -> bool {
for a in tile.pixels.chunks_exact(4).map(|c| c[3]) {
if a != 0 {
return true;
}
}
false
}
#[inline]
fn div_ceil(a: u32, b: u32) -> u32 {
(a + b - 1) / b
}