implements PLN-0125 asset backlog stable handles

This commit is contained in:
bQUARKz 2026-06-28 14:02:53 +01:00
parent 7598a15782
commit 30c24a4ea7
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
3 changed files with 458 additions and 142 deletions

View File

@ -18,10 +18,10 @@ use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
use prometeu_hal::sound_bank::SoundBank; use prometeu_hal::sound_bank::SoundBank;
use prometeu_hal::tile::Tile; use prometeu_hal::tile::Tile;
use prometeu_hal::tilemap::TileMap; use prometeu_hal::tilemap::TileMap;
use std::collections::HashMap; use std::collections::{HashMap, VecDeque};
use std::io::Read; use std::io::Read;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, RwLock}; use std::sync::{Arc, Condvar, Mutex, RwLock};
use std::thread; use std::thread;
use std::time::Instant; use std::time::Instant;
@ -31,6 +31,8 @@ type StagingMap<T> = HashMap<HandleId, StagedValue<T>>;
type AssetTable = HashMap<AssetId, AssetEntry>; type AssetTable = HashMap<AssetId, AssetEntry>;
type HandleTable = HashMap<HandleId, LoadHandleInfo>; type HandleTable = HashMap<HandleId, LoadHandleInfo>;
type TargetHandleTable = HashMap<SlotRef, HandleId>;
type TargetGenerationTable = HashMap<SlotRef, u64>;
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct GlyphAssetSlotIndex { pub struct GlyphAssetSlotIndex {
@ -182,6 +184,8 @@ impl<T> BankPolicy<T> {
pub struct AssetManager { pub struct AssetManager {
assets: Arc<RwLock<AssetTable>>, assets: Arc<RwLock<AssetTable>>,
handles: Arc<RwLock<HandleTable>>, handles: Arc<RwLock<HandleTable>>,
target_handles: Arc<RwLock<TargetHandleTable>>,
target_generations: Arc<RwLock<TargetGenerationTable>>,
next_handle_id: Mutex<HandleId>, next_handle_id: Mutex<HandleId>,
assets_data: Arc<RwLock<AssetsPayloadSource>>, assets_data: Arc<RwLock<AssetsPayloadSource>>,
@ -203,14 +207,123 @@ pub struct AssetManager {
/// Residency policy for scene banks. /// Residency policy for scene banks.
scene_policy: BankPolicy<SceneBank>, scene_policy: BankPolicy<SceneBank>,
load_worker: AssetLoadWorker,
// Commits that are ready to be applied at the next frame boundary. // Commits that are ready to be applied at the next frame boundary.
pending_commits: Mutex<Vec<HandleId>>, pending_commits: Mutex<Vec<(HandleId, u64)>>,
} }
struct LoadHandleInfo { struct LoadHandleInfo {
_asset_id: AssetId, _asset_id: AssetId,
slot: SlotRef, slot: SlotRef,
status: LoadStatus, status: LoadStatus,
request_generation: u64,
}
struct AssetLoadJob {
handle_id: HandleId,
asset_id: AssetId,
slot: SlotRef,
request_generation: u64,
entry: AssetEntry,
}
#[derive(Default)]
struct AssetLoadQueueState {
pending: VecDeque<AssetLoadJob>,
shutdown: bool,
}
#[derive(Default)]
struct AssetLoadQueue {
state: Mutex<AssetLoadQueueState>,
ready: Condvar,
}
impl AssetLoadQueue {
fn submit(&self, job: AssetLoadJob) {
let mut state = self.state.lock().unwrap();
if state.shutdown {
return;
}
state.pending.push_back(job);
self.ready.notify_one();
}
fn next_job(&self) -> Option<AssetLoadJob> {
let mut state = self.state.lock().unwrap();
loop {
if let Some(job) = state.pending.pop_front() {
return Some(job);
}
if state.shutdown {
return None;
}
state = self.ready.wait(state).unwrap();
}
}
fn clear_pending(&self) {
self.state.lock().unwrap().pending.clear();
}
fn shutdown(&self) {
let mut state = self.state.lock().unwrap();
state.pending.clear();
state.shutdown = true;
self.ready.notify_all();
}
}
struct AssetLoadWorker {
queue: Arc<AssetLoadQueue>,
join_handle: Option<thread::JoinHandle<()>>,
}
impl AssetLoadWorker {
fn new(
handles: Arc<RwLock<HandleTable>>,
target_generations: Arc<RwLock<TargetGenerationTable>>,
assets_data: Arc<RwLock<AssetsPayloadSource>>,
gfx_policy: BankPolicy<GlyphBank>,
sound_policy: BankPolicy<SoundBank>,
scene_policy: BankPolicy<SceneBank>,
) -> Self {
let queue = Arc::new(AssetLoadQueue::default());
let worker_queue = Arc::clone(&queue);
let join_handle = thread::spawn(move || {
while let Some(job) = worker_queue.next_job() {
AssetManager::process_load_job(
job,
&handles,
&target_generations,
&assets_data,
&gfx_policy,
&sound_policy,
&scene_policy,
);
}
});
Self { queue, join_handle: Some(join_handle) }
}
fn submit(&self, job: AssetLoadJob) {
self.queue.submit(job);
}
fn clear_pending(&self) {
self.queue.clear_pending();
}
}
impl Drop for AssetLoadWorker {
fn drop(&mut self) {
self.queue.shutdown();
if let Some(join_handle) = self.join_handle.take() {
let _ = join_handle.join();
}
}
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -333,6 +446,21 @@ impl AssetManager {
asset_map.insert(entry.asset_id, entry); asset_map.insert(entry.asset_id, entry);
} }
let handles = Arc::new(RwLock::new(HashMap::new()));
let target_generations = Arc::new(RwLock::new(HashMap::new()));
let assets_data = Arc::new(RwLock::new(assets_data));
let gfx_policy = BankPolicy::new();
let sound_policy = BankPolicy::new();
let scene_policy = BankPolicy::new();
let load_worker = AssetLoadWorker::new(
Arc::clone(&handles),
Arc::clone(&target_generations),
Arc::clone(&assets_data),
gfx_policy.clone(),
sound_policy.clone(),
scene_policy.clone(),
);
Self { Self {
assets: Arc::new(RwLock::new(asset_map)), assets: Arc::new(RwLock::new(asset_map)),
gfx_installer, gfx_installer,
@ -342,12 +470,15 @@ impl AssetManager {
glyph_slot_index: GlyphAssetSlotIndex::new(), glyph_slot_index: GlyphAssetSlotIndex::new(),
sound_slots: Arc::new(RwLock::new(std::array::from_fn(|_| None))), sound_slots: Arc::new(RwLock::new(std::array::from_fn(|_| None))),
scene_slots: Arc::new(RwLock::new(std::array::from_fn(|_| None))), scene_slots: Arc::new(RwLock::new(std::array::from_fn(|_| None))),
gfx_policy: BankPolicy::new(), gfx_policy,
sound_policy: BankPolicy::new(), sound_policy,
scene_policy: BankPolicy::new(), scene_policy,
handles: Arc::new(RwLock::new(HashMap::new())), handles,
target_handles: Arc::new(RwLock::new(HashMap::new())),
target_generations,
next_handle_id: Mutex::new(1), next_handle_id: Mutex::new(1),
assets_data: Arc::new(RwLock::new(assets_data)), assets_data,
load_worker,
pending_commits: Mutex::new(Vec::new()), pending_commits: Mutex::new(Vec::new()),
} }
} }
@ -435,6 +566,51 @@ impl AssetManager {
} }
} }
fn handle_for_slot(&self, slot: SlotRef) -> HandleId {
if let Some(handle_id) = self.target_handles.read().unwrap().get(&slot).copied() {
return handle_id;
}
let mut target_handles = self.target_handles.write().unwrap();
if let Some(handle_id) = target_handles.get(&slot).copied() {
return handle_id;
}
let mut next_id = self.next_handle_id.lock().unwrap();
let handle_id = *next_id;
*next_id += 1;
target_handles.insert(slot, handle_id);
handle_id
}
fn bump_target_generation(&self, slot: SlotRef) -> u64 {
let mut generations = self.target_generations.write().unwrap();
let generation = generations.entry(slot).or_insert(0);
*generation += 1;
*generation
}
fn current_target_generation(
target_generations: &Arc<RwLock<TargetGenerationTable>>,
slot: SlotRef,
) -> u64 {
target_generations.read().unwrap().get(&slot).copied().unwrap_or(0)
}
fn is_current_target_generation(
target_generations: &Arc<RwLock<TargetGenerationTable>>,
slot: SlotRef,
request_generation: u64,
) -> bool {
Self::current_target_generation(target_generations, slot) == request_generation
}
fn clear_staging_for_handle(&self, handle_id: HandleId) {
self.gfx_policy.take_staging(handle_id);
self.sound_policy.take_staging(handle_id);
self.scene_policy.take_staging(handle_id);
}
pub fn load(&self, asset_id: AssetId, slot_index: usize) -> Result<HandleId, AssetLoadError> { pub fn load(&self, asset_id: AssetId, slot_index: usize) -> Result<HandleId, AssetLoadError> {
if slot_index >= 16 { if slot_index >= 16 {
return Err(AssetLoadError::SlotIndexInvalid); return Err(AssetLoadError::SlotIndexInvalid);
@ -449,9 +625,9 @@ impl AssetManager {
BankType::SCENE => SlotRef::scene(slot_index), BankType::SCENE => SlotRef::scene(slot_index),
}; };
let mut next_id = self.next_handle_id.lock().unwrap(); let handle_id = self.handle_for_slot(slot);
let handle_id = *next_id; let request_generation = self.bump_target_generation(slot);
*next_id += 1; self.clear_staging_for_handle(handle_id);
// Check if already resident (Dedup) // Check if already resident (Dedup)
let already_resident = match entry.bank_type { let already_resident = match entry.bank_type {
@ -484,7 +660,12 @@ impl AssetManager {
if already_resident { if already_resident {
self.handles.write().unwrap().insert( self.handles.write().unwrap().insert(
handle_id, handle_id,
LoadHandleInfo { _asset_id: asset_id, slot, status: LoadStatus::READY }, LoadHandleInfo {
_asset_id: asset_id,
slot,
status: LoadStatus::READY,
request_generation,
},
); );
return Ok(handle_id); return Ok(handle_id);
} }
@ -492,124 +673,173 @@ impl AssetManager {
// Not resident, start loading // Not resident, start loading
self.handles.write().unwrap().insert( self.handles.write().unwrap().insert(
handle_id, handle_id,
LoadHandleInfo { _asset_id: asset_id, slot, status: LoadStatus::PENDING }, LoadHandleInfo {
_asset_id: asset_id,
slot,
status: LoadStatus::PENDING,
request_generation,
},
); );
let handles = self.handles.clone(); self.load_worker.submit(AssetLoadJob {
let assets_data = self.assets_data.clone(); handle_id,
let entry_clone = entry.clone(); asset_id,
slot,
// Capture policies for the worker thread request_generation,
let gfx_policy = self.gfx_policy.clone(); entry,
let sound_policy = self.sound_policy.clone();
let scene_policy = self.scene_policy.clone();
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 {
return;
}
} else {
return;
}
}
match entry_clone.bank_type {
BankType::GLYPH => {
let result = Self::perform_load_glyph_bank(&entry_clone, assets_data);
if let Ok(tilebank) = result {
let bank_arc = Arc::new(tilebank);
let resident_arc = gfx_policy.put_resident(
asset_id,
bank_arc,
entry_clone.decoded_size as usize,
);
gfx_policy.stage(
handle_id,
resident_arc,
entry_clone.decoded_size as usize,
);
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;
}
}
} else {
let mut handles_map = handles.write().unwrap();
if let Some(h) = handles_map.get_mut(&handle_id) {
h.status = LoadStatus::ERROR;
}
}
}
BankType::SOUNDS => {
let result = Self::perform_load_sound_bank(&entry_clone, assets_data);
if let Ok(soundbank) = result {
let bank_arc = Arc::new(soundbank);
let resident_arc = sound_policy.put_resident(
asset_id,
bank_arc,
entry_clone.decoded_size as usize,
);
sound_policy.stage(
handle_id,
resident_arc,
entry_clone.decoded_size as usize,
);
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;
}
}
} else {
let mut handles_map = handles.write().unwrap();
if let Some(h) = handles_map.get_mut(&handle_id) {
h.status = LoadStatus::ERROR;
}
}
}
BankType::SCENE => {
let result = Self::perform_load_scene_bank(&entry_clone, assets_data);
if let Ok(scenebank) = result {
let bank_arc = Arc::new(scenebank);
let resident_arc = scene_policy.put_resident(
asset_id,
bank_arc,
entry_clone.decoded_size as usize,
);
scene_policy.stage(
handle_id,
resident_arc,
entry_clone.decoded_size as usize,
);
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;
}
}
} else {
let mut handles_map = handles.write().unwrap();
if let Some(h) = handles_map.get_mut(&handle_id) {
h.status = LoadStatus::ERROR;
}
}
}
}
}); });
Ok(handle_id) Ok(handle_id)
} }
fn process_load_job(
job: AssetLoadJob,
handles: &Arc<RwLock<HandleTable>>,
target_generations: &Arc<RwLock<TargetGenerationTable>>,
assets_data: &Arc<RwLock<AssetsPayloadSource>>,
gfx_policy: &BankPolicy<GlyphBank>,
sound_policy: &BankPolicy<SoundBank>,
scene_policy: &BankPolicy<SceneBank>,
) {
if !Self::is_current_target_generation(target_generations, job.slot, job.request_generation)
{
return;
}
{
let mut handles_map = handles.write().unwrap();
let Some(handle) = handles_map.get_mut(&job.handle_id) else {
return;
};
if handle.request_generation != job.request_generation
|| handle.status != LoadStatus::PENDING
{
return;
}
handle.status = LoadStatus::LOADING;
}
match job.entry.bank_type {
BankType::GLYPH => {
let result = Self::perform_load_glyph_bank(&job.entry, Arc::clone(assets_data));
match result {
Ok(tilebank) => {
if !Self::is_current_target_generation(
target_generations,
job.slot,
job.request_generation,
) {
return;
}
let bank_arc = Arc::new(tilebank);
let resident_arc = gfx_policy.put_resident(
job.asset_id,
bank_arc,
job.entry.decoded_size as usize,
);
if !Self::is_current_target_generation(
target_generations,
job.slot,
job.request_generation,
) {
return;
}
gfx_policy.stage(
job.handle_id,
resident_arc,
job.entry.decoded_size as usize,
);
Self::complete_load_job(handles, &job, LoadStatus::READY);
}
Err(_) => Self::complete_load_job(handles, &job, LoadStatus::ERROR),
}
}
BankType::SOUNDS => {
let result = Self::perform_load_sound_bank(&job.entry, Arc::clone(assets_data));
match result {
Ok(soundbank) => {
if !Self::is_current_target_generation(
target_generations,
job.slot,
job.request_generation,
) {
return;
}
let bank_arc = Arc::new(soundbank);
let resident_arc = sound_policy.put_resident(
job.asset_id,
bank_arc,
job.entry.decoded_size as usize,
);
if !Self::is_current_target_generation(
target_generations,
job.slot,
job.request_generation,
) {
return;
}
sound_policy.stage(
job.handle_id,
resident_arc,
job.entry.decoded_size as usize,
);
Self::complete_load_job(handles, &job, LoadStatus::READY);
}
Err(_) => Self::complete_load_job(handles, &job, LoadStatus::ERROR),
}
}
BankType::SCENE => {
let result = Self::perform_load_scene_bank(&job.entry, Arc::clone(assets_data));
match result {
Ok(scenebank) => {
if !Self::is_current_target_generation(
target_generations,
job.slot,
job.request_generation,
) {
return;
}
let bank_arc = Arc::new(scenebank);
let resident_arc = scene_policy.put_resident(
job.asset_id,
bank_arc,
job.entry.decoded_size as usize,
);
if !Self::is_current_target_generation(
target_generations,
job.slot,
job.request_generation,
) {
return;
}
scene_policy.stage(
job.handle_id,
resident_arc,
job.entry.decoded_size as usize,
);
Self::complete_load_job(handles, &job, LoadStatus::READY);
}
Err(_) => Self::complete_load_job(handles, &job, LoadStatus::ERROR),
}
}
}
}
fn complete_load_job(
handles: &Arc<RwLock<HandleTable>>,
job: &AssetLoadJob,
status: LoadStatus,
) {
let mut handles_map = handles.write().unwrap();
if let Some(handle) = handles_map.get_mut(&job.handle_id) {
if handle.request_generation == job.request_generation
&& handle.status == LoadStatus::LOADING
{
handle.status = status;
}
}
}
fn perform_load_glyph_bank( fn perform_load_glyph_bank(
entry: &AssetEntry, entry: &AssetEntry,
assets_data: Arc<RwLock<AssetsPayloadSource>>, assets_data: Arc<RwLock<AssetsPayloadSource>>,
@ -960,7 +1190,7 @@ impl AssetManager {
return AssetOpStatus::UnknownHandle; return AssetOpStatus::UnknownHandle;
}; };
if h.status == LoadStatus::READY { if h.status == LoadStatus::READY {
self.pending_commits.lock().unwrap().push(handle); self.pending_commits.lock().unwrap().push((handle, h.request_generation));
AssetOpStatus::Ok AssetOpStatus::Ok
} else { } else {
AssetOpStatus::InvalidState AssetOpStatus::InvalidState
@ -969,23 +1199,30 @@ impl AssetManager {
pub fn cancel(&self, handle: HandleId) -> AssetOpStatus { pub fn cancel(&self, handle: HandleId) -> AssetOpStatus {
let mut final_status = AssetOpStatus::UnknownHandle; let mut final_status = AssetOpStatus::UnknownHandle;
let mut handles_map = self.handles.write().unwrap(); let mut canceled_slot = None;
if let Some(h) = handles_map.get_mut(&handle) { {
final_status = match h.status { let mut handles_map = self.handles.write().unwrap();
LoadStatus::PENDING | LoadStatus::LOADING | LoadStatus::READY => AssetOpStatus::Ok, if let Some(h) = handles_map.get_mut(&handle) {
LoadStatus::CANCELED => AssetOpStatus::Ok, final_status = match h.status {
_ => AssetOpStatus::InvalidState, LoadStatus::PENDING | LoadStatus::LOADING | LoadStatus::READY => {
}; AssetOpStatus::Ok
match h.status { }
LoadStatus::PENDING | LoadStatus::LOADING | LoadStatus::READY => { LoadStatus::CANCELED => AssetOpStatus::Ok,
h.status = LoadStatus::CANCELED; _ => AssetOpStatus::InvalidState,
};
match h.status {
LoadStatus::PENDING | LoadStatus::LOADING | LoadStatus::READY => {
h.status = LoadStatus::CANCELED;
canceled_slot = Some(h.slot);
}
_ => {}
} }
_ => {}
} }
} }
self.gfx_policy.take_staging(handle); if let Some(slot) = canceled_slot {
self.sound_policy.take_staging(handle); self.bump_target_generation(slot);
self.scene_policy.take_staging(handle); }
self.clear_staging_for_handle(handle);
final_status final_status
} }
@ -993,9 +1230,9 @@ impl AssetManager {
let mut pending = self.pending_commits.lock().unwrap(); let mut pending = self.pending_commits.lock().unwrap();
let mut handles = self.handles.write().unwrap(); let mut handles = self.handles.write().unwrap();
for handle_id in pending.drain(..) { for (handle_id, request_generation) in pending.drain(..) {
if let Some(h) = handles.get_mut(&handle_id) { if let Some(h) = handles.get_mut(&handle_id) {
if h.status == LoadStatus::READY { if h.status == LoadStatus::READY && h.request_generation == request_generation {
match h.slot.asset_type { match h.slot.asset_type {
BankType::GLYPH => { BankType::GLYPH => {
if let Some((bank, _)) = self.gfx_policy.take_staging(handle_id) { if let Some((bank, _)) = self.gfx_policy.take_staging(handle_id) {
@ -1131,6 +1368,11 @@ impl AssetManager {
self.sound_policy.clear(); self.sound_policy.clear();
self.scene_policy.clear(); self.scene_policy.clear();
self.handles.write().unwrap().clear(); self.handles.write().unwrap().clear();
self.target_handles.write().unwrap().clear();
for generation in self.target_generations.write().unwrap().values_mut() {
*generation += 1;
}
self.load_worker.clear_pending();
self.pending_commits.lock().unwrap().clear(); self.pending_commits.lock().unwrap().clear();
self.gfx_slots.write().unwrap().fill(None); self.gfx_slots.write().unwrap().fill(None);
self.glyph_slot_index.clear(); self.glyph_slot_index.clear();
@ -1526,6 +1768,80 @@ mod tests {
assert!(Arc::ptr_eq(bank1, bank2)); assert!(Arc::ptr_eq(bank1, bank2));
} }
#[test]
fn test_asset_load_uses_stable_handle_for_target_slot() {
let banks = Arc::new(MemoryBanks::new());
let gfx_installer = Arc::clone(&banks) as Arc<dyn GlyphBankPoolInstaller>;
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
let scene_installer = Arc::clone(&banks) as Arc<dyn SceneBankPoolInstaller>;
let data = test_glyph_asset_data();
let asset_entry = test_glyph_asset_entry("test_glyphs", 16, 16);
let am = AssetManager::new(
vec![asset_entry],
AssetsPayloadSource::from_bytes(data),
gfx_installer,
sound_installer,
scene_installer,
);
let first_handle = am.load(0, 0).unwrap();
let second_handle = am.load(0, 0).unwrap();
assert_eq!(first_handle, second_handle);
let start = Instant::now();
while am.status(second_handle) != LoadStatus::READY && start.elapsed().as_secs() < 5 {
thread::sleep(std::time::Duration::from_millis(10));
}
assert_eq!(am.status(second_handle), LoadStatus::READY);
}
#[test]
fn test_asset_load_supersedes_previous_request_for_target_slot() {
let banks = Arc::new(MemoryBanks::new());
let gfx_installer = Arc::clone(&banks) as Arc<dyn GlyphBankPoolInstaller>;
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
let scene_installer = Arc::clone(&banks) as Arc<dyn SceneBankPoolInstaller>;
let first_payload = test_glyph_asset_data();
let second_payload = test_glyph_asset_data();
let second_offset = first_payload.len();
let mut data = first_payload;
data.extend_from_slice(&second_payload);
let first_asset = test_glyph_asset_entry("first_glyphs", 16, 16);
let mut second_asset = test_glyph_asset_entry("second_glyphs", 16, 16);
second_asset.asset_id = 1;
second_asset.offset = second_offset as u64;
let am = AssetManager::new(
vec![first_asset, second_asset],
AssetsPayloadSource::from_bytes(data),
gfx_installer,
sound_installer,
scene_installer,
);
let first_handle = am.load(0, 0).unwrap();
let second_handle = am.load(1, 0).unwrap();
assert_eq!(first_handle, second_handle);
let start = Instant::now();
while am.status(second_handle) != LoadStatus::READY && start.elapsed().as_secs() < 5 {
thread::sleep(std::time::Duration::from_millis(10));
}
assert_eq!(am.status(second_handle), LoadStatus::READY);
assert_eq!(am.commit(second_handle), AssetOpStatus::Ok);
am.apply_commits();
let slot = am.slot_info(SlotRef::gfx(0));
assert_eq!(slot.asset_id, Some(1));
}
#[test] #[test]
fn test_sound_asset_loading() { fn test_sound_asset_loading() {
let banks = Arc::new(MemoryBanks::new()); let banks = Arc::new(MemoryBanks::new());

View File

@ -18,7 +18,7 @@
{"type":"discussion","id":"DSC-0006","status":"open","ticket":"vm-owned-random-service","title":"Agenda - VM-Owned Random Service","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0005","file":"AGD-0005-vm-owned-random-service.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0006","status":"open","ticket":"vm-owned-random-service","title":"Agenda - VM-Owned Random Service","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0005","file":"AGD-0005-vm-owned-random-service.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0007","status":"open","ticket":"app-home-filesystem-surface-and-semantics","title":"Agenda - App Home Filesystem Surface and Semantics","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0006","file":"AGD-0006-app-home-filesystem-surface-and-semantics.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0007","status":"open","ticket":"app-home-filesystem-surface-and-semantics","title":"Agenda - App Home Filesystem Surface and Semantics","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0006","file":"AGD-0006-app-home-filesystem-surface-and-semantics.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0008","status":"done","ticket":"perf-runtime-telemetry-hot-path","title":"Agenda - [PERF] Runtime Telemetry Hot Path","created_at":"2026-03-27","updated_at":"2026-06-04","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0026","file":"discussion/lessons/DSC-0008-perf-runtime-telemetry-hot-path/LSN-0026-push-based-telemetry-model.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]} {"type":"discussion","id":"DSC-0008","status":"done","ticket":"perf-runtime-telemetry-hot-path","title":"Agenda - [PERF] Runtime Telemetry Hot Path","created_at":"2026-03-27","updated_at":"2026-06-04","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0026","file":"discussion/lessons/DSC-0008-perf-runtime-telemetry-hot-path/LSN-0026-push-based-telemetry-model.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
{"type":"discussion","id":"DSC-0009","status":"in_progress","ticket":"perf-async-background-work-lanes-for-assets-and-fs","title":"Agenda - [PERF] Async Background Work Lanes for Assets and FS","created_at":"2026-03-27","updated_at":"2026-06-28","tags":["perf","asset","fs","async","scheduler","runtime"],"agendas":[{"id":"AGD-0008","file":"AGD-0008-perf-async-background-work-lanes-for-assets-and-fs.md","status":"accepted","created_at":"2026-03-27","updated_at":"2026-06-28"}],"decisions":[{"id":"DEC-0034","file":"DEC-0034-async-work-lane-and-asset-backlog-contract.md","status":"accepted","created_at":"2026-06-28","updated_at":"2026-06-28","ref_agenda":"AGD-0008"}],"plans":[{"id":"PLN-0123","file":"PLN-0123-async-work-lane-specification-propagation.md","status":"done","created_at":"2026-06-28","updated_at":"2026-06-28","ref_decisions":["DEC-0034"]},{"id":"PLN-0124","file":"PLN-0124-async-work-lane-runtime-infrastructure.md","status":"done","created_at":"2026-06-28","updated_at":"2026-06-28","ref_decisions":["DEC-0034"]},{"id":"PLN-0125","file":"PLN-0125-asset-backlog-and-stable-slot-handles.md","status":"open","created_at":"2026-06-28","updated_at":"2026-06-28","ref_decisions":["DEC-0034"]},{"id":"PLN-0126","file":"PLN-0126-asset-backlog-public-api-and-status-surface.md","status":"open","created_at":"2026-06-28","updated_at":"2026-06-28","ref_decisions":["DEC-0034"]},{"id":"PLN-0127","file":"PLN-0127-async-lane-memcard-and-fs-integration-boundaries.md","status":"open","created_at":"2026-06-28","updated_at":"2026-06-28","ref_decisions":["DEC-0034"]},{"id":"PLN-0128","file":"PLN-0128-async-asset-pipeline-contract-tests-and-telemetry.md","status":"open","created_at":"2026-06-28","updated_at":"2026-06-28","ref_decisions":["DEC-0034"]}],"lessons":[]} {"type":"discussion","id":"DSC-0009","status":"in_progress","ticket":"perf-async-background-work-lanes-for-assets-and-fs","title":"Agenda - [PERF] Async Background Work Lanes for Assets and FS","created_at":"2026-03-27","updated_at":"2026-06-28","tags":["perf","asset","fs","async","scheduler","runtime"],"agendas":[{"id":"AGD-0008","file":"AGD-0008-perf-async-background-work-lanes-for-assets-and-fs.md","status":"accepted","created_at":"2026-03-27","updated_at":"2026-06-28"}],"decisions":[{"id":"DEC-0034","file":"DEC-0034-async-work-lane-and-asset-backlog-contract.md","status":"accepted","created_at":"2026-06-28","updated_at":"2026-06-28","ref_agenda":"AGD-0008"}],"plans":[{"id":"PLN-0123","file":"PLN-0123-async-work-lane-specification-propagation.md","status":"done","created_at":"2026-06-28","updated_at":"2026-06-28","ref_decisions":["DEC-0034"]},{"id":"PLN-0124","file":"PLN-0124-async-work-lane-runtime-infrastructure.md","status":"done","created_at":"2026-06-28","updated_at":"2026-06-28","ref_decisions":["DEC-0034"]},{"id":"PLN-0125","file":"PLN-0125-asset-backlog-and-stable-slot-handles.md","status":"done","created_at":"2026-06-28","updated_at":"2026-06-28","ref_decisions":["DEC-0034"]},{"id":"PLN-0126","file":"PLN-0126-asset-backlog-public-api-and-status-surface.md","status":"open","created_at":"2026-06-28","updated_at":"2026-06-28","ref_decisions":["DEC-0034"]},{"id":"PLN-0127","file":"PLN-0127-async-lane-memcard-and-fs-integration-boundaries.md","status":"open","created_at":"2026-06-28","updated_at":"2026-06-28","ref_decisions":["DEC-0034"]},{"id":"PLN-0128","file":"PLN-0128-async-asset-pipeline-contract-tests-and-telemetry.md","status":"open","created_at":"2026-06-28","updated_at":"2026-06-28","ref_decisions":["DEC-0034"]}],"lessons":[]}
{"type":"discussion","id":"DSC-0010","status":"done","ticket":"perf-host-desktop-frame-pacing-and-presentation","title":"Agenda - [PERF] Host Desktop Frame Pacing and Presentation","created_at":"2026-03-27","updated_at":"2026-04-20","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0036","file":"discussion/lessons/DSC-0010-perf-host-desktop-frame-pacing-and-presentation/LSN-0036-frame-publication-and-host-invalidation-must-be-separate.md","status":"done","created_at":"2026-04-20","updated_at":"2026-04-20"}]} {"type":"discussion","id":"DSC-0010","status":"done","ticket":"perf-host-desktop-frame-pacing-and-presentation","title":"Agenda - [PERF] Host Desktop Frame Pacing and Presentation","created_at":"2026-03-27","updated_at":"2026-04-20","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0036","file":"discussion/lessons/DSC-0010-perf-host-desktop-frame-pacing-and-presentation/LSN-0036-frame-publication-and-host-invalidation-must-be-separate.md","status":"done","created_at":"2026-04-20","updated_at":"2026-04-20"}]}
{"type":"discussion","id":"DSC-0011","status":"abandoned","ticket":"perf-gfx-render-pipeline-and-dirty-regions","title":"Agenda - [PERF] GFX Render Pipeline and Dirty Regions","created_at":"2026-03-27","updated_at":"2026-06-28","tags":[],"agendas":[{"id":"AGD-0010","file":"AGD-0010-perf-gfx-render-pipeline-and-dirty-regions.md","status":"abandoned","created_at":"2026-03-27","updated_at":"2026-06-28","_override_reason":"User explicitly chose to remove this agenda because it no longer applies to the current render scenario; future optimizations can be discussed when needed."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to abandon this discussion because it no longer applies to the current render scenario; future optimizations can be discussed when needed."} {"type":"discussion","id":"DSC-0011","status":"abandoned","ticket":"perf-gfx-render-pipeline-and-dirty-regions","title":"Agenda - [PERF] GFX Render Pipeline and Dirty Regions","created_at":"2026-03-27","updated_at":"2026-06-28","tags":[],"agendas":[{"id":"AGD-0010","file":"AGD-0010-perf-gfx-render-pipeline-and-dirty-regions.md","status":"abandoned","created_at":"2026-03-27","updated_at":"2026-06-28","_override_reason":"User explicitly chose to remove this agenda because it no longer applies to the current render scenario; future optimizations can be discussed when needed."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to abandon this discussion because it no longer applies to the current render scenario; future optimizations can be discussed when needed."}
{"type":"discussion","id":"DSC-0012","status":"done","ticket":"perf-runtime-introspection-syscalls","title":"Agenda - [PERF] Runtime Introspection Syscalls","created_at":"2026-03-27","updated_at":"2026-04-19","tags":["perf","runtime","syscall","telemetry","debug","asset"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0034","file":"discussion/lessons/DSC-0012-perf-runtime-introspection-syscalls/LSN-0034-host-owned-debug-boundaries.md","status":"done","created_at":"2026-04-19","updated_at":"2026-04-19"}]} {"type":"discussion","id":"DSC-0012","status":"done","ticket":"perf-runtime-introspection-syscalls","title":"Agenda - [PERF] Runtime Introspection Syscalls","created_at":"2026-03-27","updated_at":"2026-04-19","tags":["perf","runtime","syscall","telemetry","debug","asset"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0034","file":"discussion/lessons/DSC-0012-perf-runtime-introspection-syscalls/LSN-0034-host-owned-debug-boundaries.md","status":"done","created_at":"2026-04-19","updated_at":"2026-04-19"}]}

View File

@ -2,7 +2,7 @@
id: PLN-0125 id: PLN-0125
ticket: perf-async-background-work-lanes-for-assets-and-fs ticket: perf-async-background-work-lanes-for-assets-and-fs
title: Asset Backlog and Stable Slot Handles title: Asset Backlog and Stable Slot Handles
status: open status: done
created: 2026-06-28 created: 2026-06-28
completed: completed:
ref_decisions: [DEC-0034] ref_decisions: [DEC-0034]