bQUARKz 7a696623b5
Some checks failed
Intrepid/Prometeu/Runtime/pipeline/pr-master There was a failure building this commit
Intrepid/Prometeu/Runtime/pipeline/head This commit looks good
[PERF] Host Debug Overlay Isolation
2026-04-10 19:22:52 +01:00

480 lines
16 KiB
Rust

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<OverlayBar>,
footer: Vec<OverlayMetric>,
}
#[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");
}
}