diff --git a/.gitignore b/.gitignore index 3e5f3ff2..03956100 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,6 @@ ehthumbs.db .env .env.* -mnt -mnt/** +sdcard +sdcard/** diff --git a/crates/host-desktop/src/main.rs b/crates/host-desktop/src/main.rs index e8e81fc5..01687d5f 100644 --- a/crates/host-desktop/src/main.rs +++ b/crates/host-desktop/src/main.rs @@ -5,23 +5,81 @@ mod log_sink; use crate::prometeu_runner::PrometeuRunner; use winit::event_loop::EventLoop; +use prometeu_core::telemetry::CertificationConfig; + +fn load_cap_config(path: &str) -> Option { + let content = std::fs::read_to_string(path).ok()?; + let mut config = CertificationConfig { + enabled: true, + ..Default::default() + }; + + for line in content.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { continue; } + let parts: Vec<&str> = line.split('=').collect(); + if parts.len() != 2 { continue; } + let key = parts[0].trim(); + let val = parts[1].trim(); + + match key { + "cycles_budget" => config.cycles_budget_per_frame = val.parse().ok(), + "max_syscalls" => config.max_syscalls_per_frame = val.parse().ok(), + "max_host_cpu_us" => config.max_host_cpu_us_per_frame = val.parse().ok(), + _ => {} + } + } + + Some(config) +} fn main() -> Result<(), Box> { let args: Vec = std::env::args().collect(); let mut fs_root = None; + let mut cap_config = None; + let mut i = 0; while i < args.len() { if args[i] == "--fs-root" && i + 1 < args.len() { fs_root = Some(args[i + 1].clone()); i += 1; + } else if args[i] == "--cap" && i + 1 < args.len() { + cap_config = load_cap_config(&args[i + 1]); + i += 1; } i += 1; } let event_loop = EventLoop::new()?; - let mut runner = PrometeuRunner::new(fs_root); + let mut runner = PrometeuRunner::new(fs_root, cap_config); event_loop.run_app(&mut runner)?; Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_load_cap_config() { + let content = "cycles_budget=500\nmax_syscalls=10\n# comentário\nmax_host_cpu_us=2000"; + let path = "test_cap.cfg"; + std::fs::write(path, content).unwrap(); + + let config = load_cap_config(path).unwrap(); + assert!(config.enabled); + assert_eq!(config.cycles_budget_per_frame, Some(500)); + assert_eq!(config.max_syscalls_per_frame, Some(10)); + assert_eq!(config.max_host_cpu_us_per_frame, Some(2000)); + + std::fs::remove_file(path).unwrap(); + } + + #[test] + fn test_load_cap_config_not_found() { + let config = load_cap_config("non_existent.cfg"); + assert!(config.is_none()); + } } \ No newline at end of file diff --git a/crates/host-desktop/src/prometeu_runner.rs b/crates/host-desktop/src/prometeu_runner.rs index b61550ab..68a7dec0 100644 --- a/crates/host-desktop/src/prometeu_runner.rs +++ b/crates/host-desktop/src/prometeu_runner.rs @@ -17,6 +17,8 @@ use winit::event_loop::{ActiveEventLoop, ControlFlow}; use winit::keyboard::{KeyCode, PhysicalKey}; use winit::window::{Window, WindowAttributes, WindowId}; +use prometeu_core::telemetry::CertificationConfig; + pub struct PrometeuRunner { window: Option<&'static Window>, pixels: Option>, @@ -35,6 +37,10 @@ pub struct PrometeuRunner { last_stats_update: Instant, frames_since_last_update: u64, + current_fps: f64, + + overlay_enabled: bool, + audio_load_accum_us: u64, audio_load_samples: u64, audio_perf_consumer: Option>>>, @@ -44,10 +50,10 @@ pub struct PrometeuRunner { } impl PrometeuRunner { - pub(crate) fn new(fs_root: Option) -> Self { + pub(crate) fn new(fs_root: Option, cap_config: Option) -> Self { let target_fps = 60; - let mut firmware = Firmware::new(); + 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)); @@ -67,6 +73,8 @@ impl PrometeuRunner { last_stats_update: Instant::now(), frames_since_last_update: 0, + current_fps: 0.0, + overlay_enabled: false, audio_load_accum_us: 0, audio_load_samples: 0, audio_perf_consumer: None, @@ -227,6 +235,12 @@ impl ApplicationHandler for PrometeuRunner { 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; + } + } + _ => {} } } @@ -304,8 +318,8 @@ impl ApplicationHandler for PrometeuRunner { // 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 fps = self.frames_since_last_update as f64 / stats_elapsed.as_secs_f64(); 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 @@ -321,7 +335,7 @@ impl ApplicationHandler for PrometeuRunner { let title = format!( "PROMETEU | GFX: {:.1} KB | FPS: {:.1} | Load: {:.1}% (C) + {:.1}% (A) | Frame: tick {} logical {}", - kb, fps, cpu_load_core, cpu_load_audio, self.firmware.os.tick_index, self.firmware.os.logical_frame_index + 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); } @@ -341,6 +355,36 @@ impl ApplicationHandler for PrometeuRunner { }; 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(); } } diff --git a/crates/prometeu-core/src/firmware/firmware.rs b/crates/prometeu-core/src/firmware/firmware.rs index fcb1716a..323dc559 100644 --- a/crates/prometeu-core/src/firmware/firmware.rs +++ b/crates/prometeu-core/src/firmware/firmware.rs @@ -6,6 +6,8 @@ use crate::prometeu_hub::PrometeuHub; use crate::prometeu_os::PrometeuOS; use crate::virtual_machine::VirtualMachine; +use crate::telemetry::CertificationConfig; + pub struct Firmware { pub vm: VirtualMachine, pub os: PrometeuOS, @@ -15,10 +17,10 @@ pub struct Firmware { } impl Firmware { - pub fn new() -> Self { + pub fn new(cap_config: Option) -> Self { Self { vm: VirtualMachine::default(), - os: PrometeuOS::new(), + os: PrometeuOS::new(cap_config), hub: PrometeuHub::new(), state: FirmwareState::Reset(ResetStep), state_initialized: false, diff --git a/crates/prometeu-core/src/hardware/gfx.rs b/crates/prometeu-core/src/hardware/gfx.rs index 0b0af984..150a5b5c 100644 --- a/crates/prometeu-core/src/hardware/gfx.rs +++ b/crates/prometeu-core/src/hardware/gfx.rs @@ -1,4 +1,5 @@ use crate::model::{Color, HudTileLayer, ScrollableTileLayer, Sprite, TileBank, TileMap, TileSize}; +use std::mem::size_of; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum BlendMode { @@ -380,6 +381,80 @@ impl Gfx { total } + + pub fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color) { + let mut cx = x; + for c in text.chars() { + self.draw_char(cx, y, c, color); + cx += 4; + } + } + + fn draw_char(&mut self, x: i32, y: i32, c: char, color: Color) { + let glyph: [u8; 5] = match c.to_ascii_uppercase() { + '0' => [0x7, 0x5, 0x5, 0x5, 0x7], + '1' => [0x2, 0x6, 0x2, 0x2, 0x7], + '2' => [0x7, 0x1, 0x7, 0x4, 0x7], + '3' => [0x7, 0x1, 0x7, 0x1, 0x7], + '4' => [0x5, 0x5, 0x7, 0x1, 0x1], + '5' => [0x7, 0x4, 0x7, 0x1, 0x7], + '6' => [0x7, 0x4, 0x7, 0x5, 0x7], + '7' => [0x7, 0x1, 0x1, 0x1, 0x1], + '8' => [0x7, 0x5, 0x7, 0x5, 0x7], + '9' => [0x7, 0x5, 0x7, 0x1, 0x7], + 'A' => [0x7, 0x5, 0x7, 0x5, 0x5], + 'B' => [0x6, 0x5, 0x6, 0x5, 0x6], + 'C' => [0x7, 0x4, 0x4, 0x4, 0x7], + 'D' => [0x6, 0x5, 0x5, 0x5, 0x6], + 'E' => [0x7, 0x4, 0x6, 0x4, 0x7], + 'F' => [0x7, 0x4, 0x6, 0x4, 0x4], + 'G' => [0x7, 0x4, 0x5, 0x5, 0x7], + 'H' => [0x5, 0x5, 0x7, 0x5, 0x5], + 'I' => [0x7, 0x2, 0x2, 0x2, 0x7], + 'J' => [0x1, 0x1, 0x1, 0x5, 0x2], + 'K' => [0x5, 0x5, 0x6, 0x5, 0x5], + 'L' => [0x4, 0x4, 0x4, 0x4, 0x7], + 'M' => [0x5, 0x7, 0x5, 0x5, 0x5], + 'N' => [0x5, 0x5, 0x5, 0x5, 0x5], + 'O' => [0x7, 0x5, 0x5, 0x5, 0x7], + 'P' => [0x7, 0x5, 0x7, 0x4, 0x4], + 'Q' => [0x7, 0x5, 0x5, 0x7, 0x1], + 'R' => [0x7, 0x5, 0x6, 0x5, 0x5], + 'S' => [0x7, 0x4, 0x7, 0x1, 0x7], + 'T' => [0x7, 0x2, 0x2, 0x2, 0x2], + 'U' => [0x5, 0x5, 0x5, 0x5, 0x7], + 'V' => [0x5, 0x5, 0x5, 0x5, 0x2], + 'W' => [0x5, 0x5, 0x5, 0x7, 0x5], + 'X' => [0x5, 0x5, 0x2, 0x5, 0x5], + 'Y' => [0x5, 0x5, 0x2, 0x2, 0x2], + 'Z' => [0x7, 0x1, 0x2, 0x4, 0x7], + ':' => [0x0, 0x2, 0x0, 0x2, 0x0], + '.' => [0x0, 0x0, 0x0, 0x0, 0x2], + ',' => [0x0, 0x0, 0x0, 0x2, 0x4], + '!' => [0x2, 0x2, 0x2, 0x0, 0x2], + '?' => [0x7, 0x1, 0x2, 0x0, 0x2], + ' ' => [0x0, 0x0, 0x0, 0x0, 0x0], + '|' => [0x2, 0x2, 0x2, 0x2, 0x2], + '/' => [0x1, 0x1, 0x2, 0x4, 0x4], + '(' => [0x2, 0x4, 0x4, 0x4, 0x2], + ')' => [0x2, 0x1, 0x1, 0x1, 0x2], + '>' => [0x4, 0x2, 0x1, 0x2, 0x4], + '<' => [0x1, 0x2, 0x4, 0x2, 0x1], + _ => [0x7, 0x7, 0x7, 0x7, 0x7], + }; + + for (row_idx, row) in glyph.iter().enumerate() { + for col_idx in 0..3 { + if (row >> (2 - col_idx)) & 1 == 1 { + let px = x + col_idx as i32; + let py = y + row_idx as i32; + if px >= 0 && px < self.w as i32 && py >= 0 && py < self.h as i32 { + self.back[py as usize * self.w + px as usize] = color.0; + } + } + } + } + } } /// Faz blend em RGB565 por canal com saturação. diff --git a/crates/prometeu-core/src/lib.rs b/crates/prometeu-core/src/lib.rs index ce0d47ee..f88ef5ed 100644 --- a/crates/prometeu-core/src/lib.rs +++ b/crates/prometeu-core/src/lib.rs @@ -1,9 +1,10 @@ pub mod hardware; pub mod log; pub mod virtual_machine; -mod model; +pub mod model; pub mod firmware; pub mod fs; +pub mod telemetry; mod prometeu_os; mod prometeu_hub; diff --git a/crates/prometeu-core/src/prometeu_os/prometeu_os.rs b/crates/prometeu-core/src/prometeu_os/prometeu_os.rs index 7496818a..21c3c6c4 100644 --- a/crates/prometeu-core/src/prometeu_os/prometeu_os.rs +++ b/crates/prometeu-core/src/prometeu_os/prometeu_os.rs @@ -3,6 +3,7 @@ use crate::hardware::{HardwareBridge, InputSignals}; use crate::log::{LogLevel, LogService, LogSource}; use crate::model::{Cartridge, Color, Sample}; use crate::prometeu_os::NativeInterface; +use crate::telemetry::{CertificationConfig, Certifier, TelemetryFrame}; use crate::virtual_machine::{Value, VirtualMachine}; use std::collections::HashMap; use std::sync::Arc; @@ -32,6 +33,12 @@ pub struct PrometeuOS { pub log_service: LogService, pub current_app_id: u32, pub logs_written_this_frame: HashMap, + + // Telemetria e Certificação + pub telemetry_current: TelemetryFrame, + pub telemetry_last: TelemetryFrame, + pub certifier: Certifier, + boot_time: Instant, } @@ -42,7 +49,7 @@ impl PrometeuOS { pub const MAX_LOG_LEN: usize = 256; pub const MAX_LOGS_PER_FRAME: u32 = 10; - pub fn new() -> Self { + pub fn new(cap_config: Option) -> Self { let boot_time = Instant::now(); let mut os = Self { tick_index: 0, @@ -60,6 +67,9 @@ impl PrometeuOS { log_service: LogService::new(4096), current_app_id: 0, logs_written_this_frame: HashMap::new(), + telemetry_current: TelemetryFrame::default(), + telemetry_last: TelemetryFrame::default(), + certifier: Certifier::new(cap_config.unwrap_or_default()), boot_time, }; @@ -138,6 +148,12 @@ impl PrometeuOS { self.logical_frame_active = true; self.logical_frame_remaining_cycles = Self::CYCLES_PER_LOGICAL_FRAME; self.begin_logical_frame(signals, hw); + + // Início do frame: resetar acumulador da telemetria (mas manter frame_index) + self.telemetry_current = TelemetryFrame { + frame_index: self.logical_frame_index, + ..Default::default() + }; } // Budget para este tick: o mínimo entre a fatia do tick e o que resta no frame lógico @@ -151,8 +167,23 @@ impl PrometeuOS { Ok(run) => { self.logical_frame_remaining_cycles = self.logical_frame_remaining_cycles.saturating_sub(run.cycles_used); + // Acumula métricas + self.telemetry_current.cycles_used += run.cycles_used; + self.telemetry_current.vm_steps += run.steps_executed; + if run.reason == crate::virtual_machine::LogicalFrameEndingReason::FrameSync { hw.gfx_mut().render_all(); + + // Finaliza telemetria do frame (host_cpu_time será refinado no final do tick) + self.telemetry_current.host_cpu_time_us = start.elapsed().as_micros() as u64; + + // Certificação (CAP) + let ts_ms = self.boot_time.elapsed().as_millis() as u64; + self.telemetry_current.violations = self.certifier.evaluate(&self.telemetry_current, &mut self.log_service, ts_ms) as u32; + + // Latch: o que o overlay lê + self.telemetry_last = self.telemetry_current; + self.logical_frame_index += 1; self.logical_frame_active = false; self.logical_frame_remaining_cycles = 0; @@ -167,6 +198,12 @@ impl PrometeuOS { } self.last_frame_cpu_time_us = start.elapsed().as_micros() as u64; + + // Se o frame acabou neste tick, atualizamos o tempo real final no latch + if !self.logical_frame_active && self.telemetry_last.frame_index == self.logical_frame_index.wrapping_sub(1) { + self.telemetry_last.host_cpu_time_us = self.last_frame_cpu_time_us; + } + None } @@ -277,7 +314,7 @@ mod tests { #[test] fn test_infinite_loop_budget_reset_bug() { - let mut os = PrometeuOS::new(); + let mut os = PrometeuOS::new(None); let mut vm = VirtualMachine::default(); let mut hw = Hardware::new(); let signals = InputSignals::default(); @@ -315,7 +352,7 @@ mod tests { #[test] fn test_budget_reset_on_frame_sync() { - let mut os = PrometeuOS::new(); + let mut os = PrometeuOS::new(None); let mut vm = VirtualMachine::default(); let mut hw = Hardware::new(); let signals = InputSignals::default(); @@ -360,7 +397,7 @@ mod tests { #[test] fn test_syscall_log_write_and_rate_limit() { - let mut os = PrometeuOS::new(); + let mut os = PrometeuOS::new(None); let mut vm = VirtualMachine::default(); let mut hw = Hardware::new(); @@ -430,6 +467,7 @@ mod tests { impl NativeInterface for PrometeuOS { fn syscall(&mut self, id: u32, vm: &mut VirtualMachine, hw: &mut dyn HardwareBridge) -> Result { + self.telemetry_current.syscalls += 1; match id { // system.has_cart() -> bool 0x0001 => { diff --git a/crates/prometeu-core/src/telemetry.rs b/crates/prometeu-core/src/telemetry.rs new file mode 100644 index 00000000..7c18996b --- /dev/null +++ b/crates/prometeu-core/src/telemetry.rs @@ -0,0 +1,112 @@ +use crate::log::{LogLevel, LogService, LogSource}; + +#[derive(Debug, Clone, Copy, Default)] +pub struct TelemetryFrame { + pub frame_index: u64, + pub vm_steps: u32, + pub cycles_used: u64, + pub syscalls: u32, + pub host_cpu_time_us: u64, + pub violations: u32, +} + +#[derive(Debug, Clone, Copy, Default)] +pub struct CertificationConfig { + pub enabled: bool, + pub cycles_budget_per_frame: Option, + pub max_syscalls_per_frame: Option, + pub max_host_cpu_us_per_frame: Option, +} + +pub struct Certifier { + pub config: CertificationConfig, +} + +impl Certifier { + pub fn new(config: CertificationConfig) -> Self { + Self { config } + } + + pub fn evaluate(&self, telemetry: &TelemetryFrame, log_service: &mut LogService, ts_ms: u64) -> usize { + if !self.config.enabled { + return 0; + } + + let mut violations = 0; + + if let Some(budget) = self.config.cycles_budget_per_frame { + if telemetry.cycles_used > budget { + log_service.log( + ts_ms, + telemetry.frame_index, + LogLevel::Warn, + LogSource::Pos, + 0xCA01, + format!("Cert: cycles_used exceeded budget ({} > {})", telemetry.cycles_used, budget), + ); + violations += 1; + } + } + + if let Some(limit) = self.config.max_syscalls_per_frame { + if telemetry.syscalls > limit { + log_service.log( + ts_ms, + telemetry.frame_index, + LogLevel::Warn, + LogSource::Pos, + 0xCA02, + format!("Cert: syscalls per frame exceeded limit ({} > {})", telemetry.syscalls, limit), + ); + violations += 1; + } + } + + if let Some(limit) = self.config.max_host_cpu_us_per_frame { + if telemetry.host_cpu_time_us > limit { + log_service.log( + ts_ms, + telemetry.frame_index, + LogLevel::Warn, + LogSource::Pos, + 0xCA03, + format!("Cert: host_cpu_time_us exceeded limit ({} > {})", telemetry.host_cpu_time_us, limit), + ); + violations += 1; + } + } + + violations + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::log::LogService; + + #[test] + fn test_certifier_violations() { + let config = CertificationConfig { + enabled: true, + cycles_budget_per_frame: Some(100), + max_syscalls_per_frame: Some(5), + max_host_cpu_us_per_frame: Some(1000), + }; + let cert = Certifier::new(config); + let mut ls = LogService::new(10); + + let mut tel = TelemetryFrame::default(); + tel.cycles_used = 150; + tel.syscalls = 10; + tel.host_cpu_time_us = 500; + + let violations = cert.evaluate(&tel, &mut ls, 1000); + assert_eq!(violations, 2); + + let logs = ls.get_recent(10); + assert_eq!(logs.len(), 2); + assert!(logs[0].msg.contains("cycles_used")); + assert!(logs[1].msg.contains("syscalls")); + } +} diff --git a/crates/prometeu-core/src/virtual_machine/virtual_machine.rs b/crates/prometeu-core/src/virtual_machine/virtual_machine.rs index aa59911f..bcb55c73 100644 --- a/crates/prometeu-core/src/virtual_machine/virtual_machine.rs +++ b/crates/prometeu-core/src/virtual_machine/virtual_machine.rs @@ -16,6 +16,7 @@ pub enum LogicalFrameEndingReason { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct BudgetReport { pub cycles_used: u64, + pub steps_executed: u32, pub reason: LogicalFrameEndingReason, } @@ -70,6 +71,7 @@ impl VirtualMachine { hw: &mut dyn HardwareBridge, ) -> Result { let start_cycles = self.cycles; + let mut steps_executed = 0; let mut ending_reason: Option = None; while (self.cycles - start_cycles) < budget @@ -85,11 +87,13 @@ impl VirtualMachine { if opcode == OpCode::FrameSync { self.pc += 2; self.cycles += OpCode::FrameSync.cycles(); + steps_executed += 1; ending_reason = Some(LogicalFrameEndingReason::FrameSync); break; } self.step(native, hw)?; + steps_executed += 1; // garante progresso real if self.pc == pc_before && self.cycles == cycles_before && !self.halted { @@ -109,6 +113,7 @@ impl VirtualMachine { Ok(BudgetReport { cycles_used: self.cycles - start_cycles, + steps_executed, reason: ending_reason.unwrap(), }) }