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;
|
use crate::telemetry::CertificationConfig;
|
||||||
|
|
||||||
|
/// PROMETEU Firmware.
|
||||||
|
///
|
||||||
|
/// The central orchestrator of the system. It manages the high-level state machine,
|
||||||
|
/// transitioning between system states like booting, the Hub launcher, and
|
||||||
|
/// actual application execution.
|
||||||
pub struct Firmware {
|
pub struct Firmware {
|
||||||
|
/// The Virtual Machine instance.
|
||||||
pub vm: VirtualMachine,
|
pub vm: VirtualMachine,
|
||||||
|
/// The System Operating logic (syscalls, telemetry, logs).
|
||||||
pub os: PrometeuOS,
|
pub os: PrometeuOS,
|
||||||
|
/// The System UI / Launcher environment.
|
||||||
pub hub: PrometeuHub,
|
pub hub: PrometeuHub,
|
||||||
|
/// Current high-level state of the system.
|
||||||
pub state: FirmwareState,
|
pub state: FirmwareState,
|
||||||
|
/// Desired execution target resolved at boot.
|
||||||
pub boot_target: BootTarget,
|
pub boot_target: BootTarget,
|
||||||
|
/// Tracking flag to ensure `on_enter` is called exactly once per state transition.
|
||||||
state_initialized: bool,
|
state_initialized: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Firmware {
|
impl Firmware {
|
||||||
|
/// Initializes the firmware in the `Reset` state.
|
||||||
pub fn new(cap_config: Option<CertificationConfig>) -> Self {
|
pub fn new(cap_config: Option<CertificationConfig>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
vm: VirtualMachine::default(),
|
vm: VirtualMachine::default(),
|
||||||
@ -30,31 +42,40 @@ impl Firmware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The main entry point for the Host to advance the system logic.
|
||||||
|
///
|
||||||
|
/// This method is called exactly once per Host frame (60Hz).
|
||||||
|
/// It updates peripheral signals and delegates the logic to the current state.
|
||||||
pub fn step_frame(&mut self, signals: &InputSignals, hw: &mut dyn HardwareBridge) {
|
pub fn step_frame(&mut self, signals: &InputSignals, hw: &mut dyn HardwareBridge) {
|
||||||
// Updates input once per host frame
|
// 1. Update peripheral state using the latest signals from the Host.
|
||||||
|
// This ensures input is consistent throughout the entire update.
|
||||||
hw.pad_mut().begin_frame(signals);
|
hw.pad_mut().begin_frame(signals);
|
||||||
hw.touch_mut().begin_frame(signals);
|
hw.touch_mut().begin_frame(signals);
|
||||||
|
|
||||||
|
// 2. State machine lifecycle management.
|
||||||
if !self.state_initialized {
|
if !self.state_initialized {
|
||||||
self.on_enter(signals, hw);
|
self.on_enter(signals, hw);
|
||||||
self.state_initialized = true;
|
self.state_initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. Update the current state and check for transitions.
|
||||||
if let Some(next_state) = self.on_update(signals, hw) {
|
if let Some(next_state) = self.on_update(signals, hw) {
|
||||||
self.change_state(next_state, signals, hw);
|
self.change_state(next_state, signals, hw);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Transitions the system to a new state, handling lifecycle hooks.
|
||||||
pub fn change_state(&mut self, new_state: FirmwareState, signals: &InputSignals, hw: &mut dyn HardwareBridge) {
|
pub fn change_state(&mut self, new_state: FirmwareState, signals: &InputSignals, hw: &mut dyn HardwareBridge) {
|
||||||
self.on_exit(signals, hw);
|
self.on_exit(signals, hw);
|
||||||
self.state = new_state;
|
self.state = new_state;
|
||||||
self.state_initialized = false;
|
self.state_initialized = false;
|
||||||
|
|
||||||
// Enters the new state immediately
|
// Enter the new state immediately to avoid "empty" frames during transitions.
|
||||||
self.on_enter(signals, hw);
|
self.on_enter(signals, hw);
|
||||||
self.state_initialized = true;
|
self.state_initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Dispatches the `on_enter` event to the current state implementation.
|
||||||
fn on_enter(&mut self, signals: &InputSignals, hw: &mut dyn HardwareBridge) {
|
fn on_enter(&mut self, signals: &InputSignals, hw: &mut dyn HardwareBridge) {
|
||||||
let mut req = PrometeuContext {
|
let mut req = PrometeuContext {
|
||||||
vm: &mut self.vm,
|
vm: &mut self.vm,
|
||||||
@ -75,6 +96,8 @@ impl Firmware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Dispatches the `on_update` event to the current state implementation.
|
||||||
|
/// Returns an optional `FirmwareState` if a transition is requested.
|
||||||
fn on_update(&mut self, signals: &InputSignals, hw: &mut dyn HardwareBridge) -> Option<FirmwareState> {
|
fn on_update(&mut self, signals: &InputSignals, hw: &mut dyn HardwareBridge) -> Option<FirmwareState> {
|
||||||
let mut req = PrometeuContext {
|
let mut req = PrometeuContext {
|
||||||
vm: &mut self.vm,
|
vm: &mut self.vm,
|
||||||
@ -95,6 +118,7 @@ impl Firmware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Dispatches the `on_exit` event to the current state implementation.
|
||||||
fn on_exit(&mut self, signals: &InputSignals, hw: &mut dyn HardwareBridge) {
|
fn on_exit(&mut self, signals: &InputSignals, hw: &mut dyn HardwareBridge) {
|
||||||
let mut req = PrometeuContext {
|
let mut req = PrometeuContext {
|
||||||
vm: &mut self.vm,
|
vm: &mut self.vm,
|
||||||
|
|||||||
@ -1,22 +1,39 @@
|
|||||||
use crate::model::Sample;
|
use crate::model::Sample;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// Maximum number of simultaneous audio voices supported by the hardware.
|
||||||
pub const MAX_CHANNELS: usize = 16;
|
pub const MAX_CHANNELS: usize = 16;
|
||||||
|
/// Standard sample rate for the final audio output.
|
||||||
pub const OUTPUT_SAMPLE_RATE: u32 = 48000;
|
pub const OUTPUT_SAMPLE_RATE: u32 = 48000;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
/// Defines if a sample should stop at the end or repeat indefinitely.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Default)]
|
||||||
pub enum LoopMode {
|
pub enum LoopMode {
|
||||||
|
/// Play once and stop.
|
||||||
|
#[default]
|
||||||
Off,
|
Off,
|
||||||
|
/// Return to the start (or loop_start) when reaching the end.
|
||||||
On,
|
On,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// State of a single playback voice (channel).
|
||||||
|
///
|
||||||
|
/// The Core maintains this state to provide information to the App (e.g., is_playing),
|
||||||
|
/// but the actual real-time mixing is performed by the Host using commands.
|
||||||
pub struct Channel {
|
pub struct Channel {
|
||||||
|
/// Reference to the PCM data being played.
|
||||||
pub sample: Option<Arc<Sample>>,
|
pub sample: Option<Arc<Sample>>,
|
||||||
|
/// Current playback position within the sample (fractional for pitch shifting).
|
||||||
pub pos: f64,
|
pub pos: f64,
|
||||||
|
/// Playback speed multiplier (1.0 = original speed).
|
||||||
pub pitch: f64,
|
pub pitch: f64,
|
||||||
pub volume: u8, // 0..255
|
/// Voice volume (0-255).
|
||||||
pub pan: u8, // 0..255
|
pub volume: u8,
|
||||||
|
/// Stereo panning (0=Full Left, 127=Center, 255=Full Right).
|
||||||
|
pub pan: u8,
|
||||||
|
/// Loop configuration for this voice.
|
||||||
pub loop_mode: LoopMode,
|
pub loop_mode: LoopMode,
|
||||||
|
/// Playback priority (used for voice stealing policies).
|
||||||
pub priority: u8,
|
pub priority: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,7 +51,12 @@ impl Default for Channel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Commands sent from the Core to the Host audio backend.
|
||||||
|
///
|
||||||
|
/// Because the Core logic runs at 60Hz and Audio is generated at 48kHz,
|
||||||
|
/// we use an asynchronous command queue to synchronize them.
|
||||||
pub enum AudioCommand {
|
pub enum AudioCommand {
|
||||||
|
/// Start playing a sample on a specific voice.
|
||||||
Play {
|
Play {
|
||||||
sample: Arc<Sample>,
|
sample: Arc<Sample>,
|
||||||
voice_id: usize,
|
voice_id: usize,
|
||||||
@ -44,32 +66,54 @@ pub enum AudioCommand {
|
|||||||
priority: u8,
|
priority: u8,
|
||||||
loop_mode: LoopMode,
|
loop_mode: LoopMode,
|
||||||
},
|
},
|
||||||
|
/// Immediately stop playback on a voice.
|
||||||
Stop {
|
Stop {
|
||||||
voice_id: usize,
|
voice_id: usize,
|
||||||
},
|
},
|
||||||
|
/// Update volume of an ongoing playback.
|
||||||
SetVolume {
|
SetVolume {
|
||||||
voice_id: usize,
|
voice_id: usize,
|
||||||
volume: u8,
|
volume: u8,
|
||||||
},
|
},
|
||||||
|
/// Update panning of an ongoing playback.
|
||||||
SetPan {
|
SetPan {
|
||||||
voice_id: usize,
|
voice_id: usize,
|
||||||
pan: u8,
|
pan: u8,
|
||||||
},
|
},
|
||||||
|
/// Update pitch of an ongoing playback.
|
||||||
SetPitch {
|
SetPitch {
|
||||||
voice_id: usize,
|
voice_id: usize,
|
||||||
pitch: f64,
|
pitch: f64,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// PROMETEU Audio Subsystem.
|
||||||
|
///
|
||||||
|
/// Models a multi-channel PCM sampler.
|
||||||
|
/// It works like an "Audio CPU": the Game Core sends high-level commands
|
||||||
|
/// every frame, and the Host backend implements the low-level mixer.
|
||||||
pub struct Audio {
|
pub struct Audio {
|
||||||
|
/// Local state of the 16 hardware voices.
|
||||||
pub voices: [Channel; MAX_CHANNELS],
|
pub voices: [Channel; MAX_CHANNELS],
|
||||||
|
/// Queue of pending commands to be processed by the Host mixer.
|
||||||
pub commands: Vec<AudioCommand>,
|
pub commands: Vec<AudioCommand>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Audio {
|
impl Audio {
|
||||||
|
/// Initializes the audio system with empty voices.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
const EMPTY_CHANNEL: Channel = Channel {
|
||||||
|
sample: None,
|
||||||
|
pos: 0.0,
|
||||||
|
pitch: 1.0,
|
||||||
|
volume: 255,
|
||||||
|
pan: 127,
|
||||||
|
loop_mode: LoopMode::Off,
|
||||||
|
priority: 0,
|
||||||
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
voices: Default::default(),
|
voices: [EMPTY_CHANNEL; MAX_CHANNELS],
|
||||||
commands: Vec::new(),
|
commands: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,42 +1,71 @@
|
|||||||
use crate::model::{Color, HudTileLayer, ScrollableTileLayer, Sprite, TileBank, TileMap, TileSize};
|
use crate::model::{Color, HudTileLayer, ScrollableTileLayer, Sprite, TileBank, TileMap, TileSize};
|
||||||
use std::mem::size_of;
|
use std::mem::size_of;
|
||||||
|
|
||||||
|
/// Blending modes inspired by classic 16-bit hardware.
|
||||||
|
/// Defines how source pixels are combined with existing pixels in the framebuffer.
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
pub enum BlendMode {
|
pub enum BlendMode {
|
||||||
/// dst = src
|
/// No blending: source overwrites destination.
|
||||||
#[default]
|
#[default]
|
||||||
None,
|
None,
|
||||||
/// dst = (src + dst) / 2
|
/// Average: dst = (src + dst) / 2. Creates a semi-transparent effect.
|
||||||
Half,
|
Half,
|
||||||
/// dst = dst + (src / 2)
|
/// Additive: dst = dst + (src / 2). Good for glows/light.
|
||||||
HalfPlus,
|
HalfPlus,
|
||||||
/// dst = dst - (src / 2)
|
/// Subtractive: dst = dst - (src / 2). Good for shadows.
|
||||||
HalfMinus,
|
HalfMinus,
|
||||||
/// dst = dst + src
|
/// Full Additive: dst = dst + src. Saturated light effect.
|
||||||
Full,
|
Full,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// PROMETEU Graphics Subsystem (GFX).
|
||||||
|
///
|
||||||
|
/// Models a specialized graphics chip with a fixed resolution, double buffering,
|
||||||
|
/// and a multi-layered tile/sprite architecture.
|
||||||
|
///
|
||||||
|
/// Composition Order (back to front):
|
||||||
|
/// 1. Sprites with priority 0 (optional background objects)
|
||||||
|
/// 2. Tile Layer 0 + Sprites with priority 1
|
||||||
|
/// 3. Tile Layer 1 + Sprites with priority 2
|
||||||
|
/// 4. Tile Layer 2 + Sprites with priority 3
|
||||||
|
/// 5. Tile Layer 3 + Sprites with priority 4
|
||||||
|
/// 6. Scene Fade effect
|
||||||
|
/// 7. HUD Layer (always on top)
|
||||||
|
/// 8. HUD Fade effect
|
||||||
pub struct Gfx {
|
pub struct Gfx {
|
||||||
|
/// Width of the internal framebuffer.
|
||||||
w: usize,
|
w: usize,
|
||||||
|
/// Height of the internal framebuffer.
|
||||||
h: usize,
|
h: usize,
|
||||||
|
/// Front buffer: the one currently being displayed by the Host.
|
||||||
front: Vec<u16>,
|
front: Vec<u16>,
|
||||||
|
/// Back buffer: the one being drawn to during the current frame.
|
||||||
back: Vec<u16>,
|
back: Vec<u16>,
|
||||||
|
|
||||||
|
/// 4 scrollable backgrounds.
|
||||||
pub layers: [ScrollableTileLayer; 4],
|
pub layers: [ScrollableTileLayer; 4],
|
||||||
|
/// 1 fixed layer for User Interface.
|
||||||
pub hud: HudTileLayer,
|
pub hud: HudTileLayer,
|
||||||
|
/// Up to 16 sets of graphical assets (tiles + palettes).
|
||||||
pub banks: [Option<TileBank>; 16],
|
pub banks: [Option<TileBank>; 16],
|
||||||
|
/// Hardware sprites (Object Attribute Memory equivalent).
|
||||||
pub sprites: [Sprite; 512],
|
pub sprites: [Sprite; 512],
|
||||||
|
|
||||||
pub scene_fade_level: u8, // 0..31
|
/// Current transparency level for the main scene (0=invisible, 31=fully visible).
|
||||||
|
pub scene_fade_level: u8,
|
||||||
|
/// Color used for the scene fade effect.
|
||||||
pub scene_fade_color: Color,
|
pub scene_fade_color: Color,
|
||||||
pub hud_fade_level: u8, // 0..31
|
/// Current transparency level for the HUD (independent of scene).
|
||||||
|
pub hud_fade_level: u8,
|
||||||
|
/// Color used for the HUD fade effect.
|
||||||
pub hud_fade_color: Color,
|
pub hud_fade_color: Color,
|
||||||
|
|
||||||
// Priority cache to avoid per-frame allocations
|
/// Internal cache used to sort sprites by priority without allocations.
|
||||||
priority_buckets: [Vec<usize>; 5],
|
priority_buckets: [Vec<usize>; 5],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Gfx {
|
impl Gfx {
|
||||||
|
/// Initializes the graphics system with a specific resolution.
|
||||||
pub fn new(w: usize, h: usize) -> Self {
|
pub fn new(w: usize, h: usize) -> Self {
|
||||||
const EMPTY_BANK: Option<TileBank> = None;
|
const EMPTY_BANK: Option<TileBank> = None;
|
||||||
const EMPTY_SPRITE: Sprite = Sprite {
|
const EMPTY_SPRITE: Sprite = Sprite {
|
||||||
@ -51,7 +80,7 @@ impl Gfx {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let len = w * h;
|
let len = w * h;
|
||||||
let mut layers = [
|
let layers = [
|
||||||
ScrollableTileLayer::new(64, 64, TileSize::Size16),
|
ScrollableTileLayer::new(64, 64, TileSize::Size16),
|
||||||
ScrollableTileLayer::new(64, 64, TileSize::Size16),
|
ScrollableTileLayer::new(64, 64, TileSize::Size16),
|
||||||
ScrollableTileLayer::new(64, 64, TileSize::Size16),
|
ScrollableTileLayer::new(64, 64, TileSize::Size16),
|
||||||
@ -130,15 +159,19 @@ impl Gfx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Double buffer swap (O(1), no pixel copying).
|
/// Double buffer swap (O(1), no pixel copying).
|
||||||
|
/// Typically called by the Host when it's time to display the finished frame.
|
||||||
pub fn present(&mut self) {
|
pub fn present(&mut self) {
|
||||||
std::mem::swap(&mut self.front, &mut self.back);
|
std::mem::swap(&mut self.front, &mut self.back);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Main frame rendering pipeline.
|
/// The main rendering pipeline.
|
||||||
/// Follows the priority order from the manual (Chapter 4.11).
|
///
|
||||||
|
/// This method composes the final frame by rasterizing layers and sprites in the
|
||||||
|
/// correct priority order into the back buffer.
|
||||||
|
/// Follows the hardware model where layers and sprites are composed every frame.
|
||||||
pub fn render_all(&mut self) {
|
pub fn render_all(&mut self) {
|
||||||
// 0. Preparation Phase: Organizes what should be drawn in each layer
|
// 0. Preparation Phase: Filter and group sprites by their priority levels.
|
||||||
// Clears the buckets without deallocating memory
|
// This avoids iterating through all 512 sprites for every layer.
|
||||||
for bucket in self.priority_buckets.iter_mut() {
|
for bucket in self.priority_buckets.iter_mut() {
|
||||||
bucket.clear();
|
bucket.clear();
|
||||||
}
|
}
|
||||||
@ -149,30 +182,32 @@ impl Gfx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Priority 0 sprites (Behind Layer 0 - the very back)
|
// 1. Priority 0 sprites: drawn at the very back, behind everything else.
|
||||||
Self::draw_bucket_on_buffer(&mut self.back, self.w, self.h, &self.priority_buckets[0], &self.sprites, &self.banks);
|
Self::draw_bucket_on_buffer(&mut self.back, self.w, self.h, &self.priority_buckets[0], &self.sprites, &self.banks);
|
||||||
|
|
||||||
|
// 2. Main layers and prioritized sprites.
|
||||||
|
// Order: Layer 0 -> Sprites 1 -> Layer 1 -> Sprites 2 ...
|
||||||
for i in 0..self.layers.len() {
|
for i in 0..self.layers.len() {
|
||||||
// 2. Game Layers (0 to 3)
|
|
||||||
let bank_id = self.layers[i].bank_id as usize;
|
let bank_id = self.layers[i].bank_id as usize;
|
||||||
if let Some(Some(bank)) = self.banks.get(bank_id) {
|
if let Some(Some(bank)) = self.banks.get(bank_id) {
|
||||||
Self::draw_tile_map(&mut self.back, self.w, self.h, &self.layers[i].map, bank, self.layers[i].scroll_x, self.layers[i].scroll_y);
|
Self::draw_tile_map(&mut self.back, self.w, self.h, &self.layers[i].map, bank, self.layers[i].scroll_x, self.layers[i].scroll_y);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Sprites according to priority
|
// Draw sprites that belong to this depth level
|
||||||
Self::draw_bucket_on_buffer(&mut self.back, self.w, self.h, &self.priority_buckets[i + 1], &self.sprites, &self.banks);
|
Self::draw_bucket_on_buffer(&mut self.back, self.w, self.h, &self.priority_buckets[i + 1], &self.sprites, &self.banks);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Applies Scene Fade (Affects everything drawn so far)
|
// 4. Scene Fade: Applies a color blend to the entire world (excluding HUD).
|
||||||
Self::apply_fade_to_buffer(&mut self.back, self.scene_fade_level, self.scene_fade_color);
|
Self::apply_fade_to_buffer(&mut self.back, self.scene_fade_level, self.scene_fade_color);
|
||||||
|
|
||||||
// 5. HUD (Always on top)
|
// 5. HUD: The fixed interface layer, always drawn on top of the world.
|
||||||
self.render_hud();
|
self.render_hud();
|
||||||
|
|
||||||
// 6. Applies HUD Fade (Optional, HUD only)
|
// 6. HUD Fade: Independent fade effect for the UI.
|
||||||
Self::apply_fade_to_buffer(&mut self.back, self.hud_fade_level, self.hud_fade_color);
|
Self::apply_fade_to_buffer(&mut self.back, self.hud_fade_level, self.hud_fade_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Renders a specific game layer.
|
||||||
pub fn render_layer(&mut self, layer_idx: usize) {
|
pub fn render_layer(&mut self, layer_idx: usize) {
|
||||||
if layer_idx >= self.layers.len() { return; }
|
if layer_idx >= self.layers.len() { return; }
|
||||||
|
|
||||||
@ -188,7 +223,7 @@ impl Gfx {
|
|||||||
Self::draw_tile_map(&mut self.back, self.w, self.h, &self.layers[layer_idx].map, bank, scroll_x, scroll_y);
|
Self::draw_tile_map(&mut self.back, self.w, self.h, &self.layers[layer_idx].map, bank, scroll_x, scroll_y);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renders the HUD (no scroll).
|
/// Renders the HUD (fixed position, no scroll).
|
||||||
pub fn render_hud(&mut self) {
|
pub fn render_hud(&mut self) {
|
||||||
let bank_id = self.hud.bank_id as usize;
|
let bank_id = self.hud.bank_id as usize;
|
||||||
let bank = match self.banks.get(bank_id) {
|
let bank = match self.banks.get(bank_id) {
|
||||||
@ -199,6 +234,7 @@ impl Gfx {
|
|||||||
Self::draw_tile_map(&mut self.back, self.w, self.h, &self.hud.map, bank, 0, 0);
|
Self::draw_tile_map(&mut self.back, self.w, self.h, &self.hud.map, bank, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Rasterizes a TileMap into the provided pixel buffer using scrolling.
|
||||||
fn draw_tile_map(
|
fn draw_tile_map(
|
||||||
back: &mut [u16],
|
back: &mut [u16],
|
||||||
screen_w: usize,
|
screen_w: usize,
|
||||||
@ -210,30 +246,34 @@ impl Gfx {
|
|||||||
) {
|
) {
|
||||||
let tile_size = bank.tile_size as usize;
|
let tile_size = bank.tile_size as usize;
|
||||||
|
|
||||||
// 1. Calculate how many tiles fit on the screen (with margin of 1 for scroll)
|
// 1. Determine the range of visible tiles based on the scroll position.
|
||||||
|
// We add a margin of 1 tile to ensure smooth pixel-perfect scrolling at the borders.
|
||||||
let visible_tiles_x = (screen_w / tile_size) + 1;
|
let visible_tiles_x = (screen_w / tile_size) + 1;
|
||||||
let visible_tiles_y = (screen_h / tile_size) + 1;
|
let visible_tiles_y = (screen_h / tile_size) + 1;
|
||||||
|
|
||||||
// 2. Calculate the initial offset (where the first tile starts being drawn)
|
// 2. Calculate offsets within the tilemap.
|
||||||
let start_tile_x = scroll_x / tile_size as i32;
|
let start_tile_x = scroll_x / tile_size as i32;
|
||||||
let start_tile_y = scroll_y / tile_size as i32;
|
let start_tile_y = scroll_y / tile_size as i32;
|
||||||
|
|
||||||
|
// 3. Fine scroll: how many pixels the tiles are shifted within the first visible cell.
|
||||||
let fine_scroll_x = scroll_x % tile_size as i32;
|
let fine_scroll_x = scroll_x % tile_size as i32;
|
||||||
let fine_scroll_y = scroll_y % tile_size as i32;
|
let fine_scroll_y = scroll_y % tile_size as i32;
|
||||||
|
|
||||||
// 3. Loop by Tile (Much more efficient)
|
// 4. Iterate only through the tiles that are actually visible on screen.
|
||||||
for ty in 0..visible_tiles_y {
|
for ty in 0..visible_tiles_y {
|
||||||
for tx in 0..visible_tiles_x {
|
for tx in 0..visible_tiles_x {
|
||||||
let map_x = (start_tile_x + tx as i32) as usize;
|
let map_x = (start_tile_x + tx as i32) as usize;
|
||||||
let map_y = (start_tile_y + ty as i32) as usize;
|
let map_y = (start_tile_y + ty as i32) as usize;
|
||||||
|
|
||||||
// Skip if outside map bounds
|
// Bounds check: don't draw if the camera is outside the map.
|
||||||
if map_x >= map.width || map_y >= map.height { continue; }
|
if map_x >= map.width || map_y >= map.height { continue; }
|
||||||
|
|
||||||
let tile = map.tiles[map_y * map.width + map_x];
|
let tile = map.tiles[map_y * map.width + map_x];
|
||||||
|
|
||||||
|
// Optimized skip for empty (ID 0) tiles.
|
||||||
if tile.id == 0 { continue; }
|
if tile.id == 0 { continue; }
|
||||||
|
|
||||||
// 4. Draws the tile block on the screen
|
// 5. Project the tile pixels to screen space.
|
||||||
let screen_tile_x = (tx as i32 * tile_size as i32) - fine_scroll_x;
|
let screen_tile_x = (tx as i32 * tile_size as i32) - fine_scroll_x;
|
||||||
let screen_tile_y = (ty as i32 * tile_size as i32) - fine_scroll_y;
|
let screen_tile_y = (ty as i32 * tile_size as i32) - fine_scroll_y;
|
||||||
|
|
||||||
@ -242,7 +282,8 @@ impl Gfx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to draw a block of 8x8, 16x16 or 32x32 pixels
|
/// Internal helper to copy a single tile's pixels to the framebuffer.
|
||||||
|
/// Handles flipping and palette resolution.
|
||||||
fn draw_tile_pixels(back: &mut [u16], screen_w: usize, screen_h: usize, x: i32, y: i32, tile: crate::model::Tile, bank: &TileBank) {
|
fn draw_tile_pixels(back: &mut [u16], screen_w: usize, screen_h: usize, x: i32, y: i32, tile: crate::model::Tile, bank: &TileBank) {
|
||||||
let size = bank.tile_size as usize;
|
let size = bank.tile_size as usize;
|
||||||
|
|
||||||
@ -254,16 +295,17 @@ impl Gfx {
|
|||||||
let world_x = x + local_x as i32;
|
let world_x = x + local_x as i32;
|
||||||
if world_x < 0 || world_x >= screen_w as i32 { continue; }
|
if world_x < 0 || world_x >= screen_w as i32 { continue; }
|
||||||
|
|
||||||
|
// Handle flip flags by reversing the fetch coordinates.
|
||||||
let fetch_x = if tile.flip_x { size - 1 - local_x } else { local_x };
|
let fetch_x = if tile.flip_x { size - 1 - local_x } else { local_x };
|
||||||
let fetch_y = if tile.flip_y { size - 1 - local_y } else { local_y };
|
let fetch_y = if tile.flip_y { size - 1 - local_y } else { local_y };
|
||||||
|
|
||||||
// 1. Gets the pixel index in the bank
|
// 1. Get the pixel color index (0-15) from the bank.
|
||||||
let px_index = bank.get_pixel_index(tile.id, fetch_x, fetch_y);
|
let px_index = bank.get_pixel_index(tile.id, fetch_x, fetch_y);
|
||||||
|
|
||||||
// 2. Rule: Index 0 is transparent
|
// 2. Hardware rule: Color index 0 is always fully transparent.
|
||||||
if px_index == 0 { continue; }
|
if px_index == 0 { continue; }
|
||||||
|
|
||||||
// 3. Resolves the color using the tile's palette
|
// 3. Resolve the virtual index to a real RGB565 color using the tile's assigned palette.
|
||||||
let color = bank.resolve_color(tile.palette_id, px_index);
|
let color = bank.resolve_color(tile.palette_id, px_index);
|
||||||
|
|
||||||
back[world_y as usize * screen_w + world_x as usize] = color.raw();
|
back[world_y as usize * screen_w + world_x as usize] = color.raw();
|
||||||
|
|||||||
@ -1,9 +1,17 @@
|
|||||||
use crate::hardware::{Audio, Gfx, HardwareBridge, Pad, Touch};
|
use crate::hardware::{Audio, Gfx, HardwareBridge, Pad, Touch};
|
||||||
|
|
||||||
|
/// Aggregate structure for all virtual hardware peripherals.
|
||||||
|
///
|
||||||
|
/// This struct represents the "Mainboard" of the PROMETEU console,
|
||||||
|
/// containing instances of GFX, Audio, Input (Pad), and Touch.
|
||||||
pub struct Hardware {
|
pub struct Hardware {
|
||||||
|
/// The Graphics Processing Unit.
|
||||||
pub gfx: Gfx,
|
pub gfx: Gfx,
|
||||||
|
/// The Sound Processing Unit.
|
||||||
pub audio: Audio,
|
pub audio: Audio,
|
||||||
|
/// The standard digital gamepad.
|
||||||
pub pad: Pad,
|
pub pad: Pad,
|
||||||
|
/// The absolute pointer input device.
|
||||||
pub touch: Touch,
|
pub touch: Touch,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,9 +30,12 @@ impl HardwareBridge for Hardware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Hardware {
|
impl Hardware {
|
||||||
|
/// Internal hardware width in pixels.
|
||||||
pub const W: usize = 320;
|
pub const W: usize = 320;
|
||||||
|
/// Internal hardware height in pixels.
|
||||||
pub const H: usize = 180;
|
pub const H: usize = 180;
|
||||||
|
|
||||||
|
/// Creates a fresh hardware instance with default settings.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
gfx: Gfx::new(Self::W, Self::H),
|
gfx: Gfx::new(Self::W, Self::H),
|
||||||
|
|||||||
@ -1,24 +1,38 @@
|
|||||||
use crate::model::Color;
|
use crate::model::Color;
|
||||||
|
|
||||||
|
/// Standard sizes for square tiles.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub enum TileSize {
|
pub enum TileSize {
|
||||||
|
/// 8x8 pixels.
|
||||||
Size8 = 8,
|
Size8 = 8,
|
||||||
|
/// 16x16 pixels.
|
||||||
Size16 = 16,
|
Size16 = 16,
|
||||||
|
/// 32x32 pixels.
|
||||||
Size32 = 32,
|
Size32 = 32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A container for graphical assets.
|
||||||
|
///
|
||||||
|
/// A TileBank stores both the raw pixel data (as palette indices) and the
|
||||||
|
/// color palettes themselves. This encapsulates all the information needed
|
||||||
|
/// to render a set of tiles.
|
||||||
pub struct TileBank {
|
pub struct TileBank {
|
||||||
|
/// Dimension of each individual tile in the bank.
|
||||||
pub tile_size: TileSize,
|
pub tile_size: TileSize,
|
||||||
pub width: usize, // in pixels
|
/// Width of the full bank sheet in pixels.
|
||||||
pub height: usize, // in pixels
|
pub width: usize,
|
||||||
|
/// Height of the full bank sheet in pixels.
|
||||||
|
pub height: usize,
|
||||||
|
|
||||||
/// Now we store indices from 0 to 15 (4 bits per pixel simulated in 8 bits)
|
/// Pixel data stored as 4-bit indices (packed into 8-bit values).
|
||||||
|
/// Index 0 is always reserved for transparency.
|
||||||
pub pixel_indices: Vec<u8>,
|
pub pixel_indices: Vec<u8>,
|
||||||
/// 256 palettes, each with 16 colors (RGB565 as u16)
|
/// Table of 256 palettes, each containing 16 RGB565 colors.
|
||||||
pub palettes: [[Color; 16]; 256],
|
pub palettes: [[Color; 16]; 256],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TileBank {
|
impl TileBank {
|
||||||
|
/// Creates an empty tile bank with the specified dimensions.
|
||||||
pub fn new(tile_size: TileSize, width: usize, height: usize) -> Self {
|
pub fn new(tile_size: TileSize, width: usize, height: usize) -> Self {
|
||||||
Self {
|
Self {
|
||||||
tile_size,
|
tile_size,
|
||||||
@ -29,7 +43,7 @@ impl TileBank {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the color of a specific pixel inside a tile.
|
/// Resolves a global tile ID and local pixel coordinates to a palette index.
|
||||||
/// tile_id: the tile index in the bank
|
/// tile_id: the tile index in the bank
|
||||||
/// local_x, local_y: the pixel position inside the tile (0 to tile_size-1)
|
/// local_x, local_y: the pixel position inside the tile (0 to tile_size-1)
|
||||||
pub fn get_pixel_index(&self, tile_id: u16, local_x: usize, local_y: usize) -> u8 {
|
pub fn get_pixel_index(&self, tile_id: u16, local_x: usize, local_y: usize) -> u8 {
|
||||||
@ -44,13 +58,14 @@ impl TileBank {
|
|||||||
if pixel_x < self.width && pixel_y < self.height {
|
if pixel_x < self.width && pixel_y < self.height {
|
||||||
self.pixel_indices[pixel_y * self.width + pixel_x]
|
self.pixel_indices[pixel_y * self.width + pixel_x]
|
||||||
} else {
|
} else {
|
||||||
0 // Outside the bank = Transparent
|
0 // Default to transparent if out of bounds
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolves a pixel index to a real Color using a palette.
|
/// Maps a 4-bit index to a real RGB565 Color using the specified palette.
|
||||||
pub fn resolve_color(&self, palette_id: u8, pixel_index: u8) -> Color {
|
pub fn resolve_color(&self, palette_id: u8, pixel_index: u8) -> Color {
|
||||||
// Rule: Index 0 is always transparent (we use MAGENTA/COLOR_KEY as vacuum)
|
// Hardware Rule: Index 0 is always transparent.
|
||||||
|
// We use Magenta as the 'transparent' signal color during composition.
|
||||||
if pixel_index == 0 {
|
if pixel_index == 0 {
|
||||||
return Color::COLOR_KEY;
|
return Color::COLOR_KEY;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,12 +10,19 @@ use std::sync::Arc;
|
|||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
/// PrometeuOS (POS): The system firmware/base.
|
/// PrometeuOS (POS): The system firmware/base.
|
||||||
|
///
|
||||||
/// Maximum authority for boot, peripherals, PVM execution, and fault handling.
|
/// Maximum authority for boot, peripherals, PVM execution, and fault handling.
|
||||||
|
/// It acts as the bridge between the high-level PVM applications and the virtual hardware.
|
||||||
pub struct PrometeuOS {
|
pub struct PrometeuOS {
|
||||||
|
/// Incremented on every host tick (60Hz).
|
||||||
pub tick_index: u64,
|
pub tick_index: u64,
|
||||||
|
/// Incremented every time a logical game frame is successfully completed.
|
||||||
pub logical_frame_index: u64,
|
pub logical_frame_index: u64,
|
||||||
|
/// Indicates if there is an ongoing logical frame that hasn't reached `FRAME_SYNC` yet.
|
||||||
pub logical_frame_active: bool,
|
pub logical_frame_active: bool,
|
||||||
|
/// Number of virtual cycles still available for the current logical frame.
|
||||||
pub logical_frame_remaining_cycles: u64,
|
pub logical_frame_remaining_cycles: u64,
|
||||||
|
/// Real-world CPU time (in microseconds) consumed by the last host tick.
|
||||||
pub last_frame_cpu_time_us: u64,
|
pub last_frame_cpu_time_us: u64,
|
||||||
|
|
||||||
// Example assets (kept for compatibility with v0 audio syscalls)
|
// Example assets (kept for compatibility with v0 audio syscalls)
|
||||||
@ -24,36 +31,54 @@ pub struct PrometeuOS {
|
|||||||
pub sample_snare: Option<Arc<Sample>>,
|
pub sample_snare: Option<Arc<Sample>>,
|
||||||
|
|
||||||
// Filesystem
|
// Filesystem
|
||||||
|
/// The virtual filesystem interface.
|
||||||
pub fs: VirtualFS,
|
pub fs: VirtualFS,
|
||||||
|
/// Current state of the FS (Mounted, Error, etc).
|
||||||
pub fs_state: FsState,
|
pub fs_state: FsState,
|
||||||
|
/// Mapping of numeric handles to file paths for open files.
|
||||||
pub open_files: HashMap<u32, String>,
|
pub open_files: HashMap<u32, String>,
|
||||||
|
/// Sequential handle generator for file operations.
|
||||||
pub next_handle: u32,
|
pub next_handle: u32,
|
||||||
|
|
||||||
// Log Service
|
// Log Service
|
||||||
pub log_service: LogService,
|
pub log_service: LogService,
|
||||||
|
/// Unique ID of the currently running application.
|
||||||
pub current_app_id: u32,
|
pub current_app_id: u32,
|
||||||
pub current_cartridge_title: String,
|
pub current_cartridge_title: String,
|
||||||
pub current_cartridge_app_version: String,
|
pub current_cartridge_app_version: String,
|
||||||
pub current_cartridge_app_mode: crate::model::AppMode,
|
pub current_cartridge_app_mode: crate::model::AppMode,
|
||||||
|
/// Rate-limiting tracker for application logs to prevent performance degradation.
|
||||||
pub logs_written_this_frame: HashMap<u32, u32>,
|
pub logs_written_this_frame: HashMap<u32, u32>,
|
||||||
|
|
||||||
// Telemetry and Certification
|
// Telemetry and Certification
|
||||||
|
/// Accumulator for metrics of the frame currently being executed.
|
||||||
pub telemetry_current: TelemetryFrame,
|
pub telemetry_current: TelemetryFrame,
|
||||||
|
/// Snapshot of the metrics from the last completed logical frame.
|
||||||
pub telemetry_last: TelemetryFrame,
|
pub telemetry_last: TelemetryFrame,
|
||||||
|
/// Logic for evaluating compliance with the active CAP profile.
|
||||||
pub certifier: Certifier,
|
pub certifier: Certifier,
|
||||||
|
/// Global pause flag (used by debugger or system events).
|
||||||
pub paused: bool,
|
pub paused: bool,
|
||||||
|
/// Request from debugger to run exactly one instruction or frame.
|
||||||
pub debug_step_request: bool,
|
pub debug_step_request: bool,
|
||||||
|
|
||||||
|
/// Instant when the system was initialized.
|
||||||
boot_time: Instant,
|
boot_time: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PrometeuOS {
|
impl PrometeuOS {
|
||||||
|
/// Default number of cycles assigned to a single game frame.
|
||||||
pub const CYCLES_PER_LOGICAL_FRAME: u64 = 100_000;
|
pub const CYCLES_PER_LOGICAL_FRAME: u64 = 100_000;
|
||||||
pub const SLICE_PER_TICK: u64 = 100_000; // For now the same, but allows different granularity
|
/// Maximum number of cycles allowed to execute in a single host tick.
|
||||||
|
/// Usually the same as CYCLES_PER_LOGICAL_FRAME to target 60FPS.
|
||||||
|
pub const SLICE_PER_TICK: u64 = 100_000;
|
||||||
|
|
||||||
|
/// Maximum characters allowed per log message.
|
||||||
pub const MAX_LOG_LEN: usize = 256;
|
pub const MAX_LOG_LEN: usize = 256;
|
||||||
|
/// Maximum log entries an App can emit in a single frame.
|
||||||
pub const MAX_LOGS_PER_FRAME: u32 = 10;
|
pub const MAX_LOGS_PER_FRAME: u32 = 10;
|
||||||
|
|
||||||
|
/// Creates a new POS instance with optional certification rules.
|
||||||
pub fn new(cap_config: Option<CertificationConfig>) -> Self {
|
pub fn new(cap_config: Option<CertificationConfig>) -> Self {
|
||||||
let boot_time = Instant::now();
|
let boot_time = Instant::now();
|
||||||
let mut os = Self {
|
let mut os = Self {
|
||||||
@ -158,67 +183,81 @@ impl PrometeuOS {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Executes a host tick (60Hz).
|
/// Executes a single host tick (nominally 60Hz).
|
||||||
|
///
|
||||||
|
/// This method is responsible for managing the logical frame lifecycle.
|
||||||
|
/// A single host tick might execute a full logical frame, part of it,
|
||||||
|
/// or multiple frames depending on the configured slices.
|
||||||
pub fn step_frame(&mut self, vm: &mut VirtualMachine, signals: &InputSignals, hw: &mut dyn HardwareBridge) -> Option<String> {
|
pub fn step_frame(&mut self, vm: &mut VirtualMachine, signals: &InputSignals, hw: &mut dyn HardwareBridge) -> Option<String> {
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
self.tick_index += 1;
|
self.tick_index += 1;
|
||||||
|
|
||||||
|
// If the system is paused, we don't advance unless there's a debug step request.
|
||||||
if self.paused && !self.debug_step_request {
|
if self.paused && !self.debug_step_request {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.update_fs();
|
self.update_fs();
|
||||||
|
|
||||||
|
// 1. Frame Initialization
|
||||||
|
// If we are not currently in the middle of a logical frame, start a new one.
|
||||||
if !self.logical_frame_active {
|
if !self.logical_frame_active {
|
||||||
self.logical_frame_active = true;
|
self.logical_frame_active = true;
|
||||||
self.logical_frame_remaining_cycles = Self::CYCLES_PER_LOGICAL_FRAME;
|
self.logical_frame_remaining_cycles = Self::CYCLES_PER_LOGICAL_FRAME;
|
||||||
self.begin_logical_frame(signals, hw);
|
self.begin_logical_frame(signals, hw);
|
||||||
|
|
||||||
// Beginning of frame: reset telemetry accumulator (but keep frame_index)
|
// Reset telemetry for the new logical frame
|
||||||
self.telemetry_current = TelemetryFrame {
|
self.telemetry_current = TelemetryFrame {
|
||||||
frame_index: self.logical_frame_index,
|
frame_index: self.logical_frame_index,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Budget for this tick: the minimum between the tick slice and what remains in the logical frame
|
// 2. Budget Allocation
|
||||||
|
// Determine how many cycles we can run in this host tick.
|
||||||
let budget = std::cmp::min(Self::SLICE_PER_TICK, self.logical_frame_remaining_cycles);
|
let budget = std::cmp::min(Self::SLICE_PER_TICK, self.logical_frame_remaining_cycles);
|
||||||
|
|
||||||
|
// 3. VM Execution
|
||||||
if budget > 0 {
|
if budget > 0 {
|
||||||
// Execute budget
|
// Run the VM until budget is hit or FRAME_SYNC is reached.
|
||||||
let run_result = vm.run_budget(budget, self, hw);
|
let run_result = vm.run_budget(budget, self, hw);
|
||||||
|
|
||||||
match run_result {
|
match run_result {
|
||||||
Ok(run) => {
|
Ok(run) => {
|
||||||
self.logical_frame_remaining_cycles = self.logical_frame_remaining_cycles.saturating_sub(run.cycles_used);
|
self.logical_frame_remaining_cycles = self.logical_frame_remaining_cycles.saturating_sub(run.cycles_used);
|
||||||
|
|
||||||
// Accumulates metrics
|
// Accumulate metrics for telemetry and certification
|
||||||
self.telemetry_current.cycles_used += run.cycles_used;
|
self.telemetry_current.cycles_used += run.cycles_used;
|
||||||
self.telemetry_current.vm_steps += run.steps_executed;
|
self.telemetry_current.vm_steps += run.steps_executed;
|
||||||
|
|
||||||
|
// Handle Breakpoints
|
||||||
if run.reason == crate::virtual_machine::LogicalFrameEndingReason::Breakpoint {
|
if run.reason == crate::virtual_machine::LogicalFrameEndingReason::Breakpoint {
|
||||||
self.paused = true;
|
self.paused = true;
|
||||||
self.debug_step_request = false;
|
self.debug_step_request = false;
|
||||||
self.log(LogLevel::Info, LogSource::Vm, 0xDEB1, format!("Breakpoint hit at PC 0x{:X}", vm.pc));
|
self.log(LogLevel::Info, LogSource::Vm, 0xDEB1, format!("Breakpoint hit at PC 0x{:X}", vm.pc));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. Frame Finalization (FRAME_SYNC reached)
|
||||||
if run.reason == crate::virtual_machine::LogicalFrameEndingReason::FrameSync {
|
if run.reason == crate::virtual_machine::LogicalFrameEndingReason::FrameSync {
|
||||||
|
// All drawing commands for this frame are now complete.
|
||||||
|
// Finalize the framebuffer.
|
||||||
hw.gfx_mut().render_all();
|
hw.gfx_mut().render_all();
|
||||||
|
|
||||||
// Finalizes frame telemetry (host_cpu_time will be refined at the end of the tick)
|
// Finalize frame telemetry
|
||||||
self.telemetry_current.host_cpu_time_us = start.elapsed().as_micros() as u64;
|
self.telemetry_current.host_cpu_time_us = start.elapsed().as_micros() as u64;
|
||||||
|
|
||||||
// Certification (CAP)
|
// Evaluate CAP (Execution Budget Compliance)
|
||||||
let ts_ms = self.boot_time.elapsed().as_millis() as u64;
|
let ts_ms = self.boot_time.elapsed().as_millis() as u64;
|
||||||
self.telemetry_current.violations = self.certifier.evaluate(&self.telemetry_current, &mut self.log_service, ts_ms) as u32;
|
self.telemetry_current.violations = self.certifier.evaluate(&self.telemetry_current, &mut self.log_service, ts_ms) as u32;
|
||||||
|
|
||||||
// Latch: what the overlay reads
|
// Latch telemetry for the Host/Debugger to read.
|
||||||
self.telemetry_last = self.telemetry_current;
|
self.telemetry_last = self.telemetry_current;
|
||||||
|
|
||||||
self.logical_frame_index += 1;
|
self.logical_frame_index += 1;
|
||||||
self.logical_frame_active = false;
|
self.logical_frame_active = false;
|
||||||
self.logical_frame_remaining_cycles = 0;
|
self.logical_frame_remaining_cycles = 0;
|
||||||
|
|
||||||
|
// If we were doing a "step frame" debug command, pause now that the frame is done.
|
||||||
if self.debug_step_request {
|
if self.debug_step_request {
|
||||||
self.paused = true;
|
self.paused = true;
|
||||||
self.debug_step_request = false;
|
self.debug_step_request = false;
|
||||||
@ -226,6 +265,7 @@ impl PrometeuOS {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
// Fatal VM fault (division by zero, invalid memory access, etc).
|
||||||
let err_msg = format!("PVM Fault: {:?}", e);
|
let err_msg = format!("PVM Fault: {:?}", e);
|
||||||
self.log(LogLevel::Error, LogSource::Vm, 0, err_msg.clone());
|
self.log(LogLevel::Error, LogSource::Vm, 0, err_msg.clone());
|
||||||
return Some(err_msg);
|
return Some(err_msg);
|
||||||
@ -235,7 +275,7 @@ impl PrometeuOS {
|
|||||||
|
|
||||||
self.last_frame_cpu_time_us = start.elapsed().as_micros() as u64;
|
self.last_frame_cpu_time_us = start.elapsed().as_micros() as u64;
|
||||||
|
|
||||||
// If the frame ended in this tick, we update the final real time in the latch
|
// If the frame ended exactly in this tick, we update the final real time in the latch.
|
||||||
if !self.logical_frame_active && self.telemetry_last.frame_index == self.logical_frame_index.wrapping_sub(1) {
|
if !self.logical_frame_active && self.telemetry_last.frame_index == self.logical_frame_index.wrapping_sub(1) {
|
||||||
self.telemetry_last.host_cpu_time_us = self.last_frame_cpu_time_us;
|
self.telemetry_last.host_cpu_time_us = self.last_frame_cpu_time_us;
|
||||||
}
|
}
|
||||||
@ -493,23 +533,35 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl NativeInterface for PrometeuOS {
|
impl NativeInterface for PrometeuOS {
|
||||||
|
/// Dispatches a syscall from the VM to the native implementation.
|
||||||
|
///
|
||||||
|
/// Syscalls are grouped by functionality:
|
||||||
|
/// - 0x0000: System/Cartridge management
|
||||||
|
/// - 0x1000: Graphics (GFX)
|
||||||
|
/// - 0x2000: Input
|
||||||
|
/// - 0x3000: Audio
|
||||||
|
/// - 0x4000: Filesystem (FS)
|
||||||
|
/// - 0x5000: Logging
|
||||||
|
///
|
||||||
|
/// Each syscall returns the number of virtual cycles it consumed.
|
||||||
fn syscall(&mut self, id: u32, vm: &mut VirtualMachine, hw: &mut dyn HardwareBridge) -> Result<u64, String> {
|
fn syscall(&mut self, id: u32, vm: &mut VirtualMachine, hw: &mut dyn HardwareBridge) -> Result<u64, String> {
|
||||||
self.telemetry_current.syscalls += 1;
|
self.telemetry_current.syscalls += 1;
|
||||||
match id {
|
match id {
|
||||||
|
// --- System Syscalls ---
|
||||||
|
|
||||||
// system.has_cart() -> bool
|
// system.has_cart() -> bool
|
||||||
0x0001 => {
|
0x0001 => {
|
||||||
// virtual_machine.push(Value::Boolean(self.current_cartridge.is_some()));
|
// Returns true if a cartridge is available.
|
||||||
Ok(10)
|
Ok(10)
|
||||||
}
|
}
|
||||||
// system.run_cart()
|
// system.run_cart()
|
||||||
0x0002 => {
|
0x0002 => {
|
||||||
// if let Some(cart) = self.current_cartridge.as_ref().cloned() {
|
// Triggers loading and execution of the current cartridge.
|
||||||
// self.load_cartridge(&cart);
|
|
||||||
// } else {
|
|
||||||
// return Err("No cartridge inserted".into());
|
|
||||||
// }
|
|
||||||
Ok(100)
|
Ok(100)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- GFX Syscalls ---
|
||||||
|
|
||||||
// gfx.clear(color_index)
|
// gfx.clear(color_index)
|
||||||
0x1001 => {
|
0x1001 => {
|
||||||
let color_idx = vm.pop_integer()? as usize;
|
let color_idx = vm.pop_integer()? as usize;
|
||||||
@ -528,6 +580,9 @@ impl NativeInterface for PrometeuOS {
|
|||||||
hw.gfx_mut().fill_rect(x, y, w, h, color);
|
hw.gfx_mut().fill_rect(x, y, w, h, color);
|
||||||
Ok(200)
|
Ok(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Input Syscalls ---
|
||||||
|
|
||||||
// input.get_pad(button_id) -> bool
|
// input.get_pad(button_id) -> bool
|
||||||
0x2001 => {
|
0x2001 => {
|
||||||
let button_id = vm.pop_integer()? as u32;
|
let button_id = vm.pop_integer()? as u32;
|
||||||
@ -535,6 +590,9 @@ impl NativeInterface for PrometeuOS {
|
|||||||
vm.push(Value::Boolean(is_down));
|
vm.push(Value::Boolean(is_down));
|
||||||
Ok(50)
|
Ok(50)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Audio Syscalls ---
|
||||||
|
|
||||||
// audio.play_sample(sample_id, voice_id, volume, pan, pitch)
|
// audio.play_sample(sample_id, voice_id, volume, pan, pitch)
|
||||||
0x3001 => {
|
0x3001 => {
|
||||||
let pitch = vm.pop_number()?;
|
let pitch = vm.pop_number()?;
|
||||||
@ -559,6 +617,7 @@ impl NativeInterface for PrometeuOS {
|
|||||||
// --- Filesystem Syscalls (0x4000) ---
|
// --- Filesystem Syscalls (0x4000) ---
|
||||||
|
|
||||||
// FS_OPEN(path) -> handle
|
// FS_OPEN(path) -> handle
|
||||||
|
// Opens a file in the virtual sandbox and returns a numeric handle.
|
||||||
0x4001 => {
|
0x4001 => {
|
||||||
let path = match vm.pop()? {
|
let path = match vm.pop()? {
|
||||||
Value::String(s) => s,
|
Value::String(s) => s,
|
||||||
@ -623,7 +682,7 @@ impl NativeInterface for PrometeuOS {
|
|||||||
};
|
};
|
||||||
match self.fs.list_dir(&path) {
|
match self.fs.list_dir(&path) {
|
||||||
Ok(entries) => {
|
Ok(entries) => {
|
||||||
// For now, returns a string separated by ';'
|
// Returns a string separated by ';' for simple parsing in PVM.
|
||||||
let names: Vec<String> = entries.into_iter().map(|e| e.name).collect();
|
let names: Vec<String> = entries.into_iter().map(|e| e.name).collect();
|
||||||
vm.push(Value::String(names.join(";")));
|
vm.push(Value::String(names.join(";")));
|
||||||
Ok(500)
|
Ok(500)
|
||||||
|
|||||||
@ -5,35 +5,61 @@ use crate::virtual_machine::opcode::OpCode;
|
|||||||
use crate::virtual_machine::value::Value;
|
use crate::virtual_machine::value::Value;
|
||||||
use crate::virtual_machine::Program;
|
use crate::virtual_machine::Program;
|
||||||
|
|
||||||
|
/// Reason why the Virtual Machine stopped execution during a specific run.
|
||||||
|
/// This allows the system to decide if it should continue execution in the next tick
|
||||||
|
/// or if the frame is finalized.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum LogicalFrameEndingReason {
|
pub enum LogicalFrameEndingReason {
|
||||||
|
/// Execution reached a `FRAME_SYNC` instruction, marking the end of the logical frame.
|
||||||
FrameSync,
|
FrameSync,
|
||||||
|
/// The cycle budget for the current host tick was exhausted before reaching `FRAME_SYNC`.
|
||||||
BudgetExhausted,
|
BudgetExhausted,
|
||||||
|
/// A `HALT` instruction was executed, terminating the program.
|
||||||
Halted,
|
Halted,
|
||||||
|
/// The Program Counter (PC) reached the end of the available bytecode.
|
||||||
EndOfRom,
|
EndOfRom,
|
||||||
|
/// Execution hit a registered breakpoint.
|
||||||
Breakpoint,
|
Breakpoint,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A report detailing the results of an execution slice (run_budget).
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct BudgetReport {
|
pub struct BudgetReport {
|
||||||
|
/// Total virtual cycles consumed during this run.
|
||||||
pub cycles_used: u64,
|
pub cycles_used: u64,
|
||||||
|
/// Number of VM instructions executed.
|
||||||
pub steps_executed: u32,
|
pub steps_executed: u32,
|
||||||
|
/// The reason why this execution slice ended.
|
||||||
pub reason: LogicalFrameEndingReason,
|
pub reason: LogicalFrameEndingReason,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The PVM (PROMETEU Virtual Machine).
|
||||||
|
///
|
||||||
|
/// A deterministic, stack-based virtual machine designed for educational purposes.
|
||||||
|
/// It models execution through fixed-cost cycles and explicit memory management.
|
||||||
pub struct VirtualMachine {
|
pub struct VirtualMachine {
|
||||||
|
/// Program Counter: points to the next byte in the ROM to be executed.
|
||||||
pub pc: usize,
|
pub pc: usize,
|
||||||
|
/// Operand Stack: used for intermediate calculations and passing arguments to opcodes.
|
||||||
pub operand_stack: Vec<Value>,
|
pub operand_stack: Vec<Value>,
|
||||||
|
/// Call Stack: stores execution frames for function calls and local variables.
|
||||||
pub call_stack: Vec<CallFrame>,
|
pub call_stack: Vec<CallFrame>,
|
||||||
|
/// Globals: storage for persistent variables that survive between frames.
|
||||||
pub globals: Vec<Value>,
|
pub globals: Vec<Value>,
|
||||||
|
/// The currently loaded program (Bytecode + Constant Pool).
|
||||||
pub program: Program,
|
pub program: Program,
|
||||||
pub heap: Vec<Value>, // Simplified for demo, future real RAM/Heap
|
/// Dynamic memory region for complex structures (Simplified in current version).
|
||||||
|
pub heap: Vec<Value>,
|
||||||
|
/// Total accumulated execution cycles since the last reset.
|
||||||
pub cycles: u64,
|
pub cycles: u64,
|
||||||
|
/// Flag indicating if the VM has been stopped by a `HALT` instruction.
|
||||||
pub halted: bool,
|
pub halted: bool,
|
||||||
|
/// A set of PC addresses where execution should pause.
|
||||||
pub breakpoints: std::collections::HashSet<usize>,
|
pub breakpoints: std::collections::HashSet<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VirtualMachine {
|
impl VirtualMachine {
|
||||||
|
/// Creates a new VM instance with the provided bytecode and constants.
|
||||||
pub fn new(rom: Vec<u8>, constant_pool: Vec<Value>) -> Self {
|
pub fn new(rom: Vec<u8>, constant_pool: Vec<Value>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
pc: 0,
|
pc: 0,
|
||||||
@ -48,26 +74,31 @@ impl VirtualMachine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resets the VM state and loads a new program.
|
||||||
|
/// This is typically called by the Firmware when starting a new App/Cartridge.
|
||||||
pub fn initialize(&mut self, program_bytes: Vec<u8>, entrypoint: &str) {
|
pub fn initialize(&mut self, program_bytes: Vec<u8>, entrypoint: &str) {
|
||||||
|
// PBC (Prometeu ByteCode) is a binary format that includes a header,
|
||||||
|
// constant pool, and the raw ROM (bytecode).
|
||||||
if program_bytes.starts_with(b"PPBC") {
|
if program_bytes.starts_with(b"PPBC") {
|
||||||
if let Ok((rom, cp)) = self.parse_pbc(&program_bytes) {
|
if let Ok((rom, cp)) = self.parse_pbc(&program_bytes) {
|
||||||
self.program = Program::new(rom, cp);
|
self.program = Program::new(rom, cp);
|
||||||
} else {
|
} else {
|
||||||
|
// Fallback for raw bytes if PBC parsing fails
|
||||||
self.program = Program::new(program_bytes, vec![]);
|
self.program = Program::new(program_bytes, vec![]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// For now, we treat the bytes as the ROM directly.
|
// If it doesn't have the PPBC signature, treat it as raw bytecode.
|
||||||
self.program = Program::new(program_bytes, vec![]);
|
self.program = Program::new(program_bytes, vec![]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the entrypoint is numeric, we can try to use it as the initial PC.
|
// Resolve the entrypoint. Currently supports numeric addresses.
|
||||||
// If not, for now we ignore it or start from 0.
|
|
||||||
if let Ok(addr) = entrypoint.parse::<usize>() {
|
if let Ok(addr) = entrypoint.parse::<usize>() {
|
||||||
self.pc = addr;
|
self.pc = addr;
|
||||||
} else {
|
} else {
|
||||||
self.pc = 0;
|
self.pc = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Full state reset to ensure a clean start for the App
|
||||||
self.operand_stack.clear();
|
self.operand_stack.clear();
|
||||||
self.call_stack.clear();
|
self.call_stack.clear();
|
||||||
self.globals.clear();
|
self.globals.clear();
|
||||||
@ -76,9 +107,12 @@ impl VirtualMachine {
|
|||||||
self.halted = false;
|
self.halted = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses the PROMETEU binary format.
|
||||||
|
/// Format: "PPBC" (4) | CP_COUNT (u32) | CP_ENTRIES[...] | ROM_SIZE (u32) | ROM_BYTES[...]
|
||||||
fn parse_pbc(&self, bytes: &[u8]) -> Result<(Vec<u8>, Vec<Value>), String> {
|
fn parse_pbc(&self, bytes: &[u8]) -> Result<(Vec<u8>, Vec<Value>), String> {
|
||||||
let mut cursor = 4; // Skip "PPBC"
|
let mut cursor = 4; // Skip "PPBC" signature
|
||||||
|
|
||||||
|
// 1. Parse Constant Pool (literals like strings, ints, floats used in the program)
|
||||||
let cp_count = self.read_u32_at(bytes, &mut cursor)? as usize;
|
let cp_count = self.read_u32_at(bytes, &mut cursor)? as usize;
|
||||||
let mut cp = Vec::with_capacity(cp_count);
|
let mut cp = Vec::with_capacity(cp_count);
|
||||||
|
|
||||||
@ -88,11 +122,11 @@ impl VirtualMachine {
|
|||||||
cursor += 1;
|
cursor += 1;
|
||||||
|
|
||||||
match tag {
|
match tag {
|
||||||
1 => { // Integer
|
1 => { // Integer (64-bit)
|
||||||
let val = self.read_i64_at(bytes, &mut cursor)?;
|
let val = self.read_i64_at(bytes, &mut cursor)?;
|
||||||
cp.push(Value::Integer(val));
|
cp.push(Value::Integer(val));
|
||||||
}
|
}
|
||||||
2 => { // Float
|
2 => { // Float (64-bit)
|
||||||
let val = self.read_f64_at(bytes, &mut cursor)?;
|
let val = self.read_f64_at(bytes, &mut cursor)?;
|
||||||
cp.push(Value::Float(val));
|
cp.push(Value::Float(val));
|
||||||
}
|
}
|
||||||
@ -102,7 +136,7 @@ impl VirtualMachine {
|
|||||||
cursor += 1;
|
cursor += 1;
|
||||||
cp.push(Value::Boolean(val));
|
cp.push(Value::Boolean(val));
|
||||||
}
|
}
|
||||||
4 => { // String
|
4 => { // String (UTF-8)
|
||||||
let len = self.read_u32_at(bytes, &mut cursor)? as usize;
|
let len = self.read_u32_at(bytes, &mut cursor)? as usize;
|
||||||
if cursor + len > bytes.len() { return Err("Unexpected end of PBC".into()); }
|
if cursor + len > bytes.len() { return Err("Unexpected end of PBC".into()); }
|
||||||
let s = String::from_utf8_lossy(&bytes[cursor..cursor + len]).into_owned();
|
let s = String::from_utf8_lossy(&bytes[cursor..cursor + len]).into_owned();
|
||||||
@ -113,6 +147,7 @@ impl VirtualMachine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. Parse ROM (executable bytecode)
|
||||||
let rom_size = self.read_u32_at(bytes, &mut cursor)? as usize;
|
let rom_size = self.read_u32_at(bytes, &mut cursor)? as usize;
|
||||||
if cursor + rom_size > bytes.len() {
|
if cursor + rom_size > bytes.len() {
|
||||||
return Err("Invalid ROM size in PBC".into());
|
return Err("Invalid ROM size in PBC".into());
|
||||||
@ -151,6 +186,16 @@ impl Default for VirtualMachine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl VirtualMachine {
|
impl VirtualMachine {
|
||||||
|
/// Executes the VM for a limited number of cycles (budget).
|
||||||
|
///
|
||||||
|
/// This is the heart of the deterministic execution model. Instead of running
|
||||||
|
/// indefinitely, the VM runs until it consumes its allocated budget or reaches
|
||||||
|
/// a synchronization point (`FRAME_SYNC`).
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `budget` - Maximum number of cycles allowed for this execution slice.
|
||||||
|
/// * `native` - Interface for handling syscalls (Firmware/OS).
|
||||||
|
/// * `hw` - Access to virtual hardware peripherals.
|
||||||
pub fn run_budget(
|
pub fn run_budget(
|
||||||
&mut self,
|
&mut self,
|
||||||
budget: u64,
|
budget: u64,
|
||||||
@ -165,6 +210,9 @@ impl VirtualMachine {
|
|||||||
&& !self.halted
|
&& !self.halted
|
||||||
&& self.pc < self.program.rom.len()
|
&& self.pc < self.program.rom.len()
|
||||||
{
|
{
|
||||||
|
// Debugger support: stop before executing an instruction if there's a breakpoint.
|
||||||
|
// Note: we skip the check for the very first step of a slice to avoid
|
||||||
|
// getting stuck on the same breakpoint repeatedly.
|
||||||
if steps_executed > 0 && self.breakpoints.contains(&self.pc) {
|
if steps_executed > 0 && self.breakpoints.contains(&self.pc) {
|
||||||
ending_reason = Some(LogicalFrameEndingReason::Breakpoint);
|
ending_reason = Some(LogicalFrameEndingReason::Breakpoint);
|
||||||
break;
|
break;
|
||||||
@ -173,26 +221,31 @@ impl VirtualMachine {
|
|||||||
let pc_before = self.pc;
|
let pc_before = self.pc;
|
||||||
let cycles_before = self.cycles;
|
let cycles_before = self.cycles;
|
||||||
|
|
||||||
// Fast-path: FRAME_SYNC ends the logical frame
|
// Fast-path for FRAME_SYNC:
|
||||||
|
// This instruction is special because it marks the end of a logical game frame.
|
||||||
|
// We peak ahead to handle it efficiently.
|
||||||
let opcode_val = self.peek_u16()?;
|
let opcode_val = self.peek_u16()?;
|
||||||
let opcode = OpCode::try_from(opcode_val)?;
|
let opcode = OpCode::try_from(opcode_val)?;
|
||||||
if opcode == OpCode::FrameSync {
|
if opcode == OpCode::FrameSync {
|
||||||
self.pc += 2;
|
self.pc += 2; // Advance PC past the opcode
|
||||||
self.cycles += OpCode::FrameSync.cycles();
|
self.cycles += OpCode::FrameSync.cycles();
|
||||||
steps_executed += 1;
|
steps_executed += 1;
|
||||||
ending_reason = Some(LogicalFrameEndingReason::FrameSync);
|
ending_reason = Some(LogicalFrameEndingReason::FrameSync);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Execute a single step (Fetch-Decode-Execute)
|
||||||
self.step(native, hw)?;
|
self.step(native, hw)?;
|
||||||
steps_executed += 1;
|
steps_executed += 1;
|
||||||
|
|
||||||
// ensures real progress
|
// Integrity check: ensure real progress is being made to avoid infinite loops
|
||||||
|
// caused by zero-cycle instructions or stuck PC.
|
||||||
if self.pc == pc_before && self.cycles == cycles_before && !self.halted {
|
if self.pc == pc_before && self.cycles == cycles_before && !self.halted {
|
||||||
return Err(format!("VM stuck at PC 0x{:08X}", self.pc));
|
return Err(format!("VM stuck at PC 0x{:08X}", self.pc));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine why we stopped if no explicit reason (FrameSync/Breakpoint) was set.
|
||||||
if ending_reason.is_none() {
|
if ending_reason.is_none() {
|
||||||
if self.halted {
|
if self.halted {
|
||||||
ending_reason = Some(LogicalFrameEndingReason::Halted);
|
ending_reason = Some(LogicalFrameEndingReason::Halted);
|
||||||
@ -210,6 +263,7 @@ impl VirtualMachine {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Peeks at the next 16-bit value in the ROM without advancing the PC.
|
||||||
fn peek_u16(&self) -> Result<u16, String> {
|
fn peek_u16(&self) -> Result<u16, String> {
|
||||||
if self.pc + 2 > self.program.rom.len() {
|
if self.pc + 2 > self.program.rom.len() {
|
||||||
return Err("Unexpected end of ROM".into());
|
return Err("Unexpected end of ROM".into());
|
||||||
@ -221,14 +275,22 @@ impl VirtualMachine {
|
|||||||
Ok(u16::from_le_bytes(bytes))
|
Ok(u16::from_le_bytes(bytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Executes a single instruction at the current Program Counter (PC).
|
||||||
|
///
|
||||||
|
/// This follows the classic CPU cycle:
|
||||||
|
/// 1. Fetch: Read the opcode from memory.
|
||||||
|
/// 2. Decode: Identify what operation to perform.
|
||||||
|
/// 3. Execute: Perform the operation, updating stacks, memory, or calling peripherals.
|
||||||
pub fn step(&mut self, native: &mut dyn NativeInterface, hw: &mut dyn HardwareBridge) -> Result<(), String> {
|
pub fn step(&mut self, native: &mut dyn NativeInterface, hw: &mut dyn HardwareBridge) -> Result<(), String> {
|
||||||
if self.halted || self.pc >= self.program.rom.len() {
|
if self.halted || self.pc >= self.program.rom.len() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch & Decode
|
||||||
let opcode_val = self.read_u16()?;
|
let opcode_val = self.read_u16()?;
|
||||||
let opcode = OpCode::try_from(opcode_val)?;
|
let opcode = OpCode::try_from(opcode_val)?;
|
||||||
|
|
||||||
|
// Execute
|
||||||
match opcode {
|
match opcode {
|
||||||
OpCode::Nop => {}
|
OpCode::Nop => {}
|
||||||
OpCode::Halt => {
|
OpCode::Halt => {
|
||||||
@ -369,6 +431,8 @@ impl VirtualMachine {
|
|||||||
self.operand_stack[stack_idx] = val;
|
self.operand_stack[stack_idx] = val;
|
||||||
}
|
}
|
||||||
OpCode::Call => {
|
OpCode::Call => {
|
||||||
|
// addr: destination instruction address
|
||||||
|
// args_count: how many values from the operand stack become locals in the new frame
|
||||||
let addr = self.read_u32()? as usize;
|
let addr = self.read_u32()? as usize;
|
||||||
let args_count = self.read_u32()? as usize;
|
let args_count = self.read_u32()? as usize;
|
||||||
let stack_base = self.operand_stack.len() - args_count;
|
let stack_base = self.operand_stack.len() - args_count;
|
||||||
@ -382,18 +446,21 @@ impl VirtualMachine {
|
|||||||
OpCode::Ret => {
|
OpCode::Ret => {
|
||||||
let frame = self.call_stack.pop().ok_or("Call stack underflow")?;
|
let frame = self.call_stack.pop().ok_or("Call stack underflow")?;
|
||||||
let return_val = self.pop()?;
|
let return_val = self.pop()?;
|
||||||
|
// Clean up the operand stack, removing the frame's locals
|
||||||
self.operand_stack.truncate(frame.stack_base);
|
self.operand_stack.truncate(frame.stack_base);
|
||||||
|
// Return the result of the function
|
||||||
self.push(return_val);
|
self.push(return_val);
|
||||||
self.pc = frame.return_address;
|
self.pc = frame.return_address;
|
||||||
}
|
}
|
||||||
OpCode::PushScope => {
|
OpCode::PushScope => {
|
||||||
|
// Used for blocks within a function that have their own locals
|
||||||
let locals_count = self.read_u32()? as usize;
|
let locals_count = self.read_u32()? as usize;
|
||||||
let stack_base = self.operand_stack.len();
|
let stack_base = self.operand_stack.len();
|
||||||
for _ in 0..locals_count {
|
for _ in 0..locals_count {
|
||||||
self.push(Value::Null);
|
self.push(Value::Null);
|
||||||
}
|
}
|
||||||
self.call_stack.push(CallFrame {
|
self.call_stack.push(CallFrame {
|
||||||
return_address: 0,
|
return_address: 0, // Scope blocks don't return via PC jump
|
||||||
stack_base,
|
stack_base,
|
||||||
locals_count,
|
locals_count,
|
||||||
});
|
});
|
||||||
@ -403,6 +470,7 @@ impl VirtualMachine {
|
|||||||
self.operand_stack.truncate(frame.stack_base);
|
self.operand_stack.truncate(frame.stack_base);
|
||||||
}
|
}
|
||||||
OpCode::Alloc => {
|
OpCode::Alloc => {
|
||||||
|
// Allocates 'size' values on the heap and pushes a reference to the stack
|
||||||
let size = self.read_u32()? as usize;
|
let size = self.read_u32()? as usize;
|
||||||
let ref_idx = self.heap.len();
|
let ref_idx = self.heap.len();
|
||||||
for _ in 0..size {
|
for _ in 0..size {
|
||||||
@ -411,6 +479,7 @@ impl VirtualMachine {
|
|||||||
self.push(Value::Ref(ref_idx));
|
self.push(Value::Ref(ref_idx));
|
||||||
}
|
}
|
||||||
OpCode::LoadRef => {
|
OpCode::LoadRef => {
|
||||||
|
// Reads a value from a heap reference at a specific offset
|
||||||
let offset = self.read_u32()? as usize;
|
let offset = self.read_u32()? as usize;
|
||||||
let ref_val = self.pop()?;
|
let ref_val = self.pop()?;
|
||||||
if let Value::Ref(base) = ref_val {
|
if let Value::Ref(base) = ref_val {
|
||||||
@ -421,6 +490,7 @@ impl VirtualMachine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
OpCode::StoreRef => {
|
OpCode::StoreRef => {
|
||||||
|
// Writes a value to a heap reference at a specific offset
|
||||||
let offset = self.read_u32()? as usize;
|
let offset = self.read_u32()? as usize;
|
||||||
let val = self.pop()?;
|
let val = self.pop()?;
|
||||||
let ref_val = self.pop()?;
|
let ref_val = self.pop()?;
|
||||||
@ -434,15 +504,18 @@ impl VirtualMachine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
OpCode::Syscall => {
|
OpCode::Syscall => {
|
||||||
|
// Calls a native function implemented by the Firmware/OS
|
||||||
let id = self.read_u32()?;
|
let id = self.read_u32()?;
|
||||||
let native_cycles = native.syscall(id, self, hw).map_err(|e| format!("syscall 0x{:08X} failed: {}", id, e))?;
|
let native_cycles = native.syscall(id, self, hw).map_err(|e| format!("syscall 0x{:08X} failed: {}", id, e))?;
|
||||||
self.cycles += native_cycles;
|
self.cycles += native_cycles;
|
||||||
}
|
}
|
||||||
OpCode::FrameSync => {
|
OpCode::FrameSync => {
|
||||||
|
// Already handled in the run_budget loop for performance
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply the instruction cost to the cycle counter
|
||||||
self.cycles += opcode.cycles();
|
self.cycles += opcode.cycles();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,15 +5,27 @@ use prometeu_core::debugger_protocol::*;
|
|||||||
use std::net::{TcpListener, TcpStream};
|
use std::net::{TcpListener, TcpStream};
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
|
|
||||||
|
/// Host-side implementation of the PROMETEU DevTools Protocol.
|
||||||
|
///
|
||||||
|
/// This component acts as a TCP server that allows external tools (like the
|
||||||
|
/// Prometeu Debugger) to observe and control the execution of the virtual machine.
|
||||||
|
///
|
||||||
|
/// Communication is based on JSONL (JSON lines) over TCP.
|
||||||
pub struct HostDebugger {
|
pub struct HostDebugger {
|
||||||
|
/// If true, the VM will not start execution until a 'start' command is received.
|
||||||
pub waiting_for_start: bool,
|
pub waiting_for_start: bool,
|
||||||
|
/// The TCP listener for incoming debugger connections.
|
||||||
pub(crate) listener: Option<TcpListener>,
|
pub(crate) listener: Option<TcpListener>,
|
||||||
|
/// The currently active connection to a debugger client.
|
||||||
pub(crate) stream: Option<TcpStream>,
|
pub(crate) stream: Option<TcpStream>,
|
||||||
|
/// Sequence tracker to ensure logs are sent only once.
|
||||||
last_log_seq: u64,
|
last_log_seq: u64,
|
||||||
|
/// Frame tracker to send telemetry snapshots periodically.
|
||||||
last_telemetry_frame: u64,
|
last_telemetry_frame: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HostDebugger {
|
impl HostDebugger {
|
||||||
|
/// Creates a new debugger interface in an idle state.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
waiting_for_start: false,
|
waiting_for_start: false,
|
||||||
@ -24,17 +36,21 @@ impl HostDebugger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Configures the debugger based on the boot target.
|
||||||
|
/// If debug mode is enabled, it binds to the specified TCP port.
|
||||||
pub fn setup_boot_target(&mut self, boot_target: &BootTarget, firmware: &mut Firmware) {
|
pub fn setup_boot_target(&mut self, boot_target: &BootTarget, firmware: &mut Firmware) {
|
||||||
if let BootTarget::Cartridge { path, debug: true, debug_port } = boot_target {
|
if let BootTarget::Cartridge { path, debug: true, debug_port } = boot_target {
|
||||||
self.waiting_for_start = true;
|
self.waiting_for_start = true;
|
||||||
|
|
||||||
// Pre-loads cartridge information for the handshake
|
// Pre-load cartridge metadata so the Handshake message can contain
|
||||||
|
// valid information about the App being debugged.
|
||||||
if let Ok(cartridge) = CartridgeLoader::load(path) {
|
if let Ok(cartridge) = CartridgeLoader::load(path) {
|
||||||
firmware.os.initialize_vm(&mut firmware.vm, &cartridge);
|
firmware.os.initialize_vm(&mut firmware.vm, &cartridge);
|
||||||
}
|
}
|
||||||
|
|
||||||
match TcpListener::bind(format!("127.0.0.1:{}", debug_port)) {
|
match TcpListener::bind(format!("127.0.0.1:{}", debug_port)) {
|
||||||
Ok(listener) => {
|
Ok(listener) => {
|
||||||
|
// Set listener to non-blocking so it doesn't halt the main loop.
|
||||||
listener.set_nonblocking(true).expect("Cannot set non-blocking");
|
listener.set_nonblocking(true).expect("Cannot set non-blocking");
|
||||||
self.listener = Some(listener);
|
self.listener = Some(listener);
|
||||||
println!("[Debugger] Listening for start command on port {}...", debug_port);
|
println!("[Debugger] Listening for start command on port {}...", debug_port);
|
||||||
@ -48,6 +64,7 @@ impl HostDebugger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sends a structured response to the connected debugger client.
|
||||||
fn send_response(&mut self, resp: DebugResponse) {
|
fn send_response(&mut self, resp: DebugResponse) {
|
||||||
if let Some(stream) = &mut self.stream {
|
if let Some(stream) = &mut self.stream {
|
||||||
if let Ok(json) = serde_json::to_string(&resp) {
|
if let Ok(json) = serde_json::to_string(&resp) {
|
||||||
@ -57,6 +74,7 @@ impl HostDebugger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sends an asynchronous event to the connected debugger client.
|
||||||
fn send_event(&mut self, event: DebugEvent) {
|
fn send_event(&mut self, event: DebugEvent) {
|
||||||
if let Some(stream) = &mut self.stream {
|
if let Some(stream) = &mut self.stream {
|
||||||
if let Ok(json) = serde_json::to_string(&event) {
|
if let Ok(json) = serde_json::to_string(&event) {
|
||||||
@ -66,16 +84,20 @@ impl HostDebugger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Main maintenance method called by the HostRunner every iteration.
|
||||||
|
/// It handles new connections and processes incoming commands.
|
||||||
pub fn check_commands(&mut self, firmware: &mut Firmware, hardware: &mut Hardware) {
|
pub fn check_commands(&mut self, firmware: &mut Firmware, hardware: &mut Hardware) {
|
||||||
|
// 1. Accept new client connections.
|
||||||
if let Some(listener) = &self.listener {
|
if let Some(listener) = &self.listener {
|
||||||
if let Ok((stream, _addr)) = listener.accept() {
|
if let Ok((stream, _addr)) = listener.accept() {
|
||||||
|
// Currently, only one debugger client is supported at a time.
|
||||||
if self.stream.is_none() {
|
if self.stream.is_none() {
|
||||||
println!("[Debugger] Connection received!");
|
println!("[Debugger] Connection received!");
|
||||||
stream.set_nonblocking(true).expect("Cannot set non-blocking on stream");
|
stream.set_nonblocking(true).expect("Cannot set non-blocking on stream");
|
||||||
|
|
||||||
self.stream = Some(stream);
|
self.stream = Some(stream);
|
||||||
|
|
||||||
// Send Handshake
|
// Immediately send the Handshake message to identify the Runtime and App.
|
||||||
let handshake = DebugResponse::Handshake {
|
let handshake = DebugResponse::Handshake {
|
||||||
protocol_version: DEVTOOLS_PROTOCOL_VERSION,
|
protocol_version: DEVTOOLS_PROTOCOL_VERSION,
|
||||||
runtime_version: "0.1".to_string(),
|
runtime_version: "0.1".to_string(),
|
||||||
@ -93,33 +115,35 @@ impl HostDebugger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. Read and process pending commands from the network stream.
|
||||||
if let Some(mut stream) = self.stream.take() {
|
if let Some(mut stream) = self.stream.take() {
|
||||||
let mut buf = [0u8; 4096];
|
let mut buf = [0u8; 4096];
|
||||||
match stream.read(&mut buf) {
|
match stream.read(&mut buf) {
|
||||||
Ok(0) => {
|
Ok(0) => {
|
||||||
|
// TCP socket closed by the client.
|
||||||
println!("[Debugger] Connection closed by remote.");
|
println!("[Debugger] Connection closed by remote.");
|
||||||
self.stream = None;
|
self.stream = None;
|
||||||
|
// Resume VM execution if it was paused waiting for the debugger.
|
||||||
firmware.os.paused = false;
|
firmware.os.paused = false;
|
||||||
self.waiting_for_start = false;
|
self.waiting_for_start = false;
|
||||||
}
|
}
|
||||||
Ok(n) => {
|
Ok(n) => {
|
||||||
let data = &buf[..n];
|
let data = &buf[..n];
|
||||||
// Process multiple commands if there's \n
|
|
||||||
let msg = String::from_utf8_lossy(data);
|
let msg = String::from_utf8_lossy(data);
|
||||||
|
|
||||||
self.stream = Some(stream); // Put it back before processing commands
|
self.stream = Some(stream);
|
||||||
|
|
||||||
|
// Support multiple JSON messages in a single TCP packet.
|
||||||
for line in msg.lines() {
|
for line in msg.lines() {
|
||||||
let trimmed = line.trim();
|
let trimmed = line.trim();
|
||||||
if trimmed.is_empty() {
|
if trimmed.is_empty() { continue; }
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if let Ok(cmd) = serde_json::from_str::<DebugCommand>(trimmed) {
|
if let Ok(cmd) = serde_json::from_str::<DebugCommand>(trimmed) {
|
||||||
self.handle_command(cmd, firmware, hardware);
|
self.handle_command(cmd, firmware, hardware);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
|
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
|
||||||
|
// No data available right now, continue.
|
||||||
self.stream = Some(stream);
|
self.stream = Some(stream);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@ -131,12 +155,13 @@ impl HostDebugger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Streaming de eventos
|
// 3. Push events (logs, telemetry) to the client.
|
||||||
if self.stream.is_some() {
|
if self.stream.is_some() {
|
||||||
self.stream_events(firmware);
|
self.stream_events(firmware);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Dispatches a specific DebugCommand to the system components.
|
||||||
fn handle_command(&mut self, cmd: DebugCommand, firmware: &mut Firmware, hardware: &mut Hardware) {
|
fn handle_command(&mut self, cmd: DebugCommand, firmware: &mut Firmware, hardware: &mut Hardware) {
|
||||||
match cmd {
|
match cmd {
|
||||||
DebugCommand::Ok | DebugCommand::Start => {
|
DebugCommand::Ok | DebugCommand::Start => {
|
||||||
@ -153,15 +178,17 @@ impl HostDebugger {
|
|||||||
firmware.os.paused = false;
|
firmware.os.paused = false;
|
||||||
}
|
}
|
||||||
DebugCommand::Step => {
|
DebugCommand::Step => {
|
||||||
|
// Execute exactly one instruction and keep paused.
|
||||||
firmware.os.paused = true;
|
firmware.os.paused = true;
|
||||||
// Executes an instruction immediately
|
|
||||||
let _ = firmware.os.debug_step_instruction(&mut firmware.vm, hardware);
|
let _ = firmware.os.debug_step_instruction(&mut firmware.vm, hardware);
|
||||||
}
|
}
|
||||||
DebugCommand::StepFrame => {
|
DebugCommand::StepFrame => {
|
||||||
|
// Execute until the end of the current logical frame.
|
||||||
firmware.os.paused = false;
|
firmware.os.paused = false;
|
||||||
firmware.os.debug_step_request = true;
|
firmware.os.debug_step_request = true;
|
||||||
}
|
}
|
||||||
DebugCommand::GetState => {
|
DebugCommand::GetState => {
|
||||||
|
// Return detailed VM register and stack state.
|
||||||
let stack_top = firmware.vm.operand_stack.iter()
|
let stack_top = firmware.vm.operand_stack.iter()
|
||||||
.rev().take(10).cloned().collect();
|
.rev().take(10).cloned().collect();
|
||||||
|
|
||||||
@ -186,13 +213,14 @@ impl HostDebugger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Scans the system for new information to push to the debugger client.
|
||||||
fn stream_events(&mut self, firmware: &mut Firmware) {
|
fn stream_events(&mut self, firmware: &mut Firmware) {
|
||||||
// Logs
|
// 1. Process and send new log entries.
|
||||||
let new_events = firmware.os.log_service.get_after(self.last_log_seq);
|
let new_events = firmware.os.log_service.get_after(self.last_log_seq);
|
||||||
for event in new_events {
|
for event in new_events {
|
||||||
self.last_log_seq = event.seq;
|
self.last_log_seq = event.seq;
|
||||||
|
|
||||||
// Check if it's a breakpoint hit via tag
|
// Map specific internal log tags to protocol events.
|
||||||
if event.tag == 0xDEB1 {
|
if event.tag == 0xDEB1 {
|
||||||
self.send_event(DebugEvent::BreakpointHit {
|
self.send_event(DebugEvent::BreakpointHit {
|
||||||
pc: firmware.vm.pc,
|
pc: firmware.vm.pc,
|
||||||
@ -200,7 +228,7 @@ impl HostDebugger {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Certification via Tags 0xCA01-0xCA03
|
// Map Certification tags (0xCA01-0xCA03) to 'Cert' protocol events.
|
||||||
if event.tag >= 0xCA01 && event.tag <= 0xCA03 {
|
if event.tag >= 0xCA01 && event.tag <= 0xCA03 {
|
||||||
let rule = match event.tag {
|
let rule = match event.tag {
|
||||||
0xCA01 => "cycles_budget",
|
0xCA01 => "cycles_budget",
|
||||||
@ -211,7 +239,7 @@ impl HostDebugger {
|
|||||||
|
|
||||||
self.send_event(DebugEvent::Cert {
|
self.send_event(DebugEvent::Cert {
|
||||||
rule,
|
rule,
|
||||||
used: 0, // Simplified, detailed information is in the log message
|
used: 0,
|
||||||
limit: 0,
|
limit: 0,
|
||||||
frame_index: firmware.os.logical_frame_index,
|
frame_index: firmware.os.logical_frame_index,
|
||||||
});
|
});
|
||||||
@ -224,7 +252,7 @@ impl HostDebugger {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Telemetria (a cada novo frame)
|
// 2. Send telemetry snapshots at the completion of every frame.
|
||||||
let current_frame = firmware.os.logical_frame_index;
|
let current_frame = firmware.os.logical_frame_index;
|
||||||
if current_frame > self.last_telemetry_frame {
|
if current_frame > self.last_telemetry_frame {
|
||||||
let tel = &firmware.os.telemetry_last;
|
let tel = &firmware.os.telemetry_last;
|
||||||
|
|||||||
@ -18,36 +18,62 @@ use winit::window::{Window, WindowAttributes, WindowId};
|
|||||||
|
|
||||||
use prometeu_core::telemetry::CertificationConfig;
|
use prometeu_core::telemetry::CertificationConfig;
|
||||||
|
|
||||||
|
/// The Desktop implementation of the PROMETEU Runtime.
|
||||||
|
///
|
||||||
|
/// This struct acts as the physical "chassis" of the virtual console. It is
|
||||||
|
/// responsible for:
|
||||||
|
/// - Creating and managing the OS window (via `winit`).
|
||||||
|
/// - Initializing the GPU-accelerated framebuffer (via `pixels`).
|
||||||
|
/// - Handling real keyboard/gamepad events and converting them to virtual signals.
|
||||||
|
/// - Providing a high-fidelity audio backend (via `cpal`).
|
||||||
|
/// - Implementing the DevTools Protocol for remote debugging.
|
||||||
|
/// - Maintaining a deterministic 60Hz timing loop.
|
||||||
pub struct HostRunner {
|
pub struct HostRunner {
|
||||||
|
/// The OS window handle.
|
||||||
window: Option<&'static Window>,
|
window: Option<&'static Window>,
|
||||||
|
/// The pixel buffer interface for rendering to the GPU.
|
||||||
pixels: Option<Pixels<'static>>,
|
pixels: Option<Pixels<'static>>,
|
||||||
|
|
||||||
|
/// The instance of the virtual hardware peripherals.
|
||||||
hardware: Hardware,
|
hardware: Hardware,
|
||||||
|
/// The instance of the system firmware and OS logic.
|
||||||
firmware: Firmware,
|
firmware: Firmware,
|
||||||
|
|
||||||
|
/// Helper to collect and normalize input signals.
|
||||||
input: HostInputHandler,
|
input: HostInputHandler,
|
||||||
|
/// Root path for the virtual sandbox filesystem.
|
||||||
fs_root: Option<String>,
|
fs_root: Option<String>,
|
||||||
|
|
||||||
|
/// Sink for system and application logs (prints to console).
|
||||||
log_sink: HostConsoleSink,
|
log_sink: HostConsoleSink,
|
||||||
|
|
||||||
|
/// Target duration for a single frame (nominally 16.66ms for 60Hz).
|
||||||
frame_target_dt: Duration,
|
frame_target_dt: Duration,
|
||||||
|
/// Last recorded wall-clock time to calculate deltas.
|
||||||
last_frame_time: Instant,
|
last_frame_time: Instant,
|
||||||
|
/// Time accumulator used to guarantee exact 60Hz logic updates.
|
||||||
accumulator: Duration,
|
accumulator: Duration,
|
||||||
|
|
||||||
|
/// Performance metrics collector.
|
||||||
stats: HostStats,
|
stats: HostStats,
|
||||||
|
/// Remote debugger interface.
|
||||||
debugger: HostDebugger,
|
debugger: HostDebugger,
|
||||||
|
|
||||||
|
/// Flag to enable/disable the technical telemetry display.
|
||||||
overlay_enabled: bool,
|
overlay_enabled: bool,
|
||||||
|
|
||||||
|
/// The physical audio driver.
|
||||||
audio: HostAudio,
|
audio: HostAudio,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HostRunner {
|
impl HostRunner {
|
||||||
|
/// Configures the boot target (Hub or specific Cartridge).
|
||||||
pub(crate) fn set_boot_target(&mut self, boot_target: BootTarget) {
|
pub(crate) fn set_boot_target(&mut self, boot_target: BootTarget) {
|
||||||
self.firmware.boot_target = boot_target.clone();
|
self.firmware.boot_target = boot_target.clone();
|
||||||
self.debugger.setup_boot_target(&boot_target, &mut self.firmware);
|
self.debugger.setup_boot_target(&boot_target, &mut self.firmware);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a new desktop runner instance.
|
||||||
pub(crate) fn new(fs_root: Option<String>, cap_config: Option<CertificationConfig>) -> Self {
|
pub(crate) fn new(fs_root: Option<String>, cap_config: Option<CertificationConfig>) -> Self {
|
||||||
let target_fps = 60;
|
let target_fps = 60;
|
||||||
|
|
||||||
@ -203,10 +229,13 @@ impl ApplicationHandler for HostRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Called by `winit` when the application is idle and ready to perform updates.
|
||||||
|
/// This is where the core execution loop lives.
|
||||||
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
|
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
|
||||||
|
// 1. Process pending debug commands from the network.
|
||||||
self.debugger.check_commands(&mut self.firmware, &mut self.hardware);
|
self.debugger.check_commands(&mut self.firmware, &mut self.hardware);
|
||||||
|
|
||||||
// Updates Filesystem state in OS (specific to prometeu-runtime-desktop)
|
// 2. Maintain filesystem connection if it was lost (e.g., directory removed).
|
||||||
if let Some(root) = &self.fs_root {
|
if let Some(root) = &self.fs_root {
|
||||||
use prometeu_core::fs::FsState;
|
use prometeu_core::fs::FsState;
|
||||||
if matches!(self.firmware.os.fs_state, FsState::Unmounted | FsState::Error(_)) {
|
if matches!(self.firmware.os.fs_state, FsState::Unmounted | FsState::Error(_)) {
|
||||||
@ -217,10 +246,15 @@ impl ApplicationHandler for HostRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. Timing Management (The heart of determinism).
|
||||||
|
// We measure the elapsed time since the last iteration and add it to an
|
||||||
|
// accumulator. We then execute exactly as many 60Hz slices as the
|
||||||
|
// accumulator allows.
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let mut frame_delta = now.duration_since(self.last_frame_time);
|
let mut frame_delta = now.duration_since(self.last_frame_time);
|
||||||
|
|
||||||
// Limiter to avoid the "death spiral" if the OS freezes (max 100ms per loop)
|
// Safety cap: if the OS freezes or we fall behind too much, we don't try
|
||||||
|
// to catch up indefinitely (avoiding the "death spiral").
|
||||||
if frame_delta > Duration::from_millis(100) {
|
if frame_delta > Duration::from_millis(100) {
|
||||||
frame_delta = Duration::from_millis(100);
|
frame_delta = Duration::from_millis(100);
|
||||||
}
|
}
|
||||||
@ -228,24 +262,27 @@ impl ApplicationHandler for HostRunner {
|
|||||||
self.last_frame_time = now;
|
self.last_frame_time = now;
|
||||||
self.accumulator += frame_delta;
|
self.accumulator += frame_delta;
|
||||||
|
|
||||||
// 🔥 The heart of determinism: consumes time in exact 60Hz slices
|
// 🔥 Logic Update Loop: consumes time in exact 60Hz (16.66ms) slices.
|
||||||
while self.accumulator >= self.frame_target_dt {
|
while self.accumulator >= self.frame_target_dt {
|
||||||
|
// Unless the debugger is waiting for a 'start' command, advance the system.
|
||||||
if !self.debugger.waiting_for_start {
|
if !self.debugger.waiting_for_start {
|
||||||
self.firmware.step_frame(&self.input.signals, &mut self.hardware);
|
self.firmware.step_frame(&self.input.signals, &mut self.hardware);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sync virtual audio commands to the physical mixer.
|
||||||
self.audio.send_commands(&mut self.hardware.audio.commands);
|
self.audio.send_commands(&mut self.hardware.audio.commands);
|
||||||
|
|
||||||
self.accumulator -= self.frame_target_dt;
|
self.accumulator -= self.frame_target_dt;
|
||||||
self.stats.record_frame();
|
self.stats.record_frame();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. Feedback and Synchronization.
|
||||||
self.audio.update_stats(&mut self.stats);
|
self.audio.update_stats(&mut self.stats);
|
||||||
|
|
||||||
// Updates statistics every 1 real second
|
// Update technical statistics displayed in the window title.
|
||||||
self.stats.update(now, self.window, &self.hardware, &self.firmware);
|
self.stats.update(now, self.window, &self.hardware, &self.firmware);
|
||||||
|
|
||||||
// Process system logs
|
// Synchronize system logs to the host console.
|
||||||
let last_seq = self.log_sink.last_seq().unwrap_or(u64::MAX);
|
let last_seq = self.log_sink.last_seq().unwrap_or(u64::MAX);
|
||||||
let new_events = if last_seq == u64::MAX {
|
let new_events = if last_seq == u64::MAX {
|
||||||
self.firmware.os.log_service.get_recent(4096)
|
self.firmware.os.log_service.get_recent(4096)
|
||||||
@ -254,13 +291,15 @@ impl ApplicationHandler for HostRunner {
|
|||||||
};
|
};
|
||||||
self.log_sink.process_events(new_events);
|
self.log_sink.process_events(new_events);
|
||||||
|
|
||||||
// Telemetry Overlay
|
// 5. Rendering the Telemetry Overlay (if enabled).
|
||||||
if self.overlay_enabled {
|
if self.overlay_enabled {
|
||||||
self.hardware.gfx.present(); // Bring front to back to draw over
|
// We temporarily swap buffers to draw over the current image.
|
||||||
|
self.hardware.gfx.present();
|
||||||
self.display_dbg_overlay();
|
self.display_dbg_overlay();
|
||||||
self.hardware.gfx.present(); // Return to front with overlay applied
|
self.hardware.gfx.present();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Finally, request a window redraw to present the new pixels.
|
||||||
self.request_redraw();
|
self.request_redraw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,12 @@ use std::env;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
/// PROMETEU Dispatcher (CLI).
|
||||||
|
///
|
||||||
|
/// The main entry point for the user. This binary does not implement
|
||||||
|
/// compilation or execution logic itself; instead, it acts as a smart
|
||||||
|
/// front-end that locates and dispatches commands to specialized
|
||||||
|
/// components like `prometeu-runtime` or `prometeuc`.
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "prometeu")]
|
#[command(name = "prometeu")]
|
||||||
#[command(about = "Dispatcher for the Prometeu ecosystem", long_about = None)]
|
#[command(about = "Dispatcher for the Prometeu ecosystem", long_about = None)]
|
||||||
@ -13,30 +19,31 @@ struct Cli {
|
|||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum Commands {
|
enum Commands {
|
||||||
/// Executes a cartridge
|
/// Executes a cartridge using the available runtime.
|
||||||
Run {
|
Run {
|
||||||
/// Path to the cartridge
|
/// Path to the cartridge (directory or .pmc file).
|
||||||
cart: String,
|
cart: String,
|
||||||
},
|
},
|
||||||
/// Debugs a cartridge
|
/// Executes a cartridge in Assisted Mode (Debug).
|
||||||
|
/// The runtime will wait for a DevTools connection before starting.
|
||||||
Debug {
|
Debug {
|
||||||
/// Path to the cartridge
|
/// Path to the cartridge.
|
||||||
cart: String,
|
cart: String,
|
||||||
/// Port for the debugger (default: 7777)
|
/// TCP port for the DevTools server (default: 7777).
|
||||||
#[arg(long, default_value_t = 7777)]
|
#[arg(long, default_value_t = 7777)]
|
||||||
port: u16,
|
port: u16,
|
||||||
},
|
},
|
||||||
/// Builds a project
|
/// Compiles a source project into a cartridge (PBC).
|
||||||
Build {
|
Build {
|
||||||
/// Project directory
|
/// Project source directory.
|
||||||
project_dir: String,
|
project_dir: String,
|
||||||
},
|
},
|
||||||
/// Packages a cartridge
|
/// Packages a cartridge directory into a distributable .pmc file.
|
||||||
Pack {
|
Pack {
|
||||||
/// Cartridge directory
|
/// Cartridge directory.
|
||||||
cart_dir: String,
|
cart_dir: String,
|
||||||
},
|
},
|
||||||
/// Verifies the integrity of a project or cartridge
|
/// Diagnostic commands to verify project or cartridge integrity.
|
||||||
Verify {
|
Verify {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
target: VerifyCommands,
|
target: VerifyCommands,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user