From cc700c6cf8821101a7e90911c3e48b28ace9fd98 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 17 Apr 2026 17:55:04 +0100 Subject: [PATCH] implements PLN-0023 --- .../console/prometeu-drivers/src/hardware.rs | 20 ++- .../prometeu-hal/src/hardware_bridge.rs | 6 +- .../src/virtual_machine_runtime/dispatch.rs | 123 +++++++++++++- .../src/virtual_machine_runtime/tests.rs | 158 ++++++++++++++---- 4 files changed, 261 insertions(+), 46 deletions(-) diff --git a/crates/console/prometeu-drivers/src/hardware.rs b/crates/console/prometeu-drivers/src/hardware.rs index c9de3447..d648356b 100644 --- a/crates/console/prometeu-drivers/src/hardware.rs +++ b/crates/console/prometeu-drivers/src/hardware.rs @@ -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 } diff --git a/crates/console/prometeu-hal/src/hardware_bridge.rs b/crates/console/prometeu-hal/src/hardware_bridge.rs index 2bcb3376..28ccb77f 100644 --- a/crates/console/prometeu-hal/src/hardware_bridge.rs +++ b/crates/console/prometeu-hal/src/hardware_bridge.rs @@ -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; 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 95a00ecb..2a0fb39b 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs @@ -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::try_from(value).map_err(|_| ComposerOpStatus::ArgRangeInvalid) + } + + fn int_arg_to_i32_trap(value: i64, name: &str) -> Result { + 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::try_from(value).map_err(|_| ComposerOpStatus::ArgRangeInvalid) + } + + fn int_arg_to_u16_status(value: i64) -> Result { + 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)?; 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 53df08e8..68780ef9 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs @@ -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, }], );