2026-03-24 13:40:46 +00:00

607 lines
23 KiB
Rust

use crate::audio::HostAudio;
use crate::debugger::HostDebugger;
use crate::fs_backend::HostDirBackend;
use crate::input::HostInputHandler;
use crate::log_sink::HostConsoleSink;
use crate::stats::HostStats;
use crate::utilities::draw_rgb565_to_rgba8;
use pixels::wgpu::PresentMode;
use pixels::{Pixels, PixelsBuilder, SurfaceTexture};
use prometeu_drivers::hardware::Hardware;
use prometeu_drivers::AudioCommand;
use prometeu_firmware::{BootTarget, Firmware};
use prometeu_hal::color::Color;
use prometeu_hal::telemetry::CertificationConfig;
use std::time::{Duration, Instant};
use winit::application::ApplicationHandler;
use winit::dpi::LogicalSize;
use winit::event::{ElementState, WindowEvent};
use winit::event_loop::{ActiveEventLoop, ControlFlow};
use winit::keyboard::{KeyCode, PhysicalKey};
use winit::window::{Window, WindowAttributes, WindowId};
/// 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,
/// Last known pause state to sync with audio.
last_paused_state: bool,
}
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;
let mut firmware = Firmware::new(cap_config);
if let Some(root) = &fs_root {
let backend = HostDirBackend::new(root);
firmware.os.mount_fs(Box::new(backend));
}
Self {
window: None,
pixels: None,
hardware: Hardware::new(),
firmware,
input: HostInputHandler::new(),
fs_root,
log_sink: HostConsoleSink::new(),
frame_target_dt: Duration::from_nanos(1_000_000_000 / target_fps),
last_frame_time: Instant::now(),
accumulator: Duration::ZERO,
stats: HostStats::new(),
debugger: HostDebugger::new(),
overlay_enabled: false,
audio: HostAudio::new(),
last_paused_state: false,
}
}
fn window(&self) -> &'static Window {
self.window.expect("window not created yet")
}
fn resize_surface(&mut self, width: u32, height: u32) {
if let Some(p) = self.pixels.as_mut() {
let _ = p.resize_surface(width, height);
}
}
fn request_redraw(&self) {
if let Some(w) = self.window.as_ref() {
w.request_redraw();
}
}
fn display_dbg_overlay(&mut self) {
let tel = &self.firmware.os.telemetry_last;
let color_text = Color::WHITE;
let color_bg = Color::INDIGO; // Dark blue to stand out
let color_warn = Color::RED;
self.hardware.gfx.fill_rect(5, 5, 175, 100, color_bg);
self.hardware.gfx.draw_text(
10,
10,
&format!("FPS: {:.1}", self.stats.current_fps),
color_text,
);
self.hardware.gfx.draw_text(
10,
18,
&format!("HOST: {:.2}MS", tel.host_cpu_time_us as f64 / 1000.0),
color_text,
);
self.hardware.gfx.draw_text(10, 26, &format!("STEPS: {}", tel.vm_steps), color_text);
self.hardware.gfx.draw_text(10, 34, &format!("SYSC: {}", tel.syscalls), color_text);
let cycles_pct = if tel.cycles_budget > 0 {
(tel.cycles_used as f64 / tel.cycles_budget as f64) * 100.0
} else {
0.0
};
self.hardware.gfx.draw_text(
10,
42,
&format!("CYC: {}/{} ({:.1}%)", tel.cycles_used, tel.cycles_budget, cycles_pct),
color_text,
);
self.hardware.gfx.draw_text(
10,
50,
&format!("GFX: {}K/16M ({}S)", tel.gfx_used_bytes / 1024, tel.gfx_slots_occupied),
color_text,
);
if tel.gfx_inflight_bytes > 0 {
self.hardware.gfx.draw_text(
10,
58,
&format!("LOAD GFX: {}KB", tel.gfx_inflight_bytes / 1024),
color_warn,
);
}
self.hardware.gfx.draw_text(
10,
66,
&format!("AUD: {}K/32M ({}S)", tel.audio_used_bytes / 1024, tel.audio_slots_occupied),
color_text,
);
if tel.audio_inflight_bytes > 0 {
self.hardware.gfx.draw_text(
10,
74,
&format!("LOAD AUD: {}KB", tel.audio_inflight_bytes / 1024),
color_warn,
);
}
let cert_color = if tel.violations > 0 { color_warn } else { color_text };
self.hardware.gfx.draw_text(10, 82, &format!("CERT LAST: {}", tel.violations), cert_color);
if tel.violations > 0 {
if let Some(event) = self
.firmware
.os
.log_service
.get_recent(10)
.into_iter()
.rev()
.find(|e| e.tag >= 0xCA01 && e.tag <= 0xCA03)
{
let mut msg = event.msg.clone();
if msg.len() > 30 {
msg.truncate(30);
}
self.hardware.gfx.draw_text(10, 90, &msg, color_warn);
}
}
}
}
impl ApplicationHandler for HostRunner {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
let attrs = WindowAttributes::default()
.with_title(format!(
"PROMETEU | GFX: {:.1} KB | FPS: {:.1} | Load: {:.1}% (C) + {:.1}% (A) | Frame: tick {} logical {} done {}",
0.0, 0.0, 0.0, 0, 0, 0, 0))
.with_inner_size(LogicalSize::new(960.0, 540.0))
.with_min_inner_size(LogicalSize::new(320.0, 180.0));
let window = event_loop.create_window(attrs).expect("failed to create window");
// 🔥 Leak: Window becomes &'static Window (bootstrap)
let window: &'static Window = Box::leak(Box::new(window));
self.window = Some(window);
let size = window.inner_size();
let surface_texture = SurfaceTexture::new(size.width, size.height, window);
let mut pixels =
PixelsBuilder::new(Hardware::W as u32, Hardware::H as u32, surface_texture)
.present_mode(PresentMode::Fifo) // activate vsync
.build()
.expect("failed to create Pixels");
pixels.frame_mut().fill(0);
self.pixels = Some(pixels);
if let Err(err) = self.audio.init() {
eprintln!("[HostAudio] Disabled: {}", err);
}
event_loop.set_control_flow(ControlFlow::Poll);
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
self.input.handle_event(&event, self.window());
match event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::Resized(size) => {
self.resize_surface(size.width, size.height);
}
WindowEvent::ScaleFactorChanged { .. } => {
let size = self.window().inner_size();
self.resize_surface(size.width, size.height);
}
WindowEvent::RedrawRequested => {
// Get Pixels directly from the field (not via helper that gets the entire &mut self)
let pixels = self.pixels.as_mut().expect("pixels not initialized");
{
// Mutable borrow of the frame (lasts only within this block)
let frame = pixels.frame_mut();
// Immutable borrow of prometeu-core (different field, ok)
let src = self.hardware.gfx.front_buffer();
draw_rgb565_to_rgba8(src, frame);
} // <- frame borrow ends here
if pixels.render().is_err() {
event_loop.exit();
}
}
WindowEvent::KeyboardInput { event, .. } => {
if let PhysicalKey::Code(code) = event.physical_key {
let is_down = event.state == ElementState::Pressed;
if is_down && code == KeyCode::KeyD && self.debugger.waiting_for_start {
self.debugger.waiting_for_start = false;
println!("[Debugger] Execution started!");
}
if is_down && code == KeyCode::F1 {
self.overlay_enabled = !self.overlay_enabled;
}
}
}
_ => {}
}
}
/// 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);
// 2. Maintain filesystem connection if it was lost (e.g., directory removed).
if let Some(root) = &self.fs_root {
use prometeu_system::fs::FsState;
if matches!(self.firmware.os.fs_state, FsState::Unmounted | FsState::Error(_)) {
if std::path::Path::new(root).exists() {
let backend = HostDirBackend::new(root);
self.firmware.os.mount_fs(Box::new(backend));
}
}
}
// 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);
// 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);
}
self.last_frame_time = now;
self.accumulator += frame_delta;
// 🔥 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.tick(&self.input.signals, &mut self.hardware);
}
// Sync pause state with audio.
// We do this AFTER firmware.tick to avoid MasterPause/Resume commands
// being cleared by the OS if a new logical frame starts in this tick.
let is_paused = self.firmware.os.paused || self.debugger.waiting_for_start;
if is_paused != self.last_paused_state {
self.last_paused_state = is_paused;
let cmd =
if is_paused { AudioCommand::MasterPause } else { AudioCommand::MasterResume };
self.hardware.audio.commands.push(cmd);
}
// 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);
// Update technical statistics displayed in the window title.
self.stats.update(now, self.window, &self.hardware, &self.firmware);
// 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)
} else {
self.firmware.os.log_service.get_after(last_seq)
};
self.log_sink.process_events(new_events);
// 5. Rendering the Telemetry Overlay (if enabled).
if self.overlay_enabled {
// We temporarily swap buffers to draw over the current image.
self.hardware.gfx.present();
self.display_dbg_overlay();
self.hardware.gfx.present();
}
// Finally, request a window redraw to present the new pixels.
self.request_redraw();
}
}
#[cfg(test)]
mod tests {
use super::*;
use prometeu_firmware::BootTarget;
use prometeu_hal::debugger_protocol::DEVTOOLS_PROTOCOL_VERSION;
use std::io::{Read, Write};
use std::net::TcpStream;
#[test]
fn test_debug_port_opens() {
let mut runner = HostRunner::new(None, None);
let port = 9999;
runner.set_boot_target(BootTarget::Cartridge {
path: "dummy.bin".to_string(),
debug: true,
debug_port: port,
});
assert!(runner.debugger.waiting_for_start);
assert!(runner.debugger.listener.is_some());
// Check if we can connect
{
let mut stream =
TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect");
// Short sleep to ensure the OS processes
std::thread::sleep(std::time::Duration::from_millis(100));
// Simulates the loop to accept the connection
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
assert!(runner.debugger.stream.is_some(), "Stream should have been kept open");
// Handshake Check
let mut buf = [0u8; 2048];
let n = stream.read(&mut buf).expect("Should read handshake");
let resp: serde_json::Value =
serde_json::from_slice(&buf[..n]).expect("Handshake should be valid JSON");
assert_eq!(resp["type"], "handshake");
assert_eq!(resp["protocol_version"], DEVTOOLS_PROTOCOL_VERSION);
// Send start via JSON
stream
.write_all(b"{\"type\":\"start\"}\n")
.expect("Connection should be open for writing");
std::thread::sleep(std::time::Duration::from_millis(50));
// Process the received command
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
assert!(
!runner.debugger.waiting_for_start,
"Execution should have started after start command"
);
assert!(
runner.debugger.listener.is_some(),
"Listener should remain open for reconnections"
);
}
// Now that the stream is out of the test scope, the runner should detect closure on next check
std::thread::sleep(std::time::Duration::from_millis(50));
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
assert!(
runner.debugger.stream.is_none(),
"Stream should have been closed after client disconnected"
);
}
#[test]
fn test_debug_reconnection() {
let mut runner = HostRunner::new(None, None);
let port = 9998;
runner.set_boot_target(BootTarget::Cartridge {
path: "dummy.bin".to_string(),
debug: true,
debug_port: port,
});
// 1. Connect and start
{
let mut stream =
TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect 1");
std::thread::sleep(std::time::Duration::from_millis(50));
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
assert!(runner.debugger.stream.is_some());
stream.write_all(b"{\"type\":\"start\"}\n").expect("Should write start");
std::thread::sleep(std::time::Duration::from_millis(50));
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
assert!(!runner.debugger.waiting_for_start);
// Currently the listener is closed here.
}
// 2. Disconnect (clears stream in runner)
std::thread::sleep(std::time::Duration::from_millis(50));
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
assert!(runner.debugger.stream.is_none());
// 3. Try to reconnect - SHOULD FAIL currently, but we want it to WORK
let stream2 = TcpStream::connect(format!("127.0.0.1:{}", port));
assert!(stream2.is_ok(), "Should accept new connection even after start");
std::thread::sleep(std::time::Duration::from_millis(50));
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
assert!(
runner.debugger.stream.is_some(),
"Stream should have been accepted on reconnection"
);
}
#[test]
fn test_debug_refuse_second_connection() {
let mut runner = HostRunner::new(None, None);
let port = 9997;
runner.set_boot_target(BootTarget::Cartridge {
path: "dummy.bin".to_string(),
debug: true,
debug_port: port,
});
// 1. First connection
let mut _stream1 =
TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect 1");
std::thread::sleep(std::time::Duration::from_millis(50));
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
assert!(runner.debugger.stream.is_some());
// 2. Second connection
let mut stream2 = TcpStream::connect(format!("127.0.0.1:{}", port))
.expect("Should connect 2 (OS accepts)");
std::thread::sleep(std::time::Duration::from_millis(50));
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware); // Should accept and close stream2
// Check if stream2 was closed by the server
let mut buf = [0u8; 10];
stream2.set_read_timeout(Some(std::time::Duration::from_millis(100))).unwrap();
let res = stream2.read(&mut buf);
assert!(
matches!(res, Ok(0)) || res.is_err(),
"Second connection should be closed by server"
);
assert!(runner.debugger.stream.is_some(), "First connection should continue active");
}
#[test]
fn test_get_state_returns_response() {
let mut runner = HostRunner::new(None, None);
let port = 9996;
runner.set_boot_target(BootTarget::Cartridge {
path: "dummy.bin".to_string(),
debug: true,
debug_port: port,
});
let stream = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect");
std::thread::sleep(std::time::Duration::from_millis(100));
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
use std::io::BufRead;
let mut reader = std::io::BufReader::new(stream);
let mut line = String::new();
reader.read_line(&mut line).expect("Should read handshake");
assert!(line.contains("handshake"));
// Send getState
reader.get_mut().write_all(b"{\"type\":\"getState\"}\n").expect("Should write getState");
std::thread::sleep(std::time::Duration::from_millis(100));
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
// Check if response received (may have events/logs before)
loop {
line.clear();
reader.read_line(&mut line).expect("Should read line");
if line.is_empty() {
break;
}
if let Ok(resp) = serde_json::from_str::<serde_json::Value>(&line) {
if resp["type"] == "getState" {
return;
}
}
}
panic!("Did not receive getState response");
}
#[test]
fn test_debug_resume_on_disconnect() {
let mut runner = HostRunner::new(None, None);
let port = 9995;
runner.set_boot_target(BootTarget::Cartridge {
path: "dummy.bin".to_string(),
debug: true,
debug_port: port,
});
// 1. Connect and pause
{
let mut stream =
TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect");
std::thread::sleep(std::time::Duration::from_millis(50));
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
stream.write_all(b"{\"type\":\"pause\"}\n").expect("Should write pause");
std::thread::sleep(std::time::Duration::from_millis(50));
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
assert!(runner.firmware.os.paused, "VM should be paused");
}
// 2. Disconnect (stream goes out of scope)
std::thread::sleep(std::time::Duration::from_millis(50));
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
// 3. Check if unpaused
assert!(!runner.firmware.os.paused, "VM should have unpaused after disconnect");
assert!(!runner.debugger.waiting_for_start, "VM should have left waiting_for_start state");
}
}