434 lines
14 KiB
Rust
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());
|
|
}
|
|
}
|