implements PR-011d asset manager on-demand payload and op mode

This commit is contained in:
bQUARKz 2026-03-11 06:50:12 +00:00
parent 077378a3b5
commit 4b31248dff
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
2 changed files with 201 additions and 21 deletions

View File

@ -11,6 +11,7 @@ use prometeu_hal::sample::Sample;
use prometeu_hal::sound_bank::SoundBank;
use prometeu_hal::tile_bank::{TileBank, TileSize};
use std::collections::HashMap;
use std::io::Read;
use std::sync::{Arc, Mutex, RwLock};
use std::thread;
use std::time::Instant;
@ -132,6 +133,12 @@ struct LoadHandleInfo {
status: LoadStatus,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AssetOpMode {
DirectFromSlice,
StageInMemory,
}
impl AssetBridge for AssetManager {
fn initialize_for_cartridge(
&self,
@ -171,6 +178,14 @@ impl AssetBridge for AssetManager {
}
impl AssetManager {
fn op_mode_for(entry: &AssetEntry) -> Result<AssetOpMode, String> {
match (entry.bank_type, entry.codec.as_str()) {
(BankType::TILES, "RAW") => Ok(AssetOpMode::StageInMemory),
(BankType::SOUNDS, "RAW") => Ok(AssetOpMode::DirectFromSlice),
_ => Err(format!("Unsupported codec: {}", entry.codec)),
}
}
pub fn new(
assets: Vec<AssetEntry>,
assets_data: AssetsPayloadSource,
@ -453,19 +468,27 @@ impl AssetManager {
entry: &AssetEntry,
assets_data: Arc<RwLock<AssetsPayloadSource>>,
) -> Result<TileBank, String> {
if entry.codec != "RAW" {
return Err(format!("Unsupported codec: {}", entry.codec));
}
let buffer = {
let op_mode = Self::op_mode_for(entry)?;
let slice = {
let assets_data = assets_data.read().unwrap();
assets_data
.open_slice(entry.offset, entry.size)
.map_err(|_| "Asset offset/size out of bounds".to_string())?
.read_all()
.map_err(|_| "Asset payload read failed".to_string())?
};
match op_mode {
AssetOpMode::StageInMemory => {
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())?;
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")?;
@ -502,23 +525,69 @@ impl AssetManager {
Ok(TileBank { tile_size, width, height, pixel_indices, palettes })
}
fn decode_tile_bank_from_reader(
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 = 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;
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; 2048];
reader.read_exact(&mut palette_data).map_err(|_| "Buffer too small for TILEBANK".to_string())?;
let mut palettes = [[Color::BLACK; 16]; 64];
for (p, pal) in palettes.iter_mut().enumerate() {
for (c, slot) in pal.iter_mut().enumerate() {
let offset = (p * 16 + c) * 2;
let color_raw =
u16::from_le_bytes([palette_data[offset], palette_data[offset + 1]]);
*slot = Color(color_raw);
}
}
Ok(TileBank { tile_size, width, height, pixel_indices, palettes })
}
fn perform_load_sound_bank(
entry: &AssetEntry,
assets_data: Arc<RwLock<AssetsPayloadSource>>,
) -> Result<SoundBank, String> {
if entry.codec != "RAW" {
return Err(format!("Unsupported codec: {}", entry.codec));
}
let buffer = {
let op_mode = Self::op_mode_for(entry)?;
let slice = {
let assets_data = assets_data.read().unwrap();
assets_data
.open_slice(entry.offset, entry.size)
.map_err(|_| "Asset offset/size out of bounds".to_string())?
.read_all()
.map_err(|_| "Asset payload read failed".to_string())?
};
match op_mode {
AssetOpMode::DirectFromSlice => {
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())?;
Self::decode_sound_bank_from_buffer(entry, &buffer)
}
}
}
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;
@ -533,6 +602,15 @@ impl AssetManager {
Ok(SoundBank::new(vec![sample]))
}
fn decode_sound_bank_from_reader(
entry: &AssetEntry,
reader: &mut impl Read,
) -> Result<SoundBank, String> {
let mut raw = Vec::new();
reader.read_to_end(&mut raw).map_err(|_| "Asset payload read failed".to_string())?;
Self::decode_sound_bank_from_buffer(entry, &raw)
}
pub fn status(&self, handle: HandleId) -> LoadStatus {
self.handles
.read()
@ -806,6 +884,31 @@ mod tests {
}
}
#[test]
fn test_op_mode_for_tiles_raw_stages_in_memory() {
let entry = test_tile_asset_entry("tiles", test_tile_asset_data().len());
assert_eq!(AssetManager::op_mode_for(&entry), Ok(AssetOpMode::StageInMemory));
}
#[test]
fn test_op_mode_for_sounds_raw_reads_direct_from_slice() {
let entry = AssetEntry {
asset_id: 1,
asset_name: "sound".to_string(),
bank_type: BankType::SOUNDS,
offset: 0,
size: 8,
decoded_size: 8,
codec: "RAW".to_string(),
metadata: serde_json::json!({
"sample_rate": 44100
}),
};
assert_eq!(AssetManager::op_mode_for(&entry), Ok(AssetOpMode::DirectFromSlice));
}
#[test]
fn test_asset_loading_flow() {
let banks = Arc::new(MemoryBanks::new());

View File

@ -2,7 +2,7 @@ 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::io::{self, Cursor, Read, Seek, SeekFrom};
use std::path::PathBuf;
use std::sync::Arc;
@ -125,18 +125,95 @@ pub enum AssetsPayloadSlice {
}
impl AssetsPayloadSlice {
pub fn read_all(&self) -> io::Result<Vec<u8>> {
pub fn open_reader(&self) -> io::Result<AssetsPayloadReader> {
match self {
Self::Memory { bytes, start, len } => Ok(bytes[*start..*start + *len].to_vec()),
Self::Memory { bytes, start, len } => {
let data = Arc::<[u8]>::from(bytes[*start..*start + *len].to_vec());
Ok(AssetsPayloadReader::Memory(Cursor::new(data)))
}
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)
let absolute_start = source.payload_offset + offset;
file.seek(SeekFrom::Start(absolute_start))?;
Ok(AssetsPayloadReader::File(FileSliceReader {
file,
start: absolute_start,
len: *size,
position: 0,
}))
}
}
}
pub fn read_all(&self) -> io::Result<Vec<u8>> {
let mut reader = self.open_reader()?;
let mut buffer = Vec::new();
reader.read_to_end(&mut buffer)?;
Ok(buffer)
}
}
pub enum AssetsPayloadReader {
Memory(Cursor<Arc<[u8]>>),
File(FileSliceReader),
}
impl Read for AssetsPayloadReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self {
Self::Memory(reader) => reader.read(buf),
Self::File(reader) => reader.read(buf),
}
}
}
impl Seek for AssetsPayloadReader {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
match self {
Self::Memory(reader) => reader.seek(pos),
Self::File(reader) => reader.seek(pos),
}
}
}
pub struct FileSliceReader {
file: File,
start: u64,
len: u64,
position: u64,
}
impl Read for FileSliceReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if self.position >= self.len {
return Ok(0);
}
let remaining = (self.len - self.position) as usize;
let to_read = remaining.min(buf.len());
let read = self.file.read(&mut buf[..to_read])?;
self.position += read as u64;
Ok(read)
}
}
impl Seek for FileSliceReader {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
let next = match pos {
SeekFrom::Start(offset) => offset as i128,
SeekFrom::Current(delta) => self.position as i128 + delta as i128,
SeekFrom::End(delta) => self.len as i128 + delta as i128,
};
if next < 0 || next as u64 > self.len {
return Err(invalid_input("slice seek out of bounds"));
}
let next = next as u64;
self.file.seek(SeekFrom::Start(self.start + next))?;
self.position = next;
Ok(self.position)
}
}
fn invalid_input(message: &'static str) -> io::Error {