added comments extensively

This commit is contained in:
bQUARKz 2026-01-19 07:47:54 +00:00 committed by Nilton Constantino
parent 6468fc279d
commit 3f8033bdb8
No known key found for this signature in database
10 changed files with 446 additions and 104 deletions

View File

@ -9,16 +9,28 @@ use crate::virtual_machine::VirtualMachine;
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 {
/// The Virtual Machine instance.
pub vm: VirtualMachine,
/// The System Operating logic (syscalls, telemetry, logs).
pub os: PrometeuOS,
/// The System UI / Launcher environment.
pub hub: PrometeuHub,
/// Current high-level state of the system.
pub state: FirmwareState,
/// Desired execution target resolved at boot.
pub boot_target: BootTarget,
/// Tracking flag to ensure `on_enter` is called exactly once per state transition.
state_initialized: bool,
}
impl Firmware {
/// Initializes the firmware in the `Reset` state.
pub fn new(cap_config: Option<CertificationConfig>) -> Self {
Self {
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) {
// 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.touch_mut().begin_frame(signals);
// 2. State machine lifecycle management.
if !self.state_initialized {
self.on_enter(signals, hw);
self.state_initialized = true;
}
// 3. Update the current state and check for transitions.
if let Some(next_state) = self.on_update(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) {
self.on_exit(signals, hw);
self.state = new_state;
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.state_initialized = true;
}
/// Dispatches the `on_enter` event to the current state implementation.
fn on_enter(&mut self, signals: &InputSignals, hw: &mut dyn HardwareBridge) {
let mut req = PrometeuContext {
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> {
let mut req = PrometeuContext {
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) {
let mut req = PrometeuContext {
vm: &mut self.vm,

View File

@ -1,22 +1,39 @@
use crate::model::Sample;
use std::sync::Arc;
/// Maximum number of simultaneous audio voices supported by the hardware.
pub const MAX_CHANNELS: usize = 16;
/// Standard sample rate for the final audio output.
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 {
/// Play once and stop.
#[default]
Off,
/// Return to the start (or loop_start) when reaching the end.
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 {
/// Reference to the PCM data being played.
pub sample: Option<Arc<Sample>>,
/// Current playback position within the sample (fractional for pitch shifting).
pub pos: f64,
/// Playback speed multiplier (1.0 = original speed).
pub pitch: f64,
pub volume: u8, // 0..255
pub pan: u8, // 0..255
/// Voice volume (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,
/// Playback priority (used for voice stealing policies).
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 {
/// Start playing a sample on a specific voice.
Play {
sample: Arc<Sample>,
voice_id: usize,
@ -44,32 +66,54 @@ pub enum AudioCommand {
priority: u8,
loop_mode: LoopMode,
},
/// Immediately stop playback on a voice.
Stop {
voice_id: usize,
},
/// Update volume of an ongoing playback.
SetVolume {
voice_id: usize,
volume: u8,
},
/// Update panning of an ongoing playback.
SetPan {
voice_id: usize,
pan: u8,
},
/// Update pitch of an ongoing playback.
SetPitch {
voice_id: usize,
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 {
/// Local state of the 16 hardware voices.
pub voices: [Channel; MAX_CHANNELS],
/// Queue of pending commands to be processed by the Host mixer.
pub commands: Vec<AudioCommand>,
}
impl Audio {
/// Initializes the audio system with empty voices.
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 {
voices: Default::default(),
voices: [EMPTY_CHANNEL; MAX_CHANNELS],
commands: Vec::new(),
}
}

View File

@ -1,42 +1,71 @@
use crate::model::{Color, HudTileLayer, ScrollableTileLayer, Sprite, TileBank, TileMap, TileSize};
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)]
pub enum BlendMode {
/// dst = src
/// No blending: source overwrites destination.
#[default]
None,
/// dst = (src + dst) / 2
/// Average: dst = (src + dst) / 2. Creates a semi-transparent effect.
Half,
/// dst = dst + (src / 2)
/// Additive: dst = dst + (src / 2). Good for glows/light.
HalfPlus,
/// dst = dst - (src / 2)
/// Subtractive: dst = dst - (src / 2). Good for shadows.
HalfMinus,
/// dst = dst + src
/// Full Additive: dst = dst + src. Saturated light effect.
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 {
/// Width of the internal framebuffer.
w: usize,
/// Height of the internal framebuffer.
h: usize,
/// Front buffer: the one currently being displayed by the Host.
front: Vec<u16>,
/// Back buffer: the one being drawn to during the current frame.
back: Vec<u16>,
/// 4 scrollable backgrounds.
pub layers: [ScrollableTileLayer; 4],
/// 1 fixed layer for User Interface.
pub hud: HudTileLayer,
/// Up to 16 sets of graphical assets (tiles + palettes).
pub banks: [Option<TileBank>; 16],
/// Hardware sprites (Object Attribute Memory equivalent).
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 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,
// Priority cache to avoid per-frame allocations
/// Internal cache used to sort sprites by priority without allocations.
priority_buckets: [Vec<usize>; 5],
}
impl Gfx {
/// Initializes the graphics system with a specific resolution.
pub fn new(w: usize, h: usize) -> Self {
const EMPTY_BANK: Option<TileBank> = None;
const EMPTY_SPRITE: Sprite = Sprite {
@ -51,7 +80,7 @@ impl Gfx {
};
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),
@ -130,15 +159,19 @@ impl Gfx {
}
/// 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) {
std::mem::swap(&mut self.front, &mut self.back);
}
/// Main frame rendering pipeline.
/// Follows the priority order from the manual (Chapter 4.11).
/// The main rendering pipeline.
///
/// 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) {
// 0. Preparation Phase: Organizes what should be drawn in each layer
// Clears the buckets without deallocating memory
// 0. Preparation Phase: Filter and group sprites by their priority levels.
// This avoids iterating through all 512 sprites for every layer.
for bucket in self.priority_buckets.iter_mut() {
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);
// 2. Main layers and prioritized sprites.
// Order: Layer 0 -> Sprites 1 -> Layer 1 -> Sprites 2 ...
for i in 0..self.layers.len() {
// 2. Game Layers (0 to 3)
let bank_id = self.layers[i].bank_id as usize;
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);
}
// 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);
}
// 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);
// 5. HUD (Always on top)
// 5. HUD: The fixed interface layer, always drawn on top of the world.
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);
}
/// Renders a specific game layer.
pub fn render_layer(&mut self, layer_idx: usize) {
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);
}
/// Renders the HUD (no scroll).
/// Renders the HUD (fixed position, no scroll).
pub fn render_hud(&mut self) {
let bank_id = self.hud.bank_id as usize;
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);
}
/// Rasterizes a TileMap into the provided pixel buffer using scrolling.
fn draw_tile_map(
back: &mut [u16],
screen_w: usize,
@ -210,30 +246,34 @@ impl Gfx {
) {
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_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_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_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 tx in 0..visible_tiles_x {
let map_x = (start_tile_x + tx 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; }
let tile = map.tiles[map_y * map.width + map_x];
// Optimized skip for empty (ID 0) tiles.
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_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) {
let size = bank.tile_size as usize;
@ -254,16 +295,17 @@ impl Gfx {
let world_x = x + local_x as i32;
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_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);
// 2. Rule: Index 0 is transparent
// 2. Hardware rule: Color index 0 is always fully transparent.
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);
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};
/// 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 {
/// The Graphics Processing Unit.
pub gfx: Gfx,
/// The Sound Processing Unit.
pub audio: Audio,
/// The standard digital gamepad.
pub pad: Pad,
/// The absolute pointer input device.
pub touch: Touch,
}
@ -22,9 +30,12 @@ impl HardwareBridge for Hardware {
}
impl Hardware {
/// Internal hardware width in pixels.
pub const W: usize = 320;
/// Internal hardware height in pixels.
pub const H: usize = 180;
/// Creates a fresh hardware instance with default settings.
pub fn new() -> Self {
Self {
gfx: Gfx::new(Self::W, Self::H),

View File

@ -1,24 +1,38 @@
use crate::model::Color;
/// Standard sizes for square tiles.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum TileSize {
/// 8x8 pixels.
Size8 = 8,
/// 16x16 pixels.
Size16 = 16,
/// 32x32 pixels.
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 {
/// Dimension of each individual tile in the bank.
pub tile_size: TileSize,
pub width: usize, // in pixels
pub height: usize, // in pixels
/// Width of the full bank sheet 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>,
/// 256 palettes, each with 16 colors (RGB565 as u16)
/// Table of 256 palettes, each containing 16 RGB565 colors.
pub palettes: [[Color; 16]; 256],
}
impl TileBank {
/// Creates an empty tile bank with the specified dimensions.
pub fn new(tile_size: TileSize, width: usize, height: usize) -> Self {
Self {
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
/// 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 {
@ -44,13 +58,14 @@ impl TileBank {
if pixel_x < self.width && pixel_y < self.height {
self.pixel_indices[pixel_y * self.width + pixel_x]
} 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 {
// 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 {
return Color::COLOR_KEY;
}

View File

@ -10,12 +10,19 @@ use std::sync::Arc;
use std::time::Instant;
/// PrometeuOS (POS): The system firmware/base.
///
/// 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 {
/// Incremented on every host tick (60Hz).
pub tick_index: u64,
/// Incremented every time a logical game frame is successfully completed.
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,
/// Number of virtual cycles still available for the current logical frame.
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,
// Example assets (kept for compatibility with v0 audio syscalls)
@ -24,36 +31,54 @@ pub struct PrometeuOS {
pub sample_snare: Option<Arc<Sample>>,
// Filesystem
/// The virtual filesystem interface.
pub fs: VirtualFS,
/// Current state of the FS (Mounted, Error, etc).
pub fs_state: FsState,
/// Mapping of numeric handles to file paths for open files.
pub open_files: HashMap<u32, String>,
/// Sequential handle generator for file operations.
pub next_handle: u32,
// Log Service
pub log_service: LogService,
/// Unique ID of the currently running application.
pub current_app_id: u32,
pub current_cartridge_title: String,
pub current_cartridge_app_version: String,
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>,
// Telemetry and Certification
/// Accumulator for metrics of the frame currently being executed.
pub telemetry_current: TelemetryFrame,
/// Snapshot of the metrics from the last completed logical frame.
pub telemetry_last: TelemetryFrame,
/// Logic for evaluating compliance with the active CAP profile.
pub certifier: Certifier,
/// Global pause flag (used by debugger or system events).
pub paused: bool,
/// Request from debugger to run exactly one instruction or frame.
pub debug_step_request: bool,
/// Instant when the system was initialized.
boot_time: Instant,
}
impl PrometeuOS {
/// Default number of cycles assigned to a single game frame.
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;
/// Maximum log entries an App can emit in a single frame.
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 {
let boot_time = Instant::now();
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> {
let start = std::time::Instant::now();
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 {
return None;
}
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 {
self.logical_frame_active = true;
self.logical_frame_remaining_cycles = Self::CYCLES_PER_LOGICAL_FRAME;
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 {
frame_index: self.logical_frame_index,
..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);
// 3. VM Execution
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);
match run_result {
Ok(run) => {
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.vm_steps += run.steps_executed;
// Handle Breakpoints
if run.reason == crate::virtual_machine::LogicalFrameEndingReason::Breakpoint {
self.paused = true;
self.debug_step_request = false;
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 {
// All drawing commands for this frame are now complete.
// Finalize the framebuffer.
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;
// Certification (CAP)
// Evaluate CAP (Execution Budget Compliance)
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;
// Latch: what the overlay reads
// Latch telemetry for the Host/Debugger to read.
self.telemetry_last = self.telemetry_current;
self.logical_frame_index += 1;
self.logical_frame_active = false;
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 {
self.paused = true;
self.debug_step_request = false;
@ -226,6 +265,7 @@ impl PrometeuOS {
}
}
Err(e) => {
// Fatal VM fault (division by zero, invalid memory access, etc).
let err_msg = format!("PVM Fault: {:?}", e);
self.log(LogLevel::Error, LogSource::Vm, 0, err_msg.clone());
return Some(err_msg);
@ -235,7 +275,7 @@ impl PrometeuOS {
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) {
self.telemetry_last.host_cpu_time_us = self.last_frame_cpu_time_us;
}
@ -493,23 +533,35 @@ mod tests {
}
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> {
self.telemetry_current.syscalls += 1;
match id {
// --- System Syscalls ---
// system.has_cart() -> bool
0x0001 => {
// virtual_machine.push(Value::Boolean(self.current_cartridge.is_some()));
// Returns true if a cartridge is available.
Ok(10)
}
// system.run_cart()
0x0002 => {
// if let Some(cart) = self.current_cartridge.as_ref().cloned() {
// self.load_cartridge(&cart);
// } else {
// return Err("No cartridge inserted".into());
// }
// Triggers loading and execution of the current cartridge.
Ok(100)
}
// --- GFX Syscalls ---
// gfx.clear(color_index)
0x1001 => {
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);
Ok(200)
}
// --- Input Syscalls ---
// input.get_pad(button_id) -> bool
0x2001 => {
let button_id = vm.pop_integer()? as u32;
@ -535,6 +590,9 @@ impl NativeInterface for PrometeuOS {
vm.push(Value::Boolean(is_down));
Ok(50)
}
// --- Audio Syscalls ---
// audio.play_sample(sample_id, voice_id, volume, pan, pitch)
0x3001 => {
let pitch = vm.pop_number()?;
@ -559,6 +617,7 @@ impl NativeInterface for PrometeuOS {
// --- Filesystem Syscalls (0x4000) ---
// FS_OPEN(path) -> handle
// Opens a file in the virtual sandbox and returns a numeric handle.
0x4001 => {
let path = match vm.pop()? {
Value::String(s) => s,
@ -623,7 +682,7 @@ impl NativeInterface for PrometeuOS {
};
match self.fs.list_dir(&path) {
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();
vm.push(Value::String(names.join(";")));
Ok(500)

View File

@ -5,35 +5,61 @@ use crate::virtual_machine::opcode::OpCode;
use crate::virtual_machine::value::Value;
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)]
pub enum LogicalFrameEndingReason {
/// Execution reached a `FRAME_SYNC` instruction, marking the end of the logical frame.
FrameSync,
/// The cycle budget for the current host tick was exhausted before reaching `FRAME_SYNC`.
BudgetExhausted,
/// A `HALT` instruction was executed, terminating the program.
Halted,
/// The Program Counter (PC) reached the end of the available bytecode.
EndOfRom,
/// Execution hit a registered breakpoint.
Breakpoint,
}
/// A report detailing the results of an execution slice (run_budget).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BudgetReport {
/// Total virtual cycles consumed during this run.
pub cycles_used: u64,
/// Number of VM instructions executed.
pub steps_executed: u32,
/// The reason why this execution slice ended.
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 {
/// Program Counter: points to the next byte in the ROM to be executed.
pub pc: usize,
/// Operand Stack: used for intermediate calculations and passing arguments to opcodes.
pub operand_stack: Vec<Value>,
/// Call Stack: stores execution frames for function calls and local variables.
pub call_stack: Vec<CallFrame>,
/// Globals: storage for persistent variables that survive between frames.
pub globals: Vec<Value>,
/// The currently loaded program (Bytecode + Constant Pool).
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,
/// Flag indicating if the VM has been stopped by a `HALT` instruction.
pub halted: bool,
/// A set of PC addresses where execution should pause.
pub breakpoints: std::collections::HashSet<usize>,
}
impl VirtualMachine {
/// Creates a new VM instance with the provided bytecode and constants.
pub fn new(rom: Vec<u8>, constant_pool: Vec<Value>) -> Self {
Self {
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) {
// 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 let Ok((rom, cp)) = self.parse_pbc(&program_bytes) {
self.program = Program::new(rom, cp);
} else {
// Fallback for raw bytes if PBC parsing fails
self.program = Program::new(program_bytes, vec![]);
}
} 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![]);
}
// If the entrypoint is numeric, we can try to use it as the initial PC.
// If not, for now we ignore it or start from 0.
// Resolve the entrypoint. Currently supports numeric addresses.
if let Ok(addr) = entrypoint.parse::<usize>() {
self.pc = addr;
} else {
self.pc = 0;
}
// Full state reset to ensure a clean start for the App
self.operand_stack.clear();
self.call_stack.clear();
self.globals.clear();
@ -76,9 +107,12 @@ impl VirtualMachine {
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> {
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 mut cp = Vec::with_capacity(cp_count);
@ -88,11 +122,11 @@ impl VirtualMachine {
cursor += 1;
match tag {
1 => { // Integer
1 => { // Integer (64-bit)
let val = self.read_i64_at(bytes, &mut cursor)?;
cp.push(Value::Integer(val));
}
2 => { // Float
2 => { // Float (64-bit)
let val = self.read_f64_at(bytes, &mut cursor)?;
cp.push(Value::Float(val));
}
@ -102,7 +136,7 @@ impl VirtualMachine {
cursor += 1;
cp.push(Value::Boolean(val));
}
4 => { // String
4 => { // String (UTF-8)
let len = self.read_u32_at(bytes, &mut cursor)? as usize;
if cursor + len > bytes.len() { return Err("Unexpected end of PBC".into()); }
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;
if cursor + rom_size > bytes.len() {
return Err("Invalid ROM size in PBC".into());
@ -151,6 +186,16 @@ impl Default for 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(
&mut self,
budget: u64,
@ -165,6 +210,9 @@ impl VirtualMachine {
&& !self.halted
&& 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) {
ending_reason = Some(LogicalFrameEndingReason::Breakpoint);
break;
@ -173,26 +221,31 @@ impl VirtualMachine {
let pc_before = self.pc;
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 = OpCode::try_from(opcode_val)?;
if opcode == OpCode::FrameSync {
self.pc += 2;
self.pc += 2; // Advance PC past the opcode
self.cycles += OpCode::FrameSync.cycles();
steps_executed += 1;
ending_reason = Some(LogicalFrameEndingReason::FrameSync);
break;
}
// Execute a single step (Fetch-Decode-Execute)
self.step(native, hw)?;
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 {
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 self.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> {
if self.pc + 2 > self.program.rom.len() {
return Err("Unexpected end of ROM".into());
@ -221,14 +275,22 @@ impl VirtualMachine {
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> {
if self.halted || self.pc >= self.program.rom.len() {
return Ok(());
}
// Fetch & Decode
let opcode_val = self.read_u16()?;
let opcode = OpCode::try_from(opcode_val)?;
// Execute
match opcode {
OpCode::Nop => {}
OpCode::Halt => {
@ -369,6 +431,8 @@ impl VirtualMachine {
self.operand_stack[stack_idx] = val;
}
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 args_count = self.read_u32()? as usize;
let stack_base = self.operand_stack.len() - args_count;
@ -382,18 +446,21 @@ impl VirtualMachine {
OpCode::Ret => {
let frame = self.call_stack.pop().ok_or("Call stack underflow")?;
let return_val = self.pop()?;
// Clean up the operand stack, removing the frame's locals
self.operand_stack.truncate(frame.stack_base);
// Return the result of the function
self.push(return_val);
self.pc = frame.return_address;
}
OpCode::PushScope => {
// Used for blocks within a function that have their own locals
let locals_count = self.read_u32()? as usize;
let stack_base = self.operand_stack.len();
for _ in 0..locals_count {
self.push(Value::Null);
}
self.call_stack.push(CallFrame {
return_address: 0,
return_address: 0, // Scope blocks don't return via PC jump
stack_base,
locals_count,
});
@ -403,6 +470,7 @@ impl VirtualMachine {
self.operand_stack.truncate(frame.stack_base);
}
OpCode::Alloc => {
// Allocates 'size' values on the heap and pushes a reference to the stack
let size = self.read_u32()? as usize;
let ref_idx = self.heap.len();
for _ in 0..size {
@ -411,6 +479,7 @@ impl VirtualMachine {
self.push(Value::Ref(ref_idx));
}
OpCode::LoadRef => {
// Reads a value from a heap reference at a specific offset
let offset = self.read_u32()? as usize;
let ref_val = self.pop()?;
if let Value::Ref(base) = ref_val {
@ -421,6 +490,7 @@ impl VirtualMachine {
}
}
OpCode::StoreRef => {
// Writes a value to a heap reference at a specific offset
let offset = self.read_u32()? as usize;
let val = self.pop()?;
let ref_val = self.pop()?;
@ -434,15 +504,18 @@ impl VirtualMachine {
}
}
OpCode::Syscall => {
// Calls a native function implemented by the Firmware/OS
let id = self.read_u32()?;
let native_cycles = native.syscall(id, self, hw).map_err(|e| format!("syscall 0x{:08X} failed: {}", id, e))?;
self.cycles += native_cycles;
}
OpCode::FrameSync => {
// Already handled in the run_budget loop for performance
return Ok(());
}
}
// Apply the instruction cost to the cycle counter
self.cycles += opcode.cycles();
Ok(())
}

View File

@ -5,15 +5,27 @@ use prometeu_core::debugger_protocol::*;
use std::net::{TcpListener, TcpStream};
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 {
/// If true, the VM will not start execution until a 'start' command is received.
pub waiting_for_start: bool,
/// The TCP listener for incoming debugger connections.
pub(crate) listener: Option<TcpListener>,
/// The currently active connection to a debugger client.
pub(crate) stream: Option<TcpStream>,
/// Sequence tracker to ensure logs are sent only once.
last_log_seq: u64,
/// Frame tracker to send telemetry snapshots periodically.
last_telemetry_frame: u64,
}
impl HostDebugger {
/// Creates a new debugger interface in an idle state.
pub fn new() -> Self {
Self {
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) {
if let BootTarget::Cartridge { path, debug: true, debug_port } = boot_target {
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) {
firmware.os.initialize_vm(&mut firmware.vm, &cartridge);
}
match TcpListener::bind(format!("127.0.0.1:{}", debug_port)) {
Ok(listener) => {
// Set listener to non-blocking so it doesn't halt the main loop.
listener.set_nonblocking(true).expect("Cannot set non-blocking");
self.listener = Some(listener);
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) {
if let Some(stream) = &mut self.stream {
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) {
if let Some(stream) = &mut self.stream {
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) {
// 1. Accept new client connections.
if let Some(listener) = &self.listener {
if let Ok((stream, _addr)) = listener.accept() {
// Currently, only one debugger client is supported at a time.
if self.stream.is_none() {
println!("[Debugger] Connection received!");
stream.set_nonblocking(true).expect("Cannot set non-blocking on stream");
self.stream = Some(stream);
// Send Handshake
// Immediately send the Handshake message to identify the Runtime and App.
let handshake = DebugResponse::Handshake {
protocol_version: DEVTOOLS_PROTOCOL_VERSION,
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() {
let mut buf = [0u8; 4096];
match stream.read(&mut buf) {
Ok(0) => {
// TCP socket closed by the client.
println!("[Debugger] Connection closed by remote.");
self.stream = None;
// Resume VM execution if it was paused waiting for the debugger.
firmware.os.paused = false;
self.waiting_for_start = false;
}
Ok(n) => {
let data = &buf[..n];
// Process multiple commands if there's \n
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() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if trimmed.is_empty() { continue; }
if let Ok(cmd) = serde_json::from_str::<DebugCommand>(trimmed) {
self.handle_command(cmd, firmware, hardware);
}
}
}
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
// No data available right now, continue.
self.stream = Some(stream);
}
Err(e) => {
@ -131,12 +155,13 @@ impl HostDebugger {
}
}
// Streaming de eventos
// 3. Push events (logs, telemetry) to the client.
if self.stream.is_some() {
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) {
match cmd {
DebugCommand::Ok | DebugCommand::Start => {
@ -153,15 +178,17 @@ impl HostDebugger {
firmware.os.paused = false;
}
DebugCommand::Step => {
// Execute exactly one instruction and keep paused.
firmware.os.paused = true;
// Executes an instruction immediately
let _ = firmware.os.debug_step_instruction(&mut firmware.vm, hardware);
}
DebugCommand::StepFrame => {
// Execute until the end of the current logical frame.
firmware.os.paused = false;
firmware.os.debug_step_request = true;
}
DebugCommand::GetState => {
// Return detailed VM register and stack state.
let stack_top = firmware.vm.operand_stack.iter()
.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) {
// Logs
// 1. Process and send new log entries.
let new_events = firmware.os.log_service.get_after(self.last_log_seq);
for event in new_events {
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 {
self.send_event(DebugEvent::BreakpointHit {
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 {
let rule = match event.tag {
0xCA01 => "cycles_budget",
@ -211,7 +239,7 @@ impl HostDebugger {
self.send_event(DebugEvent::Cert {
rule,
used: 0, // Simplified, detailed information is in the log message
used: 0,
limit: 0,
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;
if current_frame > self.last_telemetry_frame {
let tel = &firmware.os.telemetry_last;

View File

@ -18,36 +18,62 @@ use winit::window::{Window, WindowAttributes, WindowId};
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 {
/// The OS window handle.
window: Option<&'static Window>,
/// The pixel buffer interface for rendering to the GPU.
pixels: Option<Pixels<'static>>,
/// The instance of the virtual hardware peripherals.
hardware: Hardware,
/// The instance of the system firmware and OS logic.
firmware: Firmware,
/// Helper to collect and normalize input signals.
input: HostInputHandler,
/// Root path for the virtual sandbox filesystem.
fs_root: Option<String>,
/// Sink for system and application logs (prints to console).
log_sink: HostConsoleSink,
/// Target duration for a single frame (nominally 16.66ms for 60Hz).
frame_target_dt: Duration,
/// Last recorded wall-clock time to calculate deltas.
last_frame_time: Instant,
/// Time accumulator used to guarantee exact 60Hz logic updates.
accumulator: Duration,
/// Performance metrics collector.
stats: HostStats,
/// Remote debugger interface.
debugger: HostDebugger,
/// Flag to enable/disable the technical telemetry display.
overlay_enabled: bool,
/// The physical audio driver.
audio: HostAudio,
}
impl HostRunner {
/// Configures the boot target (Hub or specific Cartridge).
pub(crate) fn set_boot_target(&mut self, boot_target: BootTarget) {
self.firmware.boot_target = boot_target.clone();
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 {
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) {
// 1. Process pending debug commands from the network.
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 {
use prometeu_core::fs::FsState;
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 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) {
frame_delta = Duration::from_millis(100);
}
@ -228,24 +262,27 @@ impl ApplicationHandler for HostRunner {
self.last_frame_time = now;
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 {
// Unless the debugger is waiting for a 'start' command, advance the system.
if !self.debugger.waiting_for_start {
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.accumulator -= self.frame_target_dt;
self.stats.record_frame();
}
// 4. Feedback and Synchronization.
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);
// Process system logs
// Synchronize system logs to the host console.
let last_seq = self.log_sink.last_seq().unwrap_or(u64::MAX);
let new_events = if last_seq == u64::MAX {
self.firmware.os.log_service.get_recent(4096)
@ -254,13 +291,15 @@ impl ApplicationHandler for HostRunner {
};
self.log_sink.process_events(new_events);
// Telemetry Overlay
// 5. Rendering the Telemetry Overlay (if 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.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();
}
}

View File

@ -3,6 +3,12 @@ use std::env;
use std::path::{Path, PathBuf};
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)]
#[command(name = "prometeu")]
#[command(about = "Dispatcher for the Prometeu ecosystem", long_about = None)]
@ -13,30 +19,31 @@ struct Cli {
#[derive(Subcommand)]
enum Commands {
/// Executes a cartridge
/// Executes a cartridge using the available runtime.
Run {
/// Path to the cartridge
/// Path to the cartridge (directory or .pmc file).
cart: String,
},
/// Debugs a cartridge
/// Executes a cartridge in Assisted Mode (Debug).
/// The runtime will wait for a DevTools connection before starting.
Debug {
/// Path to the cartridge
/// Path to the cartridge.
cart: String,
/// Port for the debugger (default: 7777)
/// TCP port for the DevTools server (default: 7777).
#[arg(long, default_value_t = 7777)]
port: u16,
},
/// Builds a project
/// Compiles a source project into a cartridge (PBC).
Build {
/// Project directory
/// Project source directory.
project_dir: String,
},
/// Packages a cartridge
/// Packages a cartridge directory into a distributable .pmc file.
Pack {
/// Cartridge directory
/// Cartridge directory.
cart_dir: String,
},
/// Verifies the integrity of a project or cartridge
/// Diagnostic commands to verify project or cartridge integrity.
Verify {
#[command(subcommand)]
target: VerifyCommands,