From d9e680082d8d16bde9e14a5d8f4f1f55c31a0e52 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 13 Apr 2026 20:22:14 +0100 Subject: [PATCH] implements PLN-0012 --- crates/console/prometeu-hal/src/lib.rs | 1 + .../prometeu-hal/src/scene_viewport_cache.rs | 430 ++++++++++++++++++ 2 files changed, 431 insertions(+) create mode 100644 crates/console/prometeu-hal/src/scene_viewport_cache.rs diff --git a/crates/console/prometeu-hal/src/lib.rs b/crates/console/prometeu-hal/src/lib.rs index c36d1b37..7c994c67 100644 --- a/crates/console/prometeu-hal/src/lib.rs +++ b/crates/console/prometeu-hal/src/lib.rs @@ -20,6 +20,7 @@ pub mod pad_bridge; pub mod sample; pub mod scene_bank; pub mod scene_layer; +pub mod scene_viewport_cache; pub mod sound_bank; pub mod sprite; pub mod syscalls; diff --git a/crates/console/prometeu-hal/src/scene_viewport_cache.rs b/crates/console/prometeu-hal/src/scene_viewport_cache.rs new file mode 100644 index 00000000..c0c3bcdb --- /dev/null +++ b/crates/console/prometeu-hal/src/scene_viewport_cache.rs @@ -0,0 +1,430 @@ +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::MotionFactor; + 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, + motion_factor: MotionFactor { 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 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 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); + } +}