use crate::glyph_bank::TileSize; use crate::scene_bank::SceneBank; use crate::scene_viewport_cache::ViewportRegion; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct TileAnchor { pub x: i32, pub y: i32, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum CacheRefreshRequest { InvalidateLayer { layer_index: usize }, RefreshLine { layer_index: usize, cache_y: usize }, RefreshColumn { layer_index: usize, cache_x: usize }, RefreshRegion { layer_index: usize, region: ViewportRegion }, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct LayerCopyRequest { pub layer_index: usize, pub tile_size: TileSize, pub viewport_width_px: i32, pub viewport_height_px: i32, pub source_offset_x_px: i32, pub source_offset_y_px: i32, pub cache_origin_tile_x: i32, pub cache_origin_tile_y: i32, pub camera_x_px: i32, pub camera_y_px: i32, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct ResolverUpdate { pub master_anchor: TileAnchor, pub layer_anchors: [TileAnchor; 4], pub refresh_requests: Vec, pub copy_requests: [LayerCopyRequest; 4], } #[derive(Clone, Debug)] pub struct SceneViewportResolver { viewport_width_px: i32, viewport_height_px: i32, cache_width_tiles: usize, cache_height_tiles: usize, hysteresis_safe_px: i32, hysteresis_trigger_px: i32, initialized: bool, master_anchor: TileAnchor, layer_anchors: [TileAnchor; 4], } impl SceneViewportResolver { pub fn new( viewport_width_px: i32, viewport_height_px: i32, cache_width_tiles: usize, cache_height_tiles: usize, hysteresis_safe_px: i32, hysteresis_trigger_px: i32, ) -> Self { Self { viewport_width_px, viewport_height_px, cache_width_tiles, cache_height_tiles, hysteresis_safe_px, hysteresis_trigger_px, initialized: false, master_anchor: TileAnchor::default(), layer_anchors: [TileAnchor::default(); 4], } } pub fn reset_scene(&mut self) -> Vec { self.initialized = false; vec![ CacheRefreshRequest::InvalidateLayer { layer_index: 0 }, CacheRefreshRequest::InvalidateLayer { layer_index: 1 }, CacheRefreshRequest::InvalidateLayer { layer_index: 2 }, CacheRefreshRequest::InvalidateLayer { layer_index: 3 }, ] } pub fn update( &mut self, scene: &SceneBank, camera_x_px: i32, camera_y_px: i32, ) -> ResolverUpdate { let mut refresh_requests = Vec::new(); let camera_center_x = camera_x_px + self.viewport_width_px / 2; let camera_center_y = camera_y_px + self.viewport_height_px / 2; let layer_inputs: [(i32, i32, i32, i32, i32); 4] = std::array::from_fn(|i| { let layer = &scene.layers[i]; let tile_size_px = layer.tile_size as i32; let layer_camera_x_px = ((camera_x_px as f32) * layer.motion_factor.x).floor() as i32; let layer_camera_y_px = ((camera_y_px as f32) * layer.motion_factor.y).floor() as i32; let layer_center_x_px = layer_camera_x_px + self.viewport_width_px / 2; let layer_center_y_px = layer_camera_y_px + self.viewport_height_px / 2; ( tile_size_px, layer_camera_x_px, layer_camera_y_px, layer_center_x_px, layer_center_y_px, ) }); if !self.initialized { self.master_anchor = self.initial_anchor( scene.layers[0].tilemap.width, scene.layers[0].tilemap.height, scene.layers[0].tile_size as i32, camera_center_x, camera_center_y, ); for (layer_index, layer) in scene.layers.iter().enumerate() { let (_, _, _, layer_center_x_px, layer_center_y_px) = layer_inputs[layer_index]; self.layer_anchors[layer_index] = self.initial_anchor( layer.tilemap.width, layer.tilemap.height, layer.tile_size as i32, layer_center_x_px, layer_center_y_px, ); refresh_requests.push(CacheRefreshRequest::RefreshRegion { layer_index, region: ViewportRegion::new( 0, 0, self.cache_width_tiles, self.cache_height_tiles, ), }); } self.initialized = true; } else { let layer0 = &scene.layers[0]; self.master_anchor = self.advance_anchor( self.master_anchor, camera_center_x, camera_center_y, layer0.tile_size as i32, layer0.tilemap.width, layer0.tilemap.height, ); for (layer_index, layer) in scene.layers.iter().enumerate() { let previous = self.layer_anchors[layer_index]; let (tile_size_px, _, _, layer_center_x_px, layer_center_y_px) = layer_inputs[layer_index]; let next = self.advance_anchor( previous, layer_center_x_px, layer_center_y_px, tile_size_px, layer.tilemap.width, layer.tilemap.height, ); self.layer_anchors[layer_index] = next; self.emit_refresh_requests(layer_index, previous, next, &mut refresh_requests); } } let copy_requests = std::array::from_fn(|layer_index| { let layer = &scene.layers[layer_index]; let (tile_size_px, layer_camera_x_px, layer_camera_y_px, _, _) = layer_inputs[layer_index]; let anchor = self.layer_anchors[layer_index]; let cache_origin_tile_x = anchor.x - (self.cache_width_tiles as i32 / 2); let cache_origin_tile_y = anchor.y - (self.cache_height_tiles as i32 / 2); let cache_origin_x_px = cache_origin_tile_x * tile_size_px; let cache_origin_y_px = cache_origin_tile_y * tile_size_px; LayerCopyRequest { layer_index, tile_size: layer.tile_size, viewport_width_px: self.viewport_width_px, viewport_height_px: self.viewport_height_px, source_offset_x_px: layer_camera_x_px - cache_origin_x_px, source_offset_y_px: layer_camera_y_px - cache_origin_y_px, cache_origin_tile_x, cache_origin_tile_y, camera_x_px: layer_camera_x_px, camera_y_px: layer_camera_y_px, } }); ResolverUpdate { master_anchor: self.master_anchor, layer_anchors: self.layer_anchors, refresh_requests, copy_requests, } } fn initial_anchor( &self, scene_width_tiles: usize, scene_height_tiles: usize, tile_size_px: i32, camera_center_x_px: i32, camera_center_y_px: i32, ) -> TileAnchor { let proposed = TileAnchor { x: camera_center_x_px.div_euclid(tile_size_px), y: camera_center_y_px.div_euclid(tile_size_px), }; self.clamp_anchor(proposed, scene_width_tiles, scene_height_tiles) } fn advance_anchor( &self, current: TileAnchor, camera_center_x_px: i32, camera_center_y_px: i32, tile_size_px: i32, scene_width_tiles: usize, scene_height_tiles: usize, ) -> TileAnchor { let mut next = current; loop { let center_x_px = next.x * tile_size_px + tile_size_px / 2; let drift_x = camera_center_x_px - center_x_px; if drift_x.abs() <= self.hysteresis_safe_px { break; } if drift_x > self.hysteresis_trigger_px { next.x += 1; continue; } if drift_x < -self.hysteresis_trigger_px { next.x -= 1; continue; } break; } loop { let center_y_px = next.y * tile_size_px + tile_size_px / 2; let drift_y = camera_center_y_px - center_y_px; if drift_y.abs() <= self.hysteresis_safe_px { break; } if drift_y > self.hysteresis_trigger_px { next.y += 1; continue; } if drift_y < -self.hysteresis_trigger_px { next.y -= 1; continue; } break; } self.clamp_anchor(next, scene_width_tiles, scene_height_tiles) } fn clamp_anchor( &self, proposed: TileAnchor, scene_width_tiles: usize, scene_height_tiles: usize, ) -> TileAnchor { TileAnchor { x: self.clamp_axis(proposed.x, scene_width_tiles, self.cache_width_tiles), y: self.clamp_axis(proposed.y, scene_height_tiles, self.cache_height_tiles), } } fn clamp_axis(&self, proposed: i32, scene_tiles: usize, cache_tiles: usize) -> i32 { let half = (cache_tiles / 2) as i32; if scene_tiles <= cache_tiles { return half; } let min = half; let max = (scene_tiles - cache_tiles) as i32 + half; proposed.clamp(min, max) } fn emit_refresh_requests( &self, layer_index: usize, previous: TileAnchor, next: TileAnchor, requests: &mut Vec, ) { let delta_x = next.x - previous.x; let delta_y = next.y - previous.y; if delta_x == 0 && delta_y == 0 { return; } if delta_x == 0 { self.emit_line_requests(layer_index, delta_y, requests); return; } if delta_y == 0 { self.emit_column_requests(layer_index, delta_x, requests); return; } self.emit_corner_region_requests(layer_index, delta_x, delta_y, requests); } fn emit_line_requests( &self, layer_index: usize, delta_y: i32, requests: &mut Vec, ) { let count = delta_y.unsigned_abs() as usize; if delta_y > 0 { for offset in 0..count { requests.push(CacheRefreshRequest::RefreshLine { layer_index, cache_y: self.cache_height_tiles - count + offset, }); } } else { for cache_y in 0..count { requests.push(CacheRefreshRequest::RefreshLine { layer_index, cache_y }); } } } fn emit_column_requests( &self, layer_index: usize, delta_x: i32, requests: &mut Vec, ) { let count = delta_x.unsigned_abs() as usize; if delta_x > 0 { for offset in 0..count { requests.push(CacheRefreshRequest::RefreshColumn { layer_index, cache_x: self.cache_width_tiles - count + offset, }); } } else { for cache_x in 0..count { requests.push(CacheRefreshRequest::RefreshColumn { layer_index, cache_x }); } } } fn emit_corner_region_requests( &self, layer_index: usize, delta_x: i32, delta_y: i32, requests: &mut Vec, ) { let width = delta_x.unsigned_abs() as usize; let height = delta_y.unsigned_abs() as usize; let primary_x = if delta_x > 0 { self.cache_width_tiles - width } else { 0 }; requests.push(CacheRefreshRequest::RefreshRegion { layer_index, region: ViewportRegion::new(primary_x, 0, width, self.cache_height_tiles), }); let secondary_y = if delta_y > 0 { self.cache_height_tiles - height } else { 0 }; let secondary_x = if delta_x > 0 { 0 } else { width }; let secondary_width = self.cache_width_tiles.saturating_sub(width); if secondary_width > 0 { requests.push(CacheRefreshRequest::RefreshRegion { layer_index, region: ViewportRegion::new(secondary_x, secondary_y, secondary_width, height), }); } } } #[cfg(test)] mod tests { use super::*; use crate::glyph::Glyph; use crate::glyph_bank::TileSize; use crate::scene_layer::{MotionFactor, SceneLayer}; use crate::tile::Tile; use crate::tilemap::TileMap; fn make_layer( tile_size: TileSize, motion_x: f32, motion_y: f32, width: usize, height: usize, ) -> SceneLayer { let mut tiles = Vec::new(); for i in 0..(width * height) { tiles.push(Tile { active: true, glyph: Glyph { glyph_id: i as u16, palette_id: 0 }, flip_x: false, flip_y: false, }); } SceneLayer { active: true, glyph_bank_id: 1, tile_size, motion_factor: MotionFactor { x: motion_x, y: motion_y }, tilemap: TileMap { width, height, tiles }, } } fn make_scene() -> SceneBank { SceneBank { layers: [ make_layer(TileSize::Size16, 1.0, 1.0, 64, 64), make_layer(TileSize::Size16, 0.5, 0.5, 64, 64), make_layer(TileSize::Size16, 1.0, 0.75, 64, 64), make_layer(TileSize::Size16, 0.25, 1.0, 64, 64), ], } } #[test] fn first_update_initializes_master_and_layer_anchors() { let scene = make_scene(); let mut resolver = SceneViewportResolver::new(320, 180, 25, 16, 12, 20); let update = resolver.update(&scene, 0, 0); assert_eq!(update.master_anchor, TileAnchor { x: 12, y: 8 }); assert_eq!(update.layer_anchors[0], TileAnchor { x: 12, y: 8 }); assert_eq!(update.layer_anchors[1], TileAnchor { x: 12, y: 8 }); assert_eq!(update.refresh_requests.len(), 4); } #[test] fn per_layer_copy_requests_follow_motion_factor() { let scene = make_scene(); let mut resolver = SceneViewportResolver::new(320, 180, 25, 16, 12, 20); let update = resolver.update(&scene, 32, 48); assert_eq!(update.copy_requests[0].camera_x_px, 32); assert_eq!(update.copy_requests[0].camera_y_px, 48); assert_eq!(update.copy_requests[1].camera_x_px, 16); assert_eq!(update.copy_requests[1].camera_y_px, 24); assert_eq!(update.copy_requests[3].camera_x_px, 8); assert_eq!(update.copy_requests[3].camera_y_px, 48); } #[test] fn hysteresis_prevents_small_back_and_forth_refresh_churn() { let scene = make_scene(); let mut resolver = SceneViewportResolver::new(320, 180, 25, 16, 12, 20); let _ = resolver.update(&scene, 0, 0); let update = resolver.update(&scene, 8, 0); assert!(update.refresh_requests.is_empty()); assert_eq!(update.master_anchor, TileAnchor { x: 12, y: 8 }); } #[test] fn repeated_high_speed_movement_advances_in_tile_steps() { let scene = make_scene(); let mut resolver = SceneViewportResolver::new(320, 180, 25, 16, 12, 20); let _ = resolver.update(&scene, 0, 0); let update = resolver.update(&scene, 64, 0); assert!(update.master_anchor.x > 12); assert!(update.refresh_requests.iter().any(|request| matches!( request, CacheRefreshRequest::RefreshColumn { layer_index: 0, .. } | CacheRefreshRequest::RefreshRegion { layer_index: 0, .. } ))); } #[test] fn anchors_clamp_near_scene_edges() { let scene = make_scene(); let mut resolver = SceneViewportResolver::new(320, 180, 25, 16, 12, 20); let update = resolver.update(&scene, 10_000, 10_000); assert_eq!(update.master_anchor, TileAnchor { x: 51, y: 56 }); assert_eq!(update.layer_anchors[0], TileAnchor { x: 51, y: 56 }); } #[test] fn corner_trigger_converts_to_non_overlapping_region_requests() { let scene = make_scene(); let mut resolver = SceneViewportResolver::new(320, 180, 25, 16, 12, 20); let _ = resolver.update(&scene, 0, 0); let update = resolver.update(&scene, 64, 80); let regions: Vec<_> = update .refresh_requests .iter() .filter_map(|request| match request { CacheRefreshRequest::RefreshRegion { layer_index: 0, region } => Some(*region), _ => None, }) .collect(); assert_eq!(regions.len(), 2); assert_eq!(regions[0].x + regions[0].width, 25); assert_eq!(regions[0].height, 16); assert_eq!(regions[1].width, 24); assert_eq!(regions[1].height, 1); } #[test] fn reset_scene_requests_full_invalidation_for_all_layers() { let mut resolver = SceneViewportResolver::new(320, 180, 25, 16, 12, 20); let requests = resolver.reset_scene(); assert_eq!(requests.len(), 4); assert!( requests .iter() .all(|request| matches!(request, CacheRefreshRequest::InvalidateLayer { .. })) ); } }