align packer specs and loader

This commit is contained in:
bQUARKz 2026-03-20 07:50:46 +00:00
parent aa9987b46b
commit b532a05612
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
4 changed files with 263 additions and 132 deletions

View File

@ -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::<u16>();
/// Resident metadata for a decoded/materialized asset inside a BankPolicy.
#[derive(Debug)]
pub struct ResidentEntry<T> {
@ -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<u8> {
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<AssetOpMode, String> {
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<TileBank, String> {
// 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<TileBank, 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 (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<SoundBank, String> {
fn decode_sound_bank_from_buffer(
entry: &AssetEntry,
buffer: &[u8],
) -> Result<SoundBank, String> {
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<u8> {
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<dyn SoundBankPoolInstaller>;
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<dyn SoundBankPoolInstaller>;
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<dyn TileBankPoolInstaller>;
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
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<dyn SoundBankPoolInstaller>;
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<dyn SoundBankPoolInstaller>;
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<dyn TileBankPoolInstaller>;
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
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<dyn SoundBankPoolInstaller>;
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,

View File

@ -149,11 +149,11 @@ fn parse_assets_pack(path: &Path) -> Result<ParsedAssetsPack, CartridgeError> {
}
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<ParsedAssetsPack, CartridgeError> {
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();

View File

@ -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<u8>,
/// 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)
}
}

View File

@ -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::<u16>())
}
fn test_tile_decoded_size(width: usize, height: usize) -> usize {
width * height + (TILE_BANK_PALETTE_COUNT_V1 * 16 * std::mem::size_of::<u16>())
}
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<u8> {
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);