diff --git a/crates/console/prometeu-drivers/src/asset.rs b/crates/console/prometeu-drivers/src/asset.rs index 3401e1e4..20aa81c9 100644 --- a/crates/console/prometeu-drivers/src/asset.rs +++ b/crates/console/prometeu-drivers/src/asset.rs @@ -16,6 +16,11 @@ use std::sync::{Arc, Mutex, RwLock}; use std::thread; use std::time::Instant; +const TILE_BANK_PALETTE_COUNT_V1: usize = 64; +const TILE_BANK_COLORS_PER_PALETTE: usize = 16; +const TILE_BANK_PALETTE_BYTES_V1: usize = + TILE_BANK_PALETTE_COUNT_V1 * TILE_BANK_COLORS_PER_PALETTE * std::mem::size_of::(); + /// Resident metadata for a decoded/materialized asset inside a BankPolicy. #[derive(Debug)] pub struct ResidentEntry { @@ -175,6 +180,71 @@ impl AssetBridge for AssetManager { } impl AssetManager { + fn decode_tile_bank_layout( + entry: &AssetEntry, + ) -> Result<(TileSize, usize, usize, usize), String> { + let tile_size_val = + entry.metadata.get("tile_size").and_then(|v| v.as_u64()).ok_or("Missing tile_size")?; + let width = + entry.metadata.get("width").and_then(|v| v.as_u64()).ok_or("Missing width")? as usize; + let height = + entry.metadata.get("height").and_then(|v| v.as_u64()).ok_or("Missing height")? as usize; + let palette_count = entry + .metadata + .get("palette_count") + .and_then(|v| v.as_u64()) + .ok_or("Missing palette_count")? as usize; + + let tile_size = match tile_size_val { + 8 => TileSize::Size8, + 16 => TileSize::Size16, + 32 => TileSize::Size32, + _ => return Err(format!("Invalid tile_size: {}", tile_size_val)), + }; + + if palette_count != TILE_BANK_PALETTE_COUNT_V1 { + return Err(format!("Invalid palette_count: {}", palette_count)); + } + + let logical_pixels = width.checked_mul(height).ok_or("TileBank dimensions overflow")?; + let serialized_pixel_bytes = logical_pixels.div_ceil(2); + let serialized_size = serialized_pixel_bytes + .checked_add(TILE_BANK_PALETTE_BYTES_V1) + .ok_or("TileBank serialized size overflow")?; + let decoded_size = logical_pixels + .checked_add(TILE_BANK_PALETTE_BYTES_V1) + .ok_or("TileBank decoded size overflow")?; + + if entry.size != serialized_size as u64 { + return Err(format!( + "Invalid TILEBANK serialized size: expected {}, got {}", + serialized_size, entry.size + )); + } + + if entry.decoded_size != decoded_size as u64 { + return Err(format!( + "Invalid TILEBANK decoded_size: expected {}, got {}", + decoded_size, entry.decoded_size + )); + } + + Ok((tile_size, width, height, serialized_pixel_bytes)) + } + + fn unpack_tile_bank_pixels(packed_pixels: &[u8], logical_pixels: usize) -> Vec { + let mut pixel_indices = Vec::with_capacity(logical_pixels); + for &packed in packed_pixels { + if pixel_indices.len() < logical_pixels { + pixel_indices.push(packed >> 4); + } + if pixel_indices.len() < logical_pixels { + pixel_indices.push(packed & 0x0f); + } + } + pixel_indices + } + fn op_mode_for(entry: &AssetEntry) -> Result { match (entry.bank_type, entry.codec.as_str()) { (BankType::TILES, "RAW") => Ok(AssetOpMode::StageInMemory), @@ -310,9 +380,7 @@ impl AssetManager { let entry = { let assets = self.assets.read().unwrap(); let name_to_id = self.name_to_id.read().unwrap(); - let id = name_to_id - .get(asset_name) - .ok_or(AssetLoadError::AssetNotFound)?; + let id = name_to_id.get(asset_name).ok_or(AssetLoadError::AssetNotFound)?; assets.get(id).ok_or(AssetLoadError::BackendError)?.clone() }; let asset_id = entry.asset_id; @@ -471,41 +539,32 @@ impl AssetManager { match op_mode { AssetOpMode::StageInMemory => { - let buffer = slice.read_all().map_err(|_| "Asset payload read failed".to_string())?; + let buffer = + slice.read_all().map_err(|_| "Asset payload read failed".to_string())?; Self::decode_tile_bank_from_buffer(entry, &buffer) } AssetOpMode::DirectFromSlice => { - let mut reader = slice.open_reader().map_err(|_| "Asset payload read failed".to_string())?; + let mut reader = + slice.open_reader().map_err(|_| "Asset payload read failed".to_string())?; Self::decode_tile_bank_from_reader(entry, &mut reader) } } } fn decode_tile_bank_from_buffer(entry: &AssetEntry, buffer: &[u8]) -> Result { - // Decode TILEBANK metadata - let tile_size_val = - entry.metadata.get("tile_size").and_then(|v| v.as_u64()).ok_or("Missing tile_size")?; - let width = - entry.metadata.get("width").and_then(|v| v.as_u64()).ok_or("Missing width")? as usize; - let height = - entry.metadata.get("height").and_then(|v| v.as_u64()).ok_or("Missing height")? as usize; - - let tile_size = match tile_size_val { - 8 => TileSize::Size8, - 16 => TileSize::Size16, - 32 => TileSize::Size32, - _ => return Err(format!("Invalid tile_size: {}", tile_size_val)), - }; - - let pixel_data_size = width * height; - if buffer.len() < pixel_data_size + 2048 { + let (tile_size, width, height, packed_pixel_bytes) = Self::decode_tile_bank_layout(entry)?; + if buffer.len() < packed_pixel_bytes + TILE_BANK_PALETTE_BYTES_V1 { return Err("Buffer too small for TILEBANK".to_string()); } - let pixel_indices = buffer[0..pixel_data_size].to_vec(); - let palette_data = &buffer[pixel_data_size..pixel_data_size + 2048]; + let logical_pixels = width * height; + let packed_pixels = &buffer[0..packed_pixel_bytes]; + let pixel_indices = Self::unpack_tile_bank_pixels(packed_pixels, logical_pixels); + let palette_data = + &buffer[packed_pixel_bytes..packed_pixel_bytes + TILE_BANK_PALETTE_BYTES_V1]; - let mut palettes = [[Color::BLACK; 16]; 64]; + let mut palettes = + [[Color::BLACK; TILE_BANK_COLORS_PER_PALETTE]; TILE_BANK_PALETTE_COUNT_V1]; for (p, pal) in palettes.iter_mut().enumerate() { for (c, slot) in pal.iter_mut().enumerate() { let offset = (p * 16 + c) * 2; @@ -522,28 +581,22 @@ impl AssetManager { entry: &AssetEntry, reader: &mut impl Read, ) -> Result { - let tile_size_val = - entry.metadata.get("tile_size").and_then(|v| v.as_u64()).ok_or("Missing tile_size")?; - let width = - entry.metadata.get("width").and_then(|v| v.as_u64()).ok_or("Missing width")? as usize; - let height = - entry.metadata.get("height").and_then(|v| v.as_u64()).ok_or("Missing height")? as usize; + let (tile_size, width, height, packed_pixel_bytes) = Self::decode_tile_bank_layout(entry)?; + let logical_pixels = width * height; + let mut packed_pixels = vec![0_u8; packed_pixel_bytes]; + reader + .read_exact(&mut packed_pixels) + .map_err(|_| "Buffer too small for TILEBANK".to_string())?; - let tile_size = match tile_size_val { - 8 => TileSize::Size8, - 16 => TileSize::Size16, - 32 => TileSize::Size32, - _ => return Err(format!("Invalid tile_size: {}", tile_size_val)), - }; + let pixel_indices = Self::unpack_tile_bank_pixels(&packed_pixels, logical_pixels); - let pixel_data_size = width * height; - let mut pixel_indices = vec![0_u8; pixel_data_size]; - reader.read_exact(&mut pixel_indices).map_err(|_| "Buffer too small for TILEBANK".to_string())?; + let mut palette_data = [0_u8; TILE_BANK_PALETTE_BYTES_V1]; + reader + .read_exact(&mut palette_data) + .map_err(|_| "Buffer too small for TILEBANK".to_string())?; - let mut palette_data = [0_u8; 2048]; - reader.read_exact(&mut palette_data).map_err(|_| "Buffer too small for TILEBANK".to_string())?; - - let mut palettes = [[Color::BLACK; 16]; 64]; + let mut palettes = + [[Color::BLACK; TILE_BANK_COLORS_PER_PALETTE]; TILE_BANK_PALETTE_COUNT_V1]; for (p, pal) in palettes.iter_mut().enumerate() { for (c, slot) in pal.iter_mut().enumerate() { let offset = (p * 16 + c) * 2; @@ -570,17 +623,22 @@ impl AssetManager { match op_mode { AssetOpMode::DirectFromSlice => { - let mut reader = slice.open_reader().map_err(|_| "Asset payload read failed".to_string())?; + let mut reader = + slice.open_reader().map_err(|_| "Asset payload read failed".to_string())?; Self::decode_sound_bank_from_reader(entry, &mut reader) } AssetOpMode::StageInMemory => { - let buffer = slice.read_all().map_err(|_| "Asset payload read failed".to_string())?; + let buffer = + slice.read_all().map_err(|_| "Asset payload read failed".to_string())?; Self::decode_sound_bank_from_buffer(entry, &buffer) } } } - fn decode_sound_bank_from_buffer(entry: &AssetEntry, buffer: &[u8]) -> Result { + fn decode_sound_bank_from_buffer( + entry: &AssetEntry, + buffer: &[u8], + ) -> Result { let sample_rate = entry.metadata.get("sample_rate").and_then(|v| v.as_u64()).unwrap_or(44100) as u32; @@ -631,9 +689,7 @@ impl AssetManager { let mut handles_map = self.handles.write().unwrap(); if let Some(h) = handles_map.get_mut(&handle) { final_status = match h.status { - LoadStatus::PENDING | LoadStatus::LOADING | LoadStatus::READY => { - AssetOpStatus::Ok - } + LoadStatus::PENDING | LoadStatus::LOADING | LoadStatus::READY => AssetOpStatus::Ok, LoadStatus::CANCELED => AssetOpStatus::Ok, _ => AssetOpStatus::InvalidState, }; @@ -836,32 +892,82 @@ mod tests { use super::*; use crate::memory_banks::{MemoryBanks, SoundBankPoolAccess, TileBankPoolAccess}; + fn expected_tile_payload_size(width: usize, height: usize) -> usize { + (width * height).div_ceil(2) + TILE_BANK_PALETTE_BYTES_V1 + } + + fn expected_tile_decoded_size(width: usize, height: usize) -> usize { + width * height + TILE_BANK_PALETTE_BYTES_V1 + } + fn test_tile_asset_data() -> Vec { - let mut data = vec![1u8; 256]; - data.extend_from_slice(&[0u8; 2048]); + let mut data = vec![0x11u8; 128]; + data.extend_from_slice(&[0u8; TILE_BANK_PALETTE_BYTES_V1]); data } - fn test_tile_asset_entry(asset_name: &str, data_len: usize) -> AssetEntry { + fn test_tile_asset_entry(asset_name: &str, width: usize, height: usize) -> AssetEntry { AssetEntry { asset_id: 0, asset_name: asset_name.to_string(), bank_type: BankType::TILES, offset: 0, - size: data_len as u64, - decoded_size: data_len as u64, + size: expected_tile_payload_size(width, height) as u64, + decoded_size: expected_tile_decoded_size(width, height) as u64, codec: "RAW".to_string(), metadata: serde_json::json!({ "tile_size": 16, - "width": 16, - "height": 16 + "width": width, + "height": height, + "palette_count": TILE_BANK_PALETTE_COUNT_V1 }), } } + #[test] + fn test_decode_tile_bank_unpacks_packed_pixels_and_reads_palette_colors() { + let entry = test_tile_asset_entry("tiles", 2, 2); + let mut data = vec![0x10, 0x23]; + data.extend_from_slice(&[0u8; TILE_BANK_PALETTE_BYTES_V1]); + data[2] = 0x34; + data[3] = 0x12; + + let bank = AssetManager::decode_tile_bank_from_buffer(&entry, &data).expect("tile decode"); + + assert_eq!(bank.pixel_indices, vec![1, 0, 2, 3]); + assert_eq!(bank.palettes[0][0], Color(0x1234)); + } + + #[test] + fn test_decode_tile_bank_rejects_short_packed_buffer() { + let entry = test_tile_asset_entry("tiles", 16, 16); + let data = vec![0u8; expected_tile_payload_size(16, 16) - 1]; + + let err = match AssetManager::decode_tile_bank_from_buffer(&entry, &data) { + Ok(_) => panic!("tile decode should reject short buffer"), + Err(err) => err, + }; + + assert_eq!(err, "Buffer too small for TILEBANK"); + } + + #[test] + fn test_decode_tile_bank_requires_palette_count_64() { + let mut entry = test_tile_asset_entry("tiles", 16, 16); + entry.metadata["palette_count"] = serde_json::json!(32); + + let err = match AssetManager::decode_tile_bank_from_buffer(&entry, &test_tile_asset_data()) + { + Ok(_) => panic!("tile decode should reject invalid palette_count"), + Err(err) => err, + }; + + assert_eq!(err, "Invalid palette_count: 32"); + } + #[test] fn test_op_mode_for_tiles_raw_stages_in_memory() { - let entry = test_tile_asset_entry("tiles", test_tile_asset_data().len()); + let entry = test_tile_asset_entry("tiles", 16, 16); assert_eq!(AssetManager::op_mode_for(&entry), Ok(AssetOpMode::StageInMemory)); } @@ -891,7 +997,7 @@ mod tests { let sound_installer = Arc::clone(&banks) as Arc; let data = test_tile_asset_data(); - let asset_entry = test_tile_asset_entry("test_tiles", data.len()); + let asset_entry = test_tile_asset_entry("test_tiles", 16, 16); let am = AssetManager::new( vec![asset_entry], @@ -931,7 +1037,7 @@ mod tests { let sound_installer = Arc::clone(&banks) as Arc; let data = test_tile_asset_data(); - let asset_entry = test_tile_asset_entry("test_tiles", data.len()); + let asset_entry = test_tile_asset_entry("test_tiles", 16, 16); let am = AssetManager::new( vec![asset_entry], @@ -1024,17 +1130,17 @@ mod tests { let preload = vec![PreloadEntry { asset_id: 2, slot: 5 }]; - let am = AssetManager::new( - vec![], - AssetsPayloadSource::empty(), - gfx_installer, - sound_installer, - ); + let am = + AssetManager::new(vec![], AssetsPayloadSource::empty(), gfx_installer, sound_installer); // Before init, slot 5 is empty assert!(banks.sound_bank_slot(5).is_none()); - am.initialize_for_cartridge(vec![asset_entry], preload, AssetsPayloadSource::from_bytes(data)); + am.initialize_for_cartridge( + vec![asset_entry], + preload, + AssetsPayloadSource::from_bytes(data), + ); // After init, slot 5 should be occupied because of preload assert!(banks.sound_bank_slot(5).is_some()); @@ -1046,12 +1152,8 @@ mod tests { let banks = Arc::new(MemoryBanks::new()); let gfx_installer = Arc::clone(&banks) as Arc; let sound_installer = Arc::clone(&banks) as Arc; - let am = AssetManager::new( - vec![], - AssetsPayloadSource::empty(), - gfx_installer, - sound_installer, - ); + let am = + AssetManager::new(vec![], AssetsPayloadSource::empty(), gfx_installer, sound_installer); let result = am.load("missing", SlotRef::gfx(0)); @@ -1065,7 +1167,7 @@ mod tests { let sound_installer = Arc::clone(&banks) as Arc; let data = test_tile_asset_data(); let am = AssetManager::new( - vec![test_tile_asset_entry("test_tiles", data.len())], + vec![test_tile_asset_entry("test_tiles", 16, 16)], AssetsPayloadSource::from_bytes(data), gfx_installer, sound_installer, @@ -1083,7 +1185,7 @@ mod tests { let sound_installer = Arc::clone(&banks) as Arc; let data = test_tile_asset_data(); let am = AssetManager::new( - vec![test_tile_asset_entry("test_tiles", data.len())], + vec![test_tile_asset_entry("test_tiles", 16, 16)], AssetsPayloadSource::from_bytes(data), gfx_installer, sound_installer, @@ -1099,12 +1201,8 @@ mod tests { let banks = Arc::new(MemoryBanks::new()); let gfx_installer = Arc::clone(&banks) as Arc; let sound_installer = Arc::clone(&banks) as Arc; - let am = AssetManager::new( - vec![], - AssetsPayloadSource::empty(), - gfx_installer, - sound_installer, - ); + let am = + AssetManager::new(vec![], AssetsPayloadSource::empty(), gfx_installer, sound_installer); assert_eq!(am.status(999), LoadStatus::UnknownHandle); } @@ -1116,7 +1214,7 @@ mod tests { let sound_installer = Arc::clone(&banks) as Arc; let data = test_tile_asset_data(); let am = AssetManager::new( - vec![test_tile_asset_entry("test_tiles", data.len())], + vec![test_tile_asset_entry("test_tiles", 16, 16)], AssetsPayloadSource::from_bytes(data), gfx_installer, sound_installer, diff --git a/crates/console/prometeu-hal/src/cartridge_loader.rs b/crates/console/prometeu-hal/src/cartridge_loader.rs index 749e41d2..2257a622 100644 --- a/crates/console/prometeu-hal/src/cartridge_loader.rs +++ b/crates/console/prometeu-hal/src/cartridge_loader.rs @@ -149,11 +149,11 @@ fn parse_assets_pack(path: &Path) -> Result { } let header_start = ASSETS_PA_PRELUDE_SIZE; - let header_len = usize::try_from(prelude.header_len).map_err(|_| CartridgeError::InvalidFormat)?; - let header_end = header_start - .checked_add(header_len) - .ok_or(CartridgeError::InvalidFormat)?; - let payload_offset = usize::try_from(prelude.payload_offset).map_err(|_| CartridgeError::InvalidFormat)?; + let header_len = + usize::try_from(prelude.header_len).map_err(|_| CartridgeError::InvalidFormat)?; + let header_end = header_start.checked_add(header_len).ok_or(CartridgeError::InvalidFormat)?; + let payload_offset = + usize::try_from(prelude.payload_offset).map_err(|_| CartridgeError::InvalidFormat)?; let file_len = usize::try_from(file.metadata().map_err(|_| CartridgeError::IoError)?.len()) .map_err(|_| CartridgeError::InvalidFormat)?; @@ -170,12 +170,16 @@ fn parse_assets_pack(path: &Path) -> Result { let header: AssetsPackHeader = serde_json::from_slice(&header_bytes).map_err(|_| CartridgeError::InvalidFormat)?; validate_preload(&header.asset_table, &header.preload)?; - let payload_len = u64::try_from(file_len - payload_offset).map_err(|_| CartridgeError::InvalidFormat)?; + let payload_len = + u64::try_from(file_len - payload_offset).map_err(|_| CartridgeError::InvalidFormat)?; Ok(ParsedAssetsPack { header, payload_offset: prelude.payload_offset, payload_len }) } -fn validate_preload(asset_table: &[AssetEntry], preload: &[crate::asset::PreloadEntry]) -> Result<(), CartridgeError> { +fn validate_preload( + asset_table: &[AssetEntry], + preload: &[crate::asset::PreloadEntry], +) -> Result<(), CartridgeError> { let mut claimed_slots = HashSet::<(BankType, usize)>::new(); for item in preload { @@ -197,6 +201,7 @@ mod tests { use super::*; use crate::asset::{AssetEntry, BankType, PreloadEntry}; use crate::cartridge::{ASSETS_PA_MAGIC, ASSETS_PA_SCHEMA_VERSION, AssetsPackPrelude}; + use crate::tile_bank::TILE_BANK_PALETTE_COUNT_V1; use serde_json::json; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicU64, Ordering}; @@ -363,12 +368,13 @@ mod tests { bank_type: BankType::TILES, offset, size, - decoded_size: size, + decoded_size: 16 * 16 + (TILE_BANK_PALETTE_COUNT_V1 as u64 * 16 * 2), codec: "RAW".to_string(), metadata: json!({ "tile_size": 16, "width": 16, - "height": 16 + "height": 16, + "palette_count": TILE_BANK_PALETTE_COUNT_V1 }), } } @@ -444,16 +450,18 @@ mod tests { bank_type: BankType::TILES, offset: 4, size: 4, - decoded_size: 4, + decoded_size: 16 * 16 + (TILE_BANK_PALETTE_COUNT_V1 as u64 * 16 * 2), codec: "RAW".to_string(), metadata: json!({ "tile_size": 16, "width": 16, - "height": 16 + "height": 16, + "palette_count": TILE_BANK_PALETTE_COUNT_V1 }), }, ]; - let preload = vec![PreloadEntry { asset_id: 7, slot: 2 }, PreloadEntry { asset_id: 8, slot: 2 }]; + let preload = + vec![PreloadEntry { asset_id: 7, slot: 2 }, PreloadEntry { asset_id: 8, slot: 2 }]; dir.write_assets_pa(asset_table, preload, &[1_u8, 2, 3, 4, 5, 6, 7, 8]); let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err(); diff --git a/crates/console/prometeu-hal/src/tile_bank.rs b/crates/console/prometeu-hal/src/tile_bank.rs index 15e0cac2..422ba496 100644 --- a/crates/console/prometeu-hal/src/tile_bank.rs +++ b/crates/console/prometeu-hal/src/tile_bank.rs @@ -1,5 +1,8 @@ use crate::color::Color; +pub const TILE_BANK_PALETTE_COUNT_V1: usize = 64; +pub const TILE_BANK_COLORS_PER_PALETTE: usize = 16; + /// Standard sizes for square tiles. #[derive(Clone, Copy, Debug, PartialEq)] pub enum TileSize { @@ -13,9 +16,11 @@ pub enum TileSize { /// A container for graphical assets. /// -/// A TileBank stores both the raw pixel data (as palette indices) and the -/// color palettes themselves. This encapsulates all the information needed -/// to render a set of tiles. +/// A TileBank stores the decoded runtime representation of a tile-bank asset. +/// +/// Serialized `assets.pa` payloads keep pixel indices packed as `4bpp` nibbles. +/// After decode, the runtime expands them to one `u8` palette index per pixel +/// while preserving the same `0..15` logical range. pub struct TileBank { /// Dimension of each individual tile in the bank. pub tile_size: TileSize, @@ -24,11 +29,12 @@ pub struct TileBank { /// Height of the full bank sheet in pixels. pub height: usize, - /// Pixel data stored as 4-bit indices (packed into 8-bit values). + /// Decoded pixel data stored as one palette index per pixel. + /// Serialized payloads are packed; runtime memory is expanded. /// Index 0 is always reserved for transparency. pub pixel_indices: Vec, - /// Table of 64 palettes, each containing 16 RGB565 colors, total of 1024 colors for a bank. - pub palettes: [[Color; 16]; 64], + /// Runtime-facing v1 palette table: 64 palettes of 16 RGB565 colors each. + pub palettes: [[Color; TILE_BANK_COLORS_PER_PALETTE]; TILE_BANK_PALETTE_COUNT_V1], } impl TileBank { @@ -39,7 +45,7 @@ impl TileBank { width, height, pixel_indices: vec![0; width * height], // Index 0 = Transparent - palettes: [[Color::BLACK; 16]; 64], + palettes: [[Color::BLACK; TILE_BANK_COLORS_PER_PALETTE]; TILE_BANK_PALETTE_COUNT_V1], } } @@ -70,6 +76,10 @@ impl TileBank { return Color::COLOR_KEY; } - self.palettes[palette_id as usize][pixel_index as usize] + self.palettes + .get(palette_id as usize) + .and_then(|palette| palette.get(pixel_index as usize)) + .copied() + .unwrap_or(Color::COLOR_KEY) } } diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs b/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs index 298018b9..419054d6 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs @@ -1,16 +1,17 @@ use super::*; +use crate::fs::{FsBackend, FsEntry, FsError}; use prometeu_bytecode::TRAP_TYPE; use prometeu_bytecode::Value; use prometeu_bytecode::assembler::assemble; use prometeu_bytecode::model::{BytecodeModule, ConstantPoolEntry, FunctionMeta, SyscallDecl}; use prometeu_drivers::hardware::Hardware; -use crate::fs::{FsBackend, FsEntry, FsError}; use prometeu_hal::AudioOpStatus; -use prometeu_hal::asset::{AssetEntry, AssetLoadError, AssetOpStatus, BankType, LoadStatus}; use prometeu_hal::GfxOpStatus; use prometeu_hal::InputSignals; +use prometeu_hal::asset::{AssetEntry, AssetLoadError, AssetOpStatus, BankType, LoadStatus}; use prometeu_hal::cartridge::{AssetsPayloadSource, Cartridge}; use prometeu_hal::syscalls::caps; +use prometeu_hal::tile_bank::TILE_BANK_PALETTE_COUNT_V1; use prometeu_vm::VmInitError; use std::collections::HashMap; @@ -92,6 +93,14 @@ fn serialized_single_function_module_with_consts( .serialize() } +fn test_tile_payload_size(width: usize, height: usize) -> usize { + (width * height).div_ceil(2) + (TILE_BANK_PALETTE_COUNT_V1 * 16 * std::mem::size_of::()) +} + +fn test_tile_decoded_size(width: usize, height: usize) -> usize { + width * height + (TILE_BANK_PALETTE_COUNT_V1 * 16 * std::mem::size_of::()) +} + fn test_tile_asset_entry(asset_name: &str, data_len: usize) -> AssetEntry { AssetEntry { asset_id: 7, @@ -99,19 +108,21 @@ fn test_tile_asset_entry(asset_name: &str, data_len: usize) -> AssetEntry { bank_type: BankType::TILES, offset: 0, size: data_len as u64, - decoded_size: data_len as u64, + decoded_size: test_tile_decoded_size(16, 16) as u64, codec: "RAW".to_string(), metadata: serde_json::json!({ "tile_size": 16, "width": 16, - "height": 16 + "height": 16, + "palette_count": TILE_BANK_PALETTE_COUNT_V1 }), } } fn test_tile_asset_data() -> Vec { - let mut data = vec![1u8; 256]; - data.extend_from_slice(&[0u8; 2048]); + let mut data = + vec![0x11u8; test_tile_payload_size(16, 16) - (TILE_BANK_PALETTE_COUNT_V1 * 16 * 2)]; + data.extend_from_slice(&[0u8; TILE_BANK_PALETTE_COUNT_V1 * 16 * 2]); data } @@ -405,10 +416,7 @@ fn tick_gfx_set_sprite_invalid_index_returns_status_not_crash() { let report = runtime.tick(&mut vm, &signals, &mut hardware); assert!(report.is_none(), "invalid sprite index must not crash"); assert!(vm.is_halted()); - assert_eq!( - vm.operand_stack_top(1), - vec![Value::Int64(GfxOpStatus::InvalidSpriteIndex as i64)] - ); + assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(GfxOpStatus::InvalidSpriteIndex as i64)]); } #[test] @@ -432,12 +440,19 @@ fn tick_gfx_set_sprite_invalid_range_returns_status_not_crash() { }], ); let cartridge = cartridge_with_program(program, caps::GFX); + let asset_data = test_tile_asset_data(); + + hardware.assets.initialize_for_cartridge( + vec![test_tile_asset_entry("tile_asset", asset_data.len())], + vec![prometeu_hal::asset::PreloadEntry { asset_id: 7, slot: 0 }], + AssetsPayloadSource::from_bytes(asset_data), + ); runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); let report = runtime.tick(&mut vm, &signals, &mut hardware); assert!(report.is_none(), "invalid gfx parameter range must not crash"); assert!(vm.is_halted()); - assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(GfxOpStatus::BankInvalid as i64)]); + assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(GfxOpStatus::ArgRangeInvalid as i64)]); } #[test] @@ -446,8 +461,10 @@ fn tick_audio_play_sample_operational_error_returns_status_not_crash() { let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); - let code = assemble("PUSH_I32 -1\nPUSH_I32 0\nPUSH_I32 255\nPUSH_I32 128\nPUSH_I32 1\nHOSTCALL 0\nHALT") - .expect("assemble"); + let code = assemble( + "PUSH_I32 -1\nPUSH_I32 0\nPUSH_I32 255\nPUSH_I32 128\nPUSH_I32 1\nHOSTCALL 0\nHALT", + ) + .expect("assemble"); let program = serialized_single_function_module( code, vec![SyscallDecl { @@ -548,9 +565,8 @@ fn tick_audio_play_type_mismatch_surfaces_trap_not_panic() { let cartridge = cartridge_with_program(program, caps::AUDIO); runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); - let report = runtime - .tick(&mut vm, &signals, &mut hardware) - .expect("type mismatch must surface as trap"); + let report = + runtime.tick(&mut vm, &signals, &mut hardware).expect("type mismatch must surface as trap"); match report { CrashReport::VmTrap { trap } => { assert_eq!(trap.code, TRAP_TYPE); @@ -593,7 +609,8 @@ fn tick_asset_load_missing_asset_returns_status_and_zero_handle() { let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); - let code = assemble("PUSH_CONST 0\nPUSH_I32 0\nPUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"); + let code = + assemble("PUSH_CONST 0\nPUSH_I32 0\nPUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"); let program = serialized_single_function_module_with_consts( code, vec![ConstantPoolEntry::String("missing_asset".into())], @@ -629,7 +646,8 @@ fn tick_asset_load_invalid_slot_returns_status_and_zero_handle() { vec![], AssetsPayloadSource::from_bytes(asset_data), ); - let code = assemble("PUSH_CONST 0\nPUSH_I32 0\nPUSH_I32 16\nHOSTCALL 0\nHALT").expect("assemble"); + let code = + assemble("PUSH_CONST 0\nPUSH_I32 0\nPUSH_I32 16\nHOSTCALL 0\nHALT").expect("assemble"); let program = serialized_single_function_module_with_consts( code, vec![ConstantPoolEntry::String("tile_asset".into())], @@ -665,7 +683,8 @@ fn tick_asset_load_kind_mismatch_returns_status_and_zero_handle() { vec![], AssetsPayloadSource::from_bytes(asset_data), ); - let code = assemble("PUSH_CONST 0\nPUSH_I32 1\nPUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"); + let code = + assemble("PUSH_CONST 0\nPUSH_I32 1\nPUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"); let program = serialized_single_function_module_with_consts( code, vec![ConstantPoolEntry::String("tile_asset".into())], @@ -721,10 +740,8 @@ fn tick_asset_commit_invalid_transition_returns_status_not_crash() { let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); - let code = assemble( - "PUSH_I32 1\nHOSTCALL 0\nPOP_N 1\nPUSH_I32 1\nHOSTCALL 1\nHALT", - ) - .expect("assemble"); + let code = assemble("PUSH_I32 1\nHOSTCALL 0\nPOP_N 1\nPUSH_I32 1\nHOSTCALL 1\nHALT") + .expect("assemble"); let program = serialized_single_function_module( code, vec![ @@ -849,7 +866,8 @@ fn tick_asset_load_invalid_kind_surfaces_trap_not_panic() { let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); - let code = assemble("PUSH_CONST 0\nPUSH_I32 2\nPUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"); + let code = + assemble("PUSH_CONST 0\nPUSH_I32 2\nPUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"); let program = serialized_single_function_module_with_consts( code, vec![ConstantPoolEntry::String("tile_asset".into())], @@ -892,9 +910,7 @@ fn tick_status_first_surface_smoke_across_gfx_audio_and_asset() { .expect("assemble"); let program = serialized_single_function_module_with_consts( code, - vec![ - ConstantPoolEntry::String("missing_asset".into()), - ], + vec![ConstantPoolEntry::String("missing_asset".into())], vec![ SyscallDecl { module: "gfx".into(), @@ -959,9 +975,8 @@ fn tick_gfx_set_sprite_type_mismatch_surfaces_trap_not_panic() { let cartridge = cartridge_with_program(program, caps::GFX); runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); - let report = runtime - .tick(&mut vm, &signals, &mut hardware) - .expect("type mismatch must surface as trap"); + let report = + runtime.tick(&mut vm, &signals, &mut hardware).expect("type mismatch must surface as trap"); match report { CrashReport::VmTrap { trap } => { assert_eq!(trap.code, TRAP_TYPE);