added comments extensively

This commit is contained in:
bQUARKz 2026-01-19 07:47:54 +00:00
parent 9ba2aab1a5
commit 075fe8526c
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
10 changed files with 446 additions and 104 deletions

View File

@ -9,16 +9,28 @@ use crate::virtual_machine::VirtualMachine;
use crate::telemetry::CertificationConfig; use crate::telemetry::CertificationConfig;
/// PROMETEU Firmware.
///
/// The central orchestrator of the system. It manages the high-level state machine,
/// transitioning between system states like booting, the Hub launcher, and
/// actual application execution.
pub struct Firmware { pub struct Firmware {
/// The Virtual Machine instance.
pub vm: VirtualMachine, pub vm: VirtualMachine,
/// The System Operating logic (syscalls, telemetry, logs).
pub os: PrometeuOS, pub os: PrometeuOS,
/// The System UI / Launcher environment.
pub hub: PrometeuHub, pub hub: PrometeuHub,
/// Current high-level state of the system.
pub state: FirmwareState, pub state: FirmwareState,
/// Desired execution target resolved at boot.
pub boot_target: BootTarget, pub boot_target: BootTarget,
/// Tracking flag to ensure `on_enter` is called exactly once per state transition.
state_initialized: bool, state_initialized: bool,
} }
impl Firmware { impl Firmware {
/// Initializes the firmware in the `Reset` state.
pub fn new(cap_config: Option<CertificationConfig>) -> Self { pub fn new(cap_config: Option<CertificationConfig>) -> Self {
Self { Self {
vm: VirtualMachine::default(), vm: VirtualMachine::default(),
@ -30,31 +42,40 @@ impl Firmware {
} }
} }
/// The main entry point for the Host to advance the system logic.
///
/// This method is called exactly once per Host frame (60Hz).
/// It updates peripheral signals and delegates the logic to the current state.
pub fn step_frame(&mut self, signals: &InputSignals, hw: &mut dyn HardwareBridge) { pub fn step_frame(&mut self, signals: &InputSignals, hw: &mut dyn HardwareBridge) {
// Updates input once per host frame // 1. Update 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.pad_mut().begin_frame(signals);
hw.touch_mut().begin_frame(signals); hw.touch_mut().begin_frame(signals);
// 2. State machine lifecycle management.
if !self.state_initialized { if !self.state_initialized {
self.on_enter(signals, hw); self.on_enter(signals, hw);
self.state_initialized = true; self.state_initialized = true;
} }
// 3. Update the current state and check for transitions.
if let Some(next_state) = self.on_update(signals, hw) { if let Some(next_state) = self.on_update(signals, hw) {
self.change_state(next_state, signals, hw); self.change_state(next_state, signals, hw);
} }
} }
/// Transitions the system to a new state, handling lifecycle hooks.
pub fn change_state(&mut self, new_state: FirmwareState, signals: &InputSignals, hw: &mut dyn HardwareBridge) { pub fn change_state(&mut self, new_state: FirmwareState, signals: &InputSignals, hw: &mut dyn HardwareBridge) {
self.on_exit(signals, hw); self.on_exit(signals, hw);
self.state = new_state; self.state = new_state;
self.state_initialized = false; self.state_initialized = false;
// Enters the new state immediately // Enter the new state immediately to avoid "empty" frames during transitions.
self.on_enter(signals, hw); self.on_enter(signals, hw);
self.state_initialized = true; 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, hw: &mut dyn HardwareBridge) {
let mut req = PrometeuContext { let mut req = PrometeuContext {
vm: &mut self.vm, vm: &mut self.vm,
@ -75,6 +96,8 @@ impl Firmware {
} }
} }
/// Dispatches the `on_update` event to the current state implementation.
/// Returns an optional `FirmwareState` if a transition is requested.
fn on_update(&mut self, signals: &InputSignals, hw: &mut dyn HardwareBridge) -> Option<FirmwareState> { fn on_update(&mut self, signals: &InputSignals, hw: &mut dyn HardwareBridge) -> Option<FirmwareState> {
let mut req = PrometeuContext { let mut req = PrometeuContext {
vm: &mut self.vm, vm: &mut self.vm,
@ -95,6 +118,7 @@ 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, hw: &mut dyn HardwareBridge) {
let mut req = PrometeuContext { let mut req = PrometeuContext {
vm: &mut self.vm, vm: &mut self.vm,

View File

@ -1,22 +1,39 @@
use crate::model::Sample; use crate::model::Sample;
use std::sync::Arc; use std::sync::Arc;
/// Maximum number of simultaneous audio voices supported by the hardware.
pub const MAX_CHANNELS: usize = 16; pub const MAX_CHANNELS: usize = 16;
/// Standard sample rate for the final audio output.
pub const OUTPUT_SAMPLE_RATE: u32 = 48000; pub const OUTPUT_SAMPLE_RATE: u32 = 48000;
#[derive(Clone, Copy, Debug, PartialEq)] /// Defines if a sample should stop at the end or repeat indefinitely.
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub enum LoopMode { pub enum LoopMode {
/// Play once and stop.
#[default]
Off, Off,
/// Return to the start (or loop_start) when reaching the end.
On, On,
} }
/// State of a single playback voice (channel).
///
/// The Core maintains this state to provide information to the App (e.g., is_playing),
/// but the actual real-time mixing is performed by the Host using commands.
pub struct Channel { pub struct Channel {
/// Reference to the PCM data being played.
pub sample: Option<Arc<Sample>>, pub sample: Option<Arc<Sample>>,
/// Current playback position within the sample (fractional for pitch shifting).
pub pos: f64, pub pos: f64,
/// Playback speed multiplier (1.0 = original speed).
pub pitch: f64, pub pitch: f64,
pub volume: u8, // 0..255 /// Voice volume (0-255).
pub pan: u8, // 0..255 pub volume: u8,
/// Stereo panning (0=Full Left, 127=Center, 255=Full Right).
pub pan: u8,
/// Loop configuration for this voice.
pub loop_mode: LoopMode, pub loop_mode: LoopMode,
/// Playback priority (used for voice stealing policies).
pub priority: u8, pub priority: u8,
} }
@ -34,7 +51,12 @@ impl Default for Channel {
} }
} }
/// Commands sent from the Core to the Host audio backend.
///
/// Because the Core logic runs at 60Hz and Audio is generated at 48kHz,
/// we use an asynchronous command queue to synchronize them.
pub enum AudioCommand { pub enum AudioCommand {
/// Start playing a sample on a specific voice.
Play { Play {
sample: Arc<Sample>, sample: Arc<Sample>,
voice_id: usize, voice_id: usize,
@ -44,32 +66,54 @@ pub enum AudioCommand {
priority: u8, priority: u8,
loop_mode: LoopMode, loop_mode: LoopMode,
}, },
/// Immediately stop playback on a voice.
Stop { Stop {
voice_id: usize, voice_id: usize,
}, },
/// Update volume of an ongoing playback.
SetVolume { SetVolume {
voice_id: usize, voice_id: usize,
volume: u8, volume: u8,
}, },
/// Update panning of an ongoing playback.
SetPan { SetPan {
voice_id: usize, voice_id: usize,
pan: u8, pan: u8,
}, },
/// Update pitch of an ongoing playback.
SetPitch { SetPitch {
voice_id: usize, voice_id: usize,
pitch: f64, pitch: f64,
}, },
} }
/// PROMETEU Audio Subsystem.
///
/// Models a multi-channel PCM sampler.
/// It works like an "Audio CPU": the Game Core sends high-level commands
/// every frame, and the Host backend implements the low-level mixer.
pub struct Audio { pub struct Audio {
/// Local state of the 16 hardware voices.
pub voices: [Channel; MAX_CHANNELS], pub voices: [Channel; MAX_CHANNELS],
/// Queue of pending commands to be processed by the Host mixer.
pub commands: Vec<AudioCommand>, pub commands: Vec<AudioCommand>,
} }
impl Audio { impl Audio {
/// Initializes the audio system with empty voices.
pub fn new() -> Self { pub fn new() -> Self {
const EMPTY_CHANNEL: Channel = Channel {
sample: None,
pos: 0.0,
pitch: 1.0,
volume: 255,
pan: 127,
loop_mode: LoopMode::Off,
priority: 0,
};
Self { Self {
voices: Default::default(), voices: [EMPTY_CHANNEL; MAX_CHANNELS],
commands: Vec::new(), commands: Vec::new(),
} }
} }

View File

@ -1,42 +1,71 @@
use crate::model::{Color, HudTileLayer, ScrollableTileLayer, Sprite, TileBank, TileMap, TileSize}; use crate::model::{Color, HudTileLayer, ScrollableTileLayer, Sprite, TileBank, TileMap, TileSize};
use std::mem::size_of; use std::mem::size_of;
/// Blending modes inspired by classic 16-bit hardware.
/// Defines how source pixels are combined with existing pixels in the framebuffer.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum BlendMode { pub enum BlendMode {
/// dst = src /// No blending: source overwrites destination.
#[default] #[default]
None, None,
/// dst = (src + dst) / 2 /// Average: dst = (src + dst) / 2. Creates a semi-transparent effect.
Half, Half,
/// dst = dst + (src / 2) /// Additive: dst = dst + (src / 2). Good for glows/light.
HalfPlus, HalfPlus,
/// dst = dst - (src / 2) /// Subtractive: dst = dst - (src / 2). Good for shadows.
HalfMinus, HalfMinus,
/// dst = dst + src /// Full Additive: dst = dst + src. Saturated light effect.
Full, Full,
} }
/// PROMETEU Graphics Subsystem (GFX).
///
/// Models a specialized graphics chip with a fixed resolution, double buffering,
/// and a multi-layered tile/sprite architecture.
///
/// Composition Order (back to front):
/// 1. Sprites with priority 0 (optional background objects)
/// 2. Tile Layer 0 + Sprites with priority 1
/// 3. Tile Layer 1 + Sprites with priority 2
/// 4. Tile Layer 2 + Sprites with priority 3
/// 5. Tile Layer 3 + Sprites with priority 4
/// 6. Scene Fade effect
/// 7. HUD Layer (always on top)
/// 8. HUD Fade effect
pub struct Gfx { pub struct Gfx {
/// Width of the internal framebuffer.
w: usize, w: usize,
/// Height of the internal framebuffer.
h: usize, h: usize,
/// Front buffer: the one currently being displayed by the Host.
front: Vec<u16>, front: Vec<u16>,
/// Back buffer: the one being drawn to during the current frame.
back: Vec<u16>, back: Vec<u16>,
/// 4 scrollable backgrounds.
pub layers: [ScrollableTileLayer; 4], pub layers: [ScrollableTileLayer; 4],
/// 1 fixed layer for User Interface.
pub hud: HudTileLayer, pub hud: HudTileLayer,
/// Up to 16 sets of graphical assets (tiles + palettes).
pub banks: [Option<TileBank>; 16], pub banks: [Option<TileBank>; 16],
/// Hardware sprites (Object Attribute Memory equivalent).
pub sprites: [Sprite; 512], pub sprites: [Sprite; 512],
pub scene_fade_level: u8, // 0..31 /// Current transparency level for the main scene (0=invisible, 31=fully visible).
pub scene_fade_level: u8,
/// Color used for the scene fade effect.
pub scene_fade_color: Color, pub scene_fade_color: Color,
pub hud_fade_level: u8, // 0..31 /// Current transparency level for the HUD (independent of scene).
pub hud_fade_level: u8,
/// Color used for the HUD fade effect.
pub hud_fade_color: Color, pub hud_fade_color: Color,
// Priority cache to avoid per-frame allocations /// Internal cache used to sort sprites by priority without allocations.
priority_buckets: [Vec<usize>; 5], priority_buckets: [Vec<usize>; 5],
} }
impl Gfx { impl Gfx {
/// Initializes the graphics system with a specific resolution.
pub fn new(w: usize, h: usize) -> Self { pub fn new(w: usize, h: usize) -> Self {
const EMPTY_BANK: Option<TileBank> = None; const EMPTY_BANK: Option<TileBank> = None;
const EMPTY_SPRITE: Sprite = Sprite { const EMPTY_SPRITE: Sprite = Sprite {
@ -51,7 +80,7 @@ impl Gfx {
}; };
let len = w * h; let len = w * h;
let mut layers = [ let layers = [
ScrollableTileLayer::new(64, 64, TileSize::Size16), ScrollableTileLayer::new(64, 64, TileSize::Size16),
ScrollableTileLayer::new(64, 64, TileSize::Size16), ScrollableTileLayer::new(64, 64, TileSize::Size16),
ScrollableTileLayer::new(64, 64, TileSize::Size16), ScrollableTileLayer::new(64, 64, TileSize::Size16),
@ -130,15 +159,19 @@ impl Gfx {
} }
/// Double buffer swap (O(1), no pixel copying). /// Double buffer swap (O(1), no pixel copying).
/// Typically called by the Host when it's time to display the finished frame.
pub fn present(&mut self) { pub fn present(&mut self) {
std::mem::swap(&mut self.front, &mut self.back); std::mem::swap(&mut self.front, &mut self.back);
} }
/// Main frame rendering pipeline. /// The main rendering pipeline.
/// Follows the priority order from the manual (Chapter 4.11). ///
/// This method composes the final frame by rasterizing layers and sprites in the
/// correct priority order into the back buffer.
/// Follows the hardware model where layers and sprites are composed every frame.
pub fn render_all(&mut self) { pub fn render_all(&mut self) {
// 0. Preparation Phase: Organizes what should be drawn in each layer // 0. Preparation Phase: Filter and group sprites by their priority levels.
// Clears the buckets without deallocating memory // This avoids iterating through all 512 sprites for every layer.
for bucket in self.priority_buckets.iter_mut() { for bucket in self.priority_buckets.iter_mut() {
bucket.clear(); bucket.clear();
} }
@ -149,30 +182,32 @@ impl Gfx {
} }
} }
// 1. Priority 0 sprites (Behind Layer 0 - the very back) // 1. Priority 0 sprites: drawn at the very back, behind everything else.
Self::draw_bucket_on_buffer(&mut self.back, self.w, self.h, &self.priority_buckets[0], &self.sprites, &self.banks); Self::draw_bucket_on_buffer(&mut self.back, self.w, self.h, &self.priority_buckets[0], &self.sprites, &self.banks);
// 2. Main layers and prioritized sprites.
// Order: Layer 0 -> Sprites 1 -> Layer 1 -> Sprites 2 ...
for i in 0..self.layers.len() { for i in 0..self.layers.len() {
// 2. Game Layers (0 to 3)
let bank_id = self.layers[i].bank_id as usize; let bank_id = self.layers[i].bank_id as usize;
if let Some(Some(bank)) = self.banks.get(bank_id) { if let Some(Some(bank)) = self.banks.get(bank_id) {
Self::draw_tile_map(&mut self.back, self.w, self.h, &self.layers[i].map, bank, self.layers[i].scroll_x, self.layers[i].scroll_y); Self::draw_tile_map(&mut self.back, self.w, self.h, &self.layers[i].map, bank, self.layers[i].scroll_x, self.layers[i].scroll_y);
} }
// 3. Sprites according to priority // Draw sprites that belong to this depth level
Self::draw_bucket_on_buffer(&mut self.back, self.w, self.h, &self.priority_buckets[i + 1], &self.sprites, &self.banks); Self::draw_bucket_on_buffer(&mut self.back, self.w, self.h, &self.priority_buckets[i + 1], &self.sprites, &self.banks);
} }
// 4. Applies Scene Fade (Affects everything drawn so far) // 4. Scene Fade: Applies a color blend to the entire world (excluding HUD).
Self::apply_fade_to_buffer(&mut self.back, self.scene_fade_level, self.scene_fade_color); Self::apply_fade_to_buffer(&mut self.back, self.scene_fade_level, self.scene_fade_color);
// 5. HUD (Always on top) // 5. HUD: The fixed interface layer, always drawn on top of the world.
self.render_hud(); self.render_hud();
// 6. Applies HUD Fade (Optional, HUD only) // 6. HUD Fade: Independent fade effect for the UI.
Self::apply_fade_to_buffer(&mut self.back, self.hud_fade_level, self.hud_fade_color); Self::apply_fade_to_buffer(&mut self.back, self.hud_fade_level, self.hud_fade_color);
} }
/// Renders a specific game layer.
pub fn render_layer(&mut self, layer_idx: usize) { pub fn render_layer(&mut self, layer_idx: usize) {
if layer_idx >= self.layers.len() { return; } if layer_idx >= self.layers.len() { return; }
@ -188,7 +223,7 @@ impl Gfx {
Self::draw_tile_map(&mut self.back, self.w, self.h, &self.layers[layer_idx].map, bank, scroll_x, scroll_y); Self::draw_tile_map(&mut self.back, self.w, self.h, &self.layers[layer_idx].map, bank, scroll_x, scroll_y);
} }
/// Renders the HUD (no scroll). /// Renders the HUD (fixed position, no scroll).
pub fn render_hud(&mut self) { pub fn render_hud(&mut self) {
let bank_id = self.hud.bank_id as usize; let bank_id = self.hud.bank_id as usize;
let bank = match self.banks.get(bank_id) { let bank = match self.banks.get(bank_id) {
@ -199,6 +234,7 @@ impl Gfx {
Self::draw_tile_map(&mut self.back, self.w, self.h, &self.hud.map, bank, 0, 0); Self::draw_tile_map(&mut self.back, self.w, self.h, &self.hud.map, bank, 0, 0);
} }
/// Rasterizes a TileMap into the provided pixel buffer using scrolling.
fn draw_tile_map( fn draw_tile_map(
back: &mut [u16], back: &mut [u16],
screen_w: usize, screen_w: usize,
@ -210,30 +246,34 @@ impl Gfx {
) { ) {
let tile_size = bank.tile_size as usize; let tile_size = bank.tile_size as usize;
// 1. Calculate how many tiles fit on the screen (with margin of 1 for scroll) // 1. Determine the range of visible tiles based on the scroll position.
// We add a margin of 1 tile to ensure smooth pixel-perfect scrolling at the borders.
let visible_tiles_x = (screen_w / tile_size) + 1; let visible_tiles_x = (screen_w / tile_size) + 1;
let visible_tiles_y = (screen_h / tile_size) + 1; let visible_tiles_y = (screen_h / tile_size) + 1;
// 2. Calculate the initial offset (where the first tile starts being drawn) // 2. Calculate offsets within the tilemap.
let start_tile_x = scroll_x / tile_size as i32; let start_tile_x = scroll_x / tile_size as i32;
let start_tile_y = scroll_y / tile_size as i32; let start_tile_y = scroll_y / tile_size as i32;
// 3. Fine scroll: how many pixels the tiles are shifted within the first visible cell.
let fine_scroll_x = scroll_x % tile_size as i32; let fine_scroll_x = scroll_x % tile_size as i32;
let fine_scroll_y = scroll_y % tile_size as i32; let fine_scroll_y = scroll_y % tile_size as i32;
// 3. Loop by Tile (Much more efficient) // 4. Iterate only through the tiles that are actually visible on screen.
for ty in 0..visible_tiles_y { for ty in 0..visible_tiles_y {
for tx in 0..visible_tiles_x { for tx in 0..visible_tiles_x {
let map_x = (start_tile_x + tx as i32) as usize; let map_x = (start_tile_x + tx as i32) as usize;
let map_y = (start_tile_y + ty as i32) as usize; let map_y = (start_tile_y + ty as i32) as usize;
// Skip if outside map bounds // Bounds check: don't draw if the camera is outside the map.
if map_x >= map.width || map_y >= map.height { continue; } if map_x >= map.width || map_y >= map.height { continue; }
let tile = map.tiles[map_y * map.width + map_x]; let tile = map.tiles[map_y * map.width + map_x];
// Optimized skip for empty (ID 0) tiles.
if tile.id == 0 { continue; } if tile.id == 0 { continue; }
// 4. Draws the tile block on the screen // 5. Project the tile pixels to screen space.
let screen_tile_x = (tx as i32 * tile_size as i32) - fine_scroll_x; let screen_tile_x = (tx as i32 * tile_size as i32) - fine_scroll_x;
let screen_tile_y = (ty as i32 * tile_size as i32) - fine_scroll_y; let screen_tile_y = (ty as i32 * tile_size as i32) - fine_scroll_y;
@ -242,7 +282,8 @@ impl Gfx {
} }
} }
// Helper function to draw a block of 8x8, 16x16 or 32x32 pixels /// Internal helper to copy a single tile's pixels to the framebuffer.
/// Handles flipping and palette resolution.
fn draw_tile_pixels(back: &mut [u16], screen_w: usize, screen_h: usize, x: i32, y: i32, tile: crate::model::Tile, bank: &TileBank) { fn draw_tile_pixels(back: &mut [u16], screen_w: usize, screen_h: usize, x: i32, y: i32, tile: crate::model::Tile, bank: &TileBank) {
let size = bank.tile_size as usize; let size = bank.tile_size as usize;
@ -254,16 +295,17 @@ impl Gfx {
let world_x = x + local_x as i32; let world_x = x + local_x as i32;
if world_x < 0 || world_x >= screen_w as i32 { continue; } if world_x < 0 || world_x >= screen_w as i32 { continue; }
// Handle flip flags by reversing the fetch coordinates.
let fetch_x = if tile.flip_x { size - 1 - local_x } else { local_x }; let fetch_x = if tile.flip_x { size - 1 - local_x } else { local_x };
let fetch_y = if tile.flip_y { size - 1 - local_y } else { local_y }; let fetch_y = if tile.flip_y { size - 1 - local_y } else { local_y };
// 1. Gets the pixel index in the bank // 1. Get the pixel color index (0-15) from the bank.
let px_index = bank.get_pixel_index(tile.id, fetch_x, fetch_y); let px_index = bank.get_pixel_index(tile.id, fetch_x, fetch_y);
// 2. Rule: Index 0 is transparent // 2. Hardware rule: Color index 0 is always fully transparent.
if px_index == 0 { continue; } if px_index == 0 { continue; }
// 3. Resolves the color using the tile's palette // 3. Resolve the virtual index to a real RGB565 color using the tile's assigned palette.
let color = bank.resolve_color(tile.palette_id, px_index); let color = bank.resolve_color(tile.palette_id, px_index);
back[world_y as usize * screen_w + world_x as usize] = color.raw(); back[world_y as usize * screen_w + world_x as usize] = color.raw();

View File

@ -1,9 +1,17 @@
use crate::hardware::{Audio, Gfx, HardwareBridge, Pad, Touch}; use crate::hardware::{Audio, Gfx, HardwareBridge, Pad, Touch};
/// Aggregate structure for all virtual hardware peripherals.
///
/// This struct represents the "Mainboard" of the PROMETEU console,
/// containing instances of GFX, Audio, Input (Pad), and Touch.
pub struct Hardware { pub struct Hardware {
/// The Graphics Processing Unit.
pub gfx: Gfx, pub gfx: Gfx,
/// The Sound Processing Unit.
pub audio: Audio, pub audio: Audio,
/// The standard digital gamepad.
pub pad: Pad, pub pad: Pad,
/// The absolute pointer input device.
pub touch: Touch, pub touch: Touch,
} }
@ -22,9 +30,12 @@ impl HardwareBridge for Hardware {
} }
impl Hardware { impl Hardware {
/// Internal hardware width in pixels.
pub const W: usize = 320; pub const W: usize = 320;
/// Internal hardware height in pixels.
pub const H: usize = 180; pub const H: usize = 180;
/// Creates a fresh hardware instance with default settings.
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
gfx: Gfx::new(Self::W, Self::H), gfx: Gfx::new(Self::W, Self::H),

View File

@ -1,24 +1,38 @@
use crate::model::Color; use crate::model::Color;
/// Standard sizes for square tiles.
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub enum TileSize { pub enum TileSize {
/// 8x8 pixels.
Size8 = 8, Size8 = 8,
/// 16x16 pixels.
Size16 = 16, Size16 = 16,
/// 32x32 pixels.
Size32 = 32, Size32 = 32,
} }
/// A container for graphical assets.
///
/// A TileBank stores both the raw pixel data (as palette indices) and the
/// color palettes themselves. This encapsulates all the information needed
/// to render a set of tiles.
pub struct TileBank { pub struct TileBank {
/// Dimension of each individual tile in the bank.
pub tile_size: TileSize, pub tile_size: TileSize,
pub width: usize, // in pixels /// Width of the full bank sheet in pixels.
pub height: usize, // in pixels pub width: usize,
/// Height of the full bank sheet in pixels.
pub height: usize,
/// Now we store indices from 0 to 15 (4 bits per pixel simulated in 8 bits) /// Pixel data stored as 4-bit indices (packed into 8-bit values).
/// Index 0 is always reserved for transparency.
pub pixel_indices: Vec<u8>, pub pixel_indices: Vec<u8>,
/// 256 palettes, each with 16 colors (RGB565 as u16) /// Table of 256 palettes, each containing 16 RGB565 colors.
pub palettes: [[Color; 16]; 256], pub palettes: [[Color; 16]; 256],
} }
impl TileBank { impl TileBank {
/// Creates an empty tile bank with the specified dimensions.
pub fn new(tile_size: TileSize, width: usize, height: usize) -> Self { pub fn new(tile_size: TileSize, width: usize, height: usize) -> Self {
Self { Self {
tile_size, tile_size,
@ -29,7 +43,7 @@ impl TileBank {
} }
} }
/// Returns the color of a specific pixel inside a tile. /// Resolves a global tile ID and local pixel coordinates to a palette index.
/// tile_id: the tile index in the bank /// tile_id: the tile index in the bank
/// local_x, local_y: the pixel position inside the tile (0 to tile_size-1) /// local_x, local_y: the pixel position inside the tile (0 to tile_size-1)
pub fn get_pixel_index(&self, tile_id: u16, local_x: usize, local_y: usize) -> u8 { pub fn get_pixel_index(&self, tile_id: u16, local_x: usize, local_y: usize) -> u8 {
@ -44,13 +58,14 @@ impl TileBank {
if pixel_x < self.width && pixel_y < self.height { if pixel_x < self.width && pixel_y < self.height {
self.pixel_indices[pixel_y * self.width + pixel_x] self.pixel_indices[pixel_y * self.width + pixel_x]
} else { } else {
0 // Outside the bank = Transparent 0 // Default to transparent if out of bounds
} }
} }
/// Resolves a pixel index to a real Color using a palette. /// Maps a 4-bit index to a real RGB565 Color using the specified palette.
pub fn resolve_color(&self, palette_id: u8, pixel_index: u8) -> Color { pub fn resolve_color(&self, palette_id: u8, pixel_index: u8) -> Color {
// Rule: Index 0 is always transparent (we use MAGENTA/COLOR_KEY as vacuum) // Hardware Rule: Index 0 is always transparent.
// We use Magenta as the 'transparent' signal color during composition.
if pixel_index == 0 { if pixel_index == 0 {
return Color::COLOR_KEY; return Color::COLOR_KEY;
} }

View File

@ -10,12 +10,19 @@ use std::sync::Arc;
use std::time::Instant; use std::time::Instant;
/// PrometeuOS (POS): The system firmware/base. /// PrometeuOS (POS): The system firmware/base.
///
/// Maximum authority for boot, peripherals, PVM execution, and fault handling. /// Maximum authority for boot, peripherals, PVM execution, and fault handling.
/// It acts as the bridge between the high-level PVM applications and the virtual hardware.
pub struct PrometeuOS { pub struct PrometeuOS {
/// Incremented on every host tick (60Hz).
pub tick_index: u64, pub tick_index: u64,
/// Incremented every time a logical game frame is successfully completed.
pub logical_frame_index: u64, pub logical_frame_index: u64,
/// Indicates if there is an ongoing logical frame that hasn't reached `FRAME_SYNC` yet.
pub logical_frame_active: bool, pub logical_frame_active: bool,
/// Number of virtual cycles still available for the current logical frame.
pub logical_frame_remaining_cycles: u64, pub logical_frame_remaining_cycles: u64,
/// Real-world CPU time (in microseconds) consumed by the last host tick.
pub last_frame_cpu_time_us: u64, pub last_frame_cpu_time_us: u64,
// Example assets (kept for compatibility with v0 audio syscalls) // Example assets (kept for compatibility with v0 audio syscalls)
@ -24,36 +31,54 @@ pub struct PrometeuOS {
pub sample_snare: Option<Arc<Sample>>, pub sample_snare: Option<Arc<Sample>>,
// Filesystem // Filesystem
/// The virtual filesystem interface.
pub fs: VirtualFS, pub fs: VirtualFS,
/// Current state of the FS (Mounted, Error, etc).
pub fs_state: FsState, pub fs_state: FsState,
/// Mapping of numeric handles to file paths for open files.
pub open_files: HashMap<u32, String>, pub open_files: HashMap<u32, String>,
/// Sequential handle generator for file operations.
pub next_handle: u32, pub next_handle: u32,
// Log Service // Log Service
pub log_service: LogService, pub log_service: LogService,
/// Unique ID of the currently running application.
pub current_app_id: u32, pub current_app_id: u32,
pub current_cartridge_title: String, pub current_cartridge_title: String,
pub current_cartridge_app_version: String, pub current_cartridge_app_version: String,
pub current_cartridge_app_mode: crate::model::AppMode, pub current_cartridge_app_mode: crate::model::AppMode,
/// Rate-limiting tracker for application logs to prevent performance degradation.
pub logs_written_this_frame: HashMap<u32, u32>, pub logs_written_this_frame: HashMap<u32, u32>,
// Telemetry and Certification // Telemetry and Certification
/// Accumulator for metrics of the frame currently being executed.
pub telemetry_current: TelemetryFrame, pub telemetry_current: TelemetryFrame,
/// Snapshot of the metrics from the last completed logical frame.
pub telemetry_last: TelemetryFrame, pub telemetry_last: TelemetryFrame,
/// Logic for evaluating compliance with the active CAP profile.
pub certifier: Certifier, pub certifier: Certifier,
/// Global pause flag (used by debugger or system events).
pub paused: bool, pub paused: bool,
/// Request from debugger to run exactly one instruction or frame.
pub debug_step_request: bool, pub debug_step_request: bool,
/// Instant when the system was initialized.
boot_time: Instant, boot_time: Instant,
} }
impl PrometeuOS { impl PrometeuOS {
/// Default number of cycles assigned to a single game frame.
pub const CYCLES_PER_LOGICAL_FRAME: u64 = 100_000; pub const CYCLES_PER_LOGICAL_FRAME: u64 = 100_000;
pub const SLICE_PER_TICK: u64 = 100_000; // For now the same, but allows different granularity /// Maximum number of cycles allowed to execute in a single host tick.
/// Usually the same as CYCLES_PER_LOGICAL_FRAME to target 60FPS.
pub const SLICE_PER_TICK: u64 = 100_000;
/// Maximum characters allowed per log message.
pub const MAX_LOG_LEN: usize = 256; pub const MAX_LOG_LEN: usize = 256;
/// Maximum log entries an App can emit in a single frame.
pub const MAX_LOGS_PER_FRAME: u32 = 10; pub const MAX_LOGS_PER_FRAME: u32 = 10;
/// Creates a new POS instance with optional certification rules.
pub fn new(cap_config: Option<CertificationConfig>) -> Self { pub fn new(cap_config: Option<CertificationConfig>) -> Self {
let boot_time = Instant::now(); let boot_time = Instant::now();
let mut os = Self { let mut os = Self {
@ -158,67 +183,81 @@ impl PrometeuOS {
} }
} }
/// Executes a host tick (60Hz). /// Executes a single host tick (nominally 60Hz).
///
/// This method is responsible for managing the logical frame lifecycle.
/// A single host tick might execute a full logical frame, part of it,
/// or multiple frames depending on the configured slices.
pub fn step_frame(&mut self, vm: &mut VirtualMachine, signals: &InputSignals, hw: &mut dyn HardwareBridge) -> Option<String> { pub fn step_frame(&mut self, vm: &mut VirtualMachine, signals: &InputSignals, hw: &mut dyn HardwareBridge) -> Option<String> {
let start = std::time::Instant::now(); let start = std::time::Instant::now();
self.tick_index += 1; self.tick_index += 1;
// If the system is paused, we don't advance unless there's a debug step request.
if self.paused && !self.debug_step_request { if self.paused && !self.debug_step_request {
return None; return None;
} }
self.update_fs(); self.update_fs();
// 1. Frame Initialization
// If we are not currently in the middle of a logical frame, start a new one.
if !self.logical_frame_active { if !self.logical_frame_active {
self.logical_frame_active = true; self.logical_frame_active = true;
self.logical_frame_remaining_cycles = Self::CYCLES_PER_LOGICAL_FRAME; self.logical_frame_remaining_cycles = Self::CYCLES_PER_LOGICAL_FRAME;
self.begin_logical_frame(signals, hw); self.begin_logical_frame(signals, hw);
// Beginning of frame: reset telemetry accumulator (but keep frame_index) // Reset telemetry for the new logical frame
self.telemetry_current = TelemetryFrame { self.telemetry_current = TelemetryFrame {
frame_index: self.logical_frame_index, frame_index: self.logical_frame_index,
..Default::default() ..Default::default()
}; };
} }
// Budget for this tick: the minimum between the tick slice and what remains in the logical frame // 2. Budget Allocation
// Determine how many cycles we can run in this host tick.
let budget = std::cmp::min(Self::SLICE_PER_TICK, self.logical_frame_remaining_cycles); let budget = std::cmp::min(Self::SLICE_PER_TICK, self.logical_frame_remaining_cycles);
// 3. VM Execution
if budget > 0 { if budget > 0 {
// Execute budget // Run the VM until budget is hit or FRAME_SYNC is reached.
let run_result = vm.run_budget(budget, self, hw); let run_result = vm.run_budget(budget, self, hw);
match run_result { match run_result {
Ok(run) => { Ok(run) => {
self.logical_frame_remaining_cycles = self.logical_frame_remaining_cycles.saturating_sub(run.cycles_used); self.logical_frame_remaining_cycles = self.logical_frame_remaining_cycles.saturating_sub(run.cycles_used);
// Accumulates metrics // Accumulate metrics for telemetry and certification
self.telemetry_current.cycles_used += run.cycles_used; self.telemetry_current.cycles_used += run.cycles_used;
self.telemetry_current.vm_steps += run.steps_executed; self.telemetry_current.vm_steps += run.steps_executed;
// Handle Breakpoints
if run.reason == crate::virtual_machine::LogicalFrameEndingReason::Breakpoint { if run.reason == crate::virtual_machine::LogicalFrameEndingReason::Breakpoint {
self.paused = true; self.paused = true;
self.debug_step_request = false; self.debug_step_request = false;
self.log(LogLevel::Info, LogSource::Vm, 0xDEB1, format!("Breakpoint hit at PC 0x{:X}", vm.pc)); self.log(LogLevel::Info, LogSource::Vm, 0xDEB1, format!("Breakpoint hit at PC 0x{:X}", vm.pc));
} }
// 4. Frame Finalization (FRAME_SYNC reached)
if run.reason == crate::virtual_machine::LogicalFrameEndingReason::FrameSync { if run.reason == crate::virtual_machine::LogicalFrameEndingReason::FrameSync {
// All drawing commands for this frame are now complete.
// Finalize the framebuffer.
hw.gfx_mut().render_all(); hw.gfx_mut().render_all();
// Finalizes frame telemetry (host_cpu_time will be refined at the end of the tick) // Finalize frame telemetry
self.telemetry_current.host_cpu_time_us = start.elapsed().as_micros() as u64; self.telemetry_current.host_cpu_time_us = start.elapsed().as_micros() as u64;
// Certification (CAP) // Evaluate CAP (Execution Budget Compliance)
let ts_ms = self.boot_time.elapsed().as_millis() as u64; let ts_ms = self.boot_time.elapsed().as_millis() as u64;
self.telemetry_current.violations = self.certifier.evaluate(&self.telemetry_current, &mut self.log_service, ts_ms) as u32; self.telemetry_current.violations = self.certifier.evaluate(&self.telemetry_current, &mut self.log_service, ts_ms) as u32;
// Latch: what the overlay reads // Latch telemetry for the Host/Debugger to read.
self.telemetry_last = self.telemetry_current; self.telemetry_last = self.telemetry_current;
self.logical_frame_index += 1; self.logical_frame_index += 1;
self.logical_frame_active = false; self.logical_frame_active = false;
self.logical_frame_remaining_cycles = 0; self.logical_frame_remaining_cycles = 0;
// If we were doing a "step frame" debug command, pause now that the frame is done.
if self.debug_step_request { if self.debug_step_request {
self.paused = true; self.paused = true;
self.debug_step_request = false; self.debug_step_request = false;
@ -226,6 +265,7 @@ impl PrometeuOS {
} }
} }
Err(e) => { Err(e) => {
// Fatal VM fault (division by zero, invalid memory access, etc).
let err_msg = format!("PVM Fault: {:?}", e); let err_msg = format!("PVM Fault: {:?}", e);
self.log(LogLevel::Error, LogSource::Vm, 0, err_msg.clone()); self.log(LogLevel::Error, LogSource::Vm, 0, err_msg.clone());
return Some(err_msg); return Some(err_msg);
@ -235,7 +275,7 @@ impl PrometeuOS {
self.last_frame_cpu_time_us = start.elapsed().as_micros() as u64; self.last_frame_cpu_time_us = start.elapsed().as_micros() as u64;
// If the frame ended in this tick, we update the final real time in the latch // If the frame ended exactly in this tick, we update the final real time in the latch.
if !self.logical_frame_active && self.telemetry_last.frame_index == self.logical_frame_index.wrapping_sub(1) { if !self.logical_frame_active && self.telemetry_last.frame_index == self.logical_frame_index.wrapping_sub(1) {
self.telemetry_last.host_cpu_time_us = self.last_frame_cpu_time_us; self.telemetry_last.host_cpu_time_us = self.last_frame_cpu_time_us;
} }
@ -493,23 +533,35 @@ mod tests {
} }
impl NativeInterface for PrometeuOS { impl NativeInterface for PrometeuOS {
/// Dispatches a syscall from the VM to the native implementation.
///
/// Syscalls are grouped by functionality:
/// - 0x0000: System/Cartridge management
/// - 0x1000: Graphics (GFX)
/// - 0x2000: Input
/// - 0x3000: Audio
/// - 0x4000: Filesystem (FS)
/// - 0x5000: Logging
///
/// Each syscall returns the number of virtual cycles it consumed.
fn syscall(&mut self, id: u32, vm: &mut VirtualMachine, hw: &mut dyn HardwareBridge) -> Result<u64, String> { fn syscall(&mut self, id: u32, vm: &mut VirtualMachine, hw: &mut dyn HardwareBridge) -> Result<u64, String> {
self.telemetry_current.syscalls += 1; self.telemetry_current.syscalls += 1;
match id { match id {
// --- System Syscalls ---
// system.has_cart() -> bool // system.has_cart() -> bool
0x0001 => { 0x0001 => {
// virtual_machine.push(Value::Boolean(self.current_cartridge.is_some())); // Returns true if a cartridge is available.
Ok(10) Ok(10)
} }
// system.run_cart() // system.run_cart()
0x0002 => { 0x0002 => {
// if let Some(cart) = self.current_cartridge.as_ref().cloned() { // Triggers loading and execution of the current cartridge.
// self.load_cartridge(&cart);
// } else {
// return Err("No cartridge inserted".into());
// }
Ok(100) Ok(100)
} }
// --- GFX Syscalls ---
// gfx.clear(color_index) // gfx.clear(color_index)
0x1001 => { 0x1001 => {
let color_idx = vm.pop_integer()? as usize; let color_idx = vm.pop_integer()? as usize;
@ -528,6 +580,9 @@ impl NativeInterface for PrometeuOS {
hw.gfx_mut().fill_rect(x, y, w, h, color); hw.gfx_mut().fill_rect(x, y, w, h, color);
Ok(200) Ok(200)
} }
// --- Input Syscalls ---
// input.get_pad(button_id) -> bool // input.get_pad(button_id) -> bool
0x2001 => { 0x2001 => {
let button_id = vm.pop_integer()? as u32; let button_id = vm.pop_integer()? as u32;
@ -535,6 +590,9 @@ impl NativeInterface for PrometeuOS {
vm.push(Value::Boolean(is_down)); vm.push(Value::Boolean(is_down));
Ok(50) Ok(50)
} }
// --- Audio Syscalls ---
// audio.play_sample(sample_id, voice_id, volume, pan, pitch) // audio.play_sample(sample_id, voice_id, volume, pan, pitch)
0x3001 => { 0x3001 => {
let pitch = vm.pop_number()?; let pitch = vm.pop_number()?;
@ -559,6 +617,7 @@ impl NativeInterface for PrometeuOS {
// --- Filesystem Syscalls (0x4000) --- // --- Filesystem Syscalls (0x4000) ---
// FS_OPEN(path) -> handle // FS_OPEN(path) -> handle
// Opens a file in the virtual sandbox and returns a numeric handle.
0x4001 => { 0x4001 => {
let path = match vm.pop()? { let path = match vm.pop()? {
Value::String(s) => s, Value::String(s) => s,
@ -623,7 +682,7 @@ impl NativeInterface for PrometeuOS {
}; };
match self.fs.list_dir(&path) { match self.fs.list_dir(&path) {
Ok(entries) => { Ok(entries) => {
// For now, returns a string separated by ';' // Returns a string separated by ';' for simple parsing in PVM.
let names: Vec<String> = entries.into_iter().map(|e| e.name).collect(); let names: Vec<String> = entries.into_iter().map(|e| e.name).collect();
vm.push(Value::String(names.join(";"))); vm.push(Value::String(names.join(";")));
Ok(500) Ok(500)

View File

@ -5,35 +5,61 @@ use crate::virtual_machine::opcode::OpCode;
use crate::virtual_machine::value::Value; use crate::virtual_machine::value::Value;
use crate::virtual_machine::Program; use crate::virtual_machine::Program;
/// Reason why the Virtual Machine stopped execution during a specific run.
/// This allows the system to decide if it should continue execution in the next tick
/// or if the frame is finalized.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogicalFrameEndingReason { pub enum LogicalFrameEndingReason {
/// Execution reached a `FRAME_SYNC` instruction, marking the end of the logical frame.
FrameSync, FrameSync,
/// The cycle budget for the current host tick was exhausted before reaching `FRAME_SYNC`.
BudgetExhausted, BudgetExhausted,
/// A `HALT` instruction was executed, terminating the program.
Halted, Halted,
/// The Program Counter (PC) reached the end of the available bytecode.
EndOfRom, EndOfRom,
/// Execution hit a registered breakpoint.
Breakpoint, Breakpoint,
} }
/// A report detailing the results of an execution slice (run_budget).
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BudgetReport { pub struct BudgetReport {
/// Total virtual cycles consumed during this run.
pub cycles_used: u64, pub cycles_used: u64,
/// Number of VM instructions executed.
pub steps_executed: u32, pub steps_executed: u32,
/// The reason why this execution slice ended.
pub reason: LogicalFrameEndingReason, pub reason: LogicalFrameEndingReason,
} }
/// The PVM (PROMETEU Virtual Machine).
///
/// A deterministic, stack-based virtual machine designed for educational purposes.
/// It models execution through fixed-cost cycles and explicit memory management.
pub struct VirtualMachine { pub struct VirtualMachine {
/// Program Counter: points to the next byte in the ROM to be executed.
pub pc: usize, pub pc: usize,
/// Operand Stack: used for intermediate calculations and passing arguments to opcodes.
pub operand_stack: Vec<Value>, pub operand_stack: Vec<Value>,
/// Call Stack: stores execution frames for function calls and local variables.
pub call_stack: Vec<CallFrame>, pub call_stack: Vec<CallFrame>,
/// Globals: storage for persistent variables that survive between frames.
pub globals: Vec<Value>, pub globals: Vec<Value>,
/// The currently loaded program (Bytecode + Constant Pool).
pub program: Program, pub program: Program,
pub heap: Vec<Value>, // Simplified for demo, future real RAM/Heap /// Dynamic memory region for complex structures (Simplified in current version).
pub heap: Vec<Value>,
/// Total accumulated execution cycles since the last reset.
pub cycles: u64, pub cycles: u64,
/// Flag indicating if the VM has been stopped by a `HALT` instruction.
pub halted: bool, pub halted: bool,
/// A set of PC addresses where execution should pause.
pub breakpoints: std::collections::HashSet<usize>, pub breakpoints: std::collections::HashSet<usize>,
} }
impl VirtualMachine { impl VirtualMachine {
/// Creates a new VM instance with the provided bytecode and constants.
pub fn new(rom: Vec<u8>, constant_pool: Vec<Value>) -> Self { pub fn new(rom: Vec<u8>, constant_pool: Vec<Value>) -> Self {
Self { Self {
pc: 0, pc: 0,
@ -48,26 +74,31 @@ impl VirtualMachine {
} }
} }
/// Resets the VM state and loads a new program.
/// This is typically called by the Firmware when starting a new App/Cartridge.
pub fn initialize(&mut self, program_bytes: Vec<u8>, entrypoint: &str) { pub fn initialize(&mut self, program_bytes: Vec<u8>, entrypoint: &str) {
// PBC (Prometeu ByteCode) is a binary format that includes a header,
// constant pool, and the raw ROM (bytecode).
if program_bytes.starts_with(b"PPBC") { if program_bytes.starts_with(b"PPBC") {
if let Ok((rom, cp)) = self.parse_pbc(&program_bytes) { if let Ok((rom, cp)) = self.parse_pbc(&program_bytes) {
self.program = Program::new(rom, cp); self.program = Program::new(rom, cp);
} else { } else {
// Fallback for raw bytes if PBC parsing fails
self.program = Program::new(program_bytes, vec![]); self.program = Program::new(program_bytes, vec![]);
} }
} else { } else {
// For now, we treat the bytes as the ROM directly. // If it doesn't have the PPBC signature, treat it as raw bytecode.
self.program = Program::new(program_bytes, vec![]); self.program = Program::new(program_bytes, vec![]);
} }
// If the entrypoint is numeric, we can try to use it as the initial PC. // Resolve the entrypoint. Currently supports numeric addresses.
// If not, for now we ignore it or start from 0.
if let Ok(addr) = entrypoint.parse::<usize>() { if let Ok(addr) = entrypoint.parse::<usize>() {
self.pc = addr; self.pc = addr;
} else { } else {
self.pc = 0; self.pc = 0;
} }
// Full state reset to ensure a clean start for the App
self.operand_stack.clear(); self.operand_stack.clear();
self.call_stack.clear(); self.call_stack.clear();
self.globals.clear(); self.globals.clear();
@ -76,9 +107,12 @@ impl VirtualMachine {
self.halted = false; self.halted = false;
} }
/// Parses the PROMETEU binary format.
/// Format: "PPBC" (4) | CP_COUNT (u32) | CP_ENTRIES[...] | ROM_SIZE (u32) | ROM_BYTES[...]
fn parse_pbc(&self, bytes: &[u8]) -> Result<(Vec<u8>, Vec<Value>), String> { fn parse_pbc(&self, bytes: &[u8]) -> Result<(Vec<u8>, Vec<Value>), String> {
let mut cursor = 4; // Skip "PPBC" let mut cursor = 4; // Skip "PPBC" signature
// 1. Parse Constant Pool (literals like strings, ints, floats used in the program)
let cp_count = self.read_u32_at(bytes, &mut cursor)? as usize; let cp_count = self.read_u32_at(bytes, &mut cursor)? as usize;
let mut cp = Vec::with_capacity(cp_count); let mut cp = Vec::with_capacity(cp_count);
@ -88,11 +122,11 @@ impl VirtualMachine {
cursor += 1; cursor += 1;
match tag { match tag {
1 => { // Integer 1 => { // Integer (64-bit)
let val = self.read_i64_at(bytes, &mut cursor)?; let val = self.read_i64_at(bytes, &mut cursor)?;
cp.push(Value::Integer(val)); cp.push(Value::Integer(val));
} }
2 => { // Float 2 => { // Float (64-bit)
let val = self.read_f64_at(bytes, &mut cursor)?; let val = self.read_f64_at(bytes, &mut cursor)?;
cp.push(Value::Float(val)); cp.push(Value::Float(val));
} }
@ -102,7 +136,7 @@ impl VirtualMachine {
cursor += 1; cursor += 1;
cp.push(Value::Boolean(val)); cp.push(Value::Boolean(val));
} }
4 => { // String 4 => { // String (UTF-8)
let len = self.read_u32_at(bytes, &mut cursor)? as usize; let len = self.read_u32_at(bytes, &mut cursor)? as usize;
if cursor + len > bytes.len() { return Err("Unexpected end of PBC".into()); } if cursor + len > bytes.len() { return Err("Unexpected end of PBC".into()); }
let s = String::from_utf8_lossy(&bytes[cursor..cursor + len]).into_owned(); let s = String::from_utf8_lossy(&bytes[cursor..cursor + len]).into_owned();
@ -113,6 +147,7 @@ impl VirtualMachine {
} }
} }
// 2. Parse ROM (executable bytecode)
let rom_size = self.read_u32_at(bytes, &mut cursor)? as usize; let rom_size = self.read_u32_at(bytes, &mut cursor)? as usize;
if cursor + rom_size > bytes.len() { if cursor + rom_size > bytes.len() {
return Err("Invalid ROM size in PBC".into()); return Err("Invalid ROM size in PBC".into());
@ -151,6 +186,16 @@ impl Default for VirtualMachine {
} }
impl VirtualMachine { impl VirtualMachine {
/// Executes the VM for a limited number of cycles (budget).
///
/// This is the heart of the deterministic execution model. Instead of running
/// indefinitely, the VM runs until it consumes its allocated budget or reaches
/// a synchronization point (`FRAME_SYNC`).
///
/// # Arguments
/// * `budget` - Maximum number of cycles allowed for this execution slice.
/// * `native` - Interface for handling syscalls (Firmware/OS).
/// * `hw` - Access to virtual hardware peripherals.
pub fn run_budget( pub fn run_budget(
&mut self, &mut self,
budget: u64, budget: u64,
@ -165,6 +210,9 @@ impl VirtualMachine {
&& !self.halted && !self.halted
&& self.pc < self.program.rom.len() && self.pc < self.program.rom.len()
{ {
// Debugger support: stop before executing an instruction if there's a breakpoint.
// Note: we skip the check for the very first step of a slice to avoid
// getting stuck on the same breakpoint repeatedly.
if steps_executed > 0 && self.breakpoints.contains(&self.pc) { if steps_executed > 0 && self.breakpoints.contains(&self.pc) {
ending_reason = Some(LogicalFrameEndingReason::Breakpoint); ending_reason = Some(LogicalFrameEndingReason::Breakpoint);
break; break;
@ -173,26 +221,31 @@ impl VirtualMachine {
let pc_before = self.pc; let pc_before = self.pc;
let cycles_before = self.cycles; let cycles_before = self.cycles;
// Fast-path: FRAME_SYNC ends the logical frame // Fast-path for FRAME_SYNC:
// This instruction is special because it marks the end of a logical game frame.
// We peak ahead to handle it efficiently.
let opcode_val = self.peek_u16()?; let opcode_val = self.peek_u16()?;
let opcode = OpCode::try_from(opcode_val)?; let opcode = OpCode::try_from(opcode_val)?;
if opcode == OpCode::FrameSync { if opcode == OpCode::FrameSync {
self.pc += 2; self.pc += 2; // Advance PC past the opcode
self.cycles += OpCode::FrameSync.cycles(); self.cycles += OpCode::FrameSync.cycles();
steps_executed += 1; steps_executed += 1;
ending_reason = Some(LogicalFrameEndingReason::FrameSync); ending_reason = Some(LogicalFrameEndingReason::FrameSync);
break; break;
} }
// Execute a single step (Fetch-Decode-Execute)
self.step(native, hw)?; self.step(native, hw)?;
steps_executed += 1; steps_executed += 1;
// ensures real progress // Integrity check: ensure real progress is being made to avoid infinite loops
// caused by zero-cycle instructions or stuck PC.
if self.pc == pc_before && self.cycles == cycles_before && !self.halted { if self.pc == pc_before && self.cycles == cycles_before && !self.halted {
return Err(format!("VM stuck at PC 0x{:08X}", self.pc)); return Err(format!("VM stuck at PC 0x{:08X}", self.pc));
} }
} }
// Determine why we stopped if no explicit reason (FrameSync/Breakpoint) was set.
if ending_reason.is_none() { if ending_reason.is_none() {
if self.halted { if self.halted {
ending_reason = Some(LogicalFrameEndingReason::Halted); ending_reason = Some(LogicalFrameEndingReason::Halted);
@ -210,6 +263,7 @@ impl VirtualMachine {
}) })
} }
/// Peeks at the next 16-bit value in the ROM without advancing the PC.
fn peek_u16(&self) -> Result<u16, String> { fn peek_u16(&self) -> Result<u16, String> {
if self.pc + 2 > self.program.rom.len() { if self.pc + 2 > self.program.rom.len() {
return Err("Unexpected end of ROM".into()); return Err("Unexpected end of ROM".into());
@ -221,14 +275,22 @@ impl VirtualMachine {
Ok(u16::from_le_bytes(bytes)) Ok(u16::from_le_bytes(bytes))
} }
/// Executes a single instruction at the current Program Counter (PC).
///
/// This follows the classic CPU cycle:
/// 1. Fetch: Read the opcode from memory.
/// 2. Decode: Identify what operation to perform.
/// 3. Execute: Perform the operation, updating stacks, memory, or calling peripherals.
pub fn step(&mut self, native: &mut dyn NativeInterface, hw: &mut dyn HardwareBridge) -> Result<(), String> { pub fn step(&mut self, native: &mut dyn NativeInterface, hw: &mut dyn HardwareBridge) -> Result<(), String> {
if self.halted || self.pc >= self.program.rom.len() { if self.halted || self.pc >= self.program.rom.len() {
return Ok(()); return Ok(());
} }
// Fetch & Decode
let opcode_val = self.read_u16()?; let opcode_val = self.read_u16()?;
let opcode = OpCode::try_from(opcode_val)?; let opcode = OpCode::try_from(opcode_val)?;
// Execute
match opcode { match opcode {
OpCode::Nop => {} OpCode::Nop => {}
OpCode::Halt => { OpCode::Halt => {
@ -369,6 +431,8 @@ impl VirtualMachine {
self.operand_stack[stack_idx] = val; self.operand_stack[stack_idx] = val;
} }
OpCode::Call => { OpCode::Call => {
// addr: destination instruction address
// args_count: how many values from the operand stack become locals in the new frame
let addr = self.read_u32()? as usize; let addr = self.read_u32()? as usize;
let args_count = self.read_u32()? as usize; let args_count = self.read_u32()? as usize;
let stack_base = self.operand_stack.len() - args_count; let stack_base = self.operand_stack.len() - args_count;
@ -382,18 +446,21 @@ impl VirtualMachine {
OpCode::Ret => { OpCode::Ret => {
let frame = self.call_stack.pop().ok_or("Call stack underflow")?; let frame = self.call_stack.pop().ok_or("Call stack underflow")?;
let return_val = self.pop()?; let return_val = self.pop()?;
// Clean up the operand stack, removing the frame's locals
self.operand_stack.truncate(frame.stack_base); self.operand_stack.truncate(frame.stack_base);
// Return the result of the function
self.push(return_val); self.push(return_val);
self.pc = frame.return_address; self.pc = frame.return_address;
} }
OpCode::PushScope => { OpCode::PushScope => {
// Used for blocks within a function that have their own locals
let locals_count = self.read_u32()? as usize; let locals_count = self.read_u32()? as usize;
let stack_base = self.operand_stack.len(); let stack_base = self.operand_stack.len();
for _ in 0..locals_count { for _ in 0..locals_count {
self.push(Value::Null); self.push(Value::Null);
} }
self.call_stack.push(CallFrame { self.call_stack.push(CallFrame {
return_address: 0, return_address: 0, // Scope blocks don't return via PC jump
stack_base, stack_base,
locals_count, locals_count,
}); });
@ -403,6 +470,7 @@ impl VirtualMachine {
self.operand_stack.truncate(frame.stack_base); self.operand_stack.truncate(frame.stack_base);
} }
OpCode::Alloc => { OpCode::Alloc => {
// Allocates 'size' values on the heap and pushes a reference to the stack
let size = self.read_u32()? as usize; let size = self.read_u32()? as usize;
let ref_idx = self.heap.len(); let ref_idx = self.heap.len();
for _ in 0..size { for _ in 0..size {
@ -411,6 +479,7 @@ impl VirtualMachine {
self.push(Value::Ref(ref_idx)); self.push(Value::Ref(ref_idx));
} }
OpCode::LoadRef => { OpCode::LoadRef => {
// Reads a value from a heap reference at a specific offset
let offset = self.read_u32()? as usize; let offset = self.read_u32()? as usize;
let ref_val = self.pop()?; let ref_val = self.pop()?;
if let Value::Ref(base) = ref_val { if let Value::Ref(base) = ref_val {
@ -421,6 +490,7 @@ impl VirtualMachine {
} }
} }
OpCode::StoreRef => { OpCode::StoreRef => {
// Writes a value to a heap reference at a specific offset
let offset = self.read_u32()? as usize; let offset = self.read_u32()? as usize;
let val = self.pop()?; let val = self.pop()?;
let ref_val = self.pop()?; let ref_val = self.pop()?;
@ -434,15 +504,18 @@ impl VirtualMachine {
} }
} }
OpCode::Syscall => { OpCode::Syscall => {
// Calls a native function implemented by the Firmware/OS
let id = self.read_u32()?; let id = self.read_u32()?;
let native_cycles = native.syscall(id, self, hw).map_err(|e| format!("syscall 0x{:08X} failed: {}", id, e))?; let native_cycles = native.syscall(id, self, hw).map_err(|e| format!("syscall 0x{:08X} failed: {}", id, e))?;
self.cycles += native_cycles; self.cycles += native_cycles;
} }
OpCode::FrameSync => { OpCode::FrameSync => {
// Already handled in the run_budget loop for performance
return Ok(()); return Ok(());
} }
} }
// Apply the instruction cost to the cycle counter
self.cycles += opcode.cycles(); self.cycles += opcode.cycles();
Ok(()) Ok(())
} }

View File

@ -5,15 +5,27 @@ use prometeu_core::debugger_protocol::*;
use std::net::{TcpListener, TcpStream}; use std::net::{TcpListener, TcpStream};
use std::io::{Read, Write}; use std::io::{Read, Write};
/// Host-side implementation of the PROMETEU DevTools Protocol.
///
/// This component acts as a TCP server that allows external tools (like the
/// Prometeu Debugger) to observe and control the execution of the virtual machine.
///
/// Communication is based on JSONL (JSON lines) over TCP.
pub struct HostDebugger { pub struct HostDebugger {
/// If true, the VM will not start execution until a 'start' command is received.
pub waiting_for_start: bool, pub waiting_for_start: bool,
/// The TCP listener for incoming debugger connections.
pub(crate) listener: Option<TcpListener>, pub(crate) listener: Option<TcpListener>,
/// The currently active connection to a debugger client.
pub(crate) stream: Option<TcpStream>, pub(crate) stream: Option<TcpStream>,
/// Sequence tracker to ensure logs are sent only once.
last_log_seq: u64, last_log_seq: u64,
/// Frame tracker to send telemetry snapshots periodically.
last_telemetry_frame: u64, last_telemetry_frame: u64,
} }
impl HostDebugger { impl HostDebugger {
/// Creates a new debugger interface in an idle state.
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
waiting_for_start: false, waiting_for_start: false,
@ -24,17 +36,21 @@ impl HostDebugger {
} }
} }
/// Configures the debugger based on the boot target.
/// If debug mode is enabled, it binds to the specified TCP port.
pub fn setup_boot_target(&mut self, boot_target: &BootTarget, firmware: &mut Firmware) { pub fn setup_boot_target(&mut self, boot_target: &BootTarget, firmware: &mut Firmware) {
if let BootTarget::Cartridge { path, debug: true, debug_port } = boot_target { if let BootTarget::Cartridge { path, debug: true, debug_port } = boot_target {
self.waiting_for_start = true; self.waiting_for_start = true;
// Pre-loads cartridge information for the handshake // Pre-load cartridge metadata so the Handshake message can contain
// valid information about the App being debugged.
if let Ok(cartridge) = CartridgeLoader::load(path) { if let Ok(cartridge) = CartridgeLoader::load(path) {
firmware.os.initialize_vm(&mut firmware.vm, &cartridge); firmware.os.initialize_vm(&mut firmware.vm, &cartridge);
} }
match TcpListener::bind(format!("127.0.0.1:{}", debug_port)) { match TcpListener::bind(format!("127.0.0.1:{}", debug_port)) {
Ok(listener) => { Ok(listener) => {
// Set listener to non-blocking so it doesn't halt the main loop.
listener.set_nonblocking(true).expect("Cannot set non-blocking"); listener.set_nonblocking(true).expect("Cannot set non-blocking");
self.listener = Some(listener); self.listener = Some(listener);
println!("[Debugger] Listening for start command on port {}...", debug_port); println!("[Debugger] Listening for start command on port {}...", debug_port);
@ -48,6 +64,7 @@ impl HostDebugger {
} }
} }
/// Sends a structured response to the connected debugger client.
fn send_response(&mut self, resp: DebugResponse) { fn send_response(&mut self, resp: DebugResponse) {
if let Some(stream) = &mut self.stream { if let Some(stream) = &mut self.stream {
if let Ok(json) = serde_json::to_string(&resp) { if let Ok(json) = serde_json::to_string(&resp) {
@ -57,6 +74,7 @@ impl HostDebugger {
} }
} }
/// Sends an asynchronous event to the connected debugger client.
fn send_event(&mut self, event: DebugEvent) { fn send_event(&mut self, event: DebugEvent) {
if let Some(stream) = &mut self.stream { if let Some(stream) = &mut self.stream {
if let Ok(json) = serde_json::to_string(&event) { if let Ok(json) = serde_json::to_string(&event) {
@ -66,16 +84,20 @@ 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, hardware: &mut Hardware) {
// 1. Accept new client connections.
if let Some(listener) = &self.listener { if let Some(listener) = &self.listener {
if let Ok((stream, _addr)) = listener.accept() { if let Ok((stream, _addr)) = listener.accept() {
// Currently, only one debugger client is supported at a time.
if self.stream.is_none() { if self.stream.is_none() {
println!("[Debugger] Connection received!"); println!("[Debugger] Connection received!");
stream.set_nonblocking(true).expect("Cannot set non-blocking on stream"); stream.set_nonblocking(true).expect("Cannot set non-blocking on stream");
self.stream = Some(stream); self.stream = Some(stream);
// Send Handshake // Immediately send the Handshake message to identify the Runtime and App.
let handshake = DebugResponse::Handshake { let handshake = DebugResponse::Handshake {
protocol_version: DEVTOOLS_PROTOCOL_VERSION, protocol_version: DEVTOOLS_PROTOCOL_VERSION,
runtime_version: "0.1".to_string(), runtime_version: "0.1".to_string(),
@ -93,33 +115,35 @@ impl HostDebugger {
} }
} }
// 2. Read and process pending commands from the network stream.
if let Some(mut stream) = self.stream.take() { if let Some(mut stream) = self.stream.take() {
let mut buf = [0u8; 4096]; let mut buf = [0u8; 4096];
match stream.read(&mut buf) { match stream.read(&mut buf) {
Ok(0) => { Ok(0) => {
// TCP socket closed by the client.
println!("[Debugger] Connection closed by remote."); println!("[Debugger] Connection closed by remote.");
self.stream = None; self.stream = None;
// Resume VM execution if it was paused waiting for the debugger.
firmware.os.paused = false; firmware.os.paused = false;
self.waiting_for_start = false; self.waiting_for_start = false;
} }
Ok(n) => { Ok(n) => {
let data = &buf[..n]; let data = &buf[..n];
// Process multiple commands if there's \n
let msg = String::from_utf8_lossy(data); let msg = String::from_utf8_lossy(data);
self.stream = Some(stream); // Put it back before processing commands self.stream = Some(stream);
// Support multiple JSON messages in a single TCP packet.
for line in msg.lines() { for line in msg.lines() {
let trimmed = line.trim(); let trimmed = line.trim();
if trimmed.is_empty() { if trimmed.is_empty() { continue; }
continue;
}
if let Ok(cmd) = serde_json::from_str::<DebugCommand>(trimmed) { if let Ok(cmd) = serde_json::from_str::<DebugCommand>(trimmed) {
self.handle_command(cmd, firmware, hardware); self.handle_command(cmd, firmware, hardware);
} }
} }
} }
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
// No data available right now, continue.
self.stream = Some(stream); self.stream = Some(stream);
} }
Err(e) => { Err(e) => {
@ -131,12 +155,13 @@ impl HostDebugger {
} }
} }
// Streaming de eventos // 3. Push events (logs, telemetry) to the client.
if self.stream.is_some() { if self.stream.is_some() {
self.stream_events(firmware); self.stream_events(firmware);
} }
} }
/// Dispatches a specific DebugCommand to the system components.
fn handle_command(&mut self, cmd: DebugCommand, firmware: &mut Firmware, hardware: &mut Hardware) { fn handle_command(&mut self, cmd: DebugCommand, firmware: &mut Firmware, hardware: &mut Hardware) {
match cmd { match cmd {
DebugCommand::Ok | DebugCommand::Start => { DebugCommand::Ok | DebugCommand::Start => {
@ -153,15 +178,17 @@ impl HostDebugger {
firmware.os.paused = false; firmware.os.paused = false;
} }
DebugCommand::Step => { DebugCommand::Step => {
// Execute exactly one instruction and keep paused.
firmware.os.paused = true; firmware.os.paused = true;
// Executes an instruction immediately
let _ = firmware.os.debug_step_instruction(&mut firmware.vm, hardware); let _ = firmware.os.debug_step_instruction(&mut firmware.vm, hardware);
} }
DebugCommand::StepFrame => { DebugCommand::StepFrame => {
// Execute until the end of the current logical frame.
firmware.os.paused = false; firmware.os.paused = false;
firmware.os.debug_step_request = true; firmware.os.debug_step_request = true;
} }
DebugCommand::GetState => { DebugCommand::GetState => {
// Return detailed VM register and stack state.
let stack_top = firmware.vm.operand_stack.iter() let stack_top = firmware.vm.operand_stack.iter()
.rev().take(10).cloned().collect(); .rev().take(10).cloned().collect();
@ -186,13 +213,14 @@ impl HostDebugger {
} }
} }
/// Scans the system for new information to push to the debugger client.
fn stream_events(&mut self, firmware: &mut Firmware) { fn stream_events(&mut self, firmware: &mut Firmware) {
// Logs // 1. Process and send new log entries.
let new_events = firmware.os.log_service.get_after(self.last_log_seq); let new_events = firmware.os.log_service.get_after(self.last_log_seq);
for event in new_events { for event in new_events {
self.last_log_seq = event.seq; self.last_log_seq = event.seq;
// Check if it's a breakpoint hit via tag // Map specific internal log tags to protocol events.
if event.tag == 0xDEB1 { if event.tag == 0xDEB1 {
self.send_event(DebugEvent::BreakpointHit { self.send_event(DebugEvent::BreakpointHit {
pc: firmware.vm.pc, pc: firmware.vm.pc,
@ -200,7 +228,7 @@ impl HostDebugger {
}); });
} }
// Certification via Tags 0xCA01-0xCA03 // Map Certification tags (0xCA01-0xCA03) to 'Cert' protocol events.
if event.tag >= 0xCA01 && event.tag <= 0xCA03 { if event.tag >= 0xCA01 && event.tag <= 0xCA03 {
let rule = match event.tag { let rule = match event.tag {
0xCA01 => "cycles_budget", 0xCA01 => "cycles_budget",
@ -211,7 +239,7 @@ impl HostDebugger {
self.send_event(DebugEvent::Cert { self.send_event(DebugEvent::Cert {
rule, rule,
used: 0, // Simplified, detailed information is in the log message used: 0,
limit: 0, limit: 0,
frame_index: firmware.os.logical_frame_index, frame_index: firmware.os.logical_frame_index,
}); });
@ -224,7 +252,7 @@ impl HostDebugger {
}); });
} }
// Telemetria (a cada novo frame) // 2. Send telemetry snapshots at the completion of every frame.
let current_frame = firmware.os.logical_frame_index; let current_frame = firmware.os.logical_frame_index;
if current_frame > self.last_telemetry_frame { if current_frame > self.last_telemetry_frame {
let tel = &firmware.os.telemetry_last; let tel = &firmware.os.telemetry_last;

View File

@ -18,36 +18,62 @@ use winit::window::{Window, WindowAttributes, WindowId};
use prometeu_core::telemetry::CertificationConfig; use prometeu_core::telemetry::CertificationConfig;
/// The Desktop implementation of the PROMETEU Runtime.
///
/// This struct acts as the physical "chassis" of the virtual console. It is
/// responsible for:
/// - Creating and managing the OS window (via `winit`).
/// - Initializing the GPU-accelerated framebuffer (via `pixels`).
/// - Handling real keyboard/gamepad events and converting them to virtual signals.
/// - Providing a high-fidelity audio backend (via `cpal`).
/// - Implementing the DevTools Protocol for remote debugging.
/// - Maintaining a deterministic 60Hz timing loop.
pub struct HostRunner { pub struct HostRunner {
/// The OS window handle.
window: Option<&'static Window>, window: Option<&'static Window>,
/// The pixel buffer interface for rendering to the GPU.
pixels: Option<Pixels<'static>>, pixels: Option<Pixels<'static>>,
/// The instance of the virtual hardware peripherals.
hardware: Hardware, hardware: Hardware,
/// The instance of the system firmware and OS logic.
firmware: Firmware, firmware: Firmware,
/// Helper to collect and normalize input signals.
input: HostInputHandler, input: HostInputHandler,
/// Root path for the virtual sandbox filesystem.
fs_root: Option<String>, fs_root: Option<String>,
/// Sink for system and application logs (prints to console).
log_sink: HostConsoleSink, log_sink: HostConsoleSink,
/// Target duration for a single frame (nominally 16.66ms for 60Hz).
frame_target_dt: Duration, frame_target_dt: Duration,
/// Last recorded wall-clock time to calculate deltas.
last_frame_time: Instant, last_frame_time: Instant,
/// Time accumulator used to guarantee exact 60Hz logic updates.
accumulator: Duration, accumulator: Duration,
/// Performance metrics collector.
stats: HostStats, stats: HostStats,
/// Remote debugger interface.
debugger: HostDebugger, debugger: HostDebugger,
/// Flag to enable/disable the technical telemetry display.
overlay_enabled: bool, overlay_enabled: bool,
/// The physical audio driver.
audio: HostAudio, audio: HostAudio,
} }
impl HostRunner { impl HostRunner {
/// Configures the boot target (Hub or specific Cartridge).
pub(crate) fn set_boot_target(&mut self, boot_target: BootTarget) { pub(crate) fn set_boot_target(&mut self, boot_target: BootTarget) {
self.firmware.boot_target = boot_target.clone(); self.firmware.boot_target = boot_target.clone();
self.debugger.setup_boot_target(&boot_target, &mut self.firmware); self.debugger.setup_boot_target(&boot_target, &mut self.firmware);
} }
/// Creates a new desktop runner instance.
pub(crate) fn new(fs_root: Option<String>, cap_config: Option<CertificationConfig>) -> Self { pub(crate) fn new(fs_root: Option<String>, cap_config: Option<CertificationConfig>) -> Self {
let target_fps = 60; let target_fps = 60;
@ -203,10 +229,13 @@ impl ApplicationHandler for HostRunner {
} }
} }
/// Called by `winit` when the application is idle and ready to perform updates.
/// This is where the core execution loop lives.
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
// 1. Process pending debug commands from the network.
self.debugger.check_commands(&mut self.firmware, &mut self.hardware); self.debugger.check_commands(&mut self.firmware, &mut self.hardware);
// Updates Filesystem state in OS (specific to prometeu-runtime-desktop) // 2. Maintain filesystem connection if it was lost (e.g., directory removed).
if let Some(root) = &self.fs_root { if let Some(root) = &self.fs_root {
use prometeu_core::fs::FsState; use prometeu_core::fs::FsState;
if matches!(self.firmware.os.fs_state, FsState::Unmounted | FsState::Error(_)) { if matches!(self.firmware.os.fs_state, FsState::Unmounted | FsState::Error(_)) {
@ -217,10 +246,15 @@ impl ApplicationHandler for HostRunner {
} }
} }
// 3. Timing Management (The heart of determinism).
// We measure the elapsed time since the last iteration and add it to an
// accumulator. We then execute exactly as many 60Hz slices as the
// accumulator allows.
let now = Instant::now(); let now = Instant::now();
let mut frame_delta = now.duration_since(self.last_frame_time); let mut frame_delta = now.duration_since(self.last_frame_time);
// Limiter to avoid the "death spiral" if the OS freezes (max 100ms per loop) // Safety cap: if the OS freezes or we fall behind too much, we don't try
// to catch up indefinitely (avoiding the "death spiral").
if frame_delta > Duration::from_millis(100) { if frame_delta > Duration::from_millis(100) {
frame_delta = Duration::from_millis(100); frame_delta = Duration::from_millis(100);
} }
@ -228,24 +262,27 @@ impl ApplicationHandler for HostRunner {
self.last_frame_time = now; self.last_frame_time = now;
self.accumulator += frame_delta; self.accumulator += frame_delta;
// 🔥 The heart of determinism: consumes time in exact 60Hz slices // 🔥 Logic Update Loop: consumes time in exact 60Hz (16.66ms) slices.
while self.accumulator >= self.frame_target_dt { while self.accumulator >= self.frame_target_dt {
// Unless the debugger is waiting for a 'start' command, advance the system.
if !self.debugger.waiting_for_start { if !self.debugger.waiting_for_start {
self.firmware.step_frame(&self.input.signals, &mut self.hardware); self.firmware.step_frame(&self.input.signals, &mut self.hardware);
} }
// Sync virtual audio commands to the physical mixer.
self.audio.send_commands(&mut self.hardware.audio.commands); self.audio.send_commands(&mut self.hardware.audio.commands);
self.accumulator -= self.frame_target_dt; self.accumulator -= self.frame_target_dt;
self.stats.record_frame(); self.stats.record_frame();
} }
// 4. Feedback and Synchronization.
self.audio.update_stats(&mut self.stats); self.audio.update_stats(&mut self.stats);
// Updates statistics every 1 real second // Update technical statistics displayed in the window title.
self.stats.update(now, self.window, &self.hardware, &self.firmware); self.stats.update(now, self.window, &self.hardware, &self.firmware);
// Process system logs // Synchronize system logs to the host console.
let last_seq = self.log_sink.last_seq().unwrap_or(u64::MAX); let last_seq = self.log_sink.last_seq().unwrap_or(u64::MAX);
let new_events = if last_seq == u64::MAX { let new_events = if last_seq == u64::MAX {
self.firmware.os.log_service.get_recent(4096) self.firmware.os.log_service.get_recent(4096)
@ -254,13 +291,15 @@ impl ApplicationHandler for HostRunner {
}; };
self.log_sink.process_events(new_events); self.log_sink.process_events(new_events);
// Telemetry Overlay // 5. Rendering the Telemetry Overlay (if enabled).
if self.overlay_enabled { if self.overlay_enabled {
self.hardware.gfx.present(); // Bring front to back to draw over // We temporarily swap buffers to draw over the current image.
self.hardware.gfx.present();
self.display_dbg_overlay(); self.display_dbg_overlay();
self.hardware.gfx.present(); // Return to front with overlay applied self.hardware.gfx.present();
} }
// Finally, request a window redraw to present the new pixels.
self.request_redraw(); self.request_redraw();
} }
} }

View File

@ -3,6 +3,12 @@ use std::env;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
/// PROMETEU Dispatcher (CLI).
///
/// The main entry point for the user. This binary does not implement
/// compilation or execution logic itself; instead, it acts as a smart
/// front-end that locates and dispatches commands to specialized
/// components like `prometeu-runtime` or `prometeuc`.
#[derive(Parser)] #[derive(Parser)]
#[command(name = "prometeu")] #[command(name = "prometeu")]
#[command(about = "Dispatcher for the Prometeu ecosystem", long_about = None)] #[command(about = "Dispatcher for the Prometeu ecosystem", long_about = None)]
@ -13,30 +19,31 @@ struct Cli {
#[derive(Subcommand)] #[derive(Subcommand)]
enum Commands { enum Commands {
/// Executes a cartridge /// Executes a cartridge using the available runtime.
Run { Run {
/// Path to the cartridge /// Path to the cartridge (directory or .pmc file).
cart: String, cart: String,
}, },
/// Debugs a cartridge /// Executes a cartridge in Assisted Mode (Debug).
/// The runtime will wait for a DevTools connection before starting.
Debug { Debug {
/// Path to the cartridge /// Path to the cartridge.
cart: String, cart: String,
/// Port for the debugger (default: 7777) /// TCP port for the DevTools server (default: 7777).
#[arg(long, default_value_t = 7777)] #[arg(long, default_value_t = 7777)]
port: u16, port: u16,
}, },
/// Builds a project /// Compiles a source project into a cartridge (PBC).
Build { Build {
/// Project directory /// Project source directory.
project_dir: String, project_dir: String,
}, },
/// Packages a cartridge /// Packages a cartridge directory into a distributable .pmc file.
Pack { Pack {
/// Cartridge directory /// Cartridge directory.
cart_dir: String, cart_dir: String,
}, },
/// Verifies the integrity of a project or cartridge /// Diagnostic commands to verify project or cartridge integrity.
Verify { Verify {
#[command(subcommand)] #[command(subcommand)]
target: VerifyCommands, target: VerifyCommands,