bQUARKz 29d531f622
All checks were successful
Intrepid/Prometeu/Runtime/pipeline/head This commit looks good
Intrepid/Prometeu/Runtime/pipeline/pr-master This commit looks good
(fix) issues regarding tilemap cache and render
2026-05-01 05:54:23 +01:00

934 lines
30 KiB
Rust

use crate::asset::GlyphAssetSlotIndex;
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, ResolverUpdate, 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>,
glyph_slot_index: GlyphAssetSlotIndex,
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>,
glyph_slot_index: GlyphAssetSlotIndex,
) -> Self {
Self {
scene_bank_pool,
glyph_slot_index,
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 _ = self.resolve_glyph_slots(&scene, scene_bank_id, "bind_scene");
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) = self.active_scene.as_deref() {
let scene_bank_id = self.active_scene_id.expect("active scene id must exist");
let resolved_glyph_slots =
self.resolve_glyph_slots(scene, scene_bank_id, "render_frame");
let (Some(cache), Some(resolver)) = (self.cache.as_mut(), self.resolver.as_mut())
else {
panic!("SCENE runtime invariant broken: active scene without cache/resolver");
};
let update = resolver.update(scene, self.camera_x_px, self.camera_y_px);
Self::sync_cache_windows(cache, &update);
Self::apply_refresh_requests(cache, scene, &update.refresh_requests);
// `FrameComposer` owns only canonical game-frame composition.
// Deferred `gfx.*` primitives are drained later by a separate
// overlay/debug stage outside this service boundary.
gfx.render_scene_from_cache(cache, &update, &resolved_glyph_slots);
return;
}
// No-scene frames still stop at canonical game composition. Final
// overlay/debug work remains outside `FrameComposer`.
gfx.render_no_scene_frame();
}
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 resolve_glyph_slots(
&self,
scene: &SceneBank,
scene_bank_id: usize,
phase: &str,
) -> [usize; 4] {
std::array::from_fn(|layer_index| {
let layer = &scene.layers[layer_index];
self.glyph_slot_index.glyph_slot_for_asset(layer.glyph_asset_id).unwrap_or_else(|| {
panic!(
"SCENE dependency fatal during {}: scene_bank_id={}, layer={}, glyph_asset_id={}",
phase, scene_bank_id, layer_index, layer.glyph_asset_id
)
})
})
}
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);
}
}
}
}
fn sync_cache_windows(cache: &mut SceneViewportCache, update: &ResolverUpdate) {
for request in &update.copy_requests {
cache.move_layer_window_to(
request.layer_index,
request.cache_origin_tile_x,
request.cache_origin_tile_y,
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gfx::Gfx;
use crate::hardware::Hardware;
use crate::memory_banks::{
GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolInstaller,
};
use prometeu_hal::asset::AssetId;
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;
use std::panic::{AssertUnwindSafe, catch_unwind};
fn make_scene() -> SceneBank {
make_scene_with_palette(1, 1, TileSize::Size8)
}
fn make_scene_with_palette(
glyph_asset_id: AssetId,
palette_id: u8,
tile_size: TileSize,
) -> SceneBank {
make_scene_grid(glyph_asset_id, palette_id, tile_size, 2, 2)
}
fn make_scene_grid(
glyph_asset_id: AssetId,
palette_id: u8,
tile_size: TileSize,
width: usize,
height: usize,
) -> SceneBank {
let layer = SceneLayer {
active: true,
glyph_asset_id,
tile_size,
parallax_factor: ParallaxFactor { x: 1.0, y: 0.5 },
tilemap: TileMap {
width,
height,
tiles: vec![
Tile {
active: true,
glyph: Glyph { glyph_id: 0, palette_id },
flip_x: false,
flip_y: false,
};
width * height
],
},
};
SceneBank { layers: std::array::from_fn(|_| layer.clone()) }
}
fn make_glyph_slot_index(bindings: &[(AssetId, usize)]) -> GlyphAssetSlotIndex {
let index = GlyphAssetSlotIndex::new();
let mut slots = [None; 16];
for (asset_id, slot) in bindings {
slots[*slot] = Some(*asset_id);
}
index.rebuild_from_slots(&slots);
index
}
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(
Hardware::W,
Hardware::H,
Arc::new(MemoryBanks::new()),
make_glyph_slot_index(&[]),
);
assert_eq!(frame_composer.viewport_size(), (Hardware::W, Hardware::H));
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(Hardware::W, Hardware::H, banks, make_glyph_slot_index(&[(1, 0)]));
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(Hardware::W, Hardware::H, banks, make_glyph_slot_index(&[(1, 0)]));
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(Hardware::W, Hardware::H, banks, make_glyph_slot_index(&[(1, 0)]));
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(
Hardware::W,
Hardware::H,
Arc::new(MemoryBanks::new()),
make_glyph_slot_index(&[]),
);
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(Hardware::W, Hardware::H, banks, make_glyph_slot_index(&[(1, 0)]));
assert!(frame_composer.bind_scene(0));
let cache = frame_composer.cache().expect("cache should exist for bound scene");
assert_eq!((cache.width(), cache.height()), (65, 38));
}
#[test]
fn missing_scene_binding_falls_back_to_no_scene_state() {
let mut frame_composer = FrameComposer::new(
Hardware::W,
Hardware::H,
Arc::new(MemoryBanks::new()),
make_glyph_slot_index(&[]),
);
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 bind_scene_panics_when_scene_dependency_is_not_resident() {
let banks = Arc::new(MemoryBanks::new());
banks.install_scene_bank(0, Arc::new(make_scene()));
let mut frame_composer =
FrameComposer::new(Hardware::W, Hardware::H, banks, make_glyph_slot_index(&[]));
let panic = catch_unwind(AssertUnwindSafe(|| frame_composer.bind_scene(0)))
.expect_err("bind_scene should panic when dependency is missing");
let message = if let Some(message) = panic.downcast_ref::<String>() {
message.clone()
} else if let Some(message) = panic.downcast_ref::<&str>() {
(*message).to_string()
} else {
String::new()
};
assert!(message.contains("SCENE dependency fatal during bind_scene"));
assert!(message.contains("glyph_asset_id=1"));
}
#[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(
Hardware::W,
Hardware::H,
Arc::new(MemoryBanks::new()),
make_glyph_slot_index(&[]),
);
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>,
make_glyph_slot_index(&[]),
);
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>,
make_glyph_slot_index(&[(0, 0)]),
);
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_realigns_cache_window_before_refresh_when_camera_moves() {
let banks = Arc::new(MemoryBanks::new());
let scene = Arc::new(make_scene_grid(0, 2, TileSize::Size8, 16, 16));
banks.install_glyph_bank(0, Arc::new(make_glyph_bank(TileSize::Size8, 2, Color::BLUE)));
banks.install_scene_bank(0, Arc::clone(&scene));
let mut frame_composer = FrameComposer::new(
16,
16,
Arc::clone(&banks) as Arc<dyn SceneBankPoolAccess>,
make_glyph_slot_index(&[(0, 0)]),
);
assert!(frame_composer.bind_scene(0));
let mut expected_resolver = FrameComposer::build_scene_runtime(16, 16, &scene).1;
expected_resolver.update(&scene, 0, 0);
let expected_update = expected_resolver.update(&scene, 40, 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);
frame_composer.set_camera(40, 0);
frame_composer.render_frame(&mut gfx);
let cache = frame_composer.cache().expect("cache should exist");
assert_eq!(
cache.layers[0].logical_origin(),
(
expected_update.copy_requests[0].cache_origin_tile_x,
expected_update.copy_requests[0].cache_origin_tile_y,
)
);
}
#[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>,
make_glyph_slot_index(&[(0, 0), (1, 1)]),
);
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());
}
#[test]
fn render_frame_panics_when_dependency_disappears_after_bind() {
let banks = Arc::new(MemoryBanks::new());
banks.install_glyph_bank(0, Arc::new(make_glyph_bank(TileSize::Size8, 1, Color::RED)));
banks.install_scene_bank(0, Arc::new(make_scene_with_palette(1, 1, TileSize::Size8)));
let glyph_slot_index = make_glyph_slot_index(&[(1, 0)]);
let mut frame_composer = FrameComposer::new(
16,
16,
Arc::clone(&banks) as Arc<dyn SceneBankPoolAccess>,
glyph_slot_index.clone(),
);
assert!(frame_composer.bind_scene(0));
glyph_slot_index.rebuild_from_slots(&[None; 16]);
let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc<dyn GlyphBankPoolAccess>);
let panic = catch_unwind(AssertUnwindSafe(|| frame_composer.render_frame(&mut gfx)))
.expect_err("render_frame should panic when dependency disappears");
let message = if let Some(message) = panic.downcast_ref::<String>() {
message.clone()
} else if let Some(message) = panic.downcast_ref::<&str>() {
(*message).to_string()
} else {
String::new()
};
assert!(message.contains("SCENE dependency fatal during render_frame"));
assert!(message.contains("glyph_asset_id=1"));
}
#[test]
fn render_frame_resolves_glyph_dependency_by_asset_id_not_by_slot_number() {
let banks = Arc::new(MemoryBanks::new());
banks.install_glyph_bank(7, Arc::new(make_glyph_bank(TileSize::Size8, 2, Color::BLUE)));
banks.install_scene_bank(0, Arc::new(make_scene_with_palette(42, 2, TileSize::Size8)));
let mut frame_composer = FrameComposer::new(
16,
16,
Arc::clone(&banks) as Arc<dyn SceneBankPoolAccess>,
make_glyph_slot_index(&[(42, 7)]),
);
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_eq!(gfx.front_buffer()[0], Color::BLUE.raw());
}
}