implements PLN-0126 asset backlog public api

This commit is contained in:
bQUARKz 2026-06-28 14:08:30 +01:00
parent 30c24a4ea7
commit 961557583b
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
11 changed files with 604 additions and 13 deletions

View File

@ -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<AssetLoadJob>,
active: Option<AssetLoadJob>,
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<AssetLoadJob> {
self.state.lock().unwrap().active.clone()
}
fn pending_position(&self, handle_id: HandleId) -> Option<usize> {
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<AssetLoadJob> {
self.queue.active_job()
}
fn pending_position(&self, handle_id: HandleId) -> Option<usize> {
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<SlotRef> {
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<AssetId> {
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<HandleId, AssetLoadError> {
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<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 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<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 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());

View File

@ -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<AssetId>,
pub active_bank_type: Option<BankType>,
pub active_slot: Option<usize>,
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<AssetId>,
pub handle: HandleId,
pub state: LoadStatus,
pub position: u32,
pub progress: u8,
}
#[derive(Debug, Clone, Serialize, Deserialize)]

View File

@ -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<BankTelemetry>;
fn slot_info(&self, slot: SlotRef) -> SlotStats;

View File

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

View File

@ -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),
];

View File

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

View File

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

View File

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

View File

@ -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<u32, String> = 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<u32, String> = 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);

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

View File

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