diff --git a/crates/console/prometeu-drivers/src/frame_composer.rs b/crates/console/prometeu-drivers/src/frame_composer.rs index f5b83c8e..63930e94 100644 --- a/crates/console/prometeu-drivers/src/frame_composer.rs +++ b/crates/console/prometeu-drivers/src/frame_composer.rs @@ -22,6 +22,7 @@ const EMPTY_SPRITE: Sprite = Sprite { pub enum SceneStatus { #[default] Unbound, + Available { scene_bank_id: usize }, } #[derive(Clone, Debug)] @@ -177,6 +178,35 @@ impl FrameComposer { (self.camera_x_px, self.camera_y_px) } + pub fn bind_scene(&mut self, scene_bank_id: usize) -> bool { + let Some(scene) = self.scene_bank_pool.scene_bank_slot(scene_bank_id) else { + self.unbind_scene(); + return false; + }; + + let (cache, resolver) = + Self::build_scene_runtime(self.viewport_width_px, self.viewport_height_px, &scene); + self.active_scene_id = Some(scene_bank_id); + self.active_scene = Some(scene); + self.scene_status = SceneStatus::Available { scene_bank_id }; + self.cache = Some(cache); + self.resolver = Some(resolver); + true + } + + pub fn unbind_scene(&mut self) { + self.active_scene_id = None; + self.active_scene = None; + self.scene_status = SceneStatus::Unbound; + self.cache = None; + self.resolver = None; + } + + pub fn set_camera(&mut self, x: i32, y: i32) { + self.camera_x_px = x; + self.camera_y_px = y; + } + pub fn cache(&self) -> Option<&SceneViewportCache> { self.cache.as_ref() } @@ -204,6 +234,31 @@ impl FrameComposer { pub fn ordered_sprites(&self) -> Vec { self.sprite_controller.ordered_sprites() } + + fn build_scene_runtime( + viewport_width_px: usize, + viewport_height_px: usize, + scene: &SceneBank, + ) -> (SceneViewportCache, SceneViewportResolver) { + let min_tile_px = + scene.layers.iter().map(|layer| layer.tile_size as usize).min().unwrap_or(8); + let cache_width_tiles = viewport_width_px.div_ceil(min_tile_px) + 5; + let cache_height_tiles = viewport_height_px.div_ceil(min_tile_px) + 4; + let hysteresis_safe_px = min_tile_px.saturating_sub(4) as i32; + let hysteresis_trigger_px = (min_tile_px + 4) as i32; + + ( + SceneViewportCache::new(scene, cache_width_tiles, cache_height_tiles), + SceneViewportResolver::new( + viewport_width_px as i32, + viewport_height_px as i32, + cache_width_tiles, + cache_height_tiles, + hysteresis_safe_px, + hysteresis_trigger_px, + ), + ) + } } #[cfg(test)] @@ -269,6 +324,75 @@ mod tests { assert_eq!(scene.layers[0].parallax_factor.y, 0.5); } + #[test] + fn bind_scene_stores_scene_identity_and_shared_reference() { + let banks = Arc::new(MemoryBanks::new()); + banks.install_scene_bank(3, Arc::new(make_scene())); + + let expected_scene = banks.scene_bank_slot(3).expect("scene bank slot 3 should be resident"); + let mut frame_composer = FrameComposer::new(320, 180, banks); + + assert!(frame_composer.bind_scene(3)); + + assert_eq!(frame_composer.active_scene_id(), Some(3)); + assert!(Arc::ptr_eq( + frame_composer.active_scene().expect("active scene should exist"), + &expected_scene, + )); + assert_eq!(frame_composer.scene_status(), SceneStatus::Available { scene_bank_id: 3 }); + assert!(frame_composer.cache().is_some()); + assert!(frame_composer.resolver().is_some()); + } + + #[test] + fn unbind_scene_clears_scene_and_cache_state() { + let banks = Arc::new(MemoryBanks::new()); + banks.install_scene_bank(1, Arc::new(make_scene())); + + let mut frame_composer = FrameComposer::new(320, 180, banks); + assert!(frame_composer.bind_scene(1)); + + frame_composer.unbind_scene(); + + assert_eq!(frame_composer.active_scene_id(), None); + assert!(frame_composer.active_scene().is_none()); + assert_eq!(frame_composer.scene_status(), SceneStatus::Unbound); + assert!(frame_composer.cache().is_none()); + assert!(frame_composer.resolver().is_none()); + } + + #[test] + fn set_camera_stores_top_left_pixel_coordinates() { + let mut frame_composer = FrameComposer::new(320, 180, Arc::new(MemoryBanks::new())); + + frame_composer.set_camera(-12, 48); + + assert_eq!(frame_composer.camera(), (-12, 48)); + } + + #[test] + fn bind_scene_derives_cache_and_resolver_from_eight_pixel_layers() { + let banks = Arc::new(MemoryBanks::new()); + banks.install_scene_bank(0, Arc::new(make_scene())); + + let mut frame_composer = FrameComposer::new(320, 180, banks); + assert!(frame_composer.bind_scene(0)); + + let cache = frame_composer.cache().expect("cache should exist for bound scene"); + assert_eq!((cache.width(), cache.height()), (45, 27)); + } + + #[test] + fn missing_scene_binding_falls_back_to_no_scene_state() { + let mut frame_composer = FrameComposer::new(320, 180, Arc::new(MemoryBanks::new())); + + assert!(!frame_composer.bind_scene(7)); + + assert_eq!(frame_composer.scene_status(), SceneStatus::Unbound); + assert!(frame_composer.cache().is_none()); + assert!(frame_composer.resolver().is_none()); + } + #[test] fn sprite_controller_begin_frame_resets_sprite_count_and_buckets() { let mut controller = SpriteController::new();