dev/asset-management #6
@ -38,7 +38,7 @@ impl<T> ResidentEntry<T> {
|
||||
/// 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>>>>,
|
||||
resident: Arc<RwLock<HashMap<u32, ResidentEntry<T>>>>,
|
||||
|
||||
/// Staging area: handle -> value ready to commit.
|
||||
staging: Arc<RwLock<HashMap<HandleId, Arc<T>>>>,
|
||||
@ -53,15 +53,15 @@ impl<T> BankPolicy<T> {
|
||||
}
|
||||
|
||||
/// Try get a resident value by asset_id (dedupe path).
|
||||
pub fn get_resident(&self, asset_id: &str) -> Option<Arc<T>> {
|
||||
pub fn get_resident(&self, asset_id: u32) -> Option<Arc<T>> {
|
||||
let mut map = self.resident.write().unwrap();
|
||||
let entry = map.get_mut(asset_id)?;
|
||||
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> {
|
||||
pub fn put_resident(&self, asset_id: u32, value: Arc<T>, bytes: usize) -> Arc<T> {
|
||||
let mut map = self.resident.write().unwrap();
|
||||
match map.get_mut(&asset_id) {
|
||||
Some(existing) => {
|
||||
@ -94,7 +94,8 @@ impl<T> BankPolicy<T> {
|
||||
}
|
||||
|
||||
pub struct AssetManager {
|
||||
assets: Arc<RwLock<HashMap<String, AssetEntry>>>,
|
||||
assets: Arc<RwLock<HashMap<u32, AssetEntry>>>,
|
||||
name_to_id: Arc<RwLock<HashMap<String, u32>>>,
|
||||
handles: Arc<RwLock<HashMap<HandleId, LoadHandleInfo>>>,
|
||||
next_handle_id: Mutex<HandleId>,
|
||||
assets_data: Arc<RwLock<Vec<u8>>>,
|
||||
@ -104,8 +105,8 @@ pub struct AssetManager {
|
||||
sound_installer: Arc<dyn SoundBankPoolInstaller>,
|
||||
|
||||
/// Track what is installed in each hardware slot (for stats/info).
|
||||
gfx_slots: Arc<RwLock<[Option<String>; 16]>>,
|
||||
sound_slots: Arc<RwLock<[Option<String>; 16]>>,
|
||||
gfx_slots: Arc<RwLock<[Option<u32>; 16]>>,
|
||||
sound_slots: Arc<RwLock<[Option<u32>; 16]>>,
|
||||
|
||||
/// Residency policy for GFX tile banks.
|
||||
gfx_policy: BankPolicy<TileBank>,
|
||||
@ -117,7 +118,7 @@ pub struct AssetManager {
|
||||
}
|
||||
|
||||
struct LoadHandleInfo {
|
||||
_asset_id: String,
|
||||
_asset_id: u32,
|
||||
slot: SlotRef,
|
||||
status: LoadStatus,
|
||||
}
|
||||
@ -130,12 +131,15 @@ impl AssetManager {
|
||||
sound_installer: Arc<dyn SoundBankPoolInstaller>,
|
||||
) -> Self {
|
||||
let mut asset_map = HashMap::new();
|
||||
let mut name_to_id = HashMap::new();
|
||||
for entry in assets {
|
||||
asset_map.insert(entry.asset_id.clone(), entry);
|
||||
name_to_id.insert(entry.asset_name.clone(), entry.asset_id);
|
||||
asset_map.insert(entry.asset_id, entry);
|
||||
}
|
||||
|
||||
Self {
|
||||
assets: Arc::new(RwLock::new(asset_map)),
|
||||
name_to_id: Arc::new(RwLock::new(name_to_id)),
|
||||
gfx_installer,
|
||||
sound_installer,
|
||||
gfx_slots: Arc::new(RwLock::new(std::array::from_fn(|_| None))),
|
||||
@ -153,9 +157,12 @@ impl AssetManager {
|
||||
self.shutdown();
|
||||
{
|
||||
let mut asset_map = self.assets.write().unwrap();
|
||||
let mut name_to_id = self.name_to_id.write().unwrap();
|
||||
asset_map.clear();
|
||||
name_to_id.clear();
|
||||
for entry in assets.iter() {
|
||||
asset_map.insert(entry.asset_id.clone(), entry.clone());
|
||||
name_to_id.insert(entry.asset_name.clone(), entry.asset_id);
|
||||
asset_map.insert(entry.asset_id, entry.clone());
|
||||
}
|
||||
}
|
||||
*self.assets_data.write().unwrap() = assets_data;
|
||||
@ -164,7 +171,10 @@ impl AssetManager {
|
||||
for item in preload {
|
||||
let entry_opt = {
|
||||
let assets = self.assets.read().unwrap();
|
||||
assets.get(&item.asset_id).cloned()
|
||||
let name_to_id = self.name_to_id.read().unwrap();
|
||||
name_to_id.get(&item.asset_name)
|
||||
.and_then(|id| assets.get(id))
|
||||
.cloned()
|
||||
};
|
||||
|
||||
if let Some(entry) = entry_opt {
|
||||
@ -173,43 +183,46 @@ impl AssetManager {
|
||||
BankType::TILES => {
|
||||
if let Ok(bank) = Self::perform_load_tile_bank(&entry, self.assets_data.clone()) {
|
||||
let bank_arc = Arc::new(bank);
|
||||
self.gfx_policy.put_resident(entry.asset_id.clone(), Arc::clone(&bank_arc), entry.decoded_size as usize);
|
||||
self.gfx_policy.put_resident(entry.asset_id, Arc::clone(&bank_arc), entry.decoded_size as usize);
|
||||
self.gfx_installer.install_tile_bank(slot_index, bank_arc);
|
||||
let mut slots = self.gfx_slots.write().unwrap();
|
||||
if slot_index < slots.len() {
|
||||
slots[slot_index] = Some(entry.asset_id.clone());
|
||||
slots[slot_index] = Some(entry.asset_id);
|
||||
}
|
||||
println!("[AssetManager] Preloaded tile asset '{}' into slot {}", entry.asset_id, slot_index);
|
||||
println!("[AssetManager] Preloaded tile asset '{}' (id: {}) into slot {}", entry.asset_name, entry.asset_id, slot_index);
|
||||
} else {
|
||||
eprintln!("[AssetManager] Failed to preload tile asset '{}'", entry.asset_id);
|
||||
eprintln!("[AssetManager] Failed to preload tile asset '{}'", entry.asset_name);
|
||||
}
|
||||
}
|
||||
BankType::SOUNDS => {
|
||||
if let Ok(bank) = Self::perform_load_sound_bank(&entry, self.assets_data.clone()) {
|
||||
let bank_arc = Arc::new(bank);
|
||||
self.sound_policy.put_resident(entry.asset_id.clone(), Arc::clone(&bank_arc), entry.decoded_size as usize);
|
||||
self.sound_policy.put_resident(entry.asset_id, Arc::clone(&bank_arc), entry.decoded_size as usize);
|
||||
self.sound_installer.install_sound_bank(slot_index, bank_arc);
|
||||
let mut slots = self.sound_slots.write().unwrap();
|
||||
if slot_index < slots.len() {
|
||||
slots[slot_index] = Some(entry.asset_id.clone());
|
||||
slots[slot_index] = Some(entry.asset_id);
|
||||
}
|
||||
println!("[AssetManager] Preloaded sound asset '{}' into slot {}", entry.asset_id, slot_index);
|
||||
println!("[AssetManager] Preloaded sound asset '{}' (id: {}) into slot {}", entry.asset_name, entry.asset_id, slot_index);
|
||||
} else {
|
||||
eprintln!("[AssetManager] Failed to preload sound asset '{}'", entry.asset_id);
|
||||
eprintln!("[AssetManager] Failed to preload sound asset '{}'", entry.asset_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("[AssetManager] Preload failed: asset '{}' not found in table", item.asset_id);
|
||||
eprintln!("[AssetManager] Preload failed: asset '{}' not found in table", item.asset_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load(&self, asset_id: &str, slot: SlotRef) -> Result<HandleId, String> {
|
||||
pub fn load(&self, asset_name: &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()
|
||||
let name_to_id = self.name_to_id.read().unwrap();
|
||||
let id = name_to_id.get(asset_name).ok_or_else(|| format!("Asset not found: {}", asset_name))?;
|
||||
assets.get(id).ok_or_else(|| format!("Asset ID {} not found in table", id))?.clone()
|
||||
};
|
||||
let asset_id = entry.asset_id;
|
||||
|
||||
if slot.asset_type != entry.bank_type {
|
||||
return Err("INCOMPATIBLE_SLOT_KIND".to_string());
|
||||
@ -237,7 +250,7 @@ impl AssetManager {
|
||||
|
||||
if already_resident {
|
||||
self.handles.write().unwrap().insert(handle_id, LoadHandleInfo {
|
||||
_asset_id: asset_id.to_string(),
|
||||
_asset_id: asset_id,
|
||||
slot,
|
||||
status: LoadStatus::READY,
|
||||
});
|
||||
@ -246,7 +259,7 @@ impl AssetManager {
|
||||
|
||||
// Not resident, start loading
|
||||
self.handles.write().unwrap().insert(handle_id, LoadHandleInfo {
|
||||
_asset_id: asset_id.to_string(),
|
||||
_asset_id: asset_id,
|
||||
slot,
|
||||
status: LoadStatus::PENDING,
|
||||
});
|
||||
@ -254,7 +267,6 @@ impl AssetManager {
|
||||
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();
|
||||
|
||||
// Capture policies for the worker thread
|
||||
let gfx_policy_resident = Arc::clone(&self.gfx_policy.resident);
|
||||
@ -284,13 +296,13 @@ impl AssetManager {
|
||||
let bank_arc = Arc::new(tilebank);
|
||||
let resident_arc = {
|
||||
let mut map = gfx_policy_resident.write().unwrap();
|
||||
if let Some(existing) = map.get_mut(&asset_id_clone) {
|
||||
if let Some(existing) = map.get_mut(&asset_id) {
|
||||
existing.last_used = Instant::now();
|
||||
existing.loads += 1;
|
||||
Arc::clone(&existing.value)
|
||||
} else {
|
||||
let entry = ResidentEntry::new(Arc::clone(&bank_arc), entry_clone.decoded_size as usize);
|
||||
map.insert(asset_id_clone, entry);
|
||||
map.insert(asset_id, entry);
|
||||
bank_arc
|
||||
}
|
||||
};
|
||||
@ -310,13 +322,13 @@ impl AssetManager {
|
||||
let bank_arc = Arc::new(soundbank);
|
||||
let resident_arc = {
|
||||
let mut map = sound_policy_resident.write().unwrap();
|
||||
if let Some(existing) = map.get_mut(&asset_id_clone) {
|
||||
if let Some(existing) = map.get_mut(&asset_id) {
|
||||
existing.last_used = Instant::now();
|
||||
existing.loads += 1;
|
||||
Arc::clone(&existing.value)
|
||||
} else {
|
||||
let entry = ResidentEntry::new(Arc::clone(&bank_arc), entry_clone.decoded_size as usize);
|
||||
map.insert(asset_id_clone, entry);
|
||||
map.insert(asset_id, entry);
|
||||
bank_arc
|
||||
}
|
||||
};
|
||||
@ -459,7 +471,7 @@ impl AssetManager {
|
||||
self.gfx_installer.install_tile_bank(h.slot.index, bank);
|
||||
let mut slots = self.gfx_slots.write().unwrap();
|
||||
if h.slot.index < slots.len() {
|
||||
slots[h.slot.index] = Some(h._asset_id.clone());
|
||||
slots[h.slot.index] = Some(h._asset_id);
|
||||
}
|
||||
h.status = LoadStatus::COMMITTED;
|
||||
}
|
||||
@ -469,7 +481,7 @@ impl AssetManager {
|
||||
self.sound_installer.install_sound_bank(h.slot.index, bank);
|
||||
let mut slots = self.sound_slots.write().unwrap();
|
||||
if h.slot.index < slots.len() {
|
||||
slots[h.slot.index] = Some(h._asset_id.clone());
|
||||
slots[h.slot.index] = Some(h._asset_id);
|
||||
}
|
||||
h.status = LoadStatus::COMMITTED;
|
||||
}
|
||||
@ -573,17 +585,20 @@ impl AssetManager {
|
||||
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()
|
||||
let (bytes, asset_name) = if let Some(id) = &asset_id {
|
||||
let bytes = self.gfx_policy.resident.read().unwrap()
|
||||
.get(id)
|
||||
.map(|entry| entry.bytes)
|
||||
.unwrap_or(0)
|
||||
.unwrap_or(0);
|
||||
let name = self.assets.read().unwrap().get(id).map(|e| e.asset_name.clone());
|
||||
(bytes, name)
|
||||
} else {
|
||||
0
|
||||
(0, None)
|
||||
};
|
||||
|
||||
SlotStats {
|
||||
asset_id,
|
||||
asset_name,
|
||||
generation: 0,
|
||||
resident_bytes: bytes,
|
||||
}
|
||||
@ -592,17 +607,20 @@ impl AssetManager {
|
||||
let slots = self.sound_slots.read().unwrap();
|
||||
let asset_id = slots.get(slot.index).and_then(|s| s.clone());
|
||||
|
||||
let bytes = if let Some(id) = &asset_id {
|
||||
self.sound_policy.resident.read().unwrap()
|
||||
let (bytes, asset_name) = if let Some(id) = &asset_id {
|
||||
let bytes = self.sound_policy.resident.read().unwrap()
|
||||
.get(id)
|
||||
.map(|entry| entry.bytes)
|
||||
.unwrap_or(0)
|
||||
.unwrap_or(0);
|
||||
let name = self.assets.read().unwrap().get(id).map(|e| e.asset_name.clone());
|
||||
(bytes, name)
|
||||
} else {
|
||||
0
|
||||
(0, None)
|
||||
};
|
||||
|
||||
SlotStats {
|
||||
asset_id,
|
||||
asset_name,
|
||||
generation: 0,
|
||||
resident_bytes: bytes,
|
||||
}
|
||||
@ -635,7 +653,8 @@ mod tests {
|
||||
data.extend_from_slice(&[0u8; 2048]);
|
||||
|
||||
let asset_entry = AssetEntry {
|
||||
asset_id: "test_tiles".to_string(),
|
||||
asset_id: 0,
|
||||
asset_name: "test_tiles".to_string(),
|
||||
bank_type: BankType::TILES,
|
||||
offset: 0,
|
||||
size: data.len() as u64,
|
||||
@ -684,7 +703,8 @@ mod tests {
|
||||
data.extend_from_slice(&[0u8; 2048]);
|
||||
|
||||
let asset_entry = AssetEntry {
|
||||
asset_id: "test_tiles".to_string(),
|
||||
asset_id: 0,
|
||||
asset_name: "test_tiles".to_string(),
|
||||
bank_type: BankType::TILES,
|
||||
offset: 0,
|
||||
size: data.len() as u64,
|
||||
@ -724,7 +744,8 @@ mod tests {
|
||||
let data = vec![0u8; 200];
|
||||
|
||||
let asset_entry = AssetEntry {
|
||||
asset_id: "test_sound".to_string(),
|
||||
asset_id: 1,
|
||||
asset_name: "test_sound".to_string(),
|
||||
bank_type: BankType::SOUNDS,
|
||||
offset: 0,
|
||||
size: data.len() as u64,
|
||||
@ -763,7 +784,8 @@ mod tests {
|
||||
let data = vec![0u8; 200];
|
||||
|
||||
let asset_entry = AssetEntry {
|
||||
asset_id: "preload_sound".to_string(),
|
||||
asset_id: 2,
|
||||
asset_name: "preload_sound".to_string(),
|
||||
bank_type: BankType::SOUNDS,
|
||||
offset: 0,
|
||||
size: data.len() as u64,
|
||||
@ -775,7 +797,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let preload = vec![
|
||||
PreloadEntry { asset_id: "preload_sound".to_string(), slot: 5 }
|
||||
PreloadEntry { asset_name: "preload_sound".to_string(), slot: 5 }
|
||||
];
|
||||
|
||||
let am = AssetManager::new(vec![], vec![], gfx_installer, sound_installer);
|
||||
@ -787,6 +809,6 @@ mod tests {
|
||||
|
||||
// After init, slot 5 should be occupied because of preload
|
||||
assert!(banks.sound_bank_slot(5).is_some());
|
||||
assert_eq!(am.slot_info(SlotRef::audio(5)).asset_id, Some("preload_sound".to_string()));
|
||||
assert_eq!(am.slot_info(SlotRef::audio(5)).asset_id, Some(2));
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,8 @@ pub enum BankType {
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct AssetEntry {
|
||||
pub asset_id: String,
|
||||
pub asset_id: u32,
|
||||
pub asset_name: String,
|
||||
pub bank_type: BankType,
|
||||
pub offset: u64,
|
||||
pub size: u64,
|
||||
@ -24,7 +25,7 @@ pub struct AssetEntry {
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct PreloadEntry {
|
||||
pub asset_id: String,
|
||||
pub asset_name: String,
|
||||
pub slot: usize,
|
||||
}
|
||||
|
||||
@ -50,7 +51,8 @@ pub struct BankStats {
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SlotStats {
|
||||
pub asset_id: Option<String>,
|
||||
pub asset_id: Option<u32>,
|
||||
pub asset_name: Option<String>,
|
||||
pub generation: u32,
|
||||
pub resident_bytes: usize,
|
||||
}
|
||||
|
||||
@ -900,6 +900,7 @@ impl NativeInterface for PrometeuOS {
|
||||
|
||||
let asset_type = match asset_type_val {
|
||||
0 => crate::model::BankType::TILES,
|
||||
1 => crate::model::BankType::SOUNDS,
|
||||
_ => return Err("Invalid asset type".to_string()),
|
||||
};
|
||||
let slot = crate::model::SlotRef { asset_type, index: slot_index };
|
||||
@ -942,6 +943,7 @@ impl NativeInterface for PrometeuOS {
|
||||
let asset_type_val = vm.pop_integer()? as u32;
|
||||
let asset_type = match asset_type_val {
|
||||
0 => crate::model::BankType::TILES,
|
||||
1 => crate::model::BankType::SOUNDS,
|
||||
_ => return Err("Invalid asset type".to_string()),
|
||||
};
|
||||
let info = hw.assets().bank_info(asset_type);
|
||||
@ -954,6 +956,7 @@ impl NativeInterface for PrometeuOS {
|
||||
let asset_type_val = vm.pop_integer()? as u32;
|
||||
let asset_type = match asset_type_val {
|
||||
0 => crate::model::BankType::TILES,
|
||||
1 => crate::model::BankType::SOUNDS,
|
||||
_ => return Err("Invalid asset type".to_string()),
|
||||
};
|
||||
let slot = crate::model::SlotRef { asset_type, index: slot_index };
|
||||
|
||||
@ -44,7 +44,8 @@ It describes **content**, not residency.
|
||||
|
||||
### Required Fields (conceptual)
|
||||
|
||||
* `asset_id` (string or hash)
|
||||
* `asset_id` (integer, internal identifier)
|
||||
* `asset_name` (string, user-facing identifier)
|
||||
* `asset_type` (TILEBANK, SOUNDBANK, BLOB, TILEMAP, ...)
|
||||
* `bank_kind` (mandatory, single)
|
||||
* `offset` (byte offset in cartridge)
|
||||
@ -139,12 +140,12 @@ Conceptually, each Bank is a **specialized allocator**.
|
||||
### Conceptual API
|
||||
|
||||
```text
|
||||
handle = asset.load(asset_id, slotRef, flags)
|
||||
handle = asset.load(asset_name, slotRef, flags)
|
||||
```
|
||||
|
||||
Load flow:
|
||||
|
||||
1. Resolve `asset_id` via Asset Table
|
||||
1. Resolve `asset_name` via Asset Table to get its `asset_id`
|
||||
2. Read `bank_kind` from asset entry
|
||||
3. Validate compatibility with `slotRef`
|
||||
4. Enqueue load request
|
||||
@ -264,6 +265,7 @@ Each Bank must expose:
|
||||
* inflight memory
|
||||
* occupied slots
|
||||
* `asset_id` per slot
|
||||
* `asset_name` per slot
|
||||
* generation per slot
|
||||
|
||||
This enables debuggers to visualize:
|
||||
@ -279,7 +281,7 @@ This enables debuggers to visualize:
|
||||
The following syscalls form the minimal hardware contract for asset management:
|
||||
|
||||
```text
|
||||
asset.load(asset_id, slotRef, flags) -> handle
|
||||
asset.load(asset_name, slotRef, flags) -> handle
|
||||
asset.status(handle) -> LoadStatus
|
||||
asset.commit(handle)
|
||||
asset.cancel(handle)
|
||||
@ -292,7 +294,7 @@ Where:
|
||||
|
||||
* `LoadStatus` ∈ { PENDING, LOADING, READY, COMMITTED, CANCELED, ERROR }
|
||||
* `BankStats` exposes memory usage and limits
|
||||
* `SlotStats` exposes current asset_id and generation
|
||||
* `SlotStats` exposes current asset_id, asset_name and generation
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -8,7 +8,8 @@
|
||||
"entrypoint": "0",
|
||||
"asset_table": [
|
||||
{
|
||||
"asset_id": "bgm_music",
|
||||
"asset_id": 0,
|
||||
"asset_name": "bgm_music",
|
||||
"bank_type": "SOUNDS",
|
||||
"offset": 0,
|
||||
"size": 88200,
|
||||
@ -19,7 +20,8 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"asset_id": "mouse_cursor",
|
||||
"asset_id": 1,
|
||||
"asset_name": "mouse_cursor",
|
||||
"bank_type": "TILES",
|
||||
"offset": 88200,
|
||||
"size": 2304,
|
||||
@ -33,7 +35,7 @@
|
||||
}
|
||||
],
|
||||
"preload": [
|
||||
{ "asset_id": "bgm_music", "slot": 0 },
|
||||
{ "asset_id": "mouse_cursor", "slot": 1 }
|
||||
{ "asset_name": "bgm_music", "slot": 0 },
|
||||
{ "asset_name": "mouse_cursor", "slot": 1 }
|
||||
]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user