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::AudioCommand; use prometeu_drivers::hardware::Hardware; 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>, /// The instance of the virtual hardware peripherals. hardware: Hardware, /// The instance of the system firmware and OS logic. firmware: Firmware, /// Helper to collect and normalize input signals. input: HostInputHandler, /// Root path for the virtual sandbox filesystem. fs_root: Option, /// Sink for system and application logs (prints to console). log_sink: HostConsoleSink, /// Target duration for a single frame (nominally 16.66ms for 60Hz). frame_target_dt: Duration, /// Last recorded wall-clock time to calculate deltas. last_frame_time: Instant, /// Time accumulator used to guarantee exact 60Hz logic updates. accumulator: Duration, /// Performance metrics collector. stats: HostStats, /// Remote debugger interface. debugger: HostDebugger, /// Flag to enable/disable the technical telemetry display. overlay_enabled: bool, /// The physical audio driver. audio: HostAudio, /// 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, cap_config: Option) -> 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 && 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); } if let Some(report) = self.firmware.os.last_crash_report.as_ref() { let mut msg = report.summary(); if msg.len() > 30 { msg.truncate(30); } self.hardware.gfx.draw_text(10, 98, &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(_)) && 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] #[ignore = "requires localhost TCP bind/connect; run via `cargo test -p prometeu-host-desktop-winit --lib -- --ignored`"] 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] #[ignore = "requires localhost TCP bind/connect; run via `cargo test -p prometeu-host-desktop-winit --lib -- --ignored`"] 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] #[ignore = "requires localhost TCP bind/connect; run via `cargo test -p prometeu-host-desktop-winit --lib -- --ignored`"] 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] #[ignore = "requires localhost TCP bind/connect; run via `cargo test -p prometeu-host-desktop-winit --lib -- --ignored`"] 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::(&line) { if resp["type"] == "getState" { return; } } } panic!("Did not receive getState response"); } #[test] #[ignore = "requires localhost TCP bind/connect; run via `cargo test -p prometeu-host-desktop-winit --lib -- --ignored`"] 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"); } }