implements PLN-0084 remove host debug overlay
This commit is contained in:
parent
295666b310
commit
d79c11faa8
@ -4,7 +4,6 @@ pub mod debugger;
|
||||
pub mod fs_backend;
|
||||
pub mod input;
|
||||
pub mod log_sink;
|
||||
pub mod overlay;
|
||||
pub mod runner;
|
||||
pub mod stats;
|
||||
pub mod utilities;
|
||||
|
||||
@ -1,487 +0,0 @@
|
||||
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: &mut Firmware) -> OverlaySnapshot {
|
||||
let tel = firmware.os.vm().telemetry_snapshot();
|
||||
let recent_logs = firmware.os.recent_logs(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.vm().last_crash_report() {
|
||||
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
|
||||
.vm()
|
||||
.cert_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);
|
||||
let scene_ratio = ratio(tel.scene_slots_used as u64, tel.scene_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,
|
||||
},
|
||||
OverlayBar {
|
||||
label: "SCENE",
|
||||
value: format!("{} / {} slots", tel.scene_slots_used, tel.scene_slots_total),
|
||||
ratio: scene_ratio,
|
||||
warn: tel.scene_slots_total > 0 && tel.scene_slots_used >= tel.scene_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::*;
|
||||
use prometeu_drivers::hardware::Hardware;
|
||||
|
||||
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; Hardware::W * Hardware::H * 4];
|
||||
draw_overlay(&mut frame, Hardware::W, Hardware::H, &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");
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,6 @@ use crate::debugger::HostDebugger;
|
||||
use crate::fs_backend::HostDirBackend;
|
||||
use crate::input::HostInputHandler;
|
||||
use crate::log_sink::HostConsoleSink;
|
||||
use crate::overlay::{capture_snapshot, draw_overlay};
|
||||
use crate::stats::HostStats;
|
||||
use crate::utilities::draw_rgba8888_to_rgba8;
|
||||
use pixels::wgpu::PresentMode;
|
||||
@ -128,9 +127,6 @@ pub struct HostRunner {
|
||||
/// directly instead of depending on guest-visible inspection syscalls.
|
||||
debugger: HostDebugger,
|
||||
|
||||
/// Flag to enable/disable the technical telemetry display.
|
||||
overlay_enabled: bool,
|
||||
|
||||
/// The physical audio driver.
|
||||
audio: HostAudio,
|
||||
/// Last known pause state to sync with audio.
|
||||
@ -170,7 +166,6 @@ impl HostRunner {
|
||||
|
||||
stats: HostStats::new(),
|
||||
debugger: HostDebugger::new(),
|
||||
overlay_enabled: false,
|
||||
audio: HostAudio::new(),
|
||||
last_paused_state: false,
|
||||
presentation: PresentationState::default(),
|
||||
@ -272,9 +267,6 @@ impl ApplicationHandler for HostRunner {
|
||||
}
|
||||
|
||||
WindowEvent::RedrawRequested => {
|
||||
let overlay_snapshot =
|
||||
self.overlay_enabled.then(|| capture_snapshot(&self.stats, &mut self.firmware));
|
||||
|
||||
// Get Pixels directly from the field (not via helper that gets the entire &mut self)
|
||||
let pixels = self.pixels.as_mut().expect("pixels not initialized");
|
||||
|
||||
@ -286,10 +278,6 @@ impl ApplicationHandler for HostRunner {
|
||||
let src = self.hardware.gfx.front_buffer();
|
||||
|
||||
draw_rgba8888_to_rgba8(src, frame);
|
||||
|
||||
if let Some(snapshot) = overlay_snapshot.as_ref() {
|
||||
draw_overlay(frame, Hardware::W, Hardware::H, snapshot);
|
||||
}
|
||||
} // <- frame borrow ends here
|
||||
|
||||
if pixels.render().is_err() {
|
||||
@ -309,12 +297,6 @@ impl ApplicationHandler for HostRunner {
|
||||
self.request_redraw_if_needed();
|
||||
println!("[Debugger] Execution started!");
|
||||
}
|
||||
|
||||
if is_down && code == KeyCode::F1 {
|
||||
self.overlay_enabled = !self.overlay_enabled;
|
||||
self.invalidate_host_surface();
|
||||
self.request_redraw_if_needed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -332,12 +314,8 @@ impl ApplicationHandler for HostRunner {
|
||||
// 1. Process pending debug commands from the network.
|
||||
self.debugger.check_commands(&mut self.firmware, &mut self.hardware);
|
||||
|
||||
// Sync inspection mode state. This is host-owned overlay/debugger control,
|
||||
// not a guest-visible debug ABI switch.
|
||||
self.firmware
|
||||
.os
|
||||
.vm()
|
||||
.set_inspection_active(self.overlay_enabled || self.debugger.stream.is_some());
|
||||
// Sync debugger inspection state; this is not a guest-visible debug ABI switch.
|
||||
self.firmware.os.vm().set_inspection_active(self.debugger.stream.is_some());
|
||||
|
||||
// 2. Maintain a filesystem connection if it was lost (e.g., directory removed).
|
||||
if let Some(root) = &self.fs_root
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{"type":"meta","next_id":{"DSC":39,"AGD":39,"DEC":31,"PLN":87,"LSN":47,"CLSN":1}}
|
||||
{"type":"discussion","id":"DSC-0038","status":"in_progress","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-05-25","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[{"id":"AGD-0038","file":"AGD-0038-renderframepacket-boundary-for-classic-2d-renderer.md","status":"accepted","created_at":"2026-05-25","updated_at":"2026-05-25"}],"decisions":[{"id":"DEC-0030","file":"DEC-0030-logical-render-pipeline-command-boundary.md","status":"accepted","created_at":"2026-05-25","updated_at":"2026-05-25","ref_agenda":"AGD-0038"}],"plans":[{"id":"PLN-0073","file":"PLN-0073-render-contract-specs.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0074","file":"PLN-0074-hal-render-submission-types.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0075","file":"PLN-0075-rendermanager-core.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0076","file":"PLN-0076-capabilities-and-abi-domain-split.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0077","file":"PLN-0077-composer-buffer-and-game2d-packet.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0078","file":"PLN-0078-classic2d-game-renderer-consumer.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0079","file":"PLN-0079-gfx2d-primitive-domain.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0080","file":"PLN-0080-shell-ui-packet-and-gfxui-domain.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0081","file":"PLN-0081-shell-hub-migration.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0082","file":"PLN-0082-frame-publication-and-present-boundary.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0083","file":"PLN-0083-fade-removal.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0084","file":"PLN-0084-host-debug-overlay-removal.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0085","file":"PLN-0085-pbs-stdlib-and-syscall-declarations.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0086","file":"PLN-0086-end-to-end-render-boundary-validation.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]}],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0038","status":"in_progress","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-05-25","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[{"id":"AGD-0038","file":"AGD-0038-renderframepacket-boundary-for-classic-2d-renderer.md","status":"accepted","created_at":"2026-05-25","updated_at":"2026-05-25"}],"decisions":[{"id":"DEC-0030","file":"DEC-0030-logical-render-pipeline-command-boundary.md","status":"accepted","created_at":"2026-05-25","updated_at":"2026-05-25","ref_agenda":"AGD-0038"}],"plans":[{"id":"PLN-0073","file":"PLN-0073-render-contract-specs.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0074","file":"PLN-0074-hal-render-submission-types.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0075","file":"PLN-0075-rendermanager-core.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0076","file":"PLN-0076-capabilities-and-abi-domain-split.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0077","file":"PLN-0077-composer-buffer-and-game2d-packet.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0078","file":"PLN-0078-classic2d-game-renderer-consumer.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0079","file":"PLN-0079-gfx2d-primitive-domain.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0080","file":"PLN-0080-shell-ui-packet-and-gfxui-domain.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0081","file":"PLN-0081-shell-hub-migration.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0082","file":"PLN-0082-frame-publication-and-present-boundary.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0083","file":"PLN-0083-fade-removal.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0084","file":"PLN-0084-host-debug-overlay-removal.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0085","file":"PLN-0085-pbs-stdlib-and-syscall-declarations.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0086","file":"PLN-0086-end-to-end-render-boundary-validation.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]}],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]}
|
||||
{"type":"discussion","id":"DSC-0034","status":"done","ticket":"system-os-domain-facades","title":"Agenda - SystemOS Domain Facades","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","services","api-surface","lifecycle","fs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0043","file":"discussion/lessons/DSC-0034-system-os-domain-facades/LSN-0043-systemos-domain-facades.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]}
|
||||
{"type":"discussion","id":"DSC-0023","status":"done","ticket":"perf-full-migration-to-atomic-telemetry","title":"Agenda - [PERF] Full Migration to Atomic Telemetry","created_at":"2026-04-10","updated_at":"2026-04-10","tags":["perf","runtime","telemetry"],"agendas":[{"id":"AGD-0021","file":"AGD-0021-full-migration-to-atomic-telemetry.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"decisions":[{"id":"DEC-0008","file":"DEC-0008-full-migration-to-atomic-telemetry.md","status":"accepted","created_at":"2026-04-10","updated_at":"2026-04-10"}],"plans":[{"id":"PLN-0007","file":"PLN-0007-full-migration-to-atomic-telemetry.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"lessons":[{"id":"LSN-0028","file":"discussion/lessons/DSC-0023-perf-full-migration-to-atomic-telemetry/LSN-0028-converging-to-single-atomic-telemetry-source.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
id: PLN-0084
|
||||
ticket: render-frame-packet-boundary
|
||||
title: Host Debug Overlay Removal
|
||||
status: open
|
||||
status: done
|
||||
created: 2026-05-25
|
||||
ref_decisions: [DEC-0030]
|
||||
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
||||
|
||||
@ -18,7 +18,7 @@ It covers:
|
||||
- profiling;
|
||||
- event and fault visibility;
|
||||
- certification input data;
|
||||
- host-side debug overlay (HUD) isolation.
|
||||
- host-side debug UI isolation.
|
||||
|
||||
## 2 Execution Modes
|
||||
|
||||
@ -131,15 +131,15 @@ The host may:
|
||||
- observe buffers separately
|
||||
- identify excessive redraw
|
||||
|
||||
Host-owned overlay or inspection surfaces MUST observe the last published logical frame rather than force continuous redraw to probe for changes.
|
||||
Host-owned inspection surfaces MUST observe the last published logical frame rather than force continuous redraw to probe for changes.
|
||||
|
||||
When overlay, debugger, or other host-only diagnostics become visually stable:
|
||||
When debugger or other host-only diagnostics become stable:
|
||||
|
||||
- the host MUST preserve the last valid visible image;
|
||||
- the host MUST NOT keep recomposing the surface by continuous polling alone;
|
||||
- additional redraw is only justified by a newly published logical frame or a host-owned invalidation event such as overlay toggle, resize, expose, or explicit debugger-visible state transition.
|
||||
- additional redraw is only justified by a newly published logical frame or a host-owned invalidation event such as resize, expose, or explicit debugger-visible state transition.
|
||||
|
||||
Paused or breakpointed execution does not grant permission to swap logical machine buffers just to sustain a host-only HUD.
|
||||
Paused or breakpointed execution does not grant permission to swap logical machine buffers just to sustain host-only diagnostics.
|
||||
|
||||
## 6 Time Profiling (Cycles)
|
||||
|
||||
@ -285,28 +285,32 @@ Each event has:
|
||||
- cost
|
||||
- consequence
|
||||
|
||||
## 11 Host-Side Debug Overlay (HUD) Isolation
|
||||
## 11 Host-Side Debug UI Isolation
|
||||
|
||||
The visual Debug Overlay (HUD) for technical inspection is not part of the emulated machine pipeline.
|
||||
The desktop host MUST NOT inject a debug overlay into Game or Shell output. Any
|
||||
future visual debug UI is a separate product/domain concern and requires its own
|
||||
decision before it can become part of host presentation.
|
||||
|
||||
### 10.1 Responsibilities
|
||||
|
||||
1. **Runtime:** Only exposes telemetry data via the machine diagnostics surface. It does not perform HUD rendering or string formatting.
|
||||
2. **Emulated graphics contract:** Machine graphics primitives such as `fill_rect` and `draw_text` remain valid parts of the emulated graphics/syscall contract. They are not host overlay APIs.
|
||||
3. **Host overlay module:** The Desktop Host owns a dedicated overlay module that performs host-side text, panel, and simple bar composition.
|
||||
4. **Composition boundary:** The overlay is composed on the Host presentation surface after the emulated frame is ready. Overlay pixels must not be written back into the emulated framebuffer.
|
||||
5. **Host control:** Overlay visibility and presentation policy remain under Host control.
|
||||
6. **Host (Desktop):** Responsible for collecting telemetry from the runtime and rendering the HUD as a native, transparent layer.
|
||||
3. **Host diagnostics:** The Desktop Host may consume telemetry for logs,
|
||||
debugger streams, and non-render diagnostics, but it does not compose debug
|
||||
pixels into the presentation surface.
|
||||
4. **Composition boundary:** Published Game and Shell output must be presented
|
||||
without host-owned debug overlay injection.
|
||||
|
||||
### 10.2 Principles
|
||||
|
||||
- **Zero Pipeline Interference:** HUD rendering must not inject pixels into the emulated framebuffer. It is applied after upscaling or as a separate display surface.
|
||||
- **Zero Cycle Impact:** HUD-related processing (like formatting technical text) must occur outside the emulated machine cycles.
|
||||
- **Toggle Control:** The activation of the overlay (typically via **F1**) is managed by the Host layer.
|
||||
- **Zero Pipeline Interference:** Host diagnostics must not inject pixels into
|
||||
the emulated framebuffer or the host presentation surface.
|
||||
- **Zero Cycle Impact:** Diagnostic processing must occur outside the emulated
|
||||
machine cycles.
|
||||
|
||||
### 10.3 Atomic Telemetry Model
|
||||
|
||||
To ensure zero-impact synchronization between the VM and the Host Debug Overlay, PROMETEU uses a **push-based atomic model**:
|
||||
To ensure zero-impact synchronization between the VM and host diagnostics, PROMETEU uses a **push-based atomic model**:
|
||||
|
||||
1. **Atomic Storage:** Metrics such as cycles, syscalls, and memory usage are stored in a dedicated `AtomicTelemetry` structure using thread-safe atomic types (`AtomicU64`, `AtomicU32`, etc.).
|
||||
2. **Lockless Access:** The Host (Desktop) reads these metrics asynchronously and without locks by taking a `snapshot()` of the atomic state.
|
||||
@ -320,4 +324,4 @@ All machine diagnostics and profiling data:
|
||||
- feed the host-owned certification report;
|
||||
- are collected deterministically;
|
||||
- do not require a guest-visible debug ABI;
|
||||
- are consistent regardless of whether the Host HUD is active or not.
|
||||
- are consistent regardless of whether host diagnostics are active or not.
|
||||
|
||||
@ -51,7 +51,8 @@ The contract is about logical behavior, not identical physical latency or throug
|
||||
- audio output
|
||||
- physical input collection
|
||||
- access to the sandbox file system
|
||||
- **technical inspection surfaces (Debug Overlay/HUD)**
|
||||
- technical inspection and debugger integrations that do not modify presented
|
||||
Game or Shell pixels
|
||||
|
||||
The host provides realization of machine surfaces. It does not redefine cartridge semantics.
|
||||
|
||||
@ -123,8 +124,8 @@ The graphics system:
|
||||
The platform layer:
|
||||
|
||||
- consumes closed render submissions through a render-surface implementation
|
||||
- **may overlay technical HUDs without modifying the logical framebuffer**
|
||||
- may transport published RGBA8888 output into a host presentation surface where a host-only overlay layer is composed
|
||||
- transports published RGBA8888 output into a host presentation surface without
|
||||
injecting host-owned debug overlay pixels
|
||||
|
||||
The host presentation layer MUST treat RGBA8888 as the canonical logical
|
||||
framebuffer format. RGB565 conversion is not part of the normal host
|
||||
@ -136,17 +137,19 @@ host-owned invalidation, not by perpetual redraw polling.
|
||||
In particular:
|
||||
|
||||
- a stable published submission MAY remain visible across multiple host wakeups without recomposition;
|
||||
- a host MAY redraw the same logical frame again when its own surface is invalidated by resize, expose, or host-only overlay/debug changes;
|
||||
- a host MAY redraw the same logical frame again when its own surface is invalidated by resize, expose, or debugger-visible host changes;
|
||||
- a host MUST NOT invent intermediate logical frames or require continuous redraw merely to discover whether a new logical frame exists.
|
||||
|
||||
## 9 Debug and Inspection Isolation
|
||||
|
||||
To preserve portability and certification purity, technical inspection tools (like the Debug Overlay) are moved to the Host layer.
|
||||
To preserve portability and certification purity, technical inspection tools are host-owned and must not inject pixels into Game or Shell output.
|
||||
|
||||
- **Host-exclusive:** These tools are only implemented where they are relevant (e.g., Desktop) and do not exist in the logical machine.
|
||||
- **Non-intrusive:** They must not consume machine cycles or alter memory state.
|
||||
- **Consistent Results:** A cartridge will produce the same logical results and certification metrics regardless of the Host's inspection capabilities.
|
||||
- **Contract Boundary:** Emulated graphics primitives such as `fill_rect` and `draw_text` remain part of the machine contract, but Host overlays must not depend on them.
|
||||
- **Contract Boundary:** Emulated graphics primitives such as `fill_rect` and
|
||||
`draw_text` remain part of the machine contract; host diagnostics must not
|
||||
depend on them for visual injection.
|
||||
|
||||
### 9.1 Atomic Telemetry Interface
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user