added asset management
This commit is contained in:
parent
29e557b655
commit
2d5777af19
@ -47,6 +47,9 @@ impl Firmware {
|
|||||||
/// This method is called exactly once per Host frame (60Hz).
|
/// This method is called exactly once per Host frame (60Hz).
|
||||||
/// It updates peripheral signals and delegates the logic to the current state.
|
/// It updates peripheral signals and delegates the logic to the current state.
|
||||||
pub fn step_frame(&mut self, signals: &InputSignals, hw: &mut dyn HardwareBridge) {
|
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.
|
// 1. Update peripheral state using the latest signals from the Host.
|
||||||
// This ensures input is consistent throughout the entire update.
|
// This ensures input is consistent throughout the entire update.
|
||||||
hw.pad_mut().begin_frame(signals);
|
hw.pad_mut().begin_frame(signals);
|
||||||
|
|||||||
@ -11,6 +11,13 @@ pub struct LoadCartridgeStep {
|
|||||||
impl LoadCartridgeStep {
|
impl LoadCartridgeStep {
|
||||||
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) {
|
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) {
|
||||||
ctx.os.log(LogLevel::Info, LogSource::Pos, 0, format!("Loading cartridge: {}", self.cartridge.title));
|
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);
|
ctx.os.initialize_vm(ctx.vm, &self.cartridge);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
429
crates/prometeu-core/src/hardware/asset.rs
Normal file
429
crates/prometeu-core/src/hardware/asset.rs
Normal file
@ -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<RwLock<HashMap<String, AssetEntry>>>,
|
||||||
|
handles: Arc<RwLock<HashMap<HandleId, LoadHandleInfo>>>,
|
||||||
|
next_handle_id: Mutex<HandleId>,
|
||||||
|
assets_data: Arc<RwLock<Vec<u8>>>,
|
||||||
|
|
||||||
|
pub memory_banks: Arc<MemoryBanks>,
|
||||||
|
|
||||||
|
// Commits that are ready to be applied at the next frame boundary.
|
||||||
|
pending_commits: Mutex<Vec<HandleId>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<AssetEntry>, assets_data: Vec<u8>, memory_banks: Arc<MemoryBanks>) -> 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<AssetEntry>, assets_data: Vec<u8>) {
|
||||||
|
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<HandleId, String> {
|
||||||
|
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<RwLock<Vec<u8>>>) -> 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];
|
||||||
|
|
||||||
|
// 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<Arc<TileBank>>; 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<Arc<TileBank>>; 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<Arc<TileBank>> = 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,7 @@
|
|||||||
use crate::model::{Color, HudTileLayer, ScrollableTileLayer, Sprite, TileBank, TileMap, TileSize};
|
use crate::model::{Color, HudTileLayer, ScrollableTileLayer, Sprite, TileBank, TileMap, TileSize};
|
||||||
use std::mem::size_of;
|
use std::mem::size_of;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use crate::hardware::MemoryBanks;
|
||||||
|
|
||||||
/// Blending modes inspired by classic 16-bit hardware.
|
/// Blending modes inspired by classic 16-bit hardware.
|
||||||
/// Defines how source pixels are combined with existing pixels in the framebuffer.
|
/// Defines how source pixels are combined with existing pixels in the framebuffer.
|
||||||
@ -46,8 +48,8 @@ pub struct Gfx {
|
|||||||
pub layers: [ScrollableTileLayer; 4],
|
pub layers: [ScrollableTileLayer; 4],
|
||||||
/// 1 fixed layer for User Interface.
|
/// 1 fixed layer for User Interface.
|
||||||
pub hud: HudTileLayer,
|
pub hud: HudTileLayer,
|
||||||
/// Up to 16 sets of graphical assets (tiles + palettes).
|
/// Memory banks containing graphical assets.
|
||||||
pub banks: [Option<TileBank>; 16],
|
pub memory_banks: Arc<MemoryBanks>,
|
||||||
/// Hardware sprites (Object Attribute Memory equivalent).
|
/// Hardware sprites (Object Attribute Memory equivalent).
|
||||||
pub sprites: [Sprite; 512],
|
pub sprites: [Sprite; 512],
|
||||||
|
|
||||||
@ -65,9 +67,8 @@ pub struct Gfx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Gfx {
|
impl Gfx {
|
||||||
/// Initializes the graphics system with a specific resolution.
|
/// Initializes the graphics system with a specific resolution and shared memory banks.
|
||||||
pub fn new(w: usize, h: usize) -> Self {
|
pub fn new(w: usize, h: usize, memory_banks: Arc<MemoryBanks>) -> Self {
|
||||||
const EMPTY_BANK: Option<TileBank> = None;
|
|
||||||
const EMPTY_SPRITE: Sprite = Sprite {
|
const EMPTY_SPRITE: Sprite = Sprite {
|
||||||
tile: crate::model::Tile { id: 0, flip_x: false, flip_y: false, palette_id: 0 },
|
tile: crate::model::Tile { id: 0, flip_x: false, flip_y: false, palette_id: 0 },
|
||||||
x: 0,
|
x: 0,
|
||||||
@ -94,7 +95,7 @@ impl Gfx {
|
|||||||
back: vec![0; len],
|
back: vec![0; len],
|
||||||
layers,
|
layers,
|
||||||
hud: HudTileLayer::new(64, 32),
|
hud: HudTileLayer::new(64, 32),
|
||||||
banks: [EMPTY_BANK; 16],
|
memory_banks,
|
||||||
sprites: [EMPTY_SPRITE; 512],
|
sprites: [EMPTY_SPRITE; 512],
|
||||||
scene_fade_level: 31,
|
scene_fade_level: 31,
|
||||||
scene_fade_color: Color::BLACK,
|
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.
|
// 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.
|
// 2. Main layers and prioritized sprites.
|
||||||
// Order: Layer 0 -> Sprites 1 -> Layer 1 -> Sprites 2 ...
|
// Order: Layer 0 -> Sprites 1 -> Layer 1 -> Sprites 2 ...
|
||||||
for i in 0..self.layers.len() {
|
for i in 0..self.layers.len() {
|
||||||
let bank_id = self.layers[i].bank_id as usize;
|
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);
|
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
|
// 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).
|
// 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);
|
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.
|
// 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.
|
// 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);
|
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_x = self.layers[layer_idx].scroll_x;
|
||||||
let scroll_y = self.layers[layer_idx].scroll_y;
|
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,
|
Some(Some(b)) => b,
|
||||||
_ => return,
|
_ => return,
|
||||||
};
|
};
|
||||||
@ -370,13 +375,18 @@ impl Gfx {
|
|||||||
|
|
||||||
/// Renders the HUD (fixed position, no scroll).
|
/// Renders the HUD (fixed position, no scroll).
|
||||||
pub fn render_hud(&mut self) {
|
pub fn render_hud(&mut self) {
|
||||||
let bank_id = self.hud.bank_id as usize;
|
let pool = self.memory_banks.gfx.pool.read().unwrap();
|
||||||
let bank = match self.banks.get(bank_id) {
|
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<Arc<TileBank>>; 16]) {
|
||||||
|
let bank_id = hud.bank_id as usize;
|
||||||
|
let bank = match pool.get(bank_id) {
|
||||||
Some(Some(b)) => b,
|
Some(Some(b)) => b,
|
||||||
_ => return,
|
_ => 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.
|
/// Rasterizes a TileMap into the provided pixel buffer using scrolling.
|
||||||
@ -464,7 +474,7 @@ impl Gfx {
|
|||||||
screen_h: usize,
|
screen_h: usize,
|
||||||
bucket: &[usize],
|
bucket: &[usize],
|
||||||
sprites: &[Sprite],
|
sprites: &[Sprite],
|
||||||
banks: &[Option<TileBank>],
|
banks: &[Option<Arc<TileBank>>],
|
||||||
) {
|
) {
|
||||||
for &idx in bucket {
|
for &idx in bucket {
|
||||||
let s = &sprites[idx];
|
let s = &sprites[idx];
|
||||||
@ -554,7 +564,8 @@ impl Gfx {
|
|||||||
total += self.hud.map.tiles.len() * size_of::<crate::model::Tile>();
|
total += self.hud.map.tiles.len() * size_of::<crate::model::Tile>();
|
||||||
|
|
||||||
// 4. Tile Banks (Assets and Palettes)
|
// 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 {
|
if let Some(bank) = bank_opt {
|
||||||
total += size_of::<TileBank>();
|
total += size_of::<TileBank>();
|
||||||
total += bank.pixel_indices.len();
|
total += bank.pixel_indices.len();
|
||||||
@ -647,10 +658,12 @@ impl Gfx {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::hardware::MemoryBanks;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_draw_pixel() {
|
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);
|
gfx.draw_pixel(5, 5, Color::WHITE);
|
||||||
assert_eq!(gfx.back[5 * 10 + 5], Color::WHITE.0);
|
assert_eq!(gfx.back[5 * 10 + 5], Color::WHITE.0);
|
||||||
|
|
||||||
@ -661,7 +674,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_draw_line() {
|
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);
|
gfx.draw_line(0, 0, 9, 9, Color::WHITE);
|
||||||
assert_eq!(gfx.back[0], Color::WHITE.0);
|
assert_eq!(gfx.back[0], Color::WHITE.0);
|
||||||
assert_eq!(gfx.back[9 * 10 + 9], Color::WHITE.0);
|
assert_eq!(gfx.back[9 * 10 + 9], Color::WHITE.0);
|
||||||
@ -669,7 +683,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_draw_rect() {
|
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);
|
gfx.draw_rect(0, 0, 10, 10, Color::WHITE);
|
||||||
assert_eq!(gfx.back[0], Color::WHITE.0);
|
assert_eq!(gfx.back[0], Color::WHITE.0);
|
||||||
assert_eq!(gfx.back[9], Color::WHITE.0);
|
assert_eq!(gfx.back[9], Color::WHITE.0);
|
||||||
@ -679,14 +694,16 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_fill_circle() {
|
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);
|
gfx.fill_circle(5, 5, 2, Color::WHITE);
|
||||||
assert_eq!(gfx.back[5 * 10 + 5], Color::WHITE.0);
|
assert_eq!(gfx.back[5 * 10 + 5], Color::WHITE.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_draw_square() {
|
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);
|
gfx.draw_square(2, 2, 6, 6, Color::WHITE, Color::BLACK);
|
||||||
// Border
|
// Border
|
||||||
assert_eq!(gfx.back[2 * 10 + 2], Color::WHITE.0);
|
assert_eq!(gfx.back[2 * 10 + 2], Color::WHITE.0);
|
||||||
|
|||||||
@ -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.
|
/// Aggregate structure for all virtual hardware peripherals.
|
||||||
///
|
///
|
||||||
/// This struct represents the "Mainboard" of the PROMETEU console,
|
/// This struct represents the "Mainboard" of the PROMETEU console,
|
||||||
/// containing instances of GFX, Audio, Input (Pad), and Touch.
|
/// containing instances of GFX, Audio, Input (Pad), and Touch.
|
||||||
pub struct Hardware {
|
pub struct Hardware {
|
||||||
|
/// Shared memory banks for hardware assets.
|
||||||
|
pub memory_banks: Arc<MemoryBanks>,
|
||||||
/// The Graphics Processing Unit.
|
/// The Graphics Processing Unit.
|
||||||
pub gfx: Gfx,
|
pub gfx: Gfx,
|
||||||
/// The Sound Processing Unit.
|
/// The Sound Processing Unit.
|
||||||
@ -13,6 +16,8 @@ pub struct Hardware {
|
|||||||
pub pad: Pad,
|
pub pad: Pad,
|
||||||
/// The absolute pointer input device.
|
/// The absolute pointer input device.
|
||||||
pub touch: Touch,
|
pub touch: Touch,
|
||||||
|
/// The Asset Management system.
|
||||||
|
pub assets: AssetManager,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HardwareBridge for Hardware {
|
impl HardwareBridge for Hardware {
|
||||||
@ -27,6 +32,11 @@ impl HardwareBridge for Hardware {
|
|||||||
|
|
||||||
fn touch(&self) -> &Touch { &self.touch }
|
fn touch(&self) -> &Touch { &self.touch }
|
||||||
fn touch_mut(&mut self) -> &mut Touch { &mut 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 {
|
impl Hardware {
|
||||||
@ -37,11 +47,14 @@ impl Hardware {
|
|||||||
|
|
||||||
/// Creates a fresh hardware instance with default settings.
|
/// Creates a fresh hardware instance with default settings.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
let memory_banks = Arc::new(MemoryBanks::new());
|
||||||
Self {
|
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(),
|
audio: Audio::new(),
|
||||||
pad: Pad::default(),
|
pad: Pad::default(),
|
||||||
touch: Touch::default(),
|
touch: Touch::default(),
|
||||||
|
assets: AssetManager::new(vec![], vec![], Arc::clone(&memory_banks)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
24
crates/prometeu-core/src/hardware/memory_banks.rs
Normal file
24
crates/prometeu-core/src/hardware/memory_banks.rs
Normal file
@ -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<TileBank, 16>,
|
||||||
|
// In the future, add other banks here:
|
||||||
|
// pub audio: Bank<SoundBank, 32>,
|
||||||
|
// pub blobs: Bank<Blob, 8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MemoryBanks {
|
||||||
|
/// Creates a new, empty set of memory banks.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
gfx: Bank::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,16 +1,21 @@
|
|||||||
|
mod asset;
|
||||||
mod gfx;
|
mod gfx;
|
||||||
mod pad;
|
mod pad;
|
||||||
mod touch;
|
mod touch;
|
||||||
mod input_signal;
|
mod input_signal;
|
||||||
mod audio;
|
mod audio;
|
||||||
|
mod memory_banks;
|
||||||
pub mod hardware;
|
pub mod hardware;
|
||||||
|
|
||||||
|
pub use asset::AssetManager;
|
||||||
|
pub use crate::model::HandleId;
|
||||||
pub use gfx::Gfx;
|
pub use gfx::Gfx;
|
||||||
pub use gfx::BlendMode;
|
pub use gfx::BlendMode;
|
||||||
pub use input_signal::InputSignals;
|
pub use input_signal::InputSignals;
|
||||||
pub use pad::Pad;
|
pub use pad::Pad;
|
||||||
pub use touch::Touch;
|
pub use touch::Touch;
|
||||||
pub use audio::{Audio, AudioCommand, Channel, LoopMode, MAX_CHANNELS, OUTPUT_SAMPLE_RATE};
|
pub use audio::{Audio, AudioCommand, Channel, LoopMode, MAX_CHANNELS, OUTPUT_SAMPLE_RATE};
|
||||||
|
pub use memory_banks::MemoryBanks;
|
||||||
|
|
||||||
pub trait HardwareBridge {
|
pub trait HardwareBridge {
|
||||||
fn gfx(&self) -> &Gfx;
|
fn gfx(&self) -> &Gfx;
|
||||||
@ -24,4 +29,9 @@ pub trait HardwareBridge {
|
|||||||
|
|
||||||
fn touch(&self) -> &Touch;
|
fn touch(&self) -> &Touch;
|
||||||
fn touch_mut(&mut self) -> &mut Touch;
|
fn touch_mut(&mut self) -> &mut Touch;
|
||||||
|
|
||||||
|
fn assets(&self) -> &AssetManager;
|
||||||
|
fn assets_mut(&mut self) -> &mut AssetManager;
|
||||||
|
|
||||||
|
fn memory_banks(&self) -> &MemoryBanks;
|
||||||
}
|
}
|
||||||
|
|||||||
185
crates/prometeu-core/src/model/asset.rs
Normal file
185
crates/prometeu-core/src/model/asset.rs
Normal file
@ -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<String>,
|
||||||
|
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<T> {
|
||||||
|
/// The resident, materialized object.
|
||||||
|
pub value: Arc<T>,
|
||||||
|
|
||||||
|
/// 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<T> ResidentEntry<T> {
|
||||||
|
pub fn new(value: Arc<T>, bytes: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
value,
|
||||||
|
bytes,
|
||||||
|
pins: 0,
|
||||||
|
loads: 1,
|
||||||
|
last_used: Instant::now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Bank<T, const S: usize> {
|
||||||
|
/// Dedup table: asset_id -> resident entry (value + telemetry).
|
||||||
|
pub resident: Arc<RwLock<HashMap<String, ResidentEntry<T>>>>,
|
||||||
|
|
||||||
|
/// Slot pool: hardware-visible residency pointers.
|
||||||
|
pub pool: Arc<RwLock<[Option<Arc<T>>; S]>>,
|
||||||
|
|
||||||
|
/// Staging area: handle -> value ready to commit.
|
||||||
|
pub staging: Arc<RwLock<HashMap<HandleId, Arc<T>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, const S: usize> Bank<T, S> {
|
||||||
|
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<Arc<T>> {
|
||||||
|
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<T>.
|
||||||
|
/// - 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<T>, bytes: usize) -> Arc<T> {
|
||||||
|
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<T>) {
|
||||||
|
self.staging.write().unwrap().insert(handle, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Take staged value (used by commit path).
|
||||||
|
pub fn take_staging(&self, handle: HandleId) -> Option<Arc<T>> {
|
||||||
|
self.staging.write().unwrap().remove(&handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Install (commit) a value into a slot (pointer swap).
|
||||||
|
pub fn install(&self, slot: usize, value: Arc<T>) {
|
||||||
|
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<Arc<T>> {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
use std::path::PathBuf;
|
use crate::model::asset::AssetEntry;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
@ -15,7 +15,8 @@ pub struct Cartridge {
|
|||||||
pub app_mode: AppMode,
|
pub app_mode: AppMode,
|
||||||
pub entrypoint: String,
|
pub entrypoint: String,
|
||||||
pub program: Vec<u8>,
|
pub program: Vec<u8>,
|
||||||
pub assets_path: Option<PathBuf>,
|
pub assets: Vec<u8>,
|
||||||
|
pub asset_table: Vec<AssetEntry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
@ -26,7 +27,9 @@ pub struct CartridgeDTO {
|
|||||||
pub app_mode: AppMode,
|
pub app_mode: AppMode,
|
||||||
pub entrypoint: String,
|
pub entrypoint: String,
|
||||||
pub program: Vec<u8>,
|
pub program: Vec<u8>,
|
||||||
pub assets_path: Option<PathBuf>,
|
pub assets: Vec<u8>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub asset_table: Vec<AssetEntry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<CartridgeDTO> for Cartridge {
|
impl From<CartridgeDTO> for Cartridge {
|
||||||
@ -38,7 +41,8 @@ impl From<CartridgeDTO> for Cartridge {
|
|||||||
app_mode: dto.app_mode,
|
app_mode: dto.app_mode,
|
||||||
entrypoint: dto.entrypoint,
|
entrypoint: dto.entrypoint,
|
||||||
program: dto.program,
|
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_version: String,
|
||||||
pub app_mode: AppMode,
|
pub app_mode: AppMode,
|
||||||
pub entrypoint: String,
|
pub entrypoint: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub asset_table: Vec<AssetEntry>,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,11 +48,11 @@ impl DirectoryCartridgeLoader {
|
|||||||
|
|
||||||
let program = fs::read(program_path).map_err(|_| CartridgeError::IoError)?;
|
let program = fs::read(program_path).map_err(|_| CartridgeError::IoError)?;
|
||||||
|
|
||||||
let assets_path = path.join("assets");
|
let assets_pa_path = path.join("assets.pa");
|
||||||
let assets_path = if assets_path.exists() && assets_path.is_dir() {
|
let assets = if assets_pa_path.exists() {
|
||||||
Some(assets_path)
|
fs::read(assets_pa_path).map_err(|_| CartridgeError::IoError)?
|
||||||
} else {
|
} else {
|
||||||
None
|
Vec::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
let dto = CartridgeDTO {
|
let dto = CartridgeDTO {
|
||||||
@ -62,7 +62,8 @@ impl DirectoryCartridgeLoader {
|
|||||||
app_mode: manifest.app_mode,
|
app_mode: manifest.app_mode,
|
||||||
entrypoint: manifest.entrypoint,
|
entrypoint: manifest.entrypoint,
|
||||||
program,
|
program,
|
||||||
assets_path,
|
assets,
|
||||||
|
asset_table: manifest.asset_table,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Cartridge::from(dto))
|
Ok(Cartridge::from(dto))
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
mod asset;
|
||||||
mod color;
|
mod color;
|
||||||
mod button;
|
mod button;
|
||||||
mod tile;
|
mod tile;
|
||||||
@ -9,6 +10,7 @@ mod cartridge;
|
|||||||
mod cartridge_loader;
|
mod cartridge_loader;
|
||||||
mod window;
|
mod window;
|
||||||
|
|
||||||
|
pub use asset::{AssetEntry, BankType, BankStats, LoadStatus, SlotRef, SlotStats, HandleId, Bank, ResidentEntry};
|
||||||
pub use button::{Button, ButtonId};
|
pub use button::{Button, ButtonId};
|
||||||
pub use cartridge::{AppMode, Cartridge, CartridgeDTO, CartridgeError};
|
pub use cartridge::{AppMode, Cartridge, CartridgeDTO, CartridgeError};
|
||||||
pub use cartridge_loader::{CartridgeLoader, DirectoryCartridgeLoader, PackedCartridgeLoader};
|
pub use cartridge_loader::{CartridgeLoader, DirectoryCartridgeLoader, PackedCartridgeLoader};
|
||||||
|
|||||||
@ -410,7 +410,8 @@ mod tests {
|
|||||||
app_mode: AppMode::Game,
|
app_mode: AppMode::Game,
|
||||||
entrypoint: "0".to_string(),
|
entrypoint: "0".to_string(),
|
||||||
program: rom,
|
program: rom,
|
||||||
assets_path: None,
|
assets: vec![],
|
||||||
|
asset_table: vec![],
|
||||||
};
|
};
|
||||||
os.initialize_vm(&mut vm, &cartridge);
|
os.initialize_vm(&mut vm, &cartridge);
|
||||||
|
|
||||||
@ -450,7 +451,8 @@ mod tests {
|
|||||||
app_mode: AppMode::Game,
|
app_mode: AppMode::Game,
|
||||||
entrypoint: "0".to_string(),
|
entrypoint: "0".to_string(),
|
||||||
program: rom,
|
program: rom,
|
||||||
assets_path: None,
|
assets: vec![],
|
||||||
|
asset_table: vec![],
|
||||||
};
|
};
|
||||||
os.initialize_vm(&mut vm, &cartridge);
|
os.initialize_vm(&mut vm, &cartridge);
|
||||||
|
|
||||||
@ -860,6 +862,82 @@ impl NativeInterface for PrometeuOS {
|
|||||||
let level = vm.pop_integer()?;
|
let level = vm.pop_integer()?;
|
||||||
self.syscall_log_write(vm, level, tag, msg)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -41,6 +41,16 @@ pub enum Syscall {
|
|||||||
// Log
|
// Log
|
||||||
LogWrite = 0x5001,
|
LogWrite = 0x5001,
|
||||||
LogWriteTag = 0x5002,
|
LogWriteTag = 0x5002,
|
||||||
|
|
||||||
|
// Asset
|
||||||
|
AssetLoad = 0x6001,
|
||||||
|
AssetStatus = 0x6002,
|
||||||
|
AssetCommit = 0x6003,
|
||||||
|
AssetCancel = 0x6004,
|
||||||
|
|
||||||
|
// Bank
|
||||||
|
BankInfo = 0x6101,
|
||||||
|
BankSlotInfo = 0x6102,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Syscall {
|
impl Syscall {
|
||||||
@ -74,6 +84,12 @@ impl Syscall {
|
|||||||
0x4007 => Some(Self::FsDelete),
|
0x4007 => Some(Self::FsDelete),
|
||||||
0x5001 => Some(Self::LogWrite),
|
0x5001 => Some(Self::LogWrite),
|
||||||
0x5002 => Some(Self::LogWriteTag),
|
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,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -140,6 +156,12 @@ impl Syscall {
|
|||||||
"fs.delete" => Some(Self::FsDelete),
|
"fs.delete" => Some(Self::FsDelete),
|
||||||
"log.write" => Some(Self::LogWrite),
|
"log.write" => Some(Self::LogWrite),
|
||||||
"log.writeTag" | "log.write_tag" => Some(Self::LogWriteTag),
|
"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
|
None
|
||||||
}
|
}
|
||||||
|
|||||||
@ -709,6 +709,9 @@ mod tests {
|
|||||||
fn pad_mut(&mut self) -> &mut crate::hardware::Pad { todo!() }
|
fn pad_mut(&mut self) -> &mut crate::hardware::Pad { todo!() }
|
||||||
fn touch(&self) -> &crate::hardware::Touch { todo!() }
|
fn touch(&self) -> &crate::hardware::Touch { todo!() }
|
||||||
fn touch_mut(&mut self) -> &mut 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]
|
#[test]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user