diff --git a/crates/console/prometeu-drivers/src/gfx.rs b/crates/console/prometeu-drivers/src/gfx.rs index 4a1c9dbb..47b06899 100644 --- a/crates/console/prometeu-drivers/src/gfx.rs +++ b/crates/console/prometeu-drivers/src/gfx.rs @@ -79,6 +79,21 @@ pub struct Gfx { const GLYPH_UNKNOWN: [u8; 5] = [0x7, 0x7, 0x7, 0x7, 0x7]; +struct RenderTarget<'a> { + back: &'a mut [u16], + screen_w: usize, + screen_h: usize, +} + +#[derive(Clone, Copy)] +struct CachedTileDraw<'a> { + x: i32, + y: i32, + entry: CachedTileEntry, + bank: &'a GlyphBank, + tile_size: prometeu_hal::glyph_bank::TileSize, +} + #[inline] fn glyph_for_char(c: char) -> &'static [u8; 5] { match c.to_ascii_uppercase() { @@ -629,6 +644,7 @@ impl Gfx { request: &LayerCopyRequest, glyph_banks: &dyn GlyphBankPoolAccess, ) { + let mut target = RenderTarget { back, screen_w, screen_h }; let layer_cache = &cache.layers[request.layer_index]; if !layer_cache.valid { return; @@ -657,52 +673,43 @@ impl Gfx { } Self::draw_cached_tile_pixels( - back, - screen_w, - screen_h, - screen_tile_x, - screen_tile_y, - entry, - &bank, - request.tile_size, + &mut target, + CachedTileDraw { + x: screen_tile_x, + y: screen_tile_y, + entry, + bank: &bank, + tile_size: 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; + fn draw_cached_tile_pixels(target: &mut RenderTarget<'_>, tile: CachedTileDraw<'_>) { + let size = tile.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 { + let world_y = tile.y + local_y as i32; + if world_y < 0 || world_y >= target.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 { + let world_x = tile.x + local_x as i32; + if world_x < 0 || world_x >= target.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); + let fetch_x = if tile.entry.flip_x() { size - 1 - local_x } else { local_x }; + let fetch_y = if tile.entry.flip_y() { size - 1 - local_y } else { local_y }; + let px_index = tile.bank.get_pixel_index(tile.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(); + let color = tile.bank.resolve_color(tile.entry.palette_id, px_index); + target.back[world_y as usize * target.screen_w + world_x as usize] = color.raw(); } } } diff --git a/crates/console/prometeu-hal/src/scene_viewport_cache.rs b/crates/console/prometeu-hal/src/scene_viewport_cache.rs index 4c2ee614..028cc7ff 100644 --- a/crates/console/prometeu-hal/src/scene_viewport_cache.rs +++ b/crates/console/prometeu-hal/src/scene_viewport_cache.rs @@ -325,6 +325,34 @@ mod tests { assert_eq!(cache.layers[0].ring_origin(), (1, 1)); } + #[test] + fn layer_cache_wraps_ring_origin_for_negative_and_large_movements() { + let scene = make_scene(); + let mut cache = SceneViewportCache::new(&scene, 3, 3); + + cache.move_layer_window_by(0, -1, -4); + assert_eq!(cache.layers[0].logical_origin(), (-1, -4)); + assert_eq!(cache.layers[0].ring_origin(), (2, 2)); + + cache.move_layer_window_by(0, 7, 8); + assert_eq!(cache.layers[0].logical_origin(), (6, 4)); + assert_eq!(cache.layers[0].ring_origin(), (0, 1)); + } + + #[test] + fn move_window_to_matches_incremental_ring_movement() { + let scene = make_scene(); + let mut direct = SceneViewportCache::new(&scene, 4, 4); + let mut incremental = SceneViewportCache::new(&scene, 4, 4); + + direct.move_layer_window_to(0, 9, -6); + incremental.move_layer_window_by(0, 5, -2); + incremental.move_layer_window_by(0, 4, -4); + + assert_eq!(direct.layers[0].logical_origin(), incremental.layers[0].logical_origin()); + assert_eq!(direct.layers[0].ring_origin(), incremental.layers[0].ring_origin()); + } + #[test] fn cache_entry_fields_are_derived_from_scene_tiles() { let scene = make_scene(); @@ -415,6 +443,113 @@ mod tests { assert_eq!(cache.layers[3].entry(3, 3).glyph_id, 415); } + #[test] + fn refresh_after_wrapped_window_move_materializes_new_logical_tiles() { + let scene = make_scene(); + let mut cache = SceneViewportCache::new(&scene, 3, 3); + + cache.refresh_layer_all(&scene, 0); + cache.move_layer_window_to(0, 1, 2); + cache.refresh_layer_all(&scene, 0); + + assert_eq!(cache.layers[0].logical_origin(), (1, 2)); + assert_eq!(cache.layers[0].ring_origin(), (1, 2)); + assert_eq!(cache.layers[0].entry(0, 0).glyph_id, 109); + assert_eq!(cache.layers[0].entry(1, 0).glyph_id, 110); + assert_eq!(cache.layers[0].entry(2, 0).glyph_id, 111); + assert_eq!(cache.layers[0].entry(0, 1).glyph_id, 113); + assert_eq!(cache.layers[0].entry(2, 1).glyph_id, 115); + assert_eq!(cache.layers[0].entry(0, 2), CachedTileEntry::default()); + } + + #[test] + fn partial_refresh_uses_wrapped_physical_slots_after_window_move() { + let scene = make_scene(); + let mut cache = SceneViewportCache::new(&scene, 3, 3); + + cache.move_layer_window_to(0, 1, 0); + cache.refresh_layer_column(&scene, 0, 0); + + assert_eq!(cache.layers[0].ring_origin(), (1, 0)); + assert_eq!(cache.layers[0].entry(0, 0).glyph_id, 101); + assert_eq!(cache.layers[0].entry(0, 1).glyph_id, 105); + assert_eq!(cache.layers[0].entry(0, 2).glyph_id, 109); + assert_eq!(cache.layers[0].entry(1, 0), CachedTileEntry::default()); + assert_eq!(cache.layers[0].entry(2, 0), CachedTileEntry::default()); + } + + #[test] + fn out_of_bounds_logical_origins_materialize_default_entries_after_wrap() { + let scene = make_scene(); + let mut cache = SceneViewportCache::new(&scene, 2, 2); + + cache.move_layer_window_to(0, -2, 3); + cache.refresh_layer_all(&scene, 0); + + for y in 0..2 { + for x in 0..2 { + assert_eq!(cache.layers[0].entry(x, y), CachedTileEntry::default()); + } + } + } + + #[test] + fn ringbuffer_preserves_logical_tile_mapping_across_long_mixed_movements() { + let scene = make_scene(); + let mut cache = SceneViewportCache::new(&scene, 3, 3); + let motions = [ + (1, 0), + (0, 1), + (2, 2), + (-1, 0), + (0, -2), + (4, 1), + (-3, 3), + (5, -4), + (-6, 2), + (3, -3), + (7, 7), + (-8, -5), + ]; + + for &(dx, dy) in &motions { + cache.move_layer_window_by(0, dx, dy); + cache.refresh_layer_all(&scene, 0); + + let (origin_x, origin_y) = cache.layers[0].logical_origin(); + for cache_y in 0..cache.height() { + for cache_x in 0..cache.width() { + let expected_scene_x = origin_x + cache_x as i32; + let expected_scene_y = origin_y + cache_y as i32; + + let expected = if expected_scene_x < 0 + || expected_scene_y < 0 + || expected_scene_x as usize >= scene.layers[0].tilemap.width + || expected_scene_y as usize >= scene.layers[0].tilemap.height + { + CachedTileEntry::default() + } else { + let tile_x = expected_scene_x as usize; + let tile_y = expected_scene_y as usize; + let tile = scene.layers[0].tilemap.tiles + [tile_y * scene.layers[0].tilemap.width + tile_x]; + CachedTileEntry::from_tile(&scene.layers[0], tile) + }; + + assert_eq!( + cache.layers[0].entry(cache_x, cache_y), + expected, + "mismatch at logical origin ({}, {}), cache ({}, {})", + origin_x, + origin_y, + cache_x, + cache_y + ); + } + } + } + } + #[test] fn materialization_populates_all_four_layers() { let scene = make_scene();