dev/asset-management #6

Merged
bquarkz merged 16 commits from dev/asset-management into master 2026-01-22 15:22:14 +00:00
5 changed files with 88 additions and 57 deletions
Showing only changes of commit 81f7813c7c - Show all commits

View File

@ -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));
}
}

View File

@ -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,
}

View File

@ -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 };

View File

@ -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
---

View File

@ -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 }
]
}