use crate::glyph_bank::TileSize; use crate::scene_bank::SceneBank; use crate::scene_layer::SceneLayer; use crate::tile::Tile; const FLAG_FLIP_X: u8 = 0b0000_0001; const FLAG_FLIP_Y: u8 = 0b0000_0010; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct CachedTileEntry { pub active: bool, pub glyph_id: u16, pub palette_id: u8, pub flags: u8, pub glyph_bank_id: u8, } impl CachedTileEntry { pub fn flip_x(self) -> bool { (self.flags & FLAG_FLIP_X) != 0 } pub fn flip_y(self) -> bool { (self.flags & FLAG_FLIP_Y) != 0 } fn from_tile(layer: &SceneLayer, tile: Tile) -> Self { let mut flags = 0_u8; if tile.flip_x { flags |= FLAG_FLIP_X; } if tile.flip_y { flags |= FLAG_FLIP_Y; } Self { active: tile.active, glyph_id: tile.glyph.glyph_id, palette_id: tile.glyph.palette_id, flags, glyph_bank_id: layer.glyph_bank_id, } } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct ViewportRegion { pub x: usize, pub y: usize, pub width: usize, pub height: usize, } impl ViewportRegion { pub const fn new(x: usize, y: usize, width: usize, height: usize) -> Self { Self { x, y, width, height } } } #[derive(Clone, Debug)] pub struct SceneViewportLayerCache { width: usize, height: usize, logical_origin_x: i32, logical_origin_y: i32, ring_origin_x: usize, ring_origin_y: usize, pub glyph_bank_id: u8, pub tile_size: TileSize, entries: Vec, pub valid: bool, } impl SceneViewportLayerCache { pub fn new(layer: &SceneLayer, width: usize, height: usize) -> Self { Self { width, height, logical_origin_x: 0, logical_origin_y: 0, ring_origin_x: 0, ring_origin_y: 0, glyph_bank_id: layer.glyph_bank_id, tile_size: layer.tile_size, entries: vec![CachedTileEntry::default(); width * height], valid: false, } } pub fn width(&self) -> usize { self.width } pub fn height(&self) -> usize { self.height } pub fn logical_origin(&self) -> (i32, i32) { (self.logical_origin_x, self.logical_origin_y) } pub fn ring_origin(&self) -> (usize, usize) { (self.ring_origin_x, self.ring_origin_y) } pub fn entry(&self, cache_x: usize, cache_y: usize) -> CachedTileEntry { self.entries[self.physical_index(cache_x, cache_y)] } pub fn invalidate_all(&mut self) { self.entries.fill(CachedTileEntry::default()); self.valid = false; } pub fn move_window_to(&mut self, origin_x: i32, origin_y: i32) { let delta_x = origin_x - self.logical_origin_x; let delta_y = origin_y - self.logical_origin_y; self.logical_origin_x = origin_x; self.logical_origin_y = origin_y; self.ring_origin_x = Self::wrapped_origin(self.ring_origin_x, delta_x, self.width); self.ring_origin_y = Self::wrapped_origin(self.ring_origin_y, delta_y, self.height); } pub fn move_window_by(&mut self, delta_x: i32, delta_y: i32) { self.move_window_to(self.logical_origin_x + delta_x, self.logical_origin_y + delta_y); } pub fn refresh_line(&mut self, layer: &SceneLayer, cache_y: usize) { self.refresh_region(layer, ViewportRegion::new(0, cache_y, self.width, 1)); } pub fn refresh_column(&mut self, layer: &SceneLayer, cache_x: usize) { self.refresh_region(layer, ViewportRegion::new(cache_x, 0, 1, self.height)); } pub fn refresh_region(&mut self, layer: &SceneLayer, region: ViewportRegion) { self.glyph_bank_id = layer.glyph_bank_id; self.tile_size = layer.tile_size; let max_x = region.x.saturating_add(region.width).min(self.width); let max_y = region.y.saturating_add(region.height).min(self.height); for cache_y in region.y..max_y { for cache_x in region.x..max_x { let entry = self.materialize_entry(layer, cache_x, cache_y); let idx = self.physical_index(cache_x, cache_y); self.entries[idx] = entry; } } self.valid = true; } pub fn refresh_all(&mut self, layer: &SceneLayer) { self.refresh_region(layer, ViewportRegion::new(0, 0, self.width, self.height)); } fn materialize_entry( &self, layer: &SceneLayer, cache_x: usize, cache_y: usize, ) -> CachedTileEntry { let scene_x = self.logical_origin_x + cache_x as i32; let scene_y = self.logical_origin_y + cache_y as i32; if scene_x < 0 || scene_y < 0 { return CachedTileEntry::default(); } let tile_x = scene_x as usize; let tile_y = scene_y as usize; if tile_x >= layer.tilemap.width || tile_y >= layer.tilemap.height { return CachedTileEntry::default(); } let tile = layer.tilemap.tiles[tile_y * layer.tilemap.width + tile_x]; CachedTileEntry::from_tile(layer, tile) } fn physical_index(&self, cache_x: usize, cache_y: usize) -> usize { let physical_x = (self.ring_origin_x + cache_x) % self.width; let physical_y = (self.ring_origin_y + cache_y) % self.height; physical_y * self.width + physical_x } fn wrapped_origin(current: usize, delta: i32, span: usize) -> usize { if span == 0 { return 0; } let span_i32 = span as i32; let current_i32 = current as i32; (current_i32 + delta).rem_euclid(span_i32) as usize } } #[derive(Clone, Debug)] pub struct SceneViewportCache { width: usize, height: usize, pub layers: [SceneViewportLayerCache; 4], } impl SceneViewportCache { pub fn new(scene: &SceneBank, width: usize, height: usize) -> Self { Self { width, height, layers: std::array::from_fn(|i| { SceneViewportLayerCache::new(&scene.layers[i], width, height) }), } } pub fn width(&self) -> usize { self.width } pub fn height(&self) -> usize { self.height } pub fn invalidate_all(&mut self) { for layer in &mut self.layers { layer.invalidate_all(); } } pub fn move_layer_window_to(&mut self, layer_idx: usize, origin_x: i32, origin_y: i32) { self.layers[layer_idx].move_window_to(origin_x, origin_y); } pub fn move_layer_window_by(&mut self, layer_idx: usize, delta_x: i32, delta_y: i32) { self.layers[layer_idx].move_window_by(delta_x, delta_y); } pub fn refresh_layer_line(&mut self, scene: &SceneBank, layer_idx: usize, cache_y: usize) { self.layers[layer_idx].refresh_line(&scene.layers[layer_idx], cache_y); } pub fn refresh_layer_column(&mut self, scene: &SceneBank, layer_idx: usize, cache_x: usize) { self.layers[layer_idx].refresh_column(&scene.layers[layer_idx], cache_x); } pub fn refresh_layer_region( &mut self, scene: &SceneBank, layer_idx: usize, region: ViewportRegion, ) { self.layers[layer_idx].refresh_region(&scene.layers[layer_idx], region); } pub fn refresh_layer_all(&mut self, scene: &SceneBank, layer_idx: usize) { self.layers[layer_idx].refresh_all(&scene.layers[layer_idx]); } pub fn materialize_all_layers(&mut self, scene: &SceneBank) { for layer_idx in 0..self.layers.len() { self.refresh_layer_all(scene, layer_idx); } } } #[cfg(test)] mod tests { use super::*; use crate::glyph::Glyph; use crate::glyph_bank::TileSize; use crate::scene_layer::ParallaxFactor; use crate::tile::Tile; use crate::tilemap::TileMap; fn make_tile(glyph_id: u16, palette_id: u8, flip_x: bool, flip_y: bool) -> Tile { Tile { active: true, glyph: Glyph { glyph_id, palette_id }, flip_x, flip_y } } fn make_layer(glyph_bank_id: u8, base_glyph: u16) -> SceneLayer { let mut tiles = Vec::new(); for y in 0..4 { for x in 0..4 { tiles.push(make_tile( base_glyph + (y * 4 + x) as u16, glyph_bank_id, x % 2 == 0, y % 2 == 1, )); } } SceneLayer { active: true, glyph_bank_id, tile_size: TileSize::Size16, parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 }, tilemap: TileMap { width: 4, height: 4, tiles }, } } fn make_scene() -> SceneBank { SceneBank { layers: [ make_layer(1, 100), make_layer(2, 200), make_layer(3, 300), make_layer(4, 400), ], } } #[test] fn layer_cache_wraps_ring_origin_under_window_movement() { let scene = make_scene(); let mut cache = SceneViewportCache::new(&scene, 3, 3); cache.move_layer_window_by(0, 1, 2); assert_eq!(cache.layers[0].logical_origin(), (1, 2)); assert_eq!(cache.layers[0].ring_origin(), (1, 2)); cache.move_layer_window_by(0, 3, 2); assert_eq!(cache.layers[0].logical_origin(), (4, 4)); assert_eq!(cache.layers[0].ring_origin(), (1, 1)); } #[test] fn layer_cache_wraps_ring_origin_for_negative_and_large_movements() { let scene = make_scene(); let mut cache = SceneViewportCache::new(&scene, 3, 3); cache.move_layer_window_by(0, -1, -4); assert_eq!(cache.layers[0].logical_origin(), (-1, -4)); assert_eq!(cache.layers[0].ring_origin(), (2, 2)); cache.move_layer_window_by(0, 7, 8); assert_eq!(cache.layers[0].logical_origin(), (6, 4)); assert_eq!(cache.layers[0].ring_origin(), (0, 1)); } #[test] fn move_window_to_matches_incremental_ring_movement() { let scene = make_scene(); let mut direct = SceneViewportCache::new(&scene, 4, 4); let mut incremental = SceneViewportCache::new(&scene, 4, 4); direct.move_layer_window_to(0, 9, -6); incremental.move_layer_window_by(0, 5, -2); incremental.move_layer_window_by(0, 4, -4); assert_eq!(direct.layers[0].logical_origin(), incremental.layers[0].logical_origin()); assert_eq!(direct.layers[0].ring_origin(), incremental.layers[0].ring_origin()); } #[test] fn cache_entry_fields_are_derived_from_scene_tiles() { let scene = make_scene(); let mut cache = SceneViewportCache::new(&scene, 2, 2); cache.refresh_layer_all(&scene, 0); let entry = cache.layers[0].entry(1, 1); assert!(entry.active); assert_eq!(entry.glyph_id, 105); assert_eq!(entry.palette_id, 1); assert_eq!(entry.glyph_bank_id, 1); assert!(!entry.flip_x()); assert!(entry.flip_y()); } #[test] fn line_refresh_only_updates_the_requested_line() { let scene = make_scene(); let mut cache = SceneViewportCache::new(&scene, 3, 3); cache.refresh_layer_line(&scene, 0, 1); assert_eq!(cache.layers[0].entry(0, 0), CachedTileEntry::default()); assert_eq!(cache.layers[0].entry(0, 1).glyph_id, 104); assert_eq!(cache.layers[0].entry(2, 1).glyph_id, 106); assert_eq!(cache.layers[0].entry(0, 2), CachedTileEntry::default()); } #[test] fn column_refresh_only_updates_the_requested_column() { let scene = make_scene(); let mut cache = SceneViewportCache::new(&scene, 3, 3); cache.refresh_layer_column(&scene, 1, 2); assert_eq!(cache.layers[1].entry(0, 0), CachedTileEntry::default()); assert_eq!(cache.layers[1].entry(2, 0).glyph_id, 202); assert_eq!(cache.layers[1].entry(2, 2).glyph_id, 210); assert_eq!(cache.layers[1].entry(1, 2), CachedTileEntry::default()); } #[test] fn region_refresh_only_updates_the_requested_area() { let scene = make_scene(); let mut cache = SceneViewportCache::new(&scene, 3, 3); cache.refresh_layer_region(&scene, 2, ViewportRegion::new(1, 1, 2, 2)); assert_eq!(cache.layers[2].entry(0, 0), CachedTileEntry::default()); assert_eq!(cache.layers[2].entry(1, 1).glyph_id, 305); assert_eq!(cache.layers[2].entry(2, 2).glyph_id, 310); assert_eq!(cache.layers[2].entry(0, 2), CachedTileEntry::default()); } #[test] fn scene_swap_invalidation_clears_all_layers() { let scene = make_scene(); let mut cache = SceneViewportCache::new(&scene, 2, 2); cache.materialize_all_layers(&scene); cache.invalidate_all(); for layer in &cache.layers { assert!(!layer.valid); for y in 0..cache.height() { for x in 0..cache.width() { assert_eq!(layer.entry(x, y), CachedTileEntry::default()); } } } } #[test] fn corner_style_region_update_does_not_touch_outside_tiles() { let scene = make_scene(); let mut cache = SceneViewportCache::new(&scene, 4, 4); cache.materialize_all_layers(&scene); let before = cache.layers[3].entry(1, 1); cache.layers[3].invalidate_all(); cache.refresh_layer_region(&scene, 3, ViewportRegion::new(2, 2, 2, 2)); assert_eq!(cache.layers[3].entry(0, 0), CachedTileEntry::default()); assert_eq!(cache.layers[3].entry(1, 1), CachedTileEntry::default()); assert_ne!(cache.layers[3].entry(2, 2), CachedTileEntry::default()); assert_eq!(before.glyph_id, 405); assert_eq!(cache.layers[3].entry(3, 3).glyph_id, 415); } #[test] fn refresh_after_wrapped_window_move_materializes_new_logical_tiles() { let scene = make_scene(); let mut cache = SceneViewportCache::new(&scene, 3, 3); cache.refresh_layer_all(&scene, 0); cache.move_layer_window_to(0, 1, 2); cache.refresh_layer_all(&scene, 0); assert_eq!(cache.layers[0].logical_origin(), (1, 2)); assert_eq!(cache.layers[0].ring_origin(), (1, 2)); assert_eq!(cache.layers[0].entry(0, 0).glyph_id, 109); assert_eq!(cache.layers[0].entry(1, 0).glyph_id, 110); assert_eq!(cache.layers[0].entry(2, 0).glyph_id, 111); assert_eq!(cache.layers[0].entry(0, 1).glyph_id, 113); assert_eq!(cache.layers[0].entry(2, 1).glyph_id, 115); assert_eq!(cache.layers[0].entry(0, 2), CachedTileEntry::default()); } #[test] fn partial_refresh_uses_wrapped_physical_slots_after_window_move() { let scene = make_scene(); let mut cache = SceneViewportCache::new(&scene, 3, 3); cache.move_layer_window_to(0, 1, 0); cache.refresh_layer_column(&scene, 0, 0); assert_eq!(cache.layers[0].ring_origin(), (1, 0)); assert_eq!(cache.layers[0].entry(0, 0).glyph_id, 101); assert_eq!(cache.layers[0].entry(0, 1).glyph_id, 105); assert_eq!(cache.layers[0].entry(0, 2).glyph_id, 109); assert_eq!(cache.layers[0].entry(1, 0), CachedTileEntry::default()); assert_eq!(cache.layers[0].entry(2, 0), CachedTileEntry::default()); } #[test] fn out_of_bounds_logical_origins_materialize_default_entries_after_wrap() { let scene = make_scene(); let mut cache = SceneViewportCache::new(&scene, 2, 2); cache.move_layer_window_to(0, -2, 3); cache.refresh_layer_all(&scene, 0); for y in 0..2 { for x in 0..2 { assert_eq!(cache.layers[0].entry(x, y), CachedTileEntry::default()); } } } #[test] fn ringbuffer_preserves_logical_tile_mapping_across_long_mixed_movements() { let scene = make_scene(); let mut cache = SceneViewportCache::new(&scene, 3, 3); let motions = [ (1, 0), (0, 1), (2, 2), (-1, 0), (0, -2), (4, 1), (-3, 3), (5, -4), (-6, 2), (3, -3), (7, 7), (-8, -5), ]; for &(dx, dy) in &motions { cache.move_layer_window_by(0, dx, dy); cache.refresh_layer_all(&scene, 0); let (origin_x, origin_y) = cache.layers[0].logical_origin(); for cache_y in 0..cache.height() { for cache_x in 0..cache.width() { let expected_scene_x = origin_x + cache_x as i32; let expected_scene_y = origin_y + cache_y as i32; let expected = if expected_scene_x < 0 || expected_scene_y < 0 || expected_scene_x as usize >= scene.layers[0].tilemap.width || expected_scene_y as usize >= scene.layers[0].tilemap.height { CachedTileEntry::default() } else { let tile_x = expected_scene_x as usize; let tile_y = expected_scene_y as usize; let tile = scene.layers[0].tilemap.tiles [tile_y * scene.layers[0].tilemap.width + tile_x]; CachedTileEntry::from_tile(&scene.layers[0], tile) }; assert_eq!( cache.layers[0].entry(cache_x, cache_y), expected, "mismatch at logical origin ({}, {}), cache ({}, {})", origin_x, origin_y, cache_x, cache_y ); } } } } #[test] fn materialization_populates_all_four_layers() { let scene = make_scene(); let mut cache = SceneViewportCache::new(&scene, 2, 2); cache.materialize_all_layers(&scene); assert_eq!(cache.layers[0].entry(0, 0).glyph_id, 100); assert_eq!(cache.layers[1].entry(0, 0).glyph_id, 200); assert_eq!(cache.layers[2].entry(1, 1).glyph_id, 305); assert_eq!(cache.layers[3].entry(1, 0).glyph_id, 401); } }