bQUARKz 9cf9fa6163
All checks were successful
Intrepid/Prometeu/Runtime/pipeline/head This commit looks good
Intrepid/Prometeu/Runtime/pipeline/pr-master This commit looks good
implements PLN-0109
2026-06-15 10:47:34 +01:00

434 lines
14 KiB
Rust

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 {
/// 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<MemoryBanks>) -> Self {
let assets = AssetManager::new(
vec![],
AssetsPayloadSource::empty(),
Arc::clone(&memory_banks) as Arc<dyn GlyphBankPoolInstaller>,
Arc::clone(&memory_banks) as Arc<dyn SoundBankPoolInstaller>,
Arc::clone(&memory_banks) as Arc<dyn SceneBankPoolInstaller>,
);
let glyph_slot_index = assets.glyph_asset_slot_index();
Self {
gfx: Gfx::new(
Self::W,
Self::H,
Arc::clone(&memory_banks) as Arc<dyn GlyphBankPoolAccess>,
),
frame_composer: FrameComposer::new(
Self::W,
Self::H,
Arc::clone(&memory_banks) as Arc<dyn SceneBankPoolAccess>,
glyph_slot_index,
),
audio: Audio::new(Arc::clone(&memory_banks) as Arc<dyn SoundBankPoolAccess>),
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());
}
}