From 2d5777af191bd41bb2f74ada36703b0140e2b4d7 Mon Sep 17 00:00:00 2001 From: Nilton Constantino Date: Thu, 22 Jan 2026 10:08:20 +0000 Subject: [PATCH] added asset management --- crates/prometeu-core/src/firmware/firmware.rs | 3 + .../firmware/firmware_step_load_cartridge.rs | 7 + crates/prometeu-core/src/hardware/asset.rs | 429 ++++++++++++++++++ crates/prometeu-core/src/hardware/gfx.rs | 59 ++- crates/prometeu-core/src/hardware/hardware.rs | 17 +- .../src/hardware/memory_banks.rs | 24 + crates/prometeu-core/src/hardware/mod.rs | 10 + crates/prometeu-core/src/model/asset.rs | 185 ++++++++ crates/prometeu-core/src/model/cartridge.rs | 14 +- .../src/model/cartridge_loader.rs | 11 +- crates/prometeu-core/src/model/mod.rs | 2 + .../src/prometeu_os/prometeu_os.rs | 82 +++- .../prometeu-core/src/prometeu_os/syscalls.rs | 22 + .../src/virtual_machine/virtual_machine.rs | 3 + 14 files changed, 834 insertions(+), 34 deletions(-) create mode 100644 crates/prometeu-core/src/hardware/asset.rs create mode 100644 crates/prometeu-core/src/hardware/memory_banks.rs create mode 100644 crates/prometeu-core/src/model/asset.rs diff --git a/crates/prometeu-core/src/firmware/firmware.rs b/crates/prometeu-core/src/firmware/firmware.rs index a6d109af..2e2060b9 100644 --- a/crates/prometeu-core/src/firmware/firmware.rs +++ b/crates/prometeu-core/src/firmware/firmware.rs @@ -47,6 +47,9 @@ impl Firmware { /// This method is called exactly once per Host frame (60Hz). /// It updates peripheral signals and delegates the logic to the current state. pub fn step_frame(&mut self, signals: &InputSignals, hw: &mut dyn HardwareBridge) { + // 0. Process asset commits at the beginning of the frame boundary. + hw.assets_mut().apply_commits(); + // 1. Update peripheral state using the latest signals from the Host. // This ensures input is consistent throughout the entire update. hw.pad_mut().begin_frame(signals); diff --git a/crates/prometeu-core/src/firmware/firmware_step_load_cartridge.rs b/crates/prometeu-core/src/firmware/firmware_step_load_cartridge.rs index fd6918ab..5ac2b4ad 100644 --- a/crates/prometeu-core/src/firmware/firmware_step_load_cartridge.rs +++ b/crates/prometeu-core/src/firmware/firmware_step_load_cartridge.rs @@ -11,6 +11,13 @@ pub struct LoadCartridgeStep { impl LoadCartridgeStep { pub fn on_enter(&mut self, ctx: &mut PrometeuContext) { ctx.os.log(LogLevel::Info, LogSource::Pos, 0, format!("Loading cartridge: {}", self.cartridge.title)); + + // Initialize Asset Manager + ctx.hw.assets_mut().initialize_for_cartridge( + self.cartridge.asset_table.clone(), + self.cartridge.assets.clone() + ); + ctx.os.initialize_vm(ctx.vm, &self.cartridge); } diff --git a/crates/prometeu-core/src/hardware/asset.rs b/crates/prometeu-core/src/hardware/asset.rs new file mode 100644 index 00000000..9a53fd05 --- /dev/null +++ b/crates/prometeu-core/src/hardware/asset.rs @@ -0,0 +1,429 @@ +use std::collections::HashMap; +use std::sync::{Arc, RwLock, Mutex}; +use std::thread; +use crate::model::{AssetEntry, BankType, BankStats, LoadStatus, SlotRef, SlotStats, TileBank, TileSize, Color, HandleId}; +use crate::hardware::MemoryBanks; + +pub struct AssetManager { + assets: Arc>>, + handles: Arc>>, + next_handle_id: Mutex, + assets_data: Arc>>, + + pub memory_banks: Arc, + + // Commits that are ready to be applied at the next frame boundary. + pending_commits: Mutex>, +} + +struct LoadHandleInfo { + _asset_id: String, + slot: SlotRef, + status: LoadStatus, +} + +impl Default for AssetManager { + fn default() -> Self { + Self::new(vec![], vec![], Arc::new(MemoryBanks::new())) + } +} + +impl AssetManager { + pub fn new(assets: Vec, assets_data: Vec, memory_banks: Arc) -> Self { + let mut asset_map = HashMap::new(); + for entry in assets { + asset_map.insert(entry.asset_id.clone(), entry); + } + + Self { + assets: Arc::new(RwLock::new(asset_map)), + memory_banks, + handles: Arc::new(RwLock::new(HashMap::new())), + next_handle_id: Mutex::new(1), + assets_data: Arc::new(RwLock::new(assets_data)), + pending_commits: Mutex::new(Vec::new()), + } + } + + pub fn initialize_for_cartridge(&self, assets: Vec, assets_data: Vec) { + self.shutdown(); + let mut asset_map = self.assets.write().unwrap(); + asset_map.clear(); + for entry in assets { + asset_map.insert(entry.asset_id.clone(), entry); + } + *self.assets_data.write().unwrap() = assets_data; + } + + pub fn load(&self, asset_id: &str, slot: SlotRef) -> Result { + let entry = { + let assets = self.assets.read().unwrap(); + assets.get(asset_id).ok_or_else(|| format!("Asset not found: {}", asset_id))?.clone() + }; + + if slot.asset_type != entry.bank_type { + return Err("INCOMPATIBLE_SLOT_KIND".to_string()); + } + + let mut next_id = self.next_handle_id.lock().unwrap(); + let handle_id = *next_id; + *next_id += 1; + + // Check if already resident + if let Some(bank) = self.memory_banks.gfx.get_resident(asset_id) { + // Dedup: already resident + self.handles.write().unwrap().insert(handle_id, LoadHandleInfo { + _asset_id: asset_id.to_string(), + slot, + status: LoadStatus::READY, + }); + self.memory_banks.gfx.stage(handle_id, bank); + return Ok(handle_id); + } + + // Not resident, start loading + self.handles.write().unwrap().insert(handle_id, LoadHandleInfo { + _asset_id: asset_id.to_string(), + slot, + status: LoadStatus::PENDING, + }); + + let memory_banks = Arc::clone(&self.memory_banks); + let handles = self.handles.clone(); + let assets_data = self.assets_data.clone(); + let entry_clone = entry.clone(); + let asset_id_clone = asset_id.to_string(); + + thread::spawn(move || { + // Update status to LOADING + { + let mut handles_map = handles.write().unwrap(); + if let Some(h) = handles_map.get_mut(&handle_id) { + if h.status == LoadStatus::PENDING { + h.status = LoadStatus::LOADING; + } else { + // Might have been canceled + return; + } + } else { + return; + } + } + + // Perform IO and Decode + let result = Self::perform_load(&entry_clone, assets_data); + + match result { + Ok(tilebank) => { + let bank_arc = Arc::new(tilebank); + + // Insert or reuse a resident entry (dedup) + let resident_arc = memory_banks.gfx.put_resident(asset_id_clone, bank_arc, entry_clone.decoded_size as usize); + + // Add to staging + memory_banks.gfx.stage(handle_id, resident_arc); + + // Update status to READY + let mut handles_map = handles.write().unwrap(); + if let Some(h) = handles_map.get_mut(&handle_id) { + if h.status == LoadStatus::LOADING { + h.status = LoadStatus::READY; + } + } + } + Err(_) => { + let mut handles_map = handles.write().unwrap(); + if let Some(h) = handles_map.get_mut(&handle_id) { + h.status = LoadStatus::ERROR; + } + } + } + }); + + Ok(handle_id) + } + + fn perform_load(entry: &AssetEntry, assets_data: Arc>>) -> Result { + 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]; + + // 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 { + 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 mut palettes = [[Color::BLACK; 16]; 64]; + for p in 0..64 { + for c in 0..16 { + let offset = (p * 16 + c) * 2; + let color_raw = u16::from_le_bytes([palette_data[offset], palette_data[offset + 1]]); + palettes[p][c] = Color(color_raw); + } + } + + Ok(TileBank { + tile_size, + width, + height, + pixel_indices, + palettes, + }) + } + + pub fn status(&self, handle: HandleId) -> LoadStatus { + self.handles.read().unwrap().get(&handle).map(|h| h.status).unwrap_or(LoadStatus::ERROR) + } + + pub fn commit(&self, handle: HandleId) { + let mut handles_map = self.handles.write().unwrap(); + if let Some(h) = handles_map.get_mut(&handle) { + if h.status == LoadStatus::READY { + self.pending_commits.lock().unwrap().push(handle); + } + } + } + + pub fn cancel(&self, handle: HandleId) { + let mut handles_map = self.handles.write().unwrap(); + if let Some(h) = handles_map.get_mut(&handle) { + match h.status { + LoadStatus::PENDING | LoadStatus::LOADING | LoadStatus::READY => { + h.status = LoadStatus::CANCELED; + // We don't actually stop the worker thread if it's already LOADING, + // but we will ignore its result when it finishes. + } + _ => {} + } + } + self.memory_banks.gfx.take_staging(handle); + } + + /// Collects all pending commits and returns them. + /// This is called at the frame boundary to apply the changes to the hardware. + pub fn apply_commits(&self) { + let mut pending = self.pending_commits.lock().unwrap(); + let mut handles = self.handles.write().unwrap(); + + for handle_id in pending.drain(..) { + if let Some(h) = handles.get_mut(&handle_id) { + if h.status == LoadStatus::READY { + if let Some(bank) = self.memory_banks.gfx.take_staging(handle_id) { + if h.slot.asset_type == BankType::TILES { + self.memory_banks.gfx.install(h.slot.index, bank); + } + h.status = LoadStatus::COMMITTED; + } + } + } + } + } + + pub fn bank_info(&self, kind: BankType, _gfx_banks: &[Option>; 16]) -> BankStats { + match kind { + BankType::TILES => { + let mut used_bytes = 0; + { + let resident = self.memory_banks.gfx.resident.read().unwrap(); + for entry in resident.values() { + used_bytes += entry.bytes; + } + } + + let mut inflight_bytes = 0; + { + let staging = self.memory_banks.gfx.staging.read().unwrap(); + let assets = self.assets.read().unwrap(); + let handles = self.handles.read().unwrap(); + + // This is a bit complex because we need to map handle -> asset_id -> decoded_size + for (handle_id, _) in staging.iter() { + if let Some(h) = handles.get(handle_id) { + if let Some(entry) = assets.get(&h._asset_id) { + inflight_bytes += entry.decoded_size as usize; + } + } + } + } + + BankStats { + total_bytes: 16 * 1024 * 1024, // 16MB budget (arbitrary for now) + used_bytes, + free_bytes: (16usize * 1024 * 1024).saturating_sub(used_bytes), + inflight_bytes, + slot_count: 16, + } + } + } + } + + pub fn slot_info(&self, slot: SlotRef, gfx_banks: &[Option>; 16]) -> SlotStats { + match slot.asset_type { + BankType::TILES => { + if let Some(Some(bank)) = gfx_banks.get(slot.index) { + // We need asset_id. + // Let's find it in resident entries. + let resident = self.memory_banks.gfx.resident.read().unwrap(); + let (asset_id, bytes) = resident.iter() + .find(|(_, entry)| Arc::ptr_eq(&entry.value, bank)) + .map(|(id, entry)| (Some(id.clone()), entry.bytes)) + .unwrap_or((None, 0)); + + SlotStats { + asset_id, + generation: 0, // generation not yet implemented + resident_bytes: bytes, + } + } else { + SlotStats { + asset_id: None, + generation: 0, + resident_bytes: 0, + } + } + } + } + } + + pub fn shutdown(&self) { + self.memory_banks.gfx.resident.write().unwrap().clear(); + self.memory_banks.gfx.staging.write().unwrap().clear(); + self.handles.write().unwrap().clear(); + self.pending_commits.lock().unwrap().clear(); + // gfx_pool is cleared by Hardware when it owns Gfx + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::Instant; + + #[test] + fn test_asset_loading_flow() { + let banks = Arc::new(MemoryBanks::new()); + // Mock data for a 16x16 tilebank (256 pixels) + 2048 bytes of palette + let mut data = vec![1u8; 256]; // all pixel indices are 1 + data.extend_from_slice(&[0u8; 2048]); // all colors are BLACK (0,0) + + let asset_entry = AssetEntry { + asset_id: "test_tiles".to_string(), + bank_type: BankType::TILES, + offset: 0, + size: data.len() as u64, + decoded_size: data.len() as u64, + codec: "RAW".to_string(), + metadata: serde_json::json!({ + "tile_size": 16, + "width": 16, + "height": 16 + }), + }; + + let am = AssetManager::new(vec![asset_entry], data, Arc::clone(&banks)); + let slot = SlotRef::gfx(0); + + let handle = am.load("test_tiles", slot).expect("Should start loading"); + + // Wait for loading to finish (since it's a thread) + let mut status = am.status(handle); + let start = Instant::now(); + while status != LoadStatus::READY && start.elapsed().as_secs() < 5 { + thread::sleep(std::time::Duration::from_millis(10)); + status = am.status(handle); + } + + assert_eq!(status, LoadStatus::READY); + + // Check staging + { + let staging = am.memory_banks.gfx.staging.read().unwrap(); + assert!(staging.contains_key(&handle)); + } + + // Commit + am.commit(handle); + + const EMPTY_BANK: Option> = None; + let mut gfx_banks = [EMPTY_BANK; 16]; + am.apply_commits(); + + // Let's verify if it's installed in the shared pool + { + let pool = am.memory_banks.gfx.pool.read().unwrap(); + assert!(pool[0].is_some()); + gfx_banks[0] = pool[0].clone(); + } + + assert_eq!(am.status(handle), LoadStatus::COMMITTED); + assert!(gfx_banks[0].is_some()); + } + + #[test] + fn test_asset_dedup() { + let banks = Arc::new(MemoryBanks::new()); + let mut data = vec![1u8; 256]; + data.extend_from_slice(&[0u8; 2048]); + + let asset_entry = AssetEntry { + asset_id: "test_tiles".to_string(), + bank_type: BankType::TILES, + offset: 0, + size: data.len() as u64, + decoded_size: data.len() as u64, + codec: "RAW".to_string(), + metadata: serde_json::json!({ + "tile_size": 16, + "width": 16, + "height": 16 + }), + }; + + let am = AssetManager::new(vec![asset_entry], data, Arc::clone(&banks)); + + // Load once + let handle1 = am.load("test_tiles", SlotRef::gfx(0)).unwrap(); + let start = Instant::now(); + while am.status(handle1) != LoadStatus::READY && start.elapsed().as_secs() < 5 { + thread::sleep(std::time::Duration::from_millis(10)); + } + + // Load again into another slot + let handle2 = am.load("test_tiles", SlotRef::gfx(1)).unwrap(); + + // Second load should be READY immediately (or very fast) because of dedup + assert_eq!(am.status(handle2), LoadStatus::READY); + + // Check that both handles point to the same Arc + let staging = am.memory_banks.gfx.staging.read().unwrap(); + let bank1 = staging.get(&handle1).unwrap(); + let bank2 = staging.get(&handle2).unwrap(); + assert!(Arc::ptr_eq(bank1, bank2)); + } +} diff --git a/crates/prometeu-core/src/hardware/gfx.rs b/crates/prometeu-core/src/hardware/gfx.rs index 61381e85..d339240a 100644 --- a/crates/prometeu-core/src/hardware/gfx.rs +++ b/crates/prometeu-core/src/hardware/gfx.rs @@ -1,5 +1,7 @@ use crate::model::{Color, HudTileLayer, ScrollableTileLayer, Sprite, TileBank, TileMap, TileSize}; use std::mem::size_of; +use std::sync::Arc; +use crate::hardware::MemoryBanks; /// Blending modes inspired by classic 16-bit hardware. /// Defines how source pixels are combined with existing pixels in the framebuffer. @@ -46,8 +48,8 @@ pub struct Gfx { pub layers: [ScrollableTileLayer; 4], /// 1 fixed layer for User Interface. pub hud: HudTileLayer, - /// Up to 16 sets of graphical assets (tiles + palettes). - pub banks: [Option; 16], + /// Memory banks containing graphical assets. + pub memory_banks: Arc, /// Hardware sprites (Object Attribute Memory equivalent). pub sprites: [Sprite; 512], @@ -65,9 +67,8 @@ pub struct Gfx { } impl Gfx { - /// Initializes the graphics system with a specific resolution. - pub fn new(w: usize, h: usize) -> Self { - const EMPTY_BANK: Option = None; + /// Initializes the graphics system with a specific resolution and shared memory banks. + pub fn new(w: usize, h: usize, memory_banks: Arc) -> Self { const EMPTY_SPRITE: Sprite = Sprite { tile: crate::model::Tile { id: 0, flip_x: false, flip_y: false, palette_id: 0 }, x: 0, @@ -94,7 +95,7 @@ impl Gfx { back: vec![0; len], layers, hud: HudTileLayer::new(64, 32), - banks: [EMPTY_BANK; 16], + memory_banks, sprites: [EMPTY_SPRITE; 512], scene_fade_level: 31, scene_fade_color: Color::BLACK, @@ -327,26 +328,29 @@ impl Gfx { } } + let pool_guard = self.memory_banks.gfx.pool.read().unwrap(); + let pool = &*pool_guard; + // 1. Priority 0 sprites: drawn at the very back, behind everything else. - Self::draw_bucket_on_buffer(&mut self.back, self.w, self.h, &self.priority_buckets[0], &self.sprites, &self.banks); + Self::draw_bucket_on_buffer(&mut self.back, self.w, self.h, &self.priority_buckets[0], &self.sprites, pool); // 2. Main layers and prioritized sprites. // Order: Layer 0 -> Sprites 1 -> Layer 1 -> Sprites 2 ... for i in 0..self.layers.len() { let bank_id = self.layers[i].bank_id as usize; - if let Some(Some(bank)) = self.banks.get(bank_id) { + if let Some(Some(bank)) = pool.get(bank_id) { Self::draw_tile_map(&mut self.back, self.w, self.h, &self.layers[i].map, bank, self.layers[i].scroll_x, self.layers[i].scroll_y); } // Draw sprites that belong to this depth level - Self::draw_bucket_on_buffer(&mut self.back, self.w, self.h, &self.priority_buckets[i + 1], &self.sprites, &self.banks); + Self::draw_bucket_on_buffer(&mut self.back, self.w, self.h, &self.priority_buckets[i + 1], &self.sprites, pool); } // 4. Scene Fade: Applies a color blend to the entire world (excluding HUD). Self::apply_fade_to_buffer(&mut self.back, self.scene_fade_level, self.scene_fade_color); // 5. HUD: The fixed interface layer, always drawn on top of the world. - self.render_hud(); + Self::render_hud_with_pool(&mut self.back, self.w, self.h, &self.hud, pool); // 6. HUD Fade: Independent fade effect for the UI. Self::apply_fade_to_buffer(&mut self.back, self.hud_fade_level, self.hud_fade_color); @@ -360,7 +364,8 @@ impl Gfx { let scroll_x = self.layers[layer_idx].scroll_x; let scroll_y = self.layers[layer_idx].scroll_y; - let bank = match self.banks.get(bank_id) { + let pool = self.memory_banks.gfx.pool.read().unwrap(); + let bank = match pool.get(bank_id) { Some(Some(b)) => b, _ => return, }; @@ -370,13 +375,18 @@ impl Gfx { /// Renders the HUD (fixed position, no scroll). pub fn render_hud(&mut self) { - let bank_id = self.hud.bank_id as usize; - let bank = match self.banks.get(bank_id) { + let pool = self.memory_banks.gfx.pool.read().unwrap(); + Self::render_hud_with_pool(&mut self.back, self.w, self.h, &self.hud, &*pool); + } + + fn render_hud_with_pool(back: &mut [u16], w: usize, h: usize, hud: &HudTileLayer, pool: &[Option>; 16]) { + let bank_id = hud.bank_id as usize; + let bank = match pool.get(bank_id) { Some(Some(b)) => b, _ => return, }; - Self::draw_tile_map(&mut self.back, self.w, self.h, &self.hud.map, bank, 0, 0); + Self::draw_tile_map(back, w, h, &hud.map, bank, 0, 0); } /// Rasterizes a TileMap into the provided pixel buffer using scrolling. @@ -464,7 +474,7 @@ impl Gfx { screen_h: usize, bucket: &[usize], sprites: &[Sprite], - banks: &[Option], + banks: &[Option>], ) { for &idx in bucket { let s = &sprites[idx]; @@ -554,7 +564,8 @@ impl Gfx { total += self.hud.map.tiles.len() * size_of::(); // 4. Tile Banks (Assets and Palettes) - for bank_opt in &self.banks { + let pool = self.memory_banks.gfx.pool.read().unwrap(); + for bank_opt in pool.iter() { if let Some(bank) = bank_opt { total += size_of::(); total += bank.pixel_indices.len(); @@ -647,10 +658,12 @@ impl Gfx { #[cfg(test)] mod tests { use super::*; + use crate::hardware::MemoryBanks; #[test] fn test_draw_pixel() { - let mut gfx = Gfx::new(10, 10); + let banks = Arc::new(MemoryBanks::new()); + let mut gfx = Gfx::new(10, 10, banks); gfx.draw_pixel(5, 5, Color::WHITE); assert_eq!(gfx.back[5 * 10 + 5], Color::WHITE.0); @@ -661,7 +674,8 @@ mod tests { #[test] fn test_draw_line() { - let mut gfx = Gfx::new(10, 10); + let banks = Arc::new(MemoryBanks::new()); + let mut gfx = Gfx::new(10, 10, banks); gfx.draw_line(0, 0, 9, 9, Color::WHITE); assert_eq!(gfx.back[0], Color::WHITE.0); assert_eq!(gfx.back[9 * 10 + 9], Color::WHITE.0); @@ -669,7 +683,8 @@ mod tests { #[test] fn test_draw_rect() { - let mut gfx = Gfx::new(10, 10); + let banks = Arc::new(MemoryBanks::new()); + let mut gfx = Gfx::new(10, 10, banks); gfx.draw_rect(0, 0, 10, 10, Color::WHITE); assert_eq!(gfx.back[0], Color::WHITE.0); assert_eq!(gfx.back[9], Color::WHITE.0); @@ -679,14 +694,16 @@ mod tests { #[test] fn test_fill_circle() { - let mut gfx = Gfx::new(10, 10); + let banks = Arc::new(MemoryBanks::new()); + let mut gfx = Gfx::new(10, 10, banks); gfx.fill_circle(5, 5, 2, Color::WHITE); assert_eq!(gfx.back[5 * 10 + 5], Color::WHITE.0); } #[test] fn test_draw_square() { - let mut gfx = Gfx::new(10, 10); + let banks = Arc::new(MemoryBanks::new()); + let mut gfx = Gfx::new(10, 10, banks); gfx.draw_square(2, 2, 6, 6, Color::WHITE, Color::BLACK); // Border assert_eq!(gfx.back[2 * 10 + 2], Color::WHITE.0); diff --git a/crates/prometeu-core/src/hardware/hardware.rs b/crates/prometeu-core/src/hardware/hardware.rs index 8495c9cf..7f519a39 100644 --- a/crates/prometeu-core/src/hardware/hardware.rs +++ b/crates/prometeu-core/src/hardware/hardware.rs @@ -1,10 +1,13 @@ -use crate::hardware::{Audio, Gfx, HardwareBridge, Pad, Touch}; +use crate::hardware::{AssetManager, Audio, Gfx, HardwareBridge, Pad, Touch, MemoryBanks}; +use std::sync::Arc; /// Aggregate structure for all virtual hardware peripherals. /// /// This struct represents the "Mainboard" of the PROMETEU console, /// containing instances of GFX, Audio, Input (Pad), and Touch. pub struct Hardware { + /// Shared memory banks for hardware assets. + pub memory_banks: Arc, /// The Graphics Processing Unit. pub gfx: Gfx, /// The Sound Processing Unit. @@ -13,6 +16,8 @@ pub struct Hardware { pub pad: Pad, /// The absolute pointer input device. pub touch: Touch, + /// The Asset Management system. + pub assets: AssetManager, } impl HardwareBridge for Hardware { @@ -27,6 +32,11 @@ impl HardwareBridge for Hardware { fn touch(&self) -> &Touch { &self.touch } fn touch_mut(&mut self) -> &mut Touch { &mut self.touch } + + fn assets(&self) -> &AssetManager { &self.assets } + fn assets_mut(&mut self) -> &mut AssetManager { &mut self.assets } + + fn memory_banks(&self) -> &MemoryBanks { &self.memory_banks } } impl Hardware { @@ -37,11 +47,14 @@ impl Hardware { /// Creates a fresh hardware instance with default settings. pub fn new() -> Self { + let memory_banks = Arc::new(MemoryBanks::new()); Self { - gfx: Gfx::new(Self::W, Self::H), + memory_banks: Arc::clone(&memory_banks), + gfx: Gfx::new(Self::W, Self::H, Arc::clone(&memory_banks)), audio: Audio::new(), pad: Pad::default(), touch: Touch::default(), + assets: AssetManager::new(vec![], vec![], Arc::clone(&memory_banks)), } } } diff --git a/crates/prometeu-core/src/hardware/memory_banks.rs b/crates/prometeu-core/src/hardware/memory_banks.rs new file mode 100644 index 00000000..a153fbb9 --- /dev/null +++ b/crates/prometeu-core/src/hardware/memory_banks.rs @@ -0,0 +1,24 @@ +use crate::model::{Bank, TileBank}; + +/// Centralized container for all hardware memory banks. +/// +/// This structure owns the actual residency pools, staging areas, and +/// deduplication tables for different types of hardware assets. +/// It is shared between the AssetManager (writer) and hardware +/// consumers like Gfx (reader). +pub struct MemoryBanks { + /// Graphical tile banks. + pub gfx: Bank, + // In the future, add other banks here: + // pub audio: Bank, + // pub blobs: Bank, +} + +impl MemoryBanks { + /// Creates a new, empty set of memory banks. + pub fn new() -> Self { + Self { + gfx: Bank::new(), + } + } +} diff --git a/crates/prometeu-core/src/hardware/mod.rs b/crates/prometeu-core/src/hardware/mod.rs index e530dc0a..6e3e8f7c 100644 --- a/crates/prometeu-core/src/hardware/mod.rs +++ b/crates/prometeu-core/src/hardware/mod.rs @@ -1,16 +1,21 @@ +mod asset; mod gfx; mod pad; mod touch; mod input_signal; mod audio; +mod memory_banks; pub mod hardware; +pub use asset::AssetManager; +pub use crate::model::HandleId; pub use gfx::Gfx; pub use gfx::BlendMode; pub use input_signal::InputSignals; pub use pad::Pad; pub use touch::Touch; pub use audio::{Audio, AudioCommand, Channel, LoopMode, MAX_CHANNELS, OUTPUT_SAMPLE_RATE}; +pub use memory_banks::MemoryBanks; pub trait HardwareBridge { fn gfx(&self) -> &Gfx; @@ -24,4 +29,9 @@ pub trait HardwareBridge { fn touch(&self) -> &Touch; fn touch_mut(&mut self) -> &mut Touch; + + fn assets(&self) -> &AssetManager; + fn assets_mut(&mut self) -> &mut AssetManager; + + fn memory_banks(&self) -> &MemoryBanks; } diff --git a/crates/prometeu-core/src/model/asset.rs b/crates/prometeu-core/src/model/asset.rs new file mode 100644 index 00000000..3c48069c --- /dev/null +++ b/crates/prometeu-core/src/model/asset.rs @@ -0,0 +1,185 @@ +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; +use std::time::Instant; +use serde::{Deserialize, Serialize}; + +pub type HandleId = u32; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)] +#[allow(non_camel_case_types)] +pub enum BankType { + TILES, // TILE_BANK + // SOUNDS, + // TILEMAPS, + // BLOBS, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct AssetEntry { + pub asset_id: String, + pub bank_type: BankType, + pub offset: u64, + pub size: u64, + pub decoded_size: u64, + pub codec: String, // e.g., "RAW" + pub metadata: serde_json::Value, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum LoadStatus { + PENDING, + LOADING, + READY, + COMMITTED, + CANCELED, + ERROR, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BankStats { + pub total_bytes: usize, + pub used_bytes: usize, + pub free_bytes: usize, + pub inflight_bytes: usize, + pub slot_count: usize, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SlotStats { + pub asset_id: Option, + pub generation: u32, + pub resident_bytes: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct SlotRef { + pub asset_type: BankType, + pub index: usize, +} + +impl SlotRef { + pub fn gfx(index: usize) -> Self { + Self { + asset_type: BankType::TILES, + index, + } + } +} + +#[derive(Debug)] +pub struct ResidentEntry { + /// The resident, materialized object. + pub value: Arc, + + /// Resident size in bytes (post-decode). Used for telemetry/budgets. + pub bytes: usize, + + /// Pin count (optional): if > 0, entry should not be evicted by policy. + pub pins: u32, + + /// Telemetry / profiling fields (optional but useful). + pub loads: u64, + pub last_used: Instant, +} + +impl ResidentEntry { + pub fn new(value: Arc, bytes: usize) -> Self { + Self { + value, + bytes, + pins: 0, + loads: 1, + last_used: Instant::now(), + } + } +} + +pub struct Bank { + /// Dedup table: asset_id -> resident entry (value + telemetry). + pub resident: Arc>>>, + + /// Slot pool: hardware-visible residency pointers. + pub pool: Arc>; S]>>, + + /// Staging area: handle -> value ready to commit. + pub staging: Arc>>>, +} + +impl Bank { + pub fn new() -> Self { + Self { + resident: Arc::new(RwLock::new(HashMap::new())), + pool: Arc::new(RwLock::new(std::array::from_fn(|_| None))), + staging: Arc::new(RwLock::new(HashMap::new())), + } + } + + /// Try get a resident value by asset_id (dedupe path). + pub fn get_resident(&self, asset_id: &str) -> Option> { + let mut map = self.resident.write().unwrap(); + let entry = map.get_mut(asset_id)?; + entry.last_used = Instant::now(); + Some(Arc::clone(&entry.value)) + } + + /// Insert or reuse a resident entry. Returns the resident Arc. + /// - If already resident, updates telemetry and returns existing value. + /// - If new, inserts ResidentEntry and returns inserted value. + pub fn put_resident(&self, asset_id: String, value: Arc, bytes: usize) -> Arc { + let mut map = self.resident.write().unwrap(); + match map.get_mut(&asset_id) { + Some(existing) => { + existing.last_used = Instant::now(); + existing.loads += 1; + Arc::clone(&existing.value) + } + None => { + let entry = ResidentEntry::new(Arc::clone(&value), bytes); + map.insert(asset_id, entry); + value + } + } + } + + /// Place a value into staging for a given handle. + pub fn stage(&self, handle: HandleId, value: Arc) { + self.staging.write().unwrap().insert(handle, value); + } + + /// Take staged value (used by commit path). + pub fn take_staging(&self, handle: HandleId) -> Option> { + self.staging.write().unwrap().remove(&handle) + } + + /// Install (commit) a value into a slot (pointer swap). + pub fn install(&self, slot: usize, value: Arc) { + let mut pool = self.pool.write().unwrap(); + if slot < S { + pool[slot] = Some(value); + } + } + + /// Read current slot value (if any). + pub fn slot_current(&self, slot: usize) -> Option> { + if slot < S { + self.pool.read().unwrap()[slot].as_ref().map(Arc::clone) + } else { + None + } + } + + /// Optional: pin/unpin API (future eviction policy support). + pub fn pin(&self, asset_id: &str) { + if let Some(e) = self.resident.write().unwrap().get_mut(asset_id) { + e.pins = e.pins.saturating_add(1); + e.last_used = Instant::now(); + } + } + + pub fn unpin(&self, asset_id: &str) { + if let Some(e) = self.resident.write().unwrap().get_mut(asset_id) { + e.pins = e.pins.saturating_sub(1); + e.last_used = Instant::now(); + } + } +} diff --git a/crates/prometeu-core/src/model/cartridge.rs b/crates/prometeu-core/src/model/cartridge.rs index f45a8793..c4735a67 100644 --- a/crates/prometeu-core/src/model/cartridge.rs +++ b/crates/prometeu-core/src/model/cartridge.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use crate::model::asset::AssetEntry; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] @@ -15,7 +15,8 @@ pub struct Cartridge { pub app_mode: AppMode, pub entrypoint: String, pub program: Vec, - pub assets_path: Option, + pub assets: Vec, + pub asset_table: Vec, } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -26,7 +27,9 @@ pub struct CartridgeDTO { pub app_mode: AppMode, pub entrypoint: String, pub program: Vec, - pub assets_path: Option, + pub assets: Vec, + #[serde(default)] + pub asset_table: Vec, } impl From for Cartridge { @@ -38,7 +41,8 @@ impl From for Cartridge { app_mode: dto.app_mode, entrypoint: dto.entrypoint, program: dto.program, - assets_path: dto.assets_path, + assets: dto.assets, + asset_table: dto.asset_table, } } } @@ -62,4 +66,6 @@ pub struct CartridgeManifest { pub app_version: String, pub app_mode: AppMode, pub entrypoint: String, + #[serde(default)] + pub asset_table: Vec, } diff --git a/crates/prometeu-core/src/model/cartridge_loader.rs b/crates/prometeu-core/src/model/cartridge_loader.rs index fec55501..6e858eee 100644 --- a/crates/prometeu-core/src/model/cartridge_loader.rs +++ b/crates/prometeu-core/src/model/cartridge_loader.rs @@ -48,11 +48,11 @@ impl DirectoryCartridgeLoader { let program = fs::read(program_path).map_err(|_| CartridgeError::IoError)?; - let assets_path = path.join("assets"); - let assets_path = if assets_path.exists() && assets_path.is_dir() { - Some(assets_path) + let assets_pa_path = path.join("assets.pa"); + let assets = if assets_pa_path.exists() { + fs::read(assets_pa_path).map_err(|_| CartridgeError::IoError)? } else { - None + Vec::new() }; let dto = CartridgeDTO { @@ -62,7 +62,8 @@ impl DirectoryCartridgeLoader { app_mode: manifest.app_mode, entrypoint: manifest.entrypoint, program, - assets_path, + assets, + asset_table: manifest.asset_table, }; Ok(Cartridge::from(dto)) diff --git a/crates/prometeu-core/src/model/mod.rs b/crates/prometeu-core/src/model/mod.rs index 8eb4733b..50505054 100644 --- a/crates/prometeu-core/src/model/mod.rs +++ b/crates/prometeu-core/src/model/mod.rs @@ -1,3 +1,4 @@ +mod asset; mod color; mod button; mod tile; @@ -9,6 +10,7 @@ mod cartridge; mod cartridge_loader; mod window; +pub use asset::{AssetEntry, BankType, BankStats, LoadStatus, SlotRef, SlotStats, HandleId, Bank, ResidentEntry}; pub use button::{Button, ButtonId}; pub use cartridge::{AppMode, Cartridge, CartridgeDTO, CartridgeError}; pub use cartridge_loader::{CartridgeLoader, DirectoryCartridgeLoader, PackedCartridgeLoader}; diff --git a/crates/prometeu-core/src/prometeu_os/prometeu_os.rs b/crates/prometeu-core/src/prometeu_os/prometeu_os.rs index 7680268c..a85c8dde 100644 --- a/crates/prometeu-core/src/prometeu_os/prometeu_os.rs +++ b/crates/prometeu-core/src/prometeu_os/prometeu_os.rs @@ -410,7 +410,8 @@ mod tests { app_mode: AppMode::Game, entrypoint: "0".to_string(), program: rom, - assets_path: None, + assets: vec![], + asset_table: vec![], }; os.initialize_vm(&mut vm, &cartridge); @@ -450,7 +451,8 @@ mod tests { app_mode: AppMode::Game, entrypoint: "0".to_string(), program: rom, - assets_path: None, + assets: vec![], + asset_table: vec![], }; os.initialize_vm(&mut vm, &cartridge); @@ -860,6 +862,82 @@ impl NativeInterface for PrometeuOS { let level = vm.pop_integer()?; self.syscall_log_write(vm, level, tag, msg) } + + // --- Asset Syscalls --- + Syscall::AssetLoad => { + let asset_id = match vm.pop()? { + Value::String(s) => s, + _ => return Err("Expected string asset_id".into()), + }; + let asset_type_val = vm.pop_integer()? as u32; + let slot_index = vm.pop_integer()? as usize; + + let asset_type = match asset_type_val { + 0 => crate::model::BankType::TILES, + _ => return Err("Invalid asset type".to_string()), + }; + let slot = crate::model::SlotRef { asset_type, index: slot_index }; + + match hw.assets().load(&asset_id, slot) { + Ok(handle) => { + vm.push(Value::Int64(handle as i64)); + Ok(1000) + } + Err(e) => Err(e), + } + } + Syscall::AssetStatus => { + let handle = vm.pop_integer()? as u32; + let status = hw.assets().status(handle); + let status_val = match status { + crate::model::LoadStatus::PENDING => 0, + crate::model::LoadStatus::LOADING => 1, + crate::model::LoadStatus::READY => 2, + crate::model::LoadStatus::COMMITTED => 3, + crate::model::LoadStatus::CANCELED => 4, + crate::model::LoadStatus::ERROR => 5, + }; + vm.push(Value::Int64(status_val)); + Ok(100) + } + Syscall::AssetCommit => { + let handle = vm.pop_integer()? as u32; + hw.assets().commit(handle); + vm.push(Value::Null); + Ok(100) + } + Syscall::AssetCancel => { + let handle = vm.pop_integer()? as u32; + hw.assets().cancel(handle); + vm.push(Value::Null); + Ok(100) + } + Syscall::BankInfo => { + let asset_type_val = vm.pop_integer()? as u32; + let asset_type = match asset_type_val { + 0 => crate::model::BankType::TILES, + _ => return Err("Invalid asset type".to_string()), + }; + let pool = hw.memory_banks().gfx.pool.read().unwrap(); + let info = hw.assets().bank_info(asset_type, &*pool); + let json = serde_json::to_string(&info).unwrap_or_default(); + vm.push(Value::String(json)); + Ok(500) + } + Syscall::BankSlotInfo => { + let slot_index = vm.pop_integer()? as usize; + let asset_type_val = vm.pop_integer()? as u32; + let asset_type = match asset_type_val { + 0 => crate::model::BankType::TILES, + _ => return Err("Invalid asset type".to_string()), + }; + let slot = crate::model::SlotRef { asset_type, index: slot_index }; + let pool = hw.memory_banks().gfx.pool.read().unwrap(); + let info = hw.assets().slot_info(slot, &*pool); + let json = serde_json::to_string(&info).unwrap_or_default(); + vm.push(Value::String(json)); + Ok(500) + } } } } \ No newline at end of file diff --git a/crates/prometeu-core/src/prometeu_os/syscalls.rs b/crates/prometeu-core/src/prometeu_os/syscalls.rs index f5be87c4..67187455 100644 --- a/crates/prometeu-core/src/prometeu_os/syscalls.rs +++ b/crates/prometeu-core/src/prometeu_os/syscalls.rs @@ -41,6 +41,16 @@ pub enum Syscall { // Log LogWrite = 0x5001, LogWriteTag = 0x5002, + + // Asset + AssetLoad = 0x6001, + AssetStatus = 0x6002, + AssetCommit = 0x6003, + AssetCancel = 0x6004, + + // Bank + BankInfo = 0x6101, + BankSlotInfo = 0x6102, } impl Syscall { @@ -74,6 +84,12 @@ impl Syscall { 0x4007 => Some(Self::FsDelete), 0x5001 => Some(Self::LogWrite), 0x5002 => Some(Self::LogWriteTag), + 0x6001 => Some(Self::AssetLoad), + 0x6002 => Some(Self::AssetStatus), + 0x6003 => Some(Self::AssetCommit), + 0x6004 => Some(Self::AssetCancel), + 0x6101 => Some(Self::BankInfo), + 0x6102 => Some(Self::BankSlotInfo), _ => None, } } @@ -140,6 +156,12 @@ impl Syscall { "fs.delete" => Some(Self::FsDelete), "log.write" => Some(Self::LogWrite), "log.writeTag" | "log.write_tag" => Some(Self::LogWriteTag), + "asset.load" => Some(Self::AssetLoad), + "asset.status" => Some(Self::AssetStatus), + "asset.commit" => Some(Self::AssetCommit), + "asset.cancel" => Some(Self::AssetCancel), + "bank.info" => Some(Self::BankInfo), + "bank.slotInfo" | "bank.slot_info" => Some(Self::BankSlotInfo), _ => { None } diff --git a/crates/prometeu-core/src/virtual_machine/virtual_machine.rs b/crates/prometeu-core/src/virtual_machine/virtual_machine.rs index fc25ced0..d823736c 100644 --- a/crates/prometeu-core/src/virtual_machine/virtual_machine.rs +++ b/crates/prometeu-core/src/virtual_machine/virtual_machine.rs @@ -709,6 +709,9 @@ mod tests { fn pad_mut(&mut self) -> &mut crate::hardware::Pad { todo!() } fn touch(&self) -> &crate::hardware::Touch { todo!() } fn touch_mut(&mut self) -> &mut crate::hardware::Touch { todo!() } + fn assets(&self) -> &crate::hardware::AssetManager { todo!() } + fn assets_mut(&mut self) -> &mut crate::hardware::AssetManager { todo!() } + fn memory_banks(&self) -> &crate::hardware::MemoryBanks { todo!() } } #[test]