refactor about memorybanks
This commit is contained in:
parent
4fe9c6d81d
commit
0374f57242
@ -1,8 +1,97 @@
|
||||
use crate::hardware::memory_banks::GfxTileBankPoolInstaller;
|
||||
use crate::model::{AssetEntry, BankStats, BankType, Color, HandleId, LoadStatus, SlotRef, SlotStats, TileBank, TileSize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, RwLock, Mutex};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::thread;
|
||||
use crate::model::{AssetEntry, BankType, BankStats, LoadStatus, SlotRef, SlotStats, TileBank, TileSize, Color, HandleId};
|
||||
use crate::hardware::MemoryBanks;
|
||||
use std::time::Instant;
|
||||
|
||||
/// Resident metadata for a decoded/materialized asset inside a BankPolicy.
|
||||
#[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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Encapsulates the residency and staging policy for a specific type of asset.
|
||||
/// This is internal to the AssetManager and not visible to peripherals.
|
||||
pub struct BankPolicy<T> {
|
||||
/// Dedup table: asset_id -> resident entry (value + telemetry).
|
||||
resident: Arc<RwLock<HashMap<String, ResidentEntry<T>>>>,
|
||||
|
||||
/// Staging area: handle -> value ready to commit.
|
||||
staging: Arc<RwLock<HashMap<HandleId, Arc<T>>>>,
|
||||
}
|
||||
|
||||
impl<T> BankPolicy<T> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
resident: Arc::new(RwLock::new(HashMap::new())),
|
||||
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>.
|
||||
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)
|
||||
}
|
||||
|
||||
pub fn clear(&self) {
|
||||
self.resident.write().unwrap().clear();
|
||||
self.staging.write().unwrap().clear();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AssetManager {
|
||||
assets: Arc<RwLock<HashMap<String, AssetEntry>>>,
|
||||
@ -10,7 +99,14 @@ pub struct AssetManager {
|
||||
next_handle_id: Mutex<HandleId>,
|
||||
assets_data: Arc<RwLock<Vec<u8>>>,
|
||||
|
||||
pub memory_banks: Arc<MemoryBanks>,
|
||||
/// Narrow hardware interfaces
|
||||
gfx_installer: Arc<dyn GfxTileBankPoolInstaller>,
|
||||
|
||||
/// Track what is installed in each hardware slot (for stats/info).
|
||||
gfx_slots: Arc<RwLock<[Option<String>; 16]>>,
|
||||
|
||||
/// Residency policy for GFX tile banks.
|
||||
gfx_policy: BankPolicy<TileBank>,
|
||||
|
||||
// Commits that are ready to be applied at the next frame boundary.
|
||||
pending_commits: Mutex<Vec<HandleId>>,
|
||||
@ -22,14 +118,12 @@ struct LoadHandleInfo {
|
||||
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 {
|
||||
pub fn new(
|
||||
assets: Vec<AssetEntry>,
|
||||
assets_data: Vec<u8>,
|
||||
gfx_installer: Arc<dyn GfxTileBankPoolInstaller>,
|
||||
) -> Self {
|
||||
let mut asset_map = HashMap::new();
|
||||
for entry in assets {
|
||||
asset_map.insert(entry.asset_id.clone(), entry);
|
||||
@ -37,7 +131,9 @@ impl AssetManager {
|
||||
|
||||
Self {
|
||||
assets: Arc::new(RwLock::new(asset_map)),
|
||||
memory_banks,
|
||||
gfx_installer,
|
||||
gfx_slots: Arc::new(RwLock::new(std::array::from_fn(|_| None))),
|
||||
gfx_policy: BankPolicy::new(),
|
||||
handles: Arc::new(RwLock::new(HashMap::new())),
|
||||
next_handle_id: Mutex::new(1),
|
||||
assets_data: Arc::new(RwLock::new(assets_data)),
|
||||
@ -70,14 +166,14 @@ impl AssetManager {
|
||||
*next_id += 1;
|
||||
|
||||
// Check if already resident
|
||||
if let Some(bank) = self.memory_banks.gfx.get_resident(asset_id) {
|
||||
if let Some(bank) = self.gfx_policy.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);
|
||||
self.gfx_policy.stage(handle_id, bank);
|
||||
return Ok(handle_id);
|
||||
}
|
||||
|
||||
@ -88,7 +184,8 @@ impl AssetManager {
|
||||
status: LoadStatus::PENDING,
|
||||
});
|
||||
|
||||
let memory_banks = Arc::clone(&self.memory_banks);
|
||||
let gfx_policy_resident = Arc::clone(&self.gfx_policy.resident);
|
||||
let gfx_policy_staging = Arc::clone(&self.gfx_policy.staging);
|
||||
let handles = self.handles.clone();
|
||||
let assets_data = self.assets_data.clone();
|
||||
let entry_clone = entry.clone();
|
||||
@ -118,10 +215,24 @@ impl AssetManager {
|
||||
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);
|
||||
let resident_arc = {
|
||||
let mut map = gfx_policy_resident.write().unwrap();
|
||||
match map.get_mut(&asset_id_clone) {
|
||||
Some(existing) => {
|
||||
existing.last_used = Instant::now();
|
||||
existing.loads += 1;
|
||||
Arc::clone(&existing.value)
|
||||
}
|
||||
None => {
|
||||
let entry = ResidentEntry::new(Arc::clone(&bank_arc), entry_clone.decoded_size as usize);
|
||||
map.insert(asset_id_clone, entry);
|
||||
bank_arc
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Add to staging
|
||||
memory_banks.gfx.stage(handle_id, resident_arc);
|
||||
gfx_policy_staging.write().unwrap().insert(handle_id, resident_arc);
|
||||
|
||||
// Update status to READY
|
||||
let mut handles_map = handles.write().unwrap();
|
||||
@ -216,17 +327,13 @@ impl AssetManager {
|
||||
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);
|
||||
self.gfx_policy.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();
|
||||
@ -234,9 +341,15 @@ impl AssetManager {
|
||||
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 let Some(bank) = self.gfx_policy.take_staging(handle_id) {
|
||||
if h.slot.asset_type == BankType::TILES {
|
||||
self.memory_banks.gfx.install(h.slot.index, bank);
|
||||
self.gfx_installer.install_tilebank(h.slot.index, bank);
|
||||
|
||||
// Update internal tracking of what's in the slot
|
||||
let mut slots = self.gfx_slots.write().unwrap();
|
||||
if h.slot.index < slots.len() {
|
||||
slots[h.slot.index] = Some(h._asset_id.clone());
|
||||
}
|
||||
}
|
||||
h.status = LoadStatus::COMMITTED;
|
||||
}
|
||||
@ -245,12 +358,12 @@ impl AssetManager {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bank_info(&self, kind: BankType, _gfx_banks: &[Option<Arc<TileBank>>; 16]) -> BankStats {
|
||||
pub fn bank_info(&self, kind: BankType) -> BankStats {
|
||||
match kind {
|
||||
BankType::TILES => {
|
||||
let mut used_bytes = 0;
|
||||
{
|
||||
let resident = self.memory_banks.gfx.resident.read().unwrap();
|
||||
let resident = self.gfx_policy.resident.read().unwrap();
|
||||
for entry in resident.values() {
|
||||
used_bytes += entry.bytes;
|
||||
}
|
||||
@ -258,11 +371,10 @@ impl AssetManager {
|
||||
|
||||
let mut inflight_bytes = 0;
|
||||
{
|
||||
let staging = self.memory_banks.gfx.staging.read().unwrap();
|
||||
let staging = self.gfx_policy.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) {
|
||||
@ -273,7 +385,7 @@ impl AssetManager {
|
||||
}
|
||||
|
||||
BankStats {
|
||||
total_bytes: 16 * 1024 * 1024, // 16MB budget (arbitrary for now)
|
||||
total_bytes: 16 * 1024 * 1024,
|
||||
used_bytes,
|
||||
free_bytes: (16usize * 1024 * 1024).saturating_sub(used_bytes),
|
||||
inflight_bytes,
|
||||
@ -283,111 +395,48 @@ impl AssetManager {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn slot_info(&self, slot: SlotRef, gfx_banks: &[Option<Arc<TileBank>>; 16]) -> SlotStats {
|
||||
pub fn slot_info(&self, slot: SlotRef) -> 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,
|
||||
}
|
||||
let slots = self.gfx_slots.read().unwrap();
|
||||
let asset_id = slots.get(slot.index).and_then(|s| s.clone());
|
||||
|
||||
let bytes = if let Some(id) = &asset_id {
|
||||
self.gfx_policy.resident.read().unwrap()
|
||||
.get(id)
|
||||
.map(|entry| entry.bytes)
|
||||
.unwrap_or(0)
|
||||
} else {
|
||||
SlotStats {
|
||||
asset_id: None,
|
||||
generation: 0,
|
||||
resident_bytes: 0,
|
||||
}
|
||||
0
|
||||
};
|
||||
|
||||
SlotStats {
|
||||
asset_id,
|
||||
generation: 0,
|
||||
resident_bytes: bytes,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shutdown(&self) {
|
||||
self.memory_banks.gfx.resident.write().unwrap().clear();
|
||||
self.memory_banks.gfx.staging.write().unwrap().clear();
|
||||
self.gfx_policy.clear();
|
||||
self.handles.write().unwrap().clear();
|
||||
self.pending_commits.lock().unwrap().clear();
|
||||
// gfx_pool is cleared by Hardware when it owns Gfx
|
||||
self.gfx_slots.write().unwrap().fill(None);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::time::Instant;
|
||||
use crate::hardware::memory_banks::{GfxTileBankPoolAccess, MemoryBanks};
|
||||
|
||||
#[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 gfx_installer = Arc::clone(&banks) as Arc<dyn GfxTileBankPoolInstaller>;
|
||||
|
||||
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]);
|
||||
|
||||
@ -405,23 +454,66 @@ mod tests {
|
||||
}),
|
||||
};
|
||||
|
||||
let am = AssetManager::new(vec![asset_entry], data, Arc::clone(&banks));
|
||||
let am = AssetManager::new(vec![asset_entry], data, gfx_installer);
|
||||
let slot = SlotRef::gfx(0);
|
||||
|
||||
let handle = am.load("test_tiles", slot).expect("Should start loading");
|
||||
|
||||
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);
|
||||
|
||||
{
|
||||
let staging = am.gfx_policy.staging.read().unwrap();
|
||||
assert!(staging.contains_key(&handle));
|
||||
}
|
||||
|
||||
am.commit(handle);
|
||||
am.apply_commits();
|
||||
|
||||
assert_eq!(am.status(handle), LoadStatus::COMMITTED);
|
||||
assert!(banks.tilebank_slot(0).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_asset_dedup() {
|
||||
let banks = Arc::new(MemoryBanks::new());
|
||||
let gfx_installer = Arc::clone(&banks) as Arc<dyn GfxTileBankPoolInstaller>;
|
||||
|
||||
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, gfx_installer);
|
||||
|
||||
// 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 staging = am.gfx_policy.staging.read().unwrap();
|
||||
let bank1 = staging.get(&handle1).unwrap();
|
||||
let bank2 = staging.get(&handle2).unwrap();
|
||||
assert!(Arc::ptr_eq(bank1, bank2));
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use crate::hardware::MemoryBanks;
|
||||
use crate::hardware::memory_banks::GfxTileBankPoolAccess;
|
||||
use crate::model::{Color, HudTileLayer, ScrollableTileLayer, Sprite, TileBank, TileMap, TileSize};
|
||||
use std::sync::Arc;
|
||||
|
||||
@ -47,8 +47,8 @@ pub struct Gfx {
|
||||
pub layers: [ScrollableTileLayer; 4],
|
||||
/// 1 fixed layer for User Interface.
|
||||
pub hud: HudTileLayer,
|
||||
/// Memory banks containing graphical assets.
|
||||
pub memory_banks: Arc<MemoryBanks>,
|
||||
/// Interface to access graphical memory banks.
|
||||
pub tile_banks: Arc<dyn GfxTileBankPoolAccess>,
|
||||
/// Hardware sprites (Object Attribute Memory equivalent).
|
||||
pub sprites: [Sprite; 512],
|
||||
|
||||
@ -67,7 +67,7 @@ pub struct Gfx {
|
||||
|
||||
impl Gfx {
|
||||
/// Initializes the graphics system with a specific resolution and shared memory banks.
|
||||
pub fn new(w: usize, h: usize, memory_banks: Arc<MemoryBanks>) -> Self {
|
||||
pub fn new(w: usize, h: usize, tile_banks: Arc<dyn GfxTileBankPoolAccess>) -> 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 +94,7 @@ impl Gfx {
|
||||
back: vec![0; len],
|
||||
layers,
|
||||
hud: HudTileLayer::new(64, 32),
|
||||
memory_banks,
|
||||
tile_banks,
|
||||
sprites: [EMPTY_SPRITE; 512],
|
||||
scene_fade_level: 31,
|
||||
scene_fade_color: Color::BLACK,
|
||||
@ -327,29 +327,26 @@ 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, pool);
|
||||
Self::draw_bucket_on_buffer(&mut self.back, self.w, self.h, &self.priority_buckets[0], &self.sprites, &*self.tile_banks);
|
||||
|
||||
// 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)) = 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);
|
||||
if let Some(bank) = self.tile_banks.tilebank_slot(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, pool);
|
||||
Self::draw_bucket_on_buffer(&mut self.back, self.w, self.h, &self.priority_buckets[i + 1], &self.sprites, &*self.tile_banks);
|
||||
}
|
||||
|
||||
// 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_with_pool(&mut self.back, self.w, self.h, &self.hud, pool);
|
||||
Self::render_hud_with_pool(&mut self.back, self.w, self.h, &self.hud, &*self.tile_banks);
|
||||
|
||||
// 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);
|
||||
@ -363,29 +360,27 @@ impl Gfx {
|
||||
let scroll_x = self.layers[layer_idx].scroll_x;
|
||||
let scroll_y = self.layers[layer_idx].scroll_y;
|
||||
|
||||
let pool = self.memory_banks.gfx.pool.read().unwrap();
|
||||
let bank = match pool.get(bank_id) {
|
||||
Some(Some(b)) => b,
|
||||
let bank = match self.tile_banks.tilebank_slot(bank_id) {
|
||||
Some(b) => b,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
Self::draw_tile_map(&mut self.back, self.w, self.h, &self.layers[layer_idx].map, bank, scroll_x, scroll_y);
|
||||
Self::draw_tile_map(&mut self.back, self.w, self.h, &self.layers[layer_idx].map, &bank, scroll_x, scroll_y);
|
||||
}
|
||||
|
||||
/// Renders the HUD (fixed position, no scroll).
|
||||
pub fn render_hud(&mut self) {
|
||||
let pool = self.memory_banks.gfx.pool.read().unwrap();
|
||||
Self::render_hud_with_pool(&mut self.back, self.w, self.h, &self.hud, &*pool);
|
||||
Self::render_hud_with_pool(&mut self.back, self.w, self.h, &self.hud, &*self.tile_banks);
|
||||
}
|
||||
|
||||
fn render_hud_with_pool(back: &mut [u16], w: usize, h: usize, hud: &HudTileLayer, pool: &[Option<Arc<TileBank>>; 16]) {
|
||||
fn render_hud_with_pool(back: &mut [u16], w: usize, h: usize, hud: &HudTileLayer, tile_banks: &dyn GfxTileBankPoolAccess) {
|
||||
let bank_id = hud.bank_id as usize;
|
||||
let bank = match pool.get(bank_id) {
|
||||
Some(Some(b)) => b,
|
||||
let bank = match tile_banks.tilebank_slot(bank_id) {
|
||||
Some(b) => b,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
Self::draw_tile_map(back, w, h, &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.
|
||||
@ -473,13 +468,13 @@ impl Gfx {
|
||||
screen_h: usize,
|
||||
bucket: &[usize],
|
||||
sprites: &[Sprite],
|
||||
banks: &[Option<Arc<TileBank>>],
|
||||
tile_banks: &dyn GfxTileBankPoolAccess,
|
||||
) {
|
||||
for &idx in bucket {
|
||||
let s = &sprites[idx];
|
||||
let bank_id = s.bank_id as usize;
|
||||
if let Some(Some(bank)) = banks.get(bank_id) {
|
||||
Self::draw_sprite_pixel_by_pixel(back, screen_w, screen_h, s, bank);
|
||||
if let Some(bank) = tile_banks.tilebank_slot(bank_id) {
|
||||
Self::draw_sprite_pixel_by_pixel(back, screen_w, screen_h, s, &bank);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
use crate::hardware::{AssetManager, Audio, Gfx, HardwareBridge, Pad, Touch, MemoryBanks};
|
||||
use crate::hardware::memory_banks::{GfxTileBankPoolAccess, GfxTileBankPoolInstaller};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Aggregate structure for all virtual hardware peripherals.
|
||||
@ -50,11 +51,15 @@ impl Hardware {
|
||||
let memory_banks = Arc::new(MemoryBanks::new());
|
||||
Self {
|
||||
memory_banks: Arc::clone(&memory_banks),
|
||||
gfx: Gfx::new(Self::W, Self::H, Arc::clone(&memory_banks)),
|
||||
gfx: Gfx::new(Self::W, Self::H, Arc::clone(&memory_banks) as Arc<dyn GfxTileBankPoolAccess>),
|
||||
audio: Audio::new(),
|
||||
pad: Pad::default(),
|
||||
touch: Touch::default(),
|
||||
assets: AssetManager::new(vec![], vec![], Arc::clone(&memory_banks)),
|
||||
assets: AssetManager::new(
|
||||
vec![],
|
||||
vec![],
|
||||
Arc::clone(&memory_banks) as Arc<dyn GfxTileBankPoolInstaller>,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,24 +1,54 @@
|
||||
use crate::model::{Bank, TileBank};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use crate::model::TileBank;
|
||||
|
||||
/// Non-generic interface for peripherals to access graphical tile banks.
|
||||
pub trait GfxTileBankPoolAccess: Send + Sync {
|
||||
/// Returns a reference to the resident TileBank in the specified slot, if any.
|
||||
fn tilebank_slot(&self, slot: usize) -> Option<Arc<TileBank>>;
|
||||
/// Returns the total number of slots available in this bank.
|
||||
fn tilebank_slot_count(&self) -> usize;
|
||||
}
|
||||
|
||||
/// Non-generic interface for the AssetManager to install graphical tile banks.
|
||||
pub trait GfxTileBankPoolInstaller: Send + Sync {
|
||||
/// Atomically swaps the resident TileBank in the specified slot.
|
||||
fn install_tilebank(&self, slot: usize, bank: Arc<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).
|
||||
/// MemoryBanks represents the actual hardware slot state.
|
||||
/// Peripherals consume this state via narrow, non-generic traits.
|
||||
/// AssetManager coordinates residency and installs assets into these slots.
|
||||
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>,
|
||||
gfx_tilebank_pool: Arc<RwLock<[Option<Arc<TileBank>>; 16]>>,
|
||||
}
|
||||
|
||||
impl MemoryBanks {
|
||||
/// Creates a new, empty set of memory banks.
|
||||
/// Creates a new set of memory banks with empty slots.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
gfx: Bank::new(),
|
||||
gfx_tilebank_pool: Arc::new(RwLock::new(std::array::from_fn(|_| None))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GfxTileBankPoolAccess for MemoryBanks {
|
||||
fn tilebank_slot(&self, slot: usize) -> Option<Arc<TileBank>> {
|
||||
let pool = self.gfx_tilebank_pool.read().unwrap();
|
||||
pool.get(slot).and_then(|s| s.as_ref().map(Arc::clone))
|
||||
}
|
||||
|
||||
fn tilebank_slot_count(&self) -> usize {
|
||||
16
|
||||
}
|
||||
}
|
||||
|
||||
impl GfxTileBankPoolInstaller for MemoryBanks {
|
||||
fn install_tilebank(&self, slot: usize, bank: Arc<TileBank>) {
|
||||
let mut pool = self.gfx_tilebank_pool.write().unwrap();
|
||||
if slot < 16 {
|
||||
pool[slot] = Some(bank);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,3 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::Instant;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub type HandleId = u32;
|
||||
@ -65,121 +62,3 @@ impl SlotRef {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ mod cartridge;
|
||||
mod cartridge_loader;
|
||||
mod window;
|
||||
|
||||
pub use asset::{AssetEntry, BankType, BankStats, LoadStatus, SlotRef, SlotStats, HandleId, Bank, ResidentEntry};
|
||||
pub use asset::{AssetEntry, BankType, BankStats, LoadStatus, SlotRef, SlotStats, HandleId};
|
||||
pub use button::{Button, ButtonId};
|
||||
pub use cartridge::{AppMode, Cartridge, CartridgeDTO, CartridgeError};
|
||||
pub use cartridge_loader::{CartridgeLoader, DirectoryCartridgeLoader, PackedCartridgeLoader};
|
||||
|
||||
@ -918,8 +918,7 @@ impl NativeInterface for PrometeuOS {
|
||||
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 info = hw.assets().bank_info(asset_type);
|
||||
let json = serde_json::to_string(&info).unwrap_or_default();
|
||||
vm.push(Value::String(json));
|
||||
Ok(500)
|
||||
@ -932,8 +931,7 @@ impl NativeInterface for PrometeuOS {
|
||||
_ => 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 info = hw.assets().slot_info(slot);
|
||||
let json = serde_json::to_string(&info).unwrap_or_default();
|
||||
vm.push(Value::String(json));
|
||||
Ok(500)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user