diff --git a/crates/console/prometeu-drivers/src/asset.rs b/crates/console/prometeu-drivers/src/asset.rs index 5adb1f12..c1dff3c7 100644 --- a/crates/console/prometeu-drivers/src/asset.rs +++ b/crates/console/prometeu-drivers/src/asset.rs @@ -18,10 +18,10 @@ use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer}; use prometeu_hal::sound_bank::SoundBank; use prometeu_hal::tile::Tile; use prometeu_hal::tilemap::TileMap; -use std::collections::HashMap; +use std::collections::{HashMap, VecDeque}; use std::io::Read; use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::{Arc, Mutex, RwLock}; +use std::sync::{Arc, Condvar, Mutex, RwLock}; use std::thread; use std::time::Instant; @@ -31,6 +31,8 @@ type StagingMap = HashMap>; type AssetTable = HashMap; type HandleTable = HashMap; +type TargetHandleTable = HashMap; +type TargetGenerationTable = HashMap; #[derive(Clone, Default)] pub struct GlyphAssetSlotIndex { @@ -182,6 +184,8 @@ impl BankPolicy { pub struct AssetManager { assets: Arc>, handles: Arc>, + target_handles: Arc>, + target_generations: Arc>, next_handle_id: Mutex, assets_data: Arc>, @@ -203,14 +207,123 @@ pub struct AssetManager { /// Residency policy for scene banks. scene_policy: BankPolicy, + load_worker: AssetLoadWorker, + // Commits that are ready to be applied at the next frame boundary. - pending_commits: Mutex>, + pending_commits: Mutex>, } struct LoadHandleInfo { _asset_id: AssetId, slot: SlotRef, 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, + shutdown: bool, +} + +#[derive(Default)] +struct AssetLoadQueue { + state: Mutex, + 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 { + 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, + join_handle: Option>, +} + +impl AssetLoadWorker { + fn new( + handles: Arc>, + target_generations: Arc>, + assets_data: Arc>, + gfx_policy: BankPolicy, + sound_policy: BankPolicy, + scene_policy: BankPolicy, + ) -> 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)] @@ -333,6 +446,21 @@ impl AssetManager { 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 { assets: Arc::new(RwLock::new(asset_map)), gfx_installer, @@ -342,12 +470,15 @@ impl AssetManager { glyph_slot_index: GlyphAssetSlotIndex::new(), sound_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(), - sound_policy: BankPolicy::new(), - scene_policy: BankPolicy::new(), - handles: Arc::new(RwLock::new(HashMap::new())), + gfx_policy, + sound_policy, + scene_policy, + handles, + target_handles: Arc::new(RwLock::new(HashMap::new())), + target_generations, next_handle_id: Mutex::new(1), - assets_data: Arc::new(RwLock::new(assets_data)), + assets_data, + load_worker, 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>, + slot: SlotRef, + ) -> u64 { + target_generations.read().unwrap().get(&slot).copied().unwrap_or(0) + } + + fn is_current_target_generation( + target_generations: &Arc>, + 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 { if slot_index >= 16 { return Err(AssetLoadError::SlotIndexInvalid); @@ -449,9 +625,9 @@ impl AssetManager { BankType::SCENE => SlotRef::scene(slot_index), }; - let mut next_id = self.next_handle_id.lock().unwrap(); - let handle_id = *next_id; - *next_id += 1; + let handle_id = self.handle_for_slot(slot); + let request_generation = self.bump_target_generation(slot); + self.clear_staging_for_handle(handle_id); // Check if already resident (Dedup) let already_resident = match entry.bank_type { @@ -484,7 +660,12 @@ impl AssetManager { if already_resident { self.handles.write().unwrap().insert( 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); } @@ -492,124 +673,173 @@ impl AssetManager { // Not resident, start loading self.handles.write().unwrap().insert( 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(); - let assets_data = self.assets_data.clone(); - let entry_clone = entry.clone(); - - // Capture policies for the worker thread - let gfx_policy = self.gfx_policy.clone(); - 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; - } - } - } - } + self.load_worker.submit(AssetLoadJob { + handle_id, + asset_id, + slot, + request_generation, + entry, }); Ok(handle_id) } + fn process_load_job( + job: AssetLoadJob, + handles: &Arc>, + target_generations: &Arc>, + assets_data: &Arc>, + gfx_policy: &BankPolicy, + sound_policy: &BankPolicy, + scene_policy: &BankPolicy, + ) { + 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>, + 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( entry: &AssetEntry, assets_data: Arc>, @@ -960,7 +1190,7 @@ impl AssetManager { return AssetOpStatus::UnknownHandle; }; if h.status == LoadStatus::READY { - self.pending_commits.lock().unwrap().push(handle); + self.pending_commits.lock().unwrap().push((handle, h.request_generation)); AssetOpStatus::Ok } else { AssetOpStatus::InvalidState @@ -969,23 +1199,30 @@ impl AssetManager { pub fn cancel(&self, handle: HandleId) -> AssetOpStatus { let mut final_status = AssetOpStatus::UnknownHandle; - let mut handles_map = self.handles.write().unwrap(); - if let Some(h) = handles_map.get_mut(&handle) { - final_status = match h.status { - LoadStatus::PENDING | LoadStatus::LOADING | LoadStatus::READY => AssetOpStatus::Ok, - LoadStatus::CANCELED => AssetOpStatus::Ok, - _ => AssetOpStatus::InvalidState, - }; - match h.status { - LoadStatus::PENDING | LoadStatus::LOADING | LoadStatus::READY => { - h.status = LoadStatus::CANCELED; + let mut canceled_slot = None; + { + let mut handles_map = self.handles.write().unwrap(); + if let Some(h) = handles_map.get_mut(&handle) { + final_status = match h.status { + LoadStatus::PENDING | LoadStatus::LOADING | LoadStatus::READY => { + AssetOpStatus::Ok + } + LoadStatus::CANCELED => AssetOpStatus::Ok, + _ => 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); - self.sound_policy.take_staging(handle); - self.scene_policy.take_staging(handle); + if let Some(slot) = canceled_slot { + self.bump_target_generation(slot); + } + self.clear_staging_for_handle(handle); final_status } @@ -993,9 +1230,9 @@ impl AssetManager { let mut pending = self.pending_commits.lock().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 h.status == LoadStatus::READY { + if h.status == LoadStatus::READY && h.request_generation == request_generation { match h.slot.asset_type { BankType::GLYPH => { if let Some((bank, _)) = self.gfx_policy.take_staging(handle_id) { @@ -1131,6 +1368,11 @@ impl AssetManager { self.sound_policy.clear(); self.scene_policy.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.gfx_slots.write().unwrap().fill(None); self.glyph_slot_index.clear(); @@ -1526,6 +1768,80 @@ mod tests { 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; + let sound_installer = Arc::clone(&banks) as Arc; + let scene_installer = Arc::clone(&banks) as Arc; + + 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; + let sound_installer = Arc::clone(&banks) as Arc; + let scene_installer = Arc::clone(&banks) as Arc; + + 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] fn test_sound_asset_loading() { let banks = Arc::new(MemoryBanks::new()); diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 109b9620..95b9972c 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -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-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-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-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"}]} diff --git a/discussion/workflow/plans/PLN-0125-asset-backlog-and-stable-slot-handles.md b/discussion/workflow/plans/PLN-0125-asset-backlog-and-stable-slot-handles.md index 65bb3669..df656308 100644 --- a/discussion/workflow/plans/PLN-0125-asset-backlog-and-stable-slot-handles.md +++ b/discussion/workflow/plans/PLN-0125-asset-backlog-and-stable-slot-handles.md @@ -2,7 +2,7 @@ id: PLN-0125 ticket: perf-async-background-work-lanes-for-assets-and-fs title: Asset Backlog and Stable Slot Handles -status: open +status: done created: 2026-06-28 completed: ref_decisions: [DEC-0034]