diff --git a/crates/prometeu-core/src/firmware/firmware.rs b/crates/prometeu-core/src/firmware/firmware.rs index cc31a84d..a6d109af 100644 --- a/crates/prometeu-core/src/firmware/firmware.rs +++ b/crates/prometeu-core/src/firmware/firmware.rs @@ -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) -> 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 { 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, diff --git a/crates/prometeu-core/src/hardware/audio.rs b/crates/prometeu-core/src/hardware/audio.rs index 6c4d918f..849cc3dd 100644 --- a/crates/prometeu-core/src/hardware/audio.rs +++ b/crates/prometeu-core/src/hardware/audio.rs @@ -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>, + /// 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, 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, } 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(), } } diff --git a/crates/prometeu-core/src/hardware/gfx.rs b/crates/prometeu-core/src/hardware/gfx.rs index a4f8b2f2..3e7ab817 100644 --- a/crates/prometeu-core/src/hardware/gfx.rs +++ b/crates/prometeu-core/src/hardware/gfx.rs @@ -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, + /// Back buffer: the one being drawn to during the current frame. back: Vec, + /// 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; 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; 5], } impl Gfx { + /// Initializes the graphics system with a specific resolution. pub fn new(w: usize, h: usize) -> Self { const EMPTY_BANK: Option = 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(); diff --git a/crates/prometeu-core/src/hardware/hardware.rs b/crates/prometeu-core/src/hardware/hardware.rs index 031c8468..8495c9cf 100644 --- a/crates/prometeu-core/src/hardware/hardware.rs +++ b/crates/prometeu-core/src/hardware/hardware.rs @@ -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), diff --git a/crates/prometeu-core/src/model/tile_bank.rs b/crates/prometeu-core/src/model/tile_bank.rs index a7bb1522..47c7489f 100644 --- a/crates/prometeu-core/src/model/tile_bank.rs +++ b/crates/prometeu-core/src/model/tile_bank.rs @@ -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, - /// 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; } diff --git a/crates/prometeu-core/src/prometeu_os/prometeu_os.rs b/crates/prometeu-core/src/prometeu_os/prometeu_os.rs index ff4f6ec3..8a0657af 100644 --- a/crates/prometeu-core/src/prometeu_os/prometeu_os.rs +++ b/crates/prometeu-core/src/prometeu_os/prometeu_os.rs @@ -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>, // 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, + /// 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, // 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) -> 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 { 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 { 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 = entries.into_iter().map(|e| e.name).collect(); vm.push(Value::String(names.join(";"))); Ok(500) diff --git a/crates/prometeu-core/src/virtual_machine/virtual_machine.rs b/crates/prometeu-core/src/virtual_machine/virtual_machine.rs index 53673f65..c97f371f 100644 --- a/crates/prometeu-core/src/virtual_machine/virtual_machine.rs +++ b/crates/prometeu-core/src/virtual_machine/virtual_machine.rs @@ -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, + /// Call Stack: stores execution frames for function calls and local variables. pub call_stack: Vec, + /// Globals: storage for persistent variables that survive between frames. pub globals: Vec, + /// The currently loaded program (Bytecode + Constant Pool). pub program: Program, - pub heap: Vec, // Simplified for demo, future real RAM/Heap + /// Dynamic memory region for complex structures (Simplified in current version). + pub heap: Vec, + /// 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, } impl VirtualMachine { + /// Creates a new VM instance with the provided bytecode and constants. pub fn new(rom: Vec, constant_pool: Vec) -> 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, 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::() { 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, Vec), 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 { 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(()) } diff --git a/crates/prometeu-runtime-desktop/src/debugger.rs b/crates/prometeu-runtime-desktop/src/debugger.rs index 943dedf8..84247690 100644 --- a/crates/prometeu-runtime-desktop/src/debugger.rs +++ b/crates/prometeu-runtime-desktop/src/debugger.rs @@ -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, + /// The currently active connection to a debugger client. pub(crate) stream: Option, + /// 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::(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; diff --git a/crates/prometeu-runtime-desktop/src/runner.rs b/crates/prometeu-runtime-desktop/src/runner.rs index 22e45475..6ae61c74 100644 --- a/crates/prometeu-runtime-desktop/src/runner.rs +++ b/crates/prometeu-runtime-desktop/src/runner.rs @@ -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>, + /// 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, + /// 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, cap_config: Option) -> 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(); } } diff --git a/crates/prometeu/src/main.rs b/crates/prometeu/src/main.rs index ea7d6ad2..43c51a6f 100644 --- a/crates/prometeu/src/main.rs +++ b/crates/prometeu/src/main.rs @@ -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,