2026-04-17 13:28:34 +01:00

715 lines
22 KiB
Rust

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<usize>; 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<Sprite> {
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<dyn SceneBankPoolAccess>,
viewport_width_px: usize,
viewport_height_px: usize,
active_scene_id: Option<usize>,
active_scene: Option<Arc<SceneBank>>,
scene_status: SceneStatus,
camera_x_px: i32,
camera_y_px: i32,
cache: Option<SceneViewportCache>,
resolver: Option<SceneViewportResolver>,
sprite_controller: SpriteController,
}
impl FrameComposer {
pub fn new(
viewport_width_px: usize,
viewport_height_px: usize,
scene_bank_pool: Arc<dyn SceneBankPoolAccess>,
) -> 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<dyn SceneBankPoolAccess> {
&self.scene_bank_pool
}
pub fn scene_bank_slot(&self, slot: usize) -> Option<Arc<SceneBank>> {
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<usize> {
self.active_scene_id
}
pub fn active_scene(&self) -> Option<&Arc<SceneBank>> {
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<Sprite> {
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<u16> = 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<dyn SceneBankPoolAccess>);
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<dyn GlyphBankPoolAccess>);
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<dyn SceneBankPoolAccess>);
assert!(frame_composer.bind_scene(0));
let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc<dyn GlyphBankPoolAccess>);
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<dyn SceneBankPoolAccess>);
let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc<dyn GlyphBankPoolAccess>);
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());
}
}