From 201226b8921bd5acea034ecc3c1802162e72d419 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 27 Mar 2026 15:05:37 +0000 Subject: [PATCH] asset manager load by asset id and slot index --- crates/console/prometeu-drivers/src/asset.rs | 64 +++-------- .../console/prometeu-hal/src/asset_bridge.rs | 4 +- .../src/syscalls/domains/asset.rs | 2 +- .../prometeu-hal/src/syscalls/tests.rs | 17 +-- .../src/virtual_machine_runtime/dispatch.rs | 28 ++--- .../src/virtual_machine_runtime/tests.rs | 105 ++---------------- .../prometeu-vm/src/virtual_machine.rs | 28 ++--- discussion/index.ndjson | 3 +- .../LSN-0019-asset-load-id-abi-convergence.md | 48 ++++++++ docs/specs/runtime/15-asset-management.md | 15 ++- .../specs/runtime/16-host-abi-and-syscalls.md | 17 +++ docs/vm-arch/ARCHITECTURE.md | 1 + docs/vm-arch/ISA_CORE.md | 8 ++ 13 files changed, 154 insertions(+), 186 deletions(-) create mode 100644 discussion/lessons/DSC-0018-asset-load-asset-id-int-contract/LSN-0019-asset-load-id-abi-convergence.md diff --git a/crates/console/prometeu-drivers/src/asset.rs b/crates/console/prometeu-drivers/src/asset.rs index b50eecce..26b82eab 100644 --- a/crates/console/prometeu-drivers/src/asset.rs +++ b/crates/console/prometeu-drivers/src/asset.rs @@ -110,7 +110,6 @@ impl BankPolicy { pub struct AssetManager { assets: Arc>>, - name_to_id: Arc>>, handles: Arc>>, next_handle_id: Mutex, assets_data: Arc>, @@ -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 { - self.load(asset_name, slot) + fn load(&self, asset_id: AssetId, slot_index: usize) -> Result { + self.load(asset_id, slot_index) } fn status(&self, handle: HandleId) -> LoadStatus { self.status(handle) @@ -268,15 +267,12 @@ impl AssetManager { sound_installer: Arc, ) -> 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 { - if slot.index >= 16 { + pub fn load(&self, asset_id: AssetId, slot_index: usize) -> Result { + 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; - let sound_installer = Arc::clone(&banks) as Arc; - 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)); diff --git a/crates/console/prometeu-hal/src/asset_bridge.rs b/crates/console/prometeu-hal/src/asset_bridge.rs index b94d5d56..7de6aade 100644 --- a/crates/console/prometeu-hal/src/asset_bridge.rs +++ b/crates/console/prometeu-hal/src/asset_bridge.rs @@ -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, assets_data: AssetsPayloadSource, ); - fn load(&self, asset_name: &str, slot: SlotRef) -> Result; + fn load(&self, asset_id: AssetId, slot_index: usize) -> Result; fn status(&self, handle: HandleId) -> LoadStatus; fn commit(&self, handle: HandleId) -> AssetOpStatus; fn cancel(&self, handle: HandleId) -> AssetOpStatus; diff --git a/crates/console/prometeu-hal/src/syscalls/domains/asset.rs b/crates/console/prometeu-hal/src/syscalls/domains/asset.rs index 30178e50..a57cbba1 100644 --- a/crates/console/prometeu-hal/src/syscalls/domains/asset.rs +++ b/crates/console/prometeu-hal/src/syscalls/domains/asset.rs @@ -7,7 +7,7 @@ pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[ "asset", "load", 1, - 3, + 2, 2, caps::ASSET, Determinism::NonDeterministic, diff --git a/crates/console/prometeu-hal/src/syscalls/tests.rs b/crates/console/prometeu-hal/src/syscalls/tests.rs index 35e8b737..8f3f55af 100644 --- a/crates/console/prometeu-hal/src/syscalls/tests.rs +++ b/crates/console/prometeu-hal/src/syscalls/tests.rs @@ -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); diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs b/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs index 06546bad..2d7375c4 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs @@ -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, 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; } diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs b/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs index c2a67bcc..810c73c1 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs @@ -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, }, ], diff --git a/crates/console/prometeu-vm/src/virtual_machine.rs b/crates/console/prometeu-vm/src/virtual_machine.rs index 3e0ec593..cc8538da 100644 --- a/crates/console/prometeu-vm/src/virtual_machine.rs +++ b/crates/console/prometeu-vm/src/virtual_machine.rs @@ -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); diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 31c71f2c..391f108a 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -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"}]} diff --git a/discussion/lessons/DSC-0018-asset-load-asset-id-int-contract/LSN-0019-asset-load-id-abi-convergence.md b/discussion/lessons/DSC-0018-asset-load-asset-id-int-contract/LSN-0019-asset-load-id-abi-convergence.md new file mode 100644 index 00000000..b4fc3ff4 --- /dev/null +++ b/discussion/lessons/DSC-0018-asset-load-asset-id-int-contract/LSN-0019-asset-load-id-abi-convergence.md @@ -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. diff --git a/docs/specs/runtime/15-asset-management.md b/docs/specs/runtime/15-asset-management.md index 1ea014c2..c14093fa 100644 --- a/docs/specs/runtime/15-asset-management.md +++ b/docs/specs/runtime/15-asset-management.md @@ -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` diff --git a/docs/specs/runtime/16-host-abi-and-syscalls.md b/docs/specs/runtime/16-host-abi-and-syscalls.md index 7d03234e..7ca43033 100644 --- a/docs/specs/runtime/16-host-abi-and-syscalls.md +++ b/docs/specs/runtime/16-host-abi-and-syscalls.md @@ -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. diff --git a/docs/vm-arch/ARCHITECTURE.md b/docs/vm-arch/ARCHITECTURE.md index 6b5a883b..4f81f556 100644 --- a/docs/vm-arch/ARCHITECTURE.md +++ b/docs/vm-arch/ARCHITECTURE.md @@ -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. diff --git a/docs/vm-arch/ISA_CORE.md b/docs/vm-arch/ISA_CORE.md index b254cbd6..f2cea12d 100644 --- a/docs/vm-arch/ISA_CORE.md +++ b/docs/vm-arch/ISA_CORE.md @@ -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).