implements PLN-0012
This commit is contained in:
parent
0723057038
commit
d9e680082d
@ -20,6 +20,7 @@ pub mod pad_bridge;
|
|||||||
pub mod sample;
|
pub mod sample;
|
||||||
pub mod scene_bank;
|
pub mod scene_bank;
|
||||||
pub mod scene_layer;
|
pub mod scene_layer;
|
||||||
|
pub mod scene_viewport_cache;
|
||||||
pub mod sound_bank;
|
pub mod sound_bank;
|
||||||
pub mod sprite;
|
pub mod sprite;
|
||||||
pub mod syscalls;
|
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