use crate::stats::HostStats; use prometeu_firmware::Firmware; const PANEL_X: usize = 6; const PANEL_Y: usize = 3; const PANEL_WIDTH: usize = 170; const PANEL_PADDING_X: usize = 8; const PANEL_PADDING_Y: usize = 3; const LINE_HEIGHT: usize = 12; const CHAR_SCALE: usize = 1; const BAR_WIDTH: usize = PANEL_WIDTH - (PANEL_PADDING_X * 2); const BAR_HEIGHT: usize = 6; const BG: [u8; 4] = [10, 18, 32, 208]; const BORDER: [u8; 4] = [90, 126, 170, 255]; const TEXT: [u8; 4] = [235, 240, 255, 255]; const DIM: [u8; 4] = [150, 168, 196, 255]; const WARN: [u8; 4] = [255, 104, 104, 255]; const BAR_BG: [u8; 4] = [30, 42, 61, 255]; const BAR_FILL: [u8; 4] = [91, 184, 255, 255]; const BAR_WARN: [u8; 4] = [255, 150, 102, 255]; const OVERLAY_HEAP_FALLBACK_BYTES: usize = 8 * 1024 * 1024; #[derive(Debug, Clone)] pub(crate) struct OverlayMetric { label: &'static str, value: String, warn: bool, } #[derive(Debug, Clone)] pub(crate) struct OverlayBar { label: &'static str, value: String, ratio: f32, warn: bool, } #[derive(Debug, Clone)] pub(crate) struct OverlaySnapshot { rows: Vec<(OverlayMetric, OverlayMetric)>, bars: Vec, footer: Vec, } #[derive(Clone, Copy)] struct Rect { x: usize, y: usize, width: usize, height: usize, } struct FrameCanvas<'a> { frame: &'a mut [u8], width: usize, height: usize, } pub(crate) fn capture_snapshot(stats: &HostStats, firmware: &Firmware) -> OverlaySnapshot { let tel = firmware.os.atomic_telemetry.snapshot(); let recent_logs = firmware.os.log_service.get_recent(10); let violations_count = recent_logs.iter().filter(|e| e.tag >= 0xCA01 && e.tag <= 0xCA07).count(); let mut footer = Vec::new(); if violations_count > 0 && let Some(event) = recent_logs.into_iter().rev().find(|e| e.tag >= 0xCA01 && e.tag <= 0xCA07) { footer.push(OverlayMetric { label: "CERT", value: truncate_value(&event.msg, 28), warn: true, }); } if let Some(report) = firmware.os.last_crash_report.as_ref() { footer.push(OverlayMetric { label: "CRASH", value: truncate_value(&report.summary(), 28), warn: true, }); } let cycles_ratio = ratio(tel.cycles_used, tel.cycles_budget); let heap_total_bytes = firmware .os .certifier .config .max_heap_bytes .or(if tel.heap_max_bytes > 0 { Some(tel.heap_max_bytes) } else { None }) .unwrap_or(OVERLAY_HEAP_FALLBACK_BYTES); let heap_ratio = ratio(tel.heap_used_bytes as u64, heap_total_bytes as u64); let glyph_ratio = ratio(tel.glyph_slots_used as u64, tel.glyph_slots_total as u64); let sound_ratio = ratio(tel.sound_slots_used as u64, tel.sound_slots_total as u64); OverlaySnapshot { rows: vec![ ( OverlayMetric { label: "FPS", value: format!("{:.1}", stats.current_fps), warn: false, }, OverlayMetric { label: "CERT", value: violations_count.to_string(), warn: violations_count > 0, }, ), ( OverlayMetric { label: "HOST", value: format!("{:.2}ms", stats.average_host_cpu_ms()), warn: false, }, OverlayMetric { label: "STEPS", value: tel.vm_steps.to_string(), warn: false }, ), ( OverlayMetric { label: "SYSC", value: tel.syscalls.to_string(), warn: false }, OverlayMetric { label: "LOGS", value: tel.logs_count.to_string(), warn: false }, ), ], bars: vec![ OverlayBar { label: "HEAP", value: format!( "{}K{}", tel.heap_used_bytes.div_ceil(1024), if heap_total_bytes > 0 { format!(" / {}K", heap_total_bytes.div_ceil(1024)) } else { String::new() } ), ratio: heap_ratio, warn: tel.heap_used_bytes >= heap_total_bytes, }, OverlayBar { label: "BUDGET", value: if tel.cycles_budget > 0 { format!( "{:.1}K/{:.1}K", tel.cycles_used as f64 / 1000.0, tel.cycles_budget as f64 / 1000.0 ) } else { "0.0K/0.0K".to_string() }, ratio: cycles_ratio, warn: cycles_ratio >= 0.9, }, OverlayBar { label: "GLYPH", value: format!("{} / {} slots", tel.glyph_slots_used, tel.glyph_slots_total), ratio: glyph_ratio, warn: tel.glyph_slots_total > 0 && tel.glyph_slots_used >= tel.glyph_slots_total, }, OverlayBar { label: "SOUNDS", value: format!("{} / {} slots", tel.sound_slots_used, tel.sound_slots_total), ratio: sound_ratio, warn: tel.sound_slots_total > 0 && tel.sound_slots_used >= tel.sound_slots_total, }, ], footer, } } pub(crate) fn draw_overlay( frame: &mut [u8], frame_width: usize, frame_height: usize, snapshot: &OverlaySnapshot, ) { let mut canvas = FrameCanvas { frame, width: frame_width, height: frame_height }; let panel_height = frame_height.saturating_sub(PANEL_Y * 2); let panel_rect = Rect { x: PANEL_X, y: PANEL_Y, width: PANEL_WIDTH, height: panel_height }; fill_rect_alpha(&mut canvas, panel_rect, BG); stroke_rect(&mut canvas, panel_rect, BORDER); let mut y = PANEL_Y + PANEL_PADDING_Y; for (left, right) in &snapshot.rows { draw_metric_pair(canvas.frame, canvas.width, canvas.height, y, left, right); y += LINE_HEIGHT; } for bar in &snapshot.bars { let color = if bar.warn { WARN } else { TEXT }; draw_text( canvas.frame, canvas.width, canvas.height, PANEL_X + PANEL_PADDING_X, y, bar.label, DIM, ); draw_text(canvas.frame, canvas.width, canvas.height, PANEL_X + 48, y, &bar.value, color); y += LINE_HEIGHT - 2; let bar_x = PANEL_X + PANEL_PADDING_X; let bar_rect = Rect { x: bar_x, y, width: BAR_WIDTH, height: BAR_HEIGHT }; fill_rect(&mut canvas, bar_rect, BAR_BG); let fill_width = ((BAR_WIDTH as f32) * bar.ratio.clamp(0.0, 1.0)).round() as usize; fill_rect( &mut canvas, Rect { x: bar_x, y, width: fill_width, height: BAR_HEIGHT }, if bar.warn { BAR_WARN } else { BAR_FILL }, ); stroke_rect(&mut canvas, bar_rect, BORDER); y += BAR_HEIGHT + 6; } for line in &snapshot.footer { let color = if line.warn { WARN } else { TEXT }; draw_text( canvas.frame, canvas.width, canvas.height, PANEL_X + PANEL_PADDING_X, y, line.label, DIM, ); draw_text(canvas.frame, canvas.width, canvas.height, PANEL_X + 48, y, &line.value, color); y += LINE_HEIGHT; } } fn draw_metric_pair( frame: &mut [u8], frame_width: usize, frame_height: usize, y: usize, left: &OverlayMetric, right: &OverlayMetric, ) { let left_x = PANEL_X + PANEL_PADDING_X; let right_x = PANEL_X + 86; draw_metric(frame, frame_width, frame_height, left_x, y, left); draw_metric(frame, frame_width, frame_height, right_x, y, right); } fn draw_metric( frame: &mut [u8], frame_width: usize, frame_height: usize, x: usize, y: usize, metric: &OverlayMetric, ) { let color = if metric.warn { WARN } else { TEXT }; draw_text(frame, frame_width, frame_height, x, y, metric.label, DIM); draw_text(frame, frame_width, frame_height, x + 30, y, &metric.value, color); } fn ratio(value: u64, total: u64) -> f32 { if total == 0 { 0.0 } else { (value as f32 / total as f32).clamp(0.0, 1.0) } } fn truncate_value(value: &str, max_len: usize) -> String { let mut upper = value.to_ascii_uppercase(); if upper.len() > max_len { upper.truncate(max_len); } upper } fn draw_text( frame: &mut [u8], frame_width: usize, frame_height: usize, x: usize, y: usize, text: &str, color: [u8; 4], ) { let mut cursor_x = x; for ch in text.chars() { if ch == '\n' { continue; } draw_char(frame, frame_width, frame_height, cursor_x, y, ch.to_ascii_uppercase(), color); cursor_x += 6 * CHAR_SCALE; } } fn draw_char( frame: &mut [u8], frame_width: usize, frame_height: usize, x: usize, y: usize, ch: char, color: [u8; 4], ) { let glyph = glyph_bits(ch); for (row, bits) in glyph.iter().enumerate() { for col in 0..5 { if bits & (1 << (4 - col)) != 0 { let mut canvas = FrameCanvas { frame, width: frame_width, height: frame_height }; fill_rect( &mut canvas, Rect { x: x + col * CHAR_SCALE, y: y + row * CHAR_SCALE, width: CHAR_SCALE, height: CHAR_SCALE, }, color, ); } } } } fn fill_rect_alpha(canvas: &mut FrameCanvas<'_>, rect: Rect, color: [u8; 4]) { let max_x = (rect.x + rect.width).min(canvas.width); let max_y = (rect.y + rect.height).min(canvas.height); for py in rect.y..max_y { for px in rect.x..max_x { blend_pixel(canvas.frame, canvas.width, px, py, color); } } } fn fill_rect(canvas: &mut FrameCanvas<'_>, rect: Rect, color: [u8; 4]) { let max_x = (rect.x + rect.width).min(canvas.width); let max_y = (rect.y + rect.height).min(canvas.height); for py in rect.y..max_y { for px in rect.x..max_x { write_pixel(canvas.frame, canvas.width, px, py, color); } } } fn stroke_rect(canvas: &mut FrameCanvas<'_>, rect: Rect, color: [u8; 4]) { if rect.width == 0 || rect.height == 0 { return; } fill_rect(canvas, Rect { x: rect.x, y: rect.y, width: rect.width, height: 1 }, color); fill_rect( canvas, Rect { x: rect.x, y: rect.y + rect.height.saturating_sub(1), width: rect.width, height: 1 }, color, ); fill_rect(canvas, Rect { x: rect.x, y: rect.y, width: 1, height: rect.height }, color); fill_rect( canvas, Rect { x: rect.x + rect.width.saturating_sub(1), y: rect.y, width: 1, height: rect.height }, color, ); } fn blend_pixel(frame: &mut [u8], frame_width: usize, x: usize, y: usize, color: [u8; 4]) { let idx = (y * frame_width + x) * 4; let alpha = color[3] as f32 / 255.0; let inv = 1.0 - alpha; frame[idx] = (frame[idx] as f32 * inv + color[0] as f32 * alpha).round() as u8; frame[idx + 1] = (frame[idx + 1] as f32 * inv + color[1] as f32 * alpha).round() as u8; frame[idx + 2] = (frame[idx + 2] as f32 * inv + color[2] as f32 * alpha).round() as u8; frame[idx + 3] = 0xFF; } fn write_pixel(frame: &mut [u8], frame_width: usize, x: usize, y: usize, color: [u8; 4]) { let idx = (y * frame_width + x) * 4; frame[idx] = color[0]; frame[idx + 1] = color[1]; frame[idx + 2] = color[2]; frame[idx + 3] = color[3]; } fn glyph_bits(ch: char) -> [u8; 7] { match ch { 'A' => [0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11], 'B' => [0x1E, 0x11, 0x11, 0x1E, 0x11, 0x11, 0x1E], 'C' => [0x0E, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0E], 'D' => [0x1E, 0x12, 0x11, 0x11, 0x11, 0x12, 0x1E], 'E' => [0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x1F], 'F' => [0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x10], 'G' => [0x0E, 0x11, 0x10, 0x17, 0x11, 0x11, 0x0E], 'H' => [0x11, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11], 'I' => [0x0E, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0E], 'J' => [0x01, 0x01, 0x01, 0x01, 0x11, 0x11, 0x0E], 'K' => [0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11], 'L' => [0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1F], 'M' => [0x11, 0x1B, 0x15, 0x15, 0x11, 0x11, 0x11], 'N' => [0x11, 0x11, 0x19, 0x15, 0x13, 0x11, 0x11], 'O' => [0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E], 'P' => [0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10, 0x10], 'Q' => [0x0E, 0x11, 0x11, 0x11, 0x15, 0x12, 0x0D], 'R' => [0x1E, 0x11, 0x11, 0x1E, 0x14, 0x12, 0x11], 'S' => [0x0F, 0x10, 0x10, 0x0E, 0x01, 0x01, 0x1E], 'T' => [0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04], 'U' => [0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E], 'V' => [0x11, 0x11, 0x11, 0x11, 0x11, 0x0A, 0x04], 'W' => [0x11, 0x11, 0x11, 0x15, 0x15, 0x15, 0x0A], 'X' => [0x11, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x11], 'Y' => [0x11, 0x11, 0x0A, 0x04, 0x04, 0x04, 0x04], 'Z' => [0x1F, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1F], '0' => [0x0E, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0E], '1' => [0x04, 0x0C, 0x14, 0x04, 0x04, 0x04, 0x1F], '2' => [0x0E, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1F], '3' => [0x1E, 0x01, 0x01, 0x0E, 0x01, 0x01, 0x1E], '4' => [0x02, 0x06, 0x0A, 0x12, 0x1F, 0x02, 0x02], '5' => [0x1F, 0x10, 0x10, 0x1E, 0x01, 0x01, 0x1E], '6' => [0x0E, 0x10, 0x10, 0x1E, 0x11, 0x11, 0x0E], '7' => [0x1F, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08], '8' => [0x0E, 0x11, 0x11, 0x0E, 0x11, 0x11, 0x0E], '9' => [0x0E, 0x11, 0x11, 0x0F, 0x01, 0x01, 0x0E], ':' => [0x00, 0x04, 0x04, 0x00, 0x04, 0x04, 0x00], '.' => [0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x06], '/' => [0x01, 0x02, 0x02, 0x04, 0x08, 0x08, 0x10], '%' => [0x19, 0x19, 0x02, 0x04, 0x08, 0x13, 0x13], '(' => [0x02, 0x04, 0x08, 0x08, 0x08, 0x04, 0x02], ')' => [0x08, 0x04, 0x02, 0x02, 0x02, 0x04, 0x08], '-' => [0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00], '_' => [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F], '+' => [0x00, 0x04, 0x04, 0x1F, 0x04, 0x04, 0x00], ' ' => [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], '?' => [0x0E, 0x11, 0x01, 0x02, 0x04, 0x00, 0x04], _ => [0x0E, 0x11, 0x01, 0x02, 0x04, 0x00, 0x04], } } #[cfg(test)] mod tests { use super::*; fn sample_snapshot() -> OverlaySnapshot { OverlaySnapshot { rows: vec![ ( OverlayMetric { label: "FPS", value: "60.0".to_string(), warn: false }, OverlayMetric { label: "CERT", value: "2".to_string(), warn: true }, ), ( OverlayMetric { label: "HOST", value: "1.23ms".to_string(), warn: false }, OverlayMetric { label: "STEPS", value: "420".to_string(), warn: false }, ), ], bars: vec![ OverlayBar { label: "HEAP", value: "1024K / 8192K".to_string(), ratio: 0.125, warn: false, }, OverlayBar { label: "BUDGET", value: "9.0K/10.0K".to_string(), ratio: 0.9, warn: true, }, ], footer: vec![OverlayMetric { label: "CRASH", value: "VM PANIC".to_string(), warn: true, }], } } #[test] fn draw_overlay_writes_to_host_rgba_frame() { let mut frame = vec![0u8; 320 * 180 * 4]; draw_overlay(&mut frame, 320, 180, &sample_snapshot()); assert!(frame.iter().any(|&byte| byte != 0)); } #[test] fn truncate_value_normalizes_and_caps() { assert_eq!(truncate_value("panic: lowercase", 12), "PANIC: LOWER"); } }