dev/new-scene-plt #15
@ -5,7 +5,7 @@ pub const GLYPH_BANK_PALETTE_COUNT_V1: usize = 64;
|
||||
pub const GLYPH_BANK_COLORS_PER_PALETTE: usize = 16;
|
||||
|
||||
/// Standard sizes for square tiles.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub enum TileSize {
|
||||
/// 8x8 pixels.
|
||||
Size8 = 8,
|
||||
|
||||
@ -21,6 +21,7 @@ pub mod sample;
|
||||
pub mod scene_bank;
|
||||
pub mod scene_layer;
|
||||
pub mod scene_viewport_cache;
|
||||
pub mod scene_viewport_resolver;
|
||||
pub mod sound_bank;
|
||||
pub mod sprite;
|
||||
pub mod syscalls;
|
||||
|
||||
536
crates/console/prometeu-hal/src/scene_viewport_resolver.rs
Normal file
536
crates/console/prometeu-hal/src/scene_viewport_resolver.rs
Normal file
@ -0,0 +1,536 @@
|
||||
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<CacheRefreshRequest>,
|
||||
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<CacheRefreshRequest> {
|
||||
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<CacheRefreshRequest>,
|
||||
) {
|
||||
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<CacheRefreshRequest>,
|
||||
) {
|
||||
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<CacheRefreshRequest>,
|
||||
) {
|
||||
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<CacheRefreshRequest>,
|
||||
) {
|
||||
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 { .. }))
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user