PR004: implement asset status-first lifecycle surface

This commit is contained in:
bQUARKz 2026-03-09 07:02:39 +00:00
parent 998252aa25
commit c8b3b6afcc
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
6 changed files with 130 additions and 26 deletions

View File

@ -2,7 +2,8 @@
use crate::memory_banks::{SoundBankPoolInstaller, TileBankPoolInstaller}; use crate::memory_banks::{SoundBankPoolInstaller, TileBankPoolInstaller};
use prometeu_hal::AssetBridge; use prometeu_hal::AssetBridge;
use prometeu_hal::asset::{ use prometeu_hal::asset::{
AssetEntry, BankStats, BankType, HandleId, LoadStatus, PreloadEntry, SlotRef, SlotStats, AssetEntry, AssetLoadError, AssetOpStatus, BankStats, BankType, HandleId, LoadStatus,
PreloadEntry, SlotRef, SlotStats,
}; };
use prometeu_hal::color::Color; use prometeu_hal::color::Color;
use prometeu_hal::sample::Sample; use prometeu_hal::sample::Sample;
@ -139,16 +140,16 @@ impl AssetBridge for AssetManager {
) { ) {
self.initialize_for_cartridge(assets, preload, assets_data) self.initialize_for_cartridge(assets, preload, assets_data)
} }
fn load(&self, asset_name: &str, slot: SlotRef) -> Result<HandleId, String> { fn load(&self, asset_name: &str, slot: SlotRef) -> Result<HandleId, AssetLoadError> {
self.load(asset_name, slot) self.load(asset_name, slot)
} }
fn status(&self, handle: HandleId) -> LoadStatus { fn status(&self, handle: HandleId) -> LoadStatus {
self.status(handle) self.status(handle)
} }
fn commit(&self, handle: HandleId) { fn commit(&self, handle: HandleId) -> AssetOpStatus {
self.commit(handle) self.commit(handle)
} }
fn cancel(&self, handle: HandleId) { fn cancel(&self, handle: HandleId) -> AssetOpStatus {
self.cancel(handle) self.cancel(handle)
} }
fn apply_commits(&self) { fn apply_commits(&self) {
@ -293,19 +294,22 @@ impl AssetManager {
} }
} }
pub fn load(&self, asset_name: &str, slot: SlotRef) -> Result<HandleId, String> { pub fn load(&self, asset_name: &str, slot: SlotRef) -> Result<HandleId, AssetLoadError> {
if slot.index >= 16 {
return Err(AssetLoadError::SlotIndexInvalid);
}
let entry = { let entry = {
let assets = self.assets.read().unwrap(); let assets = self.assets.read().unwrap();
let name_to_id = self.name_to_id.read().unwrap(); let name_to_id = self.name_to_id.read().unwrap();
let id = name_to_id let id = name_to_id
.get(asset_name) .get(asset_name)
.ok_or_else(|| format!("Asset not found: {}", asset_name))?; .ok_or(AssetLoadError::AssetNotFound)?;
assets.get(id).ok_or_else(|| format!("Asset ID {} not found in table", id))?.clone() assets.get(id).ok_or(AssetLoadError::BackendError)?.clone()
}; };
let asset_id = entry.asset_id; let asset_id = entry.asset_id;
if slot.asset_type != entry.bank_type { if slot.asset_type != entry.bank_type {
return Err("INCOMPATIBLE_SLOT_KIND".to_string()); return Err(AssetLoadError::SlotKindMismatch);
} }
let mut next_id = self.next_handle_id.lock().unwrap(); let mut next_id = self.next_handle_id.lock().unwrap();
@ -533,21 +537,38 @@ impl AssetManager {
} }
pub fn status(&self, handle: HandleId) -> LoadStatus { pub fn status(&self, handle: HandleId) -> LoadStatus {
self.handles.read().unwrap().get(&handle).map(|h| h.status).unwrap_or(LoadStatus::ERROR) self.handles
.read()
.unwrap()
.get(&handle)
.map(|h| h.status)
.unwrap_or(LoadStatus::UnknownHandle)
} }
pub fn commit(&self, handle: HandleId) { pub fn commit(&self, handle: HandleId) -> AssetOpStatus {
let mut handles_map = self.handles.write().unwrap(); let mut handles_map = self.handles.write().unwrap();
if let Some(h) = handles_map.get_mut(&handle) { let Some(h) = handles_map.get_mut(&handle) else {
if h.status == LoadStatus::READY { return AssetOpStatus::UnknownHandle;
self.pending_commits.lock().unwrap().push(handle); };
} if h.status == LoadStatus::READY {
self.pending_commits.lock().unwrap().push(handle);
AssetOpStatus::Ok
} else {
AssetOpStatus::InvalidState
} }
} }
pub fn cancel(&self, handle: HandleId) { pub fn cancel(&self, handle: HandleId) -> AssetOpStatus {
let mut final_status = AssetOpStatus::UnknownHandle;
let mut handles_map = self.handles.write().unwrap(); let mut handles_map = self.handles.write().unwrap();
if let Some(h) = handles_map.get_mut(&handle) { 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 { match h.status {
LoadStatus::PENDING | LoadStatus::LOADING | LoadStatus::READY => { LoadStatus::PENDING | LoadStatus::LOADING | LoadStatus::READY => {
h.status = LoadStatus::CANCELED; h.status = LoadStatus::CANCELED;
@ -557,6 +578,7 @@ impl AssetManager {
} }
self.gfx_policy.take_staging(handle); self.gfx_policy.take_staging(handle);
self.sound_policy.take_staging(handle); self.sound_policy.take_staging(handle);
final_status
} }
pub fn apply_commits(&self) { pub fn apply_commits(&self) {

View File

@ -37,6 +37,24 @@ pub enum LoadStatus {
COMMITTED, COMMITTED,
CANCELED, CANCELED,
ERROR, ERROR,
UnknownHandle,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum AssetLoadError {
AssetNotFound = 1,
SlotKindMismatch = 2,
SlotIndexInvalid = 3,
BackendError = 4,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum AssetOpStatus {
Ok = 0,
UnknownHandle = 1,
InvalidState = 2,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]

View File

@ -1,5 +1,6 @@
use crate::asset::{ use crate::asset::{
AssetEntry, BankStats, BankType, HandleId, LoadStatus, PreloadEntry, SlotRef, SlotStats, AssetEntry, AssetLoadError, AssetOpStatus, BankStats, BankType, HandleId, LoadStatus,
PreloadEntry, SlotRef, SlotStats,
}; };
pub trait AssetBridge { pub trait AssetBridge {
@ -9,10 +10,10 @@ pub trait AssetBridge {
preload: Vec<PreloadEntry>, preload: Vec<PreloadEntry>,
assets_data: Vec<u8>, assets_data: Vec<u8>,
); );
fn load(&self, asset_name: &str, slot: SlotRef) -> Result<HandleId, String>; fn load(&self, asset_name: &str, slot: SlotRef) -> Result<HandleId, AssetLoadError>;
fn status(&self, handle: HandleId) -> LoadStatus; fn status(&self, handle: HandleId) -> LoadStatus;
fn commit(&self, handle: HandleId); fn commit(&self, handle: HandleId) -> AssetOpStatus;
fn cancel(&self, handle: HandleId); fn cancel(&self, handle: HandleId) -> AssetOpStatus;
fn apply_commits(&self); fn apply_commits(&self);
fn bank_info(&self, kind: BankType) -> BankStats; fn bank_info(&self, kind: BankType) -> BankStats;
fn slot_info(&self, slot: SlotRef) -> SlotStats; fn slot_info(&self, slot: SlotRef) -> SlotStats;

View File

@ -8,7 +8,7 @@ pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
"load", "load",
1, 1,
3, 3,
1, 2,
caps::ASSET, caps::ASSET,
Determinism::NonDeterministic, Determinism::NonDeterministic,
false, false,
@ -32,7 +32,7 @@ pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
"commit", "commit",
1, 1,
1, 1,
0, 1,
caps::ASSET, caps::ASSET,
Determinism::NonDeterministic, Determinism::NonDeterministic,
false, false,
@ -44,7 +44,7 @@ pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
"cancel", "cancel",
1, 1,
1, 1,
0, 1,
caps::ASSET, caps::ASSET,
Determinism::NonDeterministic, Determinism::NonDeterministic,
false, false,

View File

@ -1,6 +1,6 @@
use super::*; use super::*;
use prometeu_bytecode::{TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_TYPE, Value}; use prometeu_bytecode::{TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_TYPE, Value};
use prometeu_hal::asset::{BankType, LoadStatus, SlotRef}; use prometeu_hal::asset::{AssetLoadError, AssetOpStatus, BankType, LoadStatus, SlotRef};
use prometeu_hal::color::Color; use prometeu_hal::color::Color;
use prometeu_hal::log::{LogLevel, LogSource}; use prometeu_hal::log::{LogLevel, LogSource};
use prometeu_hal::sprite::Sprite; use prometeu_hal::sprite::Sprite;
@ -373,10 +373,21 @@ impl NativeInterface for VirtualMachineRuntime {
match hw.assets().load(&asset_id, slot) { match hw.assets().load(&asset_id, slot) {
Ok(handle) => { Ok(handle) => {
ret.push_int(AssetOpStatus::Ok as i64);
ret.push_int(handle as i64); ret.push_int(handle as i64);
Ok(()) Ok(())
} }
Err(e) => Err(VmFault::Panic(e)), Err(status) => {
let status_val = match status {
AssetLoadError::AssetNotFound => 3,
AssetLoadError::SlotKindMismatch => 4,
AssetLoadError::SlotIndexInvalid => 5,
AssetLoadError::BackendError => 6,
};
ret.push_int(status_val);
ret.push_int(0);
Ok(())
}
} }
} }
Syscall::AssetStatus => { Syscall::AssetStatus => {
@ -387,16 +398,19 @@ impl NativeInterface for VirtualMachineRuntime {
LoadStatus::COMMITTED => 3, LoadStatus::COMMITTED => 3,
LoadStatus::CANCELED => 4, LoadStatus::CANCELED => 4,
LoadStatus::ERROR => 5, LoadStatus::ERROR => 5,
LoadStatus::UnknownHandle => 6,
}; };
ret.push_int(status_val); ret.push_int(status_val);
Ok(()) Ok(())
} }
Syscall::AssetCommit => { Syscall::AssetCommit => {
hw.assets().commit(expect_int(args, 0)? as u32); let status = hw.assets().commit(expect_int(args, 0)? as u32);
ret.push_int(status as i64);
Ok(()) Ok(())
} }
Syscall::AssetCancel => { Syscall::AssetCancel => {
hw.assets().cancel(expect_int(args, 0)? as u32); let status = hw.assets().cancel(expect_int(args, 0)? as u32);
ret.push_int(status as i64);
Ok(()) Ok(())
} }
Syscall::BankInfo => { Syscall::BankInfo => {

View File

@ -126,3 +126,52 @@ These preload entries are consumed during cartridge initialization so the asset
- [`13-cartridge.md`](13-cartridge.md) defines cartridge fields that carry `asset_table`, `preload`, and `assets.pa`. - [`13-cartridge.md`](13-cartridge.md) defines cartridge fields that carry `asset_table`, `preload`, and `assets.pa`.
- [`16-host-abi-and-syscalls.md`](16-host-abi-and-syscalls.md) defines the syscall boundary used to manipulate assets. - [`16-host-abi-and-syscalls.md`](16-host-abi-and-syscalls.md) defines the syscall boundary used to manipulate assets.
- [`03-memory-stack-heap-and-allocation.md`](03-memory-stack-heap-and-allocation.md) defines the distinction between VM heap memory and host-owned memory. - [`03-memory-stack-heap-and-allocation.md`](03-memory-stack-heap-and-allocation.md) defines the distinction between VM heap memory and host-owned memory.
## 11 Syscall Surface and Status Policy
`asset` follows status-first policy.
Fault boundary:
- `Trap`: structural ABI misuse (type/arity/capability/shape mismatch);
- `status`: operational failure;
- `Panic`: internal invariant break only.
### 11.1 MVP syscall shape
- `asset.load(name, kind, slot) -> (status:int, handle:int)`
- `asset.status(handle) -> status:int`
- `asset.commit(handle) -> status:int`
- `asset.cancel(handle) -> status:int`
Rules:
- `handle` is valid only when `load` status is `OK`;
- failed `load` returns `handle = 0`;
- `commit` and `cancel` must not be silent no-op for unknown/invalid handle state.
### 11.2 Minimum status tables
`asset.load` request statuses:
- `0` = `OK`
- `3` = `ASSET_NOT_FOUND`
- `4` = `SLOT_KIND_MISMATCH`
- `5` = `SLOT_INDEX_INVALID`
- `6` = `BACKEND_ERROR`
`asset.status` lifecycle statuses:
- `0` = `PENDING`
- `1` = `LOADING`
- `2` = `READY`
- `3` = `COMMITTED`
- `4` = `CANCELED`
- `5` = `ERROR`
- `6` = `UNKNOWN_HANDLE`
`asset.commit` and `asset.cancel` operation statuses:
- `0` = `OK`
- `1` = `UNKNOWN_HANDLE`
- `2` = `INVALID_STATE`