implements PLN-0017

This commit is contained in:
bQUARKz 2026-04-17 13:19:03 +01:00
parent 94c80e61ba
commit ed05f337ce
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
9 changed files with 261 additions and 38 deletions

View File

@ -14,7 +14,7 @@ use prometeu_hal::glyph::Glyph;
use prometeu_hal::glyph_bank::{GlyphBank, TileSize};
use prometeu_hal::sample::Sample;
use prometeu_hal::scene_bank::SceneBank;
use prometeu_hal::scene_layer::{MotionFactor, SceneLayer};
use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
use prometeu_hal::sound_bank::SoundBank;
use prometeu_hal::tile::Tile;
use prometeu_hal::tilemap::TileMap;
@ -748,7 +748,7 @@ impl AssetManager {
active: false,
glyph_bank_id: 0,
tile_size: TileSize::Size8,
motion_factor: MotionFactor { x: 1.0, y: 1.0 },
parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 },
tilemap: TileMap { width: 0, height: 0, tiles: Vec::new() },
});
let mut layers = layers;
@ -770,20 +770,20 @@ impl AssetManager {
32 => TileSize::Size32,
other => return Err(format!("Invalid SCENE tile size: {}", other)),
};
let motion_factor_x = f32::from_le_bytes([
let parallax_factor_x = f32::from_le_bytes([
buffer[offset + 4],
buffer[offset + 5],
buffer[offset + 6],
buffer[offset + 7],
]);
let motion_factor_y = f32::from_le_bytes([
let parallax_factor_y = f32::from_le_bytes([
buffer[offset + 8],
buffer[offset + 9],
buffer[offset + 10],
buffer[offset + 11],
]);
if !motion_factor_x.is_finite() || !motion_factor_y.is_finite() {
return Err("Invalid SCENE motion_factor".to_string());
if !parallax_factor_x.is_finite() || !parallax_factor_y.is_finite() {
return Err("Invalid SCENE parallax_factor".to_string());
}
let width = u32::from_le_bytes([
@ -847,7 +847,7 @@ impl AssetManager {
active: (flags & 0b0000_0001) != 0,
glyph_bank_id,
tile_size,
motion_factor: MotionFactor { x: motion_factor_x, y: motion_factor_y },
parallax_factor: ParallaxFactor { x: parallax_factor_x, y: parallax_factor_y },
tilemap: TileMap { width, height, tiles },
};
}
@ -1105,7 +1105,7 @@ mod tests {
SCENE_PAYLOAD_MAGIC_V1, SCENE_PAYLOAD_VERSION_V1,
};
use prometeu_hal::glyph::Glyph;
use prometeu_hal::scene_layer::{MotionFactor, SceneLayer};
use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
use prometeu_hal::tile::Tile;
use prometeu_hal::tilemap::TileMap;
@ -1144,11 +1144,11 @@ mod tests {
fn test_scene() -> SceneBank {
let make_layer =
|glyph_bank_id: u8, motion_x: f32, motion_y: f32, tile_size: TileSize| SceneLayer {
|glyph_bank_id: u8, parallax_x: f32, parallax_y: f32, tile_size: TileSize| SceneLayer {
active: glyph_bank_id != 3,
glyph_bank_id,
tile_size,
motion_factor: MotionFactor { x: motion_x, y: motion_y },
parallax_factor: ParallaxFactor { x: parallax_x, y: parallax_y },
tilemap: TileMap {
width: 2,
height: 2,
@ -1227,8 +1227,8 @@ mod tests {
data.push(layer.glyph_bank_id);
data.push(layer.tile_size as u8);
data.push(0);
data.extend_from_slice(&layer.motion_factor.x.to_le_bytes());
data.extend_from_slice(&layer.motion_factor.y.to_le_bytes());
data.extend_from_slice(&layer.parallax_factor.x.to_le_bytes());
data.extend_from_slice(&layer.parallax_factor.y.to_le_bytes());
data.extend_from_slice(&(layer.tilemap.width as u32).to_le_bytes());
data.extend_from_slice(&(layer.tilemap.height as u32).to_le_bytes());
data.extend_from_slice(&(layer.tilemap.tiles.len() as u32).to_le_bytes());
@ -1359,7 +1359,7 @@ mod tests {
let decoded = AssetManager::decode_scene_bank_from_buffer(&entry, &data).expect("scene");
assert_eq!(decoded.layers[1].glyph_bank_id, 1);
assert_eq!(decoded.layers[1].motion_factor.x, 0.5);
assert_eq!(decoded.layers[1].parallax_factor.x, 0.5);
assert_eq!(decoded.layers[2].tile_size, TileSize::Size32);
assert_eq!(decoded.layers[0].tilemap.tiles[1].flip_x, true);
assert_eq!(decoded.layers[2].tilemap.tiles[2].flip_y, true);

View File

@ -0,0 +1,197 @@
use crate::memory_banks::SceneBankPoolAccess;
use prometeu_hal::glyph::Glyph;
use prometeu_hal::scene_bank::SceneBank;
use prometeu_hal::scene_viewport_cache::SceneViewportCache;
use prometeu_hal::scene_viewport_resolver::SceneViewportResolver;
use prometeu_hal::sprite::Sprite;
use std::sync::Arc;
const EMPTY_SPRITE: Sprite = Sprite {
glyph: Glyph { glyph_id: 0, palette_id: 0 },
x: 0,
y: 0,
bank_id: 0,
active: false,
flip_x: false,
flip_y: false,
priority: 0,
};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SceneStatus {
#[default]
Unbound,
}
#[derive(Clone, Debug)]
pub struct SpriteController {
sprites: [Sprite; 512],
}
impl Default for SpriteController {
fn default() -> Self {
Self::new()
}
}
impl SpriteController {
pub fn new() -> Self {
Self { sprites: [EMPTY_SPRITE; 512] }
}
pub fn sprites(&self) -> &[Sprite; 512] {
&self.sprites
}
pub fn sprites_mut(&mut self) -> &mut [Sprite; 512] {
&mut self.sprites
}
}
pub struct FrameComposer {
scene_bank_pool: Arc<dyn SceneBankPoolAccess>,
viewport_width_px: usize,
viewport_height_px: usize,
active_scene_id: Option<usize>,
active_scene: Option<Arc<SceneBank>>,
scene_status: SceneStatus,
camera_x_px: i32,
camera_y_px: i32,
cache: Option<SceneViewportCache>,
resolver: Option<SceneViewportResolver>,
sprite_controller: SpriteController,
}
impl FrameComposer {
pub fn new(
viewport_width_px: usize,
viewport_height_px: usize,
scene_bank_pool: Arc<dyn SceneBankPoolAccess>,
) -> Self {
Self {
scene_bank_pool,
viewport_width_px,
viewport_height_px,
active_scene_id: None,
active_scene: None,
scene_status: SceneStatus::Unbound,
camera_x_px: 0,
camera_y_px: 0,
cache: None,
resolver: None,
sprite_controller: SpriteController::new(),
}
}
pub fn viewport_size(&self) -> (usize, usize) {
(self.viewport_width_px, self.viewport_height_px)
}
pub fn scene_bank_pool(&self) -> &Arc<dyn SceneBankPoolAccess> {
&self.scene_bank_pool
}
pub fn scene_bank_slot(&self, slot: usize) -> Option<Arc<SceneBank>> {
self.scene_bank_pool.scene_bank_slot(slot)
}
pub fn scene_bank_slot_count(&self) -> usize {
self.scene_bank_pool.scene_bank_slot_count()
}
pub fn active_scene_id(&self) -> Option<usize> {
self.active_scene_id
}
pub fn active_scene(&self) -> Option<&Arc<SceneBank>> {
self.active_scene.as_ref()
}
pub fn scene_status(&self) -> SceneStatus {
self.scene_status
}
pub fn camera(&self) -> (i32, i32) {
(self.camera_x_px, self.camera_y_px)
}
pub fn cache(&self) -> Option<&SceneViewportCache> {
self.cache.as_ref()
}
pub fn resolver(&self) -> Option<&SceneViewportResolver> {
self.resolver.as_ref()
}
pub fn sprite_controller(&self) -> &SpriteController {
&self.sprite_controller
}
pub fn sprite_controller_mut(&mut self) -> &mut SpriteController {
&mut self.sprite_controller
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::memory_banks::{MemoryBanks, SceneBankPoolInstaller};
use prometeu_hal::glyph::Glyph;
use prometeu_hal::glyph_bank::TileSize;
use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
use prometeu_hal::tile::Tile;
use prometeu_hal::tilemap::TileMap;
fn make_scene() -> SceneBank {
let layer = SceneLayer {
active: true,
glyph_bank_id: 1,
tile_size: TileSize::Size8,
parallax_factor: ParallaxFactor { x: 1.0, y: 0.5 },
tilemap: TileMap {
width: 2,
height: 2,
tiles: vec![
Tile {
active: true,
glyph: Glyph { glyph_id: 9, palette_id: 1 },
flip_x: false,
flip_y: false,
};
4
],
},
};
SceneBank { layers: std::array::from_fn(|_| layer.clone()) }
}
#[test]
fn frame_composer_starts_unbound_with_empty_owned_state() {
let frame_composer = FrameComposer::new(320, 180, Arc::new(MemoryBanks::new()));
assert_eq!(frame_composer.viewport_size(), (320, 180));
assert_eq!(frame_composer.active_scene_id(), None);
assert!(frame_composer.active_scene().is_none());
assert_eq!(frame_composer.scene_status(), SceneStatus::Unbound);
assert_eq!(frame_composer.camera(), (0, 0));
assert!(frame_composer.cache().is_none());
assert!(frame_composer.resolver().is_none());
assert_eq!(frame_composer.sprite_controller().sprites().len(), 512);
assert!(frame_composer.sprite_controller().sprites().iter().all(|sprite| !sprite.active));
}
#[test]
fn frame_composer_exposes_shared_scene_bank_access() {
let banks = Arc::new(MemoryBanks::new());
banks.install_scene_bank(3, Arc::new(make_scene()));
let frame_composer = FrameComposer::new(320, 180, banks);
let scene =
frame_composer.scene_bank_slot(3).expect("scene bank slot 3 should be resident");
assert_eq!(frame_composer.scene_bank_slot_count(), 16);
assert_eq!(scene.layers[0].tile_size, TileSize::Size8);
assert_eq!(scene.layers[0].parallax_factor.y, 0.5);
}
}

View File

@ -822,7 +822,7 @@ mod tests {
use crate::memory_banks::{GlyphBankPoolInstaller, MemoryBanks};
use prometeu_hal::glyph_bank::TileSize;
use prometeu_hal::scene_bank::SceneBank;
use prometeu_hal::scene_layer::{MotionFactor, SceneLayer};
use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
use prometeu_hal::scene_viewport_cache::SceneViewportCache;
use prometeu_hal::scene_viewport_resolver::SceneViewportResolver;
use prometeu_hal::tile::Tile;
@ -853,7 +853,7 @@ mod tests {
active: true,
glyph_bank_id,
tile_size: TileSize::Size8,
motion_factor: MotionFactor { x: 1.0, y: 1.0 },
parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 },
tilemap: TileMap {
width,
height,
@ -875,7 +875,7 @@ mod tests {
active: false,
glyph_bank_id,
tile_size: TileSize::Size8,
motion_factor: MotionFactor { x: 1.0, y: 1.0 },
parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 },
tilemap: TileMap { width, height, tiles: vec![Tile::default(); width * height] },
}
}

View File

@ -1,9 +1,10 @@
use crate::asset::AssetManager;
use crate::audio::Audio;
use crate::frame_composer::FrameComposer;
use crate::gfx::Gfx;
use crate::memory_banks::{
GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolInstaller,
SoundBankPoolAccess, SoundBankPoolInstaller,
GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolAccess,
SceneBankPoolInstaller, SoundBankPoolAccess, SoundBankPoolInstaller,
};
use crate::pad::Pad;
use crate::touch::Touch;
@ -26,6 +27,8 @@ use std::sync::Arc;
pub struct Hardware {
/// The Graphics Processing Unit (GPU). Handles drawing primitives, sprites, and tilemaps.
pub gfx: Gfx,
/// Canonical frame orchestration owner for scene/camera/cache/resolver/sprites.
pub frame_composer: FrameComposer,
/// The Sound Processing Unit (SPU). Manages sample playback and volume control.
pub audio: Audio,
/// The standard digital gamepad. Provides state for D-Pad, face buttons, and triggers.
@ -98,6 +101,11 @@ impl Hardware {
Self::H,
Arc::clone(&memory_banks) as Arc<dyn GlyphBankPoolAccess>,
),
frame_composer: FrameComposer::new(
Self::W,
Self::H,
Arc::clone(&memory_banks) as Arc<dyn SceneBankPoolAccess>,
),
audio: Audio::new(Arc::clone(&memory_banks) as Arc<dyn SoundBankPoolAccess>),
pad: Pad::default(),
touch: Touch::default(),
@ -122,7 +130,7 @@ mod tests {
use prometeu_hal::glyph::Glyph;
use prometeu_hal::glyph_bank::{GlyphBank, TileSize};
use prometeu_hal::scene_bank::SceneBank;
use prometeu_hal::scene_layer::{MotionFactor, SceneLayer};
use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
use prometeu_hal::scene_viewport_cache::SceneViewportCache;
use prometeu_hal::scene_viewport_resolver::SceneViewportResolver;
use prometeu_hal::tile::Tile;
@ -142,7 +150,7 @@ mod tests {
active: true,
glyph_bank_id: 0,
tile_size: TileSize::Size8,
motion_factor: MotionFactor { x: 1.0, y: 1.0 },
parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 },
tilemap: TileMap {
width: 4,
height: 4,
@ -182,4 +190,20 @@ mod tests {
assert_eq!(hardware.gfx.front_buffer()[0], Color::RED.raw());
}
#[test]
fn hardware_constructs_frame_composer_with_shared_scene_bank_access() {
let banks = Arc::new(MemoryBanks::new());
banks.install_scene_bank(2, Arc::new(make_scene()));
let hardware = Hardware::new_with_memory_banks(banks);
let scene = hardware
.frame_composer
.scene_bank_slot(2)
.expect("scene bank slot 2 should be resident");
assert_eq!(hardware.frame_composer.viewport_size(), (Hardware::W, Hardware::H));
assert_eq!(hardware.frame_composer.scene_bank_slot_count(), 16);
assert_eq!(scene.layers[0].tile_size, TileSize::Size8);
}
}

View File

@ -1,5 +1,6 @@
mod asset;
mod audio;
mod frame_composer;
mod gfx;
pub mod hardware;
mod memory_banks;
@ -8,6 +9,7 @@ mod touch;
pub use crate::asset::AssetManager;
pub use crate::audio::{Audio, AudioCommand, Channel, MAX_CHANNELS, OUTPUT_SAMPLE_RATE};
pub use crate::frame_composer::{FrameComposer, SceneStatus, SpriteController};
pub use crate::gfx::Gfx;
pub use crate::memory_banks::{
GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolAccess,

View File

@ -10,16 +10,16 @@ mod tests {
use super::*;
use crate::glyph::Glyph;
use crate::glyph_bank::TileSize;
use crate::scene_layer::MotionFactor;
use crate::scene_layer::ParallaxFactor;
use crate::tile::Tile;
use crate::tilemap::TileMap;
fn layer(glyph_bank_id: u8, motion_x: f32, motion_y: f32, glyph_id: u16) -> SceneLayer {
fn layer(glyph_bank_id: u8, parallax_x: f32, parallax_y: f32, glyph_id: u16) -> SceneLayer {
SceneLayer {
active: true,
glyph_bank_id,
tile_size: TileSize::Size16,
motion_factor: MotionFactor { x: motion_x, y: motion_y },
parallax_factor: ParallaxFactor { x: parallax_x, y: parallax_y },
tilemap: TileMap {
width: 1,
height: 1,

View File

@ -2,7 +2,7 @@ use crate::glyph_bank::TileSize;
use crate::tilemap::TileMap;
#[derive(Clone, Copy, Debug)]
pub struct MotionFactor {
pub struct ParallaxFactor {
pub x: f32,
pub y: f32,
}
@ -12,7 +12,7 @@ pub struct SceneLayer {
pub active: bool,
pub glyph_bank_id: u8,
pub tile_size: TileSize,
pub motion_factor: MotionFactor,
pub parallax_factor: ParallaxFactor,
pub tilemap: TileMap,
}
@ -23,12 +23,12 @@ mod tests {
use crate::tile::Tile;
#[test]
fn scene_layer_preserves_motion_factor_and_tilemap_ownership() {
fn scene_layer_preserves_parallax_factor_and_tilemap_ownership() {
let layer = SceneLayer {
active: true,
glyph_bank_id: 7,
tile_size: TileSize::Size16,
motion_factor: MotionFactor { x: 0.5, y: 0.75 },
parallax_factor: ParallaxFactor { x: 0.5, y: 0.75 },
tilemap: TileMap {
width: 2,
height: 1,
@ -50,8 +50,8 @@ mod tests {
};
assert_eq!(layer.glyph_bank_id, 7);
assert_eq!(layer.motion_factor.x, 0.5);
assert_eq!(layer.motion_factor.y, 0.75);
assert_eq!(layer.parallax_factor.x, 0.5);
assert_eq!(layer.parallax_factor.y, 0.75);
assert_eq!(layer.tilemap.width, 2);
assert_eq!(layer.tilemap.tiles[1].glyph.glyph_id, 22);
assert!(layer.tilemap.tiles[1].flip_x);

View File

@ -270,7 +270,7 @@ mod tests {
use super::*;
use crate::glyph::Glyph;
use crate::glyph_bank::TileSize;
use crate::scene_layer::MotionFactor;
use crate::scene_layer::ParallaxFactor;
use crate::tile::Tile;
use crate::tilemap::TileMap;
@ -295,7 +295,7 @@ mod tests {
active: true,
glyph_bank_id,
tile_size: TileSize::Size16,
motion_factor: MotionFactor { x: 1.0, y: 1.0 },
parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 },
tilemap: TileMap { width: 4, height: 4, tiles },
}
}

View File

@ -96,8 +96,8 @@ impl SceneViewportResolver {
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_camera_x_px = ((camera_x_px as f32) * layer.parallax_factor.x).floor() as i32;
let layer_camera_y_px = ((camera_y_px as f32) * layer.parallax_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;
(
@ -388,14 +388,14 @@ mod tests {
use super::*;
use crate::glyph::Glyph;
use crate::glyph_bank::TileSize;
use crate::scene_layer::{MotionFactor, SceneLayer};
use crate::scene_layer::{ParallaxFactor, SceneLayer};
use crate::tile::Tile;
use crate::tilemap::TileMap;
fn make_layer(
tile_size: TileSize,
motion_x: f32,
motion_y: f32,
parallax_x: f32,
parallax_y: f32,
width: usize,
height: usize,
) -> SceneLayer {
@ -413,7 +413,7 @@ mod tests {
active: true,
glyph_bank_id: 1,
tile_size,
motion_factor: MotionFactor { x: motion_x, y: motion_y },
parallax_factor: ParallaxFactor { x: parallax_x, y: parallax_y },
tilemap: TileMap { width, height, tiles },
}
}
@ -443,7 +443,7 @@ mod tests {
}
#[test]
fn per_layer_copy_requests_follow_motion_factor() {
fn per_layer_copy_requests_follow_parallax_factor() {
let scene = make_scene();
let mut resolver = SceneViewportResolver::new(320, 180, 25, 16, 12, 20);