diff --git a/crates/host-desktop/src/audio_mixer.rs b/crates/host-desktop/src/audio.rs similarity index 62% rename from crates/host-desktop/src/audio_mixer.rs rename to crates/host-desktop/src/audio.rs index 9423a2d2..1cd65dc6 100644 --- a/crates/host-desktop/src/audio_mixer.rs +++ b/crates/host-desktop/src/audio.rs @@ -1,7 +1,88 @@ use prometeu_core::hardware::{AudioCommand, Channel, LoopMode, MAX_CHANNELS, OUTPUT_SAMPLE_RATE}; - +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use ringbuf::traits::{Consumer, Producer, Split}; +use ringbuf::HeapRb; +use std::sync::Arc; use std::time::Duration; +pub struct HostAudio { + pub producer: Option>>>, + pub perf_consumer: Option>>>, + _stream: Option, +} + +impl HostAudio { + pub fn new() -> Self { + Self { + producer: None, + perf_consumer: None, + _stream: None, + } + } + + pub fn init(&mut self) { + let host = cpal::default_host(); + let device = host + .default_output_device() + .expect("no output device available"); + + let config = cpal::StreamConfig { + channels: 2, + sample_rate: cpal::SampleRate(OUTPUT_SAMPLE_RATE), + buffer_size: cpal::BufferSize::Default, + }; + + let rb = HeapRb::::new(1024); + let (prod, mut cons) = rb.split(); + + self.producer = Some(prod); + + let mut mixer = AudioMixer::new(); + + // Para passar dados de performance da thread de áudio para a principal + let audio_perf_rb = HeapRb::::new(64); + let (mut perf_prod, perf_cons) = audio_perf_rb.split(); + + let stream = device + .build_output_stream( + &config, + move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { + // Consome comandos da ringbuffer + while let Some(cmd) = cons.try_pop() { + mixer.process_command(cmd); + } + // Mixa áudio + mixer.fill_buffer(data); + // Envia tempo de processamento em microssegundos + let _ = perf_prod.try_push(mixer.last_processing_time.as_micros() as u64); + }, + |err| eprintln!("audio stream error: {}", err), + None, + ) + .expect("failed to build audio stream"); + + stream.play().expect("failed to play audio stream"); + self._stream = Some(stream); + self.perf_consumer = Some(perf_cons); + } + + pub fn send_commands(&mut self, commands: &mut Vec) { + if let Some(producer) = &mut self.producer { + for cmd in commands.drain(..) { + let _ = producer.try_push(cmd); + } + } + } + + pub fn update_stats(&mut self, stats: &mut crate::stats::HostStats) { + if let Some(cons) = &mut self.perf_consumer { + while let Some(us) = cons.try_pop() { + stats.record_audio_perf(us); + } + } + } +} + pub struct AudioMixer { voices: [Channel; MAX_CHANNELS], pub last_processing_time: Duration, diff --git a/crates/host-desktop/src/cap.rs b/crates/host-desktop/src/cap.rs new file mode 100644 index 00000000..511e9788 --- /dev/null +++ b/crates/host-desktop/src/cap.rs @@ -0,0 +1,53 @@ +use prometeu_core::telemetry::CertificationConfig; + +pub fn load_cap_config(path: &str) -> Option { + let content = std::fs::read_to_string(path).ok()?; + let mut config = CertificationConfig { + enabled: true, + ..Default::default() + }; + + for line in content.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { continue; } + let parts: Vec<&str> = line.split('=').collect(); + if parts.len() != 2 { continue; } + let key = parts[0].trim(); + let val = parts[1].trim(); + + match key { + "cycles_budget" => config.cycles_budget_per_frame = val.parse().ok(), + "max_syscalls" => config.max_syscalls_per_frame = val.parse().ok(), + "max_host_cpu_us" => config.max_host_cpu_us_per_frame = val.parse().ok(), + _ => {} + } + } + + Some(config) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_load_cap_config() { + let content = "cycles_budget=500\nmax_syscalls=10\n# comentário\nmax_host_cpu_us=2000"; + let path = "test_cap.cfg"; + std::fs::write(path, content).unwrap(); + + let config = load_cap_config(path).unwrap(); + assert!(config.enabled); + assert_eq!(config.cycles_budget_per_frame, Some(500)); + assert_eq!(config.max_syscalls_per_frame, Some(10)); + assert_eq!(config.max_host_cpu_us_per_frame, Some(2000)); + + std::fs::remove_file(path).unwrap(); + } + + #[test] + fn test_load_cap_config_not_found() { + let config = load_cap_config("non_existent.cfg"); + assert!(config.is_none()); + } +} diff --git a/crates/host-desktop/src/debugger.rs b/crates/host-desktop/src/debugger.rs new file mode 100644 index 00000000..7c383a99 --- /dev/null +++ b/crates/host-desktop/src/debugger.rs @@ -0,0 +1,240 @@ +use prometeu_core::firmware::{BootTarget, Firmware}; +use prometeu_core::Hardware; +use prometeu_core::model::CartridgeLoader; +use prometeu_core::debugger_protocol::*; +use std::net::{TcpListener, TcpStream}; +use std::io::{Read, Write}; + +pub struct HostDebugger { + pub waiting_for_start: bool, + pub(crate) listener: Option, + pub(crate) stream: Option, + last_log_seq: u64, + last_telemetry_frame: u64, +} + +impl HostDebugger { + pub fn new() -> Self { + Self { + waiting_for_start: false, + listener: None, + stream: None, + last_log_seq: 0, + last_telemetry_frame: 0, + } + } + + pub fn setup_boot_target(&mut self, boot_target: &BootTarget, firmware: &mut Firmware) { + if let BootTarget::Cartridge { path, debug: true, debug_port } = boot_target { + self.waiting_for_start = true; + + // Pré-carrega informações do cartucho para o handshake + if let Ok(cartridge) = CartridgeLoader::load(path) { + firmware.os.initialize_vm(&mut firmware.vm, &cartridge); + } + + match TcpListener::bind(format!("127.0.0.1:{}", debug_port)) { + Ok(listener) => { + listener.set_nonblocking(true).expect("Cannot set non-blocking"); + self.listener = Some(listener); + println!("[Debugger] Listening for start command on port {}...", debug_port); + } + Err(e) => { + eprintln!("[Debugger] Failed to bind to port {}: {}", debug_port, e); + } + } + + println!("[Debugger] (Or press D to start execution)"); + } + } + + fn send_response(&mut self, resp: DebugResponse) { + if let Some(stream) = &mut self.stream { + if let Ok(json) = serde_json::to_string(&resp) { + let _ = stream.write_all(json.as_bytes()); + let _ = stream.write_all(b"\n"); + } + } + } + + fn send_event(&mut self, event: DebugEvent) { + if let Some(stream) = &mut self.stream { + if let Ok(json) = serde_json::to_string(&event) { + let _ = stream.write_all(json.as_bytes()); + let _ = stream.write_all(b"\n"); + } + } + } + + pub fn check_commands(&mut self, firmware: &mut Firmware, hardware: &mut Hardware) { + if let Some(listener) = &self.listener { + if let Ok((stream, _addr)) = listener.accept() { + if self.stream.is_none() { + println!("[Debugger] Connection received!"); + stream.set_nonblocking(true).expect("Cannot set non-blocking on stream"); + + self.stream = Some(stream); + + // Enviar Handshake + let handshake = DebugResponse::Handshake { + protocol_version: DEVTOOLS_PROTOCOL_VERSION, + runtime_version: "0.1".to_string(), + cartridge: HandshakeCartridge { + app_id: firmware.os.current_app_id, + title: firmware.os.current_cartridge_title.clone(), + app_version: firmware.os.current_cartridge_app_version.clone(), + app_mode: firmware.os.current_cartridge_app_mode, + }, + }; + self.send_response(handshake); + } else { + println!("[Debugger] Connection refused: already connected."); + } + } + } + + if let Some(mut stream) = self.stream.take() { + let mut buf = [0u8; 4096]; + match stream.read(&mut buf) { + Ok(0) => { + println!("[Debugger] Connection closed by remote."); + self.stream = None; + firmware.os.paused = false; + self.waiting_for_start = false; + } + Ok(n) => { + let data = &buf[..n]; + // Processar múltiplos comandos se houver \n + let msg = String::from_utf8_lossy(data); + + self.stream = Some(stream); // Coloca de volta antes de processar comandos + + for line in msg.lines() { + let trimmed = line.trim(); + if trimmed.is_empty() { + continue; + } + if let Ok(cmd) = serde_json::from_str::(trimmed) { + self.handle_command(cmd, firmware, hardware); + } + } + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + self.stream = Some(stream); + } + Err(e) => { + eprintln!("[Debugger] Connection error: {}", e); + self.stream = None; + firmware.os.paused = false; + self.waiting_for_start = false; + } + } + } + + // Streaming de eventos + if self.stream.is_some() { + self.stream_events(firmware); + } + } + + fn handle_command(&mut self, cmd: DebugCommand, firmware: &mut Firmware, hardware: &mut Hardware) { + match cmd { + DebugCommand::Ok | DebugCommand::Start => { + if self.waiting_for_start { + println!("[Debugger] Starting execution..."); + self.waiting_for_start = false; + } + firmware.os.paused = false; + } + DebugCommand::Pause => { + firmware.os.paused = true; + } + DebugCommand::Resume => { + firmware.os.paused = false; + } + DebugCommand::Step => { + firmware.os.paused = true; + // Executa uma instrução imediatamente + let _ = firmware.os.debug_step_instruction(&mut firmware.vm, hardware); + } + DebugCommand::StepFrame => { + firmware.os.paused = false; + firmware.os.debug_step_request = true; + } + DebugCommand::GetState => { + let stack_top = firmware.vm.operand_stack.iter() + .rev().take(10).cloned().collect(); + + let resp = DebugResponse::GetState { + pc: firmware.vm.pc, + stack_top, + frame_index: firmware.os.logical_frame_index, + app_id: firmware.os.current_app_id, + }; + self.send_response(resp); + } + DebugCommand::SetBreakpoint { pc } => { + firmware.vm.breakpoints.insert(pc); + } + DebugCommand::ListBreakpoints => { + let pcs = firmware.vm.breakpoints.iter().cloned().collect(); + self.send_response(DebugResponse::Breakpoints { pcs }); + } + DebugCommand::ClearBreakpoint { pc } => { + firmware.vm.breakpoints.remove(&pc); + } + } + } + + fn stream_events(&mut self, firmware: &mut Firmware) { + // Logs + let new_events = firmware.os.log_service.get_after(self.last_log_seq); + for event in new_events { + self.last_log_seq = event.seq; + + // Verifica se é um breakpoint hit via tag + if event.tag == 0xDEB1 { + self.send_event(DebugEvent::BreakpointHit { + pc: firmware.vm.pc, + frame_index: firmware.os.logical_frame_index, + }); + } + + // Certificação via Tags 0xCA01-0xCA03 + if event.tag >= 0xCA01 && event.tag <= 0xCA03 { + let rule = match event.tag { + 0xCA01 => "cycles_budget", + 0xCA02 => "max_syscalls", + 0xCA03 => "max_host_cpu_us", + _ => "unknown" + }.to_string(); + + self.send_event(DebugEvent::Cert { + rule, + used: 0, // Simplificado, informações detalhadas estão na msg do log + limit: 0, + frame_index: firmware.os.logical_frame_index, + }); + } + + self.send_event(DebugEvent::Log { + level: format!("{:?}", event.level), + source: format!("{:?}", event.source), + msg: event.msg.clone(), + }); + } + + // Telemetria (a cada novo frame) + let current_frame = firmware.os.logical_frame_index; + if current_frame > self.last_telemetry_frame { + let tel = &firmware.os.telemetry_last; + self.send_event(DebugEvent::Telemetry { + frame_index: tel.frame_index, + vm_steps: tel.vm_steps, + syscalls: tel.syscalls, + cycles: tel.cycles_used, + }); + self.last_telemetry_frame = current_frame; + } + } +} diff --git a/crates/host-desktop/src/fs_desktop_backend.rs b/crates/host-desktop/src/fs_backend.rs similarity index 100% rename from crates/host-desktop/src/fs_desktop_backend.rs rename to crates/host-desktop/src/fs_backend.rs diff --git a/crates/host-desktop/src/input.rs b/crates/host-desktop/src/input.rs new file mode 100644 index 00000000..f645c8a6 --- /dev/null +++ b/crates/host-desktop/src/input.rs @@ -0,0 +1,78 @@ +use winit::event::{ElementState, MouseButton, WindowEvent}; +use winit::keyboard::{KeyCode, PhysicalKey}; +use winit::window::Window; +use prometeu_core::hardware::InputSignals; +use prometeu_core::Hardware; + +pub struct HostInputHandler { + pub signals: InputSignals, +} + +impl HostInputHandler { + pub fn new() -> Self { + Self { + signals: InputSignals::default(), + } + } + + pub fn handle_event(&mut self, event: &WindowEvent, window: &Window) { + match event { + WindowEvent::KeyboardInput { event, .. } => { + if let PhysicalKey::Code(code) = event.physical_key { + let is_down = event.state == ElementState::Pressed; + + match code { + KeyCode::ArrowUp => self.signals.up_signal = is_down, + KeyCode::ArrowDown => self.signals.down_signal = is_down, + KeyCode::ArrowLeft => self.signals.left_signal = is_down, + KeyCode::ArrowRight => self.signals.right_signal = is_down, + + KeyCode::KeyA => self.signals.a_signal = is_down, + KeyCode::KeyD => self.signals.b_signal = is_down, + KeyCode::KeyW => self.signals.x_signal = is_down, + KeyCode::KeyS => self.signals.y_signal = is_down, + KeyCode::KeyQ => self.signals.l_signal = is_down, + KeyCode::KeyE => self.signals.r_signal = is_down, + + KeyCode::KeyZ => self.signals.start_signal = is_down, + KeyCode::ShiftLeft | KeyCode::ShiftRight => self.signals.select_signal = is_down, + + _ => {} + } + } + } + + WindowEvent::CursorMoved { position, .. } => { + let v = window_to_fb(position.x as f32, position.y as f32, window); + self.signals.x_pos = v.0; + self.signals.y_pos = v.1; + } + + WindowEvent::MouseInput { state, button, .. } => { + if *button == MouseButton::Left { + match state { + ElementState::Pressed => { + self.signals.f_signal = true; + } + ElementState::Released => { + self.signals.f_signal = false; + } + } + } + } + + _ => {} + } + } +} + +pub fn window_to_fb(wx: f32, wy: f32, window: &Window) -> (i32, i32) { + let size = window.inner_size(); + let fb_w = Hardware::W as f32; + let fb_h = Hardware::H as f32; + + let x = (wx * fb_w / size.width as f32).floor() as i32; + let y = (wy * fb_h / size.height as f32).floor() as i32; + + (x.clamp(0, Hardware::W as i32 - 1), y.clamp(0, Hardware::H as i32 - 1)) +} diff --git a/crates/host-desktop/src/main.rs b/crates/host-desktop/src/main.rs index 69e6a718..5701e30b 100644 --- a/crates/host-desktop/src/main.rs +++ b/crates/host-desktop/src/main.rs @@ -1,38 +1,17 @@ -mod audio_mixer; -mod prometeu_runner; -mod fs_desktop_backend; +mod audio; +mod runner; +mod fs_backend; mod log_sink; +mod debugger; +mod stats; +mod input; +mod cap; +mod utilities; -use crate::prometeu_runner::PrometeuRunner; +use crate::runner::HostRunner; +use crate::cap::load_cap_config; use winit::event_loop::EventLoop; use prometeu_core::firmware::BootTarget; -use prometeu_core::telemetry::CertificationConfig; - -fn load_cap_config(path: &str) -> Option { - let content = std::fs::read_to_string(path).ok()?; - let mut config = CertificationConfig { - enabled: true, - ..Default::default() - }; - - for line in content.lines() { - let line = line.trim(); - if line.is_empty() || line.starts_with('#') { continue; } - let parts: Vec<&str> = line.split('=').collect(); - if parts.len() != 2 { continue; } - let key = parts[0].trim(); - let val = parts[1].trim(); - - match key { - "cycles_budget" => config.cycles_budget_per_frame = val.parse().ok(), - "max_syscalls" => config.max_syscalls_per_frame = val.parse().ok(), - "max_host_cpu_us" => config.max_host_cpu_us_per_frame = val.parse().ok(), - _ => {} - } - } - - Some(config) -} fn main() -> Result<(), Box> { let args: Vec = std::env::args().collect(); @@ -96,36 +75,10 @@ fn main() -> Result<(), Box> { let event_loop = EventLoop::new()?; - let mut runner = PrometeuRunner::new(fs_root, cap_config); + let mut runner = HostRunner::new(fs_root, cap_config); runner.set_boot_target(boot_target); event_loop.run_app(&mut runner)?; Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_load_cap_config() { - let content = "cycles_budget=500\nmax_syscalls=10\n# comentário\nmax_host_cpu_us=2000"; - let path = "test_cap.cfg"; - std::fs::write(path, content).unwrap(); - - let config = load_cap_config(path).unwrap(); - assert!(config.enabled); - assert_eq!(config.cycles_budget_per_frame, Some(500)); - assert_eq!(config.max_syscalls_per_frame, Some(10)); - assert_eq!(config.max_host_cpu_us_per_frame, Some(2000)); - - std::fs::remove_file(path).unwrap(); - } - - #[test] - fn test_load_cap_config_not_found() { - let config = load_cap_config("non_existent.cfg"); - assert!(config.is_none()); - } } \ No newline at end of file diff --git a/crates/host-desktop/src/prometeu_runner.rs b/crates/host-desktop/src/prometeu_runner.rs deleted file mode 100644 index 468eb3ea..00000000 --- a/crates/host-desktop/src/prometeu_runner.rs +++ /dev/null @@ -1,870 +0,0 @@ -use crate::audio_mixer::AudioMixer; -use crate::fs_desktop_backend::HostDirBackend; -use crate::log_sink::HostConsoleSink; -use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; -use pixels::{Pixels, SurfaceTexture}; -use prometeu_core::firmware::{BootTarget, Firmware}; -use prometeu_core::hardware::{AudioCommand, InputSignals, OUTPUT_SAMPLE_RATE}; -use prometeu_core::Hardware; -use ringbuf::traits::{Consumer, Producer, Split}; -use ringbuf::HeapRb; -use std::net::{TcpListener, TcpStream}; -use std::sync::Arc; -use std::time::{Duration, Instant}; -use winit::application::ApplicationHandler; -use winit::dpi::LogicalSize; -use winit::event::{ElementState, MouseButton, WindowEvent}; -use winit::event_loop::{ActiveEventLoop, ControlFlow}; -use winit::keyboard::{KeyCode, PhysicalKey}; -use winit::window::{Window, WindowAttributes, WindowId}; - -use prometeu_core::model::CartridgeLoader; -use prometeu_core::telemetry::CertificationConfig; -use prometeu_core::debugger_protocol::*; -use std::io::{Read, Write}; - -pub struct PrometeuRunner { - window: Option<&'static Window>, - pixels: Option>, - - hardware: Hardware, - firmware: Firmware, - - input_signals: InputSignals, - fs_root: Option, - - log_sink: HostConsoleSink, - - frame_target_dt: Duration, - last_frame_time: Instant, - accumulator: Duration, - - last_stats_update: Instant, - frames_since_last_update: u64, - current_fps: f64, - - overlay_enabled: bool, - - debug_waiting_for_start: bool, - debug_listener: Option, - debug_stream: Option, - debug_last_log_seq: u64, - debug_last_telemetry_frame: u64, - - audio_load_accum_us: u64, - audio_load_samples: u64, - audio_perf_consumer: Option>>>, - - audio_producer: Option>>>, - _audio_stream: Option, -} - -impl PrometeuRunner { - pub(crate) fn set_boot_target(&mut self, boot_target: BootTarget) { - self.firmware.boot_target = boot_target.clone(); - if let BootTarget::Cartridge { path, debug: true, debug_port } = boot_target { - self.debug_waiting_for_start = true; - - // Pré-carrega informações do cartucho para o handshake - if let Ok(cartridge) = CartridgeLoader::load(&path) { - self.firmware.os.initialize_vm(&mut self.firmware.vm, &cartridge); - } - - match TcpListener::bind(format!("127.0.0.1:{}", debug_port)) { - Ok(listener) => { - listener.set_nonblocking(true).expect("Cannot set non-blocking"); - self.debug_listener = Some(listener); - println!("[Debugger] Listening for start command on port {}...", debug_port); - } - Err(e) => { - eprintln!("[Debugger] Failed to bind to port {}: {}", debug_port, e); - } - } - - println!("[Debugger] (Or press D to start execution)"); - } - } - - - fn send_debug_response(&mut self, resp: DebugResponse) { - if let Some(stream) = &mut self.debug_stream { - if let Ok(json) = serde_json::to_string(&resp) { - let _ = stream.write_all(json.as_bytes()); - let _ = stream.write_all(b"\n"); - } - } - } - - fn send_debug_event(&mut self, event: DebugEvent) { - if let Some(stream) = &mut self.debug_stream { - if let Ok(json) = serde_json::to_string(&event) { - let _ = stream.write_all(json.as_bytes()); - let _ = stream.write_all(b"\n"); - } - } - } - - fn check_debug_commands(&mut self) { - if let Some(listener) = &self.debug_listener { - if let Ok((stream, _addr)) = listener.accept() { - if self.debug_stream.is_none() { - println!("[Debugger] Connection received!"); - stream.set_nonblocking(true).expect("Cannot set non-blocking on stream"); - - self.debug_stream = Some(stream); - - // Enviar Handshake - let handshake = DebugResponse::Handshake { - protocol_version: DEVTOOLS_PROTOCOL_VERSION, - runtime_version: "0.1".to_string(), - cartridge: HandshakeCartridge { - app_id: self.firmware.os.current_app_id, - title: self.firmware.os.current_cartridge_title.clone(), - app_version: self.firmware.os.current_cartridge_app_version.clone(), - app_mode: self.firmware.os.current_cartridge_app_mode, - }, - }; - self.send_debug_response(handshake); - } else { - println!("[Debugger] Connection refused: already connected."); - } - } - } - - if let Some(mut stream) = self.debug_stream.take() { - let mut buf = [0u8; 4096]; - match stream.read(&mut buf) { - Ok(0) => { - println!("[Debugger] Connection closed by remote."); - self.debug_stream = None; - self.firmware.os.paused = false; - self.debug_waiting_for_start = false; - } - Ok(n) => { - let data = &buf[..n]; - // Processar múltiplos comandos se houver \n - let msg = String::from_utf8_lossy(data); - - self.debug_stream = Some(stream); // Coloca de volta antes de processar comandos - - for line in msg.lines() { - let trimmed = line.trim(); - if trimmed.is_empty() { - continue; - } - if let Ok(cmd) = serde_json::from_str::(trimmed) { - self.handle_debug_command(cmd); - } - } - } - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { - self.debug_stream = Some(stream); - } - Err(e) => { - eprintln!("[Debugger] Connection error: {}", e); - self.debug_stream = None; - self.firmware.os.paused = false; - self.debug_waiting_for_start = false; - } - } - } - - // Streaming de eventos - if self.debug_stream.is_some() { - self.stream_debug_events(); - } - } - - fn handle_debug_command(&mut self, cmd: DebugCommand) { - match cmd { - DebugCommand::Ok | DebugCommand::Start => { - if self.debug_waiting_for_start { - println!("[Debugger] Starting execution..."); - self.debug_waiting_for_start = false; - } - self.firmware.os.paused = false; - } - DebugCommand::Pause => { - self.firmware.os.paused = true; - } - DebugCommand::Resume => { - self.firmware.os.paused = false; - } - DebugCommand::Step => { - self.firmware.os.paused = true; - // Executa uma instrução imediatamente - let _ = self.firmware.os.debug_step_instruction(&mut self.firmware.vm, &mut self.hardware); - } - DebugCommand::StepFrame => { - self.firmware.os.paused = false; - self.firmware.os.debug_step_request = true; - } - DebugCommand::GetState => { - let stack_top = self.firmware.vm.operand_stack.iter() - .rev().take(10).cloned().collect(); - - let resp = DebugResponse::GetState { - pc: self.firmware.vm.pc, - stack_top, - frame_index: self.firmware.os.logical_frame_index, - app_id: self.firmware.os.current_app_id, - }; - self.send_debug_response(resp); - } - DebugCommand::SetBreakpoint { pc } => { - self.firmware.vm.breakpoints.insert(pc); - } - DebugCommand::ListBreakpoints => { - let pcs = self.firmware.vm.breakpoints.iter().cloned().collect(); - self.send_debug_response(DebugResponse::Breakpoints { pcs }); - } - DebugCommand::ClearBreakpoint { pc } => { - self.firmware.vm.breakpoints.remove(&pc); - } - } - } - - fn stream_debug_events(&mut self) { - // Logs - let new_events = self.firmware.os.log_service.get_after(self.debug_last_log_seq); - for event in new_events { - self.debug_last_log_seq = event.seq; - - // Verifica se é um breakpoint hit via tag - if event.tag == 0xDEB1 { - self.send_debug_event(DebugEvent::BreakpointHit { - pc: self.firmware.vm.pc, - frame_index: self.firmware.os.logical_frame_index, - }); - } - - // Certificação via Tags 0xCA01-0xCA03 - if event.tag >= 0xCA01 && event.tag <= 0xCA03 { - let rule = match event.tag { - 0xCA01 => "cycles_budget", - 0xCA02 => "max_syscalls", - 0xCA03 => "max_host_cpu_us", - _ => "unknown" - }.to_string(); - - self.send_debug_event(DebugEvent::Cert { - rule, - used: 0, // Simplificado, informações detalhadas estão na msg do log - limit: 0, - frame_index: self.firmware.os.logical_frame_index, - }); - } - - self.send_debug_event(DebugEvent::Log { - level: format!("{:?}", event.level), - source: format!("{:?}", event.source), - msg: event.msg.clone(), - }); - } - - // Telemetria (a cada novo frame) - let current_frame = self.firmware.os.logical_frame_index; - if current_frame > self.debug_last_telemetry_frame { - let tel = &self.firmware.os.telemetry_last; - self.send_debug_event(DebugEvent::Telemetry { - frame_index: tel.frame_index, - vm_steps: tel.vm_steps, - syscalls: tel.syscalls, - cycles: tel.cycles_used, - }); - self.debug_last_telemetry_frame = current_frame; - } - } - - 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_signals: InputSignals::default(), - 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, - - last_stats_update: Instant::now(), - frames_since_last_update: 0, - current_fps: 0.0, - overlay_enabled: false, - debug_waiting_for_start: false, - debug_listener: None, - debug_stream: None, - debug_last_log_seq: 0, - debug_last_telemetry_frame: 0, - audio_load_accum_us: 0, - audio_load_samples: 0, - audio_perf_consumer: None, - - audio_producer: None, - _audio_stream: None, - } - } - - fn init_audio(&mut self) { - let host = cpal::default_host(); - let device = host - .default_output_device() - .expect("no output device available"); - - let config = cpal::StreamConfig { - channels: 2, - sample_rate: cpal::SampleRate(OUTPUT_SAMPLE_RATE), - buffer_size: cpal::BufferSize::Default, - }; - - let rb = HeapRb::::new(1024); - let (prod, mut cons) = rb.split(); - - self.audio_producer = Some(prod); - - let mut mixer = AudioMixer::new(); - - // Para passar dados de performance da thread de áudio para a principal - let audio_perf_rb = HeapRb::::new(64); - let (mut perf_prod, perf_cons) = audio_perf_rb.split(); - - let stream = device - .build_output_stream( - &config, - move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { - // Consome comandos da ringbuffer - while let Some(cmd) = cons.try_pop() { - mixer.process_command(cmd); - } - // Mixa áudio - mixer.fill_buffer(data); - // Envia tempo de processamento em microssegundos - let _ = perf_prod.try_push(mixer.last_processing_time.as_micros() as u64); - }, - |err| eprintln!("audio stream error: {}", err), - None, - ) - .expect("failed to build audio stream"); - - stream.play().expect("failed to play audio stream"); - self._audio_stream = Some(stream); - self.audio_perf_consumer = Some(perf_cons); - } - - fn window(&self) -> &'static Window { - self.window.expect("window not created yet") - } - - // fn pixels_mut(&mut self) -> &mut Pixels<'static> { - // self.pixels.as_mut().expect("pixels 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 PrometeuRunner { - 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 {}", - 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 vira &'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 = Pixels::new(Hardware::W as u32, Hardware::H as u32, surface_texture) - .expect("failed to create Pixels"); - - pixels.frame_mut().fill(0); - - self.pixels = Some(pixels); - - self.init_audio(); - - event_loop.set_control_flow(ControlFlow::Poll); - } - - - fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) { - 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 => { - // Pegue o Pixels diretamente do campo (não via helper que pega &mut self inteiro) - let pixels = self.pixels.as_mut().expect("pixels not initialized"); - - { - // Borrow mutável do frame (dura só dentro deste bloco) - let frame = pixels.frame_mut(); - - // Borrow imutável do prometeu-core (campo diferente, ok) - let src = self.hardware.gfx.front_buffer(); - - draw_rgb565_to_rgba8(src, frame); - } // <- frame borrow termina aqui - - 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.debug_waiting_for_start { - self.debug_waiting_for_start = false; - println!("[Debugger] Execution started!"); - } - - match code { - KeyCode::ArrowUp => self.input_signals.up_signal = is_down, - KeyCode::ArrowDown => self.input_signals.down_signal = is_down, - KeyCode::ArrowLeft => self.input_signals.left_signal = is_down, - KeyCode::ArrowRight => self.input_signals.right_signal = is_down, - - // A/B (troque depois como quiser) - KeyCode::KeyA => self.input_signals.a_signal = is_down, - KeyCode::KeyD => self.input_signals.b_signal = is_down, - KeyCode::KeyW => self.input_signals.x_signal = is_down, - KeyCode::KeyS => self.input_signals.y_signal = is_down, - KeyCode::KeyQ => self.input_signals.l_signal = is_down, - KeyCode::KeyE => self.input_signals.r_signal = is_down, - - KeyCode::KeyZ => self.input_signals.start_signal = is_down, - KeyCode::ShiftLeft | KeyCode::ShiftRight => self.input_signals.select_signal = is_down, - - KeyCode::F1 => { - if is_down { - self.overlay_enabled = !self.overlay_enabled; - } - } - - _ => {} - } - } - } - - WindowEvent::CursorMoved { position, .. } => { - let v = window_to_fb(position.x as f32, position.y as f32, self.window()); - self.input_signals.x_pos = v.0; - self.input_signals.y_pos = v.1; - } - - WindowEvent::MouseInput { state, button, .. } => { - if button == MouseButton::Left { - match state { - ElementState::Pressed => { - self.input_signals.f_signal = true; - } - ElementState::Released => { - self.input_signals.f_signal = false; - } - } - } - } - - _ => {} - } - } - - fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { - self.check_debug_commands(); - - // Atualiza estado do Filesystem no OS (específico do host-desktop) - if let Some(root) = &self.fs_root { - use prometeu_core::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)); - } - } - } - - let now = Instant::now(); - let mut frame_delta = now.duration_since(self.last_frame_time); - - // Limitador para evitar a "espiral da morte" se o SO travar (máximo de 100ms por volta) - if frame_delta > Duration::from_millis(100) { - frame_delta = Duration::from_millis(100); - } - - self.last_frame_time = now; - self.accumulator += frame_delta; - - // 🔥 O coração do determinismo: consome o tempo em fatias exatas de 60Hz - while self.accumulator >= self.frame_target_dt { - if !self.debug_waiting_for_start { - self.firmware.step_frame(&self.input_signals, &mut self.hardware); - } - - // Envia comandos de áudio gerados neste frame para a thread de áudio - if let Some(producer) = &mut self.audio_producer { - for cmd in self.hardware.audio.commands.drain(..) { - let _ = producer.try_push(cmd); - } - } - - self.accumulator -= self.frame_target_dt; - self.frames_since_last_update += 1; - } - - // Drena tempos de performance do áudio - if let Some(cons) = &mut self.audio_perf_consumer { - while let Some(us) = cons.try_pop() { - self.audio_load_accum_us += us; - self.audio_load_samples += 1; - } - } - - // Atualiza estatísticas a cada 1 segundo real - let stats_elapsed = now.duration_since(self.last_stats_update); - if stats_elapsed >= Duration::from_secs(1) { - self.current_fps = self.frames_since_last_update as f64 / stats_elapsed.as_secs_f64(); - if let Some(window) = self.window { - let kb = self.hardware.gfx.memory_usage_bytes() as f64 / 1024.0; - - // comparação fixa sempre contra 60Hz, manter mesmo quando fazer teste de stress na CPU - let frame_budget_us = 16666.0; - let cpu_load_core = (self.firmware.os.last_frame_cpu_time_us as f64 / frame_budget_us) * 100.0; - - let cpu_load_audio = if self.audio_load_samples > 0 { - // O load real é (tempo total processando) / (tempo total de parede). - (self.audio_load_accum_us as f64 / stats_elapsed.as_micros() as f64) * 100.0 - } else { - 0.0 - }; - - let title = format!( - "PROMETEU | GFX: {:.1} KB | FPS: {:.1} | Load: {:.1}% (C) + {:.1}% (A) | Frame: tick {} logical {}", - kb, self.current_fps, cpu_load_core, cpu_load_audio, self.firmware.os.tick_index, self.firmware.os.logical_frame_index - ); - window.set_title(&title); - } - - self.last_stats_update = now; - self.frames_since_last_update = 0; - self.audio_load_accum_us = 0; - self.audio_load_samples = 0; - } - - // Processa logs do sistema - 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); - - // Overlay de Telemetria - if self.overlay_enabled { - self.hardware.gfx.present(); // Traz o front para o back para desenhar por cima - - let tel = &self.firmware.os.telemetry_last; - let color_text = prometeu_core::model::Color::WHITE; - let color_bg = prometeu_core::model::Color::INDIGO; // Azul escuro para destacar - let color_warn = prometeu_core::model::Color::RED; - - self.hardware.gfx.fill_rect(5, 5, 140, 65, color_bg); - self.hardware.gfx.draw_text(10, 10, &format!("FPS: {:.1}", self.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); - self.hardware.gfx.draw_text(10, 42, &format!("CYC: {}", tel.cycles_used), color_text); - - let cert_color = if tel.violations > 0 { color_warn } else { color_text }; - self.hardware.gfx.draw_text(10, 50, &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, 58, &msg, color_warn); - } - } - - self.hardware.gfx.present(); // Devolve para o front com o overlay aplicado - } - - self.request_redraw(); - } -} - -/// Mapeamento simples: window coords -> framebuffer coords (stretch). -/// Depois podemos fazer letterbox/aspect-ratio correto. -fn window_to_fb(wx: f32, wy: f32, window: &Window) -> (i32, i32) { - let size = window.inner_size(); - let fb_w = Hardware::W as f32; - let fb_h = Hardware::H as f32; - - let x = (wx * fb_w / size.width as f32).floor() as i32; - let y = (wy * fb_h / size.height as f32).floor() as i32; - - (x.clamp(0, Hardware::W as i32 - 1), y.clamp(0, Hardware::H as i32 - 1)) -} - -/// Copia RGB565 (u16) -> RGBA8888 (u8[4]) para o frame do pixels. -/// Formato do pixels: RGBA8. -fn draw_rgb565_to_rgba8(src: &[u16], dst_rgba: &mut [u8]) { - for (i, &px) in src.iter().enumerate() { - let (r8, g8, b8) = rgb565_to_rgb888(px); - let o = i * 4; - dst_rgba[o] = r8; - dst_rgba[o + 1] = g8; - dst_rgba[o + 2] = b8; - dst_rgba[o + 3] = 0xFF; - } -} - -/// Expande RGB565 para RGB888 (replicação de bits altos). -#[inline(always)] -fn rgb565_to_rgb888(px: u16) -> (u8, u8, u8) { - let r5 = ((px >> 11) & 0x1F) as u8; - let g6 = ((px >> 5) & 0x3F) as u8; - let b5 = (px & 0x1F) as u8; - - let r8 = (r5 << 3) | (r5 >> 2); - let g8 = (g6 << 2) | (g6 >> 4); - let b8 = (b5 << 3) | (b5 >> 2); - - (r8, g8, b8) -} -#[cfg(test)] -mod tests { - use super::*; - use prometeu_core::firmware::BootTarget; - - #[test] - fn test_debug_port_opens() { - let mut runner = PrometeuRunner::new(None, None); - let port = 9999; - runner.set_boot_target(BootTarget::Cartridge { - path: "dummy.bin".to_string(), - debug: true, - debug_port: port, - }); - - assert!(runner.debug_waiting_for_start); - assert!(runner.debug_listener.is_some()); - - // Verifica se conseguimos conectar - use std::net::TcpStream; - use std::io::{Write, Read}; - { - let mut stream = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Deve conectar"); - // Pequeno sleep para garantir que o SO processe - std::thread::sleep(std::time::Duration::from_millis(100)); - - // Simula o loop para aceitar a conexão - runner.check_debug_commands(); - assert!(runner.debug_stream.is_some(), "Stream deve ter sido mantido aberto"); - - // Verifica Handshake - let mut buf = [0u8; 2048]; - let n = stream.read(&mut buf).expect("Deve ler handshake"); - let resp: serde_json::Value = serde_json::from_slice(&buf[..n]).expect("Handshake deve ser JSON válido"); - assert_eq!(resp["type"], "handshake"); - assert_eq!(resp["protocol_version"], DEVTOOLS_PROTOCOL_VERSION); - - // Envia start via JSON - stream.write_all(b"{\"type\":\"start\"}\n").expect("Conexão deve estar aberta para escrita"); - std::thread::sleep(std::time::Duration::from_millis(50)); - - // Processa o comando recebido - runner.check_debug_commands(); - assert!(!runner.debug_waiting_for_start, "Execução deve ter iniciado após comando start"); - assert!(runner.debug_listener.is_some(), "Listener deve permanecer aberto para reconexões"); - } - - // Agora que o stream saiu do escopo do teste, o runner deve detectar o fechamento no próximo check - std::thread::sleep(std::time::Duration::from_millis(50)); - runner.check_debug_commands(); - assert!(runner.debug_stream.is_none(), "Stream deve ter sido fechado após o cliente desconectar"); - } - - #[test] - fn test_debug_reconnection() { - let mut runner = PrometeuRunner::new(None, None); - let port = 9998; - runner.set_boot_target(BootTarget::Cartridge { - path: "dummy.bin".to_string(), - debug: true, - debug_port: port, - }); - - // 1. Conecta e inicia - { - let mut stream = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Deve conectar 1"); - std::thread::sleep(std::time::Duration::from_millis(50)); - runner.check_debug_commands(); - assert!(runner.debug_stream.is_some()); - - stream.write_all(b"{\"type\":\"start\"}\n").expect("Deve escrever start"); - std::thread::sleep(std::time::Duration::from_millis(50)); - runner.check_debug_commands(); - assert!(!runner.debug_waiting_for_start); - // Atualmente o listener é fechado aqui. - } - - // 2. Desconecta (limpa o stream no runner) - std::thread::sleep(std::time::Duration::from_millis(50)); - runner.check_debug_commands(); - assert!(runner.debug_stream.is_none()); - - // 3. Tenta reconectar - DEVE FALHAR atualmente, mas queremos que FUNCIONE - let stream2 = TcpStream::connect(format!("127.0.0.1:{}", port)); - assert!(stream2.is_ok(), "Deve aceitar nova conexão mesmo após o start"); - - std::thread::sleep(std::time::Duration::from_millis(50)); - runner.check_debug_commands(); - assert!(runner.debug_stream.is_some(), "Stream deve ter sido aceito na reconexão"); - } - - #[test] - fn test_debug_refuse_second_connection() { - let mut runner = PrometeuRunner::new(None, None); - let port = 9997; - runner.set_boot_target(BootTarget::Cartridge { - path: "dummy.bin".to_string(), - debug: true, - debug_port: port, - }); - - // 1. Primeira conexão - let mut _stream1 = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Deve conectar 1"); - std::thread::sleep(std::time::Duration::from_millis(50)); - runner.check_debug_commands(); - assert!(runner.debug_stream.is_some()); - - // 2. Segunda conexão - let mut stream2 = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Deve conectar 2 (SO aceita)"); - std::thread::sleep(std::time::Duration::from_millis(50)); - runner.check_debug_commands(); // Deve aceitar e fechar stream2 - - // Verifica se stream2 foi fechado pelo servidor - 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(), "Segunda conexão deve ser fechada pelo servidor"); - - assert!(runner.debug_stream.is_some(), "Primeira conexão deve continuar ativa"); - } - - #[test] - fn test_get_state_returns_response() { - let mut runner = PrometeuRunner::new(None, None); - let port = 9996; - runner.set_boot_target(BootTarget::Cartridge { - path: "dummy.bin".to_string(), - debug: true, - debug_port: port, - }); - - let mut stream = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Deve conectar"); - std::thread::sleep(std::time::Duration::from_millis(100)); - runner.check_debug_commands(); - - // Limpa o buffer do handshake - let mut buf = [0u8; 2048]; - let n = stream.read(&mut buf).expect("Deve ler handshake"); - assert!(n > 0); - - // Envia getState - stream.write_all(b"{\"type\":\"getState\"}\n").expect("Deve escrever getState"); - std::thread::sleep(std::time::Duration::from_millis(100)); - - runner.check_debug_commands(); - - // Verifica se recebeu resposta - stream.set_read_timeout(Some(std::time::Duration::from_millis(500))).unwrap(); - let mut full_resp = Vec::new(); - let mut temp_buf = [0u8; 1024]; - loop { - match stream.read(&mut temp_buf) { - Ok(0) => break, - Ok(n) => { - full_resp.extend_from_slice(&temp_buf[..n]); - if full_resp.contains(&b'\n') { - break; - } - } - Err(_) => break, - } - } - assert!(!full_resp.is_empty(), "Resposta deve ter conteúdo"); - - let resp: serde_json::Value = serde_json::from_slice(&full_resp).expect("Resposta deve ser JSON válido"); - assert_eq!(resp["type"], "getState"); - } - - #[test] - fn test_debug_resume_on_disconnect() { - let mut runner = PrometeuRunner::new(None, None); - let port = 9995; - runner.set_boot_target(BootTarget::Cartridge { - path: "dummy.bin".to_string(), - debug: true, - debug_port: port, - }); - - // 1. Conecta e pausa - { - let mut stream = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Deve conectar"); - std::thread::sleep(std::time::Duration::from_millis(50)); - runner.check_debug_commands(); - - stream.write_all(b"{\"type\":\"pause\"}\n").expect("Deve escrever pause"); - std::thread::sleep(std::time::Duration::from_millis(50)); - runner.check_debug_commands(); - assert!(runner.firmware.os.paused, "VM deve estar pausada"); - } - - // 2. Desconecta (stream sai de escopo) - std::thread::sleep(std::time::Duration::from_millis(50)); - runner.check_debug_commands(); - - // 3. Verifica se despausou - assert!(!runner.firmware.os.paused, "VM deve ter despausado após desconexão"); - assert!(!runner.debug_waiting_for_start, "VM deve ter saído do estado waiting_for_start"); - } -} diff --git a/crates/host-desktop/src/runner.rs b/crates/host-desktop/src/runner.rs new file mode 100644 index 00000000..62537a27 --- /dev/null +++ b/crates/host-desktop/src/runner.rs @@ -0,0 +1,462 @@ +use crate::audio::HostAudio; +use crate::fs_backend::HostDirBackend; +use crate::log_sink::HostConsoleSink; +use crate::debugger::HostDebugger; +use crate::stats::HostStats; +use crate::input::HostInputHandler; +use crate::utilities::draw_rgb565_to_rgba8; +use pixels::{Pixels, SurfaceTexture}; +use prometeu_core::firmware::{BootTarget, Firmware}; +use prometeu_core::Hardware; +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}; + +use prometeu_core::telemetry::CertificationConfig; + +pub struct HostRunner { + window: Option<&'static Window>, + pixels: Option>, + + hardware: Hardware, + firmware: Firmware, + + input: HostInputHandler, + fs_root: Option, + + log_sink: HostConsoleSink, + + frame_target_dt: Duration, + last_frame_time: Instant, + accumulator: Duration, + + stats: HostStats, + debugger: HostDebugger, + + overlay_enabled: bool, + + audio: HostAudio, +} + +impl HostRunner { + 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); + } + + 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(), + } + } + + 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 = prometeu_core::model::Color::WHITE; + let color_bg = prometeu_core::model::Color::INDIGO; // Azul escuro para destacar + let color_warn = prometeu_core::model::Color::RED; + + self.hardware.gfx.fill_rect(5, 5, 140, 65, 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); + self.hardware.gfx.draw_text(10, 42, &format!("CYC: {}", tel.cycles_used), color_text); + + let cert_color = if tel.violations > 0 { color_warn } else { color_text }; + self.hardware.gfx.draw_text(10, 50, &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, 58, &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 {}", + 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 vira &'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 = Pixels::new(Hardware::W as u32, Hardware::H as u32, surface_texture) + .expect("failed to create Pixels"); + + pixels.frame_mut().fill(0); + + self.pixels = Some(pixels); + + self.audio.init(); + + 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 => { + // Pegue o Pixels diretamente do campo (não via helper que pega &mut self inteiro) + let pixels = self.pixels.as_mut().expect("pixels not initialized"); + + { + // Borrow mutável do frame (dura só dentro deste bloco) + let frame = pixels.frame_mut(); + + // Borrow imutável do prometeu-core (campo diferente, ok) + let src = self.hardware.gfx.front_buffer(); + + draw_rgb565_to_rgba8(src, frame); + } // <- frame borrow termina aqui + + 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; + } + } + } + + _ => {} + } + } + + fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { + self.debugger.check_commands(&mut self.firmware, &mut self.hardware); + + // Atualiza estado do Filesystem no OS (específico do host-desktop) + if let Some(root) = &self.fs_root { + use prometeu_core::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)); + } + } + } + + let now = Instant::now(); + let mut frame_delta = now.duration_since(self.last_frame_time); + + // Limitador para evitar a "espiral da morte" se o SO travar (máximo de 100ms por volta) + if frame_delta > Duration::from_millis(100) { + frame_delta = Duration::from_millis(100); + } + + self.last_frame_time = now; + self.accumulator += frame_delta; + + // 🔥 O coração do determinismo: consome o tempo em fatias exatas de 60Hz + while self.accumulator >= self.frame_target_dt { + if !self.debugger.waiting_for_start { + self.firmware.step_frame(&self.input.signals, &mut self.hardware); + } + + self.audio.send_commands(&mut self.hardware.audio.commands); + + self.accumulator -= self.frame_target_dt; + self.stats.record_frame(); + } + + self.audio.update_stats(&mut self.stats); + + // Atualiza estatísticas a cada 1 segundo real + self.stats.update(now, self.window, &self.hardware, &self.firmware); + + // Processa logs do sistema + 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); + + // Overlay de Telemetria + if self.overlay_enabled { + self.hardware.gfx.present(); // Traz o front para o back para desenhar por cima + self.display_dbg_overlay(); + self.hardware.gfx.present(); // Devolve para o front com o overlay aplicado + } + + self.request_redraw(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use prometeu_core::firmware::BootTarget; + use prometeu_core::debugger_protocol::DEVTOOLS_PROTOCOL_VERSION; + use std::net::TcpStream; + use std::io::{Read, Write}; + + #[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()); + + // Verifica se conseguimos conectar + { + let mut stream = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Deve conectar"); + // Pequeno sleep para garantir que o SO processe + std::thread::sleep(std::time::Duration::from_millis(100)); + + // Simula o loop para aceitar a conexão + runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware); + assert!(runner.debugger.stream.is_some(), "Stream deve ter sido mantido aberto"); + + // Verifica Handshake + let mut buf = [0u8; 2048]; + let n = stream.read(&mut buf).expect("Deve ler handshake"); + let resp: serde_json::Value = serde_json::from_slice(&buf[..n]).expect("Handshake deve ser JSON válido"); + assert_eq!(resp["type"], "handshake"); + assert_eq!(resp["protocol_version"], DEVTOOLS_PROTOCOL_VERSION); + + // Envia start via JSON + stream.write_all(b"{\"type\":\"start\"}\n").expect("Conexão deve estar aberta para escrita"); + std::thread::sleep(std::time::Duration::from_millis(50)); + + // Processa o comando recebido + runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware); + assert!(!runner.debugger.waiting_for_start, "Execução deve ter iniciado após comando start"); + assert!(runner.debugger.listener.is_some(), "Listener deve permanecer aberto para reconexões"); + } + + // Agora que o stream saiu do escopo do teste, o runner deve detectar o fechamento no próximo 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 deve ter sido fechado após o cliente desconectar"); + } + + #[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. Conecta e inicia + { + let mut stream = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Deve conectar 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("Deve escrever 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); + // Atualmente o listener é fechado aqui. + } + + // 2. Desconecta (limpa o stream no 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. Tenta reconectar - DEVE FALHAR atualmente, mas queremos que FUNCIONE + let stream2 = TcpStream::connect(format!("127.0.0.1:{}", port)); + assert!(stream2.is_ok(), "Deve aceitar nova conexão mesmo após o 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 deve ter sido aceito na reconexão"); + } + + #[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. Primeira conexão + let mut _stream1 = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Deve conectar 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. Segunda conexão + let mut stream2 = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Deve conectar 2 (SO aceita)"); + std::thread::sleep(std::time::Duration::from_millis(50)); + runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware); // Deve aceitar e fechar stream2 + + // Verifica se stream2 foi fechado pelo servidor + 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(), "Segunda conexão deve ser fechada pelo servidor"); + + assert!(runner.debugger.stream.is_some(), "Primeira conexão deve continuar ativa"); + } + + #[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("Deve conectar"); + 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("Deve ler handshake"); + assert!(line.contains("handshake")); + + // Envia getState + reader.get_mut().write_all(b"{\"type\":\"getState\"}\n").expect("Deve escrever getState"); + std::thread::sleep(std::time::Duration::from_millis(100)); + + runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware); + + // Verifica se recebeu resposta (pode haver eventos/logs antes) + loop { + line.clear(); + reader.read_line(&mut line).expect("Deve ler linha"); + if line.is_empty() { break; } + + if let Ok(resp) = serde_json::from_str::(&line) { + if resp["type"] == "getState" { + return; + } + } + } + panic!("Não recebeu resposta getState"); + } + + #[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. Conecta e pausa + { + let mut stream = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Deve conectar"); + 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("Deve escrever 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 deve estar pausada"); + } + + // 2. Desconecta (stream sai de escopo) + std::thread::sleep(std::time::Duration::from_millis(50)); + runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware); + + // 3. Verifica se despausou + assert!(!runner.firmware.os.paused, "VM deve ter despausado após desconexão"); + assert!(!runner.debugger.waiting_for_start, "VM deve ter saído do estado waiting_for_start"); + } +} diff --git a/crates/host-desktop/src/stats.rs b/crates/host-desktop/src/stats.rs new file mode 100644 index 00000000..2acbcb7c --- /dev/null +++ b/crates/host-desktop/src/stats.rs @@ -0,0 +1,65 @@ +use std::time::{Duration, Instant}; +use winit::window::Window; +use prometeu_core::Hardware; +use prometeu_core::firmware::Firmware; + +pub struct HostStats { + pub last_stats_update: Instant, + pub frames_since_last_update: u64, + pub current_fps: f64, + pub audio_load_accum_us: u64, + pub audio_load_samples: u64, +} + +impl HostStats { + pub fn new() -> Self { + Self { + last_stats_update: Instant::now(), + frames_since_last_update: 0, + current_fps: 0.0, + audio_load_accum_us: 0, + audio_load_samples: 0, + } + } + + pub fn record_frame(&mut self) { + self.frames_since_last_update += 1; + } + + pub fn record_audio_perf(&mut self, us: u64) { + self.audio_load_accum_us += us; + self.audio_load_samples += 1; + } + + pub fn update(&mut self, now: Instant, window: Option<&Window>, hardware: &Hardware, firmware: &Firmware) { + let stats_elapsed = now.duration_since(self.last_stats_update); + if stats_elapsed >= Duration::from_secs(1) { + self.current_fps = self.frames_since_last_update as f64 / stats_elapsed.as_secs_f64(); + + if let Some(window) = window { + let kb = hardware.gfx.memory_usage_bytes() as f64 / 1024.0; + + // comparação fixa sempre contra 60Hz, manter mesmo quando fazer teste de stress na CPU + let frame_budget_us = 16666.0; + let cpu_load_core = (firmware.os.last_frame_cpu_time_us as f64 / frame_budget_us) * 100.0; + + let cpu_load_audio = if self.audio_load_samples > 0 { + (self.audio_load_accum_us as f64 / stats_elapsed.as_micros() as f64) * 100.0 + } else { + 0.0 + }; + + let title = format!( + "PROMETEU | GFX: {:.1} KB | FPS: {:.1} | Load: {:.1}% (C) + {:.1}% (A) | Frame: tick {} logical {}", + kb, self.current_fps, cpu_load_core, cpu_load_audio, firmware.os.tick_index, firmware.os.logical_frame_index + ); + window.set_title(&title); + } + + self.last_stats_update = now; + self.frames_since_last_update = 0; + self.audio_load_accum_us = 0; + self.audio_load_samples = 0; + } + } +} diff --git a/crates/host-desktop/src/utilities.rs b/crates/host-desktop/src/utilities.rs new file mode 100644 index 00000000..4a1724a4 --- /dev/null +++ b/crates/host-desktop/src/utilities.rs @@ -0,0 +1,26 @@ +/// Copia RGB565 (u16) -> RGBA8888 (u8[4]) para o frame do pixels. +/// Formato do pixels: RGBA8. +pub fn draw_rgb565_to_rgba8(src: &[u16], dst_rgba: &mut [u8]) { + for (i, &px) in src.iter().enumerate() { + let (r8, g8, b8) = rgb565_to_rgb888(px); + let o = i * 4; + dst_rgba[o] = r8; + dst_rgba[o + 1] = g8; + dst_rgba[o + 2] = b8; + dst_rgba[o + 3] = 0xFF; + } +} + +/// Expande RGB565 para RGB888 (replicação de bits altos). +#[inline(always)] +pub fn rgb565_to_rgb888(px: u16) -> (u8, u8, u8) { + let r5 = ((px >> 11) & 0x1F) as u8; + let g6 = ((px >> 5) & 0x3F) as u8; + let b5 = (px & 0x1F) as u8; + + let r8 = (r5 << 3) | (r5 >> 2); + let g8 = (g6 << 2) | (g6 >> 4); + let b8 = (b5 << 3) | (b5 >> 2); + + (r8, g8, b8) +}