use crate::asset::AssetManager; use crate::audio::Audio; use crate::frame_composer::FrameComposer; use crate::gfx::Gfx; use crate::memory_banks::{ GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolAccess, SceneBankPoolInstaller, SoundBankPoolAccess, SoundBankPoolInstaller, }; use crate::pad::Pad; use crate::touch::Touch; use prometeu_hal::cartridge::AssetsPayloadSource; use prometeu_hal::sprite::Sprite; 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; /// Aggregate structure for all virtual hardware peripherals. /// /// This struct represents the "Mainboard" of the PROMETEU console. /// It acts as a container for all hardware subsystems. In the Prometeu /// architecture, hardware is decoupled from the OS and VM, allowing /// for easier testing and different host implementations (Desktop, Web, etc.). /// /// ### Console Specifications: /// - **Resolution**: 480x270 (16:9 Aspect Ratio). /// - **Color Format**: RGBA8888. /// - **Audio**: Stereo, Command-based mixing. /// - **Input**: 12-button Digital Gamepad + Absolute Touch/Mouse. pub struct Hardware { /// Shared physical memory bank ownership used by peripherals and worker backends. pub memory_banks: Arc, /// The Graphics Processing Unit (GPU). Handles drawing primitives, sprites, and tilemaps. pub gfx: Gfx, /// Canonical frame orchestration owner for scene/camera/cache/resolver/sprites. pub frame_composer: FrameComposer, /// The Sound Processing Unit (SPU). Manages sample playback and volume control. pub audio: Audio, /// The standard digital gamepad. Provides state for D-Pad, face buttons, and triggers. pub pad: Pad, /// The absolute pointer input device (Mouse/Touchscreen). pub touch: Touch, /// The Asset Management system (DMA). Handles loading data into VRAM (GlyphBanks) and ARAM (SoundBanks). pub assets: AssetManager, } impl Default for Hardware { fn default() -> Self { Self::new() } } impl Hardware { fn consume_render_submission(&mut self, submission: &RenderSubmission) { match &submission.packet { RenderSubmissionPacket::Game2D(packet) => { if packet.composer.bound_scene.is_none() { self.gfx.render_game2d_frame_packet(packet); } else { self.frame_composer.render_game2d_frame_packet(packet, &mut self.gfx); self.gfx.apply_gfx2d_commands(&packet.gfx2d); } } RenderSubmissionPacket::ShellUi(packet) => { self.gfx.render_shell_ui_frame_packet(packet); } } 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(); self.gfx.render_game2d_frame_packet(&packet); } else { self.frame_composer.render_frame(&mut self.gfx); } } } impl Game2DFrameComposer for Hardware { fn begin_frame(&mut self) { self.frame_composer.begin_frame(); } 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 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 } 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 } fn telemetry(&self) -> &dyn TelemetryPlatform { &NOOP_TELEMETRY } } impl Hardware { /// Internal hardware width in pixels. pub const W: usize = 480; /// Internal hardware height in pixels. pub const H: usize = 270; /// Creates a fresh hardware instance with default settings. pub fn new() -> Self { Self::new_with_memory_banks(Arc::new(MemoryBanks::new())) } /// Creates hardware with explicit shared bank ownership. pub fn new_with_memory_banks(memory_banks: Arc) -> Self { let assets = AssetManager::new( vec![], AssetsPayloadSource::empty(), Arc::clone(&memory_banks) as Arc, Arc::clone(&memory_banks) as Arc, Arc::clone(&memory_banks) as Arc, ); let glyph_slot_index = assets.glyph_asset_slot_index(); Self { memory_banks: Arc::clone(&memory_banks), gfx: Gfx::new( Self::W, Self::H, Arc::clone(&memory_banks) as Arc, ), frame_composer: FrameComposer::new( Self::W, Self::H, Arc::clone(&memory_banks) as Arc, glyph_slot_index, ), audio: Audio::new(Arc::clone(&memory_banks) as Arc), pad: Pad::default(), touch: Touch::default(), assets, } } } #[cfg(test)] mod tests { use super::*; use crate::memory_banks::{ GlyphBankPoolInstaller, SceneBankPoolAccess, SceneBankPoolInstaller, }; use prometeu_hal::color::Color; use prometeu_hal::glyph::Glyph; use prometeu_hal::glyph_bank::{GlyphBank, TileSize}; use prometeu_hal::primitives::Rect; use prometeu_hal::scene_bank::SceneBank; use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer}; use prometeu_hal::scene_viewport_cache::SceneViewportCache; use prometeu_hal::scene_viewport_resolver::SceneViewportResolver; use prometeu_hal::tile::Tile; use prometeu_hal::tilemap::TileMap; use prometeu_hal::{ BoundScenePacket, Camera2D, ComposerFramePacket, FrameId, GameSpritePacket, Gfx2dCommand, GfxUiCommand, ShellUiFramePacket, }; fn make_glyph_bank() -> GlyphBank { let mut bank = GlyphBank::new(TileSize::Size8, 8, 8); bank.palettes[0][1] = Color::RED; for pixel in &mut bank.pixel_indices { *pixel = 1; } bank } fn make_scene() -> SceneBank { make_scene_with_glyph_asset_id(1) } fn make_scene_with_glyph_asset_id(glyph_asset_id: i32) -> SceneBank { let layer = SceneLayer { active: true, glyph_asset_id, tile_size: TileSize::Size8, parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 }, tilemap: TileMap { width: 4, height: 4, tiles: vec![ Tile { active: true, glyph: Glyph { glyph_id: 0, palette_id: 0 }, flip_x: false, flip_y: false, }; 16 ], }, }; SceneBank { layers: std::array::from_fn(|_| layer.clone()) } } #[test] fn hardware_can_render_scene_from_shared_scene_bank_pipeline() { let banks = Arc::new(MemoryBanks::new()); banks.install_glyph_bank(0, Arc::new(make_glyph_bank())); banks.install_scene_bank(0, Arc::new(make_scene_with_glyph_asset_id(0))); let mut hardware = Hardware::new_with_memory_banks(Arc::clone(&banks)); let scene = banks.scene_bank_slot(0).expect("scene bank slot 0 should be resident"); let mut cache = SceneViewportCache::new(&scene, 4, 4); cache.materialize_all_layers(&scene); let mut resolver = SceneViewportResolver::new(16, 16, 4, 4, 12, 20); let update = resolver.update(&scene, 0, 0); hardware.gfx.render_scene_from_cache(&cache, &update, &[0, 0, 0, 0]); hardware.gfx.present(); assert_eq!(hardware.gfx.front_buffer()[0], Color::RED.raw()); } #[test] fn hardware_constructs_frame_composer_with_shared_scene_bank_access() { let banks = Arc::new(MemoryBanks::new()); banks.install_scene_bank(2, Arc::new(make_scene())); let hardware = Hardware::new_with_memory_banks(banks); let scene = hardware .frame_composer .scene_bank_slot(2) .expect("scene bank slot 2 should be resident"); assert_eq!(hardware.frame_composer.viewport_size(), (Hardware::W, Hardware::H)); assert_eq!(hardware.frame_composer.scene_bank_slot_count(), 16); assert_eq!(scene.layers[0].tile_size, TileSize::Size8); } #[test] fn publish_game2d_submission_applies_gfx2d_overlay_after_active_scene() { let banks = Arc::new(MemoryBanks::new()); banks.install_glyph_bank(0, Arc::new(make_glyph_bank())); banks.install_scene_bank(0, Arc::new(make_scene_with_glyph_asset_id(0))); let mut hardware = Hardware::new_with_memory_banks(banks); let mut glyph_slots = [None; 16]; glyph_slots[0] = Some(0); hardware.assets.glyph_asset_slot_index().rebuild_from_slots(&glyph_slots); assert_eq!( Game2DFrameComposer::bind_scene(&mut hardware, 0), prometeu_hal::ComposerOpStatus::Ok ); 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, }]; 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()); } #[test] fn publish_game2d_submission_uses_packet_scene_and_sprites_not_live_frame_state() { let banks = Arc::new(MemoryBanks::new()); banks.install_glyph_bank(0, Arc::new(make_glyph_bank())); banks.install_scene_bank(0, Arc::new(make_scene_with_glyph_asset_id(0))); let mut hardware = Hardware::new_with_memory_banks(banks); let mut glyph_slots = [None; 16]; glyph_slots[0] = Some(0); hardware.assets.glyph_asset_slot_index().rebuild_from_slots(&glyph_slots); assert_eq!( Game2DFrameComposer::bind_scene(&mut hardware, 0), prometeu_hal::ComposerOpStatus::Ok ); let packet = Game2DFramePacket::new( ComposerFramePacket { bound_scene: Some(BoundScenePacket { bank_id: 0 }), camera: Camera2D::default(), sprites: vec![GameSpritePacket { glyph_id: 0, palette_id: 0, x: 0, y: 0, layer: 0, bank_id: 0, flip_x: false, flip_y: false, priority: 0, }], hud: Default::default(), }, Vec::new(), ); 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()); } #[test] fn render_submission_sink_accepts_owned_game2d_submission() { let mut hardware = Hardware::new(); let packet = Game2DFramePacket::new( ComposerFramePacket::default(), vec![Gfx2dCommand::Clear { color: Color::BLUE }], ); let submission = RenderSubmission::game2d(FrameId::ZERO, packet); RenderSubmissionSink::submit_render_submission(&mut hardware, submission) .expect("local sink should accept owned Game2D submissions"); assert_eq!(hardware.gfx.front_buffer()[0], Color::BLUE.raw()); } #[test] fn render_submission_sink_accepts_owned_shell_ui_submission() { let mut hardware = Hardware::new(); let packet = ShellUiFramePacket::new(vec![GfxUiCommand::Clear { color: Color::RED }]); let submission = RenderSubmission::shell_ui(FrameId::ZERO, packet); RenderSubmissionSink::submit_render_submission(&mut hardware, submission) .expect("local sink should accept owned ShellUi submissions"); assert_eq!(hardware.gfx.front_buffer()[0], Color::RED.raw()); } }