code decoupling for host_desktop
This commit is contained in:
parent
dd8ded9671
commit
1544c511b7
@ -1,7 +1,88 @@
|
|||||||
use prometeu_core::hardware::{AudioCommand, Channel, LoopMode, MAX_CHANNELS, OUTPUT_SAMPLE_RATE};
|
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;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
pub struct HostAudio {
|
||||||
|
pub producer: Option<ringbuf::wrap::CachingProd<Arc<HeapRb<AudioCommand>>>>,
|
||||||
|
pub perf_consumer: Option<ringbuf::wrap::CachingCons<Arc<HeapRb<u64>>>>,
|
||||||
|
_stream: Option<cpal::Stream>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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::<AudioCommand>::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::<u64>::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<AudioCommand>) {
|
||||||
|
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 {
|
pub struct AudioMixer {
|
||||||
voices: [Channel; MAX_CHANNELS],
|
voices: [Channel; MAX_CHANNELS],
|
||||||
pub last_processing_time: Duration,
|
pub last_processing_time: Duration,
|
||||||
53
crates/host-desktop/src/cap.rs
Normal file
53
crates/host-desktop/src/cap.rs
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
use prometeu_core::telemetry::CertificationConfig;
|
||||||
|
|
||||||
|
pub fn load_cap_config(path: &str) -> Option<CertificationConfig> {
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
240
crates/host-desktop/src/debugger.rs
Normal file
240
crates/host-desktop/src/debugger.rs
Normal file
@ -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<TcpListener>,
|
||||||
|
pub(crate) stream: Option<TcpStream>,
|
||||||
|
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::<DebugCommand>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
78
crates/host-desktop/src/input.rs
Normal file
78
crates/host-desktop/src/input.rs
Normal file
@ -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))
|
||||||
|
}
|
||||||
@ -1,38 +1,17 @@
|
|||||||
mod audio_mixer;
|
mod audio;
|
||||||
mod prometeu_runner;
|
mod runner;
|
||||||
mod fs_desktop_backend;
|
mod fs_backend;
|
||||||
mod log_sink;
|
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 winit::event_loop::EventLoop;
|
||||||
use prometeu_core::firmware::BootTarget;
|
use prometeu_core::firmware::BootTarget;
|
||||||
use prometeu_core::telemetry::CertificationConfig;
|
|
||||||
|
|
||||||
fn load_cap_config(path: &str) -> Option<CertificationConfig> {
|
|
||||||
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<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let args: Vec<String> = std::env::args().collect();
|
let args: Vec<String> = std::env::args().collect();
|
||||||
@ -96,36 +75,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
let event_loop = EventLoop::new()?;
|
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);
|
runner.set_boot_target(boot_target);
|
||||||
|
|
||||||
event_loop.run_app(&mut runner)?;
|
event_loop.run_app(&mut runner)?;
|
||||||
|
|
||||||
Ok(())
|
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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<Pixels<'static>>,
|
|
||||||
|
|
||||||
hardware: Hardware,
|
|
||||||
firmware: Firmware,
|
|
||||||
|
|
||||||
input_signals: InputSignals,
|
|
||||||
fs_root: Option<String>,
|
|
||||||
|
|
||||||
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<TcpListener>,
|
|
||||||
debug_stream: Option<TcpStream>,
|
|
||||||
debug_last_log_seq: u64,
|
|
||||||
debug_last_telemetry_frame: u64,
|
|
||||||
|
|
||||||
audio_load_accum_us: u64,
|
|
||||||
audio_load_samples: u64,
|
|
||||||
audio_perf_consumer: Option<ringbuf::wrap::CachingCons<Arc<HeapRb<u64>>>>,
|
|
||||||
|
|
||||||
audio_producer: Option<ringbuf::wrap::CachingProd<Arc<HeapRb<AudioCommand>>>>,
|
|
||||||
_audio_stream: Option<cpal::Stream>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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::<DebugCommand>(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<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_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::<AudioCommand>::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::<u64>::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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
462
crates/host-desktop/src/runner.rs
Normal file
462
crates/host-desktop/src/runner.rs
Normal file
@ -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<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");
|
||||||
|
}
|
||||||
|
}
|
||||||
65
crates/host-desktop/src/stats.rs
Normal file
65
crates/host-desktop/src/stats.rs
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
crates/host-desktop/src/utilities.rs
Normal file
26
crates/host-desktop/src/utilities.rs
Normal file
@ -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)
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user