align packer specs and loader
This commit is contained in:
parent
aa9987b46b
commit
b532a05612
@ -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,
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user