diff --git a/crates/console/prometeu-drivers/src/gfx.rs b/crates/console/prometeu-drivers/src/gfx.rs index 9cc15110..9d9e233b 100644 --- a/crates/console/prometeu-drivers/src/gfx.rs +++ b/crates/console/prometeu-drivers/src/gfx.rs @@ -3,6 +3,8 @@ use prometeu_hal::GfxBridge; use prometeu_hal::color::Color; use prometeu_hal::glyph::Glyph; use prometeu_hal::glyph_bank::GlyphBank; +use prometeu_hal::scene_viewport_cache::{CachedTileEntry, SceneViewportCache}; +use prometeu_hal::scene_viewport_resolver::{LayerCopyRequest, ResolverUpdate}; use prometeu_hal::sprite::Sprite; use std::sync::Arc; @@ -531,17 +533,7 @@ impl Gfx { /// correct priority order into the back buffer. /// Follows the hardware model where layers and sprites are composed every frame. pub fn render_all(&mut self) { - // 0. Preparation Phase: Filter and group sprites by their priority levels. - // This avoids iterating through all 512 sprites for every layer. - for bucket in self.priority_buckets.iter_mut() { - bucket.clear(); - } - - for (idx, sprite) in self.sprites.iter().enumerate() { - if sprite.active && sprite.priority < 5 { - self.priority_buckets[sprite.priority as usize].push(idx); - } - } + self.populate_priority_buckets(); // 1. Priority 0 sprites: drawn at the very back, behind everything else. Self::draw_bucket_on_buffer( @@ -590,6 +582,48 @@ impl Gfx { Self::apply_fade_to_buffer(&mut self.back, self.hud_fade_level, self.hud_fade_color); } + /// Composes the world from the viewport cache using resolver copy requests. + /// + /// This is the cache-backed world path accepted by DEC-0013. The canonical scene + /// is not consulted here; the renderer only consumes prepared cache materialization + /// plus sprite state and fade controls. + pub fn render_scene_from_cache(&mut self, cache: &SceneViewportCache, update: &ResolverUpdate) { + self.back.fill(Color::BLACK.raw()); + self.populate_priority_buckets(); + + Self::draw_bucket_on_buffer( + &mut self.back, + self.w, + self.h, + &self.priority_buckets[0], + &self.sprites, + &*self.glyph_banks, + ); + + for layer_index in 0..cache.layers.len() { + Self::draw_cache_layer_to_buffer( + &mut self.back, + self.w, + self.h, + cache, + &update.copy_requests[layer_index], + &*self.glyph_banks, + ); + + Self::draw_bucket_on_buffer( + &mut self.back, + self.w, + self.h, + &self.priority_buckets[layer_index + 1], + &self.sprites, + &*self.glyph_banks, + ); + } + + Self::apply_fade_to_buffer(&mut self.back, self.scene_fade_level, self.scene_fade_color); + Self::apply_fade_to_buffer(&mut self.back, self.hud_fade_level, self.hud_fade_color); + } + // /// Renders a specific game layer. // pub fn render_layer(&mut self, layer_idx: usize) { // if layer_idx >= self.layers.len() { @@ -721,6 +755,104 @@ impl Gfx { // } // } + fn populate_priority_buckets(&mut self) { + for bucket in self.priority_buckets.iter_mut() { + bucket.clear(); + } + + for (idx, sprite) in self.sprites.iter().enumerate() { + if sprite.active && sprite.priority < 5 { + self.priority_buckets[sprite.priority as usize].push(idx); + } + } + } + + fn draw_cache_layer_to_buffer( + back: &mut [u16], + screen_w: usize, + screen_h: usize, + cache: &SceneViewportCache, + request: &LayerCopyRequest, + glyph_banks: &dyn GlyphBankPoolAccess, + ) { + let layer_cache = &cache.layers[request.layer_index]; + if !layer_cache.valid { + return; + } + + let Some(bank) = glyph_banks.glyph_bank_slot(layer_cache.glyph_bank_id as usize) else { + return; + }; + + let tile_size_px = request.tile_size as i32; + for cache_y in 0..layer_cache.height() { + let screen_tile_y = cache_y as i32 * tile_size_px - request.source_offset_y_px; + if screen_tile_y >= screen_h as i32 || screen_tile_y + tile_size_px <= 0 { + continue; + } + + for cache_x in 0..layer_cache.width() { + let screen_tile_x = cache_x as i32 * tile_size_px - request.source_offset_x_px; + if screen_tile_x >= screen_w as i32 || screen_tile_x + tile_size_px <= 0 { + continue; + } + + let entry = layer_cache.entry(cache_x, cache_y); + if !entry.active { + continue; + } + + Self::draw_cached_tile_pixels( + back, + screen_w, + screen_h, + screen_tile_x, + screen_tile_y, + entry, + &bank, + request.tile_size, + ); + } + } + } + + fn draw_cached_tile_pixels( + back: &mut [u16], + screen_w: usize, + screen_h: usize, + x: i32, + y: i32, + entry: CachedTileEntry, + bank: &GlyphBank, + tile_size: prometeu_hal::glyph_bank::TileSize, + ) { + let size = tile_size as usize; + + for local_y in 0..size { + let world_y = y + local_y as i32; + if world_y < 0 || world_y >= screen_h as i32 { + continue; + } + + for local_x in 0..size { + let world_x = x + local_x as i32; + if world_x < 0 || world_x >= screen_w as i32 { + continue; + } + + let fetch_x = if entry.flip_x() { size - 1 - local_x } else { local_x }; + let fetch_y = if entry.flip_y() { size - 1 - local_y } else { local_y }; + let px_index = bank.get_pixel_index(entry.glyph_id, fetch_x, fetch_y); + if px_index == 0 { + continue; + } + + let color = bank.resolve_color(entry.palette_id, px_index); + back[world_y as usize * screen_w + world_x as usize] = color.raw(); + } + } + } + fn draw_bucket_on_buffer( back: &mut [u16], screen_w: usize, @@ -844,7 +976,88 @@ impl Gfx { #[cfg(test)] mod tests { use super::*; - use crate::memory_banks::MemoryBanks; + use crate::memory_banks::{GlyphBankPoolInstaller, MemoryBanks}; + use prometeu_hal::glyph_bank::TileSize; + use prometeu_hal::scene_bank::SceneBank; + use prometeu_hal::scene_layer::{MotionFactor, SceneLayer}; + use prometeu_hal::scene_viewport_cache::SceneViewportCache; + use prometeu_hal::scene_viewport_resolver::SceneViewportResolver; + use prometeu_hal::tile::Tile; + use prometeu_hal::tilemap::TileMap; + + fn make_glyph_bank(tile_size: TileSize, palette_colors: &[(u8, Color)]) -> GlyphBank { + let size = tile_size as usize; + let mut bank = GlyphBank::new(tile_size, size, size); + for (palette_id, color) in palette_colors { + bank.palettes[*palette_id as usize][1] = *color; + } + for y in 0..size { + for x in 0..size { + bank.pixel_indices[y * bank.width + x] = 1; + } + } + bank + } + + fn make_layer( + glyph_bank_id: u8, + glyph_id: u16, + palette_id: u8, + width: usize, + height: usize, + ) -> SceneLayer { + SceneLayer { + active: true, + glyph_bank_id, + tile_size: TileSize::Size8, + motion_factor: MotionFactor { x: 1.0, y: 1.0 }, + tilemap: TileMap { + width, + height, + tiles: vec![ + Tile { + active: true, + glyph: Glyph { glyph_id, palette_id }, + flip_x: false, + flip_y: false, + }; + width * height + ], + }, + } + } + + fn make_inactive_layer(glyph_bank_id: u8, width: usize, height: usize) -> SceneLayer { + SceneLayer { + active: false, + glyph_bank_id, + tile_size: TileSize::Size8, + motion_factor: MotionFactor { x: 1.0, y: 1.0 }, + tilemap: TileMap { width, height, tiles: vec![Tile::default(); width * height] }, + } + } + + fn make_scene(palette_ids: [u8; 4]) -> SceneBank { + SceneBank { + layers: [ + make_layer(0, 0, palette_ids[0], 8, 8), + make_layer(0, 0, palette_ids[1], 8, 8), + make_layer(0, 0, palette_ids[2], 8, 8), + make_layer(0, 0, palette_ids[3], 8, 8), + ], + } + } + + fn make_scene_with_inactive_top_layers() -> SceneBank { + SceneBank { + layers: [ + make_layer(0, 0, 0, 8, 8), + make_layer(0, 0, 1, 8, 8), + make_layer(0, 0, 2, 8, 8), + make_inactive_layer(0, 8, 8), + ], + } + } #[test] fn test_draw_pixel() { @@ -896,6 +1109,79 @@ mod tests { // Fill assert_eq!(gfx.back[3 * 10 + 3], Color::BLACK.0); } + + #[test] + fn render_scene_from_cache_uses_materialized_cache_not_canonical_scene() { + let banks = Arc::new(MemoryBanks::new()); + banks.install_glyph_bank( + 0, + Arc::new(make_glyph_bank(TileSize::Size8, &[(0, Color::RED), (1, Color::GREEN)])), + ); + + let mut scene = make_scene([0, 0, 0, 0]); + let mut cache = SceneViewportCache::new(&scene, 4, 4); + cache.materialize_all_layers(&scene); + + scene.layers[0].tilemap.tiles[0].glyph.palette_id = 1; + + let mut resolver = SceneViewportResolver::new(16, 16, 4, 4, 12, 20); + let update = resolver.update(&scene, 0, 0); + + let mut gfx = Gfx::new(16, 16, banks); + gfx.scene_fade_level = 31; + gfx.hud_fade_level = 31; + gfx.render_scene_from_cache(&cache, &update); + + assert_eq!(gfx.back[0], Color::RED.raw()); + } + + #[test] + fn render_scene_from_cache_preserves_layer_and_sprite_order() { + let banks = Arc::new(MemoryBanks::new()); + banks.install_glyph_bank( + 0, + Arc::new(make_glyph_bank( + TileSize::Size8, + &[(0, Color::RED), (1, Color::GREEN), (2, Color::BLUE), (4, Color::WHITE)], + )), + ); + + let scene = make_scene_with_inactive_top_layers(); + let mut cache = SceneViewportCache::new(&scene, 4, 4); + cache.materialize_all_layers(&scene); + + let mut resolver = SceneViewportResolver::new(16, 16, 4, 4, 12, 20); + let update = resolver.update(&scene, 0, 0); + + let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc); + gfx.scene_fade_level = 31; + gfx.hud_fade_level = 31; + + gfx.sprites[0] = Sprite { + glyph: Glyph { glyph_id: 0, palette_id: 4 }, + x: 0, + y: 0, + bank_id: 0, + active: true, + flip_x: false, + flip_y: false, + priority: 0, + }; + gfx.sprites[1] = Sprite { + glyph: Glyph { glyph_id: 0, palette_id: 4 }, + x: 0, + y: 0, + bank_id: 0, + active: true, + flip_x: false, + flip_y: false, + priority: 2, + }; + + gfx.render_scene_from_cache(&cache, &update); + + assert_eq!(gfx.back[0], Color::BLUE.raw()); + } } /// Blends in RGB565 per channel with saturation.