added comments extensively
This commit is contained in:
parent
0f3105d622
commit
13ecbd1877
@ -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,
|
||||
|
||||
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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(())
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user