dev/new-scene-plt #15
@ -20,6 +20,7 @@ pub mod pad_bridge;
|
||||
pub mod sample;
|
||||
pub mod scene_bank;
|
||||
pub mod scene_layer;
|
||||
pub mod scene_viewport_cache;
|
||||
pub mod sound_bank;
|
||||
pub mod sprite;
|
||||
pub mod syscalls;
|
||||
|
||||
430
crates/console/prometeu-hal/src/scene_viewport_cache.rs
Normal file
430
crates/console/prometeu-hal/src/scene_viewport_cache.rs
Normal file
@ -0,0 +1,430 @@
|
||||
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::MotionFactor;
|
||||
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,
|
||||
motion_factor: MotionFactor { 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 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 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);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user