From 5a0476e8b0eaee2fc31992246de3fdc1d1f0bc39 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 17 Apr 2026 13:24:25 +0100 Subject: [PATCH] implements PLN-0018 --- .../prometeu-drivers/src/frame_composer.rs | 232 +++++++++++++++++- crates/console/prometeu-drivers/src/gfx.rs | 130 +++++++--- crates/console/prometeu-hal/src/gfx_bridge.rs | 1 + crates/console/prometeu-hal/src/sprite.rs | 1 + .../src/virtual_machine_runtime/dispatch.rs | 1 + 5 files changed, 323 insertions(+), 42 deletions(-) diff --git a/crates/console/prometeu-drivers/src/frame_composer.rs b/crates/console/prometeu-drivers/src/frame_composer.rs index 1a7b77cc..f5b83c8e 100644 --- a/crates/console/prometeu-drivers/src/frame_composer.rs +++ b/crates/console/prometeu-drivers/src/frame_composer.rs @@ -10,6 +10,7 @@ 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, @@ -26,6 +27,10 @@ pub enum SceneStatus { #[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 { @@ -36,7 +41,13 @@ impl Default for SpriteController { impl SpriteController { pub fn new() -> Self { - Self { sprites: [EMPTY_SPRITE; 512] } + 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] { @@ -46,6 +57,57 @@ impl SpriteController { 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 { @@ -130,13 +192,24 @@ impl FrameComposer { 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() + } } #[cfg(test)] mod tests { use super::*; use crate::memory_banks::{MemoryBanks, SceneBankPoolInstaller}; - use prometeu_hal::glyph::Glyph; use prometeu_hal::glyph_bank::TileSize; use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer}; use prometeu_hal::tile::Tile; @@ -178,7 +251,8 @@ mod tests { assert!(frame_composer.cache().is_none()); assert!(frame_composer.resolver().is_none()); assert_eq!(frame_composer.sprite_controller().sprites().len(), 512); - assert!(frame_composer.sprite_controller().sprites().iter().all(|sprite| !sprite.active)); + assert_eq!(frame_composer.sprite_controller().sprite_count(), 0); + assert_eq!(frame_composer.sprite_controller().dropped_sprites(), 0); } #[test] @@ -194,4 +268,156 @@ mod tests { assert_eq!(scene.layers[0].tile_size, TileSize::Size8); assert_eq!(scene.layers[0].parallax_factor.y, 0.5); } + + #[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); + } } diff --git a/crates/console/prometeu-drivers/src/gfx.rs b/crates/console/prometeu-drivers/src/gfx.rs index 9462d8dc..e78b3e4c 100644 --- a/crates/console/prometeu-drivers/src/gfx.rs +++ b/crates/console/prometeu-drivers/src/gfx.rs @@ -71,8 +71,10 @@ pub struct Gfx { /// Target color for the HUD fade effect. pub hud_fade_color: Color, - /// Internal cache used to sort sprites into priority groups to optimize rendering. - priority_buckets: [Vec; 5], + /// Internal sprite count for the current frame state. + sprite_count: usize, + /// Internal cache used to sort sprites by layer while keeping stable priority order. + layer_buckets: [Vec; 4], } const GLYPH_UNKNOWN: [u8; 5] = [0x7, 0x7, 0x7, 0x7, 0x7]; @@ -213,6 +215,9 @@ impl GfxBridge for Gfx { fn render_scene_from_cache(&mut self, cache: &SceneViewportCache, update: &ResolverUpdate) { self.render_scene_from_cache(cache, update) } + fn load_frame_sprites(&mut self, sprites: &[Sprite]) { + self.load_frame_sprites(sprites) + } fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color) { self.draw_text(x, y, text, color) } @@ -224,6 +229,7 @@ impl GfxBridge for Gfx { &self.sprites[index] } fn sprite_mut(&mut self, index: usize) -> &mut Sprite { + self.sprite_count = self.sprite_count.max(index.saturating_add(1)).min(self.sprites.len()); &mut self.sprites[index] } @@ -262,6 +268,7 @@ impl Gfx { glyph: EMPTY_GLYPH, x: 0, y: 0, + layer: 0, bank_id: 0, active: false, flip_x: false, @@ -277,12 +284,12 @@ impl Gfx { back: vec![0; len], glyph_banks, sprites: [EMPTY_SPRITE; 512], + sprite_count: 0, scene_fade_level: 31, scene_fade_color: Color::BLACK, hud_fade_level: 31, hud_fade_color: Color::BLACK, - priority_buckets: [ - Vec::with_capacity(128), + layer_buckets: [ Vec::with_capacity(128), Vec::with_capacity(128), Vec::with_capacity(128), @@ -530,23 +537,33 @@ impl Gfx { std::mem::swap(&mut self.front, &mut self.back); } + pub fn load_frame_sprites(&mut self, sprites: &[Sprite]) { + self.sprite_count = sprites.len().min(self.sprites.len()); + for (index, sprite) in sprites.iter().copied().take(self.sprites.len()).enumerate() { + self.sprites[index] = Sprite { active: true, ..sprite }; + } + for sprite in self.sprites.iter_mut().skip(self.sprite_count) { + sprite.active = false; + } + } + /// The main rendering pipeline. /// /// This method composes the final frame by rasterizing layers and sprites in the /// 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) { - self.populate_priority_buckets(); - - // 1. Priority 0 sprites: drawn at the very back, behind everything else. - Self::draw_bucket_on_buffer( - &mut self.back, - self.w, - self.h, - &self.priority_buckets[0], - &self.sprites, - &*self.glyph_banks, - ); + self.populate_layer_buckets(); + for bucket in &self.layer_buckets { + Self::draw_bucket_on_buffer( + &mut self.back, + self.w, + self.h, + bucket, + &self.sprites, + &*self.glyph_banks, + ); + } // 2. Scene-only fallback path: sprites and fades still work even before a // cache-backed world composition request is issued for the frame. @@ -563,18 +580,17 @@ impl Gfx { /// 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, - ); + self.populate_layer_buckets(); for layer_index in 0..cache.layers.len() { + Self::draw_bucket_on_buffer( + &mut self.back, + self.w, + self.h, + &self.layer_buckets[layer_index], + &self.sprites, + &*self.glyph_banks, + ); Self::draw_cache_layer_to_buffer( &mut self.back, self.w, @@ -583,31 +599,26 @@ impl Gfx { &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); } - fn populate_priority_buckets(&mut self) { - for bucket in self.priority_buckets.iter_mut() { + fn populate_layer_buckets(&mut self) { + for bucket in self.layer_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); + for (idx, sprite) in self.sprites.iter().take(self.sprite_count).enumerate() { + if sprite.active && (sprite.layer as usize) < self.layer_buckets.len() { + self.layer_buckets[sprite.layer as usize].push(idx); } } + + for bucket in self.layer_buckets.iter_mut() { + bucket.sort_by_key(|&idx| self.sprites[idx].priority); + } } fn draw_cache_layer_to_buffer( @@ -1004,6 +1015,7 @@ mod tests { glyph: Glyph { glyph_id: 0, palette_id: 4 }, x: 0, y: 0, + layer: 0, bank_id: 0, active: true, flip_x: false, @@ -1014,17 +1026,57 @@ mod tests { glyph: Glyph { glyph_id: 0, palette_id: 4 }, x: 0, y: 0, + layer: 2, bank_id: 0, active: true, flip_x: false, flip_y: false, priority: 2, }; + gfx.sprite_count = 2; gfx.render_scene_from_cache(&cache, &update); assert_eq!(gfx.back[0], Color::BLUE.raw()); } + + #[test] + fn load_frame_sprites_replaces_slot_first_submission_for_render_state() { + let banks = Arc::new(MemoryBanks::new()); + let mut gfx = Gfx::new(16, 16, banks as Arc); + + gfx.load_frame_sprites(&[ + Sprite { + glyph: Glyph { glyph_id: 1, palette_id: 2 }, + x: 2, + y: 3, + layer: 1, + bank_id: 4, + active: false, + flip_x: true, + flip_y: false, + priority: 7, + }, + Sprite { + glyph: Glyph { glyph_id: 5, palette_id: 6 }, + x: 7, + y: 8, + layer: 3, + bank_id: 9, + active: false, + flip_x: false, + flip_y: true, + priority: 1, + }, + ]); + + assert_eq!(gfx.sprite_count, 2); + assert!(gfx.sprites[0].active); + assert!(gfx.sprites[1].active); + assert!(!gfx.sprites[2].active); + assert_eq!(gfx.sprites[0].layer, 1); + assert_eq!(gfx.sprites[1].glyph.glyph_id, 5); + } } /// Blends in RGB565 per channel with saturation. diff --git a/crates/console/prometeu-hal/src/gfx_bridge.rs b/crates/console/prometeu-hal/src/gfx_bridge.rs index 1c677ab4..b000d20d 100644 --- a/crates/console/prometeu-hal/src/gfx_bridge.rs +++ b/crates/console/prometeu-hal/src/gfx_bridge.rs @@ -50,6 +50,7 @@ pub trait GfxBridge { fn present(&mut self); fn render_all(&mut self); fn render_scene_from_cache(&mut self, cache: &SceneViewportCache, update: &ResolverUpdate); + fn load_frame_sprites(&mut self, sprites: &[Sprite]); fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color); fn draw_char(&mut self, x: i32, y: i32, c: char, color: Color); diff --git a/crates/console/prometeu-hal/src/sprite.rs b/crates/console/prometeu-hal/src/sprite.rs index 1d5f141f..e38ae920 100644 --- a/crates/console/prometeu-hal/src/sprite.rs +++ b/crates/console/prometeu-hal/src/sprite.rs @@ -5,6 +5,7 @@ pub struct Sprite { pub glyph: Glyph, pub x: i32, pub y: i32, + pub layer: u8, pub bank_id: u8, pub active: bool, pub flip_x: bool, diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs b/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs index e6037679..86184215 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs @@ -166,6 +166,7 @@ impl NativeInterface for VirtualMachineRuntime { glyph: Glyph { glyph_id, palette_id }, x, y, + layer: 0, bank_id, active, flip_x,