asset manager load by asset id and slot index
This commit is contained in:
parent
e6978397ab
commit
201226b892
@ -110,7 +110,6 @@ impl<T> BankPolicy<T> {
|
||||
|
||||
pub struct AssetManager {
|
||||
assets: Arc<RwLock<HashMap<AssetId, AssetEntry>>>,
|
||||
name_to_id: Arc<RwLock<HashMap<String, AssetId>>>,
|
||||
handles: Arc<RwLock<HashMap<HandleId, LoadHandleInfo>>>,
|
||||
next_handle_id: Mutex<HandleId>,
|
||||
assets_data: Arc<RwLock<AssetsPayloadSource>>,
|
||||
@ -153,8 +152,8 @@ impl AssetBridge for AssetManager {
|
||||
) {
|
||||
self.initialize_for_cartridge(assets, preload, assets_data)
|
||||
}
|
||||
fn load(&self, asset_name: &str, slot: SlotRef) -> Result<HandleId, AssetLoadError> {
|
||||
self.load(asset_name, slot)
|
||||
fn load(&self, asset_id: AssetId, slot_index: usize) -> Result<HandleId, AssetLoadError> {
|
||||
self.load(asset_id, slot_index)
|
||||
}
|
||||
fn status(&self, handle: HandleId) -> LoadStatus {
|
||||
self.status(handle)
|
||||
@ -268,15 +267,12 @@ impl AssetManager {
|
||||
sound_installer: Arc<dyn SoundBankPoolInstaller>,
|
||||
) -> Self {
|
||||
let mut asset_map = HashMap::new();
|
||||
let mut name_to_id = HashMap::new();
|
||||
for entry in assets {
|
||||
name_to_id.insert(entry.asset_name.clone(), entry.asset_id);
|
||||
asset_map.insert(entry.asset_id, entry);
|
||||
}
|
||||
|
||||
Self {
|
||||
assets: Arc::new(RwLock::new(asset_map)),
|
||||
name_to_id: Arc::new(RwLock::new(name_to_id)),
|
||||
gfx_installer,
|
||||
sound_installer,
|
||||
gfx_slots: Arc::new(RwLock::new(std::array::from_fn(|_| None))),
|
||||
@ -299,11 +295,8 @@ impl AssetManager {
|
||||
self.shutdown();
|
||||
{
|
||||
let mut asset_map = self.assets.write().unwrap();
|
||||
let mut name_to_id = self.name_to_id.write().unwrap();
|
||||
asset_map.clear();
|
||||
name_to_id.clear();
|
||||
for entry in assets.iter() {
|
||||
name_to_id.insert(entry.asset_name.clone(), entry.asset_id);
|
||||
asset_map.insert(entry.asset_id, entry.clone());
|
||||
}
|
||||
}
|
||||
@ -381,21 +374,18 @@ impl AssetManager {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load(&self, asset_name: &str, slot: SlotRef) -> Result<HandleId, AssetLoadError> {
|
||||
if slot.index >= 16 {
|
||||
pub fn load(&self, asset_id: AssetId, slot_index: usize) -> Result<HandleId, AssetLoadError> {
|
||||
if slot_index >= 16 {
|
||||
return Err(AssetLoadError::SlotIndexInvalid);
|
||||
}
|
||||
let entry = {
|
||||
let assets = self.assets.read().unwrap();
|
||||
let name_to_id = self.name_to_id.read().unwrap();
|
||||
let id = name_to_id.get(asset_name).ok_or(AssetLoadError::AssetNotFound)?;
|
||||
assets.get(id).ok_or(AssetLoadError::BackendError)?.clone()
|
||||
assets.get(&asset_id).ok_or(AssetLoadError::AssetNotFound)?.clone()
|
||||
};
|
||||
let slot = match entry.bank_type {
|
||||
BankType::TILES => SlotRef::gfx(slot_index),
|
||||
BankType::SOUNDS => SlotRef::audio(slot_index),
|
||||
};
|
||||
let asset_id = entry.asset_id;
|
||||
|
||||
if slot.asset_type != entry.bank_type {
|
||||
return Err(AssetLoadError::SlotKindMismatch);
|
||||
}
|
||||
|
||||
let mut next_id = self.next_handle_id.lock().unwrap();
|
||||
let handle_id = *next_id;
|
||||
@ -1021,9 +1011,7 @@ mod tests {
|
||||
gfx_installer,
|
||||
sound_installer,
|
||||
);
|
||||
let slot = SlotRef::gfx(0);
|
||||
|
||||
let handle = am.load("test_tiles", slot).expect("Should start loading");
|
||||
let handle = am.load(0, 0).expect("Should start loading");
|
||||
|
||||
let mut status = am.status(handle);
|
||||
let start = Instant::now();
|
||||
@ -1062,13 +1050,13 @@ mod tests {
|
||||
sound_installer,
|
||||
);
|
||||
|
||||
let handle1 = am.load("test_tiles", SlotRef::gfx(0)).unwrap();
|
||||
let handle1 = am.load(0, 0).unwrap();
|
||||
let start = Instant::now();
|
||||
while am.status(handle1) != LoadStatus::READY && start.elapsed().as_secs() < 5 {
|
||||
thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
|
||||
let handle2 = am.load("test_tiles", SlotRef::gfx(1)).unwrap();
|
||||
let handle2 = am.load(0, 1).unwrap();
|
||||
assert_eq!(am.status(handle2), LoadStatus::READY);
|
||||
|
||||
let staging = am.gfx_policy.staging.read().unwrap();
|
||||
@ -1105,9 +1093,7 @@ mod tests {
|
||||
gfx_installer,
|
||||
sound_installer,
|
||||
);
|
||||
let slot = SlotRef::audio(0);
|
||||
|
||||
let handle = am.load("test_sound", slot).expect("Should start loading");
|
||||
let handle = am.load(1, 0).expect("Should start loading");
|
||||
|
||||
let start = Instant::now();
|
||||
while am.status(handle) != LoadStatus::READY && start.elapsed().as_secs() < 5 {
|
||||
@ -1171,7 +1157,7 @@ mod tests {
|
||||
let am =
|
||||
AssetManager::new(vec![], AssetsPayloadSource::empty(), gfx_installer, sound_installer);
|
||||
|
||||
let result = am.load("missing", SlotRef::gfx(0));
|
||||
let result = am.load(999, 0);
|
||||
|
||||
assert_eq!(result, Err(AssetLoadError::AssetNotFound));
|
||||
}
|
||||
@ -1189,29 +1175,11 @@ mod tests {
|
||||
sound_installer,
|
||||
);
|
||||
|
||||
let result = am.load("test_tiles", SlotRef::gfx(16));
|
||||
let result = am.load(0, 16);
|
||||
|
||||
assert_eq!(result, Err(AssetLoadError::SlotIndexInvalid));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_returns_slot_kind_mismatch() {
|
||||
let banks = Arc::new(MemoryBanks::new());
|
||||
let gfx_installer = Arc::clone(&banks) as Arc<dyn TileBankPoolInstaller>;
|
||||
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
|
||||
let data = test_tile_asset_data();
|
||||
let am = AssetManager::new(
|
||||
vec![test_tile_asset_entry("test_tiles", 16, 16)],
|
||||
AssetsPayloadSource::from_bytes(data),
|
||||
gfx_installer,
|
||||
sound_installer,
|
||||
);
|
||||
|
||||
let result = am.load("test_tiles", SlotRef::audio(0));
|
||||
|
||||
assert_eq!(result, Err(AssetLoadError::SlotKindMismatch));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_status_returns_unknown_handle() {
|
||||
let banks = Arc::new(MemoryBanks::new());
|
||||
@ -1239,7 +1207,7 @@ mod tests {
|
||||
assert_eq!(am.commit(999), AssetOpStatus::UnknownHandle);
|
||||
assert_eq!(am.cancel(999), AssetOpStatus::UnknownHandle);
|
||||
|
||||
let handle = am.load("test_tiles", SlotRef::gfx(0)).expect("load must allocate handle");
|
||||
let handle = am.load(0, 0).expect("load must allocate handle");
|
||||
let start = Instant::now();
|
||||
while am.status(handle) != LoadStatus::READY && start.elapsed().as_secs() < 5 {
|
||||
thread::sleep(std::time::Duration::from_millis(10));
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
use crate::asset::{
|
||||
AssetEntry, AssetLoadError, AssetOpStatus, BankStats, BankType, HandleId, LoadStatus,
|
||||
AssetEntry, AssetId, AssetLoadError, AssetOpStatus, BankStats, BankType, HandleId, LoadStatus,
|
||||
PreloadEntry, SlotRef, SlotStats,
|
||||
};
|
||||
use crate::cartridge::AssetsPayloadSource;
|
||||
@ -11,7 +11,7 @@ pub trait AssetBridge {
|
||||
preload: Vec<PreloadEntry>,
|
||||
assets_data: AssetsPayloadSource,
|
||||
);
|
||||
fn load(&self, asset_name: &str, slot: SlotRef) -> Result<HandleId, AssetLoadError>;
|
||||
fn load(&self, asset_id: AssetId, slot_index: usize) -> Result<HandleId, AssetLoadError>;
|
||||
fn status(&self, handle: HandleId) -> LoadStatus;
|
||||
fn commit(&self, handle: HandleId) -> AssetOpStatus;
|
||||
fn cancel(&self, handle: HandleId) -> AssetOpStatus;
|
||||
|
||||
@ -7,7 +7,7 @@ pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
|
||||
"asset",
|
||||
"load",
|
||||
1,
|
||||
3,
|
||||
2,
|
||||
2,
|
||||
caps::ASSET,
|
||||
Determinism::NonDeterministic,
|
||||
|
||||
@ -215,7 +215,7 @@ fn status_first_syscall_signatures_are_pinned() {
|
||||
assert_eq!(audio_play.ret_slots, 1);
|
||||
|
||||
let asset_load = meta_for(Syscall::AssetLoad);
|
||||
assert_eq!(asset_load.arg_slots, 3);
|
||||
assert_eq!(asset_load.arg_slots, 2);
|
||||
assert_eq!(asset_load.ret_slots, 2);
|
||||
|
||||
let asset_commit = meta_for(Syscall::AssetCommit);
|
||||
@ -256,7 +256,7 @@ fn declared_resolver_rejects_legacy_status_first_signatures() {
|
||||
name: "load".into(),
|
||||
version: 1,
|
||||
arg_slots: 3,
|
||||
ret_slots: 1,
|
||||
ret_slots: 2,
|
||||
},
|
||||
prometeu_bytecode::SyscallDecl {
|
||||
module: "asset".into(),
|
||||
@ -292,8 +292,10 @@ fn declared_resolver_rejects_legacy_status_first_signatures() {
|
||||
assert_eq!(version, decl.version);
|
||||
assert_eq!(declared_arg_slots, decl.arg_slots);
|
||||
assert_eq!(declared_ret_slots, decl.ret_slots);
|
||||
assert_eq!(expected_arg_slots, declared_arg_slots);
|
||||
assert_ne!(expected_ret_slots, declared_ret_slots);
|
||||
assert!(
|
||||
expected_arg_slots != declared_arg_slots
|
||||
|| expected_ret_slots != declared_ret_slots
|
||||
);
|
||||
}
|
||||
other => panic!("expected AbiMismatch, got {:?}", other),
|
||||
}
|
||||
@ -321,7 +323,7 @@ fn declared_resolver_accepts_mixed_status_first_surface_as_a_single_module() {
|
||||
module: "asset".into(),
|
||||
name: "load".into(),
|
||||
version: 1,
|
||||
arg_slots: 3,
|
||||
arg_slots: 2,
|
||||
ret_slots: 2,
|
||||
},
|
||||
prometeu_bytecode::SyscallDecl {
|
||||
@ -333,8 +335,9 @@ fn declared_resolver_accepts_mixed_status_first_surface_as_a_single_module() {
|
||||
},
|
||||
];
|
||||
|
||||
let resolved = resolve_declared_program_syscalls(&declared, caps::GFX | caps::AUDIO | caps::ASSET)
|
||||
.expect("mixed status-first surface must resolve together");
|
||||
let resolved =
|
||||
resolve_declared_program_syscalls(&declared, caps::GFX | caps::AUDIO | caps::ASSET)
|
||||
.expect("mixed status-first surface must resolve together");
|
||||
|
||||
assert_eq!(resolved.len(), declared.len());
|
||||
assert_eq!(resolved[0].meta.ret_slots, 1);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use super::*;
|
||||
use prometeu_bytecode::{TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_TYPE, Value};
|
||||
use crate::services::memcard::{MemcardSlotState, MemcardStatus};
|
||||
use prometeu_hal::asset::{AssetOpStatus, BankType, SlotRef};
|
||||
use prometeu_bytecode::{TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_TYPE, Value};
|
||||
use prometeu_hal::asset::{AssetId, AssetOpStatus, BankType, SlotRef};
|
||||
use prometeu_hal::cartridge::AppMode;
|
||||
use prometeu_hal::color::Color;
|
||||
use prometeu_hal::log::{LogLevel, LogSource};
|
||||
@ -446,15 +446,13 @@ impl NativeInterface for VirtualMachineRuntime {
|
||||
Ok(())
|
||||
}
|
||||
Syscall::AssetLoad => {
|
||||
let asset_id = expect_string(args, 0, "asset_id")?;
|
||||
let asset_type = match expect_int(args, 1)? as u32 {
|
||||
0 => BankType::TILES,
|
||||
1 => BankType::SOUNDS,
|
||||
_ => return Err(VmFault::Trap(TRAP_TYPE, "Invalid asset type".to_string())),
|
||||
};
|
||||
let slot = SlotRef { asset_type, index: expect_int(args, 2)? as usize };
|
||||
let raw_asset_id = expect_int(args, 0)?;
|
||||
let asset_id = AssetId::try_from(raw_asset_id).map_err(|_| {
|
||||
VmFault::Trap(TRAP_TYPE, format!("asset_id out of i32 range: {}", raw_asset_id))
|
||||
})?;
|
||||
let slot_index = expect_int(args, 1)? as usize;
|
||||
|
||||
match hw.assets().load(&asset_id, slot) {
|
||||
match hw.assets().load(asset_id, slot_index) {
|
||||
Ok(handle) => {
|
||||
ret.push_int(AssetOpStatus::Ok as i64);
|
||||
ret.push_int(handle as i64);
|
||||
@ -566,10 +564,12 @@ fn hex_decode(s: &str) -> Result<Vec<u8>, VmFault> {
|
||||
let mut out = Vec::with_capacity(bytes.len() / 2);
|
||||
let mut i = 0usize;
|
||||
while i < bytes.len() {
|
||||
let hi = nibble(bytes[i])
|
||||
.ok_or_else(|| VmFault::Trap(TRAP_TYPE, "payload_hex contains invalid hex".to_string()))?;
|
||||
let lo = nibble(bytes[i + 1])
|
||||
.ok_or_else(|| VmFault::Trap(TRAP_TYPE, "payload_hex contains invalid hex".to_string()))?;
|
||||
let hi = nibble(bytes[i]).ok_or_else(|| {
|
||||
VmFault::Trap(TRAP_TYPE, "payload_hex contains invalid hex".to_string())
|
||||
})?;
|
||||
let lo = nibble(bytes[i + 1]).ok_or_else(|| {
|
||||
VmFault::Trap(TRAP_TYPE, "payload_hex contains invalid hex".to_string())
|
||||
})?;
|
||||
out.push((hi << 4) | lo);
|
||||
i += 2;
|
||||
}
|
||||
|
||||
@ -605,16 +605,14 @@ fn tick_asset_load_missing_asset_returns_status_and_zero_handle() {
|
||||
let mut vm = VirtualMachine::default();
|
||||
let mut hardware = Hardware::new();
|
||||
let signals = InputSignals::default();
|
||||
let code =
|
||||
assemble("PUSH_CONST 0\nPUSH_I32 0\nPUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble");
|
||||
let program = serialized_single_function_module_with_consts(
|
||||
let code = assemble("PUSH_I32 999\nPUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble");
|
||||
let program = serialized_single_function_module(
|
||||
code,
|
||||
vec![ConstantPoolEntry::String("missing_asset".into())],
|
||||
vec![SyscallDecl {
|
||||
module: "asset".into(),
|
||||
name: "load".into(),
|
||||
version: 1,
|
||||
arg_slots: 3,
|
||||
arg_slots: 2,
|
||||
ret_slots: 2,
|
||||
}],
|
||||
);
|
||||
@ -642,16 +640,14 @@ fn tick_asset_load_invalid_slot_returns_status_and_zero_handle() {
|
||||
vec![],
|
||||
AssetsPayloadSource::from_bytes(asset_data),
|
||||
);
|
||||
let code =
|
||||
assemble("PUSH_CONST 0\nPUSH_I32 0\nPUSH_I32 16\nHOSTCALL 0\nHALT").expect("assemble");
|
||||
let program = serialized_single_function_module_with_consts(
|
||||
let code = assemble("PUSH_I32 7\nPUSH_I32 16\nHOSTCALL 0\nHALT").expect("assemble");
|
||||
let program = serialized_single_function_module(
|
||||
code,
|
||||
vec![ConstantPoolEntry::String("tile_asset".into())],
|
||||
vec![SyscallDecl {
|
||||
module: "asset".into(),
|
||||
name: "load".into(),
|
||||
version: 1,
|
||||
arg_slots: 3,
|
||||
arg_slots: 2,
|
||||
ret_slots: 2,
|
||||
}],
|
||||
);
|
||||
@ -667,43 +663,6 @@ fn tick_asset_load_invalid_slot_returns_status_and_zero_handle() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tick_asset_load_kind_mismatch_returns_status_and_zero_handle() {
|
||||
let mut runtime = VirtualMachineRuntime::new(None);
|
||||
let mut vm = VirtualMachine::default();
|
||||
let mut hardware = Hardware::new();
|
||||
let signals = InputSignals::default();
|
||||
let asset_data = test_tile_asset_data();
|
||||
hardware.assets.initialize_for_cartridge(
|
||||
vec![test_tile_asset_entry("tile_asset", asset_data.len())],
|
||||
vec![],
|
||||
AssetsPayloadSource::from_bytes(asset_data),
|
||||
);
|
||||
let code =
|
||||
assemble("PUSH_CONST 0\nPUSH_I32 1\nPUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble");
|
||||
let program = serialized_single_function_module_with_consts(
|
||||
code,
|
||||
vec![ConstantPoolEntry::String("tile_asset".into())],
|
||||
vec![SyscallDecl {
|
||||
module: "asset".into(),
|
||||
name: "load".into(),
|
||||
version: 1,
|
||||
arg_slots: 3,
|
||||
ret_slots: 2,
|
||||
}],
|
||||
);
|
||||
let cartridge = cartridge_with_program(program, caps::ASSET);
|
||||
|
||||
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
||||
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
||||
assert!(report.is_none(), "slot kind mismatch must not crash");
|
||||
assert!(vm.is_halted());
|
||||
assert_eq!(
|
||||
vm.operand_stack_top(2),
|
||||
vec![Value::Int64(0), Value::Int64(AssetLoadError::SlotKindMismatch as i64)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tick_asset_status_unknown_handle_returns_status_not_crash() {
|
||||
let mut runtime = VirtualMachineRuntime::new(None);
|
||||
@ -765,10 +724,7 @@ fn tick_asset_commit_invalid_transition_returns_status_not_crash() {
|
||||
vec![],
|
||||
AssetsPayloadSource::from_bytes(asset_data),
|
||||
);
|
||||
let handle = hardware
|
||||
.assets
|
||||
.load("tile_asset", prometeu_hal::asset::SlotRef::gfx(0))
|
||||
.expect("asset handle must be allocated");
|
||||
let handle = hardware.assets.load(7, 0).expect("asset handle must be allocated");
|
||||
|
||||
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
||||
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
||||
@ -829,10 +785,7 @@ fn tick_asset_cancel_invalid_transition_returns_status_not_crash() {
|
||||
vec![],
|
||||
AssetsPayloadSource::from_bytes(asset_data),
|
||||
);
|
||||
let handle = hardware
|
||||
.assets
|
||||
.load("tile_asset", prometeu_hal::asset::SlotRef::gfx(0))
|
||||
.expect("asset handle must be allocated");
|
||||
let handle = hardware.assets.load(7, 0).expect("asset handle must be allocated");
|
||||
|
||||
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
||||
|
||||
@ -856,41 +809,6 @@ fn tick_asset_cancel_invalid_transition_returns_status_not_crash() {
|
||||
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AssetOpStatus::InvalidState as i64)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tick_asset_load_invalid_kind_surfaces_trap_not_panic() {
|
||||
let mut runtime = VirtualMachineRuntime::new(None);
|
||||
let mut vm = VirtualMachine::default();
|
||||
let mut hardware = Hardware::new();
|
||||
let signals = InputSignals::default();
|
||||
let code =
|
||||
assemble("PUSH_CONST 0\nPUSH_I32 2\nPUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble");
|
||||
let program = serialized_single_function_module_with_consts(
|
||||
code,
|
||||
vec![ConstantPoolEntry::String("tile_asset".into())],
|
||||
vec![SyscallDecl {
|
||||
module: "asset".into(),
|
||||
name: "load".into(),
|
||||
version: 1,
|
||||
arg_slots: 3,
|
||||
ret_slots: 2,
|
||||
}],
|
||||
);
|
||||
let cartridge = cartridge_with_program(program, caps::ASSET);
|
||||
|
||||
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
||||
let report = runtime
|
||||
.tick(&mut vm, &signals, &mut hardware)
|
||||
.expect("invalid asset kind must surface as trap");
|
||||
match report {
|
||||
CrashReport::VmTrap { trap } => {
|
||||
assert_eq!(trap.code, TRAP_TYPE);
|
||||
assert!(trap.message.contains("Invalid asset type"));
|
||||
}
|
||||
other => panic!("expected VmTrap crash report, got {:?}", other),
|
||||
}
|
||||
assert!(matches!(runtime.last_crash_report, Some(CrashReport::VmTrap { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tick_status_first_surface_smoke_across_gfx_audio_and_asset() {
|
||||
let mut runtime = VirtualMachineRuntime::new(None);
|
||||
@ -900,13 +818,12 @@ fn tick_status_first_surface_smoke_across_gfx_audio_and_asset() {
|
||||
let code = assemble(
|
||||
"PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 1\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\n\
|
||||
PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 255\nPUSH_I32 128\nPUSH_I32 1\nPUSH_I32 0\nHOSTCALL 1\n\
|
||||
PUSH_CONST 0\nPUSH_I32 0\nPUSH_I32 0\nHOSTCALL 2\n\
|
||||
PUSH_I32 999\nPUSH_I32 0\nHOSTCALL 2\n\
|
||||
HALT"
|
||||
)
|
||||
.expect("assemble");
|
||||
let program = serialized_single_function_module_with_consts(
|
||||
let program = serialized_single_function_module(
|
||||
code,
|
||||
vec![ConstantPoolEntry::String("missing_asset".into())],
|
||||
vec![
|
||||
SyscallDecl {
|
||||
module: "gfx".into(),
|
||||
@ -926,7 +843,7 @@ fn tick_status_first_surface_smoke_across_gfx_audio_and_asset() {
|
||||
module: "asset".into(),
|
||||
name: "load".into(),
|
||||
version: 1,
|
||||
arg_slots: 3,
|
||||
arg_slots: 2,
|
||||
ret_slots: 2,
|
||||
},
|
||||
],
|
||||
|
||||
@ -2940,7 +2940,7 @@ mod tests {
|
||||
module: "asset".into(),
|
||||
name: "load".into(),
|
||||
version: 1,
|
||||
arg_slots: 3,
|
||||
arg_slots: 2,
|
||||
ret_slots: 2,
|
||||
},
|
||||
SyscallDecl {
|
||||
@ -2996,7 +2996,7 @@ mod tests {
|
||||
module: "asset".into(),
|
||||
name: "load".into(),
|
||||
version: 1,
|
||||
arg_slots: 3,
|
||||
arg_slots: 2,
|
||||
ret_slots: 1,
|
||||
},
|
||||
SyscallDecl {
|
||||
@ -3021,17 +3021,19 @@ mod tests {
|
||||
let bytes = serialized_single_hostcall_module(syscall.clone());
|
||||
let err = vm.initialize(bytes).expect_err("legacy ABI must be rejected");
|
||||
match err {
|
||||
VmInitError::LoaderPatchFailed(crate::vm_init_error::LoaderPatchError::ResolveFailed(
|
||||
prometeu_hal::syscalls::DeclaredLoadError::AbiMismatch {
|
||||
module,
|
||||
name,
|
||||
version,
|
||||
declared_arg_slots,
|
||||
declared_ret_slots,
|
||||
expected_arg_slots,
|
||||
expected_ret_slots,
|
||||
},
|
||||
)) => {
|
||||
VmInitError::LoaderPatchFailed(
|
||||
crate::vm_init_error::LoaderPatchError::ResolveFailed(
|
||||
prometeu_hal::syscalls::DeclaredLoadError::AbiMismatch {
|
||||
module,
|
||||
name,
|
||||
version,
|
||||
declared_arg_slots,
|
||||
declared_ret_slots,
|
||||
expected_arg_slots,
|
||||
expected_ret_slots,
|
||||
},
|
||||
),
|
||||
) => {
|
||||
assert_eq!(module, syscall.module);
|
||||
assert_eq!(name, syscall.name);
|
||||
assert_eq!(version, syscall.version);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
{"type":"meta","next_id":{"DSC":18,"AGD":17,"DEC":1,"PLN":1,"LSN":19,"CLSN":1}}
|
||||
{"type":"meta","next_id":{"DSC":19,"AGD":18,"DEC":2,"PLN":2,"LSN":20,"CLSN":1}}
|
||||
{"type":"discussion","id":"DSC-0001","status":"done","ticket":"legacy-runtime-learn-import","title":"Import legacy runtime learn into discussion lessons","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["migration","tech-debt"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0001","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0001-prometeu-learn-index.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0002","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0002-historical-asset-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0003","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0003-historical-audio-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0004","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0004-historical-cartridge-boot-protocol-and-manifest-authority.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0005","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0005-historical-game-memcard-slots-surface-and-semantics.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0006","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0006-historical-gfx-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0007","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0007-historical-retired-fault-and-input-decisions.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0008","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0008-historical-vm-core-and-assets.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0009","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0009-mental-model-asset-management.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0010","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0010-mental-model-audio.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0011","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0011-mental-model-gfx.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0012","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0012-mental-model-input.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0013","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0013-mental-model-observability-and-debugging.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0014","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0014-mental-model-portability-and-cross-platform.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0015","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0015-mental-model-save-memory-and-memcard.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0016","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0016-mental-model-status-first-and-fault-thinking.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0017","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0017-mental-model-time-and-cycles.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0018","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0018-mental-model-touch.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]}
|
||||
{"type":"discussion","id":"DSC-0002","status":"open","ticket":"runtime-edge-test-plan","title":"Agenda - Runtime Edge Test Plan","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0001","file":"AGD-0001-runtime-edge-test-plan.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0003","status":"open","ticket":"packed-cartridge-loader-pmc","title":"Agenda - Packed Cartridge Loader PMC","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0002","file":"AGD-0002-packed-cartridge-loader-pmc.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
@ -16,3 +16,4 @@
|
||||
{"type":"discussion","id":"DSC-0015","status":"open","ticket":"perf-cartridge-boot-and-program-ownership","title":"Agenda - [PERF] Cartridge Boot and Program Ownership","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0014","file":"AGD-0014-perf-cartridge-boot-and-program-ownership.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0016","status":"open","ticket":"tilemap-empty-cell-vs-tile-id-zero","title":"Tilemap Empty Cell vs Tile ID Zero","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0015","file":"AGD-0015-tilemap-empty-cell-vs-tile-id-zero.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0017","status":"open","ticket":"asset-entry-metadata-normalization-contract","title":"Asset Entry Metadata Normalization Contract","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0016","file":"AGD-0016-asset-entry-metadata-normalization-contract.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0018","status":"done","ticket":"asset-load-asset-id-int-contract","title":"Asset Load Asset ID Int Contract","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["asset","runtime","abi"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0019","file":"lessons/DSC-0018-asset-load-asset-id-int-contract/LSN-0019-asset-load-id-abi-convergence.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]}
|
||||
|
||||
@ -0,0 +1,48 @@
|
||||
---
|
||||
id: LSN-0019
|
||||
ticket: asset-load-asset-id-int-contract
|
||||
title: Asset Load ABI Must Converge with Asset Table Identity
|
||||
created: 2026-03-27
|
||||
tags: [asset, runtime, abi]
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The runtime had already converged on numeric asset identity in the cartridge model:
|
||||
|
||||
- `AssetId` was `i32`;
|
||||
- `preload` used `{ asset_id, slot }`;
|
||||
- cartridge bootstrap validated preload entries against `asset_table` by `asset_id`.
|
||||
|
||||
The remaining inconsistency was the public runtime syscall surface. `asset.load` still accepted a string identifier plus an explicit asset kind, forcing the runtime to keep a `name_to_id` translation path alive even though `asset_table` already carried canonical identity and bank classification.
|
||||
|
||||
## Key Decisions
|
||||
|
||||
### Asset Load Asset ID Int Contract
|
||||
|
||||
**What:**
|
||||
`asset.load` now uses the canonical runtime form `asset.load(asset_id, slot)`. The public ABI no longer accepts string asset names or an explicit `asset_type` argument.
|
||||
|
||||
**Why:**
|
||||
Once `asset_table` exists, asset kind is derivable from `asset_id`. Keeping `asset_type` in the ABI only duplicates information and creates a second place for disagreement. Keeping string names in the load path also preserves a shadow identity model that the runtime no longer needs.
|
||||
|
||||
**Trade-offs:**
|
||||
This is a hard cut with no compatibility layer. Legacy producers that still emit name-based asset loads must update, but the runtime stays simpler and the contract becomes unambiguous.
|
||||
|
||||
## Patterns and Algorithms
|
||||
|
||||
- Treat `asset_table` as the single source of truth for operational asset identity.
|
||||
- Infer internal bank-qualified slot routing from the resolved `AssetEntry` rather than from caller-supplied ABI fields.
|
||||
- Keep human-readable names as metadata and telemetry only, not as runtime lookup keys.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- If preload, loader validation, and on-demand runtime loading use different identity forms, the system develops a split contract that is easy to document incorrectly and hard to evolve safely.
|
||||
- ABI migrations that leave dual support for both names and IDs tend to preserve ambiguity longer than intended.
|
||||
- Removing a public parameter such as `asset_type` may also remove previously observable error states, so tests and specs must be updated together.
|
||||
|
||||
## Takeaways
|
||||
|
||||
- When a cartridge-wide identity table already exists, public runtime surfaces should use that identity directly.
|
||||
- Derivable ABI fields should not remain public inputs once the derivation source is canonical.
|
||||
- Asset preload and on-demand asset load should follow the same identity model end to end.
|
||||
@ -163,15 +163,17 @@ The current runtime exposes bank types:
|
||||
- `TILES`
|
||||
- `SOUNDS`
|
||||
|
||||
Assets are loaded into explicit slots identified by bank context plus index.
|
||||
Assets are loaded into explicit slots identified by slot index at the public ABI boundary.
|
||||
|
||||
Conceptual slot reference:
|
||||
The runtime resolves bank context from `asset_table` using `asset_id`.
|
||||
|
||||
Internally, the runtime may still use a bank-qualified slot reference such as:
|
||||
|
||||
```text
|
||||
SlotRef { bank_type, index }
|
||||
```
|
||||
|
||||
This prevents ambiguity between graphics and audio residency.
|
||||
That internal representation is derived from the resolved `AssetEntry`, not supplied by the caller.
|
||||
|
||||
## 6 Load Lifecycle
|
||||
|
||||
@ -285,7 +287,7 @@ Fault boundary:
|
||||
|
||||
### 11.1 MVP syscall shape
|
||||
|
||||
- `asset.load(name, kind, slot) -> (status:int, handle:int)`
|
||||
- `asset.load(asset_id, slot) -> (status:int, handle:int)`
|
||||
- `asset.status(handle) -> status:int`
|
||||
- `asset.commit(handle) -> status:int`
|
||||
- `asset.cancel(handle) -> status:int`
|
||||
@ -295,7 +297,9 @@ 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.
|
||||
- slot validation, kind mismatch, and residency/lifecycle rejection remain in `asset` status space and are not delegated to `bank`.
|
||||
- `asset.load` resolves the target bank type from `asset_table` using `asset_id`;
|
||||
- public callers must not supply `asset_name` or `bank_type` to `asset.load`;
|
||||
- slot validation and residency/lifecycle rejection remain in `asset` status space and are not delegated to `bank`.
|
||||
|
||||
### 11.2 Minimum status tables
|
||||
|
||||
@ -303,7 +307,6 @@ Rules:
|
||||
|
||||
- `0` = `OK`
|
||||
- `3` = `ASSET_NOT_FOUND`
|
||||
- `4` = `SLOT_KIND_MISMATCH`
|
||||
- `5` = `SLOT_INDEX_INVALID`
|
||||
- `6` = `BACKEND_ERROR`
|
||||
|
||||
|
||||
@ -181,6 +181,23 @@ Canonical operations in v1 are:
|
||||
|
||||
Semantics and domain status catalog are defined by [`08-save-memory-and-memcard.md`](08-save-memory-and-memcard.md).
|
||||
|
||||
### Asset surface (`asset`, v1)
|
||||
|
||||
The asset runtime surface also follows the status-first ABI shape.
|
||||
|
||||
Canonical operations in v1 are:
|
||||
|
||||
- `asset.load(asset_id, slot) -> (status, handle)`
|
||||
- `asset.status(handle) -> status`
|
||||
- `asset.commit(handle) -> status`
|
||||
- `asset.cancel(handle) -> status`
|
||||
|
||||
For `asset.load`:
|
||||
|
||||
- `asset_id` is a signed 32-bit runtime identity;
|
||||
- `slot` is the target slot index;
|
||||
- bank kind is resolved from `asset_table` by `asset_id`, not supplied by the caller.
|
||||
|
||||
## 7 Syscalls as Callable Entities (Not First-Class)
|
||||
|
||||
Syscalls behave like call sites, not like first-class guest values.
|
||||
|
||||
@ -149,6 +149,7 @@ The verifier statically checks bytecode for structural safety and stack‑shape
|
||||
- `SyscallMeta` defines expected arity and return slot counts. The loader resolves `HOSTCALL` against this metadata and rejects raw `SYSCALL` in PBX pre-load artifacts; the verifier checks final IDs/arity/return‑slot counts against the same metadata.
|
||||
- Arguments and returns
|
||||
- Arguments are taken from the operand stack in the order defined by the ABI. Returns use multi‑slot results via a host‑side return buffer (`HostReturn`) which the VM copies back onto the stack, or zero slots for “void”. A mismatch in result counts is a fault/panic per current hardening logic.
|
||||
- Example: the canonical asset runtime load surface is `asset.load(asset_id, slot) -> (status, handle)`. The caller does not supply `asset_name` or `asset_type`; bank kind is derived from `asset_table` using `asset_id`.
|
||||
- Capabilities
|
||||
- Cartridge capability flags are applied before load-time host resolution. Missing required capability aborts load; invoking a syscall without the required capability also traps defensively at runtime.
|
||||
|
||||
|
||||
@ -78,6 +78,14 @@ Authority rule:
|
||||
- `INTRINSIC u32` — final numeric VM-owned intrinsic call.
|
||||
- `FRAME_SYNC` — yield until the next frame boundary (e.g., vblank); explicit safepoint.
|
||||
|
||||
Host service arity is not encoded in the opcode itself. It is defined by resolved syscall metadata.
|
||||
|
||||
Example:
|
||||
|
||||
- `asset.load` currently resolves with `arg_slots = 2` and `ret_slots = 2`.
|
||||
- The canonical stack contract is `asset_id, slot -> status, handle`.
|
||||
- Callers do not provide an explicit asset kind; the runtime derives it from `asset_table`.
|
||||
|
||||
#### Canonical Intrinsic Registry Artifact
|
||||
|
||||
- Final intrinsic IDs and intrinsic stack metadata are published in [`INTRINSICS.csv`](INTRINSICS.csv).
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user