implements PR-011c assets.pa payload source and open slice

This commit is contained in:
bQUARKz 2026-03-11 06:48:11 +00:00
parent d3321c4400
commit 077378a3b5
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
7 changed files with 215 additions and 71 deletions

View File

@ -5,6 +5,7 @@ use prometeu_hal::asset::{
AssetEntry, AssetLoadError, AssetOpStatus, BankStats, BankType, HandleId, LoadStatus,
PreloadEntry, SlotRef, SlotStats,
};
use prometeu_hal::cartridge::AssetsPayloadSource;
use prometeu_hal::color::Color;
use prometeu_hal::sample::Sample;
use prometeu_hal::sound_bank::SoundBank;
@ -106,7 +107,7 @@ pub struct AssetManager {
name_to_id: Arc<RwLock<HashMap<String, u32>>>,
handles: Arc<RwLock<HashMap<HandleId, LoadHandleInfo>>>,
next_handle_id: Mutex<HandleId>,
assets_data: Arc<RwLock<Vec<u8>>>,
assets_data: Arc<RwLock<AssetsPayloadSource>>,
/// Narrow hardware interfaces
gfx_installer: Arc<dyn TileBankPoolInstaller>,
@ -136,7 +137,7 @@ impl AssetBridge for AssetManager {
&self,
assets: Vec<AssetEntry>,
preload: Vec<PreloadEntry>,
assets_data: Vec<u8>,
assets_data: AssetsPayloadSource,
) {
self.initialize_for_cartridge(assets, preload, assets_data)
}
@ -172,7 +173,7 @@ impl AssetBridge for AssetManager {
impl AssetManager {
pub fn new(
assets: Vec<AssetEntry>,
assets_data: Vec<u8>,
assets_data: AssetsPayloadSource,
gfx_installer: Arc<dyn TileBankPoolInstaller>,
sound_installer: Arc<dyn SoundBankPoolInstaller>,
) -> Self {
@ -203,7 +204,7 @@ impl AssetManager {
&self,
assets: Vec<AssetEntry>,
preload: Vec<PreloadEntry>,
assets_data: Vec<u8>,
assets_data: AssetsPayloadSource,
) {
self.shutdown();
{
@ -450,22 +451,20 @@ impl AssetManager {
fn perform_load_tile_bank(
entry: &AssetEntry,
assets_data: Arc<RwLock<Vec<u8>>>,
assets_data: Arc<RwLock<AssetsPayloadSource>>,
) -> Result<TileBank, String> {
if entry.codec != "RAW" {
return Err(format!("Unsupported codec: {}", entry.codec));
}
let assets_data = assets_data.read().unwrap();
let start = entry.offset as usize;
let end = start + entry.size as usize;
if end > assets_data.len() {
return Err("Asset offset/size out of bounds".to_string());
}
let buffer = &assets_data[start..end];
let buffer = {
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())?
.read_all()
.map_err(|_| "Asset payload read failed".to_string())?
};
// Decode TILEBANK metadata
let tile_size_val =
@ -505,22 +504,20 @@ impl AssetManager {
fn perform_load_sound_bank(
entry: &AssetEntry,
assets_data: Arc<RwLock<Vec<u8>>>,
assets_data: Arc<RwLock<AssetsPayloadSource>>,
) -> Result<SoundBank, String> {
if entry.codec != "RAW" {
return Err(format!("Unsupported codec: {}", entry.codec));
}
let assets_data = assets_data.read().unwrap();
let start = entry.offset as usize;
let end = start + entry.size as usize;
if end > assets_data.len() {
return Err("Asset offset/size out of bounds".to_string());
}
let buffer = &assets_data[start..end];
let buffer = {
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())?
.read_all()
.map_err(|_| "Asset payload read failed".to_string())?
};
let sample_rate =
entry.metadata.get("sample_rate").and_then(|v| v.as_u64()).unwrap_or(44100) as u32;
@ -818,7 +815,12 @@ mod tests {
let data = test_tile_asset_data();
let asset_entry = test_tile_asset_entry("test_tiles", data.len());
let am = AssetManager::new(vec![asset_entry], data, gfx_installer, sound_installer);
let am = AssetManager::new(
vec![asset_entry],
AssetsPayloadSource::from_bytes(data),
gfx_installer,
sound_installer,
);
let slot = SlotRef::gfx(0);
let handle = am.load("test_tiles", slot).expect("Should start loading");
@ -853,7 +855,12 @@ mod tests {
let data = test_tile_asset_data();
let asset_entry = test_tile_asset_entry("test_tiles", data.len());
let am = AssetManager::new(vec![asset_entry], data, gfx_installer, sound_installer);
let am = AssetManager::new(
vec![asset_entry],
AssetsPayloadSource::from_bytes(data),
gfx_installer,
sound_installer,
);
let handle1 = am.load("test_tiles", SlotRef::gfx(0)).unwrap();
let start = Instant::now();
@ -892,7 +899,12 @@ mod tests {
}),
};
let am = AssetManager::new(vec![asset_entry], data, gfx_installer, sound_installer);
let am = AssetManager::new(
vec![asset_entry],
AssetsPayloadSource::from_bytes(data),
gfx_installer,
sound_installer,
);
let slot = SlotRef::audio(0);
let handle = am.load("test_sound", slot).expect("Should start loading");
@ -934,12 +946,17 @@ mod tests {
let preload = vec![PreloadEntry { asset_name: "preload_sound".to_string(), slot: 5 }];
let am = AssetManager::new(vec![], vec![], 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, 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());
@ -958,8 +975,13 @@ mod tests {
let preload = vec![PreloadEntry { asset_name: "my_tiles".to_string(), slot: 3 }];
let am = AssetManager::new(vec![], vec![], gfx_installer, sound_installer);
am.initialize_for_cartridge(vec![asset_entry], preload, data);
let am = AssetManager::new(
vec![],
AssetsPayloadSource::empty(),
gfx_installer,
sound_installer,
);
am.initialize_for_cartridge(vec![asset_entry], preload, AssetsPayloadSource::from_bytes(data));
assert_eq!(am.find_slot_by_name("my_tiles", BankType::TILES), Some(3));
assert_eq!(am.find_slot_by_name("unknown", BankType::TILES), None);
@ -971,7 +993,12 @@ 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![], vec![], gfx_installer, sound_installer);
let am = AssetManager::new(
vec![],
AssetsPayloadSource::empty(),
gfx_installer,
sound_installer,
);
let result = am.load("missing", SlotRef::gfx(0));
@ -986,7 +1013,7 @@ mod tests {
let data = test_tile_asset_data();
let am = AssetManager::new(
vec![test_tile_asset_entry("test_tiles", data.len())],
data,
AssetsPayloadSource::from_bytes(data),
gfx_installer,
sound_installer,
);
@ -1004,7 +1031,7 @@ mod tests {
let data = test_tile_asset_data();
let am = AssetManager::new(
vec![test_tile_asset_entry("test_tiles", data.len())],
data,
AssetsPayloadSource::from_bytes(data),
gfx_installer,
sound_installer,
);
@ -1019,7 +1046,12 @@ 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![], vec![], gfx_installer, sound_installer);
let am = AssetManager::new(
vec![],
AssetsPayloadSource::empty(),
gfx_installer,
sound_installer,
);
assert_eq!(am.status(999), LoadStatus::UnknownHandle);
}
@ -1032,7 +1064,7 @@ mod tests {
let data = test_tile_asset_data();
let am = AssetManager::new(
vec![test_tile_asset_entry("test_tiles", data.len())],
data,
AssetsPayloadSource::from_bytes(data),
gfx_installer,
sound_installer,
);

View File

@ -7,6 +7,7 @@ use crate::memory_banks::{
};
use crate::pad::Pad;
use crate::touch::Touch;
use prometeu_hal::cartridge::AssetsPayloadSource;
use prometeu_hal::{AssetBridge, AudioBridge, GfxBridge, HardwareBridge, PadBridge, TouchBridge};
use std::sync::Arc;
@ -98,7 +99,7 @@ impl Hardware {
touch: Touch::default(),
assets: AssetManager::new(
vec![],
vec![],
AssetsPayloadSource::empty(),
Arc::clone(&memory_banks) as Arc<dyn TileBankPoolInstaller>,
Arc::clone(&memory_banks) as Arc<dyn SoundBankPoolInstaller>,
),

View File

@ -171,7 +171,7 @@ mod tests {
use prometeu_bytecode::assembler::assemble;
use prometeu_bytecode::model::{BytecodeModule, FunctionMeta, SyscallDecl};
use prometeu_drivers::hardware::Hardware;
use prometeu_hal::cartridge::AppMode;
use prometeu_hal::cartridge::{AppMode, AssetsPayloadSource};
use prometeu_hal::syscalls::caps;
use prometeu_system::CrashReport;
@ -184,7 +184,7 @@ mod tests {
entrypoint: "".into(),
capabilities: 0,
program: vec![0, 0, 0, 0],
assets: vec![],
assets: AssetsPayloadSource::empty(),
asset_table: vec![],
preload: vec![],
}
@ -221,7 +221,7 @@ mod tests {
entrypoint: "".into(),
capabilities: caps::GFX,
program,
assets: vec![],
assets: AssetsPayloadSource::empty(),
asset_table: vec![],
preload: vec![],
}

View File

@ -2,13 +2,14 @@ use crate::asset::{
AssetEntry, AssetLoadError, AssetOpStatus, BankStats, BankType, HandleId, LoadStatus,
PreloadEntry, SlotRef, SlotStats,
};
use crate::cartridge::AssetsPayloadSource;
pub trait AssetBridge {
fn initialize_for_cartridge(
&self,
assets: Vec<AssetEntry>,
preload: Vec<PreloadEntry>,
assets_data: Vec<u8>,
assets_data: AssetsPayloadSource,
);
fn load(&self, asset_name: &str, slot: SlotRef) -> Result<HandleId, AssetLoadError>;
fn status(&self, handle: HandleId) -> LoadStatus;

View File

@ -1,6 +1,10 @@
use crate::asset::{AssetEntry, PreloadEntry};
use crate::syscalls::CapFlags;
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom};
use std::path::PathBuf;
use std::sync::Arc;
pub const ASSETS_PA_MAGIC: [u8; 4] = *b"ASPA";
pub const ASSETS_PA_SCHEMA_VERSION: u32 = 1;
@ -21,12 +25,12 @@ pub struct Cartridge {
pub entrypoint: String,
pub capabilities: CapFlags,
pub program: Vec<u8>,
pub assets: Vec<u8>,
pub assets: AssetsPayloadSource,
pub asset_table: Vec<AssetEntry>,
pub preload: Vec<PreloadEntry>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[derive(Debug, Clone)]
pub struct CartridgeDTO {
pub app_id: u32,
pub title: String,
@ -35,10 +39,8 @@ pub struct CartridgeDTO {
pub entrypoint: String,
pub capabilities: CapFlags,
pub program: Vec<u8>,
pub assets: Vec<u8>,
#[serde(default)]
pub assets: AssetsPayloadSource,
pub asset_table: Vec<AssetEntry>,
#[serde(default)]
pub preload: Vec<PreloadEntry>,
}
@ -59,6 +61,88 @@ impl From<CartridgeDTO> for Cartridge {
}
}
#[derive(Debug, Clone)]
pub enum AssetsPayloadSource {
Memory(Arc<[u8]>),
File(Arc<FileAssetsPayloadSource>),
}
impl AssetsPayloadSource {
pub fn empty() -> Self {
Self::Memory(Arc::<[u8]>::from(Vec::<u8>::new()))
}
pub fn from_bytes(bytes: Vec<u8>) -> Self {
Self::Memory(Arc::<[u8]>::from(bytes))
}
pub fn from_file(path: PathBuf, payload_offset: u64, payload_len: u64) -> Self {
Self::File(Arc::new(FileAssetsPayloadSource { path, payload_offset, payload_len }))
}
pub fn is_empty(&self) -> bool {
match self {
Self::Memory(bytes) => bytes.is_empty(),
Self::File(source) => source.payload_len == 0,
}
}
pub fn open_slice(&self, offset: u64, size: u64) -> io::Result<AssetsPayloadSlice> {
match self {
Self::Memory(bytes) => {
let start = usize::try_from(offset).map_err(|_| invalid_input("asset offset overflow"))?;
let len = usize::try_from(size).map_err(|_| invalid_input("asset size overflow"))?;
let end = start.checked_add(len).ok_or_else(|| invalid_input("asset range overflow"))?;
if end > bytes.len() {
return Err(invalid_input("asset range out of bounds"));
}
Ok(AssetsPayloadSlice::Memory { bytes: Arc::clone(bytes), start, len })
}
Self::File(source) => {
let end = offset.checked_add(size).ok_or_else(|| invalid_input("asset range overflow"))?;
if end > source.payload_len {
return Err(invalid_input("asset range out of bounds"));
}
Ok(AssetsPayloadSlice::File { source: Arc::clone(source), offset, size })
}
}
}
}
#[derive(Debug)]
pub struct FileAssetsPayloadSource {
pub path: PathBuf,
pub payload_offset: u64,
pub payload_len: u64,
}
#[derive(Debug, Clone)]
pub enum AssetsPayloadSlice {
Memory { bytes: Arc<[u8]>, start: usize, len: usize },
File { source: Arc<FileAssetsPayloadSource>, offset: u64, size: u64 },
}
impl AssetsPayloadSlice {
pub fn read_all(&self) -> io::Result<Vec<u8>> {
match self {
Self::Memory { bytes, start, len } => Ok(bytes[*start..*start + *len].to_vec()),
Self::File { source, offset, size } => {
let mut file = File::open(&source.path)?;
file.seek(SeekFrom::Start(source.payload_offset + offset))?;
let mut buffer = vec![0_u8; usize::try_from(*size).map_err(|_| invalid_input("asset size overflow"))?];
file.read_exact(&mut buffer)?;
Ok(buffer)
}
}
}
}
fn invalid_input(message: &'static str) -> io::Error {
io::Error::new(io::ErrorKind::InvalidInput, message)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AssetsPackPrelude {
pub magic: [u8; 4],

View File

@ -1,10 +1,12 @@
use crate::cartridge::{
ASSETS_PA_MAGIC, ASSETS_PA_PRELUDE_SIZE, ASSETS_PA_SCHEMA_VERSION, AssetsPackHeader,
AssetsPackPrelude, Capability, Cartridge, CartridgeDTO, CartridgeError, CartridgeManifest,
AssetsPackPrelude, AssetsPayloadSource, Capability, Cartridge, CartridgeDTO, CartridgeError,
CartridgeManifest,
};
use crate::syscalls::{CapFlags, caps};
use std::collections::HashSet;
use std::fs;
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
pub struct CartridgeLoader;
@ -59,15 +61,22 @@ impl DirectoryCartridgeLoader {
let assets_pa_path = path.join("assets.pa");
let (assets, asset_table, preload) = if assets_pa_path.exists() {
let assets_pa = fs::read(assets_pa_path).map_err(|_| CartridgeError::IoError)?;
let parsed = parse_assets_pack(&assets_pa)?;
(parsed.payload.to_vec(), parsed.header.asset_table, parsed.header.preload)
let parsed = parse_assets_pack(&assets_pa_path)?;
(
AssetsPayloadSource::from_file(
assets_pa_path.clone(),
parsed.payload_offset,
parsed.payload_len,
),
parsed.header.asset_table,
parsed.header.preload,
)
} else {
if capabilities & caps::ASSET != 0 {
return Err(CartridgeError::MissingAssets);
}
(Vec::new(), Vec::new(), Vec::new())
(AssetsPayloadSource::empty(), Vec::new(), Vec::new())
};
let dto = CartridgeDTO {
@ -120,37 +129,48 @@ fn normalize_capabilities(capabilities: &[Capability]) -> Result<CapFlags, Cartr
Ok(normalized)
}
struct ParsedAssetsPack<'a> {
struct ParsedAssetsPack {
header: AssetsPackHeader,
payload: &'a [u8],
payload_offset: u64,
payload_len: u64,
}
fn parse_assets_pack(bytes: &[u8]) -> Result<ParsedAssetsPack<'_>, CartridgeError> {
fn parse_assets_pack(path: &Path) -> Result<ParsedAssetsPack, CartridgeError> {
let mut file = fs::File::open(path).map_err(|_| CartridgeError::IoError)?;
let mut prelude_bytes = [0_u8; ASSETS_PA_PRELUDE_SIZE];
file.read_exact(&mut prelude_bytes).map_err(|_| CartridgeError::InvalidFormat)?;
let prelude =
AssetsPackPrelude::from_bytes(bytes).ok_or(CartridgeError::InvalidFormat)?;
AssetsPackPrelude::from_bytes(&prelude_bytes).ok_or(CartridgeError::InvalidFormat)?;
if prelude.magic != ASSETS_PA_MAGIC || prelude.schema_version != ASSETS_PA_SCHEMA_VERSION {
return Err(CartridgeError::InvalidFormat);
}
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(prelude.header_len as usize)
.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)?;
if payload_offset < header_start || header_end > bytes.len() || payload_offset > bytes.len() {
if payload_offset < header_start || header_end > file_len || payload_offset > file_len {
return Err(CartridgeError::InvalidFormat);
}
if header_end != payload_offset {
return Err(CartridgeError::InvalidFormat);
}
let header_bytes = &bytes[header_start..header_end];
file.seek(SeekFrom::Start(header_start as u64)).map_err(|_| CartridgeError::IoError)?;
let mut header_bytes = vec![0_u8; header_len];
file.read_exact(&mut header_bytes).map_err(|_| CartridgeError::InvalidFormat)?;
let header: AssetsPackHeader =
serde_json::from_slice(header_bytes).map_err(|_| CartridgeError::InvalidFormat)?;
serde_json::from_slice(&header_bytes).map_err(|_| CartridgeError::InvalidFormat)?;
let payload_len = u64::try_from(file_len - payload_offset).map_err(|_| CartridgeError::InvalidFormat)?;
Ok(ParsedAssetsPack { header, payload: &bytes[payload_offset..] })
Ok(ParsedAssetsPack { header, payload_offset: prelude.payload_offset, payload_len })
}
#[cfg(test)]
@ -366,7 +386,13 @@ mod tests {
let cartridge = DirectoryCartridgeLoader::load(dir.path()).expect("cartridge must load");
assert_eq!(cartridge.assets, payload);
let slice = cartridge
.assets
.open_slice(0, payload.len() as u64)
.expect("payload slice must open")
.read_all()
.expect("payload slice must read");
assert_eq!(slice, payload);
assert_eq!(cartridge.asset_table.len(), 1);
assert_eq!(cartridge.asset_table[0].asset_name, "tiles");
assert_eq!(cartridge.preload.len(), 1);

View File

@ -9,7 +9,7 @@ use prometeu_hal::AudioOpStatus;
use prometeu_hal::asset::{AssetEntry, AssetLoadError, AssetOpStatus, BankType, LoadStatus};
use prometeu_hal::GfxOpStatus;
use prometeu_hal::InputSignals;
use prometeu_hal::cartridge::Cartridge;
use prometeu_hal::cartridge::{AssetsPayloadSource, Cartridge};
use prometeu_hal::syscalls::caps;
use prometeu_vm::VmInitError;
use std::collections::HashMap;
@ -61,7 +61,7 @@ fn cartridge_with_program(program: Vec<u8>, capabilities: u64) -> Cartridge {
entrypoint: "".into(),
capabilities,
program,
assets: vec![],
assets: AssetsPayloadSource::empty(),
asset_table: vec![],
preload: vec![],
}
@ -632,7 +632,7 @@ fn tick_asset_load_invalid_slot_returns_status_and_zero_handle() {
hardware.assets.initialize_for_cartridge(
vec![test_tile_asset_entry("tile_asset", asset_data.len())],
vec![],
asset_data,
AssetsPayloadSource::from_bytes(asset_data),
);
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(
@ -668,7 +668,7 @@ fn tick_asset_load_kind_mismatch_returns_status_and_zero_handle() {
hardware.assets.initialize_for_cartridge(
vec![test_tile_asset_entry("tile_asset", asset_data.len())],
vec![],
asset_data,
AssetsPayloadSource::from_bytes(asset_data),
);
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(
@ -755,7 +755,7 @@ fn tick_asset_commit_invalid_transition_returns_status_not_crash() {
hardware.assets.initialize_for_cartridge(
vec![test_tile_asset_entry("tile_asset", asset_data.len())],
vec![],
asset_data,
AssetsPayloadSource::from_bytes(asset_data),
);
let handle = hardware
.assets
@ -819,7 +819,7 @@ fn tick_asset_cancel_invalid_transition_returns_status_not_crash() {
hardware.assets.initialize_for_cartridge(
vec![test_tile_asset_entry("tile_asset", asset_data.len())],
vec![],
asset_data,
AssetsPayloadSource::from_bytes(asset_data),
);
let handle = hardware
.assets
@ -1025,7 +1025,7 @@ fn tick_memcard_slot_roundtrip_for_game_profile() {
entrypoint: "".into(),
capabilities: caps::FS,
program,
assets: vec![],
assets: AssetsPayloadSource::empty(),
asset_table: vec![],
preload: vec![],
};
@ -1065,7 +1065,7 @@ fn tick_memcard_access_is_denied_for_non_game_profile() {
entrypoint: "".into(),
capabilities: caps::FS,
program,
assets: vec![],
assets: AssetsPayloadSource::empty(),
asset_table: vec![],
preload: vec![],
};