Compare commits

...

10 Commits

Author SHA1 Message Date
9cf9fa6163
implements PLN-0109
All checks were successful
Intrepid/Prometeu/Runtime/pipeline/head This commit looks good
Intrepid/Prometeu/Runtime/pipeline/pr-master This commit looks good
2026-06-15 10:47:34 +01:00
117a18ee72
implements PLN-0108 2026-06-15 06:04:29 +01:00
f45b3a38cb
implements PLN-0107 2026-06-15 06:01:49 +01:00
5381ae02b1
implements PLN-0106 2026-06-15 06:00:39 +01:00
1d6b799960
implements PLN-0105 2026-06-15 05:59:06 +01:00
d0a2682366
implements PLN-0104 2026-06-15 05:56:15 +01:00
c62aef51ba
implements PLN-0103 2026-06-15 05:52:48 +01:00
d2bace2612
implements PLN-0102 2026-06-15 05:51:07 +01:00
6f3df74e79
implements PLN-0101 2026-06-15 05:48:24 +01:00
18cfeedbff
implements PLN-0100 2026-06-15 05:46:28 +01:00
31 changed files with 757 additions and 415 deletions

View File

@ -8,8 +8,8 @@ use prometeu_hal::scene_viewport_resolver::{
};
use prometeu_hal::sprite::Sprite;
use prometeu_hal::{
BoundScenePacket, Camera2D, ComposerFramePacket, Game2DFramePacket, GameSpritePacket,
GfxBridge, HudPacket,
BoundScenePacket, Camera2D, ComposerFramePacket, ComposerOpStatus, Game2DFrameComposer,
Game2DFramePacket, GameSpritePacket, GfxBridge, HudPacket,
};
use std::sync::Arc;
@ -452,6 +452,41 @@ impl FrameComposer {
}
}
impl Game2DFrameComposer for FrameComposer {
fn begin_frame(&mut self) {
FrameComposer::begin_frame(self);
}
fn bind_scene(&mut self, scene_bank_id: usize) -> ComposerOpStatus {
if FrameComposer::bind_scene(self, scene_bank_id) {
ComposerOpStatus::Ok
} else {
ComposerOpStatus::SceneUnavailable
}
}
fn unbind_scene(&mut self) {
FrameComposer::unbind_scene(self);
}
fn set_camera(&mut self, x: i32, y: i32) -> ComposerOpStatus {
FrameComposer::set_camera(self, x, y);
ComposerOpStatus::Ok
}
fn emit_sprite(&mut self, sprite: Sprite) -> ComposerOpStatus {
if FrameComposer::emit_sprite(self, sprite) {
ComposerOpStatus::Ok
} else {
ComposerOpStatus::SpriteOverflow
}
}
fn close_game2d_packet(&self) -> Game2DFramePacket {
FrameComposer::close_game2d_packet(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -896,6 +931,45 @@ mod tests {
assert!(packet.gfx2d.is_empty());
}
#[test]
fn game2d_frame_composer_facade_closes_equivalent_packet() {
let mut frame_composer = FrameComposer::new(
Hardware::W,
Hardware::H,
Arc::new(MemoryBanks::default()),
make_glyph_slot_index(&[]),
);
let composer: &mut dyn Game2DFrameComposer = &mut frame_composer;
assert_eq!(composer.set_camera(10, 20), ComposerOpStatus::Ok);
composer.begin_frame();
assert_eq!(
composer.emit_sprite(Sprite {
glyph: Glyph { glyph_id: 7, palette_id: 3 },
x: 4,
y: 5,
layer: 2,
bank_id: 1,
active: false,
flip_x: true,
flip_y: false,
priority: 9,
}),
ComposerOpStatus::Ok
);
let packet = composer.close_game2d_packet();
assert_eq!(packet.composer.bound_scene, None);
assert_eq!(packet.composer.camera, Camera2D { x: 10, y: 20 });
assert_eq!(packet.composer.sprites.len(), 1);
assert_eq!(packet.composer.sprites[0].glyph_id, 7);
assert_eq!(packet.composer.sprites[0].palette_id, 3);
assert_eq!(packet.composer.sprites[0].layer, 2);
assert!(packet.composer.sprites[0].flip_x);
assert!(packet.gfx2d.is_empty());
}
#[test]
fn composer_buffer_preserves_integer_layer_ordering() {
let mut frame_composer = FrameComposer::new(

View File

@ -10,8 +10,13 @@ use crate::pad::Pad;
use crate::touch::Touch;
use prometeu_hal::cartridge::AssetsPayloadSource;
use prometeu_hal::sprite::Sprite;
use prometeu_hal::{AssetBridge, AudioBridge, GfxBridge, HardwareBridge, PadBridge, TouchBridge};
use prometeu_hal::{Game2DFramePacket, RenderSubmission, RenderSubmissionPacket};
use prometeu_hal::{
AssetBridge, AudioBridge, InputPlatform, NoopTelemetryPlatform, PadBridge, RenderBackend,
RuntimePlatform, TelemetryPlatform, TouchBridge,
};
use prometeu_hal::{
Game2DFrameComposer, Game2DFramePacket, RenderSubmission, RenderSubmissionPacket,
};
use prometeu_hal::{RenderSubmissionSink, RenderSubmitError};
use std::sync::Arc;
@ -48,32 +53,8 @@ impl Default for Hardware {
}
}
impl HardwareBridge for Hardware {
fn begin_frame(&mut self) {
self.frame_composer.begin_frame();
}
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 close_game2d_packet(&self) -> Game2DFramePacket {
self.frame_composer.close_game2d_packet()
}
fn publish_render_submission(&mut self, submission: &RenderSubmission) {
impl Hardware {
fn consume_render_submission(&mut self, submission: &RenderSubmission) {
match &submission.packet {
RenderSubmissionPacket::Game2D(packet) => {
if packet.composer.bound_scene.is_none() {
@ -89,7 +70,19 @@ impl HardwareBridge for Hardware {
}
self.gfx.present();
}
}
impl RenderSubmissionSink for Hardware {
fn submit_render_submission(
&mut self,
submission: RenderSubmission,
) -> Result<(), RenderSubmitError> {
self.consume_render_submission(&submission);
Ok(())
}
}
impl RenderBackend for Hardware {
fn render_frame(&mut self) {
if self.frame_composer.active_scene_id().is_none() {
let packet = self.frame_composer.close_game2d_packet();
@ -98,28 +91,52 @@ impl HardwareBridge for Hardware {
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()
impl Game2DFrameComposer for Hardware {
fn begin_frame(&mut self) {
self.frame_composer.begin_frame();
}
fn gfx(&self) -> &dyn GfxBridge {
&self.gfx
}
fn gfx_mut(&mut self) -> &mut dyn GfxBridge {
&mut self.gfx
fn bind_scene(&mut self, scene_bank_id: usize) -> prometeu_hal::ComposerOpStatus {
if self.frame_composer.bind_scene(scene_bank_id) {
prometeu_hal::ComposerOpStatus::Ok
} else {
prometeu_hal::ComposerOpStatus::SceneUnavailable
}
}
fn audio(&self) -> &dyn AudioBridge {
&self.audio
}
fn audio_mut(&mut self) -> &mut dyn AudioBridge {
&mut self.audio
fn unbind_scene(&mut self) {
self.frame_composer.unbind_scene();
}
fn set_camera(&mut self, x: i32, y: i32) -> prometeu_hal::ComposerOpStatus {
self.frame_composer.set_camera(x, y);
prometeu_hal::ComposerOpStatus::Ok
}
fn emit_sprite(&mut self, sprite: Sprite) -> prometeu_hal::ComposerOpStatus {
if self.gfx.glyph_banks.glyph_bank_slot(sprite.bank_id as usize).is_none() {
return prometeu_hal::ComposerOpStatus::BankInvalid;
}
if self.frame_composer.emit_sprite(sprite) {
prometeu_hal::ComposerOpStatus::Ok
} else {
prometeu_hal::ComposerOpStatus::SpriteOverflow
}
}
fn close_game2d_packet(&self) -> Game2DFramePacket {
self.frame_composer.close_game2d_packet()
}
}
impl InputPlatform for Hardware {
fn pad(&self) -> &dyn PadBridge {
&self.pad
}
fn pad_mut(&mut self) -> &mut dyn PadBridge {
&mut self.pad
}
@ -127,25 +144,53 @@ impl HardwareBridge for Hardware {
fn touch(&self) -> &dyn TouchBridge {
&self.touch
}
fn touch_mut(&mut self) -> &mut dyn TouchBridge {
&mut self.touch
}
}
static NOOP_TELEMETRY: NoopTelemetryPlatform = NoopTelemetryPlatform;
impl RuntimePlatform for Hardware {
fn render_submission_sink(&mut self) -> &mut dyn RenderSubmissionSink {
self
}
fn render_backend(&mut self) -> &mut dyn RenderBackend {
self
}
fn game2d_frame_composer(&mut self) -> &mut dyn Game2DFrameComposer {
self
}
fn audio(&self) -> &dyn AudioBridge {
&self.audio
}
fn audio_mut(&mut self) -> &mut dyn AudioBridge {
&mut self.audio
}
fn input(&self) -> &dyn InputPlatform {
self
}
fn input_mut(&mut self) -> &mut dyn InputPlatform {
self
}
fn assets(&self) -> &dyn AssetBridge {
&self.assets
}
fn assets_mut(&mut self) -> &mut dyn AssetBridge {
&mut self.assets
}
}
impl RenderSubmissionSink for Hardware {
fn submit_render_submission(
&mut self,
submission: RenderSubmission,
) -> Result<(), RenderSubmitError> {
self.publish_render_submission(&submission);
Ok(())
fn telemetry(&self) -> &dyn TelemetryPlatform {
&NOOP_TELEMETRY
}
}
@ -294,15 +339,22 @@ mod tests {
let mut glyph_slots = [None; 16];
glyph_slots[0] = Some(0);
hardware.assets.glyph_asset_slot_index().rebuild_from_slots(&glyph_slots);
assert!(hardware.bind_scene(0));
assert_eq!(
Game2DFrameComposer::bind_scene(&mut hardware, 0),
prometeu_hal::ComposerOpStatus::Ok
);
let mut packet = hardware.close_game2d_packet();
let mut packet = Game2DFrameComposer::close_game2d_packet(&hardware);
packet.gfx2d = vec![Gfx2dCommand::FillRect {
rect: Rect { x: 0, y: 0, w: 1, h: 1 },
color: Color::BLUE,
}];
hardware.publish_render_submission(&RenderSubmission::game2d(FrameId::ZERO, packet));
RenderSubmissionSink::submit_render_submission(
&mut hardware,
RenderSubmission::game2d(FrameId::ZERO, packet),
)
.expect("local sink should publish Game2D packet");
assert_eq!(hardware.gfx.front_buffer()[0], Color::BLUE.raw());
}
@ -317,7 +369,10 @@ mod tests {
let mut glyph_slots = [None; 16];
glyph_slots[0] = Some(0);
hardware.assets.glyph_asset_slot_index().rebuild_from_slots(&glyph_slots);
assert!(hardware.bind_scene(0));
assert_eq!(
Game2DFrameComposer::bind_scene(&mut hardware, 0),
prometeu_hal::ComposerOpStatus::Ok
);
let packet = Game2DFramePacket::new(
ComposerFramePacket {
@ -339,8 +394,12 @@ mod tests {
Vec::new(),
);
hardware.begin_frame();
hardware.publish_render_submission(&RenderSubmission::game2d(FrameId::ZERO, packet));
Game2DFrameComposer::begin_frame(&mut hardware);
RenderSubmissionSink::submit_render_submission(
&mut hardware,
RenderSubmission::game2d(FrameId::ZERO, packet),
)
.expect("local sink should publish Game2D packet");
assert_eq!(hardware.gfx.front_buffer()[0], Color::RED.raw());
}

View File

@ -5,6 +5,7 @@ mod gfx;
pub mod hardware;
mod memory_banks;
mod pad;
mod test_platform;
mod touch;
pub use crate::asset::AssetManager;
@ -16,3 +17,4 @@ pub use crate::memory_banks::{
SceneBankPoolAccess, SceneBankPoolInstaller, SoundBankPoolAccess, SoundBankPoolInstaller,
};
pub use crate::pad::Pad;
pub use crate::test_platform::TestPlatform;

View File

@ -0,0 +1,100 @@
use crate::hardware::Hardware;
use crate::memory_banks::MemoryBanks;
use prometeu_hal::{
AssetBridge, AudioBridge, Game2DFrameComposer, InputPlatform, RenderBackend,
RenderSubmissionSink, RuntimePlatform, TelemetryPlatform,
};
use std::sync::Arc;
pub struct TestPlatform {
hardware: Hardware,
}
impl Default for TestPlatform {
fn default() -> Self {
Self::new()
}
}
impl TestPlatform {
pub fn new() -> Self {
Self { hardware: Hardware::new() }
}
pub fn new_with_memory_banks(memory_banks: Arc<MemoryBanks>) -> Self {
Self { hardware: Hardware::new_with_memory_banks(memory_banks) }
}
pub fn local_hardware(&self) -> &Hardware {
&self.hardware
}
pub fn local_hardware_mut(&mut self) -> &mut Hardware {
&mut self.hardware
}
}
impl RuntimePlatform for TestPlatform {
fn render_submission_sink(&mut self) -> &mut dyn RenderSubmissionSink {
self.hardware.render_submission_sink()
}
fn render_backend(&mut self) -> &mut dyn RenderBackend {
self.hardware.render_backend()
}
fn game2d_frame_composer(&mut self) -> &mut dyn Game2DFrameComposer {
self.hardware.game2d_frame_composer()
}
fn audio(&self) -> &dyn AudioBridge {
self.hardware.audio()
}
fn audio_mut(&mut self) -> &mut dyn AudioBridge {
self.hardware.audio_mut()
}
fn input(&self) -> &dyn InputPlatform {
self.hardware.input()
}
fn input_mut(&mut self) -> &mut dyn InputPlatform {
self.hardware.input_mut()
}
fn assets(&self) -> &dyn AssetBridge {
self.hardware.assets()
}
fn assets_mut(&mut self) -> &mut dyn AssetBridge {
self.hardware.assets_mut()
}
fn telemetry(&self) -> &dyn TelemetryPlatform {
self.hardware.telemetry()
}
}
#[cfg(test)]
mod tests {
use super::*;
use prometeu_hal::{FrameId, Game2DFramePacket, RenderSubmission};
#[test]
fn test_platform_initializes_required_facades() {
let mut platform = TestPlatform::new();
platform.game2d_frame_composer().begin_frame();
platform
.render_submission_sink()
.submit_render_submission(RenderSubmission::game2d(
FrameId::ZERO,
Game2DFramePacket::default(),
))
.expect("test platform render sink should accept local submission");
assert!(!platform.audio().is_playing(0));
assert!(!platform.input().pad().any());
assert!(platform.telemetry().telemetry_snapshot().is_none());
}
}

View File

@ -3,7 +3,7 @@ use crate::firmware::firmware_state::{FirmwareState, LoadCartridgeStep, ResetSte
use crate::firmware::prometeu_context::PrometeuContext;
use prometeu_hal::cartridge::Cartridge;
use prometeu_hal::telemetry::CertificationConfig;
use prometeu_hal::{HardwareBridge, InputSignals};
use prometeu_hal::{InputSignals, RuntimePlatform};
use prometeu_system::{PrometeuHub, SystemOS};
use prometeu_vm::VirtualMachine;
@ -54,24 +54,24 @@ impl Firmware {
///
/// This method is called exactly once per Host frame (60Hz).
/// It updates peripheral signals and delegates the logic to the current state.
pub fn tick(&mut self, signals: &InputSignals, hw: &mut dyn HardwareBridge) {
pub fn tick(&mut self, signals: &InputSignals, platform: &mut dyn RuntimePlatform) {
// 0. Process asset commits at the beginning of the frame boundary.
hw.assets_mut().apply_commits();
platform.assets_mut().apply_commits();
// 1. Update the peripheral state using the latest signals from the Host.
// This ensures input is consistent throughout the entire update.
hw.pad_mut().begin_frame(signals);
hw.touch_mut().begin_frame(signals);
platform.input_mut().pad_mut().begin_frame(signals);
platform.input_mut().touch_mut().begin_frame(signals);
// 2. State machine lifecycle management.
if !self.state_initialized {
self.on_enter(signals, hw);
self.on_enter(signals, platform);
self.state_initialized = true;
}
// 3. Update the current state and check for transitions.
if let Some(next_state) = self.on_update(signals, hw) {
self.change_state(next_state, signals, hw);
if let Some(next_state) = self.on_update(signals, platform) {
self.change_state(next_state, signals, platform);
}
}
@ -80,26 +80,26 @@ impl Firmware {
&mut self,
new_state: FirmwareState,
signals: &InputSignals,
hw: &mut dyn HardwareBridge,
platform: &mut dyn RuntimePlatform,
) {
self.on_exit(signals, hw);
self.on_exit(signals, platform);
self.state = new_state;
self.state_initialized = false;
// Enter the new state immediately to avoid "empty" frames during transitions.
self.on_enter(signals, hw);
self.on_enter(signals, platform);
self.state_initialized = true;
}
/// Dispatches the `on_enter` event to the current state implementation.
fn on_enter(&mut self, signals: &InputSignals, hw: &mut dyn HardwareBridge) {
fn on_enter(&mut self, signals: &InputSignals, platform: &mut dyn RuntimePlatform) {
let mut req = PrometeuContext {
vm: &mut self.vm,
os: &mut self.os,
hub: &mut self.hub,
boot_target: &self.boot_target,
signals,
hw,
platform,
};
match &mut self.state {
FirmwareState::Reset(s) => s.on_enter(&mut req),
@ -118,7 +118,7 @@ impl Firmware {
fn on_update(
&mut self,
signals: &InputSignals,
hw: &mut dyn HardwareBridge,
platform: &mut dyn RuntimePlatform,
) -> Option<FirmwareState> {
let mut req = PrometeuContext {
vm: &mut self.vm,
@ -126,7 +126,7 @@ impl Firmware {
hub: &mut self.hub,
boot_target: &self.boot_target,
signals,
hw,
platform,
};
match &mut self.state {
FirmwareState::Reset(s) => s.on_update(&mut req),
@ -141,14 +141,14 @@ impl Firmware {
}
/// Dispatches the `on_exit` event to the current state implementation.
fn on_exit(&mut self, signals: &InputSignals, hw: &mut dyn HardwareBridge) {
fn on_exit(&mut self, signals: &InputSignals, platform: &mut dyn RuntimePlatform) {
let mut req = PrometeuContext {
vm: &mut self.vm,
os: &mut self.os,
hub: &mut self.hub,
boot_target: &self.boot_target,
signals,
hw,
platform,
};
match &mut self.state {
FirmwareState::Reset(s) => s.on_exit(&mut req),
@ -174,7 +174,7 @@ mod tests {
use crate::firmware::firmware_state::HubHomeStep;
use prometeu_bytecode::assembler::assemble;
use prometeu_bytecode::model::{BytecodeModule, FunctionMeta, SyscallDecl};
use prometeu_drivers::hardware::Hardware;
use prometeu_drivers::TestPlatform;
use prometeu_hal::app_mode::AppMode;
use prometeu_hal::cartridge::AssetsPayloadSource;
use prometeu_hal::color::Color;
@ -267,28 +267,28 @@ mod tests {
}
}
fn load_shell_running_firmware() -> (Firmware, Hardware, InputSignals, TaskId) {
fn load_shell_running_firmware() -> (Firmware, TestPlatform, InputSignals, TaskId) {
let mut firmware = Firmware::new(None);
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
firmware.load_cartridge(valid_cartridge(AppMode::Shell));
firmware.tick(&signals, &mut hardware);
firmware.tick(&signals, &mut platform);
let task_id = match &firmware.state {
FirmwareState::ShellRunning(step) => step.task_id,
other => panic!("expected ShellRunning state, got {:?}", other),
};
(firmware, hardware, signals, task_id)
(firmware, platform, signals, task_id)
}
fn hub_home_firmware() -> (Firmware, Hardware) {
fn hub_home_firmware() -> (Firmware, TestPlatform) {
let mut firmware = Firmware::new(None);
firmware.state = FirmwareState::HubHome(HubHomeStep);
firmware.state_initialized = false;
(firmware, Hardware::new())
(firmware, TestPlatform::new())
}
fn focus_window(firmware: &mut Firmware, owner: WindowOwner) {
@ -306,11 +306,11 @@ mod tests {
#[test]
fn load_cartridge_transitions_to_app_crashes_when_vm_init_fails() {
let mut firmware = Firmware::new(None);
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
firmware.load_cartridge(invalid_game_cartridge());
firmware.tick(&signals, &mut hardware);
firmware.tick(&signals, &mut platform);
match &firmware.state {
FirmwareState::AppCrashes(step) => match &step.report {
@ -326,18 +326,18 @@ mod tests {
#[test]
fn game_running_transitions_to_app_crashes_when_runtime_surfaces_trap() {
let mut firmware = Firmware::new(None);
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
firmware.load_cartridge(trapping_game_cartridge());
firmware.tick(&signals, &mut hardware);
firmware.tick(&signals, &mut platform);
let task_id = match &firmware.state {
FirmwareState::GameRunning(step) => step.task_id,
other => panic!("expected GameRunning state, got {:?}", other),
};
firmware.tick(&signals, &mut hardware);
firmware.tick(&signals, &mut platform);
match &firmware.state {
FirmwareState::AppCrashes(step) => match &step.report {
@ -359,11 +359,11 @@ mod tests {
#[test]
fn reset_routes_hub_boot_target_to_splash_screen() {
let mut firmware = Firmware::new(None);
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
firmware.boot_target = BootTarget::Hub;
firmware.tick(&signals, &mut hardware);
firmware.tick(&signals, &mut platform);
assert!(matches!(firmware.state, FirmwareState::SplashScreen(_)));
}
@ -371,12 +371,12 @@ mod tests {
#[test]
fn reset_routes_cartridge_boot_target_to_launch_hub() {
let mut firmware = Firmware::new(None);
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
firmware.boot_target =
BootTarget::Cartridge { path: "missing-cart".into(), debug: false, debug_port: 7777 };
firmware.tick(&signals, &mut hardware);
firmware.tick(&signals, &mut platform);
assert!(matches!(firmware.state, FirmwareState::LaunchHub(_)));
}
@ -384,11 +384,11 @@ mod tests {
#[test]
fn load_cartridge_routes_system_apps_to_system_running() {
let mut firmware = Firmware::new(None);
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
firmware.load_cartridge(valid_cartridge(AppMode::Shell));
firmware.tick(&signals, &mut hardware);
firmware.tick(&signals, &mut platform);
assert!(matches!(firmware.state, FirmwareState::ShellRunning(_)));
assert!(firmware.os.windows().focused_window().is_some());
@ -398,18 +398,18 @@ mod tests {
#[test]
fn system_running_ticks_focused_system_app_through_hub_pipeline() {
let mut firmware = Firmware::new(None);
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
firmware.load_cartridge(valid_cartridge(AppMode::Shell));
firmware.tick(&signals, &mut hardware);
firmware.tick(&signals, &mut platform);
let task_id = match &firmware.state {
FirmwareState::ShellRunning(step) => step.task_id,
other => panic!("expected ShellRunning state, got {:?}", other),
};
let tick_index_before_system_update = firmware.os.vm().tick_index();
firmware.tick(&signals, &mut hardware);
firmware.tick(&signals, &mut platform);
assert!(matches!(firmware.state, FirmwareState::ShellRunning(_)));
assert!(firmware.os.windows().focused_window_belongs_to_task(task_id));
@ -418,10 +418,10 @@ mod tests {
#[test]
fn hub_home_does_not_launch_shell_before_home_input_is_armed() {
let (mut firmware, mut hardware) = hub_home_firmware();
let (mut firmware, mut platform) = hub_home_firmware();
let signals = InputSignals { f_signal: true, x_pos: 112, y_pos: 108, ..Default::default() };
firmware.tick(&signals, &mut hardware);
firmware.tick(&signals, &mut platform);
assert!(matches!(firmware.state, FirmwareState::HubHome(_)));
assert_eq!(firmware.os.windows().window_count(), 0);
@ -429,11 +429,11 @@ mod tests {
#[test]
fn hub_home_click_launches_shell_a_as_native_shell_task_window() {
let (mut firmware, mut hardware) = hub_home_firmware();
firmware.tick(&InputSignals::default(), &mut hardware);
let (mut firmware, mut platform) = hub_home_firmware();
firmware.tick(&InputSignals::default(), &mut platform);
let signals = InputSignals { f_signal: true, x_pos: 112, y_pos: 108, ..Default::default() };
firmware.tick(&signals, &mut hardware);
firmware.tick(&signals, &mut platform);
let task_id = match &firmware.state {
FirmwareState::ShellRunning(step) => step.task_id,
@ -452,11 +452,11 @@ mod tests {
#[test]
fn hub_home_click_launches_shell_b_as_native_shell_task_window() {
let (mut firmware, mut hardware) = hub_home_firmware();
firmware.tick(&InputSignals::default(), &mut hardware);
let (mut firmware, mut platform) = hub_home_firmware();
firmware.tick(&InputSignals::default(), &mut platform);
let signals = InputSignals { f_signal: true, x_pos: 256, y_pos: 108, ..Default::default() };
firmware.tick(&signals, &mut hardware);
firmware.tick(&signals, &mut platform);
let task_id = match &firmware.state {
FirmwareState::ShellRunning(step) => step.task_id,
@ -475,10 +475,10 @@ mod tests {
#[test]
fn shell_running_start_close_uses_lifecycle_and_returns_hub_home() {
let (mut firmware, mut hardware, _signals, task_id) = load_shell_running_firmware();
let (mut firmware, mut platform, _signals, task_id) = load_shell_running_firmware();
let close_signals = InputSignals { start_signal: true, ..Default::default() };
firmware.tick(&close_signals, &mut hardware);
firmware.tick(&close_signals, &mut platform);
assert!(matches!(firmware.state, FirmwareState::HubHome(_)));
assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Closed));
@ -490,11 +490,11 @@ mod tests {
#[test]
fn shell_running_close_click_uses_lifecycle_and_returns_hub_home() {
let (mut firmware, mut hardware, _signals, task_id) = load_shell_running_firmware();
let (mut firmware, mut platform, _signals, task_id) = load_shell_running_firmware();
let close_signals =
InputSignals { f_signal: true, x_pos: 420, y_pos: 24, ..Default::default() };
firmware.tick(&close_signals, &mut hardware);
firmware.tick(&close_signals, &mut platform);
assert!(matches!(firmware.state, FirmwareState::HubHome(_)));
assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Closed));
@ -506,10 +506,10 @@ mod tests {
#[test]
fn shell_running_closes_task_and_returns_hub_home_without_focused_window() {
let (mut firmware, mut hardware, signals, task_id) = load_shell_running_firmware();
let (mut firmware, mut platform, signals, task_id) = load_shell_running_firmware();
firmware.os.windows().remove_all_windows();
firmware.tick(&signals, &mut hardware);
firmware.tick(&signals, &mut platform);
assert!(matches!(firmware.state, FirmwareState::HubHome(_)));
assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Closed));
@ -521,10 +521,10 @@ mod tests {
#[test]
fn shell_running_closes_task_and_returns_hub_home_for_focused_hub_window() {
let (mut firmware, mut hardware, signals, task_id) = load_shell_running_firmware();
let (mut firmware, mut platform, signals, task_id) = load_shell_running_firmware();
focus_window(&mut firmware, WindowOwner::Hub);
firmware.tick(&signals, &mut hardware);
firmware.tick(&signals, &mut platform);
assert!(matches!(firmware.state, FirmwareState::HubHome(_)));
assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Closed));
@ -536,10 +536,10 @@ mod tests {
#[test]
fn shell_running_closes_task_and_returns_hub_home_for_focused_overlay_window() {
let (mut firmware, mut hardware, signals, task_id) = load_shell_running_firmware();
let (mut firmware, mut platform, signals, task_id) = load_shell_running_firmware();
focus_window(&mut firmware, WindowOwner::Overlay);
firmware.tick(&signals, &mut hardware);
firmware.tick(&signals, &mut platform);
assert!(matches!(firmware.state, FirmwareState::HubHome(_)));
assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Closed));
@ -551,10 +551,10 @@ mod tests {
#[test]
fn shell_running_closes_task_and_returns_hub_home_for_other_task_window() {
let (mut firmware, mut hardware, signals, task_id) = load_shell_running_firmware();
let (mut firmware, mut platform, signals, task_id) = load_shell_running_firmware();
focus_window(&mut firmware, WindowOwner::Task(TaskId(task_id.0 + 1)));
firmware.tick(&signals, &mut hardware);
firmware.tick(&signals, &mut platform);
assert!(matches!(firmware.state, FirmwareState::HubHome(_)));
assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Closed));
@ -567,11 +567,11 @@ mod tests {
#[test]
fn load_cartridge_routes_game_apps_to_game_running() {
let mut firmware = Firmware::new(None);
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
firmware.load_cartridge(valid_cartridge(AppMode::Game));
firmware.tick(&signals, &mut hardware);
firmware.tick(&signals, &mut platform);
assert!(matches!(firmware.state, FirmwareState::GameRunning(_)));
assert_eq!(firmware.os.windows().window_count(), 0);

View File

@ -21,13 +21,16 @@ impl AppCrashesStep {
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
// Update peripherals for input on the crash screen
ctx.hw.pad_mut().begin_frame(ctx.signals);
ctx.platform.input_mut().pad_mut().begin_frame(ctx.signals);
let packet = ShellUiFramePacket::new(vec![GfxUiCommand::Clear { color: Color::RED }]);
ctx.hw.publish_render_submission(&RenderSubmission::shell_ui(FrameId::ZERO, packet));
ctx.platform
.render_submission_sink()
.submit_render_submission(RenderSubmission::shell_ui(FrameId::ZERO, packet))
.expect("firmware crash render submission should publish");
// If START is pressed, return to the Hub
if ctx.hw.pad().start().down {
if ctx.platform.input().pad().start().down {
return Some(FirmwareState::LaunchHub(LaunchHubStep));
}

View File

@ -36,7 +36,7 @@ impl GameRunningStep {
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
}
let result = ctx.os.vm().tick(ctx.vm, ctx.signals, ctx.hw);
let result = ctx.os.vm().tick(ctx.vm, ctx.signals, ctx.platform);
if let Some(report) = result {
let _ = ctx.os.lifecycle().crash_task(self.task_id, Some(&report));

View File

@ -16,7 +16,7 @@ impl HubHomeStep {
}
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
let outcome = ctx.hub.update_shell_profile(ctx.os, ctx.vm, ctx.signals, ctx.hw);
let outcome = ctx.hub.update_shell_profile(ctx.os, ctx.vm, ctx.signals, ctx.platform);
if let Some(report) = outcome.crash {
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
}

View File

@ -30,7 +30,7 @@ impl LoadCartridgeStep {
);
// Initialize Asset Manager
ctx.hw.assets_mut().initialize_for_cartridge(
ctx.platform.assets_mut().initialize_for_cartridge(
self.cartridge.asset_table.clone(),
self.cartridge.preload.clone(),
self.cartridge.assets.clone(),

View File

@ -59,7 +59,7 @@ impl ShellRunningStep {
}
}
let outcome = ctx.hub.update_shell_profile(ctx.os, ctx.vm, ctx.signals, ctx.hw);
let outcome = ctx.hub.update_shell_profile(ctx.os, ctx.vm, ctx.signals, ctx.platform);
if let Some(report) = outcome.crash {
let _ = ctx.os.lifecycle().crash_task(self.task_id, Some(&report));

View File

@ -14,7 +14,7 @@ impl SplashScreenStep {
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) {
ctx.os.log(LogLevel::Info, LogSource::Pos, 0, "Showing SplashScreen".to_string());
// Play sound on enter
// ctx.hw.audio_mut().play(0, 0, 0, 255, 127, 1.0, 0, LoopMode::Off);
// ctx.platform.audio_mut().play(0, 0, 0, 255, 127, 1.0, 0, LoopMode::Off);
}
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
@ -22,10 +22,10 @@ impl SplashScreenStep {
const TOTAL_DURATION: u32 = 240; // 4 seconds total (updated from 2s based on total_duration logic)
// Update peripherals for input
ctx.hw.pad_mut().begin_frame(ctx.signals);
ctx.platform.input_mut().pad_mut().begin_frame(ctx.signals);
// Draw expanding square
let (sw, sh) = ctx.hw.gfx().size();
let (sw, sh) = ctx.platform.display_size();
let max_size = (sw.min(sh) as i32 / 2).max(1);
let current_size = if self.frame < ANIMATION_DURATION {
@ -44,11 +44,14 @@ impl SplashScreenStep {
color: Color::WHITE,
},
]);
ctx.hw.publish_render_submission(&RenderSubmission::shell_ui(FrameId::ZERO, packet));
ctx.platform
.render_submission_sink()
.submit_render_submission(RenderSubmission::shell_ui(FrameId::ZERO, packet))
.expect("firmware splash render submission should publish");
// Transition logic
// If any button is pressed at any time after the animation ends
if self.frame >= ANIMATION_DURATION && ctx.hw.pad().any() {
if self.frame >= ANIMATION_DURATION && ctx.platform.input().pad().any() {
return Some(FirmwareState::LaunchHub(LaunchHubStep));
}

View File

@ -1,5 +1,5 @@
use crate::firmware::boot_target::BootTarget;
use prometeu_hal::{HardwareBridge, InputSignals};
use prometeu_hal::{InputSignals, RuntimePlatform};
use prometeu_system::{PrometeuHub, SystemOS};
use prometeu_vm::VirtualMachine;
@ -9,5 +9,5 @@ pub struct PrometeuContext<'a> {
pub hub: &'a mut PrometeuHub,
pub boot_target: &'a BootTarget,
pub signals: &'a InputSignals,
pub hw: &'a mut dyn HardwareBridge,
pub platform: &'a mut dyn RuntimePlatform,
}

View File

@ -1,39 +0,0 @@
use crate::asset_bridge::AssetBridge;
use crate::audio_bridge::AudioBridge;
use crate::gfx_bridge::GfxBridge;
use crate::pad_bridge::PadBridge;
use crate::render_submission::{Game2DFramePacket, RenderSubmission};
use crate::sprite::Sprite;
use crate::touch_bridge::TouchBridge;
/// Legacy migration scaffold for code that has not moved to `platform` facades yet.
///
/// `HardwareBridge` is intentionally not the final runtime-facing contract. New
/// platform boundaries belong in `crate::platform`, and this trait must be
/// removed after runtime, firmware, host, and tests finish migrating.
pub trait HardwareBridge {
fn begin_frame(&mut self);
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 close_game2d_packet(&self) -> Game2DFramePacket;
fn publish_render_submission(&mut self, submission: &RenderSubmission);
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;
fn audio(&self) -> &dyn AudioBridge;
fn audio_mut(&mut self) -> &mut dyn AudioBridge;
fn pad(&self) -> &dyn PadBridge;
fn pad_mut(&mut self) -> &mut dyn PadBridge;
fn touch(&self) -> &dyn TouchBridge;
fn touch_mut(&mut self) -> &mut dyn TouchBridge;
fn assets(&self) -> &dyn AssetBridge;
fn assets_mut(&mut self) -> &mut dyn AssetBridge;
}

View File

@ -1,19 +1,23 @@
use crate::hardware_bridge::HardwareBridge;
use crate::platform::RuntimePlatform;
use crate::vm_fault::VmFault;
pub struct HostContext<'a> {
pub hw: Option<&'a mut dyn HardwareBridge>,
pub platform: Option<&'a mut dyn RuntimePlatform>,
}
impl<'a> HostContext<'a> {
pub fn new(hw: Option<&'a mut dyn HardwareBridge>) -> Self {
Self { hw }
pub fn new(platform: Option<&'a mut dyn RuntimePlatform>) -> Self {
Self { platform }
}
pub fn new_platform(platform: Option<&'a mut dyn RuntimePlatform>) -> Self {
Self::new(platform)
}
#[inline]
pub fn require_hw(&mut self) -> Result<&mut dyn HardwareBridge, VmFault> {
match &mut self.hw {
Some(hw) => Ok(*hw),
pub fn require_platform(&mut self) -> Result<&mut dyn RuntimePlatform, VmFault> {
match &mut self.platform {
Some(platform) => Ok(*platform),
None => Err(VmFault::Unavailable),
}
}

View File

@ -11,7 +11,6 @@ pub mod debugger_protocol;
pub mod gfx_bridge;
pub mod glyph;
pub mod glyph_bank;
pub mod hardware_bridge;
pub mod host_context;
pub mod host_return;
pub mod input_signals;
@ -40,7 +39,6 @@ pub use asset_bridge::AssetBridge;
pub use audio_bridge::{AudioBridge, AudioOpStatus, LoopMode};
pub use composer_status::ComposerOpStatus;
pub use gfx_bridge::{BlendMode, GfxBridge, GfxOpStatus};
pub use hardware_bridge::HardwareBridge;
pub use host_context::{HostContext, HostContextProvider};
pub use host_return::HostReturn;
pub use input_signals::InputSignals;
@ -49,8 +47,8 @@ pub use native_interface::{NativeInterface, SyscallId};
pub use pad_bridge::PadBridge;
pub use platform::{
AssetStoragePlatform, AudioPlatform, ClockPacingPlatform, Game2DFrameComposer, InputPlatform,
RenderBackend, RenderSubmissionSink, RenderSubmitError, RuntimePlatform, TelemetryPlatform,
TestPlatform,
NoopTelemetryPlatform, RenderBackend, RenderSubmissionSink, RenderSubmitError, RuntimePlatform,
TelemetryPlatform, TestPlatform,
};
pub use render_submission::{
BoundScenePacket, Camera2D, ComposerFramePacket, FrameId, Game2DFramePacket, GameSpritePacket,

View File

@ -31,7 +31,6 @@ pub trait Game2DFrameComposer {
fn set_camera(&mut self, x: i32, y: i32) -> ComposerOpStatus;
fn emit_sprite(&mut self, sprite: Sprite) -> ComposerOpStatus;
fn close_game2d_packet(&self) -> Game2DFramePacket;
fn has_glyph_bank(&self, bank_id: usize) -> bool;
}
pub trait AudioPlatform: AudioBridge {}
@ -55,16 +54,25 @@ pub trait TelemetryPlatform {
}
}
#[derive(Debug, Default)]
pub struct NoopTelemetryPlatform;
impl TelemetryPlatform for NoopTelemetryPlatform {}
pub trait RuntimePlatform {
fn display_size(&self) -> (usize, usize) {
(480, 270)
}
fn render_submission_sink(&mut self) -> &mut dyn RenderSubmissionSink;
fn render_backend(&mut self) -> &mut dyn RenderBackend;
fn game2d_frame_composer(&mut self) -> &mut dyn Game2DFrameComposer;
fn audio(&self) -> &dyn AudioPlatform;
fn audio_mut(&mut self) -> &mut dyn AudioPlatform;
fn audio(&self) -> &dyn AudioBridge;
fn audio_mut(&mut self) -> &mut dyn AudioBridge;
fn input(&self) -> &dyn InputPlatform;
fn input_mut(&mut self) -> &mut dyn InputPlatform;
fn assets(&self) -> &dyn AssetStoragePlatform;
fn assets_mut(&mut self) -> &mut dyn AssetStoragePlatform;
fn assets(&self) -> &dyn AssetBridge;
fn assets_mut(&mut self) -> &mut dyn AssetBridge;
fn telemetry(&self) -> &dyn TelemetryPlatform;
}
@ -114,10 +122,6 @@ mod tests {
fn close_game2d_packet(&self) -> Game2DFramePacket {
Game2DFramePacket::default()
}
fn has_glyph_bank(&self, _bank_id: usize) -> bool {
false
}
}
#[test]

View File

@ -3,7 +3,7 @@ use crate::os::SystemOS;
use prometeu_hal::app_mode::AppMode;
use prometeu_hal::cartridge::Cartridge;
use prometeu_hal::telemetry::{CertificationConfig, TelemetryFrame};
use prometeu_hal::{HardwareBridge, InputSignals};
use prometeu_hal::{InputSignals, RuntimePlatform};
use prometeu_vm::VirtualMachine;
use std::sync::atomic::Ordering;
@ -25,7 +25,7 @@ impl<'a> VmFacade<'a> {
&mut self,
vm: &mut VirtualMachine,
signals: &InputSignals,
hw: &mut dyn HardwareBridge,
platform: &mut dyn RuntimePlatform,
) -> Option<CrashReport> {
self.os.vm_runtime.tick(
&mut self.os.log_service,
@ -36,7 +36,7 @@ impl<'a> VmFacade<'a> {
&mut self.os.next_handle,
vm,
signals,
hw,
platform,
)
}
@ -47,9 +47,9 @@ impl<'a> VmFacade<'a> {
pub fn debug_step_instruction(
&mut self,
vm: &mut VirtualMachine,
hw: &mut dyn HardwareBridge,
platform: &mut dyn RuntimePlatform,
) -> Option<CrashReport> {
self.os.vm_runtime.debug_step_instruction(&mut self.os.log_service, vm, hw)
self.os.vm_runtime.debug_step_instruction(&mut self.os.log_service, vm, platform)
}
pub fn paused(&self) -> bool {

View File

@ -2,7 +2,8 @@ use crate::{CrashReport, SystemOS};
use prometeu_hal::color::Color;
use prometeu_hal::primitives::Rect;
use prometeu_hal::{
FrameId, GfxUiCommand, HardwareBridge, InputSignals, RenderSubmission, ShellUiFramePacket,
FrameId, GfxUiCommand, InputPlatform, InputSignals, RenderSubmission, RuntimePlatform,
ShellUiFramePacket,
};
use prometeu_vm::VirtualMachine;
@ -100,31 +101,35 @@ impl PrometeuHub {
pub fn gui_update(
&mut self,
os: &mut SystemOS,
hw: &mut dyn HardwareBridge,
platform: &mut dyn RuntimePlatform,
) -> Option<SystemProfileAction> {
let in_shell = os.windows().focused_window().is_some();
if !in_shell {
if !self.home_input_armed {
self.home_input_armed = !activation_input_down(hw);
self.home_input_armed = !activation_input_down(platform.input());
return None;
}
} else {
self.home_input_armed = false;
}
if let Some(action) =
action_for_click(in_shell, hw.touch().x(), hw.touch().y(), hw.touch().f().pressed)
{
let input = platform.input();
if let Some(action) = action_for_click(
in_shell,
input.touch().x(),
input.touch().y(),
input.touch().f().pressed,
) {
return Some(action);
}
None
}
pub fn render(&mut self, os: &mut SystemOS, hw: &mut dyn HardwareBridge) {
let pointer_x = hw.touch().x();
let pointer_y = hw.touch().y();
pub fn render(&mut self, os: &mut SystemOS, platform: &mut dyn RuntimePlatform) {
let pointer_x = platform.input().touch().x();
let pointer_y = platform.input().touch().y();
let packet = if os.windows().window_count() == 0 {
render_home_packet(pointer_x, pointer_y)
@ -142,7 +147,10 @@ impl PrometeuHub {
}
ShellUiFramePacket::new(commands)
};
hw.publish_render_submission(&RenderSubmission::shell_ui(FrameId::ZERO, packet));
platform
.render_submission_sink()
.submit_render_submission(RenderSubmission::shell_ui(FrameId::ZERO, packet))
.expect("hub render submission should publish");
}
pub fn update_shell_profile(
@ -150,20 +158,20 @@ impl PrometeuHub {
os: &mut SystemOS,
vm: &mut VirtualMachine,
signals: &InputSignals,
hw: &mut dyn HardwareBridge,
platform: &mut dyn RuntimePlatform,
) -> SystemProfileUpdate {
let mut crash = None;
let mut action = self.gui_update(os, hw);
let mut action = self.gui_update(os, platform);
if os.windows().focused_window().is_some() {
if hw.pad().start().down {
if platform.input().pad().start().down {
action = Some(SystemProfileAction::CloseShell);
} else if action != Some(SystemProfileAction::CloseShell) {
crash = os.vm().tick(vm, signals, hw);
crash = os.vm().tick(vm, signals, platform);
}
}
self.render(os, hw);
self.render(os, platform);
SystemProfileUpdate { crash, action }
}
@ -188,8 +196,11 @@ fn rect_contains(rect: Rect, x: i32, y: i32) -> bool {
x >= rect.x && x < rect.x + rect.w && y >= rect.y && y < rect.y + rect.h
}
fn activation_input_down(hw: &dyn HardwareBridge) -> bool {
hw.touch().f().down || hw.pad().a().down || hw.pad().b().down || hw.pad().start().down
fn activation_input_down(input: &dyn InputPlatform) -> bool {
input.touch().f().down
|| input.pad().a().down
|| input.pad().b().down
|| input.pad().start().down
}
fn render_home_packet(pointer_x: i32, pointer_y: i32) -> ShellUiFramePacket {

View File

@ -179,7 +179,7 @@ impl NativeInterface for VmRuntimeHost<'_> {
self.ensure_game_profile_syscall(syscall)?;
let hw = ctx.require_hw()?;
let platform = ctx.require_platform()?;
match syscall {
Syscall::SystemHasCart => unreachable!(),
@ -187,7 +187,6 @@ impl NativeInterface for VmRuntimeHost<'_> {
Syscall::GfxClear => {
let color = self.get_color(expect_int(args, 0)?)?;
self.gfx2d_commands.push(Gfx2dCommand::Clear { color });
hw.gfx_mut().clear(color);
Ok(())
}
Syscall::GfxUiClear => {
@ -199,7 +198,6 @@ impl NativeInterface for VmRuntimeHost<'_> {
}
let color = self.get_color(expect_int(args, 0)?)?;
self.gfxui_commands.push(GfxUiCommand::Clear { color });
hw.gfx_mut().clear(color);
Ok(())
}
Syscall::GfxFillRect => {
@ -210,7 +208,6 @@ impl NativeInterface for VmRuntimeHost<'_> {
let color = self.get_color(expect_int(args, 4)?)?;
self.gfx2d_commands
.push(Gfx2dCommand::FillRect { rect: Rect { x, y, w, h }, color });
hw.gfx_mut().fill_rect(x, y, w, h, color);
Ok(())
}
Syscall::GfxUiFillRect => {
@ -227,7 +224,6 @@ impl NativeInterface for VmRuntimeHost<'_> {
let color = self.get_color(expect_int(args, 4)?)?;
self.gfxui_commands
.push(GfxUiCommand::FillRect { rect: Rect { x, y, w, h }, color });
hw.gfx_mut().fill_rect(x, y, w, h, color);
Ok(())
}
Syscall::GfxDrawLine => {
@ -243,7 +239,6 @@ impl NativeInterface for VmRuntimeHost<'_> {
y1: y2,
color,
});
hw.gfx_mut().draw_line(x1, y1, x2, y2, color);
Ok(())
}
Syscall::GfxUiDrawLine => {
@ -265,7 +260,6 @@ impl NativeInterface for VmRuntimeHost<'_> {
y1: y2,
color,
});
hw.gfx_mut().draw_line(x1, y1, x2, y2, color);
Ok(())
}
Syscall::GfxDrawCircle => {
@ -274,7 +268,6 @@ impl NativeInterface for VmRuntimeHost<'_> {
let r = expect_int(args, 2)? as i32;
let color = self.get_color(expect_int(args, 3)?)?;
self.gfx2d_commands.push(Gfx2dCommand::DrawCircle { x, y, radius: r, color });
hw.gfx_mut().draw_circle(x, y, r, color);
Ok(())
}
Syscall::GfxUiDrawCircle => {
@ -289,7 +282,6 @@ impl NativeInterface for VmRuntimeHost<'_> {
let r = expect_int(args, 2)? as i32;
let color = self.get_color(expect_int(args, 3)?)?;
self.gfxui_commands.push(GfxUiCommand::DrawCircle { x, y, radius: r, color });
hw.gfx_mut().draw_circle(x, y, r, color);
Ok(())
}
Syscall::GfxDrawDisc => {
@ -305,7 +297,6 @@ impl NativeInterface for VmRuntimeHost<'_> {
border_color,
fill_color,
});
hw.gfx_mut().draw_disc(x, y, r, border_color, fill_color);
Ok(())
}
Syscall::GfxUiDrawDisc => {
@ -327,7 +318,6 @@ impl NativeInterface for VmRuntimeHost<'_> {
border_color,
fill_color,
});
hw.gfx_mut().draw_disc(x, y, r, border_color, fill_color);
Ok(())
}
Syscall::GfxDrawSquare => {
@ -342,7 +332,6 @@ impl NativeInterface for VmRuntimeHost<'_> {
border_color,
fill_color,
});
hw.gfx_mut().draw_square(x, y, w, h, border_color, fill_color);
Ok(())
}
Syscall::GfxUiDrawSquare => {
@ -363,7 +352,6 @@ impl NativeInterface for VmRuntimeHost<'_> {
border_color,
fill_color,
});
hw.gfx_mut().draw_square(x, y, w, h, border_color, fill_color);
Ok(())
}
Syscall::GfxDrawText => {
@ -372,7 +360,6 @@ impl NativeInterface for VmRuntimeHost<'_> {
let msg = expect_string(args, 2, "message")?;
let color = self.get_color(expect_int(args, 3)?)?;
self.gfx2d_commands.push(Gfx2dCommand::DrawText { x, y, text: msg.clone(), color });
hw.gfx_mut().draw_text(x, y, &msg, color);
Ok(())
}
Syscall::GfxUiDrawText => {
@ -387,7 +374,6 @@ impl NativeInterface for VmRuntimeHost<'_> {
let msg = expect_string(args, 2, "message")?;
let color = self.get_color(expect_int(args, 3)?)?;
self.gfxui_commands.push(GfxUiCommand::DrawText { x, y, text: msg.clone(), color });
hw.gfx_mut().draw_text(x, y, &msg, color);
Ok(())
}
Syscall::ComposerBindScene => {
@ -399,23 +385,23 @@ impl NativeInterface for VmRuntimeHost<'_> {
}
};
let bound = catch_unwind(AssertUnwindSafe(|| hw.bind_scene(scene_bank_id)))
.map_err(|payload| VmFault::Panic(Self::panic_payload_to_string(payload)))?;
let status = catch_unwind(AssertUnwindSafe(|| {
platform.game2d_frame_composer().bind_scene(scene_bank_id)
}))
.map_err(|payload| VmFault::Panic(Self::panic_payload_to_string(payload)))?;
let status =
if bound { ComposerOpStatus::Ok } else { ComposerOpStatus::SceneUnavailable };
ret.push_int(status as i64);
Ok(())
}
Syscall::ComposerUnbindScene => {
hw.unbind_scene();
platform.game2d_frame_composer().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);
platform.game2d_frame_composer().set_camera(x, y);
Ok(())
}
Syscall::ComposerEmitSprite => {
@ -463,12 +449,7 @@ impl NativeInterface for VmRuntimeHost<'_> {
}
};
if !hw.has_glyph_bank(bank_id as usize) {
ret.push_int(ComposerOpStatus::BankInvalid as i64);
return Ok(());
}
let emitted = hw.emit_sprite(Sprite {
let status = platform.game2d_frame_composer().emit_sprite(Sprite {
glyph: Glyph { glyph_id, palette_id },
x,
y,
@ -479,8 +460,6 @@ impl NativeInterface for VmRuntimeHost<'_> {
flip_y,
priority,
});
let status =
if emitted { ComposerOpStatus::Ok } else { ComposerOpStatus::SpriteOverflow };
ret.push_int(status as i64);
Ok(())
}
@ -507,7 +486,7 @@ impl NativeInterface for VmRuntimeHost<'_> {
return Ok(());
}
let status = hw.audio_mut().play(
let status = platform.audio_mut().play(
0,
sample_id_raw as u16,
voice_id_raw as usize,
@ -537,7 +516,8 @@ impl NativeInterface for VmRuntimeHost<'_> {
return Ok(());
}
if hw.assets().slot_info(SlotRef::audio(bank_id as usize)).asset_id.is_none() {
if platform.assets().slot_info(SlotRef::audio(bank_id as usize)).asset_id.is_none()
{
ret.push_int(AudioOpStatus::BankInvalid as i64);
return Ok(());
}
@ -553,7 +533,7 @@ impl NativeInterface for VmRuntimeHost<'_> {
return Ok(());
}
let status = hw.audio_mut().play(
let status = platform.audio_mut().play(
bank_id,
sample_id_raw as u16,
voice_id_raw as usize,
@ -750,7 +730,7 @@ impl NativeInterface for VmRuntimeHost<'_> {
})?;
let slot_index = expect_int(args, 1)? as usize;
match hw.assets().load(asset_id, slot_index) {
match platform.assets().load(asset_id, slot_index) {
Ok(handle) => {
ret.push_int(AssetOpStatus::Ok as i64);
ret.push_int(handle as i64);
@ -764,16 +744,16 @@ impl NativeInterface for VmRuntimeHost<'_> {
}
}
Syscall::AssetStatus => {
ret.push_int(hw.assets().status(expect_int(args, 0)? as u32) as i64);
ret.push_int(platform.assets().status(expect_int(args, 0)? as u32) as i64);
Ok(())
}
Syscall::AssetCommit => {
let status = hw.assets().commit(expect_int(args, 0)? as u32);
let status = platform.assets().commit(expect_int(args, 0)? as u32);
ret.push_int(status as i64);
Ok(())
}
Syscall::AssetCancel => {
let status = hw.assets().cancel(expect_int(args, 0)? as u32);
let status = platform.assets().cancel(expect_int(args, 0)? as u32);
ret.push_int(status as i64);
Ok(())
}
@ -783,7 +763,7 @@ impl NativeInterface for VmRuntimeHost<'_> {
1 => BankType::SOUNDS,
_ => return Err(VmFault::Trap(TRAP_TYPE, "Invalid asset type".to_string())),
};
let telemetry = hw
let telemetry = platform
.assets()
.bank_telemetry()
.into_iter()

View File

@ -5,11 +5,10 @@ use prometeu_bytecode::TRAP_TYPE;
use prometeu_bytecode::Value;
use prometeu_bytecode::assembler::assemble;
use prometeu_bytecode::model::{BytecodeModule, ConstantPoolEntry, FunctionMeta, SyscallDecl};
use prometeu_drivers::hardware::Hardware;
use prometeu_drivers::TestPlatform;
use prometeu_drivers::{GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolInstaller};
use prometeu_hal::AudioOpStatus;
use prometeu_hal::ComposerOpStatus;
use prometeu_hal::InputSignals;
use prometeu_hal::asset::{
AssetCodec, AssetEntry, AssetLoadError, AssetOpStatus, BankType, LoadStatus,
};
@ -22,6 +21,7 @@ use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
use prometeu_hal::syscalls::caps;
use prometeu_hal::tile::Tile;
use prometeu_hal::tilemap::TileMap;
use prometeu_hal::{InputSignals, RuntimePlatform};
use prometeu_vm::VmInitError;
use std::collections::HashMap;
use std::sync::Arc;
@ -247,7 +247,7 @@ fn tick_returns_error_when_vm_ends_slice_with_trap() {
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
let code = assemble("PUSH_BOOL 1\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
@ -274,7 +274,7 @@ fn tick_returns_error_when_vm_ends_slice_with_trap() {
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
&mut platform,
)
.expect("trap must surface as runtime error");
@ -298,7 +298,7 @@ fn tick_system_profile_rejects_gfx_game_surface() {
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
@ -324,7 +324,7 @@ fn tick_system_profile_rejects_gfx_game_surface() {
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
&mut platform,
)
.expect("system gfx call must trap");
@ -347,7 +347,7 @@ fn tick_system_profile_rejects_composer_game_surface() {
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
@ -373,7 +373,7 @@ fn tick_system_profile_rejects_composer_game_surface() {
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
&mut platform,
)
.expect("system composer call must trap");
@ -398,7 +398,7 @@ fn tick_game_profile_rejects_gfxui_shell_surface() {
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
@ -424,7 +424,7 @@ fn tick_game_profile_rejects_gfxui_shell_surface() {
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
&mut platform,
)
.expect("game gfxui call must trap");
@ -447,7 +447,7 @@ fn tick_shell_profile_closes_gfxui_commands_into_shell_packet() {
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
let code = assemble("PUSH_I32 287454020\nHOSTCALL 0\nFRAME_SYNC\nHALT").expect("assemble");
let program = serialized_single_function_module(
@ -472,7 +472,7 @@ fn tick_shell_profile_closes_gfxui_commands_into_shell_packet() {
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
&mut platform,
);
assert!(report.is_none());
@ -483,11 +483,110 @@ fn tick_shell_profile_closes_gfxui_commands_into_shell_packet() {
};
assert_eq!(packet.commands.len(), 1);
assert!(matches!(packet.commands[0], prometeu_hal::GfxUiCommand::Clear { .. }));
assert_eq!(hardware.gfx.front_buffer()[0], Color::from_raw(0x11223344).raw());
assert_eq!(platform.local_hardware().gfx.front_buffer()[0], Color::from_raw(0x11223344).raw());
assert_eq!(runtime.frame_scheduler.completed_game_frames(), 0);
assert_eq!(runtime.frame_scheduler.active_game_frame_id(), None);
}
#[test]
fn tick_gfx2d_clear_buffers_without_immediate_pixel_mutation_before_frame_close() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut fs = VirtualFS::new();
let mut fs_state = FsState::Unmounted;
let mut memcard = MemcardService::new();
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
let code = assemble("PUSH_I32 287454020\nHOSTCALL 0\nJMP 0").expect("assemble");
let program = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "gfx2d".into(),
name: "clear".into(),
version: 1,
arg_slots: 1,
ret_slots: 0,
}],
);
let cartridge = cartridge_with_program(program, caps::GFX);
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(
&mut log_service,
&mut fs,
&mut fs_state,
&mut memcard,
&mut open_files,
&mut next_handle,
&mut vm,
&signals,
&mut platform,
);
assert!(report.is_none());
assert!(runtime.render_manager.latest_complete_submission().is_none());
assert!(matches!(
runtime.gfx2d_commands.first(),
Some(prometeu_hal::Gfx2dCommand::Clear { color })
if *color == Color::from_raw(0x11223344)
));
assert_ne!(platform.local_hardware().gfx.front_buffer()[0], Color::from_raw(0x11223344).raw());
}
#[test]
fn tick_gfx2d_draw_text_buffers_without_immediate_pixel_mutation_before_frame_close() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut fs = VirtualFS::new();
let mut fs_state = FsState::Unmounted;
let mut memcard = MemcardService::new();
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
let code =
assemble("PUSH_I32 0\nPUSH_I32 0\nPUSH_CONST 0\nPUSH_I32 287454020\nHOSTCALL 0\nJMP 0")
.expect("assemble");
let program = serialized_single_function_module_with_consts(
code,
vec![ConstantPoolEntry::String("I".into())],
vec![SyscallDecl {
module: "gfx2d".into(),
name: "draw_text".into(),
version: 1,
arg_slots: 4,
ret_slots: 0,
}],
);
let cartridge = cartridge_with_program(program, caps::GFX);
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(
&mut log_service,
&mut fs,
&mut fs_state,
&mut memcard,
&mut open_files,
&mut next_handle,
&mut vm,
&signals,
&mut platform,
);
assert!(report.is_none());
assert!(runtime.render_manager.latest_complete_submission().is_none());
assert!(matches!(
runtime.gfx2d_commands.first(),
Some(prometeu_hal::Gfx2dCommand::DrawText { x: 0, y: 0, text, color })
if text == "I" && *color == Color::from_raw(0x11223344)
));
assert_ne!(platform.local_hardware().gfx.front_buffer()[0], Color::from_raw(0x11223344).raw());
}
#[test]
fn tick_system_profile_rejects_bank_game_surface() {
let mut runtime = VirtualMachineRuntime::new(None);
@ -498,7 +597,7 @@ fn tick_system_profile_rejects_bank_game_surface() {
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
@ -524,7 +623,7 @@ fn tick_system_profile_rejects_bank_game_surface() {
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
&mut platform,
)
.expect("system bank call must trap");
@ -547,7 +646,7 @@ fn tick_system_profile_can_use_shared_log_transport() {
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
let code = assemble("PUSH_I32 2\nPUSH_CONST 0\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module_with_consts(
@ -573,7 +672,7 @@ fn tick_system_profile_can_use_shared_log_transport() {
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
&mut platform,
);
assert!(report.is_none(), "system log transport must remain internally shared");
@ -590,7 +689,7 @@ fn tick_returns_panic_report_distinct_from_trap() {
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::new(assemble("HOSTCALL 0\nHALT").expect("assemble"), vec![]);
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
let report = runtime
@ -603,7 +702,7 @@ fn tick_returns_panic_report_distinct_from_trap() {
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
&mut platform,
)
.expect("panic must surface as runtime error");
@ -635,12 +734,12 @@ fn tick_renders_bound_eight_pixel_scene_through_frame_composer_path() {
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));
let glyph_slot_index = hardware.assets.glyph_asset_slot_index();
let mut platform = TestPlatform::new_with_memory_banks(Arc::clone(&banks));
let glyph_slot_index = platform.local_hardware().assets.glyph_asset_slot_index();
let mut slots = [None; 16];
slots[0] = Some(0);
glyph_slot_index.rebuild_from_slots(&slots);
assert!(hardware.frame_composer.bind_scene(0));
assert!(platform.local_hardware_mut().frame_composer.bind_scene(0));
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(
@ -652,11 +751,11 @@ fn tick_renders_bound_eight_pixel_scene_through_frame_composer_path() {
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
&mut platform,
);
assert!(report.is_none(), "frame render path must not crash");
assert_eq!(hardware.gfx.front_buffer()[0], Color::BLUE.raw());
assert_eq!(platform.local_hardware().gfx.front_buffer()[0], Color::BLUE.raw());
}
#[test]
@ -708,8 +807,8 @@ fn tick_renders_scene_through_public_composer_syscalls() {
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));
let glyph_slot_index = hardware.assets.glyph_asset_slot_index();
let mut platform = TestPlatform::new_with_memory_banks(Arc::clone(&banks));
let glyph_slot_index = platform.local_hardware().assets.glyph_asset_slot_index();
let mut slots = [None; 16];
slots[0] = Some(0);
glyph_slot_index.rebuild_from_slots(&slots);
@ -724,12 +823,12 @@ fn tick_renders_scene_through_public_composer_syscalls() {
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
&mut platform,
);
assert!(report.is_none(), "public composer path must not crash");
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(ComposerOpStatus::Ok as i64)]);
assert_eq!(hardware.gfx.front_buffer()[0], Color::BLUE.raw());
assert_eq!(platform.local_hardware().gfx.front_buffer()[0], Color::BLUE.raw());
}
#[test]
@ -759,7 +858,7 @@ fn tick_draw_text_survives_no_scene_frame_path() {
}],
);
let cartridge = cartridge_with_program(program, caps::GFX);
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(
@ -771,7 +870,7 @@ fn tick_draw_text_survives_no_scene_frame_path() {
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
&mut platform,
);
assert!(report.is_none(), "no-scene overlay text must not crash");
@ -782,7 +881,7 @@ fn tick_draw_text_survives_no_scene_frame_path() {
};
assert_eq!(packet.gfx2d.len(), 1);
assert!(matches!(packet.gfx2d[0], prometeu_hal::Gfx2dCommand::DrawText { .. }));
assert_eq!(hardware.gfx.front_buffer()[0], Color::from_raw(0x11223344).raw());
assert_eq!(platform.local_hardware().gfx.front_buffer()[0], Color::from_raw(0x11223344).raw());
}
#[test]
@ -808,7 +907,7 @@ fn tick_game_submissions_are_immutable_and_latest_complete_wins() {
}],
);
let cartridge = cartridge_with_program(program, caps::GFX);
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let first_report = runtime.tick(
@ -820,7 +919,7 @@ fn tick_game_submissions_are_immutable_and_latest_complete_wins() {
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
&mut platform,
);
assert!(first_report.is_none());
let first_submission =
@ -834,7 +933,7 @@ fn tick_game_submissions_are_immutable_and_latest_complete_wins() {
first_packet.gfx2d[0],
prometeu_hal::Gfx2dCommand::Clear { color } if color == Color::from_raw(0x11223344)
));
assert_eq!(hardware.gfx.front_buffer()[0], Color::from_raw(0x11223344).raw());
assert_eq!(platform.local_hardware().gfx.front_buffer()[0], Color::from_raw(0x11223344).raw());
assert_eq!(runtime.frame_scheduler.completed_game_frames(), 1);
let second_packet = prometeu_hal::Game2DFramePacket::new(
@ -858,8 +957,11 @@ fn tick_game_submissions_are_immutable_and_latest_complete_wins() {
first_packet.gfx2d[0],
prometeu_hal::Gfx2dCommand::Clear { color } if color == Color::from_raw(0x11223344)
));
prometeu_hal::HardwareBridge::publish_render_submission(&mut hardware, latest);
assert_eq!(hardware.gfx.front_buffer()[0], Color::from_raw(0x55667788).raw());
platform
.render_submission_sink()
.submit_render_submission(latest.clone())
.expect("local sink should publish latest submission");
assert_eq!(platform.local_hardware().gfx.front_buffer()[0], Color::from_raw(0x55667788).raw());
}
#[test]
@ -961,7 +1063,7 @@ fn tick_numeric_happy_path_records_zero_internal_allocations() {
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
let code =
assemble("PUSH_I32 1\nPUSH_I32 2\nADD\nPOP_N 1\nFRAME_SYNC\nHALT").expect("assemble");
@ -978,7 +1080,7 @@ fn tick_numeric_happy_path_records_zero_internal_allocations() {
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
&mut platform,
);
assert!(report.is_none());
@ -997,7 +1099,7 @@ fn tick_already_materialized_string_path_records_zero_internal_allocations() {
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
let code = assemble("PUSH_CONST 0\nSET_GLOBAL 0\nGET_GLOBAL 0\nPOP_N 1\nFRAME_SYNC\nHALT")
.expect("assemble");
@ -1018,7 +1120,7 @@ fn tick_already_materialized_string_path_records_zero_internal_allocations() {
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
&mut platform,
);
assert!(report.is_none());
@ -1093,7 +1195,7 @@ fn tick_composer_bind_scene_operational_error_returns_status_not_crash() {
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
let code = assemble("PUSH_I32 99\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
@ -1118,7 +1220,7 @@ fn tick_composer_bind_scene_operational_error_returns_status_not_crash() {
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
&mut platform,
);
assert!(report.is_none(), "operational error must not crash");
assert!(vm.is_halted());
@ -1138,7 +1240,7 @@ fn tick_composer_emit_sprite_operational_error_returns_status_not_crash() {
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let mut platform = TestPlatform::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 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT",
@ -1166,7 +1268,7 @@ fn tick_composer_emit_sprite_operational_error_returns_status_not_crash() {
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
&mut platform,
);
assert!(report.is_none(), "operational error must not crash");
assert!(vm.is_halted());
@ -1183,7 +1285,7 @@ fn tick_composer_emit_sprite_invalid_layer_returns_status_not_crash() {
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
let code = assemble(
"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",
@ -1211,7 +1313,7 @@ fn tick_composer_emit_sprite_invalid_layer_returns_status_not_crash() {
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
&mut platform,
);
assert!(report.is_none(), "invalid layer must not crash");
assert!(vm.is_halted());
@ -1228,7 +1330,7 @@ fn tick_composer_emit_sprite_invalid_range_returns_status_not_crash() {
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let mut platform = TestPlatform::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",
@ -1247,7 +1349,7 @@ fn tick_composer_emit_sprite_invalid_range_returns_status_not_crash() {
let cartridge = cartridge_with_program(program, caps::GFX);
let asset_data = test_glyph_asset_data();
hardware.assets.initialize_for_cartridge(
platform.local_hardware().assets.initialize_for_cartridge(
vec![test_glyph_asset_entry("tile_asset", asset_data.len())],
vec![prometeu_hal::asset::PreloadEntry { asset_id: 7, slot: 0 }],
AssetsPayloadSource::from_bytes(asset_data),
@ -1263,7 +1365,7 @@ fn tick_composer_emit_sprite_invalid_range_returns_status_not_crash() {
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
&mut platform,
);
assert!(report.is_none(), "invalid composer parameter range must not crash");
assert!(vm.is_halted());
@ -1283,7 +1385,7 @@ fn tick_audio_play_sample_operational_error_returns_status_not_crash() {
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
let code = assemble(
"PUSH_I32 -1\nPUSH_I32 0\nPUSH_I32 255\nPUSH_I32 128\nPUSH_I32 1\nHOSTCALL 0\nHALT",
@ -1311,7 +1413,7 @@ fn tick_audio_play_sample_operational_error_returns_status_not_crash() {
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
&mut platform,
);
assert!(report.is_none(), "operational error must not crash");
assert!(vm.is_halted());
@ -1328,7 +1430,7 @@ fn tick_audio_play_voice_invalid_returns_status_not_crash() {
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
let code = assemble(
"PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 16\nPUSH_I32 255\nPUSH_I32 128\nPUSH_I32 1\nPUSH_I32 0\nHOSTCALL 0\nHALT",
@ -1356,7 +1458,7 @@ fn tick_audio_play_voice_invalid_returns_status_not_crash() {
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
&mut platform,
);
assert!(report.is_none(), "invalid voice must not crash");
assert!(vm.is_halted());
@ -1373,7 +1475,7 @@ fn tick_audio_play_missing_asset_returns_status_not_crash() {
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
let code = assemble(
"PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 255\nPUSH_I32 128\nPUSH_I32 1\nPUSH_I32 0\nHOSTCALL 0\nHALT",
@ -1401,7 +1503,7 @@ fn tick_audio_play_missing_asset_returns_status_not_crash() {
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
&mut platform,
);
assert!(report.is_none(), "missing audio asset must not crash");
assert!(vm.is_halted());
@ -1418,7 +1520,7 @@ fn tick_audio_play_type_mismatch_surfaces_trap_not_panic() {
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
let code = assemble(
"PUSH_BOOL 1\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 255\nPUSH_I32 128\nPUSH_I32 1\nPUSH_I32 0\nHOSTCALL 0\nHALT",
@ -1447,7 +1549,7 @@ fn tick_audio_play_type_mismatch_surfaces_trap_not_panic() {
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
&mut platform,
)
.expect("type mismatch must surface as trap");
match report {
@ -1470,7 +1572,7 @@ fn tick_composer_emit_sprite_type_mismatch_surfaces_trap_not_panic() {
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let mut platform = TestPlatform::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 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT",
@ -1499,7 +1601,7 @@ fn tick_composer_emit_sprite_type_mismatch_surfaces_trap_not_panic() {
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
&mut platform,
)
.expect("type mismatch must surface as trap");
match report {

View File

@ -10,7 +10,7 @@ fn tick_asset_commit_operational_error_returns_status_not_crash() {
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
let code = assemble("PUSH_I32 999\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
@ -35,7 +35,7 @@ fn tick_asset_commit_operational_error_returns_status_not_crash() {
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
&mut platform,
);
assert!(report.is_none(), "operational error must not crash");
assert!(vm.is_halted());
@ -52,7 +52,7 @@ fn tick_asset_load_missing_asset_returns_status_and_zero_handle() {
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
let code = assemble("PUSH_I32 999\nPUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
@ -77,7 +77,7 @@ fn tick_asset_load_missing_asset_returns_status_and_zero_handle() {
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
&mut platform,
);
assert!(report.is_none(), "missing asset must not crash");
assert!(vm.is_halted());
@ -97,10 +97,10 @@ fn tick_asset_load_invalid_slot_returns_status_and_zero_handle() {
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
let asset_data = test_glyph_asset_data();
hardware.assets.initialize_for_cartridge(
platform.local_hardware().assets.initialize_for_cartridge(
vec![test_glyph_asset_entry("tile_asset", asset_data.len())],
vec![],
AssetsPayloadSource::from_bytes(asset_data),
@ -128,7 +128,7 @@ fn tick_asset_load_invalid_slot_returns_status_and_zero_handle() {
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
&mut platform,
);
assert!(report.is_none(), "invalid slot must not crash");
assert!(vm.is_halted());
@ -148,7 +148,7 @@ fn tick_asset_status_unknown_handle_returns_status_not_crash() {
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
let code = assemble("PUSH_I32 999\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
@ -173,7 +173,7 @@ fn tick_asset_status_unknown_handle_returns_status_not_crash() {
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
&mut platform,
);
assert!(report.is_none(), "unknown asset handle must not crash");
assert!(vm.is_halted());
@ -190,10 +190,10 @@ fn tick_bank_info_returns_slot_summary_not_json() {
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
let asset_data = test_glyph_asset_data();
hardware.assets.initialize_for_cartridge(
platform.local_hardware().assets.initialize_for_cartridge(
vec![test_glyph_asset_entry("tile_asset", asset_data.len())],
vec![prometeu_hal::asset::PreloadEntry { asset_id: 7, slot: 0 }],
AssetsPayloadSource::from_bytes(asset_data),
@ -221,7 +221,7 @@ fn tick_bank_info_returns_slot_summary_not_json() {
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
&mut platform,
);
assert!(report.is_none(), "bank summary must not crash");
assert!(vm.is_halted());
@ -261,7 +261,7 @@ fn tick_asset_commit_invalid_transition_returns_status_not_crash() {
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
let code = assemble("PUSH_I32 1\nHOSTCALL 0\nPOP_N 1\nPUSH_I32 1\nHOSTCALL 1\nHALT")
.expect("assemble");
@ -287,12 +287,13 @@ fn tick_asset_commit_invalid_transition_returns_status_not_crash() {
let cartridge = cartridge_with_program(program, caps::ASSET);
let asset_data = test_glyph_asset_data();
hardware.assets.initialize_for_cartridge(
platform.local_hardware().assets.initialize_for_cartridge(
vec![test_glyph_asset_entry("tile_asset", asset_data.len())],
vec![],
AssetsPayloadSource::from_bytes(asset_data),
);
let handle = hardware.assets.load(7, 0).expect("asset handle must be allocated");
let handle =
platform.local_hardware().assets.load(7, 0).expect("asset handle must be allocated");
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(
@ -304,7 +305,7 @@ fn tick_asset_commit_invalid_transition_returns_status_not_crash() {
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
&mut platform,
);
assert!(report.is_none(), "invalid transition must not crash");
assert!(vm.is_halted());
@ -322,7 +323,7 @@ fn tick_asset_cancel_unknown_handle_returns_status_not_crash() {
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
let code = assemble("PUSH_I32 999\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
@ -347,7 +348,7 @@ fn tick_asset_cancel_unknown_handle_returns_status_not_crash() {
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
&mut platform,
);
assert!(report.is_none(), "unknown handle cancel must not crash");
assert!(vm.is_halted());
@ -364,7 +365,7 @@ fn tick_asset_cancel_invalid_transition_returns_status_not_crash() {
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
let code = assemble("PUSH_I32 1\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
@ -380,17 +381,18 @@ fn tick_asset_cancel_invalid_transition_returns_status_not_crash() {
let cartridge = cartridge_with_program(program, caps::ASSET);
let asset_data = test_glyph_asset_data();
hardware.assets.initialize_for_cartridge(
platform.local_hardware().assets.initialize_for_cartridge(
vec![test_glyph_asset_entry("tile_asset", asset_data.len())],
vec![],
AssetsPayloadSource::from_bytes(asset_data),
);
let handle = hardware.assets.load(7, 0).expect("asset handle must be allocated");
let handle =
platform.local_hardware().assets.load(7, 0).expect("asset handle must be allocated");
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
loop {
match hardware.assets.status(handle) {
match platform.local_hardware().assets.status(handle) {
LoadStatus::READY => break,
LoadStatus::PENDING | LoadStatus::LOADING => {
std::thread::sleep(std::time::Duration::from_millis(1));
@ -399,9 +401,9 @@ fn tick_asset_cancel_invalid_transition_returns_status_not_crash() {
}
}
assert_eq!(hardware.assets.commit(handle), AssetOpStatus::Ok);
hardware.assets.apply_commits();
assert_eq!(hardware.assets.status(handle), LoadStatus::COMMITTED);
assert_eq!(platform.local_hardware().assets.commit(handle), AssetOpStatus::Ok);
platform.local_hardware().assets.apply_commits();
assert_eq!(platform.local_hardware().assets.status(handle), LoadStatus::COMMITTED);
let report = runtime.tick(
&mut log_service,
@ -412,7 +414,7 @@ fn tick_asset_cancel_invalid_transition_returns_status_not_crash() {
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
&mut platform,
);
assert!(report.is_none(), "cancel after commit must not crash");
assert!(vm.is_halted());
@ -429,7 +431,7 @@ fn tick_status_first_surface_smoke_across_composer_audio_and_asset() {
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let mut platform = TestPlatform::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 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\n\
@ -476,7 +478,7 @@ fn tick_status_first_surface_smoke_across_composer_audio_and_asset() {
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
&mut platform,
);
assert!(report.is_none(), "mixed status-first surface must not crash");
assert!(vm.is_halted());

View File

@ -11,7 +11,7 @@ fn tick_memcard_slot_roundtrip_for_game_profile() {
let mut next_handle = 1;
runtime.mount_fs(&mut log_service, &mut fs, &mut fs_state, Box::new(MemFsBackend::default()));
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
let code = assemble(
"PUSH_I32 0\nPUSH_I32 0\nPUSH_CONST 0\nHOSTCALL 0\nPOP_N 2\nPUSH_I32 0\nHOSTCALL 1\nPOP_N 1\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 10\nHOSTCALL 2\nHALT",
@ -66,7 +66,7 @@ fn tick_memcard_slot_roundtrip_for_game_profile() {
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
&mut platform,
);
assert!(report.is_none(), "memcard roundtrip must not crash");
assert!(vm.is_halted());
@ -86,7 +86,7 @@ fn tick_memcard_access_is_denied_for_non_game_profile() {
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
let signals = InputSignals::default();
let code = assemble("HOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
@ -121,7 +121,7 @@ fn tick_memcard_access_is_denied_for_non_game_profile() {
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
&mut platform,
);
assert!(report.is_none(), "non-game memcard call must return status");
assert!(vm.is_halted());

View File

@ -9,8 +9,8 @@ use crate::services::memcard::MemcardService;
use prometeu_hal::asset::{BankTelemetry, BankType};
use prometeu_hal::log::{LogLevel, LogService, LogSource};
use prometeu_hal::{
FrameId, HardwareBridge, HostContext, InputSignals, RenderSubmission, RenderSubmissionPacket,
ShellUiFramePacket,
FrameId, HostContext, InputSignals, RenderSubmission, RenderSubmissionPacket,
RenderSubmissionSink, RuntimePlatform, ShellUiFramePacket,
};
use prometeu_vm::LogicalFrameEndingReason;
use std::collections::HashMap;
@ -18,12 +18,14 @@ use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::atomic::Ordering;
struct HardwareRenderSurface<'a> {
hw: &'a mut dyn HardwareBridge,
sink: &'a mut dyn RenderSubmissionSink,
}
impl RenderSurface for HardwareRenderSurface<'_> {
fn consume_submission(&mut self, submission: &RenderSubmission) {
self.hw.publish_render_submission(submission);
self.sink
.submit_render_submission(submission.clone())
.expect("local render submission sink should publish");
}
}
@ -51,9 +53,9 @@ impl VirtualMachineRuntime {
}
fn bank_telemetry_summary(
hw: &dyn HardwareBridge,
platform: &dyn RuntimePlatform,
) -> (BankTelemetry, BankTelemetry, BankTelemetry) {
let telemetry = hw.assets().bank_telemetry();
let telemetry = platform.assets().bank_telemetry();
let glyph =
telemetry.iter().find(|entry| entry.bank_type == BankType::GLYPH).cloned().unwrap_or(
BankTelemetry { bank_type: BankType::GLYPH, used_slots: 0, total_slots: 0 },
@ -74,10 +76,10 @@ impl VirtualMachineRuntime {
&mut self,
log_service: &mut LogService,
vm: &mut VirtualMachine,
hw: &mut dyn HardwareBridge,
platform: &mut dyn RuntimePlatform,
) -> Option<CrashReport> {
let step_result = {
let mut ctx = HostContext::new(Some(hw));
let mut ctx = HostContext::new_platform(Some(platform));
let mut fs = VirtualFS::new();
let mut fs_state = FsState::Unmounted;
let mut memcard = MemcardService::new();
@ -131,7 +133,7 @@ impl VirtualMachineRuntime {
next_handle: &mut u32,
vm: &mut VirtualMachine,
signals: &InputSignals,
hw: &mut dyn HardwareBridge,
platform: &mut dyn RuntimePlatform,
) -> Option<CrashReport> {
let start = Instant::now();
self.tick_index += 1;
@ -141,7 +143,6 @@ impl VirtualMachineRuntime {
}
self.update_fs(log_service, fs, fs_state);
if !self.logical_frame_active {
if RenderManager::policy_for_app_mode(self.current_cartridge_app_mode)
.uses_frame_scheduler()
@ -152,7 +153,7 @@ impl VirtualMachineRuntime {
}
self.logical_frame_active = true;
self.logical_frame_remaining_cycles = Self::CYCLES_PER_LOGICAL_FRAME;
self.begin_logical_frame(signals, hw);
self.begin_logical_frame(signals, platform);
if self.needs_prepare_entry_call || vm.call_stack_is_empty() {
vm.prepare_boot_call();
@ -180,7 +181,7 @@ impl VirtualMachineRuntime {
if budget > 0 {
let run_result = {
let mut ctx = HostContext::new(Some(hw));
let mut ctx = HostContext::new_platform(Some(platform));
let mut host = VmRuntimeHost {
runtime: self,
log_service,
@ -248,7 +249,7 @@ impl VirtualMachineRuntime {
self.current_app_id,
);
if self.current_cartridge_app_mode == AppMode::Game {
let mut packet = hw.close_game2d_packet();
let mut packet = platform.game2d_frame_composer().close_game2d_packet();
packet.gfx2d = std::mem::take(&mut self.gfx2d_commands);
self.render_manager
.close_frame_with_packet(RenderSubmissionPacket::Game2D(packet))
@ -262,7 +263,8 @@ impl VirtualMachineRuntime {
}
let render_outcome = catch_unwind(AssertUnwindSafe(|| {
let mut surface = HardwareRenderSurface { hw };
let mut surface =
HardwareRenderSurface { sink: platform.render_submission_sink() };
match self
.render_manager
.active_render_policy()
@ -304,7 +306,8 @@ impl VirtualMachineRuntime {
}
// 1. Snapshot full telemetry at logical frame end
let (glyph_bank, sound_bank, scene_bank) = Self::bank_telemetry_summary(hw);
let (glyph_bank, sound_bank, scene_bank) =
Self::bank_telemetry_summary(platform);
self.atomic_telemetry
.glyph_slots_used
.store(glyph_bank.used_slots as u32, Ordering::Relaxed);
@ -395,7 +398,7 @@ impl VirtualMachineRuntime {
// 2. High-frequency telemetry update (only if inspection is active)
if self.inspection_active {
let (glyph_bank, sound_bank, scene_bank) = Self::bank_telemetry_summary(hw);
let (glyph_bank, sound_bank, scene_bank) = Self::bank_telemetry_summary(platform);
self.atomic_telemetry
.glyph_slots_used
.store(glyph_bank.used_slots as u32, Ordering::Relaxed);
@ -432,10 +435,10 @@ impl VirtualMachineRuntime {
pub(crate) fn begin_logical_frame(
&mut self,
_signals: &InputSignals,
hw: &mut dyn HardwareBridge,
platform: &mut dyn RuntimePlatform,
) {
hw.begin_frame();
hw.audio_mut().clear_commands();
platform.game2d_frame_composer().begin_frame();
platform.audio_mut().clear_commands();
self.logs_written_this_frame.clear();
}
}

View File

@ -1350,8 +1350,9 @@ fn input_touch_x(
ctx: &mut HostContext<'_>,
) -> Result<Vec<Value>, IntrinsicExecutionError> {
expect_touch_handle(args, "x")?;
let hw = ctx.require_hw().map_err(|_| IntrinsicExecutionError::HardwareUnavailable)?;
Ok(vec![Value::Int64(hw.touch().x() as i64)])
let platform =
ctx.require_platform().map_err(|_| IntrinsicExecutionError::HardwareUnavailable)?;
Ok(vec![Value::Int64(platform.input().touch().x() as i64)])
}
fn input_touch_y(
@ -1359,8 +1360,9 @@ fn input_touch_y(
ctx: &mut HostContext<'_>,
) -> Result<Vec<Value>, IntrinsicExecutionError> {
expect_touch_handle(args, "y")?;
let hw = ctx.require_hw().map_err(|_| IntrinsicExecutionError::HardwareUnavailable)?;
Ok(vec![Value::Int64(hw.touch().y() as i64)])
let platform =
ctx.require_platform().map_err(|_| IntrinsicExecutionError::HardwareUnavailable)?;
Ok(vec![Value::Int64(platform.input().touch().y() as i64)])
}
fn input_button_pressed(
@ -1435,9 +1437,11 @@ fn resolve_button(
ctx: &mut HostContext<'_>,
) -> Result<prometeu_hal::button::Button, IntrinsicExecutionError> {
let carrier = expect_builtin_carrier(args, 0)?;
let hw = ctx.require_hw().map_err(|_| IntrinsicExecutionError::HardwareUnavailable)?;
let platform =
ctx.require_platform().map_err(|_| IntrinsicExecutionError::HardwareUnavailable)?;
let input = platform.input();
if carrier == TOUCH_BUTTON_CARRIER {
return Ok(*hw.touch().f());
return Ok(*input.touch().f());
}
let Some(index) = carrier.checked_sub(PAD_BUTTON_BASE) else {
@ -1448,18 +1452,18 @@ fn resolve_button(
});
};
let button = match index {
0 => *hw.pad().up(),
1 => *hw.pad().down(),
2 => *hw.pad().left(),
3 => *hw.pad().right(),
4 => *hw.pad().a(),
5 => *hw.pad().b(),
6 => *hw.pad().x(),
7 => *hw.pad().y(),
8 => *hw.pad().l(),
9 => *hw.pad().r(),
10 => *hw.pad().start(),
11 => *hw.pad().select(),
0 => *input.pad().up(),
1 => *input.pad().down(),
2 => *input.pad().left(),
3 => *input.pad().right(),
4 => *input.pad().a(),
5 => *input.pad().b(),
6 => *input.pad().x(),
7 => *input.pad().y(),
8 => *input.pad().l(),
9 => *input.pad().r(),
10 => *input.pad().start(),
11 => *input.pad().select(),
_ => {
return Err(IntrinsicExecutionError::InvalidBuiltinCarrier {
owner: "input.button",

View File

@ -1,5 +1,5 @@
use prometeu_drivers::hardware::Hardware;
use prometeu_firmware::{BootTarget, Firmware};
use prometeu_hal::RuntimePlatform;
use prometeu_hal::cartridge_loader::CartridgeLoader;
use prometeu_hal::debugger_protocol::*;
use prometeu_hal::telemetry::{CertificationConfig, TelemetryFrame};
@ -99,7 +99,7 @@ impl HostDebugger {
/// Main maintenance method called by the HostRunner every iteration.
/// It handles new connections and processes incoming commands.
pub fn check_commands(&mut self, firmware: &mut Firmware, hardware: &mut Hardware) {
pub fn check_commands(&mut self, firmware: &mut Firmware, platform: &mut dyn RuntimePlatform) {
// 1. Accept new client connections.
if let Some(listener) = &self.listener
&& let Ok((stream, _addr)) = listener.accept()
@ -154,7 +154,7 @@ impl HostDebugger {
continue;
}
if let Ok(cmd) = serde_json::from_str::<DebugCommand>(trimmed) {
self.handle_command(cmd, firmware, hardware);
self.handle_command(cmd, firmware, platform);
}
}
}
@ -182,7 +182,7 @@ impl HostDebugger {
&mut self,
cmd: DebugCommand,
firmware: &mut Firmware,
hardware: &mut Hardware,
platform: &mut dyn RuntimePlatform,
) {
match cmd {
DebugCommand::Ok | DebugCommand::Start => {
@ -201,7 +201,7 @@ impl HostDebugger {
DebugCommand::Step => {
// Execute exactly one instruction and keep paused.
firmware.os.vm().set_paused(true);
let _ = firmware.os.vm().debug_step_instruction(&mut firmware.vm, hardware);
let _ = firmware.os.vm().debug_step_instruction(&mut firmware.vm, platform);
}
DebugCommand::StepFrame => {
// Execute until the end of the current logical frame.
@ -372,7 +372,7 @@ impl HostDebugger {
#[cfg(test)]
mod tests {
use super::*;
use prometeu_drivers::hardware::Hardware;
use prometeu_drivers::TestPlatform;
use prometeu_hal::debugger_protocol::DebugCommand;
#[test]
@ -396,15 +396,19 @@ mod tests {
fn handle_command_updates_pause_and_step_flags_without_host_io() {
let mut debugger = HostDebugger::new();
let mut firmware = Firmware::new(None);
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
debugger.handle_command(DebugCommand::Pause, &mut firmware, &mut hardware);
debugger.handle_command(DebugCommand::Pause, &mut firmware, platform.local_hardware_mut());
assert!(firmware.os.vm().paused());
debugger.handle_command(DebugCommand::Resume, &mut firmware, &mut hardware);
debugger.handle_command(DebugCommand::Resume, &mut firmware, platform.local_hardware_mut());
assert!(!firmware.os.vm().paused());
debugger.handle_command(DebugCommand::StepFrame, &mut firmware, &mut hardware);
debugger.handle_command(
DebugCommand::StepFrame,
&mut firmware,
platform.local_hardware_mut(),
);
assert!(!firmware.os.vm().paused());
assert!(firmware.os.vm().debug_step_requested());
}
@ -413,12 +417,12 @@ mod tests {
fn handle_command_start_leaves_waiting_for_start_mode() {
let mut debugger = HostDebugger::new();
let mut firmware = Firmware::new(None);
let mut hardware = Hardware::new();
let mut platform = TestPlatform::new();
debugger.waiting_for_start = true;
firmware.os.vm().set_paused(true);
debugger.handle_command(DebugCommand::Start, &mut firmware, &mut hardware);
debugger.handle_command(DebugCommand::Start, &mut firmware, platform.local_hardware_mut());
assert!(!debugger.waiting_for_start);
assert!(!firmware.os.vm().paused());

View File

@ -1,4 +1,3 @@
use prometeu_drivers::hardware::Hardware;
use prometeu_hal::InputSignals;
use winit::event::{ElementState, MouseButton, WindowEvent};
use winit::keyboard::{KeyCode, PhysicalKey};
@ -14,7 +13,7 @@ struct FbViewport {
}
impl FbViewport {
fn framebuffer_viewport(window: &Window) -> Option<FbViewport> {
fn framebuffer_viewport(window: &Window, display_size: (usize, usize)) -> Option<FbViewport> {
let size = window.inner_size();
let win_w = size.width as f32;
@ -24,8 +23,8 @@ impl FbViewport {
return None;
}
let fb_w = Hardware::W as f32;
let fb_h = Hardware::H as f32;
let fb_w = display_size.0 as f32;
let fb_h = display_size.1 as f32;
let scale_x = (win_w / fb_w).floor();
let scale_y = (win_h / fb_h).floor();
@ -44,17 +43,18 @@ impl FbViewport {
pub struct HostInputHandler {
pub signals: InputSignals,
display_size: (usize, usize),
}
impl Default for HostInputHandler {
fn default() -> Self {
Self::new()
Self::new((480, 270))
}
}
impl HostInputHandler {
pub fn new() -> Self {
Self { signals: InputSignals::default() }
pub fn new(display_size: (usize, usize)) -> Self {
Self { signals: InputSignals::default(), display_size }
}
pub fn handle_event(&mut self, event: &WindowEvent, window: &Window) {
@ -87,7 +87,9 @@ impl HostInputHandler {
}
WindowEvent::CursorMoved { position, .. } => {
if let Some((x, y)) = window_to_fb(position.x as f32, position.y as f32, window) {
if let Some((x, y)) =
window_to_fb(position.x as f32, position.y as f32, window, self.display_size)
{
self.signals.x_pos = x;
self.signals.y_pos = y;
}
@ -107,8 +109,13 @@ impl HostInputHandler {
_ => {}
}
fn window_to_fb(wx: f32, wy: f32, window: &Window) -> Option<(i32, i32)> {
let viewport = FbViewport::framebuffer_viewport(window)?;
fn window_to_fb(
wx: f32,
wy: f32,
window: &Window,
display_size: (usize, usize),
) -> Option<(i32, i32)> {
let viewport = FbViewport::framebuffer_viewport(window, display_size)?;
let local_x = wx - viewport.x;
let local_y = wy - viewport.y;
@ -120,7 +127,10 @@ impl HostInputHandler {
let fb_x = (local_x / viewport.scale).floor() as i32;
let fb_y = (local_y / viewport.scale).floor() as i32;
Some((fb_x.clamp(0, Hardware::W as i32 - 1), fb_y.clamp(0, Hardware::H as i32 - 1)))
Some((
fb_x.clamp(0, display_size.0 as i32 - 1),
fb_y.clamp(0, display_size.1 as i32 - 1),
))
}
}
}

View File

@ -10,6 +10,7 @@ use pixels::{Pixels, PixelsBuilder, SurfaceTexture};
use prometeu_drivers::AudioCommand;
use prometeu_drivers::hardware::Hardware;
use prometeu_firmware::{BootTarget, Firmware, FirmwareState};
use prometeu_hal::RuntimePlatform;
use prometeu_hal::telemetry::CertificationConfig;
use std::time::{Duration, Instant};
use winit::application::ApplicationHandler;
@ -152,12 +153,15 @@ impl HostRunner {
firmware.os.fs().mount(Box::new(backend));
}
let hardware = Hardware::new();
let display_size = hardware.display_size();
Self {
window: None,
pixels: None,
hardware: Hardware::new(),
hardware,
firmware,
input: HostInputHandler::new(),
input: HostInputHandler::new(display_size),
fs_root,
log_sink: HostConsoleSink::new(),
frame_target_dt: Duration::from_nanos(1_000_000_000 / target_fps),
@ -228,11 +232,11 @@ impl ApplicationHandler for HostRunner {
let size = window.inner_size();
let surface_texture = SurfaceTexture::new(size.width, size.height, window);
let mut pixels =
PixelsBuilder::new(Hardware::W as u32, Hardware::H as u32, surface_texture)
.present_mode(PresentMode::Fifo) // activate vsync
.build()
.expect("failed to create Pixels");
let (display_w, display_h) = self.hardware.display_size();
let mut pixels = PixelsBuilder::new(display_w as u32, display_h as u32, surface_texture)
.present_mode(PresentMode::Fifo) // activate vsync
.build()
.expect("failed to create Pixels");
pixels.frame_mut().fill(0);
@ -381,7 +385,7 @@ impl ApplicationHandler for HostRunner {
self.audio.update_stats(&mut self.stats);
// Update technical statistics displayed in the window title.
self.stats.update(now, self.window, &self.hardware, &mut self.firmware);
self.stats.update(now, self.window, &mut self.firmware);
// Synchronize system logs to the host console.
let last_seq = self.log_sink.last_seq().unwrap_or(u64::MAX);

View File

@ -1,4 +1,3 @@
use prometeu_drivers::hardware::Hardware;
use prometeu_firmware::Firmware;
use std::time::{Duration, Instant};
use winit::window::Window;
@ -60,13 +59,7 @@ impl HostStats {
}
}
pub fn update(
&mut self,
now: Instant,
window: Option<&Window>,
_hardware: &Hardware,
firmware: &mut Firmware,
) {
pub fn update(&mut self, now: Instant, window: Option<&Window>, firmware: &mut Firmware) {
let stats_elapsed = now.duration_since(self.last_stats_update);
if stats_elapsed >= Duration::from_secs(1) {
self.current_fps = self.frames_since_last_update as f64 / stats_elapsed.as_secs_f64();

View File

@ -31,6 +31,13 @@ MUST contain a frame id, the active app mode, and one typed packet:
immutable. The runtime backpressure policy is latest-complete-submission-wins;
it MUST NOT accumulate an unbounded frame queue.
The platform layer is the runtime-facing implementation boundary for render
handoff. Runtime and firmware code publish completed frames through typed
platform services such as `RenderSubmissionSink`, and they access Game 2D
composition through `Game2DFrameComposer`. They MUST NOT depend on a monolithic
hardware bridge, mutable `Hardware`, mutable `Gfx`, or live `FrameComposer`
reference as the render handoff contract.
The current 2D graphics model is based on:
- framebuffer
@ -127,6 +134,10 @@ to the logical/runtime side before handoff, or to an owning service. They MUST
NOT require the render consumer to hold mutable VM, `Hardware`, `Gfx`, or
`FrameComposer` references.
Local host implementations may keep a concrete hardware object internally as a
platform implementation detail. That object is not part of the runtime-facing
render boundary; the boundary is the typed platform service set.
The runtime does not guarantee visual integrity if a developer or framework
replaces resources behind an in-flight submission in a way that violates asset
discipline.

View File

@ -123,9 +123,13 @@ The graphics system:
The platform layer:
- exposes typed service facades for render submission, render backend
execution, Game 2D composition, input, audio, assets, and telemetry
- consumes closed render submissions through a render-surface implementation
- transports published RGBA8888 output into a host presentation surface without
injecting host-owned debug overlay pixels
- is the runtime-facing portability boundary; runtime and firmware code must not
depend on a monolithic hardware bridge or on a concrete hardware aggregate
The host presentation layer MUST treat RGBA8888 as the canonical logical
framebuffer format. RGB565 conversion is not part of the normal host

View File

@ -50,6 +50,12 @@ Does not belong in `docs/runtime/specs/` as the primary canonical source:
Here, hardware is not a generic bucket for everything. In the current package, "hardware" means the machine's virtual peripherals: GFX, AUDIO, INPUT, TOUCH, and MEMCARD/save memory. VM, firmware, cartridge, and ABI sit in sibling categories rather than inside hardware.
The implementation boundary for host portability is the platform layer. Runtime,
firmware, and host integration code use typed platform services for rendering,
input, audio, assets, and telemetry. A concrete local hardware aggregate may
exist inside a host or test platform, but it is not the normative runtime-facing
contract.
## Document Functions
- `normative`: defines the technical contract, expected behavior, or implementation-facing surface.