implements PLN-0023

This commit is contained in:
bQUARKz 2026-04-17 17:55:04 +01:00
parent dd90ff812c
commit cc700c6cf8
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
4 changed files with 261 additions and 46 deletions

View File

@ -51,14 +51,30 @@ impl HardwareBridge for Hardware {
self.frame_composer.begin_frame();
}
fn emit_sprite(&mut self, sprite: Sprite) {
let _ = self.frame_composer.emit_sprite(sprite);
fn bind_scene(&mut self, scene_bank_id: usize) -> bool {
self.frame_composer.bind_scene(scene_bank_id)
}
fn unbind_scene(&mut self) {
self.frame_composer.unbind_scene();
}
fn set_camera(&mut self, x: i32, y: i32) {
self.frame_composer.set_camera(x, y);
}
fn emit_sprite(&mut self, sprite: Sprite) -> bool {
self.frame_composer.emit_sprite(sprite)
}
fn render_frame(&mut self) {
self.frame_composer.render_frame(&mut self.gfx);
}
fn has_glyph_bank(&self, bank_id: usize) -> bool {
self.gfx.glyph_banks.glyph_bank_slot(bank_id).is_some()
}
fn gfx(&self) -> &dyn GfxBridge {
&self.gfx
}

View File

@ -7,8 +7,12 @@ use crate::touch_bridge::TouchBridge;
pub trait HardwareBridge {
fn begin_frame(&mut self);
fn emit_sprite(&mut self, sprite: Sprite);
fn bind_scene(&mut self, scene_bank_id: usize) -> bool;
fn unbind_scene(&mut self);
fn set_camera(&mut self, x: i32, y: i32);
fn emit_sprite(&mut self, sprite: Sprite) -> bool;
fn render_frame(&mut self);
fn has_glyph_bank(&self, bank_id: usize) -> bool;
fn gfx(&self) -> &dyn GfxBridge;
fn gfx_mut(&mut self) -> &mut dyn GfxBridge;

View File

@ -4,11 +4,14 @@ 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::glyph::Glyph;
use prometeu_hal::log::{LogLevel, LogSource};
use prometeu_hal::sprite::Sprite;
use prometeu_hal::syscalls::Syscall;
use prometeu_hal::vm_fault::VmFault;
use prometeu_hal::{
AudioOpStatus, HostContext, HostReturn, NativeInterface, SyscallId, expect_int,
AudioOpStatus, ComposerOpStatus, HostContext, HostReturn, NativeInterface, SyscallId,
expect_bool, expect_int,
};
use std::sync::atomic::Ordering;
@ -53,6 +56,23 @@ impl VirtualMachineRuntime {
pub(crate) fn get_color(&self, value: i64) -> Color {
Color::from_raw(value as u16)
}
fn int_arg_to_usize_status(value: i64) -> Result<usize, ComposerOpStatus> {
usize::try_from(value).map_err(|_| ComposerOpStatus::ArgRangeInvalid)
}
fn int_arg_to_i32_trap(value: i64, name: &str) -> Result<i32, VmFault> {
i32::try_from(value)
.map_err(|_| VmFault::Trap(TRAP_OOB, format!("{name} value out of bounds")))
}
fn int_arg_to_u8_status(value: i64) -> Result<u8, ComposerOpStatus> {
u8::try_from(value).map_err(|_| ComposerOpStatus::ArgRangeInvalid)
}
fn int_arg_to_u16_status(value: i64) -> Result<u16, ComposerOpStatus> {
u16::try_from(value).map_err(|_| ComposerOpStatus::ArgRangeInvalid)
}
}
impl NativeInterface for VirtualMachineRuntime {
@ -148,13 +168,100 @@ impl NativeInterface for VirtualMachineRuntime {
hw.gfx_mut().clear(Color::from_raw(color_val as u16));
Ok(())
}
Syscall::ComposerBindScene
| Syscall::ComposerUnbindScene
| Syscall::ComposerSetCamera
| Syscall::ComposerEmitSprite => Err(VmFault::Trap(
TRAP_INVALID_SYSCALL,
"Composer syscall support is not implemented yet".into(),
)),
Syscall::ComposerBindScene => {
let scene_bank_id = match Self::int_arg_to_usize_status(expect_int(args, 0)?) {
Ok(id) => id,
Err(status) => {
ret.push_int(status as i64);
return Ok(());
}
};
let status = if hw.bind_scene(scene_bank_id) {
ComposerOpStatus::Ok
} else {
ComposerOpStatus::SceneUnavailable
};
ret.push_int(status as i64);
Ok(())
}
Syscall::ComposerUnbindScene => {
hw.unbind_scene();
ret.push_int(ComposerOpStatus::Ok as i64);
Ok(())
}
Syscall::ComposerSetCamera => {
let x = Self::int_arg_to_i32_trap(expect_int(args, 0)?, "camera x")?;
let y = Self::int_arg_to_i32_trap(expect_int(args, 1)?, "camera y")?;
hw.set_camera(x, y);
Ok(())
}
Syscall::ComposerEmitSprite => {
let glyph_id = match Self::int_arg_to_u16_status(expect_int(args, 0)?) {
Ok(value) => value,
Err(status) => {
ret.push_int(status as i64);
return Ok(());
}
};
let palette_id = match Self::int_arg_to_u8_status(expect_int(args, 1)?) {
Ok(value) if value < 64 => value,
_ => {
ret.push_int(ComposerOpStatus::ArgRangeInvalid as i64);
return Ok(());
}
};
let x = Self::int_arg_to_i32_trap(expect_int(args, 2)?, "sprite x")?;
let y = Self::int_arg_to_i32_trap(expect_int(args, 3)?, "sprite y")?;
let layer = match Self::int_arg_to_u8_status(expect_int(args, 4)?) {
Ok(value) if value < 4 => value,
Ok(_) => {
ret.push_int(ComposerOpStatus::LayerInvalid as i64);
return Ok(());
}
Err(status) => {
ret.push_int(status as i64);
return Ok(());
}
};
let bank_id = match Self::int_arg_to_u8_status(expect_int(args, 5)?) {
Ok(value) => value,
Err(status) => {
ret.push_int(status as i64);
return Ok(());
}
};
let flip_x = expect_bool(args, 6)?;
let flip_y = expect_bool(args, 7)?;
let priority = match Self::int_arg_to_u8_status(expect_int(args, 8)?) {
Ok(value) => value,
Err(status) => {
ret.push_int(status as i64);
return Ok(());
}
};
if !hw.has_glyph_bank(bank_id as usize) {
ret.push_int(ComposerOpStatus::BankInvalid as i64);
return Ok(());
}
let emitted = hw.emit_sprite(Sprite {
glyph: Glyph { glyph_id, palette_id },
x,
y,
layer,
bank_id,
active: false,
flip_x,
flip_y,
priority,
});
let status =
if emitted { ComposerOpStatus::Ok } else { ComposerOpStatus::SpriteOverflow };
ret.push_int(status as i64);
Ok(())
}
Syscall::AudioPlaySample => {
let sample_id_raw = expect_int(args, 0)?;
let voice_id_raw = expect_int(args, 1)?;

View File

@ -7,7 +7,7 @@ use prometeu_bytecode::model::{BytecodeModule, ConstantPoolEntry, FunctionMeta,
use prometeu_drivers::hardware::Hardware;
use prometeu_drivers::{GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolInstaller};
use prometeu_hal::AudioOpStatus;
use prometeu_hal::GfxOpStatus;
use prometeu_hal::ComposerOpStatus;
use prometeu_hal::InputSignals;
use prometeu_hal::asset::{
AssetCodec, AssetEntry, AssetLoadError, AssetOpStatus, BankType, LoadStatus,
@ -298,6 +298,62 @@ fn tick_renders_bound_eight_pixel_scene_through_frame_composer_path() {
assert_eq!(hardware.gfx.front_buffer()[0], Color::BLUE.raw());
}
#[test]
fn tick_renders_scene_through_public_composer_syscalls() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let signals = InputSignals::default();
let code = assemble(
"PUSH_I32 0\nHOSTCALL 0\nPOP_N 1\n\
PUSH_I32 0\nPUSH_I32 0\nHOSTCALL 1\n\
PUSH_I32 0\nPUSH_I32 2\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 2\n\
FRAME_SYNC\nHALT",
)
.expect("assemble");
let program = serialized_single_function_module(
code,
vec![
SyscallDecl {
module: "composer".into(),
name: "bind_scene".into(),
version: 1,
arg_slots: 1,
ret_slots: 1,
},
SyscallDecl {
module: "composer".into(),
name: "set_camera".into(),
version: 1,
arg_slots: 2,
ret_slots: 0,
},
SyscallDecl {
module: "composer".into(),
name: "emit_sprite".into(),
version: 1,
arg_slots: 9,
ret_slots: 1,
},
],
);
let cartridge = cartridge_with_program(program, caps::GFX);
let banks = Arc::new(MemoryBanks::new());
banks.install_glyph_bank(0, Arc::new(runtime_test_glyph_bank(TileSize::Size8, 2, Color::BLUE)));
banks.install_scene_bank(0, Arc::new(runtime_test_scene(0, 2, TileSize::Size8)));
let mut hardware = Hardware::new_with_memory_banks(Arc::clone(&banks));
hardware.gfx.scene_fade_level = 31;
hardware.gfx.hud_fade_level = 31;
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
assert!(report.is_none(), "public composer path must not crash");
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(ComposerOpStatus::Ok as i64)]);
hardware.gfx.present();
assert_eq!(hardware.gfx.front_buffer()[0], Color::BLUE.raw());
}
#[test]
fn initialize_vm_success_clears_previous_crash_report() {
let mut runtime = VirtualMachineRuntime::new(None);
@ -429,22 +485,19 @@ fn initialize_vm_failure_clears_previous_identity_and_handles() {
}
#[test]
fn tick_gfx_set_sprite_operational_error_returns_status_not_crash() {
fn tick_composer_bind_scene_operational_error_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 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\nHALT",
)
.expect("assemble");
let code = assemble("PUSH_I32 99\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "gfx".into(),
name: "set_sprite".into(),
module: "composer".into(),
name: "bind_scene".into(),
version: 1,
arg_slots: 10,
arg_slots: 1,
ret_slots: 1,
}],
);
@ -454,26 +507,29 @@ 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(GfxOpStatus::BankInvalid as i64)]);
assert_eq!(
vm.operand_stack_top(1),
vec![Value::Int64(ComposerOpStatus::SceneUnavailable as i64)]
);
}
#[test]
fn tick_gfx_set_sprite_invalid_index_returns_status_not_crash() {
fn tick_composer_emit_sprite_operational_error_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 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",
"PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT",
)
.expect("assemble");
let program = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "gfx".into(),
name: "set_sprite".into(),
module: "composer".into(),
name: "emit_sprite".into(),
version: 1,
arg_slots: 10,
arg_slots: 9,
ret_slots: 1,
}],
);
@ -481,28 +537,57 @@ fn tick_gfx_set_sprite_invalid_index_returns_status_not_crash() {
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!(report.is_none(), "operational error must not crash");
assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(GfxOpStatus::InvalidSpriteIndex as i64)]);
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(ComposerOpStatus::BankInvalid as i64)]);
}
#[test]
fn tick_gfx_set_sprite_invalid_range_returns_status_not_crash() {
fn tick_composer_emit_sprite_invalid_layer_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 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",
"PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 4\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT",
)
.expect("assemble");
let program = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "gfx".into(),
name: "set_sprite".into(),
module: "composer".into(),
name: "emit_sprite".into(),
version: 1,
arg_slots: 10,
arg_slots: 9,
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 layer must not crash");
assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(ComposerOpStatus::LayerInvalid as i64)]);
}
#[test]
fn tick_composer_emit_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_I32 0\nPUSH_I32 64\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT",
)
.expect("assemble");
let program = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "composer".into(),
name: "emit_sprite".into(),
version: 1,
arg_slots: 9,
ret_slots: 1,
}],
);
@ -517,9 +602,12 @@ fn tick_gfx_set_sprite_invalid_range_returns_status_not_crash() {
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!(report.is_none(), "invalid composer parameter range must not crash");
assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(GfxOpStatus::ArgRangeInvalid as i64)]);
assert_eq!(
vm.operand_stack_top(1),
vec![Value::Int64(ComposerOpStatus::ArgRangeInvalid as i64)]
);
}
#[test]
@ -881,13 +969,13 @@ fn tick_asset_cancel_invalid_transition_returns_status_not_crash() {
}
#[test]
fn tick_status_first_surface_smoke_across_gfx_audio_and_asset() {
fn tick_status_first_surface_smoke_across_composer_audio_and_asset() {
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 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 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 999\nPUSH_I32 0\nHOSTCALL 2\n\
HALT"
@ -897,10 +985,10 @@ fn tick_status_first_surface_smoke_across_gfx_audio_and_asset() {
code,
vec![
SyscallDecl {
module: "gfx".into(),
name: "set_sprite".into(),
module: "composer".into(),
name: "emit_sprite".into(),
version: 1,
arg_slots: 10,
arg_slots: 9,
ret_slots: 1,
},
SyscallDecl {
@ -931,28 +1019,28 @@ fn tick_status_first_surface_smoke_across_gfx_audio_and_asset() {
Value::Int64(0),
Value::Int64(AssetLoadError::AssetNotFound as i64),
Value::Int64(AudioOpStatus::BankInvalid as i64),
Value::Int64(GfxOpStatus::BankInvalid as i64),
Value::Int64(ComposerOpStatus::BankInvalid as i64),
]
);
}
#[test]
fn tick_gfx_set_sprite_type_mismatch_surfaces_trap_not_panic() {
fn tick_composer_emit_sprite_type_mismatch_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_BOOL 1\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\nHALT",
"PUSH_BOOL 1\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT",
)
.expect("assemble");
let program = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "gfx".into(),
name: "set_sprite".into(),
module: "composer".into(),
name: "emit_sprite".into(),
version: 1,
arg_slots: 10,
arg_slots: 9,
ret_slots: 1,
}],
);