GFX Status-First Surface and Fault Matrix

This commit is contained in:
bQUARKz 2026-03-10 09:32:46 +00:00
parent 5df15d0c6d
commit fd5a5cd22c
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
7 changed files with 136 additions and 75 deletions

View File

@ -11,6 +11,15 @@ pub enum BlendMode {
Full,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum GfxOpStatus {
Ok = 0,
AssetNotFound = 1,
InvalidSpriteIndex = 2,
ArgRangeInvalid = 3,
}
pub trait GfxBridge {
fn size(&self) -> (usize, usize);
fn front_buffer(&self) -> &[u16];

View File

@ -29,7 +29,7 @@ pub mod window;
pub use asset_bridge::AssetBridge;
pub use audio_bridge::{AudioBridge, AudioOpStatus, LoopMode};
pub use gfx_bridge::{BlendMode, GfxBridge};
pub use gfx_bridge::{BlendMode, GfxBridge, GfxOpStatus};
pub use hardware_bridge::HardwareBridge;
pub use host_context::{HostContext, HostContextProvider};
pub use host_return::HostReturn;

View File

@ -170,10 +170,42 @@ fn declared_resolver_rejects_abi_mismatch() {
#[test]
fn status_first_syscall_signatures_are_pinned() {
let clear = meta_for(Syscall::GfxClear);
assert_eq!(clear.arg_slots, 1);
assert_eq!(clear.ret_slots, 0);
let fill_rect = meta_for(Syscall::GfxFillRect);
assert_eq!(fill_rect.arg_slots, 5);
assert_eq!(fill_rect.ret_slots, 0);
let draw_line = meta_for(Syscall::GfxDrawLine);
assert_eq!(draw_line.arg_slots, 5);
assert_eq!(draw_line.ret_slots, 0);
let draw_circle = meta_for(Syscall::GfxDrawCircle);
assert_eq!(draw_circle.arg_slots, 4);
assert_eq!(draw_circle.ret_slots, 0);
let draw_disc = meta_for(Syscall::GfxDrawDisc);
assert_eq!(draw_disc.arg_slots, 5);
assert_eq!(draw_disc.ret_slots, 0);
let draw_square = meta_for(Syscall::GfxDrawSquare);
assert_eq!(draw_square.arg_slots, 6);
assert_eq!(draw_square.ret_slots, 0);
let set_sprite = meta_for(Syscall::GfxSetSprite);
assert_eq!(set_sprite.arg_slots, 10);
assert_eq!(set_sprite.ret_slots, 1);
let draw_text = meta_for(Syscall::GfxDrawText);
assert_eq!(draw_text.arg_slots, 4);
assert_eq!(draw_text.ret_slots, 0);
let clear_565 = meta_for(Syscall::GfxClear565);
assert_eq!(clear_565.arg_slots, 1);
assert_eq!(clear_565.ret_slots, 0);
let audio_play_sample = meta_for(Syscall::AudioPlaySample);
assert_eq!(audio_play_sample.arg_slots, 5);
assert_eq!(audio_play_sample.ret_slots, 1);

View File

@ -10,15 +10,11 @@ use prometeu_hal::syscalls::Syscall;
use prometeu_hal::tile::Tile;
use prometeu_hal::vm_fault::VmFault;
use prometeu_hal::{
AudioOpStatus, HostContext, HostReturn, NativeInterface, SyscallId, expect_bool, expect_int,
AudioOpStatus, GfxOpStatus, HostContext, HostReturn, NativeInterface, SyscallId, expect_bool,
expect_int,
};
impl VirtualMachineRuntime {
const GFX_STATUS_OK: i64 = 0;
const GFX_STATUS_ASSET_NOT_FOUND: i64 = 1;
const GFX_STATUS_INVALID_SPRITE_INDEX: i64 = 2;
const GFX_STATUS_INVALID_ARG_RANGE: i64 = 3;
fn syscall_log_write(&mut self, level_val: i64, tag: u16, msg: String) -> Result<(), VmFault> {
let level = match level_val {
0 => LogLevel::Trace,
@ -139,13 +135,7 @@ impl NativeInterface for VirtualMachineRuntime {
Ok(())
}
Syscall::GfxSetSprite => {
let asset_name = match args
.first()
.ok_or_else(|| VmFault::Panic("Missing asset_name".into()))?
{
Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string asset_name".into())),
};
let asset_name = expect_string(args, 0, "asset_name")?;
let index = expect_int(args, 1)? as usize;
let x = expect_int(args, 2)? as i32;
let y = expect_int(args, 3)? as i32;
@ -157,15 +147,15 @@ impl NativeInterface for VirtualMachineRuntime {
let priority = expect_int(args, 9)? as u8;
if index >= 512 {
ret.push_int(Self::GFX_STATUS_INVALID_SPRITE_INDEX);
ret.push_int(GfxOpStatus::InvalidSpriteIndex as i64);
return Ok(());
}
if palette_id >= 64 || priority >= 5 {
ret.push_int(Self::GFX_STATUS_INVALID_ARG_RANGE);
ret.push_int(GfxOpStatus::ArgRangeInvalid as i64);
return Ok(());
}
let Some(bank_id) = hw.assets().find_slot_by_name(&asset_name, BankType::TILES) else {
ret.push_int(Self::GFX_STATUS_ASSET_NOT_FOUND);
ret.push_int(GfxOpStatus::AssetNotFound as i64);
return Ok(());
};
@ -179,19 +169,13 @@ impl NativeInterface for VirtualMachineRuntime {
flip_y,
priority,
};
ret.push_int(Self::GFX_STATUS_OK);
ret.push_int(GfxOpStatus::Ok as i64);
Ok(())
}
Syscall::GfxDrawText => {
let x = expect_int(args, 0)? as i32;
let y = expect_int(args, 1)? as i32;
let msg = match args
.get(2)
.ok_or_else(|| VmFault::Panic("Missing message".into()))?
{
Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string message".into())),
};
let msg = expect_string(args, 2, "message")?;
let color = self.get_color(expect_int(args, 3)?);
hw.gfx_mut().draw_text(x, y, &msg, color);
Ok(())
@ -536,14 +520,14 @@ impl NativeInterface for VirtualMachineRuntime {
}
fn expect_string(args: &[Value], index: usize, field: &str) -> Result<String, VmFault> {
match args.get(index).ok_or_else(|| VmFault::Panic(format!("Missing {}", field)))? {
match args.get(index).ok_or_else(|| VmFault::Trap(TRAP_TYPE, format!("Missing {}", field)))? {
Value::String(value) => Ok(value.clone()),
_ => Err(VmFault::Trap(TRAP_TYPE, format!("Expected string {}", field))),
}
}
fn expect_number(args: &[Value], index: usize, field: &str) -> Result<f64, VmFault> {
match args.get(index).ok_or_else(|| VmFault::Panic(format!("Missing {}", field)))? {
match args.get(index).ok_or_else(|| VmFault::Trap(TRAP_TYPE, format!("Missing {}", field)))? {
Value::Float(f) => Ok(*f),
Value::Int32(i) => Ok(*i as f64),
Value::Int64(i) => Ok(*i as f64),

View File

@ -7,6 +7,7 @@ use prometeu_drivers::hardware::Hardware;
use crate::fs::{FsBackend, FsEntry, FsError};
use prometeu_hal::AudioOpStatus;
use prometeu_hal::asset::AssetOpStatus;
use prometeu_hal::GfxOpStatus;
use prometeu_hal::InputSignals;
use prometeu_hal::cartridge::Cartridge;
use prometeu_hal::syscalls::caps;
@ -353,7 +354,70 @@ fn tick_gfx_set_sprite_operational_error_returns_status_not_crash() {
let report = runtime.tick(&mut vm, &signals, &mut hardware);
assert!(report.is_none(), "operational error must not crash");
assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(1)]);
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(GfxOpStatus::AssetNotFound as i64)]);
}
#[test]
fn tick_gfx_set_sprite_invalid_index_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_CONST 0\nPUSH_I32 512\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\nHALT",
)
.expect("assemble");
let program = serialized_single_function_module_with_consts(
code,
vec![ConstantPoolEntry::String("missing_tile_bank".into())],
vec![SyscallDecl {
module: "gfx".into(),
name: "set_sprite".into(),
version: 1,
arg_slots: 10,
ret_slots: 1,
}],
);
let cartridge = cartridge_with_program(program, caps::GFX);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
assert!(report.is_none(), "invalid sprite index must not crash");
assert!(vm.is_halted());
assert_eq!(
vm.operand_stack_top(1),
vec![Value::Int64(GfxOpStatus::InvalidSpriteIndex as i64)]
);
}
#[test]
fn tick_gfx_set_sprite_invalid_range_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_CONST 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 64\nPUSH_BOOL 1\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT",
)
.expect("assemble");
let program = serialized_single_function_module_with_consts(
code,
vec![ConstantPoolEntry::String("missing_tile_bank".into())],
vec![SyscallDecl {
module: "gfx".into(),
name: "set_sprite".into(),
version: 1,
arg_slots: 10,
ret_slots: 1,
}],
);
let cartridge = cartridge_with_program(program, caps::GFX);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
assert!(report.is_none(), "invalid gfx parameter range must not crash");
assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(GfxOpStatus::ArgRangeInvalid as i64)]);
}
#[test]

View File

@ -1,47 +0,0 @@
# PR002 - GFX Status-First Surface and Fault Matrix
## Briefing
A decision `008` fechou politica status-first em `gfx`, incluindo:
- sem no-op silencioso para erro operacional;
- retorno `status` quando houver possibilidade real de falha;
- `Trap` apenas estrutural e `Panic` apenas invariante.
## Alvo
Implementar o contrato de `gfx` no runtime e atualizar spec do dominio.
Arquivos principais:
- `docs/runtime/specs/04-gfx-peripheral.md`
- `crates/console/prometeu-hal/src/syscalls/domains/gfx.rs`
- `crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs`
## Escopo Funcional
- fechar matriz por syscall (`void` vs `status`);
- remover fallback implicito em `gfx.set_sprite`;
- mapear casos operacionais para status inteiros por operacao;
- remover `Panic` acidental por argumento de app no dominio `gfx`.
## Fora de Escopo
- redesign de pipeline grafico;
- mudancas de feature fora do conjunto atual de syscalls `gfx`.
## Critérios de Aceite
- metadata de syscall `gfx` (arg/ret slots) alinhada com matriz definida;
- `set_sprite` nao usa fallback implicito para banco default;
- casos operacionais invalidos retornam `status` e nao no-op silencioso;
- testes cobrindo pelo menos:
- sprite index invalido;
- asset ausente;
- faixas invalidas de parametro.
## Tests
- `cargo test -p prometeu-system`
- `cargo test -p prometeu-hal`
- testes de regressao especificos de `dispatch` e dominio `gfx`.

View File

@ -526,6 +526,25 @@ Fault boundary:
### 19.1 `gfx.set_sprite`
Return-shape matrix in v1:
| Syscall | Return | Policy basis |
| ------------------ | ------------- | ---------------------------------------------------- |
| `gfx.clear` | `void` | no real operational failure path in v1 |
| `gfx.fill_rect` | `void` | no real operational failure path in v1 |
| `gfx.draw_line` | `void` | no real operational failure path in v1 |
| `gfx.draw_circle` | `void` | no real operational failure path in v1 |
| `gfx.draw_disc` | `void` | no real operational failure path in v1 |
| `gfx.draw_square` | `void` | no real operational failure path in v1 |
| `gfx.set_sprite` | `status:int` | operational rejection must be explicit |
| `gfx.draw_text` | `void` | no real operational failure path in v1 |
| `gfx.clear_565` | `void` | no real operational failure path in v1 |
Only `gfx.set_sprite` is status-returning in v1.
All other `gfx` syscalls remain `void` unless a future domain revision introduces a real operational failure path.
### 19.2 `gfx.set_sprite`
`gfx.set_sprite` returns `status:int`.
Minimum status table: