Asset Status-First Surface and Lifecycle

This commit is contained in:
bQUARKz 2026-03-10 09:41:20 +00:00
parent 9cb0e77b01
commit 0c32457a10
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
6 changed files with 327 additions and 125 deletions

View File

@ -786,29 +786,37 @@ mod tests {
use super::*; use super::*;
use crate::memory_banks::{MemoryBanks, SoundBankPoolAccess, TileBankPoolAccess}; use crate::memory_banks::{MemoryBanks, SoundBankPoolAccess, TileBankPoolAccess};
#[test] fn test_tile_asset_data() -> Vec<u8> {
fn test_asset_loading_flow() {
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 mut data = vec![1u8; 256]; let mut data = vec![1u8; 256];
data.extend_from_slice(&[0u8; 2048]); 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_id: 0,
asset_name: "test_tiles".to_string(), asset_name: asset_name.to_string(),
bank_type: BankType::TILES, bank_type: BankType::TILES,
offset: 0, offset: 0,
size: data.len() as u64, size: data_len as u64,
decoded_size: data.len() as u64, decoded_size: data_len as u64,
codec: "RAW".to_string(), codec: "RAW".to_string(),
metadata: serde_json::json!({ metadata: serde_json::json!({
"tile_size": 16, "tile_size": 16,
"width": 16, "width": 16,
"height": 16 "height": 16
}), }),
}; }
}
#[test]
fn test_asset_loading_flow() {
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 asset_entry = test_tile_asset_entry("test_tiles", data.len());
let am = AssetManager::new(vec![asset_entry], data, gfx_installer, sound_installer); let am = AssetManager::new(vec![asset_entry], data, gfx_installer, sound_installer);
let slot = SlotRef::gfx(0); let slot = SlotRef::gfx(0);
@ -842,23 +850,8 @@ mod tests {
let gfx_installer = Arc::clone(&banks) as Arc<dyn TileBankPoolInstaller>; let gfx_installer = Arc::clone(&banks) as Arc<dyn TileBankPoolInstaller>;
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>; let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
let mut data = vec![1u8; 256]; let data = test_tile_asset_data();
data.extend_from_slice(&[0u8; 2048]); let asset_entry = test_tile_asset_entry("test_tiles", data.len());
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 am = AssetManager::new(vec![asset_entry], data, gfx_installer, sound_installer); 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<dyn TileBankPoolInstaller>; let gfx_installer = Arc::clone(&banks) as Arc<dyn TileBankPoolInstaller>;
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>; let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
let mut data = vec![0u8; 256]; // pixels let data = test_tile_asset_data();
data.extend_from_slice(&[0u8; 2048]); // palette let mut asset_entry = test_tile_asset_entry("my_tiles", data.len());
asset_entry.asset_id = 10;
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 preload = vec![PreloadEntry { asset_name: "my_tiles".to_string(), slot: 3 }]; 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("unknown", BankType::TILES), None);
assert_eq!(am.find_slot_by_name("my_tiles", BankType::SOUNDS), 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<dyn TileBankPoolInstaller>;
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
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<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", 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<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", 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<dyn TileBankPoolInstaller>;
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
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<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", 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);
}
} }

View File

@ -30,23 +30,24 @@ pub struct PreloadEntry {
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(i32)]
pub enum LoadStatus { pub enum LoadStatus {
PENDING, PENDING = 0,
LOADING, LOADING = 1,
READY, READY = 2,
COMMITTED, COMMITTED = 3,
CANCELED, CANCELED = 4,
ERROR, ERROR = 5,
UnknownHandle, UnknownHandle = 6,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)] #[repr(i32)]
pub enum AssetLoadError { pub enum AssetLoadError {
AssetNotFound = 1, AssetNotFound = 3,
SlotKindMismatch = 2, SlotKindMismatch = 4,
SlotIndexInvalid = 3, SlotIndexInvalid = 5,
BackendError = 4, BackendError = 6,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]

View File

@ -1,7 +1,7 @@
use super::*; use super::*;
use prometeu_bytecode::{TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_TYPE, Value}; use prometeu_bytecode::{TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_TYPE, Value};
use crate::services::memcard::{MemcardSlotState, MemcardStatus}; 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::cartridge::AppMode;
use prometeu_hal::color::Color; use prometeu_hal::color::Color;
use prometeu_hal::log::{LogLevel, LogSource}; use prometeu_hal::log::{LogLevel, LogSource};
@ -458,29 +458,14 @@ impl NativeInterface for VirtualMachineRuntime {
Ok(()) Ok(())
} }
Err(status) => { Err(status) => {
let status_val = match status { ret.push_int(status as i64);
AssetLoadError::AssetNotFound => 3,
AssetLoadError::SlotKindMismatch => 4,
AssetLoadError::SlotIndexInvalid => 5,
AssetLoadError::BackendError => 6,
};
ret.push_int(status_val);
ret.push_int(0); ret.push_int(0);
Ok(()) Ok(())
} }
} }
} }
Syscall::AssetStatus => { Syscall::AssetStatus => {
let status_val = match hw.assets().status(expect_int(args, 0)? as u32) { ret.push_int(hw.assets().status(expect_int(args, 0)? as u32) as i64);
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);
Ok(()) Ok(())
} }
Syscall::AssetCommit => { Syscall::AssetCommit => {

View File

@ -6,7 +6,7 @@ use prometeu_bytecode::model::{BytecodeModule, ConstantPoolEntry, FunctionMeta,
use prometeu_drivers::hardware::Hardware; use prometeu_drivers::hardware::Hardware;
use crate::fs::{FsBackend, FsEntry, FsError}; use crate::fs::{FsBackend, FsEntry, FsError};
use prometeu_hal::AudioOpStatus; 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::GfxOpStatus;
use prometeu_hal::InputSignals; use prometeu_hal::InputSignals;
use prometeu_hal::cartridge::Cartridge; use prometeu_hal::cartridge::Cartridge;
@ -92,6 +92,29 @@ fn serialized_single_function_module_with_consts(
.serialize() .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<u8> {
let mut data = vec![1u8; 256];
data.extend_from_slice(&[0u8; 2048]);
data
}
#[test] #[test]
fn initialize_vm_applies_cartridge_capabilities_before_loader_resolution() { fn initialize_vm_applies_cartridge_capabilities_before_loader_resolution() {
let mut runtime = VirtualMachineRuntime::new(None); 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)]); 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] #[test]
fn tick_gfx_set_sprite_type_mismatch_surfaces_trap_not_panic() { fn tick_gfx_set_sprite_type_mismatch_surfaces_trap_not_panic() {
let mut runtime = VirtualMachineRuntime::new(None); let mut runtime = VirtualMachineRuntime::new(None);

View File

@ -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.

View File

@ -149,6 +149,7 @@ 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`.
### 11.2 Minimum status tables ### 11.2 Minimum status tables