dev/new-scene-plt #15

Merged
bquarkz merged 8 commits from dev/new-scene-plt into master 2026-04-14 04:26:50 +00:00
2 changed files with 431 additions and 0 deletions
Showing only changes of commit d9e680082d - Show all commits

View File

@ -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;

View 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);
}
}