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 {
|
pub struct AssetManager {
|
||||||
assets: Arc<RwLock<HashMap<AssetId, AssetEntry>>>,
|
assets: Arc<RwLock<HashMap<AssetId, AssetEntry>>>,
|
||||||
name_to_id: Arc<RwLock<HashMap<String, AssetId>>>,
|
|
||||||
handles: Arc<RwLock<HashMap<HandleId, LoadHandleInfo>>>,
|
handles: Arc<RwLock<HashMap<HandleId, LoadHandleInfo>>>,
|
||||||
next_handle_id: Mutex<HandleId>,
|
next_handle_id: Mutex<HandleId>,
|
||||||
assets_data: Arc<RwLock<AssetsPayloadSource>>,
|
assets_data: Arc<RwLock<AssetsPayloadSource>>,
|
||||||
@ -153,8 +152,8 @@ 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, AssetLoadError> {
|
fn load(&self, asset_id: AssetId, slot_index: usize) -> Result<HandleId, AssetLoadError> {
|
||||||
self.load(asset_name, slot)
|
self.load(asset_id, slot_index)
|
||||||
}
|
}
|
||||||
fn status(&self, handle: HandleId) -> LoadStatus {
|
fn status(&self, handle: HandleId) -> LoadStatus {
|
||||||
self.status(handle)
|
self.status(handle)
|
||||||
@ -268,15 +267,12 @@ impl AssetManager {
|
|||||||
sound_installer: Arc<dyn SoundBankPoolInstaller>,
|
sound_installer: Arc<dyn SoundBankPoolInstaller>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let mut asset_map = HashMap::new();
|
let mut asset_map = HashMap::new();
|
||||||
let mut name_to_id = HashMap::new();
|
|
||||||
for entry in assets {
|
for entry in assets {
|
||||||
name_to_id.insert(entry.asset_name.clone(), entry.asset_id);
|
|
||||||
asset_map.insert(entry.asset_id, entry);
|
asset_map.insert(entry.asset_id, entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
assets: Arc::new(RwLock::new(asset_map)),
|
assets: Arc::new(RwLock::new(asset_map)),
|
||||||
name_to_id: Arc::new(RwLock::new(name_to_id)),
|
|
||||||
gfx_installer,
|
gfx_installer,
|
||||||
sound_installer,
|
sound_installer,
|
||||||
gfx_slots: Arc::new(RwLock::new(std::array::from_fn(|_| None))),
|
gfx_slots: Arc::new(RwLock::new(std::array::from_fn(|_| None))),
|
||||||
@ -299,11 +295,8 @@ impl AssetManager {
|
|||||||
self.shutdown();
|
self.shutdown();
|
||||||
{
|
{
|
||||||
let mut asset_map = self.assets.write().unwrap();
|
let mut asset_map = self.assets.write().unwrap();
|
||||||
let mut name_to_id = self.name_to_id.write().unwrap();
|
|
||||||
asset_map.clear();
|
asset_map.clear();
|
||||||
name_to_id.clear();
|
|
||||||
for entry in assets.iter() {
|
for entry in assets.iter() {
|
||||||
name_to_id.insert(entry.asset_name.clone(), entry.asset_id);
|
|
||||||
asset_map.insert(entry.asset_id, entry.clone());
|
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> {
|
pub fn load(&self, asset_id: AssetId, slot_index: usize) -> Result<HandleId, AssetLoadError> {
|
||||||
if slot.index >= 16 {
|
if slot_index >= 16 {
|
||||||
return Err(AssetLoadError::SlotIndexInvalid);
|
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();
|
assets.get(&asset_id).ok_or(AssetLoadError::AssetNotFound)?.clone()
|
||||||
let id = name_to_id.get(asset_name).ok_or(AssetLoadError::AssetNotFound)?;
|
};
|
||||||
assets.get(id).ok_or(AssetLoadError::BackendError)?.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 mut next_id = self.next_handle_id.lock().unwrap();
|
||||||
let handle_id = *next_id;
|
let handle_id = *next_id;
|
||||||
@ -1021,9 +1011,7 @@ mod tests {
|
|||||||
gfx_installer,
|
gfx_installer,
|
||||||
sound_installer,
|
sound_installer,
|
||||||
);
|
);
|
||||||
let slot = SlotRef::gfx(0);
|
let handle = am.load(0, 0).expect("Should start loading");
|
||||||
|
|
||||||
let handle = am.load("test_tiles", slot).expect("Should start loading");
|
|
||||||
|
|
||||||
let mut status = am.status(handle);
|
let mut status = am.status(handle);
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
@ -1062,13 +1050,13 @@ mod tests {
|
|||||||
sound_installer,
|
sound_installer,
|
||||||
);
|
);
|
||||||
|
|
||||||
let handle1 = am.load("test_tiles", SlotRef::gfx(0)).unwrap();
|
let handle1 = am.load(0, 0).unwrap();
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
while am.status(handle1) != LoadStatus::READY && start.elapsed().as_secs() < 5 {
|
while am.status(handle1) != LoadStatus::READY && start.elapsed().as_secs() < 5 {
|
||||||
thread::sleep(std::time::Duration::from_millis(10));
|
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);
|
assert_eq!(am.status(handle2), LoadStatus::READY);
|
||||||
|
|
||||||
let staging = am.gfx_policy.staging.read().unwrap();
|
let staging = am.gfx_policy.staging.read().unwrap();
|
||||||
@ -1105,9 +1093,7 @@ mod tests {
|
|||||||
gfx_installer,
|
gfx_installer,
|
||||||
sound_installer,
|
sound_installer,
|
||||||
);
|
);
|
||||||
let slot = SlotRef::audio(0);
|
let handle = am.load(1, 0).expect("Should start loading");
|
||||||
|
|
||||||
let handle = am.load("test_sound", slot).expect("Should start loading");
|
|
||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
while am.status(handle) != LoadStatus::READY && start.elapsed().as_secs() < 5 {
|
while am.status(handle) != LoadStatus::READY && start.elapsed().as_secs() < 5 {
|
||||||
@ -1171,7 +1157,7 @@ mod tests {
|
|||||||
let am =
|
let am =
|
||||||
AssetManager::new(vec![], AssetsPayloadSource::empty(), gfx_installer, sound_installer);
|
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));
|
assert_eq!(result, Err(AssetLoadError::AssetNotFound));
|
||||||
}
|
}
|
||||||
@ -1189,29 +1175,11 @@ mod tests {
|
|||||||
sound_installer,
|
sound_installer,
|
||||||
);
|
);
|
||||||
|
|
||||||
let result = am.load("test_tiles", SlotRef::gfx(16));
|
let result = am.load(0, 16);
|
||||||
|
|
||||||
assert_eq!(result, Err(AssetLoadError::SlotIndexInvalid));
|
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]
|
#[test]
|
||||||
fn test_status_returns_unknown_handle() {
|
fn test_status_returns_unknown_handle() {
|
||||||
let banks = Arc::new(MemoryBanks::new());
|
let banks = Arc::new(MemoryBanks::new());
|
||||||
@ -1239,7 +1207,7 @@ mod tests {
|
|||||||
assert_eq!(am.commit(999), AssetOpStatus::UnknownHandle);
|
assert_eq!(am.commit(999), AssetOpStatus::UnknownHandle);
|
||||||
assert_eq!(am.cancel(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();
|
let start = Instant::now();
|
||||||
while am.status(handle) != LoadStatus::READY && start.elapsed().as_secs() < 5 {
|
while am.status(handle) != LoadStatus::READY && start.elapsed().as_secs() < 5 {
|
||||||
thread::sleep(std::time::Duration::from_millis(10));
|
thread::sleep(std::time::Duration::from_millis(10));
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
use crate::asset::{
|
use crate::asset::{
|
||||||
AssetEntry, AssetLoadError, AssetOpStatus, BankStats, BankType, HandleId, LoadStatus,
|
AssetEntry, AssetId, AssetLoadError, AssetOpStatus, BankStats, BankType, HandleId, LoadStatus,
|
||||||
PreloadEntry, SlotRef, SlotStats,
|
PreloadEntry, SlotRef, SlotStats,
|
||||||
};
|
};
|
||||||
use crate::cartridge::AssetsPayloadSource;
|
use crate::cartridge::AssetsPayloadSource;
|
||||||
@ -11,7 +11,7 @@ pub trait AssetBridge {
|
|||||||
preload: Vec<PreloadEntry>,
|
preload: Vec<PreloadEntry>,
|
||||||
assets_data: AssetsPayloadSource,
|
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 status(&self, handle: HandleId) -> LoadStatus;
|
||||||
fn commit(&self, handle: HandleId) -> AssetOpStatus;
|
fn commit(&self, handle: HandleId) -> AssetOpStatus;
|
||||||
fn cancel(&self, handle: HandleId) -> AssetOpStatus;
|
fn cancel(&self, handle: HandleId) -> AssetOpStatus;
|
||||||
|
|||||||
@ -7,7 +7,7 @@ pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
|
|||||||
"asset",
|
"asset",
|
||||||
"load",
|
"load",
|
||||||
1,
|
1,
|
||||||
3,
|
2,
|
||||||
2,
|
2,
|
||||||
caps::ASSET,
|
caps::ASSET,
|
||||||
Determinism::NonDeterministic,
|
Determinism::NonDeterministic,
|
||||||
|
|||||||
@ -215,7 +215,7 @@ fn status_first_syscall_signatures_are_pinned() {
|
|||||||
assert_eq!(audio_play.ret_slots, 1);
|
assert_eq!(audio_play.ret_slots, 1);
|
||||||
|
|
||||||
let asset_load = meta_for(Syscall::AssetLoad);
|
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);
|
assert_eq!(asset_load.ret_slots, 2);
|
||||||
|
|
||||||
let asset_commit = meta_for(Syscall::AssetCommit);
|
let asset_commit = meta_for(Syscall::AssetCommit);
|
||||||
@ -256,7 +256,7 @@ fn declared_resolver_rejects_legacy_status_first_signatures() {
|
|||||||
name: "load".into(),
|
name: "load".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 3,
|
arg_slots: 3,
|
||||||
ret_slots: 1,
|
ret_slots: 2,
|
||||||
},
|
},
|
||||||
prometeu_bytecode::SyscallDecl {
|
prometeu_bytecode::SyscallDecl {
|
||||||
module: "asset".into(),
|
module: "asset".into(),
|
||||||
@ -292,8 +292,10 @@ fn declared_resolver_rejects_legacy_status_first_signatures() {
|
|||||||
assert_eq!(version, decl.version);
|
assert_eq!(version, decl.version);
|
||||||
assert_eq!(declared_arg_slots, decl.arg_slots);
|
assert_eq!(declared_arg_slots, decl.arg_slots);
|
||||||
assert_eq!(declared_ret_slots, decl.ret_slots);
|
assert_eq!(declared_ret_slots, decl.ret_slots);
|
||||||
assert_eq!(expected_arg_slots, declared_arg_slots);
|
assert!(
|
||||||
assert_ne!(expected_ret_slots, declared_ret_slots);
|
expected_arg_slots != declared_arg_slots
|
||||||
|
|| expected_ret_slots != declared_ret_slots
|
||||||
|
);
|
||||||
}
|
}
|
||||||
other => panic!("expected AbiMismatch, got {:?}", other),
|
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(),
|
module: "asset".into(),
|
||||||
name: "load".into(),
|
name: "load".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 3,
|
arg_slots: 2,
|
||||||
ret_slots: 2,
|
ret_slots: 2,
|
||||||
},
|
},
|
||||||
prometeu_bytecode::SyscallDecl {
|
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)
|
let resolved =
|
||||||
.expect("mixed status-first surface must resolve together");
|
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.len(), declared.len());
|
||||||
assert_eq!(resolved[0].meta.ret_slots, 1);
|
assert_eq!(resolved[0].meta.ret_slots, 1);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use prometeu_bytecode::{TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_TYPE, Value};
|
|
||||||
use crate::services::memcard::{MemcardSlotState, MemcardStatus};
|
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::cartridge::AppMode;
|
||||||
use prometeu_hal::color::Color;
|
use prometeu_hal::color::Color;
|
||||||
use prometeu_hal::log::{LogLevel, LogSource};
|
use prometeu_hal::log::{LogLevel, LogSource};
|
||||||
@ -446,15 +446,13 @@ impl NativeInterface for VirtualMachineRuntime {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Syscall::AssetLoad => {
|
Syscall::AssetLoad => {
|
||||||
let asset_id = expect_string(args, 0, "asset_id")?;
|
let raw_asset_id = expect_int(args, 0)?;
|
||||||
let asset_type = match expect_int(args, 1)? as u32 {
|
let asset_id = AssetId::try_from(raw_asset_id).map_err(|_| {
|
||||||
0 => BankType::TILES,
|
VmFault::Trap(TRAP_TYPE, format!("asset_id out of i32 range: {}", raw_asset_id))
|
||||||
1 => BankType::SOUNDS,
|
})?;
|
||||||
_ => return Err(VmFault::Trap(TRAP_TYPE, "Invalid asset type".to_string())),
|
let slot_index = expect_int(args, 1)? as usize;
|
||||||
};
|
|
||||||
let slot = SlotRef { asset_type, index: expect_int(args, 2)? as usize };
|
|
||||||
|
|
||||||
match hw.assets().load(&asset_id, slot) {
|
match hw.assets().load(asset_id, slot_index) {
|
||||||
Ok(handle) => {
|
Ok(handle) => {
|
||||||
ret.push_int(AssetOpStatus::Ok as i64);
|
ret.push_int(AssetOpStatus::Ok as i64);
|
||||||
ret.push_int(handle 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 out = Vec::with_capacity(bytes.len() / 2);
|
||||||
let mut i = 0usize;
|
let mut i = 0usize;
|
||||||
while i < bytes.len() {
|
while i < bytes.len() {
|
||||||
let hi = nibble(bytes[i])
|
let hi = nibble(bytes[i]).ok_or_else(|| {
|
||||||
.ok_or_else(|| VmFault::Trap(TRAP_TYPE, "payload_hex contains invalid hex".to_string()))?;
|
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 lo = nibble(bytes[i + 1]).ok_or_else(|| {
|
||||||
|
VmFault::Trap(TRAP_TYPE, "payload_hex contains invalid hex".to_string())
|
||||||
|
})?;
|
||||||
out.push((hi << 4) | lo);
|
out.push((hi << 4) | lo);
|
||||||
i += 2;
|
i += 2;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -605,16 +605,14 @@ fn tick_asset_load_missing_asset_returns_status_and_zero_handle() {
|
|||||||
let mut vm = VirtualMachine::default();
|
let mut vm = VirtualMachine::default();
|
||||||
let mut hardware = Hardware::new();
|
let mut hardware = Hardware::new();
|
||||||
let signals = InputSignals::default();
|
let signals = InputSignals::default();
|
||||||
let code =
|
let code = assemble("PUSH_I32 999\nPUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble");
|
||||||
assemble("PUSH_CONST 0\nPUSH_I32 0\nPUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble");
|
let program = serialized_single_function_module(
|
||||||
let program = serialized_single_function_module_with_consts(
|
|
||||||
code,
|
code,
|
||||||
vec![ConstantPoolEntry::String("missing_asset".into())],
|
|
||||||
vec![SyscallDecl {
|
vec![SyscallDecl {
|
||||||
module: "asset".into(),
|
module: "asset".into(),
|
||||||
name: "load".into(),
|
name: "load".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 3,
|
arg_slots: 2,
|
||||||
ret_slots: 2,
|
ret_slots: 2,
|
||||||
}],
|
}],
|
||||||
);
|
);
|
||||||
@ -642,16 +640,14 @@ fn tick_asset_load_invalid_slot_returns_status_and_zero_handle() {
|
|||||||
vec![],
|
vec![],
|
||||||
AssetsPayloadSource::from_bytes(asset_data),
|
AssetsPayloadSource::from_bytes(asset_data),
|
||||||
);
|
);
|
||||||
let code =
|
let code = assemble("PUSH_I32 7\nPUSH_I32 16\nHOSTCALL 0\nHALT").expect("assemble");
|
||||||
assemble("PUSH_CONST 0\nPUSH_I32 0\nPUSH_I32 16\nHOSTCALL 0\nHALT").expect("assemble");
|
let program = serialized_single_function_module(
|
||||||
let program = serialized_single_function_module_with_consts(
|
|
||||||
code,
|
code,
|
||||||
vec![ConstantPoolEntry::String("tile_asset".into())],
|
|
||||||
vec![SyscallDecl {
|
vec![SyscallDecl {
|
||||||
module: "asset".into(),
|
module: "asset".into(),
|
||||||
name: "load".into(),
|
name: "load".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 3,
|
arg_slots: 2,
|
||||||
ret_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]
|
#[test]
|
||||||
fn tick_asset_status_unknown_handle_returns_status_not_crash() {
|
fn tick_asset_status_unknown_handle_returns_status_not_crash() {
|
||||||
let mut runtime = VirtualMachineRuntime::new(None);
|
let mut runtime = VirtualMachineRuntime::new(None);
|
||||||
@ -765,10 +724,7 @@ fn tick_asset_commit_invalid_transition_returns_status_not_crash() {
|
|||||||
vec![],
|
vec![],
|
||||||
AssetsPayloadSource::from_bytes(asset_data),
|
AssetsPayloadSource::from_bytes(asset_data),
|
||||||
);
|
);
|
||||||
let handle = hardware
|
let handle = hardware.assets.load(7, 0).expect("asset handle must be allocated");
|
||||||
.assets
|
|
||||||
.load("tile_asset", prometeu_hal::asset::SlotRef::gfx(0))
|
|
||||||
.expect("asset handle must be allocated");
|
|
||||||
|
|
||||||
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
||||||
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
||||||
@ -829,10 +785,7 @@ fn tick_asset_cancel_invalid_transition_returns_status_not_crash() {
|
|||||||
vec![],
|
vec![],
|
||||||
AssetsPayloadSource::from_bytes(asset_data),
|
AssetsPayloadSource::from_bytes(asset_data),
|
||||||
);
|
);
|
||||||
let handle = hardware
|
let handle = hardware.assets.load(7, 0).expect("asset handle must be allocated");
|
||||||
.assets
|
|
||||||
.load("tile_asset", prometeu_hal::asset::SlotRef::gfx(0))
|
|
||||||
.expect("asset handle must be allocated");
|
|
||||||
|
|
||||||
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
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)]);
|
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]
|
#[test]
|
||||||
fn tick_status_first_surface_smoke_across_gfx_audio_and_asset() {
|
fn tick_status_first_surface_smoke_across_gfx_audio_and_asset() {
|
||||||
let mut runtime = VirtualMachineRuntime::new(None);
|
let mut runtime = VirtualMachineRuntime::new(None);
|
||||||
@ -900,13 +818,12 @@ fn tick_status_first_surface_smoke_across_gfx_audio_and_asset() {
|
|||||||
let code = assemble(
|
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 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_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"
|
HALT"
|
||||||
)
|
)
|
||||||
.expect("assemble");
|
.expect("assemble");
|
||||||
let program = serialized_single_function_module_with_consts(
|
let program = serialized_single_function_module(
|
||||||
code,
|
code,
|
||||||
vec![ConstantPoolEntry::String("missing_asset".into())],
|
|
||||||
vec![
|
vec![
|
||||||
SyscallDecl {
|
SyscallDecl {
|
||||||
module: "gfx".into(),
|
module: "gfx".into(),
|
||||||
@ -926,7 +843,7 @@ fn tick_status_first_surface_smoke_across_gfx_audio_and_asset() {
|
|||||||
module: "asset".into(),
|
module: "asset".into(),
|
||||||
name: "load".into(),
|
name: "load".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 3,
|
arg_slots: 2,
|
||||||
ret_slots: 2,
|
ret_slots: 2,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@ -2940,7 +2940,7 @@ mod tests {
|
|||||||
module: "asset".into(),
|
module: "asset".into(),
|
||||||
name: "load".into(),
|
name: "load".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 3,
|
arg_slots: 2,
|
||||||
ret_slots: 2,
|
ret_slots: 2,
|
||||||
},
|
},
|
||||||
SyscallDecl {
|
SyscallDecl {
|
||||||
@ -2996,7 +2996,7 @@ mod tests {
|
|||||||
module: "asset".into(),
|
module: "asset".into(),
|
||||||
name: "load".into(),
|
name: "load".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 3,
|
arg_slots: 2,
|
||||||
ret_slots: 1,
|
ret_slots: 1,
|
||||||
},
|
},
|
||||||
SyscallDecl {
|
SyscallDecl {
|
||||||
@ -3021,17 +3021,19 @@ mod tests {
|
|||||||
let bytes = serialized_single_hostcall_module(syscall.clone());
|
let bytes = serialized_single_hostcall_module(syscall.clone());
|
||||||
let err = vm.initialize(bytes).expect_err("legacy ABI must be rejected");
|
let err = vm.initialize(bytes).expect_err("legacy ABI must be rejected");
|
||||||
match err {
|
match err {
|
||||||
VmInitError::LoaderPatchFailed(crate::vm_init_error::LoaderPatchError::ResolveFailed(
|
VmInitError::LoaderPatchFailed(
|
||||||
prometeu_hal::syscalls::DeclaredLoadError::AbiMismatch {
|
crate::vm_init_error::LoaderPatchError::ResolveFailed(
|
||||||
module,
|
prometeu_hal::syscalls::DeclaredLoadError::AbiMismatch {
|
||||||
name,
|
module,
|
||||||
version,
|
name,
|
||||||
declared_arg_slots,
|
version,
|
||||||
declared_ret_slots,
|
declared_arg_slots,
|
||||||
expected_arg_slots,
|
declared_ret_slots,
|
||||||
expected_ret_slots,
|
expected_arg_slots,
|
||||||
},
|
expected_ret_slots,
|
||||||
)) => {
|
},
|
||||||
|
),
|
||||||
|
) => {
|
||||||
assert_eq!(module, syscall.module);
|
assert_eq!(module, syscall.module);
|
||||||
assert_eq!(name, syscall.name);
|
assert_eq!(name, syscall.name);
|
||||||
assert_eq!(version, syscall.version);
|
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-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-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":[]}
|
{"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-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-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-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`
|
- `TILES`
|
||||||
- `SOUNDS`
|
- `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
|
```text
|
||||||
SlotRef { bank_type, index }
|
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
|
## 6 Load Lifecycle
|
||||||
|
|
||||||
@ -285,7 +287,7 @@ Fault boundary:
|
|||||||
|
|
||||||
### 11.1 MVP syscall shape
|
### 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.status(handle) -> status:int`
|
||||||
- `asset.commit(handle) -> status:int`
|
- `asset.commit(handle) -> status:int`
|
||||||
- `asset.cancel(handle) -> status:int`
|
- `asset.cancel(handle) -> status:int`
|
||||||
@ -295,7 +297,9 @@ Rules:
|
|||||||
- `handle` is valid only when `load` status is `OK`;
|
- `handle` is valid only when `load` status is `OK`;
|
||||||
- failed `load` returns `handle = 0`;
|
- failed `load` returns `handle = 0`;
|
||||||
- `commit` and `cancel` must not be silent no-op for unknown/invalid handle state.
|
- `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
|
### 11.2 Minimum status tables
|
||||||
|
|
||||||
@ -303,7 +307,6 @@ Rules:
|
|||||||
|
|
||||||
- `0` = `OK`
|
- `0` = `OK`
|
||||||
- `3` = `ASSET_NOT_FOUND`
|
- `3` = `ASSET_NOT_FOUND`
|
||||||
- `4` = `SLOT_KIND_MISMATCH`
|
|
||||||
- `5` = `SLOT_INDEX_INVALID`
|
- `5` = `SLOT_INDEX_INVALID`
|
||||||
- `6` = `BACKEND_ERROR`
|
- `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).
|
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)
|
## 7 Syscalls as Callable Entities (Not First-Class)
|
||||||
|
|
||||||
Syscalls behave like call sites, not like first-class guest values.
|
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.
|
- `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 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.
|
- 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
|
- 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.
|
- 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.
|
- `INTRINSIC u32` — final numeric VM-owned intrinsic call.
|
||||||
- `FRAME_SYNC` — yield until the next frame boundary (e.g., vblank); explicit safepoint.
|
- `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
|
#### Canonical Intrinsic Registry Artifact
|
||||||
|
|
||||||
- Final intrinsic IDs and intrinsic stack metadata are published in [`INTRINSICS.csv`](INTRINSICS.csv).
|
- Final intrinsic IDs and intrinsic stack metadata are published in [`INTRINSICS.csv`](INTRINSICS.csv).
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user