use crate::memory_banks::SceneBankPoolAccess; use prometeu_hal::GfxBridge; use prometeu_hal::glyph::Glyph; use prometeu_hal::scene_bank::SceneBank; use prometeu_hal::scene_viewport_cache::SceneViewportCache; use prometeu_hal::scene_viewport_resolver::{CacheRefreshRequest, SceneViewportResolver}; use prometeu_hal::sprite::Sprite; use std::sync::Arc; const EMPTY_SPRITE: Sprite = Sprite { glyph: Glyph { glyph_id: 0, palette_id: 0 }, x: 0, y: 0, layer: 0, bank_id: 0, active: false, flip_x: false, flip_y: false, priority: 0, }; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum SceneStatus { #[default] Unbound, Available { scene_bank_id: usize, }, } #[derive(Clone, Debug)] pub struct SpriteController { sprites: [Sprite; 512], sprite_count: usize, frame_counter: u64, dropped_sprites: usize, layer_buckets: [Vec; 4], } impl Default for SpriteController { fn default() -> Self { Self::new() } } impl SpriteController { pub fn new() -> Self { Self { sprites: [EMPTY_SPRITE; 512], sprite_count: 0, frame_counter: 0, dropped_sprites: 0, layer_buckets: std::array::from_fn(|_| Vec::with_capacity(128)), } } pub fn sprites(&self) -> &[Sprite; 512] { &self.sprites } pub fn sprites_mut(&mut self) -> &mut [Sprite; 512] { &mut self.sprites } pub fn begin_frame(&mut self) { self.frame_counter = self.frame_counter.wrapping_add(1); self.sprite_count = 0; self.dropped_sprites = 0; for bucket in &mut self.layer_buckets { bucket.clear(); } } pub fn emit_sprite(&mut self, mut sprite: Sprite) -> bool { let Some(bucket) = self.layer_buckets.get_mut(sprite.layer as usize) else { self.dropped_sprites += 1; return false; }; if self.sprite_count >= self.sprites.len() { self.dropped_sprites += 1; return false; } sprite.active = true; let index = self.sprite_count; self.sprites[index] = sprite; self.sprite_count += 1; bucket.push(index); true } pub fn sprite_count(&self) -> usize { self.sprite_count } pub fn frame_counter(&self) -> u64 { self.frame_counter } pub fn dropped_sprites(&self) -> usize { self.dropped_sprites } pub fn ordered_sprites(&self) -> Vec { let mut ordered = Vec::with_capacity(self.sprite_count); for bucket in &self.layer_buckets { let mut indices = bucket.clone(); indices.sort_by_key(|&index| self.sprites[index].priority); for index in indices { ordered.push(self.sprites[index]); } } ordered } } pub struct FrameComposer { scene_bank_pool: Arc, viewport_width_px: usize, viewport_height_px: usize, active_scene_id: Option, active_scene: Option>, scene_status: SceneStatus, camera_x_px: i32, camera_y_px: i32, cache: Option, resolver: Option, sprite_controller: SpriteController, } impl FrameComposer { pub fn new( viewport_width_px: usize, viewport_height_px: usize, scene_bank_pool: Arc, ) -> Self { Self { scene_bank_pool, viewport_width_px, viewport_height_px, active_scene_id: None, active_scene: None, scene_status: SceneStatus::Unbound, camera_x_px: 0, camera_y_px: 0, cache: None, resolver: None, sprite_controller: SpriteController::new(), } } pub fn viewport_size(&self) -> (usize, usize) { (self.viewport_width_px, self.viewport_height_px) } pub fn scene_bank_pool(&self) -> &Arc { &self.scene_bank_pool } pub fn scene_bank_slot(&self, slot: usize) -> Option> { self.scene_bank_pool.scene_bank_slot(slot) } pub fn scene_bank_slot_count(&self) -> usize { self.scene_bank_pool.scene_bank_slot_count() } pub fn active_scene_id(&self) -> Option { self.active_scene_id } pub fn active_scene(&self) -> Option<&Arc> { self.active_scene.as_ref() } pub fn scene_status(&self) -> SceneStatus { self.scene_status } pub fn camera(&self) -> (i32, i32) { (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() } pub fn resolver(&self) -> Option<&SceneViewportResolver> { self.resolver.as_ref() } pub fn sprite_controller(&self) -> &SpriteController { &self.sprite_controller } pub fn sprite_controller_mut(&mut self) -> &mut SpriteController { &mut self.sprite_controller } pub fn begin_frame(&mut self) { self.sprite_controller.begin_frame(); } pub fn emit_sprite(&mut self, sprite: Sprite) -> bool { self.sprite_controller.emit_sprite(sprite) } pub fn ordered_sprites(&self) -> Vec { self.sprite_controller.ordered_sprites() } pub fn render_frame(&mut self, gfx: &mut dyn GfxBridge) { let ordered_sprites = self.ordered_sprites(); gfx.load_frame_sprites(&ordered_sprites); if let (Some(scene), Some(cache), Some(resolver)) = (self.active_scene.as_deref(), self.cache.as_mut(), self.resolver.as_mut()) { let update = resolver.update(scene, self.camera_x_px, self.camera_y_px); Self::apply_refresh_requests(cache, scene, &update.refresh_requests); gfx.render_scene_from_cache(cache, &update); return; } gfx.render_all(); } 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, ), ) } fn apply_refresh_requests( cache: &mut SceneViewportCache, scene: &SceneBank, refresh_requests: &[CacheRefreshRequest], ) { for request in refresh_requests { match *request { CacheRefreshRequest::InvalidateLayer { layer_index } => { cache.layers[layer_index].invalidate_all(); } CacheRefreshRequest::RefreshLine { layer_index, cache_y } => { cache.refresh_layer_line(scene, layer_index, cache_y); } CacheRefreshRequest::RefreshColumn { layer_index, cache_x } => { cache.refresh_layer_column(scene, layer_index, cache_x); } CacheRefreshRequest::RefreshRegion { layer_index, region } => { cache.refresh_layer_region(scene, layer_index, region); } } } } } #[cfg(test)] mod tests { use super::*; use crate::gfx::Gfx; use crate::memory_banks::{ GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolInstaller, }; use prometeu_hal::color::Color; use prometeu_hal::glyph_bank::{GlyphBank, TileSize}; use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer}; use prometeu_hal::tile::Tile; use prometeu_hal::tilemap::TileMap; fn make_scene() -> SceneBank { make_scene_with_palette(1, 1, TileSize::Size8) } fn make_scene_with_palette( glyph_bank_id: u8, palette_id: u8, tile_size: TileSize, ) -> SceneBank { let layer = SceneLayer { active: true, glyph_bank_id, tile_size, parallax_factor: ParallaxFactor { x: 1.0, y: 0.5 }, tilemap: TileMap { width: 2, height: 2, tiles: vec![ Tile { active: true, glyph: Glyph { glyph_id: 0, palette_id }, flip_x: false, flip_y: false, }; 4 ], }, }; SceneBank { layers: std::array::from_fn(|_| layer.clone()) } } fn make_glyph_bank(tile_size: TileSize, palette_id: u8, color: Color) -> GlyphBank { let size = tile_size as usize; let mut bank = GlyphBank::new(tile_size, size, size); bank.palettes[palette_id as usize][1] = color; for pixel in &mut bank.pixel_indices { *pixel = 1; } bank } #[test] fn frame_composer_starts_unbound_with_empty_owned_state() { let frame_composer = FrameComposer::new(320, 180, Arc::new(MemoryBanks::new())); assert_eq!(frame_composer.viewport_size(), (320, 180)); assert_eq!(frame_composer.active_scene_id(), None); assert!(frame_composer.active_scene().is_none()); assert_eq!(frame_composer.scene_status(), SceneStatus::Unbound); assert_eq!(frame_composer.camera(), (0, 0)); assert!(frame_composer.cache().is_none()); assert!(frame_composer.resolver().is_none()); assert_eq!(frame_composer.sprite_controller().sprites().len(), 512); assert_eq!(frame_composer.sprite_controller().sprite_count(), 0); assert_eq!(frame_composer.sprite_controller().dropped_sprites(), 0); } #[test] fn frame_composer_exposes_shared_scene_bank_access() { let banks = Arc::new(MemoryBanks::new()); banks.install_scene_bank(3, Arc::new(make_scene())); let frame_composer = FrameComposer::new(320, 180, banks); let scene = frame_composer.scene_bank_slot(3).expect("scene bank slot 3 should be resident"); assert_eq!(frame_composer.scene_bank_slot_count(), 16); assert_eq!(scene.layers[0].tile_size, TileSize::Size8); 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(); let emitted = controller.emit_sprite(Sprite { glyph: Glyph { glyph_id: 1, palette_id: 2 }, x: 4, y: 5, layer: 2, bank_id: 3, active: false, flip_x: false, flip_y: false, priority: 1, }); assert!(emitted); controller.begin_frame(); assert_eq!(controller.frame_counter(), 1); assert_eq!(controller.sprite_count(), 0); assert_eq!(controller.dropped_sprites(), 0); assert!(controller.ordered_sprites().is_empty()); } #[test] fn sprite_controller_orders_by_layer_then_priority_then_fifo() { let mut controller = SpriteController::new(); controller.begin_frame(); assert!(controller.emit_sprite(Sprite { glyph: Glyph { glyph_id: 10, palette_id: 0 }, x: 0, y: 0, layer: 1, bank_id: 0, active: false, flip_x: false, flip_y: false, priority: 2, })); assert!(controller.emit_sprite(Sprite { glyph: Glyph { glyph_id: 11, palette_id: 0 }, x: 0, y: 0, layer: 0, bank_id: 0, active: false, flip_x: false, flip_y: false, priority: 3, })); assert!(controller.emit_sprite(Sprite { glyph: Glyph { glyph_id: 12, palette_id: 0 }, x: 0, y: 0, layer: 1, bank_id: 0, active: false, flip_x: false, flip_y: false, priority: 1, })); assert!(controller.emit_sprite(Sprite { glyph: Glyph { glyph_id: 13, palette_id: 0 }, x: 0, y: 0, layer: 1, bank_id: 0, active: false, flip_x: false, flip_y: false, priority: 2, })); let ordered = controller.ordered_sprites(); let ordered_ids: Vec = ordered.iter().map(|sprite| sprite.glyph.glyph_id).collect(); assert_eq!(ordered_ids, vec![11, 12, 10, 13]); assert!(ordered.iter().all(|sprite| sprite.active)); } #[test] fn sprite_controller_drops_overflow_without_panicking() { let mut controller = SpriteController::new(); controller.begin_frame(); for glyph_id in 0..512 { assert!(controller.emit_sprite(Sprite { glyph: Glyph { glyph_id, palette_id: 0 }, x: 0, y: 0, layer: 0, bank_id: 0, active: false, flip_x: false, flip_y: false, priority: 0, })); } let overflowed = controller.emit_sprite(Sprite { glyph: Glyph { glyph_id: 999, palette_id: 0 }, x: 0, y: 0, layer: 0, bank_id: 0, active: false, flip_x: false, flip_y: false, priority: 0, }); assert!(!overflowed); assert_eq!(controller.sprite_count(), 512); assert_eq!(controller.dropped_sprites(), 1); } #[test] fn frame_composer_emits_ordered_sprites_for_rendering() { let mut frame_composer = FrameComposer::new(320, 180, Arc::new(MemoryBanks::new())); frame_composer.begin_frame(); assert!(frame_composer.emit_sprite(Sprite { glyph: Glyph { glyph_id: 21, palette_id: 0 }, x: 0, y: 0, layer: 2, bank_id: 1, active: false, flip_x: false, flip_y: false, priority: 1, })); assert!(frame_composer.emit_sprite(Sprite { glyph: Glyph { glyph_id: 20, palette_id: 0 }, x: 0, y: 0, layer: 1, bank_id: 1, active: false, flip_x: false, flip_y: false, priority: 0, })); let ordered = frame_composer.ordered_sprites(); assert_eq!(ordered.len(), 2); assert_eq!(ordered[0].glyph.glyph_id, 20); assert_eq!(ordered[1].glyph.glyph_id, 21); } #[test] fn render_frame_without_scene_uses_sprite_only_path() { let banks = Arc::new(MemoryBanks::new()); banks.install_glyph_bank(1, Arc::new(make_glyph_bank(TileSize::Size8, 3, Color::WHITE))); let mut frame_composer = FrameComposer::new(16, 16, Arc::clone(&banks) as Arc); frame_composer.begin_frame(); assert!(frame_composer.emit_sprite(Sprite { glyph: Glyph { glyph_id: 0, palette_id: 3 }, x: 0, y: 0, layer: 0, bank_id: 1, active: false, flip_x: false, flip_y: false, priority: 0, })); let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc); gfx.scene_fade_level = 31; gfx.hud_fade_level = 31; frame_composer.render_frame(&mut gfx); gfx.present(); assert_eq!(gfx.front_buffer()[0], Color::WHITE.raw()); } #[test] fn render_frame_with_scene_applies_refreshes_before_composition() { let banks = Arc::new(MemoryBanks::new()); banks.install_glyph_bank(0, Arc::new(make_glyph_bank(TileSize::Size8, 2, Color::BLUE))); banks.install_scene_bank(0, Arc::new(make_scene_with_palette(0, 2, TileSize::Size8))); let mut frame_composer = FrameComposer::new(16, 16, Arc::clone(&banks) as Arc); assert!(frame_composer.bind_scene(0)); let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc); gfx.scene_fade_level = 31; gfx.hud_fade_level = 31; frame_composer.render_frame(&mut gfx); gfx.present(); assert!( frame_composer .cache() .expect("cache should exist") .layers .iter() .all(|layer| layer.valid) ); assert_eq!(gfx.front_buffer()[0], Color::BLUE.raw()); } #[test] fn render_frame_survives_scene_transition_through_unbind_and_rebind() { let banks = Arc::new(MemoryBanks::new()); banks.install_glyph_bank(0, Arc::new(make_glyph_bank(TileSize::Size8, 1, Color::RED))); banks.install_glyph_bank(1, Arc::new(make_glyph_bank(TileSize::Size8, 2, Color::BLUE))); banks.install_glyph_bank(2, Arc::new(make_glyph_bank(TileSize::Size8, 3, Color::WHITE))); banks.install_scene_bank(0, Arc::new(make_scene_with_palette(0, 1, TileSize::Size8))); banks.install_scene_bank(1, Arc::new(make_scene_with_palette(1, 2, TileSize::Size8))); let mut frame_composer = FrameComposer::new(16, 16, Arc::clone(&banks) as Arc); let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc); gfx.scene_fade_level = 31; gfx.hud_fade_level = 31; assert!(frame_composer.bind_scene(0)); frame_composer.render_frame(&mut gfx); gfx.present(); assert_eq!(gfx.front_buffer()[0], Color::RED.raw()); frame_composer.unbind_scene(); frame_composer.begin_frame(); assert!(frame_composer.emit_sprite(Sprite { glyph: Glyph { glyph_id: 0, palette_id: 3 }, x: 0, y: 0, layer: 0, bank_id: 2, active: false, flip_x: false, flip_y: false, priority: 0, })); frame_composer.render_frame(&mut gfx); gfx.present(); assert_eq!(gfx.front_buffer()[0], Color::WHITE.raw()); frame_composer.begin_frame(); assert!(frame_composer.bind_scene(1)); frame_composer.render_frame(&mut gfx); gfx.present(); assert_eq!(gfx.front_buffer()[0], Color::BLUE.raw()); } }