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 self.debug_waiting_for_start { if let Some(listener) = &self.debug_listener { if let Ok((stream, _addr)) = listener.accept() { 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); } } } 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; } Ok(n) => { let data = &buf[..n]; // Processar múltiplos comandos se houver \n let msg = String::from_utf8_lossy(data); for line in msg.lines() { if let Ok(cmd) = serde_json::from_str::(line) { self.handle_debug_command(cmd); } } self.debug_stream = Some(stream); } 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; } } } // 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.debug_listener = None; } 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).filter_map(|v| v.as_integer()).collect(); let resp = DebugResponse::State { 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; self.debug_listener = None; // Fecha o socket 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_none(), "Listener deve ter sido fechado"); } // 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"); } }