use crate::audio::HostAudio; use crate::debugger::HostDebugger; use crate::fs_backend::HostDirBackend; use crate::input::HostInputHandler; use crate::log_sink::HostConsoleSink; use crate::overlay::{capture_snapshot, draw_overlay}; 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::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(); } } } 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 => { let overlay_snapshot = self.overlay_enabled.then(|| capture_snapshot(&self.stats, &self.firmware)); // 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); if let Some(snapshot) = overlay_snapshot.as_ref() { draw_overlay(frame, Hardware::W, Hardware::H, snapshot); } } // <- 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); // Sync inspection mode state. self.firmware.os.inspection_active = self.overlay_enabled || self.debugger.stream.is_some(); // 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); self.stats.record_host_cpu_time(self.firmware.os.last_frame_cpu_time_us); } // 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. Request redraw so the host surface can present the latest machine frame // and, when enabled, compose the overlay in the host-only RGBA surface. 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"); } }