566 lines
18 KiB
Rust
566 lines
18 KiB
Rust
use crate::glyph_bank::TileSize;
|
|
use crate::scene_bank::SceneBank;
|
|
use crate::scene_layer::SceneLayer;
|
|
use crate::tile::Tile;
|
|
|
|
const FLAG_FLIP_X: u8 = 0b0000_0001;
|
|
const FLAG_FLIP_Y: u8 = 0b0000_0010;
|
|
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct CachedTileEntry {
|
|
pub active: bool,
|
|
pub glyph_id: u16,
|
|
pub palette_id: u8,
|
|
pub flags: u8,
|
|
pub glyph_bank_id: u8,
|
|
}
|
|
|
|
impl CachedTileEntry {
|
|
pub fn flip_x(self) -> bool {
|
|
(self.flags & FLAG_FLIP_X) != 0
|
|
}
|
|
|
|
pub fn flip_y(self) -> bool {
|
|
(self.flags & FLAG_FLIP_Y) != 0
|
|
}
|
|
|
|
fn from_tile(layer: &SceneLayer, tile: Tile) -> Self {
|
|
let mut flags = 0_u8;
|
|
if tile.flip_x {
|
|
flags |= FLAG_FLIP_X;
|
|
}
|
|
if tile.flip_y {
|
|
flags |= FLAG_FLIP_Y;
|
|
}
|
|
|
|
Self {
|
|
active: tile.active,
|
|
glyph_id: tile.glyph.glyph_id,
|
|
palette_id: tile.glyph.palette_id,
|
|
flags,
|
|
glyph_bank_id: layer.glyph_bank_id,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub struct ViewportRegion {
|
|
pub x: usize,
|
|
pub y: usize,
|
|
pub width: usize,
|
|
pub height: usize,
|
|
}
|
|
|
|
impl ViewportRegion {
|
|
pub const fn new(x: usize, y: usize, width: usize, height: usize) -> Self {
|
|
Self { x, y, width, height }
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct SceneViewportLayerCache {
|
|
width: usize,
|
|
height: usize,
|
|
logical_origin_x: i32,
|
|
logical_origin_y: i32,
|
|
ring_origin_x: usize,
|
|
ring_origin_y: usize,
|
|
pub glyph_bank_id: u8,
|
|
pub tile_size: TileSize,
|
|
entries: Vec<CachedTileEntry>,
|
|
pub valid: bool,
|
|
}
|
|
|
|
impl SceneViewportLayerCache {
|
|
pub fn new(layer: &SceneLayer, width: usize, height: usize) -> Self {
|
|
Self {
|
|
width,
|
|
height,
|
|
logical_origin_x: 0,
|
|
logical_origin_y: 0,
|
|
ring_origin_x: 0,
|
|
ring_origin_y: 0,
|
|
glyph_bank_id: layer.glyph_bank_id,
|
|
tile_size: layer.tile_size,
|
|
entries: vec![CachedTileEntry::default(); width * height],
|
|
valid: false,
|
|
}
|
|
}
|
|
|
|
pub fn width(&self) -> usize {
|
|
self.width
|
|
}
|
|
|
|
pub fn height(&self) -> usize {
|
|
self.height
|
|
}
|
|
|
|
pub fn logical_origin(&self) -> (i32, i32) {
|
|
(self.logical_origin_x, self.logical_origin_y)
|
|
}
|
|
|
|
pub fn ring_origin(&self) -> (usize, usize) {
|
|
(self.ring_origin_x, self.ring_origin_y)
|
|
}
|
|
|
|
pub fn entry(&self, cache_x: usize, cache_y: usize) -> CachedTileEntry {
|
|
self.entries[self.physical_index(cache_x, cache_y)]
|
|
}
|
|
|
|
pub fn invalidate_all(&mut self) {
|
|
self.entries.fill(CachedTileEntry::default());
|
|
self.valid = false;
|
|
}
|
|
|
|
pub fn move_window_to(&mut self, origin_x: i32, origin_y: i32) {
|
|
let delta_x = origin_x - self.logical_origin_x;
|
|
let delta_y = origin_y - self.logical_origin_y;
|
|
|
|
self.logical_origin_x = origin_x;
|
|
self.logical_origin_y = origin_y;
|
|
|
|
self.ring_origin_x = Self::wrapped_origin(self.ring_origin_x, delta_x, self.width);
|
|
self.ring_origin_y = Self::wrapped_origin(self.ring_origin_y, delta_y, self.height);
|
|
}
|
|
|
|
pub fn move_window_by(&mut self, delta_x: i32, delta_y: i32) {
|
|
self.move_window_to(self.logical_origin_x + delta_x, self.logical_origin_y + delta_y);
|
|
}
|
|
|
|
pub fn refresh_line(&mut self, layer: &SceneLayer, cache_y: usize) {
|
|
self.refresh_region(layer, ViewportRegion::new(0, cache_y, self.width, 1));
|
|
}
|
|
|
|
pub fn refresh_column(&mut self, layer: &SceneLayer, cache_x: usize) {
|
|
self.refresh_region(layer, ViewportRegion::new(cache_x, 0, 1, self.height));
|
|
}
|
|
|
|
pub fn refresh_region(&mut self, layer: &SceneLayer, region: ViewportRegion) {
|
|
self.glyph_bank_id = layer.glyph_bank_id;
|
|
self.tile_size = layer.tile_size;
|
|
|
|
let max_x = region.x.saturating_add(region.width).min(self.width);
|
|
let max_y = region.y.saturating_add(region.height).min(self.height);
|
|
|
|
for cache_y in region.y..max_y {
|
|
for cache_x in region.x..max_x {
|
|
let entry = self.materialize_entry(layer, cache_x, cache_y);
|
|
let idx = self.physical_index(cache_x, cache_y);
|
|
self.entries[idx] = entry;
|
|
}
|
|
}
|
|
|
|
self.valid = true;
|
|
}
|
|
|
|
pub fn refresh_all(&mut self, layer: &SceneLayer) {
|
|
self.refresh_region(layer, ViewportRegion::new(0, 0, self.width, self.height));
|
|
}
|
|
|
|
fn materialize_entry(
|
|
&self,
|
|
layer: &SceneLayer,
|
|
cache_x: usize,
|
|
cache_y: usize,
|
|
) -> CachedTileEntry {
|
|
let scene_x = self.logical_origin_x + cache_x as i32;
|
|
let scene_y = self.logical_origin_y + cache_y as i32;
|
|
|
|
if scene_x < 0 || scene_y < 0 {
|
|
return CachedTileEntry::default();
|
|
}
|
|
|
|
let tile_x = scene_x as usize;
|
|
let tile_y = scene_y as usize;
|
|
if tile_x >= layer.tilemap.width || tile_y >= layer.tilemap.height {
|
|
return CachedTileEntry::default();
|
|
}
|
|
|
|
let tile = layer.tilemap.tiles[tile_y * layer.tilemap.width + tile_x];
|
|
CachedTileEntry::from_tile(layer, tile)
|
|
}
|
|
|
|
fn physical_index(&self, cache_x: usize, cache_y: usize) -> usize {
|
|
let physical_x = (self.ring_origin_x + cache_x) % self.width;
|
|
let physical_y = (self.ring_origin_y + cache_y) % self.height;
|
|
physical_y * self.width + physical_x
|
|
}
|
|
|
|
fn wrapped_origin(current: usize, delta: i32, span: usize) -> usize {
|
|
if span == 0 {
|
|
return 0;
|
|
}
|
|
|
|
let span_i32 = span as i32;
|
|
let current_i32 = current as i32;
|
|
(current_i32 + delta).rem_euclid(span_i32) as usize
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct SceneViewportCache {
|
|
width: usize,
|
|
height: usize,
|
|
pub layers: [SceneViewportLayerCache; 4],
|
|
}
|
|
|
|
impl SceneViewportCache {
|
|
pub fn new(scene: &SceneBank, width: usize, height: usize) -> Self {
|
|
Self {
|
|
width,
|
|
height,
|
|
layers: std::array::from_fn(|i| {
|
|
SceneViewportLayerCache::new(&scene.layers[i], width, height)
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn width(&self) -> usize {
|
|
self.width
|
|
}
|
|
|
|
pub fn height(&self) -> usize {
|
|
self.height
|
|
}
|
|
|
|
pub fn invalidate_all(&mut self) {
|
|
for layer in &mut self.layers {
|
|
layer.invalidate_all();
|
|
}
|
|
}
|
|
|
|
pub fn move_layer_window_to(&mut self, layer_idx: usize, origin_x: i32, origin_y: i32) {
|
|
self.layers[layer_idx].move_window_to(origin_x, origin_y);
|
|
}
|
|
|
|
pub fn move_layer_window_by(&mut self, layer_idx: usize, delta_x: i32, delta_y: i32) {
|
|
self.layers[layer_idx].move_window_by(delta_x, delta_y);
|
|
}
|
|
|
|
pub fn refresh_layer_line(&mut self, scene: &SceneBank, layer_idx: usize, cache_y: usize) {
|
|
self.layers[layer_idx].refresh_line(&scene.layers[layer_idx], cache_y);
|
|
}
|
|
|
|
pub fn refresh_layer_column(&mut self, scene: &SceneBank, layer_idx: usize, cache_x: usize) {
|
|
self.layers[layer_idx].refresh_column(&scene.layers[layer_idx], cache_x);
|
|
}
|
|
|
|
pub fn refresh_layer_region(
|
|
&mut self,
|
|
scene: &SceneBank,
|
|
layer_idx: usize,
|
|
region: ViewportRegion,
|
|
) {
|
|
self.layers[layer_idx].refresh_region(&scene.layers[layer_idx], region);
|
|
}
|
|
|
|
pub fn refresh_layer_all(&mut self, scene: &SceneBank, layer_idx: usize) {
|
|
self.layers[layer_idx].refresh_all(&scene.layers[layer_idx]);
|
|
}
|
|
|
|
pub fn materialize_all_layers(&mut self, scene: &SceneBank) {
|
|
for layer_idx in 0..self.layers.len() {
|
|
self.refresh_layer_all(scene, layer_idx);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::glyph::Glyph;
|
|
use crate::glyph_bank::TileSize;
|
|
use crate::scene_layer::ParallaxFactor;
|
|
use crate::tile::Tile;
|
|
use crate::tilemap::TileMap;
|
|
|
|
fn make_tile(glyph_id: u16, palette_id: u8, flip_x: bool, flip_y: bool) -> Tile {
|
|
Tile { active: true, glyph: Glyph { glyph_id, palette_id }, flip_x, flip_y }
|
|
}
|
|
|
|
fn make_layer(glyph_bank_id: u8, base_glyph: u16) -> SceneLayer {
|
|
let mut tiles = Vec::new();
|
|
for y in 0..4 {
|
|
for x in 0..4 {
|
|
tiles.push(make_tile(
|
|
base_glyph + (y * 4 + x) as u16,
|
|
glyph_bank_id,
|
|
x % 2 == 0,
|
|
y % 2 == 1,
|
|
));
|
|
}
|
|
}
|
|
|
|
SceneLayer {
|
|
active: true,
|
|
glyph_bank_id,
|
|
tile_size: TileSize::Size16,
|
|
parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 },
|
|
tilemap: TileMap { width: 4, height: 4, tiles },
|
|
}
|
|
}
|
|
|
|
fn make_scene() -> SceneBank {
|
|
SceneBank {
|
|
layers: [
|
|
make_layer(1, 100),
|
|
make_layer(2, 200),
|
|
make_layer(3, 300),
|
|
make_layer(4, 400),
|
|
],
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn layer_cache_wraps_ring_origin_under_window_movement() {
|
|
let scene = make_scene();
|
|
let mut cache = SceneViewportCache::new(&scene, 3, 3);
|
|
|
|
cache.move_layer_window_by(0, 1, 2);
|
|
assert_eq!(cache.layers[0].logical_origin(), (1, 2));
|
|
assert_eq!(cache.layers[0].ring_origin(), (1, 2));
|
|
|
|
cache.move_layer_window_by(0, 3, 2);
|
|
assert_eq!(cache.layers[0].logical_origin(), (4, 4));
|
|
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();
|
|
let mut cache = SceneViewportCache::new(&scene, 2, 2);
|
|
|
|
cache.refresh_layer_all(&scene, 0);
|
|
let entry = cache.layers[0].entry(1, 1);
|
|
|
|
assert!(entry.active);
|
|
assert_eq!(entry.glyph_id, 105);
|
|
assert_eq!(entry.palette_id, 1);
|
|
assert_eq!(entry.glyph_bank_id, 1);
|
|
assert!(!entry.flip_x());
|
|
assert!(entry.flip_y());
|
|
}
|
|
|
|
#[test]
|
|
fn line_refresh_only_updates_the_requested_line() {
|
|
let scene = make_scene();
|
|
let mut cache = SceneViewportCache::new(&scene, 3, 3);
|
|
|
|
cache.refresh_layer_line(&scene, 0, 1);
|
|
|
|
assert_eq!(cache.layers[0].entry(0, 0), CachedTileEntry::default());
|
|
assert_eq!(cache.layers[0].entry(0, 1).glyph_id, 104);
|
|
assert_eq!(cache.layers[0].entry(2, 1).glyph_id, 106);
|
|
assert_eq!(cache.layers[0].entry(0, 2), CachedTileEntry::default());
|
|
}
|
|
|
|
#[test]
|
|
fn column_refresh_only_updates_the_requested_column() {
|
|
let scene = make_scene();
|
|
let mut cache = SceneViewportCache::new(&scene, 3, 3);
|
|
|
|
cache.refresh_layer_column(&scene, 1, 2);
|
|
|
|
assert_eq!(cache.layers[1].entry(0, 0), CachedTileEntry::default());
|
|
assert_eq!(cache.layers[1].entry(2, 0).glyph_id, 202);
|
|
assert_eq!(cache.layers[1].entry(2, 2).glyph_id, 210);
|
|
assert_eq!(cache.layers[1].entry(1, 2), CachedTileEntry::default());
|
|
}
|
|
|
|
#[test]
|
|
fn region_refresh_only_updates_the_requested_area() {
|
|
let scene = make_scene();
|
|
let mut cache = SceneViewportCache::new(&scene, 3, 3);
|
|
|
|
cache.refresh_layer_region(&scene, 2, ViewportRegion::new(1, 1, 2, 2));
|
|
|
|
assert_eq!(cache.layers[2].entry(0, 0), CachedTileEntry::default());
|
|
assert_eq!(cache.layers[2].entry(1, 1).glyph_id, 305);
|
|
assert_eq!(cache.layers[2].entry(2, 2).glyph_id, 310);
|
|
assert_eq!(cache.layers[2].entry(0, 2), CachedTileEntry::default());
|
|
}
|
|
|
|
#[test]
|
|
fn scene_swap_invalidation_clears_all_layers() {
|
|
let scene = make_scene();
|
|
let mut cache = SceneViewportCache::new(&scene, 2, 2);
|
|
cache.materialize_all_layers(&scene);
|
|
|
|
cache.invalidate_all();
|
|
|
|
for layer in &cache.layers {
|
|
assert!(!layer.valid);
|
|
for y in 0..cache.height() {
|
|
for x in 0..cache.width() {
|
|
assert_eq!(layer.entry(x, y), CachedTileEntry::default());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn corner_style_region_update_does_not_touch_outside_tiles() {
|
|
let scene = make_scene();
|
|
let mut cache = SceneViewportCache::new(&scene, 4, 4);
|
|
cache.materialize_all_layers(&scene);
|
|
|
|
let before = cache.layers[3].entry(1, 1);
|
|
cache.layers[3].invalidate_all();
|
|
cache.refresh_layer_region(&scene, 3, ViewportRegion::new(2, 2, 2, 2));
|
|
|
|
assert_eq!(cache.layers[3].entry(0, 0), CachedTileEntry::default());
|
|
assert_eq!(cache.layers[3].entry(1, 1), CachedTileEntry::default());
|
|
assert_ne!(cache.layers[3].entry(2, 2), CachedTileEntry::default());
|
|
assert_eq!(before.glyph_id, 405);
|
|
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();
|
|
let mut cache = SceneViewportCache::new(&scene, 2, 2);
|
|
|
|
cache.materialize_all_layers(&scene);
|
|
|
|
assert_eq!(cache.layers[0].entry(0, 0).glyph_id, 100);
|
|
assert_eq!(cache.layers[1].entry(0, 0).glyph_id, 200);
|
|
assert_eq!(cache.layers[2].entry(1, 1).glyph_id, 305);
|
|
assert_eq!(cache.layers[3].entry(1, 0).glyph_id, 401);
|
|
}
|
|
}
|