diff --git a/crates/console/prometeu-drivers/src/asset.rs b/crates/console/prometeu-drivers/src/asset.rs index b437f64a..1027a131 100644 --- a/crates/console/prometeu-drivers/src/asset.rs +++ b/crates/console/prometeu-drivers/src/asset.rs @@ -786,29 +786,37 @@ mod tests { use super::*; use crate::memory_banks::{MemoryBanks, SoundBankPoolAccess, TileBankPoolAccess}; - #[test] - fn test_asset_loading_flow() { - let banks = Arc::new(MemoryBanks::new()); - let gfx_installer = Arc::clone(&banks) as Arc; - let sound_installer = Arc::clone(&banks) as Arc; - + fn test_tile_asset_data() -> Vec { let mut data = vec![1u8; 256]; data.extend_from_slice(&[0u8; 2048]); + data + } - let asset_entry = AssetEntry { + fn test_tile_asset_entry(asset_name: &str, data_len: usize) -> AssetEntry { + AssetEntry { asset_id: 0, - asset_name: "test_tiles".to_string(), + asset_name: asset_name.to_string(), bank_type: BankType::TILES, offset: 0, - size: data.len() as u64, - decoded_size: data.len() as u64, + size: data_len as u64, + decoded_size: data_len as u64, codec: "RAW".to_string(), metadata: serde_json::json!({ "tile_size": 16, "width": 16, "height": 16 }), - }; + } + } + + #[test] + fn test_asset_loading_flow() { + 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 asset_entry = test_tile_asset_entry("test_tiles", data.len()); let am = AssetManager::new(vec![asset_entry], data, gfx_installer, sound_installer); let slot = SlotRef::gfx(0); @@ -842,23 +850,8 @@ mod tests { let gfx_installer = Arc::clone(&banks) as Arc; let sound_installer = Arc::clone(&banks) as Arc; - let mut data = vec![1u8; 256]; - data.extend_from_slice(&[0u8; 2048]); - - let asset_entry = AssetEntry { - asset_id: 0, - asset_name: "test_tiles".to_string(), - bank_type: BankType::TILES, - offset: 0, - size: data.len() as u64, - decoded_size: data.len() as u64, - codec: "RAW".to_string(), - metadata: serde_json::json!({ - "tile_size": 16, - "width": 16, - "height": 16 - }), - }; + let data = test_tile_asset_data(); + let asset_entry = test_tile_asset_entry("test_tiles", data.len()); let am = AssetManager::new(vec![asset_entry], data, gfx_installer, sound_installer); @@ -959,19 +952,9 @@ mod tests { let gfx_installer = Arc::clone(&banks) as Arc; let sound_installer = Arc::clone(&banks) as Arc; - let mut data = vec![0u8; 256]; // pixels - data.extend_from_slice(&[0u8; 2048]); // palette - - let asset_entry = AssetEntry { - asset_id: 10, - asset_name: "my_tiles".to_string(), - bank_type: BankType::TILES, - offset: 0, - size: data.len() as u64, - decoded_size: data.len() as u64, - codec: "RAW".to_string(), - metadata: serde_json::json!({ "tile_size": 16, "width": 16, "height": 16 }), - }; + let data = test_tile_asset_data(); + let mut asset_entry = test_tile_asset_entry("my_tiles", data.len()); + asset_entry.asset_id = 10; let preload = vec![PreloadEntry { asset_name: "my_tiles".to_string(), slot: 3 }]; @@ -982,4 +965,89 @@ mod tests { assert_eq!(am.find_slot_by_name("unknown", BankType::TILES), None); assert_eq!(am.find_slot_by_name("my_tiles", BankType::SOUNDS), None); } + + #[test] + fn test_load_returns_asset_not_found() { + let banks = Arc::new(MemoryBanks::new()); + let gfx_installer = Arc::clone(&banks) as Arc; + let sound_installer = Arc::clone(&banks) as Arc; + let am = AssetManager::new(vec![], vec![], gfx_installer, sound_installer); + + let result = am.load("missing", SlotRef::gfx(0)); + + assert_eq!(result, Err(AssetLoadError::AssetNotFound)); + } + + #[test] + fn test_load_returns_slot_index_invalid() { + 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", data.len())], + data, + gfx_installer, + sound_installer, + ); + + let result = am.load("test_tiles", SlotRef::gfx(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", data.len())], + 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()); + let gfx_installer = Arc::clone(&banks) as Arc; + let sound_installer = Arc::clone(&banks) as Arc; + let am = AssetManager::new(vec![], vec![], gfx_installer, sound_installer); + + assert_eq!(am.status(999), LoadStatus::UnknownHandle); + } + + #[test] + fn test_commit_and_cancel_return_explicit_statuses() { + 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", data.len())], + data, + gfx_installer, + sound_installer, + ); + + 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 start = Instant::now(); + while am.status(handle) != LoadStatus::READY && start.elapsed().as_secs() < 5 { + thread::sleep(std::time::Duration::from_millis(10)); + } + + assert_eq!(am.cancel(handle), AssetOpStatus::Ok); + assert_eq!(am.status(handle), LoadStatus::CANCELED); + assert_eq!(am.commit(handle), AssetOpStatus::InvalidState); + } } diff --git a/crates/console/prometeu-hal/src/asset.rs b/crates/console/prometeu-hal/src/asset.rs index 771003f7..e78b7d6b 100644 --- a/crates/console/prometeu-hal/src/asset.rs +++ b/crates/console/prometeu-hal/src/asset.rs @@ -30,23 +30,24 @@ pub struct PreloadEntry { } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[repr(i32)] pub enum LoadStatus { - PENDING, - LOADING, - READY, - COMMITTED, - CANCELED, - ERROR, - UnknownHandle, + PENDING = 0, + LOADING = 1, + READY = 2, + COMMITTED = 3, + CANCELED = 4, + ERROR = 5, + UnknownHandle = 6, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(i32)] pub enum AssetLoadError { - AssetNotFound = 1, - SlotKindMismatch = 2, - SlotIndexInvalid = 3, - BackendError = 4, + AssetNotFound = 3, + SlotKindMismatch = 4, + SlotIndexInvalid = 5, + BackendError = 6, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] 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 84b4376b..7f0689ae 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::{AssetLoadError, AssetOpStatus, BankType, LoadStatus, SlotRef}; +use prometeu_hal::asset::{AssetOpStatus, BankType, SlotRef}; use prometeu_hal::cartridge::AppMode; use prometeu_hal::color::Color; use prometeu_hal::log::{LogLevel, LogSource}; @@ -458,29 +458,14 @@ impl NativeInterface for VirtualMachineRuntime { Ok(()) } Err(status) => { - let status_val = match status { - AssetLoadError::AssetNotFound => 3, - AssetLoadError::SlotKindMismatch => 4, - AssetLoadError::SlotIndexInvalid => 5, - AssetLoadError::BackendError => 6, - }; - ret.push_int(status_val); + ret.push_int(status as i64); ret.push_int(0); Ok(()) } } } Syscall::AssetStatus => { - let status_val = match hw.assets().status(expect_int(args, 0)? as u32) { - LoadStatus::PENDING => 0, - LoadStatus::LOADING => 1, - LoadStatus::READY => 2, - LoadStatus::COMMITTED => 3, - LoadStatus::CANCELED => 4, - LoadStatus::ERROR => 5, - LoadStatus::UnknownHandle => 6, - }; - ret.push_int(status_val); + ret.push_int(hw.assets().status(expect_int(args, 0)? as u32) as i64); Ok(()) } Syscall::AssetCommit => { 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 befcb10d..249184cc 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs @@ -6,7 +6,7 @@ use prometeu_bytecode::model::{BytecodeModule, ConstantPoolEntry, FunctionMeta, use prometeu_drivers::hardware::Hardware; use crate::fs::{FsBackend, FsEntry, FsError}; use prometeu_hal::AudioOpStatus; -use prometeu_hal::asset::AssetOpStatus; +use prometeu_hal::asset::{AssetEntry, AssetLoadError, AssetOpStatus, BankType, LoadStatus}; use prometeu_hal::GfxOpStatus; use prometeu_hal::InputSignals; use prometeu_hal::cartridge::Cartridge; @@ -92,6 +92,29 @@ fn serialized_single_function_module_with_consts( .serialize() } +fn test_tile_asset_entry(asset_name: &str, data_len: usize) -> AssetEntry { + AssetEntry { + asset_id: 7, + asset_name: asset_name.to_string(), + bank_type: BankType::TILES, + offset: 0, + size: data_len as u64, + decoded_size: data_len as u64, + codec: "RAW".to_string(), + metadata: serde_json::json!({ + "tile_size": 16, + "width": 16, + "height": 16 + }), + } +} + +fn test_tile_asset_data() -> Vec { + let mut data = vec![1u8; 256]; + data.extend_from_slice(&[0u8; 2048]); + data +} + #[test] fn initialize_vm_applies_cartridge_capabilities_before_loader_resolution() { let mut runtime = VirtualMachineRuntime::new(None); @@ -569,6 +592,184 @@ fn tick_asset_commit_operational_error_returns_status_not_crash() { assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AssetOpStatus::UnknownHandle as i64)]); } +#[test] +fn tick_asset_load_missing_asset_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 code = assemble("PUSH_CONST 0\nPUSH_I32 0\nPUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"); + let program = serialized_single_function_module_with_consts( + code, + vec![ConstantPoolEntry::String("missing_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(), "missing asset must not crash"); + assert!(vm.is_halted()); + assert_eq!( + vm.operand_stack_top(2), + vec![Value::Int64(0), Value::Int64(AssetLoadError::AssetNotFound as i64)] + ); +} + +#[test] +fn tick_asset_load_invalid_slot_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![], + 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( + 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(), "invalid slot must not crash"); + assert!(vm.is_halted()); + assert_eq!( + vm.operand_stack_top(2), + vec![Value::Int64(0), Value::Int64(AssetLoadError::SlotIndexInvalid as i64)] + ); +} + +#[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![], + 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); + let mut vm = VirtualMachine::default(); + let mut hardware = Hardware::new(); + let signals = InputSignals::default(); + let code = assemble("PUSH_I32 999\nHOSTCALL 0\nHALT").expect("assemble"); + let program = serialized_single_function_module( + code, + vec![SyscallDecl { + module: "asset".into(), + name: "status".into(), + version: 1, + arg_slots: 1, + ret_slots: 1, + }], + ); + 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(), "unknown asset handle must not crash"); + assert!(vm.is_halted()); + assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(LoadStatus::UnknownHandle as i64)]); +} + +#[test] +fn tick_asset_commit_invalid_transition_returns_status_not_crash() { + let mut runtime = VirtualMachineRuntime::new(None); + let mut vm = VirtualMachine::default(); + let mut hardware = Hardware::new(); + let signals = InputSignals::default(); + let code = assemble( + "PUSH_I32 1\nHOSTCALL 0\nPOP_N 1\nPUSH_I32 1\nHOSTCALL 1\nHALT", + ) + .expect("assemble"); + let program = serialized_single_function_module( + code, + vec![ + SyscallDecl { + module: "asset".into(), + name: "cancel".into(), + version: 1, + arg_slots: 1, + ret_slots: 1, + }, + SyscallDecl { + module: "asset".into(), + name: "commit".into(), + version: 1, + arg_slots: 1, + ret_slots: 1, + }, + ], + ); + let cartridge = cartridge_with_program(program, caps::ASSET); + + let asset_data = test_tile_asset_data(); + hardware.assets.initialize_for_cartridge( + vec![test_tile_asset_entry("tile_asset", asset_data.len())], + vec![], + asset_data, + ); + let handle = hardware + .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"); + let report = runtime.tick(&mut vm, &signals, &mut hardware); + assert!(report.is_none(), "invalid transition must not crash"); + assert!(vm.is_halted()); + assert_eq!(handle, 1); + assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AssetOpStatus::InvalidState as i64)]); +} + #[test] fn tick_gfx_set_sprite_type_mismatch_surfaces_trap_not_panic() { let mut runtime = VirtualMachineRuntime::new(None); diff --git a/docs/runtime/pull-requests/PR004-asset-status-first-surface-and-lifecycle.md b/docs/runtime/pull-requests/PR004-asset-status-first-surface-and-lifecycle.md deleted file mode 100644 index 95e41211..00000000 --- a/docs/runtime/pull-requests/PR004-asset-status-first-surface-and-lifecycle.md +++ /dev/null @@ -1,54 +0,0 @@ -# PR004 - Asset Status-First Surface and Lifecycle - -## Briefing - -A decision `010` fechou a superficie final de `asset`: - -- `asset.load(name, kind, slot) -> (status, handle)` -- `asset.status(handle) -> status` -- `asset.commit(handle) -> status` -- `asset.cancel(handle) -> status` - -Sem no-op silencioso e sem `Panic` operacional. - -## Alvo - -Implementar o contrato de `asset` em spec, registry e runtime. - -Arquivos principais: - -- `docs/runtime/specs/15-asset-management.md` -- `crates/console/prometeu-hal/src/syscalls/domains/asset.rs` -- `crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs` -- `crates/console/prometeu-drivers/src/asset.rs` -- `crates/console/prometeu-hal/src/asset_bridge.rs` (se necessario) - -## Escopo Funcional - -- substituir `Err -> Panic` de `asset.load` por status operacional; -- tornar `asset.commit` e `asset.cancel` retornos explicitos de status; -- explicitar `UNKNOWN_HANDLE` em `asset.status`; -- validar slot index/kind no request path de `load`; -- manter erros de residency/slot no dominio `asset` (nao migrar para `bank`). - -## Fora de Escopo - -- redesign da politica interna de residencia; -- novos dominios de asset alem de `TILES` e `SOUNDS`. - -## Critérios de Aceite - -- assinaturas de syscall `asset` alinhadas com decision `010`; -- `asset.load` nao produz `Panic` para falha operacional; -- `commit`/`cancel` nao ficam em no-op silencioso; -- tabela de status por operacao documentada e testada. - -## Tests - -- `cargo test -p prometeu-system` -- `cargo test -p prometeu-drivers` -- cenarios de: - - handle desconhecido; - - transicao invalida; - - asset nao encontrado; - - slot invalido/kind mismatch. diff --git a/docs/runtime/specs/15-asset-management.md b/docs/runtime/specs/15-asset-management.md index 96bdad9f..4141c6ec 100644 --- a/docs/runtime/specs/15-asset-management.md +++ b/docs/runtime/specs/15-asset-management.md @@ -149,6 +149,7 @@ 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`. ### 11.2 Minimum status tables