From d5ec6e77b964f807e0967489bd8c458acd45d1c0 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 13 Apr 2026 20:37:50 +0100 Subject: [PATCH] implements PLN-0016 --- crates/console/prometeu-drivers/src/asset.rs | 471 ++++++++++++++++++- crates/console/prometeu-hal/src/asset.rs | 22 + 2 files changed, 475 insertions(+), 18 deletions(-) diff --git a/crates/console/prometeu-drivers/src/asset.rs b/crates/console/prometeu-drivers/src/asset.rs index 07a33e3c..d6541337 100644 --- a/crates/console/prometeu-drivers/src/asset.rs +++ b/crates/console/prometeu-drivers/src/asset.rs @@ -3,14 +3,21 @@ use crate::memory_banks::{GlyphBankPoolInstaller, SceneBankPoolInstaller, SoundB use prometeu_hal::AssetBridge; use prometeu_hal::asset::{ AssetCodec, AssetEntry, AssetId, AssetLoadError, AssetOpStatus, BankTelemetry, BankType, - HandleId, LoadStatus, PreloadEntry, SlotRef, SlotStats, + HandleId, LoadStatus, PreloadEntry, SCENE_DECODED_LAYER_OVERHEAD_BYTES_V1, + SCENE_HEADER_BYTES_V1, SCENE_LAYER_COUNT_V1, SCENE_LAYER_HEADER_BYTES_V1, + SCENE_PAYLOAD_MAGIC_V1, SCENE_PAYLOAD_VERSION_V1, SCENE_TILE_RECORD_BYTES_V1, SlotRef, + SlotStats, }; use prometeu_hal::cartridge::AssetsPayloadSource; use prometeu_hal::color::Color; +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::sound_bank::SoundBank; +use prometeu_hal::tile::Tile; +use prometeu_hal::tilemap::TileMap; use std::collections::HashMap; use std::io::Read; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -688,11 +695,184 @@ impl AssetManager { entry: &AssetEntry, assets_data: Arc>, ) -> Result { - let _ = (entry, assets_data); - // TODO: SCENE payloads must not be decoded from ad-hoc JSON. - // Close and document the binary scene-bank format first, then implement - // the runtime decoder here using the canonical binary contract. - todo!("SCENE binary payload format is still open") + let _ = entry.metadata_as_scene_bank()?; + let op_mode = Self::op_mode_for(entry)?; + let slice = { + let assets_data = assets_data.read().unwrap(); + assets_data + .open_slice(entry.offset, entry.size) + .map_err(|_| "Asset offset/size out of bounds".to_string())? + }; + + match op_mode { + AssetOpMode::DirectFromSlice => { + let mut reader = + slice.open_reader().map_err(|_| "Asset payload read failed".to_string())?; + Self::decode_scene_bank_from_reader(entry, &mut reader) + } + AssetOpMode::StageInMemory => { + let buffer = + slice.read_all().map_err(|_| "Asset payload read failed".to_string())?; + Self::decode_scene_bank_from_buffer(entry, &buffer) + } + } + } + + fn decode_scene_bank_from_buffer( + entry: &AssetEntry, + buffer: &[u8], + ) -> Result { + let _ = entry.metadata_as_scene_bank()?; + + if buffer.len() < SCENE_HEADER_BYTES_V1 { + return Err("Buffer too small for SCENE".to_string()); + } + + if buffer[0..4] != SCENE_PAYLOAD_MAGIC_V1 { + return Err("Invalid SCENE magic".to_string()); + } + + let version = u16::from_le_bytes([buffer[4], buffer[5]]); + if version != SCENE_PAYLOAD_VERSION_V1 { + return Err(format!("Unsupported SCENE version: {}", version)); + } + + let layer_count = u16::from_le_bytes([buffer[6], buffer[7]]) as usize; + if layer_count != SCENE_LAYER_COUNT_V1 { + return Err(format!("Invalid SCENE layer count: {}", layer_count)); + } + + let mut offset = SCENE_HEADER_BYTES_V1; + let mut decoded_size = 0_usize; + let layers = std::array::from_fn(|_| SceneLayer { + active: false, + glyph_bank_id: 0, + tile_size: TileSize::Size8, + motion_factor: MotionFactor { x: 1.0, y: 1.0 }, + tilemap: TileMap { width: 0, height: 0, tiles: Vec::new() }, + }); + let mut layers = layers; + + for layer in &mut layers { + let header_end = offset + .checked_add(SCENE_LAYER_HEADER_BYTES_V1) + .ok_or("SCENE layer header offset overflow")?; + if header_end > buffer.len() { + return Err("Buffer too small for SCENE layer header".to_string()); + } + + let flags = buffer[offset]; + let glyph_bank_id = buffer[offset + 1]; + let tile_size_raw = buffer[offset + 2]; + let tile_size = match tile_size_raw { + 8 => TileSize::Size8, + 16 => TileSize::Size16, + 32 => TileSize::Size32, + other => return Err(format!("Invalid SCENE tile size: {}", other)), + }; + let motion_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([ + 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()); + } + + let width = u32::from_le_bytes([ + buffer[offset + 12], + buffer[offset + 13], + buffer[offset + 14], + buffer[offset + 15], + ]) as usize; + let height = u32::from_le_bytes([ + buffer[offset + 16], + buffer[offset + 17], + buffer[offset + 18], + buffer[offset + 19], + ]) as usize; + let tile_count = u32::from_le_bytes([ + buffer[offset + 20], + buffer[offset + 21], + buffer[offset + 22], + buffer[offset + 23], + ]) as usize; + + let expected_tile_count = + width.checked_mul(height).ok_or("SCENE tile count overflow")?; + if tile_count != expected_tile_count { + return Err(format!( + "Invalid SCENE tile count for layer: expected {}, got {}", + expected_tile_count, tile_count + )); + } + + offset = header_end; + + let tile_bytes = tile_count + .checked_mul(SCENE_TILE_RECORD_BYTES_V1) + .ok_or("SCENE tile payload overflow")?; + let tiles_end = offset.checked_add(tile_bytes).ok_or("SCENE payload overflow")?; + if tiles_end > buffer.len() { + return Err("Buffer too small for SCENE tile data".to_string()); + } + + let mut tiles = Vec::with_capacity(tile_count); + for _ in 0..tile_count { + let tile_flags = buffer[offset]; + let palette_id = buffer[offset + 1]; + let glyph_id = u16::from_le_bytes([buffer[offset + 2], buffer[offset + 3]]); + tiles.push(Tile { + active: (tile_flags & 0b0000_0001) != 0, + glyph: Glyph { glyph_id, palette_id }, + flip_x: (tile_flags & 0b0000_0010) != 0, + flip_y: (tile_flags & 0b0000_0100) != 0, + }); + offset += SCENE_TILE_RECORD_BYTES_V1; + } + + decoded_size = decoded_size + .checked_add(SCENE_DECODED_LAYER_OVERHEAD_BYTES_V1) + .and_then(|size| size.checked_add(tile_count * size_of::())) + .ok_or("SCENE decoded_size overflow")?; + + *layer = SceneLayer { + active: (flags & 0b0000_0001) != 0, + glyph_bank_id, + tile_size, + motion_factor: MotionFactor { x: motion_factor_x, y: motion_factor_y }, + tilemap: TileMap { width, height, tiles }, + }; + } + + if offset != buffer.len() { + return Err("Trailing bytes in SCENE payload".to_string()); + } + + if entry.decoded_size != decoded_size as u64 { + return Err(format!( + "Invalid SCENE decoded_size: expected {}, got {}", + decoded_size, entry.decoded_size + )); + } + + Ok(SceneBank { layers }) + } + + fn decode_scene_bank_from_reader( + entry: &AssetEntry, + reader: &mut impl Read, + ) -> Result { + let mut raw = Vec::new(); + reader.read_to_end(&mut raw).map_err(|_| "Asset payload read failed".to_string())?; + Self::decode_scene_bank_from_buffer(entry, &raw) } fn decode_sound_bank_from_buffer( @@ -917,8 +1097,17 @@ impl AssetManager { #[cfg(test)] mod tests { use super::*; - use crate::memory_banks::{GlyphBankPoolAccess, MemoryBanks, SoundBankPoolAccess}; - use prometeu_hal::asset::AssetCodec; + use crate::memory_banks::{ + GlyphBankPoolAccess, MemoryBanks, SceneBankPoolAccess, SoundBankPoolAccess, + }; + use prometeu_hal::asset::{ + AssetCodec, SCENE_DECODED_LAYER_OVERHEAD_BYTES_V1, SCENE_LAYER_COUNT_V1, + SCENE_PAYLOAD_MAGIC_V1, SCENE_PAYLOAD_VERSION_V1, + }; + use prometeu_hal::glyph::Glyph; + use prometeu_hal::scene_layer::{MotionFactor, SceneLayer}; + use prometeu_hal::tile::Tile; + use prometeu_hal::tilemap::TileMap; fn expected_glyph_payload_size(width: usize, height: usize) -> usize { (width * height).div_ceil(2) + GLYPH_BANK_PALETTE_BYTES_V1 @@ -953,6 +1142,131 @@ mod tests { } } + fn test_scene() -> SceneBank { + let make_layer = + |glyph_bank_id: u8, motion_x: f32, motion_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 }, + tilemap: TileMap { + width: 2, + height: 2, + tiles: vec![ + Tile { + active: true, + glyph: Glyph { + glyph_id: 10 + glyph_bank_id as u16, + palette_id: glyph_bank_id, + }, + flip_x: false, + flip_y: false, + }, + Tile { + active: true, + glyph: Glyph { + glyph_id: 20 + glyph_bank_id as u16, + palette_id: glyph_bank_id + 1, + }, + flip_x: true, + flip_y: false, + }, + Tile { + active: true, + glyph: Glyph { + glyph_id: 30 + glyph_bank_id as u16, + palette_id: glyph_bank_id + 2, + }, + flip_x: false, + flip_y: true, + }, + Tile { + active: glyph_bank_id != 2, + glyph: Glyph { + glyph_id: 40 + glyph_bank_id as u16, + palette_id: glyph_bank_id + 3, + }, + flip_x: true, + flip_y: true, + }, + ], + }, + }; + + SceneBank { + layers: [ + make_layer(0, 1.0, 1.0, TileSize::Size16), + make_layer(1, 0.5, 0.75, TileSize::Size8), + make_layer(2, 1.0, 0.5, TileSize::Size32), + make_layer(3, 0.25, 0.25, TileSize::Size16), + ], + } + } + + fn expected_scene_decoded_size(scene: &SceneBank) -> usize { + scene + .layers + .iter() + .map(|layer| { + SCENE_DECODED_LAYER_OVERHEAD_BYTES_V1 + + layer.tilemap.tiles.len() * size_of::() + }) + .sum() + } + + fn encode_scene_payload(scene: &SceneBank) -> Vec { + let mut data = Vec::new(); + data.extend_from_slice(&SCENE_PAYLOAD_MAGIC_V1); + data.extend_from_slice(&SCENE_PAYLOAD_VERSION_V1.to_le_bytes()); + data.extend_from_slice(&(SCENE_LAYER_COUNT_V1 as u16).to_le_bytes()); + data.extend_from_slice(&0_u32.to_le_bytes()); + + for layer in &scene.layers { + let layer_flags = if layer.active { 0b0000_0001 } else { 0 }; + data.push(layer_flags); + 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.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()); + data.extend_from_slice(&0_u32.to_le_bytes()); + + for tile in &layer.tilemap.tiles { + let mut tile_flags = 0_u8; + if tile.active { + tile_flags |= 0b0000_0001; + } + if tile.flip_x { + tile_flags |= 0b0000_0010; + } + if tile.flip_y { + tile_flags |= 0b0000_0100; + } + data.push(tile_flags); + data.push(tile.glyph.palette_id); + data.extend_from_slice(&tile.glyph.glyph_id.to_le_bytes()); + } + } + + data + } + + fn test_scene_asset_entry(asset_name: &str, data: &[u8], scene: &SceneBank) -> AssetEntry { + AssetEntry { + asset_id: 2, + asset_name: asset_name.to_string(), + bank_type: BankType::SCENE, + offset: 0, + size: data.len() as u64, + decoded_size: expected_scene_decoded_size(scene) as u64, + codec: AssetCodec::None, + metadata: serde_json::json!({}), + } + } + #[test] fn test_decode_glyph_bank_unpacks_packed_pixels_and_reads_palette_colors() { let entry = test_glyph_asset_entry("glyphs", 2, 2); @@ -1029,20 +1343,77 @@ mod tests { #[test] fn test_op_mode_for_scene_none_reads_direct_from_slice() { - let entry = AssetEntry { - asset_id: 2, - asset_name: "scene".to_string(), - bank_type: BankType::SCENE, - offset: 0, - size: 32, - decoded_size: 32, - codec: AssetCodec::None, - metadata: serde_json::json!({}), - }; + let scene = test_scene(); + let data = encode_scene_payload(&scene); + let entry = test_scene_asset_entry("scene", &data, &scene); assert_eq!(AssetManager::op_mode_for(&entry), Ok(AssetOpMode::DirectFromSlice)); } + #[test] + fn test_decode_scene_bank_from_binary_payload() { + let scene = test_scene(); + let data = encode_scene_payload(&scene); + let entry = test_scene_asset_entry("scene", &data, &scene); + + 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[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); + assert_eq!(decoded.layers[3].active, false); + } + + #[test] + fn test_decode_scene_bank_rejects_invalid_version() { + let scene = test_scene(); + let mut data = encode_scene_payload(&scene); + data[4..6].copy_from_slice(&2_u16.to_le_bytes()); + let entry = test_scene_asset_entry("scene", &data, &scene); + + let err = AssetManager::decode_scene_bank_from_buffer(&entry, &data).unwrap_err(); + + assert_eq!(err, "Unsupported SCENE version: 2"); + } + + #[test] + fn test_decode_scene_bank_rejects_invalid_tile_size() { + let scene = test_scene(); + let mut data = encode_scene_payload(&scene); + data[14] = 12; + let entry = test_scene_asset_entry("scene", &data, &scene); + + let err = AssetManager::decode_scene_bank_from_buffer(&entry, &data).unwrap_err(); + + assert_eq!(err, "Invalid SCENE tile size: 12"); + } + + #[test] + fn test_decode_scene_bank_rejects_layer_count_mismatch() { + let scene = test_scene(); + let mut data = encode_scene_payload(&scene); + data[6..8].copy_from_slice(&3_u16.to_le_bytes()); + let entry = test_scene_asset_entry("scene", &data, &scene); + + let err = AssetManager::decode_scene_bank_from_buffer(&entry, &data).unwrap_err(); + + assert_eq!(err, "Invalid SCENE layer count: 3"); + } + + #[test] + fn test_decode_scene_bank_rejects_tile_count_mismatch() { + let scene = test_scene(); + let mut data = encode_scene_payload(&scene); + data[32..36].copy_from_slice(&5_u32.to_le_bytes()); + let entry = test_scene_asset_entry("scene", &data, &scene); + + let err = AssetManager::decode_scene_bank_from_buffer(&entry, &data).unwrap_err(); + + assert_eq!(err, "Invalid SCENE tile count for layer: expected 4, got 5"); + } + #[test] fn test_asset_loading_flow() { let banks = Arc::new(MemoryBanks::new()); @@ -1210,6 +1581,70 @@ mod tests { assert_eq!(am.slot_info(SlotRef::audio(5)).asset_id, Some(2)); } + #[test] + fn test_scene_asset_loading() { + let banks = Arc::new(MemoryBanks::new()); + let gfx_installer = Arc::clone(&banks) as Arc; + let sound_installer = Arc::clone(&banks) as Arc; + let scene_installer = Arc::clone(&banks) as Arc; + + let scene = test_scene(); + let data = encode_scene_payload(&scene); + let asset_entry = test_scene_asset_entry("test_scene", &data, &scene); + + let am = AssetManager::new( + vec![asset_entry], + AssetsPayloadSource::from_bytes(data), + gfx_installer, + sound_installer, + scene_installer, + ); + let handle = am.load(2, 0).expect("Should start loading scene"); + + let start = Instant::now(); + while am.status(handle) != LoadStatus::READY && start.elapsed().as_secs() < 5 { + thread::sleep(std::time::Duration::from_millis(10)); + } + + assert_eq!(am.status(handle), LoadStatus::READY); + am.commit(handle); + am.apply_commits(); + + assert_eq!(am.status(handle), LoadStatus::COMMITTED); + assert!(banks.scene_bank_slot(0).is_some()); + } + + #[test] + fn test_scene_preload_on_init() { + let banks = Arc::new(MemoryBanks::new()); + let gfx_installer = Arc::clone(&banks) as Arc; + let sound_installer = Arc::clone(&banks) as Arc; + let scene_installer = Arc::clone(&banks) as Arc; + + let scene = test_scene(); + let data = encode_scene_payload(&scene); + let asset_entry = test_scene_asset_entry("preload_scene", &data, &scene); + let preload = vec![PreloadEntry { asset_id: 2, slot: 4 }]; + + let am = AssetManager::new( + vec![], + AssetsPayloadSource::empty(), + gfx_installer, + sound_installer, + scene_installer, + ); + + assert!(banks.scene_bank_slot(4).is_none()); + am.initialize_for_cartridge( + vec![asset_entry], + preload, + AssetsPayloadSource::from_bytes(data), + ); + + assert!(banks.scene_bank_slot(4).is_some()); + assert_eq!(am.slot_info(SlotRef::scene(4)).asset_id, Some(2)); + } + #[test] fn test_load_returns_asset_not_found() { let banks = Arc::new(MemoryBanks::new()); diff --git a/crates/console/prometeu-hal/src/asset.rs b/crates/console/prometeu-hal/src/asset.rs index 76e4b6f7..36aaf3f9 100644 --- a/crates/console/prometeu-hal/src/asset.rs +++ b/crates/console/prometeu-hal/src/asset.rs @@ -46,6 +46,17 @@ pub struct SoundBankMetadata { pub channels: u32, } +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +pub struct SceneBankMetadata {} + +pub const SCENE_PAYLOAD_MAGIC_V1: [u8; 4] = *b"SCNE"; +pub const SCENE_PAYLOAD_VERSION_V1: u16 = 1; +pub const SCENE_LAYER_COUNT_V1: usize = 4; +pub const SCENE_HEADER_BYTES_V1: usize = 12; +pub const SCENE_LAYER_HEADER_BYTES_V1: usize = 28; +pub const SCENE_TILE_RECORD_BYTES_V1: usize = 4; +pub const SCENE_DECODED_LAYER_OVERHEAD_BYTES_V1: usize = 16; + impl AssetEntry { pub fn metadata_as_glyph_bank(&self) -> Result { if self.bank_type != BankType::GLYPH { @@ -68,6 +79,17 @@ impl AssetEntry { serde_json::from_value(self.metadata.clone()) .map_err(|e| format!("Invalid SOUNDS metadata for asset {}: {}", self.asset_id, e)) } + + pub fn metadata_as_scene_bank(&self) -> Result { + if self.bank_type != BankType::SCENE { + return Err(format!( + "Asset {} is not a SCENE bank (type: {:?})", + self.asset_id, self.bank_type + )); + } + serde_json::from_value(self.metadata.clone()) + .map_err(|e| format!("Invalid SCENE metadata for asset {}: {}", self.asset_id, e)) + } } #[derive(Debug, Clone, Deserialize, Serialize)]