From 961557583b6522d4729205f76dbb33f27f96ca3c Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sun, 28 Jun 2026 14:08:30 +0100 Subject: [PATCH] implements PLN-0126 asset backlog public api --- crates/console/prometeu-drivers/src/asset.rs | 334 +++++++++++++++++- crates/console/prometeu-hal/src/asset.rs | 36 ++ .../console/prometeu-hal/src/asset_bridge.rs | 11 +- crates/console/prometeu-hal/src/syscalls.rs | 6 + .../src/syscalls/domains/asset.rs | 36 ++ .../prometeu-hal/src/syscalls/registry.rs | 12 + .../prometeu-hal/src/syscalls/tests.rs | 24 ++ .../src/services/vm_runtime/dispatch.rs | 58 +++ .../services/vm_runtime/tests_asset_bank.rs | 96 +++++ discussion/index.ndjson | 2 +- ...t-backlog-public-api-and-status-surface.md | 2 +- 11 files changed, 604 insertions(+), 13 deletions(-) diff --git a/crates/console/prometeu-drivers/src/asset.rs b/crates/console/prometeu-drivers/src/asset.rs index c1dff3c7..4265b092 100644 --- a/crates/console/prometeu-drivers/src/asset.rs +++ b/crates/console/prometeu-drivers/src/asset.rs @@ -2,11 +2,11 @@ use crate::memory_banks::{GlyphBankPoolInstaller, SceneBankPoolInstaller, SoundBankPoolInstaller}; use prometeu_hal::AssetBridge; use prometeu_hal::asset::{ - AssetCodec, AssetEntry, AssetId, AssetLoadError, AssetOpStatus, BankTelemetry, BankType, - HandleId, LoadStatus, PreloadEntry, SCENE_DECODED_LAYER_OVERHEAD_BYTES_V1, - SCENE_HEADER_BYTES_V1, SCENE_LAYER_COUNT_V1, SCENE_LAYER_HEADER_BYTES_V1, - SCENE_PAYLOAD_MAGIC_V1, SCENE_PAYLOAD_VERSION_V1, SCENE_TILE_RECORD_BYTES_V1, SlotRef, - SlotStats, + AssetBacklogInfo, AssetBacklogPosition, AssetCodec, AssetEntry, AssetId, AssetLoadError, + AssetOpStatus, AssetTargetStatus, BankTelemetry, BankType, HandleId, LoadStatus, PreloadEntry, + SCENE_DECODED_LAYER_OVERHEAD_BYTES_V1, SCENE_HEADER_BYTES_V1, SCENE_LAYER_COUNT_V1, + SCENE_LAYER_HEADER_BYTES_V1, SCENE_PAYLOAD_MAGIC_V1, SCENE_PAYLOAD_VERSION_V1, + SCENE_TILE_RECORD_BYTES_V1, SlotRef, SlotStats, }; use prometeu_hal::cartridge::AssetsPayloadSource; use prometeu_hal::color::Color; @@ -220,6 +220,7 @@ struct LoadHandleInfo { request_generation: u64, } +#[derive(Clone)] struct AssetLoadJob { handle_id: HandleId, asset_id: AssetId, @@ -231,6 +232,7 @@ struct AssetLoadJob { #[derive(Default)] struct AssetLoadQueueState { pending: VecDeque, + active: Option, shutdown: bool, } @@ -246,6 +248,7 @@ impl AssetLoadQueue { if state.shutdown { return; } + state.pending.retain(|pending| pending.slot != job.slot); state.pending.push_back(job); self.ready.notify_one(); } @@ -254,6 +257,7 @@ impl AssetLoadQueue { let mut state = self.state.lock().unwrap(); loop { if let Some(job) = state.pending.pop_front() { + state.active = Some(job.clone()); return Some(job); } if state.shutdown { @@ -267,6 +271,51 @@ impl AssetLoadQueue { self.state.lock().unwrap().pending.clear(); } + fn complete_active(&self, handle_id: HandleId, request_generation: u64) { + let mut state = self.state.lock().unwrap(); + if state.active.as_ref().is_some_and(|job| { + job.handle_id == handle_id && job.request_generation == request_generation + }) { + state.active = None; + } + } + + fn pending_count(&self) -> usize { + self.state.lock().unwrap().pending.len() + } + + fn active_job(&self) -> Option { + self.state.lock().unwrap().active.clone() + } + + fn pending_position(&self, handle_id: HandleId) -> Option { + self.state + .lock() + .unwrap() + .pending + .iter() + .position(|job| job.handle_id == handle_id) + .map(|index| index + 1) + } + + fn move_pending(&self, handle_id: HandleId, new_position: usize) -> bool { + if new_position == 0 { + return false; + } + + let mut state = self.state.lock().unwrap(); + let Some(current_index) = state.pending.iter().position(|job| job.handle_id == handle_id) + else { + return false; + }; + let Some(job) = state.pending.remove(current_index) else { + return false; + }; + let insert_index = new_position.saturating_sub(1).min(state.pending.len()); + state.pending.insert(insert_index, job); + true + } + fn shutdown(&self) { let mut state = self.state.lock().unwrap(); state.pending.clear(); @@ -294,7 +343,7 @@ impl AssetLoadWorker { let join_handle = thread::spawn(move || { while let Some(job) = worker_queue.next_job() { AssetManager::process_load_job( - job, + job.clone(), &handles, &target_generations, &assets_data, @@ -302,6 +351,7 @@ impl AssetLoadWorker { &sound_policy, &scene_policy, ); + worker_queue.complete_active(job.handle_id, job.request_generation); } }); @@ -315,6 +365,22 @@ impl AssetLoadWorker { fn clear_pending(&self) { self.queue.clear_pending(); } + + fn pending_count(&self) -> usize { + self.queue.pending_count() + } + + fn active_job(&self) -> Option { + self.queue.active_job() + } + + fn pending_position(&self, handle_id: HandleId) -> Option { + self.queue.pending_position(handle_id) + } + + fn move_pending(&self, handle_id: HandleId, new_position: usize) -> bool { + self.queue.move_pending(handle_id, new_position) + } } impl Drop for AssetLoadWorker { @@ -353,6 +419,24 @@ impl AssetBridge for AssetManager { fn cancel(&self, handle: HandleId) -> AssetOpStatus { self.cancel(handle) } + fn backlog_info(&self) -> AssetBacklogInfo { + self.backlog_info() + } + fn backlog_position(&self, handle: HandleId) -> AssetBacklogPosition { + self.backlog_position(handle) + } + fn backlog_move(&self, handle: HandleId, new_position: usize) -> AssetOpStatus { + self.backlog_move(handle, new_position) + } + fn backlog_promote(&self, handle: HandleId) -> AssetOpStatus { + self.backlog_promote(handle) + } + fn backlog_demote(&self, handle: HandleId) -> AssetOpStatus { + self.backlog_demote(handle) + } + fn target_status(&self, bank_type: BankType, slot: usize) -> AssetTargetStatus { + self.target_status(bank_type, slot) + } fn apply_commits(&self) { self.apply_commits() } @@ -611,6 +695,26 @@ impl AssetManager { self.scene_policy.take_staging(handle_id); } + fn target_for_handle(&self, handle_id: HandleId) -> Option { + self.target_handles + .read() + .unwrap() + .iter() + .find_map(|(slot, target_handle)| (*target_handle == handle_id).then_some(*slot)) + } + + fn slot_asset_id(&self, slot: SlotRef) -> Option { + match slot.asset_type { + BankType::GLYPH => self.gfx_slots.read().unwrap().get(slot.index).and_then(|s| *s), + BankType::SOUNDS => self.sound_slots.read().unwrap().get(slot.index).and_then(|s| *s), + BankType::SCENE => self.scene_slots.read().unwrap().get(slot.index).and_then(|s| *s), + } + } + + fn idle_target_state(&self, slot: SlotRef) -> LoadStatus { + if self.slot_asset_id(slot).is_some() { LoadStatus::COMMITTED } else { LoadStatus::EMPTY } + } + pub fn load(&self, asset_id: AssetId, slot_index: usize) -> Result { if slot_index >= 16 { return Err(AssetLoadError::SlotIndexInvalid); @@ -1176,12 +1280,131 @@ impl AssetManager { } pub fn status(&self, handle: HandleId) -> LoadStatus { - self.handles + if let Some(status) = self.handles.read().unwrap().get(&handle).map(|h| h.status) { + return status; + } + + self.target_for_handle(handle) + .map(|slot| self.idle_target_state(slot)) + .unwrap_or(LoadStatus::UnknownHandle) + } + + pub fn backlog_info(&self) -> AssetBacklogInfo { + let active = self.load_worker.active_job(); + AssetBacklogInfo { + status: AssetOpStatus::Ok, + pending_count: self.load_worker.pending_count() as u32, + active_handle: active.as_ref().map(|job| job.handle_id).unwrap_or(0), + active_asset_id: active.as_ref().map(|job| job.asset_id), + active_bank_type: active.as_ref().map(|job| job.slot.asset_type), + active_slot: active.as_ref().map(|job| job.slot.index), + active_progress: 0, + } + } + + pub fn backlog_position(&self, handle: HandleId) -> AssetBacklogPosition { + let Some(slot) = self.target_for_handle(handle) else { + return AssetBacklogPosition { + status: AssetOpStatus::UnknownHandle, + state: LoadStatus::UnknownHandle, + position: 0, + progress: 0, + }; + }; + + if let Some(position) = self.load_worker.pending_position(handle) { + return AssetBacklogPosition { + status: AssetOpStatus::Ok, + state: LoadStatus::QUEUED, + position: position as u32, + progress: 0, + }; + } + + if self.load_worker.active_job().as_ref().is_some_and(|job| job.handle_id == handle) { + return AssetBacklogPosition { + status: AssetOpStatus::Ok, + state: LoadStatus::ACTIVE, + position: 0, + progress: 0, + }; + } + + AssetBacklogPosition { + status: AssetOpStatus::Ok, + state: self + .handles + .read() + .unwrap() + .get(&handle) + .map(|h| h.status) + .unwrap_or_else(|| self.idle_target_state(slot)), + position: 0, + progress: 0, + } + } + + pub fn backlog_move(&self, handle: HandleId, new_position: usize) -> AssetOpStatus { + if self.target_for_handle(handle).is_none() { + return AssetOpStatus::UnknownHandle; + } + if self.load_worker.move_pending(handle, new_position) { + AssetOpStatus::Ok + } else { + AssetOpStatus::InvalidState + } + } + + pub fn backlog_promote(&self, handle: HandleId) -> AssetOpStatus { + self.backlog_move(handle, 1) + } + + pub fn backlog_demote(&self, handle: HandleId) -> AssetOpStatus { + if self.target_for_handle(handle).is_none() { + return AssetOpStatus::UnknownHandle; + } + let new_position = self.load_worker.pending_count(); + if new_position == 0 { + return AssetOpStatus::InvalidState; + } + self.backlog_move(handle, new_position) + } + + pub fn target_status(&self, bank_type: BankType, slot: usize) -> AssetTargetStatus { + if slot >= 16 { + return AssetTargetStatus { + status: AssetOpStatus::InvalidState, + asset_id: None, + handle: 0, + state: LoadStatus::INVALID, + position: 0, + progress: 0, + }; + } + + let slot_ref = match bank_type { + BankType::GLYPH => SlotRef::gfx(slot), + BankType::SOUNDS => SlotRef::audio(slot), + BankType::SCENE => SlotRef::scene(slot), + }; + let handle = self.handle_for_slot(slot_ref); + let position = self.backlog_position(handle); + let asset_id = self + .handles .read() .unwrap() .get(&handle) - .map(|h| h.status) - .unwrap_or(LoadStatus::UnknownHandle) + .map(|h| h._asset_id) + .or_else(|| self.slot_asset_id(slot_ref)); + + AssetTargetStatus { + status: AssetOpStatus::Ok, + asset_id, + handle, + state: position.state, + position: position.position, + progress: position.progress, + } } pub fn commit(&self, handle: HandleId) -> AssetOpStatus { @@ -1842,6 +2065,99 @@ mod tests { assert_eq!(slot.asset_id, Some(1)); } + #[test] + fn test_asset_backlog_queue_reorders_pending_requests() { + let queue = AssetLoadQueue::default(); + let entry = test_glyph_asset_entry("queued_glyphs", 16, 16); + let job = |handle_id: HandleId, slot: usize| AssetLoadJob { + handle_id, + asset_id: handle_id as AssetId, + slot: SlotRef::gfx(slot), + request_generation: handle_id as u64, + entry: entry.clone(), + }; + + queue.submit(job(1, 0)); + queue.submit(job(2, 1)); + queue.submit(job(3, 2)); + + assert_eq!(queue.pending_position(3), Some(3)); + assert!(queue.move_pending(3, 1)); + assert_eq!(queue.pending_position(3), Some(1)); + assert_eq!(queue.pending_position(1), Some(2)); + assert!(queue.move_pending(3, queue.pending_count())); + assert_eq!(queue.pending_position(3), Some(3)); + } + + #[test] + fn test_asset_target_status_exposes_empty_and_ready_slot_handle() { + 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 empty = am.target_status(BankType::GLYPH, 0); + assert_eq!(empty.status, AssetOpStatus::Ok); + assert_eq!(empty.asset_id, None); + assert_eq!(empty.state, LoadStatus::EMPTY); + assert_ne!(empty.handle, 0); + assert_eq!(am.status(empty.handle), LoadStatus::EMPTY); + + let handle = am.load(0, 0).unwrap(); + assert_eq!(handle, empty.handle); + + let start = Instant::now(); + while am.status(handle) != LoadStatus::READY && start.elapsed().as_secs() < 5 { + thread::sleep(std::time::Duration::from_millis(10)); + } + + let ready = am.target_status(BankType::GLYPH, 0); + assert_eq!(ready.status, AssetOpStatus::Ok); + assert_eq!(ready.asset_id, Some(0)); + assert_eq!(ready.handle, handle); + assert_eq!(ready.state, LoadStatus::READY); + } + + #[test] + fn test_asset_backlog_reorder_rejects_non_queued_request() { + 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 handle = am.load(0, 0).unwrap(); + let start = Instant::now(); + while am.status(handle) != LoadStatus::READY && start.elapsed().as_secs() < 5 { + thread::sleep(std::time::Duration::from_millis(10)); + } + + assert_eq!(am.backlog_promote(handle), AssetOpStatus::InvalidState); + assert_eq!(am.backlog_demote(handle), AssetOpStatus::InvalidState); + assert_eq!(am.backlog_move(999, 1), AssetOpStatus::UnknownHandle); + } + #[test] fn test_sound_asset_loading() { let banks = Arc::new(MemoryBanks::new()); diff --git a/crates/console/prometeu-hal/src/asset.rs b/crates/console/prometeu-hal/src/asset.rs index aa451b0f..b49483f8 100644 --- a/crates/console/prometeu-hal/src/asset.rs +++ b/crates/console/prometeu-hal/src/asset.rs @@ -112,6 +112,12 @@ pub enum LoadStatus { CANCELED = 4, ERROR = 5, UnknownHandle = 6, + QUEUED = 7, + ACTIVE = 8, + SUPERSEDED = 9, + EMPTY = 10, + INVALID = 11, + BackendUnavailable = 12, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -129,6 +135,36 @@ pub enum AssetOpStatus { Ok = 0, UnknownHandle = 1, InvalidState = 2, + Superseded = 3, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AssetBacklogInfo { + pub status: AssetOpStatus, + pub pending_count: u32, + pub active_handle: HandleId, + pub active_asset_id: Option, + pub active_bank_type: Option, + pub active_slot: Option, + pub active_progress: u8, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AssetBacklogPosition { + pub status: AssetOpStatus, + pub state: LoadStatus, + pub position: u32, + pub progress: u8, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AssetTargetStatus { + pub status: AssetOpStatus, + pub asset_id: Option, + pub handle: HandleId, + pub state: LoadStatus, + pub position: u32, + pub progress: u8, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/crates/console/prometeu-hal/src/asset_bridge.rs b/crates/console/prometeu-hal/src/asset_bridge.rs index 5c80e3fe..ad4f51f3 100644 --- a/crates/console/prometeu-hal/src/asset_bridge.rs +++ b/crates/console/prometeu-hal/src/asset_bridge.rs @@ -1,6 +1,7 @@ use crate::asset::{ - AssetEntry, AssetId, AssetLoadError, AssetOpStatus, BankTelemetry, HandleId, LoadStatus, - PreloadEntry, SlotRef, SlotStats, + AssetBacklogInfo, AssetBacklogPosition, AssetEntry, AssetId, AssetLoadError, AssetOpStatus, + AssetTargetStatus, BankTelemetry, BankType, HandleId, LoadStatus, PreloadEntry, SlotRef, + SlotStats, }; use crate::cartridge::AssetsPayloadSource; @@ -15,6 +16,12 @@ pub trait AssetBridge { fn status(&self, handle: HandleId) -> LoadStatus; fn commit(&self, handle: HandleId) -> AssetOpStatus; fn cancel(&self, handle: HandleId) -> AssetOpStatus; + fn backlog_info(&self) -> AssetBacklogInfo; + fn backlog_position(&self, handle: HandleId) -> AssetBacklogPosition; + fn backlog_move(&self, handle: HandleId, new_position: usize) -> AssetOpStatus; + fn backlog_promote(&self, handle: HandleId) -> AssetOpStatus; + fn backlog_demote(&self, handle: HandleId) -> AssetOpStatus; + fn target_status(&self, bank_type: BankType, slot: usize) -> AssetTargetStatus; fn apply_commits(&self); fn bank_telemetry(&self) -> Vec; fn slot_info(&self, slot: SlotRef) -> SlotStats; diff --git a/crates/console/prometeu-hal/src/syscalls.rs b/crates/console/prometeu-hal/src/syscalls.rs index 73ae59cf..618e20d4 100644 --- a/crates/console/prometeu-hal/src/syscalls.rs +++ b/crates/console/prometeu-hal/src/syscalls.rs @@ -69,6 +69,12 @@ pub enum Syscall { AssetStatus = 0x6002, AssetCommit = 0x6003, AssetCancel = 0x6004, + AssetBacklogInfo = 0x6005, + AssetBacklogPosition = 0x6006, + AssetBacklogMove = 0x6007, + AssetBacklogPromote = 0x6008, + AssetBacklogDemote = 0x6009, + AssetTargetStatus = 0x600A, BankInfo = 0x6101, } diff --git a/crates/console/prometeu-hal/src/syscalls/domains/asset.rs b/crates/console/prometeu-hal/src/syscalls/domains/asset.rs index 26c7ee30..849c7632 100644 --- a/crates/console/prometeu-hal/src/syscalls/domains/asset.rs +++ b/crates/console/prometeu-hal/src/syscalls/domains/asset.rs @@ -24,4 +24,40 @@ pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[ .caps(caps::ASSET) .non_deterministic() .cost(20), + SyscallRegistryEntry::builder(Syscall::AssetBacklogInfo, "asset", "backlog_info") + .args(0) + .rets(7) + .caps(caps::ASSET) + .non_deterministic() + .cost(5), + SyscallRegistryEntry::builder(Syscall::AssetBacklogPosition, "asset", "backlog_position") + .args(1) + .rets(4) + .caps(caps::ASSET) + .non_deterministic() + .cost(5), + SyscallRegistryEntry::builder(Syscall::AssetBacklogMove, "asset", "backlog_move") + .args(2) + .rets(1) + .caps(caps::ASSET) + .non_deterministic() + .cost(10), + SyscallRegistryEntry::builder(Syscall::AssetBacklogPromote, "asset", "backlog_promote") + .args(1) + .rets(1) + .caps(caps::ASSET) + .non_deterministic() + .cost(10), + SyscallRegistryEntry::builder(Syscall::AssetBacklogDemote, "asset", "backlog_demote") + .args(1) + .rets(1) + .caps(caps::ASSET) + .non_deterministic() + .cost(10), + SyscallRegistryEntry::builder(Syscall::AssetTargetStatus, "asset", "target_status") + .args(2) + .rets(6) + .caps(caps::ASSET) + .non_deterministic() + .cost(5), ]; diff --git a/crates/console/prometeu-hal/src/syscalls/registry.rs b/crates/console/prometeu-hal/src/syscalls/registry.rs index 65e4d3ad..3d0a08a6 100644 --- a/crates/console/prometeu-hal/src/syscalls/registry.rs +++ b/crates/console/prometeu-hal/src/syscalls/registry.rs @@ -53,6 +53,12 @@ impl Syscall { 0x6002 => Some(Self::AssetStatus), 0x6003 => Some(Self::AssetCommit), 0x6004 => Some(Self::AssetCancel), + 0x6005 => Some(Self::AssetBacklogInfo), + 0x6006 => Some(Self::AssetBacklogPosition), + 0x6007 => Some(Self::AssetBacklogMove), + 0x6008 => Some(Self::AssetBacklogPromote), + 0x6009 => Some(Self::AssetBacklogDemote), + 0x600A => Some(Self::AssetTargetStatus), 0x6101 => Some(Self::BankInfo), _ => None, } @@ -109,6 +115,12 @@ impl Syscall { Self::AssetStatus => "AssetStatus", Self::AssetCommit => "AssetCommit", Self::AssetCancel => "AssetCancel", + Self::AssetBacklogInfo => "AssetBacklogInfo", + Self::AssetBacklogPosition => "AssetBacklogPosition", + Self::AssetBacklogMove => "AssetBacklogMove", + Self::AssetBacklogPromote => "AssetBacklogPromote", + Self::AssetBacklogDemote => "AssetBacklogDemote", + Self::AssetTargetStatus => "AssetTargetStatus", Self::BankInfo => "BankInfo", } } diff --git a/crates/console/prometeu-hal/src/syscalls/tests.rs b/crates/console/prometeu-hal/src/syscalls/tests.rs index 3625b5ad..e4e22814 100644 --- a/crates/console/prometeu-hal/src/syscalls/tests.rs +++ b/crates/console/prometeu-hal/src/syscalls/tests.rs @@ -258,6 +258,30 @@ fn status_first_syscall_signatures_are_pinned() { assert_eq!(asset_cancel.arg_slots, 1); assert_eq!(asset_cancel.ret_slots, 1); + let asset_backlog_info = meta_for(Syscall::AssetBacklogInfo); + assert_eq!(asset_backlog_info.arg_slots, 0); + assert_eq!(asset_backlog_info.ret_slots, 7); + + let asset_backlog_position = meta_for(Syscall::AssetBacklogPosition); + assert_eq!(asset_backlog_position.arg_slots, 1); + assert_eq!(asset_backlog_position.ret_slots, 4); + + let asset_backlog_move = meta_for(Syscall::AssetBacklogMove); + assert_eq!(asset_backlog_move.arg_slots, 2); + assert_eq!(asset_backlog_move.ret_slots, 1); + + let asset_backlog_promote = meta_for(Syscall::AssetBacklogPromote); + assert_eq!(asset_backlog_promote.arg_slots, 1); + assert_eq!(asset_backlog_promote.ret_slots, 1); + + let asset_backlog_demote = meta_for(Syscall::AssetBacklogDemote); + assert_eq!(asset_backlog_demote.arg_slots, 1); + assert_eq!(asset_backlog_demote.ret_slots, 1); + + let asset_target_status = meta_for(Syscall::AssetTargetStatus); + assert_eq!(asset_target_status.arg_slots, 2); + assert_eq!(asset_target_status.ret_slots, 6); + let bank_info = meta_for(Syscall::BankInfo); assert_eq!(bank_info.arg_slots, 1); assert_eq!(bank_info.ret_slots, 2); diff --git a/crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs b/crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs index 932b8589..b650f02f 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs @@ -757,6 +757,64 @@ impl NativeInterface for VmRuntimeHost<'_> { ret.push_int(status as i64); Ok(()) } + Syscall::AssetBacklogInfo => { + let info = platform.assets().backlog_info(); + ret.push_int(info.status as i64); + ret.push_int(info.pending_count as i64); + ret.push_int(info.active_handle as i64); + ret.push_int(info.active_asset_id.unwrap_or(0) as i64); + ret.push_int(info.active_bank_type.map(|bank| bank as i64).unwrap_or(0)); + ret.push_int(info.active_slot.map(|slot| slot as i64).unwrap_or(0)); + ret.push_int(info.active_progress as i64); + Ok(()) + } + Syscall::AssetBacklogPosition => { + let position = platform.assets().backlog_position(expect_int(args, 0)? as u32); + ret.push_int(position.status as i64); + ret.push_int(position.state as i64); + ret.push_int(position.position as i64); + ret.push_int(position.progress as i64); + Ok(()) + } + Syscall::AssetBacklogMove => { + let new_position = expect_non_negative_usize(args, 1, "new_position")?; + let status = + platform.assets().backlog_move(expect_int(args, 0)? as u32, new_position); + ret.push_int(status as i64); + Ok(()) + } + Syscall::AssetBacklogPromote => { + let status = platform.assets().backlog_promote(expect_int(args, 0)? as u32); + ret.push_int(status as i64); + Ok(()) + } + Syscall::AssetBacklogDemote => { + let status = platform.assets().backlog_demote(expect_int(args, 0)? as u32); + ret.push_int(status as i64); + Ok(()) + } + Syscall::AssetTargetStatus => { + let bank_type = match expect_int(args, 0)? { + 0 => BankType::GLYPH, + 1 => BankType::SOUNDS, + 2 => BankType::SCENE, + other => { + return Err(VmFault::Trap( + TRAP_TYPE, + format!("Invalid asset bank type: {}", other), + )); + } + }; + let slot = expect_non_negative_usize(args, 1, "slot")?; + let target = platform.assets().target_status(bank_type, slot); + ret.push_int(target.status as i64); + ret.push_int(target.asset_id.unwrap_or(0) as i64); + ret.push_int(target.handle as i64); + ret.push_int(target.state as i64); + ret.push_int(target.position as i64); + ret.push_int(target.progress as i64); + Ok(()) + } Syscall::BankInfo => { let asset_type = match expect_int(args, 0)? as u32 { 0 => BankType::GLYPH, diff --git a/crates/console/prometeu-system/src/services/vm_runtime/tests_asset_bank.rs b/crates/console/prometeu-system/src/services/vm_runtime/tests_asset_bank.rs index 45ae5a2e..6fd57e59 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/tests_asset_bank.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/tests_asset_bank.rs @@ -180,6 +180,102 @@ fn tick_asset_status_unknown_handle_returns_status_not_crash() { assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(LoadStatus::UnknownHandle as i64)]); } +#[test] +fn tick_asset_target_status_empty_slot_returns_status_first_payload() { + let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; + let mut vm = VirtualMachine::default(); + let mut platform = TestPlatform::new(); + let signals = InputSignals::default(); + let code = assemble("PUSH_I32 0\nPUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"); + let program = serialized_single_function_module( + code, + vec![SyscallDecl { + module: "asset".into(), + name: "target_status".into(), + version: 1, + arg_slots: 2, + ret_slots: 6, + }], + ); + let cartridge = cartridge_with_program(program, caps::ASSET); + + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut platform, + ); + + assert!(report.is_none(), "target_status must not crash for empty slots"); + assert!(vm.is_halted()); + assert_eq!( + vm.operand_stack_top(6), + vec![ + Value::Int64(0), + Value::Int64(0), + Value::Int64(LoadStatus::EMPTY as i64), + Value::Int64(1), + Value::Int64(0), + Value::Int64(AssetOpStatus::Ok as i64), + ] + ); +} + +#[test] +fn tick_asset_backlog_promote_unknown_handle_returns_status_not_crash() { + let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; + let mut vm = VirtualMachine::default(); + let mut platform = TestPlatform::new(); + let signals = InputSignals::default(); + let code = assemble("PUSH_I32 999\nHOSTCALL 0\nHALT").expect("assemble"); + let program = serialized_single_function_module( + code, + vec![SyscallDecl { + module: "asset".into(), + name: "backlog_promote".into(), + version: 1, + arg_slots: 1, + ret_slots: 1, + }], + ); + let cartridge = cartridge_with_program(program, caps::ASSET); + + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut platform, + ); + + assert!(report.is_none(), "unknown backlog handle must not crash"); + assert!(vm.is_halted()); + assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AssetOpStatus::UnknownHandle as i64)]); +} + #[test] fn tick_bank_info_returns_slot_summary_not_json() { let mut runtime = VirtualMachineRuntime::new(None); diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 95b9972c..5cb9d955 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":"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-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":"done","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-0126-asset-backlog-public-api-and-status-surface.md b/discussion/workflow/plans/PLN-0126-asset-backlog-public-api-and-status-surface.md index 8ea105e7..4d508601 100644 --- a/discussion/workflow/plans/PLN-0126-asset-backlog-public-api-and-status-surface.md +++ b/discussion/workflow/plans/PLN-0126-asset-backlog-public-api-and-status-surface.md @@ -2,7 +2,7 @@ id: PLN-0126 ticket: perf-async-background-work-lanes-for-assets-and-fs title: Asset Backlog Public API and Status Surface -status: open +status: done created: 2026-06-28 completed: ref_decisions: [DEC-0034]