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

This commit is contained in:
bQUARKz 2026-06-15 10:47:34 +01:00
parent 117a18ee72
commit 9cf9fa6163
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
11 changed files with 60 additions and 351 deletions

View File

@ -11,8 +11,8 @@ use crate::touch::Touch;
use prometeu_hal::cartridge::AssetsPayloadSource;
use prometeu_hal::sprite::Sprite;
use prometeu_hal::{
AssetBridge, AudioBridge, GfxBridge, HardwareBridge, InputPlatform, NoopTelemetryPlatform,
PadBridge, RenderBackend, RuntimePlatform, TelemetryPlatform, TouchBridge,
AssetBridge, AudioBridge, InputPlatform, NoopTelemetryPlatform, PadBridge, RenderBackend,
RuntimePlatform, TelemetryPlatform, TouchBridge,
};
use prometeu_hal::{
Game2DFrameComposer, Game2DFramePacket, RenderSubmission, RenderSubmissionPacket,
@ -53,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() {
@ -94,54 +70,6 @@ impl HardwareBridge for Hardware {
}
self.gfx.present();
}
fn render_frame(&mut self) {
if self.frame_composer.active_scene_id().is_none() {
let packet = self.frame_composer.close_game2d_packet();
self.gfx.render_game2d_frame_packet(&packet);
} else {
self.frame_composer.render_frame(&mut self.gfx);
}
}
fn has_glyph_bank(&self, bank_id: usize) -> bool {
self.gfx.glyph_banks.glyph_bank_slot(bank_id).is_some()
}
fn gfx(&self) -> &dyn GfxBridge {
&self.gfx
}
fn gfx_mut(&mut self) -> &mut dyn GfxBridge {
&mut self.gfx
}
fn audio(&self) -> &dyn AudioBridge {
&self.audio
}
fn audio_mut(&mut self) -> &mut dyn AudioBridge {
&mut self.audio
}
fn pad(&self) -> &dyn PadBridge {
&self.pad
}
fn pad_mut(&mut self) -> &mut dyn PadBridge {
&mut self.pad
}
fn touch(&self) -> &dyn TouchBridge {
&self.touch
}
fn touch_mut(&mut self) -> &mut dyn TouchBridge {
&mut self.touch
}
fn assets(&self) -> &dyn AssetBridge {
&self.assets
}
fn assets_mut(&mut self) -> &mut dyn AssetBridge {
&mut self.assets
}
}
impl RenderSubmissionSink for Hardware {
@ -149,14 +77,19 @@ impl RenderSubmissionSink for Hardware {
&mut self,
submission: RenderSubmission,
) -> Result<(), RenderSubmitError> {
self.publish_render_submission(&submission);
self.consume_render_submission(&submission);
Ok(())
}
}
impl RenderBackend for Hardware {
fn render_frame(&mut self) {
HardwareBridge::render_frame(self);
if self.frame_composer.active_scene_id().is_none() {
let packet = self.frame_composer.close_game2d_packet();
self.gfx.render_game2d_frame_packet(&packet);
} else {
self.frame_composer.render_frame(&mut self.gfx);
}
}
}
@ -183,7 +116,7 @@ impl Game2DFrameComposer for Hardware {
}
fn emit_sprite(&mut self, sprite: Sprite) -> prometeu_hal::ComposerOpStatus {
if !self.has_glyph_bank(sprite.bank_id as usize) {
if self.gfx.glyph_banks.glyph_bank_slot(sprite.bank_id as usize).is_none() {
return prometeu_hal::ComposerOpStatus::BankInvalid;
}
@ -406,9 +339,12 @@ 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!(HardwareBridge::bind_scene(&mut hardware, 0));
assert_eq!(
Game2DFrameComposer::bind_scene(&mut hardware, 0),
prometeu_hal::ComposerOpStatus::Ok
);
let mut packet = HardwareBridge::close_game2d_packet(&hardware);
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,
@ -433,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!(HardwareBridge::bind_scene(&mut hardware, 0));
assert_eq!(
Game2DFrameComposer::bind_scene(&mut hardware, 0),
prometeu_hal::ComposerOpStatus::Ok
);
let packet = Game2DFramePacket::new(
ComposerFramePacket {
@ -455,7 +394,7 @@ mod tests {
Vec::new(),
);
HardwareBridge::begin_frame(&mut hardware);
Game2DFrameComposer::begin_frame(&mut hardware);
RenderSubmissionSink::submit_render_submission(
&mut hardware,
RenderSubmission::game2d(FrameId::ZERO, packet),

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,27 +1,17 @@
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, platform: None }
pub fn new(platform: Option<&'a mut dyn RuntimePlatform>) -> Self {
Self { platform }
}
pub fn new_platform(platform: Option<&'a mut dyn RuntimePlatform>) -> Self {
Self { hw: None, platform }
}
#[inline]
pub fn require_hw(&mut self) -> Result<&mut dyn HardwareBridge, VmFault> {
match &mut self.hw {
Some(hw) => Ok(*hw),
None => Err(VmFault::Unavailable),
}
Self::new(platform)
}
#[inline]

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,9 +47,8 @@ pub use native_interface::{NativeInterface, SyscallId};
pub use pad_bridge::PadBridge;
pub use platform::{
AssetStoragePlatform, AudioPlatform, ClockPacingPlatform, Game2DFrameComposer, InputPlatform,
LegacyHardwareGame2DFrameComposer, LegacyHardwareRenderSubmissionSink,
LegacyHardwareRuntimePlatform, NoopTelemetryPlatform, 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

@ -1,7 +1,6 @@
use crate::asset_bridge::AssetBridge;
use crate::audio_bridge::AudioBridge;
use crate::composer_status::ComposerOpStatus;
use crate::hardware_bridge::HardwareBridge;
use crate::pad_bridge::PadBridge;
use crate::render_submission::{Game2DFramePacket, RenderSubmission};
use crate::sprite::Sprite;
@ -21,26 +20,6 @@ pub trait RenderSubmissionSink {
) -> Result<(), RenderSubmitError>;
}
pub struct LegacyHardwareRenderSubmissionSink<'a> {
hw: &'a mut dyn HardwareBridge,
}
impl<'a> LegacyHardwareRenderSubmissionSink<'a> {
pub fn new(hw: &'a mut dyn HardwareBridge) -> Self {
Self { hw }
}
}
impl RenderSubmissionSink for LegacyHardwareRenderSubmissionSink<'_> {
fn submit_render_submission(
&mut self,
submission: RenderSubmission,
) -> Result<(), RenderSubmitError> {
self.hw.publish_render_submission(&submission);
Ok(())
}
}
pub trait RenderBackend {
fn render_frame(&mut self);
}
@ -54,138 +33,6 @@ pub trait Game2DFrameComposer {
fn close_game2d_packet(&self) -> Game2DFramePacket;
}
pub struct LegacyHardwareGame2DFrameComposer<'a> {
hw: &'a mut dyn HardwareBridge,
}
impl<'a> LegacyHardwareGame2DFrameComposer<'a> {
pub fn new(hw: &'a mut dyn HardwareBridge) -> Self {
Self { hw }
}
}
impl Game2DFrameComposer for LegacyHardwareGame2DFrameComposer<'_> {
fn begin_frame(&mut self) {
self.hw.begin_frame();
}
fn bind_scene(&mut self, scene_bank_id: usize) -> ComposerOpStatus {
if self.hw.bind_scene(scene_bank_id) {
ComposerOpStatus::Ok
} else {
ComposerOpStatus::SceneUnavailable
}
}
fn unbind_scene(&mut self) {
self.hw.unbind_scene();
}
fn set_camera(&mut self, x: i32, y: i32) -> ComposerOpStatus {
self.hw.set_camera(x, y);
ComposerOpStatus::Ok
}
fn emit_sprite(&mut self, sprite: Sprite) -> ComposerOpStatus {
if !self.hw.has_glyph_bank(sprite.bank_id as usize) {
return ComposerOpStatus::BankInvalid;
}
if self.hw.emit_sprite(sprite) {
ComposerOpStatus::Ok
} else {
ComposerOpStatus::SpriteOverflow
}
}
fn close_game2d_packet(&self) -> Game2DFramePacket {
self.hw.close_game2d_packet()
}
}
pub struct LegacyHardwareRuntimePlatform<'a> {
hw: &'a mut dyn HardwareBridge,
}
impl<'a> LegacyHardwareRuntimePlatform<'a> {
pub fn new(hw: &'a mut dyn HardwareBridge) -> Self {
Self { hw }
}
}
impl RenderSubmissionSink for LegacyHardwareRuntimePlatform<'_> {
fn submit_render_submission(
&mut self,
submission: RenderSubmission,
) -> Result<(), RenderSubmitError> {
self.hw.publish_render_submission(&submission);
Ok(())
}
}
impl RenderBackend for LegacyHardwareRuntimePlatform<'_> {
fn render_frame(&mut self) {
self.hw.render_frame();
}
}
impl Game2DFrameComposer for LegacyHardwareRuntimePlatform<'_> {
fn begin_frame(&mut self) {
self.hw.begin_frame();
}
fn bind_scene(&mut self, scene_bank_id: usize) -> ComposerOpStatus {
if self.hw.bind_scene(scene_bank_id) {
ComposerOpStatus::Ok
} else {
ComposerOpStatus::SceneUnavailable
}
}
fn unbind_scene(&mut self) {
self.hw.unbind_scene();
}
fn set_camera(&mut self, x: i32, y: i32) -> ComposerOpStatus {
self.hw.set_camera(x, y);
ComposerOpStatus::Ok
}
fn emit_sprite(&mut self, sprite: Sprite) -> ComposerOpStatus {
if !self.hw.has_glyph_bank(sprite.bank_id as usize) {
return ComposerOpStatus::BankInvalid;
}
if self.hw.emit_sprite(sprite) {
ComposerOpStatus::Ok
} else {
ComposerOpStatus::SpriteOverflow
}
}
fn close_game2d_packet(&self) -> Game2DFramePacket {
self.hw.close_game2d_packet()
}
}
impl InputPlatform for LegacyHardwareRuntimePlatform<'_> {
fn pad(&self) -> &dyn PadBridge {
self.hw.pad()
}
fn pad_mut(&mut self) -> &mut dyn PadBridge {
self.hw.pad_mut()
}
fn touch(&self) -> &dyn TouchBridge {
self.hw.touch()
}
fn touch_mut(&mut self) -> &mut dyn TouchBridge {
self.hw.touch_mut()
}
}
pub trait AudioPlatform: AudioBridge {}
impl<T: AudioBridge + ?Sized> AudioPlatform for T {}
@ -212,8 +59,6 @@ pub struct NoopTelemetryPlatform;
impl TelemetryPlatform for NoopTelemetryPlatform {}
static LEGACY_NOOP_TELEMETRY: NoopTelemetryPlatform = NoopTelemetryPlatform;
pub trait RuntimePlatform {
fn display_size(&self) -> (usize, usize) {
(480, 270)
@ -231,48 +76,6 @@ pub trait RuntimePlatform {
fn telemetry(&self) -> &dyn TelemetryPlatform;
}
impl RuntimePlatform for LegacyHardwareRuntimePlatform<'_> {
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.hw.audio()
}
fn audio_mut(&mut self) -> &mut dyn AudioBridge {
self.hw.audio_mut()
}
fn input(&self) -> &dyn InputPlatform {
self
}
fn input_mut(&mut self) -> &mut dyn InputPlatform {
self
}
fn assets(&self) -> &dyn AssetBridge {
self.hw.assets()
}
fn assets_mut(&mut self) -> &mut dyn AssetBridge {
self.hw.assets_mut()
}
fn telemetry(&self) -> &dyn TelemetryPlatform {
&LEGACY_NOOP_TELEMETRY
}
}
pub trait TestPlatform: RuntimePlatform {}
impl<T: RuntimePlatform + ?Sized> TestPlatform for T {}

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, RuntimePlatform};
use prometeu_hal::{InputSignals, RuntimePlatform};
use prometeu_vm::VirtualMachine;
use std::sync::atomic::Ordering;
@ -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

@ -9,9 +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, LegacyHardwareRuntimePlatform,
RenderSubmission, RenderSubmissionPacket, RenderSubmissionSink, RuntimePlatform,
ShellUiFramePacket,
FrameId, HostContext, InputSignals, RenderSubmission, RenderSubmissionPacket,
RenderSubmissionSink, RuntimePlatform, ShellUiFramePacket,
};
use prometeu_vm::LogicalFrameEndingReason;
use std::collections::HashMap;
@ -77,11 +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 platform = LegacyHardwareRuntimePlatform::new(hw);
let mut ctx = HostContext::new_platform(Some(&mut platform));
let mut ctx = HostContext::new_platform(Some(platform));
let mut fs = VirtualFS::new();
let mut fs_state = FsState::Unmounted;
let mut memcard = MemcardService::new();

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.

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.