463 lines
18 KiB
Rust
463 lines
18 KiB
Rust
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<Pixels<'static>>,
|
|
|
|
hardware: Hardware,
|
|
firmware: Firmware,
|
|
|
|
input: HostInputHandler,
|
|
fs_root: Option<String>,
|
|
|
|
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<String>, cap_config: Option<CertificationConfig>) -> 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::<serde_json::Value>(&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");
|
|
}
|
|
}
|