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_rgba8888_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, FirmwareState}; 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}; const IDLE_HOST_POLL_DT: Duration = Duration::from_millis(100); #[derive(Debug, Clone)] struct PresentationState { latest_published_frame: u64, last_presented_frame: Option, host_invalidated: bool, redraw_requested: bool, } impl Default for PresentationState { fn default() -> Self { Self { latest_published_frame: 0, last_presented_frame: None, host_invalidated: true, redraw_requested: false, } } } impl PresentationState { fn note_published_frame(&mut self, frame_index: u64) { if frame_index > self.latest_published_frame { self.latest_published_frame = frame_index; } } fn invalidate_host_surface(&mut self) { self.host_invalidated = true; } fn needs_redraw(&self) -> bool { self.host_invalidated || self.last_presented_frame != Some(self.latest_published_frame) } fn should_request_redraw(&mut self) -> bool { if self.needs_redraw() && !self.redraw_requested { self.redraw_requested = true; return true; } false } fn mark_presented(&mut self) { self.redraw_requested = false; self.host_invalidated = false; self.last_presented_frame = Some(self.latest_published_frame); } } fn desired_control_flow( now: Instant, machine_running: bool, accumulator: Duration, frame_target_dt: Duration, ) -> ControlFlow { if machine_running { let remaining = frame_target_dt.saturating_sub(accumulator); ControlFlow::WaitUntil(now + remaining) } else { ControlFlow::WaitUntil(now + IDLE_HOST_POLL_DT) } } /// 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. It consumes host-owned telemetry and runtime state /// directly instead of depending on guest-visible inspection syscalls. 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, /// Tracks whether a new logical frame or host-only surface invalidation requires presentation. presentation: PresentationState, } 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.fs().mount(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, presentation: PresentationState::default(), } } 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 invalidate_host_surface(&mut self) { self.presentation.invalidate_host_surface(); } fn request_redraw_if_needed(&mut self) { match self.firmware.state { FirmwareState::GameRunning(_) => { if self.presentation.should_request_redraw() { self.request_redraw(); } } _ => { self.request_redraw(); } } } fn machine_running(&mut self) -> bool { !self.firmware.os.vm().paused() && !self.debugger.waiting_for_start } } 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); } self.invalidate_host_surface(); self.request_redraw_if_needed(); event_loop.set_control_flow(ControlFlow::Wait); } 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); self.invalidate_host_surface(); self.request_redraw_if_needed(); } WindowEvent::ScaleFactorChanged { .. } => { let size = self.window().inner_size(); self.resize_surface(size.width, size.height); self.invalidate_host_surface(); self.request_redraw_if_needed(); } WindowEvent::RedrawRequested => { let overlay_snapshot = self.overlay_enabled.then(|| capture_snapshot(&self.stats, &mut 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_rgba8888_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(); } else { self.presentation.mark_presented(); } } 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; self.invalidate_host_surface(); self.request_redraw_if_needed(); println!("[Debugger] Execution started!"); } if is_down && code == KeyCode::F1 { self.overlay_enabled = !self.overlay_enabled; self.invalidate_host_surface(); self.request_redraw_if_needed(); } } } _ => {} } } /// 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) { let was_debugger_connected = self.debugger.stream.is_some(); let was_waiting_for_start = self.debugger.waiting_for_start; let was_paused = self.firmware.os.vm().paused(); // 1. Process pending debug commands from the network. self.debugger.check_commands(&mut self.firmware, &mut self.hardware); // Sync inspection mode state. This is host-owned overlay/debugger control, // not a guest-visible debug ABI switch. self.firmware .os .vm() .set_inspection_active(self.overlay_enabled || self.debugger.stream.is_some()); // 2. Maintain a filesystem connection if it was lost (e.g., directory removed). if let Some(root) = &self.fs_root && self.firmware.os.fs().needs_mount() && std::path::Path::new(root).exists() { let backend = HostDirBackend::new(root); self.firmware.os.fs().mount(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.vm().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.vm().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(); } self.presentation.note_published_frame(self.firmware.os.frame_index()); if was_debugger_connected != self.debugger.stream.is_some() || was_waiting_for_start != self.debugger.waiting_for_start || was_paused != self.firmware.os.vm().paused() { self.invalidate_host_surface(); } // 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, &mut 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.recent_logs(4096) } else { self.firmware.os.logs_after(last_seq) }; self.log_sink.process_events(new_events); // 5. Request redraw only when a new logical frame was published or the // host-owned presentation surface became invalid. self.request_redraw_if_needed(); event_loop.set_control_flow(desired_control_flow( Instant::now(), self.machine_running(), self.accumulator, self.frame_target_dt, )); } } #[cfg(test)] mod tests { use super::*; use prometeu_firmware::BootTarget; use prometeu_hal::debugger_protocol::DEVTOOLS_PROTOCOL_VERSION; use prometeu_hal::telemetry::{CertificationConfig, TelemetryFrame}; use std::io::{Read, Write}; use std::net::TcpStream; #[test] fn presentation_state_requires_initial_redraw_only_once() { let mut state = PresentationState::default(); assert!(state.should_request_redraw()); assert!(!state.should_request_redraw()); state.mark_presented(); assert!(!state.should_request_redraw()); } #[test] fn presentation_state_requests_redraw_for_new_frame_publication() { let mut state = PresentationState::default(); state.mark_presented(); state.note_published_frame(1); assert!(state.should_request_redraw()); state.mark_presented(); assert_eq!(state.last_presented_frame, Some(1)); } #[test] fn presentation_state_requests_redraw_for_host_invalidation_without_new_frame() { let mut state = PresentationState::default(); state.mark_presented(); state.invalidate_host_surface(); assert!(state.should_request_redraw()); state.mark_presented(); assert!(!state.needs_redraw()); } #[test] fn desired_control_flow_waits_until_next_logical_deadline_while_running() { let now = Instant::now(); let flow = desired_control_flow(now, true, Duration::from_millis(5), Duration::from_millis(16)); match flow { ControlFlow::WaitUntil(deadline) => { assert_eq!(deadline.duration_since(now), Duration::from_millis(11)); } other => panic!("unexpected control flow: {:?}", other), } } #[test] fn desired_control_flow_uses_idle_poll_when_machine_is_not_running() { let now = Instant::now(); let flow = desired_control_flow(now, false, Duration::from_millis(5), Duration::from_millis(16)); match flow { ControlFlow::WaitUntil(deadline) => { assert_eq!(deadline.duration_since(now), IDLE_HOST_POLL_DT); } other => panic!("unexpected control flow: {:?}", other), } } #[test] fn host_debugger_maps_cert_events_from_host_owned_sources() { let telemetry = TelemetryFrame { glyph_slots_used: 3, heap_used_bytes: 4096, logs_count: 7, ..Default::default() }; let config = CertificationConfig { enabled: true, max_glyph_slots_used: Some(2), max_heap_bytes: Some(2048), max_logs_per_frame: Some(5), ..Default::default() }; match HostDebugger::cert_event_from_snapshot(0xCA04, telemetry, &config, 12) { Some(prometeu_hal::debugger_protocol::DebugEvent::Cert { rule, used, limit, frame_index, }) => { assert_eq!(rule, "max_glyph_slots_used"); assert_eq!(used, 3); assert_eq!(limit, 2); assert_eq!(frame_index, 12); } other => panic!("unexpected glyph cert event: {:?}", other), } match HostDebugger::cert_event_from_snapshot(0xCA06, telemetry, &config, 12) { Some(prometeu_hal::debugger_protocol::DebugEvent::Cert { rule, used, limit, frame_index, }) => { assert_eq!(rule, "max_heap_bytes"); assert_eq!(used, 4096); assert_eq!(limit, 2048); assert_eq!(frame_index, 12); } other => panic!("unexpected heap cert event: {:?}", other), } match HostDebugger::cert_event_from_snapshot(0xCA07, telemetry, &config, 12) { Some(prometeu_hal::debugger_protocol::DebugEvent::Cert { rule, used, limit, frame_index, }) => { assert_eq!(rule, "max_logs_per_frame"); assert_eq!(used, 7); assert_eq!(limit, 5); assert_eq!(frame_index, 12); } other => panic!("unexpected logs cert event: {:?}", other), } } #[test] fn host_debugger_maps_telemetry_from_atomic_snapshot_fields() { let telemetry = TelemetryFrame { frame_index: 8, vm_steps: 123, syscalls: 9, cycles_used: 456, cycles_budget: 789, host_cpu_time_us: 321, violations: 2, glyph_slots_used: 1, glyph_slots_total: 16, sound_slots_used: 0, sound_slots_total: 8, scene_slots_used: 2, scene_slots_total: 16, ..Default::default() }; match HostDebugger::telemetry_event_from_snapshot(telemetry) { prometeu_hal::debugger_protocol::DebugEvent::Telemetry { frame_index, vm_steps, syscalls, cycles, cycles_budget, host_cpu_time_us, violations, glyph_slots_used, glyph_slots_total, sound_slots_used, sound_slots_total, scene_slots_used, scene_slots_total, } => { assert_eq!(frame_index, 8); assert_eq!(vm_steps, 123); assert_eq!(syscalls, 9); assert_eq!(cycles, 456); assert_eq!(cycles_budget, 789); assert_eq!(host_cpu_time_us, 321); assert_eq!(violations, 2); assert_eq!(glyph_slots_used, 1); assert_eq!(glyph_slots_total, 16); assert_eq!(sound_slots_used, 0); assert_eq!(sound_slots_total, 8); assert_eq!(scene_slots_used, 2); assert_eq!(scene_slots_total, 16); } other => panic!("unexpected telemetry event: {:?}", other), } } // These ignored tests intentionally cover host-dependent behavior that requires // real socket bind/connect coordination. Deterministic debugger state changes // are covered below the host layer in `debugger.rs`. #[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) && 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.vm().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.vm().paused(), "VM should have unpaused after disconnect"); assert!(!runner.debugger.waiting_for_start, "VM should have left waiting_for_start state"); } }