[PERF] Host Debug Overlay Isolation

This commit is contained in:
bQUARKz 2026-04-10 19:16:44 +01:00
parent 58a5f9a3a6
commit 4cdfaefda7
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
17 changed files with 490 additions and 402 deletions

View File

@ -2,8 +2,8 @@
use crate::memory_banks::{GlyphBankPoolInstaller, SoundBankPoolInstaller};
use prometeu_hal::AssetBridge;
use prometeu_hal::asset::{
AssetCodec, AssetEntry, AssetId, AssetLoadError, AssetOpStatus, BankStats, BankType, HandleId,
LoadStatus, PreloadEntry, SlotRef, SlotStats,
AssetCodec, AssetEntry, AssetId, AssetLoadError, AssetOpStatus, BankTelemetry, BankType,
HandleId, LoadStatus, PreloadEntry, SlotRef, SlotStats,
};
use prometeu_hal::cartridge::AssetsPayloadSource;
use prometeu_hal::color::Color;
@ -161,11 +161,6 @@ pub struct AssetManager {
/// Residency policy for sound banks.
sound_policy: BankPolicy<SoundBank>,
/// Count of occupied slots for GFX.
gfx_slots_occupied: AtomicUsize,
/// Count of occupied slots for sounds.
sound_slots_occupied: AtomicUsize,
// Commits that are ready to be applied at the next frame boundary.
pending_commits: Mutex<Vec<HandleId>>,
}
@ -206,8 +201,8 @@ impl AssetBridge for AssetManager {
fn apply_commits(&self) {
self.apply_commits()
}
fn bank_info(&self, kind: BankType) -> BankStats {
self.bank_info(kind)
fn bank_telemetry(&self) -> Vec<BankTelemetry> {
self.bank_telemetry()
}
fn slot_info(&self, slot: SlotRef) -> SlotStats {
self.slot_info(slot)
@ -302,8 +297,6 @@ impl AssetManager {
sound_slots: Arc::new(RwLock::new(std::array::from_fn(|_| None))),
gfx_policy: BankPolicy::new(),
sound_policy: BankPolicy::new(),
gfx_slots_occupied: AtomicUsize::new(0),
sound_slots_occupied: AtomicUsize::new(0),
handles: Arc::new(RwLock::new(HashMap::new())),
next_handle_id: Mutex::new(1),
assets_data: Arc::new(RwLock::new(assets_data)),
@ -732,9 +725,6 @@ impl AssetManager {
self.gfx_installer.install_glyph_bank(h.slot.index, bank);
let mut slots = self.gfx_slots.write().unwrap();
if h.slot.index < slots.len() {
if slots[h.slot.index].is_none() {
self.gfx_slots_occupied.fetch_add(1, Ordering::Relaxed);
}
slots[h.slot.index] = Some(h._asset_id);
}
h.status = LoadStatus::COMMITTED;
@ -745,9 +735,6 @@ impl AssetManager {
self.sound_installer.install_sound_bank(h.slot.index, bank);
let mut slots = self.sound_slots.write().unwrap();
if h.slot.index < slots.len() {
if slots[h.slot.index].is_none() {
self.sound_slots_occupied.fetch_add(1, Ordering::Relaxed);
}
slots[h.slot.index] = Some(h._asset_id);
}
h.status = LoadStatus::COMMITTED;
@ -759,37 +746,21 @@ impl AssetManager {
}
}
pub fn bank_info(&self, kind: BankType) -> BankStats {
match kind {
BankType::GLYPH => {
let used_bytes = self.gfx_policy.used_bytes.load(Ordering::Relaxed);
let inflight_bytes = self.gfx_policy.inflight_bytes.load(Ordering::Relaxed);
let slots_occupied = self.gfx_slots_occupied.load(Ordering::Relaxed);
pub fn bank_telemetry(&self) -> Vec<BankTelemetry> {
vec![self.bank_telemetry_for(BankType::GLYPH), self.bank_telemetry_for(BankType::SOUNDS)]
}
BankStats {
total_bytes: 16 * 1024 * 1024,
used_bytes,
free_bytes: (16usize * 1024 * 1024).saturating_sub(used_bytes),
inflight_bytes,
slot_count: 16,
slots_occupied,
}
fn bank_telemetry_for(&self, kind: BankType) -> BankTelemetry {
let used_slots = match kind {
BankType::GLYPH => {
self.gfx_slots.read().unwrap().iter().filter(|slot| slot.is_some()).count()
}
BankType::SOUNDS => {
let used_bytes = self.sound_policy.used_bytes.load(Ordering::Relaxed);
let inflight_bytes = self.sound_policy.inflight_bytes.load(Ordering::Relaxed);
let slots_occupied = self.sound_slots_occupied.load(Ordering::Relaxed);
BankStats {
total_bytes: 32 * 1024 * 1024,
used_bytes,
free_bytes: (32usize * 1024 * 1024).saturating_sub(used_bytes),
inflight_bytes,
slot_count: 16,
slots_occupied,
}
self.sound_slots.read().unwrap().iter().filter(|slot| slot.is_some()).count()
}
}
};
BankTelemetry { bank_type: kind, used_slots, total_slots: 16 }
}
pub fn slot_info(&self, slot: SlotRef) -> SlotStats {
@ -842,8 +813,6 @@ impl AssetManager {
pub fn shutdown(&self) {
self.gfx_policy.clear();
self.sound_policy.clear();
self.gfx_slots_occupied.store(0, Ordering::Relaxed);
self.sound_slots_occupied.store(0, Ordering::Relaxed);
self.handles.write().unwrap().clear();
self.pending_commits.lock().unwrap().clear();
self.gfx_slots.write().unwrap().fill(None);
@ -1196,7 +1165,6 @@ mod tests {
let width = 16;
let height = 16;
let decoded_bytes = expected_glyph_decoded_size(width, height);
let data = test_glyph_asset_data();
let am = AssetManager::new(
@ -1207,10 +1175,10 @@ mod tests {
);
// Initially zero
let info = am.bank_info(BankType::GLYPH);
assert_eq!(info.used_bytes, 0);
assert_eq!(info.inflight_bytes, 0);
assert_eq!(info.slots_occupied, 0);
let info = am.bank_telemetry();
assert_eq!(info[0].bank_type, BankType::GLYPH);
assert_eq!(info[0].used_slots, 0);
assert_eq!(info[0].total_slots, 16);
// Loading
let handle = am.load(0, 0).expect("load must allocate handle");
@ -1221,26 +1189,21 @@ mod tests {
thread::sleep(std::time::Duration::from_millis(10));
}
let info = am.bank_info(BankType::GLYPH);
// Note: put_resident happens in worker thread, then stage happens.
assert_eq!(info.used_bytes, decoded_bytes);
assert_eq!(info.inflight_bytes, decoded_bytes);
assert_eq!(info.slots_occupied, 0);
let info = am.bank_telemetry();
assert_eq!(info[0].used_slots, 0);
// Commit
am.commit(handle);
am.apply_commits();
let info = am.bank_info(BankType::GLYPH);
assert_eq!(info.used_bytes, decoded_bytes);
assert_eq!(info.inflight_bytes, 0);
assert_eq!(info.slots_occupied, 1);
let info = am.bank_telemetry();
assert_eq!(info[0].used_slots, 1);
assert_eq!(info[1].bank_type, BankType::SOUNDS);
assert_eq!(info[1].used_slots, 0);
// Shutdown resets
am.shutdown();
let info = am.bank_info(BankType::GLYPH);
assert_eq!(info.used_bytes, 0);
assert_eq!(info.inflight_bytes, 0);
assert_eq!(info.slots_occupied, 0);
let info = am.bank_telemetry();
assert_eq!(info[0].used_slots, 0);
}
}

View File

@ -105,13 +105,10 @@ pub enum AssetOpStatus {
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BankStats {
pub total_bytes: usize,
pub used_bytes: usize,
pub free_bytes: usize,
pub inflight_bytes: usize,
pub slot_count: usize,
pub slots_occupied: usize,
pub struct BankTelemetry {
pub bank_type: BankType,
pub used_slots: usize,
pub total_slots: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]

View File

@ -1,5 +1,5 @@
use crate::asset::{
AssetEntry, AssetId, AssetLoadError, AssetOpStatus, BankStats, BankType, HandleId, LoadStatus,
AssetEntry, AssetId, AssetLoadError, AssetOpStatus, BankTelemetry, HandleId, LoadStatus,
PreloadEntry, SlotRef, SlotStats,
};
use crate::cartridge::AssetsPayloadSource;
@ -16,7 +16,7 @@ pub trait AssetBridge {
fn commit(&self, handle: HandleId) -> AssetOpStatus;
fn cancel(&self, handle: HandleId) -> AssetOpStatus;
fn apply_commits(&self);
fn bank_info(&self, kind: BankType) -> BankStats;
fn bank_telemetry(&self) -> Vec<BankTelemetry>;
fn slot_info(&self, slot: SlotRef) -> SlotStats;
fn shutdown(&self);
}

View File

@ -64,12 +64,10 @@ pub enum DebugEvent {
cycles_budget: u64,
host_cpu_time_us: u64,
violations: u32,
gfx_used_bytes: usize,
gfx_inflight_bytes: usize,
gfx_slots_occupied: u32,
audio_used_bytes: usize,
audio_inflight_bytes: usize,
audio_slots_occupied: u32,
glyph_slots_used: u32,
glyph_slots_total: u32,
sound_slots_used: u32,
sound_slots_total: u32,
},
#[serde(rename = "fault")]
Fault {
@ -97,12 +95,10 @@ mod tests {
cycles_budget: 10000,
host_cpu_time_us: 1200,
violations: 0,
gfx_used_bytes: 1024,
gfx_inflight_bytes: 0,
gfx_slots_occupied: 1,
audio_used_bytes: 2048,
audio_inflight_bytes: 0,
audio_slots_occupied: 2,
glyph_slots_used: 1,
glyph_slots_total: 16,
sound_slots_used: 2,
sound_slots_total: 16,
};
let json = serde_json::to_string(&event).unwrap();

View File

@ -13,15 +13,11 @@ pub struct TelemetryFrame {
pub completed_logical_frames: u32,
pub violations: u32,
// GFX Banks
pub gfx_used_bytes: usize,
pub gfx_inflight_bytes: usize,
pub gfx_slots_occupied: u32,
// Audio Banks
pub audio_used_bytes: usize,
pub audio_inflight_bytes: usize,
pub audio_slots_occupied: u32,
// Bank telemetry
pub glyph_slots_used: u32,
pub glyph_slots_total: u32,
pub sound_slots_used: u32,
pub sound_slots_total: u32,
// RAM (Heap)
pub heap_used_bytes: usize,
@ -44,15 +40,11 @@ pub struct AtomicTelemetry {
pub completed_logical_frames: AtomicU32,
pub violations: AtomicU32,
// GFX Banks
pub gfx_used_bytes: AtomicUsize,
pub gfx_inflight_bytes: AtomicUsize,
pub gfx_slots_occupied: AtomicU32,
// Audio Banks
pub audio_used_bytes: AtomicUsize,
pub audio_inflight_bytes: AtomicUsize,
pub audio_slots_occupied: AtomicU32,
// Bank telemetry
pub glyph_slots_used: AtomicU32,
pub glyph_slots_total: AtomicU32,
pub sound_slots_used: AtomicU32,
pub sound_slots_total: AtomicU32,
// RAM (Heap)
pub heap_used_bytes: AtomicUsize,
@ -79,12 +71,10 @@ impl AtomicTelemetry {
host_cpu_time_us: self.host_cpu_time_us.load(Ordering::Relaxed),
completed_logical_frames: self.completed_logical_frames.load(Ordering::Relaxed),
violations: self.violations.load(Ordering::Relaxed),
gfx_used_bytes: self.gfx_used_bytes.load(Ordering::Relaxed),
gfx_inflight_bytes: self.gfx_inflight_bytes.load(Ordering::Relaxed),
gfx_slots_occupied: self.gfx_slots_occupied.load(Ordering::Relaxed),
audio_used_bytes: self.audio_used_bytes.load(Ordering::Relaxed),
audio_inflight_bytes: self.audio_inflight_bytes.load(Ordering::Relaxed),
audio_slots_occupied: self.audio_slots_occupied.load(Ordering::Relaxed),
glyph_slots_used: self.glyph_slots_used.load(Ordering::Relaxed),
glyph_slots_total: self.glyph_slots_total.load(Ordering::Relaxed),
sound_slots_used: self.sound_slots_used.load(Ordering::Relaxed),
sound_slots_total: self.sound_slots_total.load(Ordering::Relaxed),
heap_used_bytes: self.heap_used_bytes.load(Ordering::Relaxed),
heap_max_bytes: self.heap_max_bytes.load(Ordering::Relaxed),
logs_count: self.logs_count.load(Ordering::Relaxed),
@ -99,12 +89,10 @@ impl AtomicTelemetry {
self.host_cpu_time_us.store(0, Ordering::Relaxed);
self.completed_logical_frames.store(0, Ordering::Relaxed);
self.violations.store(0, Ordering::Relaxed);
self.gfx_used_bytes.store(0, Ordering::Relaxed);
self.gfx_inflight_bytes.store(0, Ordering::Relaxed);
self.gfx_slots_occupied.store(0, Ordering::Relaxed);
self.audio_used_bytes.store(0, Ordering::Relaxed);
self.audio_inflight_bytes.store(0, Ordering::Relaxed);
self.audio_slots_occupied.store(0, Ordering::Relaxed);
self.glyph_slots_used.store(0, Ordering::Relaxed);
self.glyph_slots_total.store(0, Ordering::Relaxed);
self.sound_slots_used.store(0, Ordering::Relaxed);
self.sound_slots_total.store(0, Ordering::Relaxed);
self.heap_used_bytes.store(0, Ordering::Relaxed);
self.vm_steps.store(0, Ordering::Relaxed);
self.logs_count.store(0, Ordering::Relaxed);
@ -118,8 +106,8 @@ pub struct CertificationConfig {
pub cycles_budget_per_frame: Option<u64>,
pub max_syscalls_per_frame: Option<u32>,
pub max_host_cpu_us_per_frame: Option<u64>,
pub max_gfx_bytes: Option<usize>,
pub max_audio_bytes: Option<usize>,
pub max_glyph_slots_used: Option<u32>,
pub max_sound_slots_used: Option<u32>,
pub max_heap_bytes: Option<usize>,
pub max_logs_per_frame: Option<u32>,
}
@ -199,9 +187,9 @@ impl Certifier {
violations += 1;
}
// 4. GFX Memory
if let Some(limit) = self.config.max_gfx_bytes
&& telemetry.gfx_used_bytes > limit
// 4. GLYPH bank slots
if let Some(limit) = self.config.max_glyph_slots_used
&& telemetry.glyph_slots_used > limit
{
log_service.log(
ts_ms,
@ -210,16 +198,16 @@ impl Certifier {
LogSource::Pos,
0xCA04,
format!(
"Cert: GFX bank exceeded memory limit ({} > {})",
telemetry.gfx_used_bytes, limit
"Cert: GLYPH bank exceeded slot limit ({} > {})",
telemetry.glyph_slots_used, limit
),
);
violations += 1;
}
// 5. Audio Memory
if let Some(limit) = self.config.max_audio_bytes
&& telemetry.audio_used_bytes > limit
// 5. SOUNDS bank slots
if let Some(limit) = self.config.max_sound_slots_used
&& telemetry.sound_slots_used > limit
{
log_service.log(
ts_ms,
@ -228,8 +216,8 @@ impl Certifier {
LogSource::Pos,
0xCA05,
format!(
"Cert: Audio bank exceeded memory limit ({} > {})",
telemetry.audio_used_bytes, limit
"Cert: SOUNDS bank exceeded slot limit ({} > {})",
telemetry.sound_slots_used, limit
),
);
violations += 1;
@ -285,7 +273,7 @@ mod tests {
cycles_budget_per_frame: Some(100),
max_syscalls_per_frame: Some(5),
max_host_cpu_us_per_frame: Some(1000),
max_gfx_bytes: Some(1024),
max_glyph_slots_used: Some(1),
..Default::default()
};
let cert = Certifier::new(config);
@ -294,7 +282,7 @@ mod tests {
tel.cycles_used = 150;
tel.syscalls = 10;
tel.host_cpu_time_us = 500;
tel.gfx_used_bytes = 2048;
tel.glyph_slots_used = 2;
let violations = cert.evaluate(&tel, &mut ls, 1000);
assert_eq!(violations, 3);
@ -303,7 +291,7 @@ mod tests {
assert_eq!(logs.len(), 3);
assert!(logs[0].msg.contains("cycles_used"));
assert!(logs[1].msg.contains("syscalls"));
assert!(logs[2].msg.contains("GFX bank"));
assert!(logs[2].msg.contains("GLYPH bank"));
}
#[test]

View File

@ -486,8 +486,17 @@ impl NativeInterface for VirtualMachineRuntime {
1 => BankType::SOUNDS,
_ => return Err(VmFault::Trap(TRAP_TYPE, "Invalid asset type".to_string())),
};
let json =
serde_json::to_string(&hw.assets().bank_info(asset_type)).unwrap_or_default();
let telemetry = hw
.assets()
.bank_telemetry()
.into_iter()
.find(|entry| entry.bank_type == asset_type)
.unwrap_or(prometeu_hal::asset::BankTelemetry {
bank_type: asset_type,
used_slots: 0,
total_slots: 0,
});
let json = serde_json::to_string(&telemetry).unwrap_or_default();
ret.push_string(json);
Ok(())
}

View File

@ -1,12 +1,26 @@
use super::*;
use crate::CrashReport;
use prometeu_hal::asset::BankType;
use prometeu_hal::asset::{BankTelemetry, BankType};
use prometeu_hal::log::{LogLevel, LogSource};
use prometeu_hal::{HardwareBridge, HostContext, InputSignals};
use prometeu_vm::LogicalFrameEndingReason;
use std::sync::atomic::Ordering;
impl VirtualMachineRuntime {
fn bank_telemetry_summary(hw: &dyn HardwareBridge) -> (BankTelemetry, BankTelemetry) {
let telemetry = hw.assets().bank_telemetry();
let glyph =
telemetry.iter().find(|entry| entry.bank_type == BankType::GLYPH).cloned().unwrap_or(
BankTelemetry { bank_type: BankType::GLYPH, used_slots: 0, total_slots: 0 },
);
let sounds =
telemetry.iter().find(|entry| entry.bank_type == BankType::SOUNDS).cloned().unwrap_or(
BankTelemetry { bank_type: BankType::SOUNDS, used_slots: 0, total_slots: 0 },
);
(glyph, sounds)
}
pub fn debug_step_instruction(
&mut self,
vm: &mut VirtualMachine,
@ -134,27 +148,19 @@ impl VirtualMachineRuntime {
hw.gfx_mut().render_all();
// 1. Snapshot full telemetry at logical frame end
let gfx_stats = hw.assets().bank_info(BankType::GLYPH);
let (glyph_bank, sound_bank) = Self::bank_telemetry_summary(hw);
self.atomic_telemetry
.gfx_used_bytes
.store(gfx_stats.used_bytes, Ordering::Relaxed);
.glyph_slots_used
.store(glyph_bank.used_slots as u32, Ordering::Relaxed);
self.atomic_telemetry
.gfx_inflight_bytes
.store(gfx_stats.inflight_bytes, Ordering::Relaxed);
.glyph_slots_total
.store(glyph_bank.total_slots as u32, Ordering::Relaxed);
self.atomic_telemetry
.gfx_slots_occupied
.store(gfx_stats.slots_occupied as u32, Ordering::Relaxed);
let audio_stats = hw.assets().bank_info(BankType::SOUNDS);
.sound_slots_used
.store(sound_bank.used_slots as u32, Ordering::Relaxed);
self.atomic_telemetry
.audio_used_bytes
.store(audio_stats.used_bytes, Ordering::Relaxed);
self.atomic_telemetry
.audio_inflight_bytes
.store(audio_stats.inflight_bytes, Ordering::Relaxed);
self.atomic_telemetry
.audio_slots_occupied
.store(audio_stats.slots_occupied as u32, Ordering::Relaxed);
.sound_slots_total
.store(sound_bank.total_slots as u32, Ordering::Relaxed);
self.atomic_telemetry
.heap_used_bytes
@ -165,7 +171,9 @@ impl VirtualMachineRuntime {
let current_frame_logs =
self.atomic_telemetry.current_logs_count.load(Ordering::Relaxed);
self.atomic_telemetry.logs_count.store(current_frame_logs, Ordering::Relaxed);
self.atomic_telemetry
.logs_count
.store(current_frame_logs, Ordering::Relaxed);
let ts_ms = self.boot_time.elapsed().as_millis() as u64;
let telemetry_snapshot = self.atomic_telemetry.snapshot();
@ -210,23 +218,19 @@ impl VirtualMachineRuntime {
// 2. High-frequency telemetry update (only if inspection is active)
if self.inspection_active {
let gfx_stats = hw.assets().bank_info(BankType::GLYPH);
self.atomic_telemetry.gfx_used_bytes.store(gfx_stats.used_bytes, Ordering::Relaxed);
let (glyph_bank, sound_bank) = Self::bank_telemetry_summary(hw);
self.atomic_telemetry
.gfx_inflight_bytes
.store(gfx_stats.inflight_bytes, Ordering::Relaxed);
.glyph_slots_used
.store(glyph_bank.used_slots as u32, Ordering::Relaxed);
self.atomic_telemetry
.gfx_slots_occupied
.store(gfx_stats.slots_occupied as u32, Ordering::Relaxed);
let audio_stats = hw.assets().bank_info(BankType::SOUNDS);
self.atomic_telemetry.audio_used_bytes.store(audio_stats.used_bytes, Ordering::Relaxed);
.glyph_slots_total
.store(glyph_bank.total_slots as u32, Ordering::Relaxed);
self.atomic_telemetry
.audio_inflight_bytes
.store(audio_stats.inflight_bytes, Ordering::Relaxed);
.sound_slots_used
.store(sound_bank.used_slots as u32, Ordering::Relaxed);
self.atomic_telemetry
.audio_slots_occupied
.store(audio_stats.slots_occupied as u32, Ordering::Relaxed);
.sound_slots_total
.store(sound_bank.total_slots as u32, Ordering::Relaxed);
self.atomic_telemetry
.heap_used_bytes

View File

@ -272,6 +272,16 @@ impl HostDebugger {
tel.host_cpu_time_us,
cert_config.max_host_cpu_us_per_frame.unwrap_or(0),
),
0xCA04 => (
"max_glyph_slots_used".to_string(),
tel.glyph_slots_used as u64,
cert_config.max_glyph_slots_used.unwrap_or(0) as u64,
),
0xCA05 => (
"max_sound_slots_used".to_string(),
tel.sound_slots_used as u64,
cert_config.max_sound_slots_used.unwrap_or(0) as u64,
),
_ => ("unknown".to_string(), 0, 0),
};
@ -302,12 +312,10 @@ impl HostDebugger {
cycles_budget: tel.cycles_budget,
host_cpu_time_us: tel.host_cpu_time_us,
violations: tel.violations,
gfx_used_bytes: tel.gfx_used_bytes,
gfx_inflight_bytes: tel.gfx_inflight_bytes,
gfx_slots_occupied: tel.gfx_slots_occupied,
audio_used_bytes: tel.audio_used_bytes,
audio_inflight_bytes: tel.audio_inflight_bytes,
audio_slots_occupied: tel.audio_slots_occupied,
glyph_slots_used: tel.glyph_slots_used,
glyph_slots_total: tel.glyph_slots_total,
sound_slots_used: tel.sound_slots_used,
sound_slots_total: tel.sound_slots_total,
});
self.last_telemetry_frame = current_frame;
}

View File

@ -42,14 +42,30 @@ pub(crate) struct OverlaySnapshot {
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 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)
&& let Some(event) =
recent_logs.into_iter().rev().find(|e| e.tag >= 0xCA01 && e.tag <= 0xCA07)
{
footer.push(OverlayMetric {
label: "CERT",
@ -75,8 +91,8 @@ pub(crate) fn capture_snapshot(stats: &HostStats, firmware: &Firmware) -> Overla
.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 gfx_ratio = ratio(tel.gfx_slots_occupied as u64, 16);
let audio_ratio = ratio(tel.audio_slots_occupied as u64, 16);
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![
@ -98,23 +114,11 @@ pub(crate) fn capture_snapshot(stats: &HostStats, firmware: &Firmware) -> Overla
value: format!("{:.2}ms", stats.average_host_cpu_ms()),
warn: false,
},
OverlayMetric {
label: "STEPS",
value: tel.vm_steps.to_string(),
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,
},
OverlayMetric { label: "SYSC", value: tel.syscalls.to_string(), warn: false },
OverlayMetric { label: "LOGS", value: tel.logs_count.to_string(), warn: false },
),
],
bars: vec![
@ -136,36 +140,27 @@ pub(crate) fn capture_snapshot(stats: &HostStats, firmware: &Firmware) -> Overla
label: "BUDGET",
value: if tel.cycles_budget > 0 {
format!(
"{:.1}K/{:.1}K {:.1}%",
"{:.1}K/{:.1}K",
tel.cycles_used as f64 / 1000.0,
tel.cycles_budget as f64 / 1000.0,
cycles_ratio * 100.0
tel.cycles_budget as f64 / 1000.0
)
} else {
"0.0K/0.0K 0.0%".to_string()
"0.0K/0.0K".to_string()
},
ratio: cycles_ratio,
warn: cycles_ratio >= 0.9,
},
OverlayBar {
label: "GFX",
value: format!(
"{} / 16 slots {}K",
tel.gfx_slots_occupied,
tel.gfx_used_bytes.div_ceil(1024)
),
ratio: gfx_ratio,
warn: tel.gfx_inflight_bytes > 0,
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: "AUD",
value: format!(
"{} / 16 slots {}K",
tel.audio_slots_occupied,
tel.audio_used_bytes.div_ceil(1024)
),
ratio: audio_ratio,
warn: tel.audio_inflight_bytes > 0,
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,
@ -178,44 +173,58 @@ pub(crate) fn draw_overlay(
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(frame, frame_width, frame_height, PANEL_X, PANEL_Y, PANEL_WIDTH, panel_height, BG);
stroke_rect(frame, frame_width, frame_height, PANEL_X, PANEL_Y, PANEL_WIDTH, panel_height, BORDER);
fill_rect_alpha(&mut canvas, panel_rect, BG);
stroke_rect(&mut canvas, panel_rect, BORDER);
let mut y = PANEL_Y + PANEL_PADDING;
for (left, right) in &snapshot.rows {
draw_metric_pair(frame, frame_width, frame_height, y, left, right);
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(frame, frame_width, frame_height, PANEL_X + PANEL_PADDING, y, bar.label, DIM);
draw_text(frame, frame_width, frame_height, PANEL_X + 48, y, &bar.value, color);
draw_text(
canvas.frame,
canvas.width,
canvas.height,
PANEL_X + PANEL_PADDING,
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;
fill_rect(frame, frame_width, frame_height, bar_x, y, BAR_WIDTH, BAR_HEIGHT, BAR_BG);
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(
frame,
frame_width,
frame_height,
bar_x,
y,
fill_width,
BAR_HEIGHT,
&mut canvas,
Rect { x: bar_x, y, width: fill_width, height: BAR_HEIGHT },
if bar.warn { BAR_WARN } else { BAR_FILL },
);
stroke_rect(frame, frame_width, frame_height, bar_x, y, BAR_WIDTH, BAR_HEIGHT, BORDER);
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(frame, frame_width, frame_height, PANEL_X + PANEL_PADDING, y, line.label, DIM);
draw_text(frame, frame_width, frame_height, PANEL_X + 48, y, &line.value, color);
draw_text(
canvas.frame,
canvas.width,
canvas.height,
PANEL_X + PANEL_PADDING,
y,
line.label,
DIM,
);
draw_text(canvas.frame, canvas.width, canvas.height, PANEL_X + 48, y, &line.value, color);
y += LINE_HEIGHT;
}
}
@ -260,7 +269,15 @@ fn truncate_value(value: &str, max_len: usize) -> String {
upper
}
fn draw_text(frame: &mut [u8], frame_width: usize, frame_height: usize, x: usize, y: usize, text: &str, color: [u8; 4]) {
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' {
@ -271,72 +288,71 @@ fn draw_text(frame: &mut [u8], frame_width: usize, frame_height: usize, x: usize
}
}
fn draw_char(frame: &mut [u8], frame_width: usize, frame_height: usize, x: usize, y: usize, ch: char, color: [u8; 4]) {
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 {
fill_rect(frame, frame_width, frame_height, x + col * CHAR_SCALE, y + row * CHAR_SCALE, CHAR_SCALE, CHAR_SCALE, color);
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(
frame: &mut [u8],
frame_width: usize,
frame_height: usize,
x: usize,
y: usize,
width: usize,
height: usize,
color: [u8; 4],
) {
let max_x = (x + width).min(frame_width);
let max_y = (y + height).min(frame_height);
for py in y..max_y {
for px in x..max_x {
blend_pixel(frame, frame_width, px, py, 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(
frame: &mut [u8],
frame_width: usize,
frame_height: usize,
x: usize,
y: usize,
width: usize,
height: usize,
color: [u8; 4],
) {
let max_x = (x + width).min(frame_width);
let max_y = (y + height).min(frame_height);
for py in y..max_y {
for px in x..max_x {
write_pixel(frame, frame_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(
frame: &mut [u8],
frame_width: usize,
frame_height: usize,
x: usize,
y: usize,
width: usize,
height: usize,
color: [u8; 4],
) {
if width == 0 || height == 0 {
fn stroke_rect(canvas: &mut FrameCanvas<'_>, rect: Rect, color: [u8; 4]) {
if rect.width == 0 || rect.height == 0 {
return;
}
fill_rect(frame, frame_width, frame_height, x, y, width, 1, color);
fill_rect(frame, frame_width, frame_height, x, y + height.saturating_sub(1), width, 1, color);
fill_rect(frame, frame_width, frame_height, x, y, 1, height, color);
fill_rect(frame, frame_width, frame_height, x + width.saturating_sub(1), y, 1, height, color);
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]) {
@ -426,18 +442,20 @@ mod tests {
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 90.0%".to_string(),
ratio: 0.9,
warn: true,
}],
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(),

View File

@ -122,7 +122,6 @@ impl HostRunner {
w.request_redraw();
}
}
}
impl ApplicationHandler for HostRunner {

View File

@ -45,8 +45,10 @@ impl HostStats {
pub fn record_host_cpu_time(&mut self, us: u64) {
self.recent_host_cpu_us[self.recent_host_cpu_cursor] = us;
self.recent_host_cpu_cursor = (self.recent_host_cpu_cursor + 1) % self.recent_host_cpu_us.len();
self.recent_host_cpu_count = self.recent_host_cpu_count.saturating_add(1).min(self.recent_host_cpu_us.len());
self.recent_host_cpu_cursor =
(self.recent_host_cpu_cursor + 1) % self.recent_host_cpu_us.len();
self.recent_host_cpu_count =
self.recent_host_cpu_count.saturating_add(1).min(self.recent_host_cpu_us.len());
}
pub fn average_host_cpu_ms(&self) -> f64 {

View File

@ -16,7 +16,7 @@
{"type":"discussion","id":"DSC-0011","status":"open","ticket":"perf-gfx-render-pipeline-and-dirty-regions","title":"Agenda - [PERF] GFX Render Pipeline and Dirty Regions","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0010","file":"workflow/agendas/AGD-0010-perf-gfx-render-pipeline-and-dirty-regions.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0012","status":"open","ticket":"perf-runtime-introspection-syscalls","title":"Agenda - [PERF] Runtime Introspection Syscalls","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0011","file":"workflow/agendas/AGD-0011-perf-runtime-introspection-syscalls.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0013","status":"open","ticket":"perf-host-debug-overlay-isolation","title":"Agenda - [PERF] Host Debug Overlay Isolation","created_at":"2026-03-27","updated_at":"2026-04-10","tags":[],"agendas":[{"id":"AGD-0012","file":"workflow/agendas/AGD-0012-perf-host-debug-overlay-isolation.md","status":"done","created_at":"2026-03-27","updated_at":"2026-04-10"},{"id":"AGD-0022","file":"AGD-0022-host-overlay-tooling-boundary-revision.md","status":"accepted","created_at":"2026-04-10","updated_at":"2026-04-10"},{"id":"AGD-0023","file":"AGD-0023-overlay-log-metric-last-frame.md","status":"accepted","created_at":"2026-04-10","updated_at":"2026-04-10"}],"decisions":[{"id":"DEC-0007","file":"workflow/decisions/DEC-0007-perf-host-debug-overlay-isolation.md","status":"accepted","created_at":"2026-04-10","updated_at":"2026-04-10"},{"id":"DEC-0009","file":"DEC-0009-host-overlay-tooling-boundary.md","status":"accepted","created_at":"2026-04-10","updated_at":"2026-04-10","ref_agenda":"AGD-0022"},{"id":"DEC-0010","file":"DEC-0010-overlay-log-metric-last-frame.md","status":"accepted","created_at":"2026-04-10","updated_at":"2026-04-10","ref_agenda":"AGD-0023"}],"plans":[{"id":"PLN-0006","file":"workflow/plans/PLN-0006-perf-host-debug-overlay-isolation.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"},{"id":"PLN-0008","file":"PLN-0008-host-overlay-native-composition-alignment.md","status":"in_progress","created_at":"2026-04-10","updated_at":"2026-04-10","ref_decisions":["DEC-0007","DEC-0008","DEC-0009","DEC-0010"]}],"lessons":[{"id":"LSN-0027","file":"lessons/DSC-0013-perf-host-debug-overlay-isolation/LSN-0027-host-debug-overlay-isolation.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
{"type":"discussion","id":"DSC-0024","status":"open","ticket":"generic-memory-bank-slot-contract","title":"Agenda - Generic Memory Bank Slot Contract","created_at":"2026-04-10","updated_at":"2026-04-10","tags":["runtime","asset","memory-bank","slots","host"],"agendas":[{"id":"AGD-0024","file":"AGD-0024-generic-memory-bank-slot-contract.md","status":"accepted","created_at":"2026-04-10","updated_at":"2026-04-10"}],"decisions":[{"id":"DEC-0012","file":"DEC-0012-asset-manager-bank-telemetry-slot-contract.md","status":"accepted","created_at":"2026-04-10","updated_at":"2026-04-10","ref_agenda":"AGD-0024"}],"plans":[{"id":"PLN-0010","file":"PLN-0010-asset-manager-bank-telemetry-slot-contract.md","status":"open","created_at":"2026-04-10","updated_at":"2026-04-10","ref_decisions":["DEC-0012"]}],"lessons":[]}
{"type":"discussion","id":"DSC-0024","status":"open","ticket":"generic-memory-bank-slot-contract","title":"Agenda - Generic Memory Bank Slot Contract","created_at":"2026-04-10","updated_at":"2026-04-10","tags":["runtime","asset","memory-bank","slots","host"],"agendas":[{"id":"AGD-0024","file":"AGD-0024-generic-memory-bank-slot-contract.md","status":"accepted","created_at":"2026-04-10","updated_at":"2026-04-10"}],"decisions":[{"id":"DEC-0012","file":"DEC-0012-asset-manager-bank-telemetry-slot-contract.md","status":"accepted","created_at":"2026-04-10","updated_at":"2026-04-10","ref_agenda":"AGD-0024"}],"plans":[{"id":"PLN-0010","file":"PLN-0010-asset-manager-bank-telemetry-slot-contract.md","status":"in_progress","created_at":"2026-04-10","updated_at":"2026-04-10","ref_decisions":["DEC-0012"]}],"lessons":[]}
{"type":"discussion","id":"DSC-0014","status":"open","ticket":"perf-vm-allocation-and-copy-pressure","title":"Agenda - [PERF] VM Allocation and Copy Pressure","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0013","file":"workflow/agendas/AGD-0013-perf-vm-allocation-and-copy-pressure.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0015","status":"open","ticket":"perf-cartridge-boot-and-program-ownership","title":"Agenda - [PERF] Cartridge Boot and Program Ownership","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0014","file":"workflow/agendas/AGD-0014-perf-cartridge-boot-and-program-ownership.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0016","status":"done","ticket":"tilemap-empty-cell-vs-tile-id-zero","title":"Tilemap Empty Cell vs Tile ID Zero","created_at":"2026-03-27","updated_at":"2026-04-09","tags":[],"agendas":[{"id":"AGD-0015","file":"workflow/agendas/AGD-0015-tilemap-empty-cell-vs-tile-id-zero.md","status":"done","created_at":"2026-03-27","updated_at":"2026-04-09"}],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0022","file":"lessons/DSC-0016-tilemap-empty-cell-semantics/LSN-0022-tilemap-empty-cell-convergence.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]}

View File

@ -82,13 +82,13 @@ Seguir com a **Opção A**, com os seguintes princípios:
2. O contrato exposto deve usar somente `used_slots` e `total_slots`.
3. `BankPolicy` e `BankStats` devem ser removidos por completo.
4. `MemoryBanks` deve possuir um contrato específico e explícito para ocupação por slots.
5. O host overlay deve consumir esse modelo genérico sem saber detalhes especiais de `GLYPH` vs `SOUND`, além de rótulo e contagem.
5. O host overlay deve consumir esse modelo genérico sem saber detalhes especiais de `GLYPH` vs `SOUNDS`, além de rótulo e contagem.
6. A revisão deve evitar abstração vazia: o contrato genérico precisa mapear diretamente para `GlyphBank` e `SoundBank`.
Nomenclatura canônica acordada para a camada genérica:
- `GLYPH`
- `SOUND`
- `SOUNDS`
`GFX` e `AUD` não devem ser usados como nomes canônicos do contrato genérico de banks, pois são apelidos de apresentação e não os nomes corretos do domínio.

View File

@ -15,19 +15,22 @@ tags: [runtime, asset, memory-bank, slots, host, telemetry]
**Accepted**
## Contexto
O modelo atual de banks ainda carrega forte semântica orientada a bytes e estruturas antigas como `BankPolicy` e `BankStats`, enquanto o consumo real desejado pelo host e pela inspeção técnica é orientado a slots. Além disso, os bytes historicamente usados nesse caminho foram considerados inadequados para sustentar o novo contrato de telemetria de banks.
AGD-0024 fechou a mudança do contrato de banks de um modelo orientado a bytes para um modelo orientado a slots. A agenda também fechou que `BankPolicy` e `BankStats` não são base aceitável para a nova superfície visível e que a telemetria operacional consumida por host e overlay deve sair diretamente do `AssetManager`.
Durante a discussão, ficou explícito que:
Os pontos que a agenda resolveu e que esta decisão precisa cristalizar sem ambiguidade são:
- o resumo visível de banks deve ser exposto diretamente pelo `AssetManager`;
- a telemetria de banks deve usar somente o enum do tipo do bank;
- a semântica operacional deve ser `used_slots / total_slots`;
- `GFX` e `AUD` não são nomes canônicos do domínio; os nomes corretos são `GLYPH` e `SOUND`;
- a certificação de banks deve deixar de usar limites em bytes e passar a usar limites por slots.
- o contrato visível é `slot-first`;
- o resumo visível por bank usa apenas `bank_type`, `used_slots` e `total_slots`;
- o detalhamento operacional ocorre por enum de slot;
- `GLYPH` e `SOUNDS` são os nomes canônicos do domínio;
- `GFX` e `AUD` não são nomes canônicos do contrato;
- a certificação deixa de usar bytes e passa a usar limites por slots;
- a genericidade principal do contrato visível não reside em `MemoryBanks`.
## Decisao
1. O contrato visível de telemetria de banks MUST be exposed by `AssetManager`.
2. O resumo de bank MUST use the following structure shape:
1. O contrato visível de telemetria de banks MUST ser exposto diretamente por `AssetManager`.
2. O contrato visível de telemetria de banks MUST NOT ter `MemoryBanks` como superfície principal de exposição. `MemoryBanks` MAY manter detalhes internos de suporte, mas o ponto canônico de consumo visível do sistema SHALL ser `AssetManager`.
3. O resumo canônico por bank MUST usar somente os campos `bank_type`, `used_slots` e `total_slots`, no formato:
```rust
pub struct BankTelemetry {
@ -37,42 +40,53 @@ pub struct BankTelemetry {
}
```
3. O `bank_type` MUST use canonical domain names. For the current banks, the canonical names are `GLYPH` and `SOUND`.
4. O contrato de telemetria de banks MUST be slot-first and MUST NOT depend on byte counts.
5. `BankPolicy` e `BankStats` MUST be removed completely from the bank telemetry contract path.
6. O host overlay MUST consume `AssetManager` bank telemetry instead of hardcoded bank-specific logic.
7. As regras de certificação `max_gfx_bytes` e `max_audio_bytes` MUST be removed.
8. A certificação de banks MUST migrate to slot-based limits, using canonical bank-specific slot limits such as:
4. O contrato visível de telemetria de banks MUST ser `slot-first` e MUST NOT depender de contagens de bytes.
5. `bank_type` MUST usar nomes canônicos do domínio. Para os banks atuais, os nomes canônicos são `GLYPH` e `SOUNDS`.
6. `GFX` e `AUD` MUST NOT aparecer como nomes canônicos do contrato genérico de banks. Eles MAY existir apenas como rótulos de apresentação, quando estritamente necessário fora do contrato canônico.
7. `BankPolicy` e `BankStats` MUST ser removidos completamente do caminho do contrato visível de telemetria de banks e MUST NOT permanecer como modelagem principal desse domínio exposto.
8. O overlay do host MUST consumir a telemetria exposta por `AssetManager` e MUST NOT manter lógica hardcoded dependente de bancos específicos para calcular ocupação principal.
9. O detalhamento operacional por slot MUST ser definido por enum de slot, e qualquer inspeção detalhada de ocupação SHALL seguir esse modelo, não um detalhamento baseado em bytes.
10. As regras de certificação `max_gfx_bytes` e `max_audio_bytes` MUST ser removidas.
11. A certificação de banks MUST migrar para limites por slots, usando nomes canônicos de bank, tais como:
- `max_glyph_slots_used`
- `max_sound_slots_used`
9. Any remaining byte-based accounting MAY survive only as internal implementation detail if strictly necessary, but it MUST NOT remain part of the exposed bank telemetry contract.
12. Qualquer contabilização residual em bytes MAY sobreviver apenas como detalhe interno de implementação, quando estritamente necessária, e MUST NOT permanecer no contrato visível de telemetria de banks.
## Rationale
- Slots são a unidade operacional correta para entender ocupação de banks.
- O `AssetManager` já é o lugar que conhece carregamento, commit e ocupação prática dos banks.
- Remover bytes do contrato elimina uma fonte de telemetria considerada enganosa.
- `GLYPH` e `SOUND` preservam a linguagem correta do domínio e evitam apelidos frágeis na interface.
- Certificação por slots mantém coerência entre contrato exposto, overlay e limites técnicos.
- Slots são a unidade operacional real percebida por host, overlay e depuração de banks.
- `AssetManager` é o ponto que já conhece a ocupação efetiva dos banks e, portanto, é o lugar correto para expor o resumo visível.
- Remover bytes do contrato visível evita uma telemetria enganosa e elimina a ambiguidade que a agenda abriu para resolver.
- Fixar `GLYPH` e `SOUNDS` como nomes canônicos evita contaminar o contrato com apelidos de apresentação.
- Fixar enum de slot como detalhamento operacional evita reabrir a discussão sob outra forma durante a implementação.
- Migrar certificação para slots mantém o contrato exposto, o overlay e os limites técnicos usando a mesma semântica.
## Invariantes / Contrato
- A telemetria de banks exposta ao restante do sistema sai do `AssetManager`.
- Cada entrada de bank telemetria informa somente tipo do bank, slots usados e slots totais.
- O contrato canônico usa `GLYPH` e `SOUND`.
- A telemetria de banks exposta ao restante do sistema sai de `AssetManager`.
- O contrato visível principal não reside em `MemoryBanks`.
- Cada entrada de telemetria de bank informa somente `bank_type`, `used_slots` e `total_slots`.
- O contrato canônico usa `GLYPH` e `SOUNDS`.
- O detalhamento operacional de slots usa enum de slot.
- O overlay não depende de bytes para mostrar ocupação de banks.
- A certificação de banks não depende de bytes.
## Impactos
- **AssetManager:** precisa expor `Vec<BankTelemetry>` ou equivalente direto.
- **HAL / Bridges:** precisam alinhar interfaces consumidoras ao contrato slot-first.
- **Overlay:** deve iterar a telemetria de banks do `AssetManager`.
- **AssetManager:** deve expor `Vec<BankTelemetry>` ou equivalente direto como superfície canônica visível.
- **MemoryBanks:** pode continuar como suporte interno, mas não deve concentrar o contrato visível principal de telemetria.
- **HAL / Bridges:** precisam alinhar qualquer interface consumidora ao contrato `slot-first`.
- **Overlay:** deve iterar a telemetria de banks do `AssetManager` e parar de recomputar a ocupação principal a partir de bytes.
- **Certifier:** precisa trocar limites de bytes por limites de slots para banks.
- **Legado:** `BankPolicy`, `BankStats`, e caminhos ancorados em `max_gfx_bytes` / `max_audio_bytes` precisam ser removidos ou recolocados fora do contrato exposto.
- **Legado:** `BankPolicy`, `BankStats`, `max_gfx_bytes` e `max_audio_bytes` precisam ser removidos do caminho canônico dessa capacidade.
## Referencias
- `AGD-0024`: Generic Memory Bank Slot Contract.
- `DEC-0010`: Overlay Log Metric Uses Last Completed Frame.
## Propagacao Necessaria
1. Criar plano de execução para refatorar `AssetManager`, HAL, overlay e certificação.
2. Remover o caminho de telemetria de banks baseado em bytes.
3. Migrar limites de certificação de banks para slots.
1. O plano de execução deve refatorar `AssetManager`, `MemoryBanks`, HAL/bridges, overlay e certificação sem reabrir a arquitetura base.
2. O caminho de telemetria de banks baseado em bytes deve ser removido do contrato visível.
3. O detalhamento operacional por enum de slot deve ser propagado para os pontos que inspecionam ocupação detalhada.
4. A certificação de banks deve migrar de limites em bytes para limites em slots.
## Revision Log
- 2026-04-10: Initial decision emitted from `AGD-0024`.
- 2026-04-10: Decision tightened to align explicitly with agenda closure on `AssetManager` ownership, slot-enum operational detail, and `MemoryBanks` non-canonical residency.

View File

@ -2,152 +2,193 @@
id: PLN-0010
ticket: generic-memory-bank-slot-contract
title: PR/Plan - Asset Manager Bank Telemetry Slot Contract
status: open
status: in_progress
created: 2026-04-10
completed:
tags: [runtime, asset, memory-bank, slots, host, telemetry]
tags: [runtime, asset, memory-bank, slots, host, telemetry, certification]
---
## Objective
## Briefing
Implement the `DEC-0012` bank telemetry contract so `AssetManager` exposes slot-based bank summaries using canonical bank names `GLYPH` and `SOUND`, while host overlay and certification consume slot-based limits instead of byte-based bank metrics.
Implement `DEC-0012` by replacing the current byte-oriented bank telemetry path with a slot-first contract exposed directly by `AssetManager`. The execution MUST remove `BankPolicy` and `BankStats` from the visible telemetry path, keep canonical bank names aligned with `BankType` (`GLYPH`, `SOUNDS`), make the host overlay consume the new `AssetManager` summary, and migrate bank certification from byte limits to slot limits.
## Background
## Decisions de Origem
`DEC-0012` locked the bank telemetry contract around a simple slot-first structure:
- `DEC-0012` - Asset Manager Bank Telemetry Slot Contract
```rust
pub struct BankTelemetry {
pub bank_type: BankType,
pub used_slots: usize,
pub total_slots: usize,
}
```
## Alvo
This replaces the old bank telemetry path that relied on byte-oriented structures and presentation aliases. The implementation must remove `BankPolicy` and `BankStats` from the exposed bank telemetry contract path, move visible bank summaries to `AssetManager`, and migrate certification rules from byte-based thresholds to slot-based thresholds.
Deliver a canonical bank telemetry path with these properties:
## Scope
- `AssetManager` exposes the visible per-bank summary.
- The visible summary shape is slot-first and limited to `bank_type`, `used_slots`, and `total_slots`.
- Detailed inspection remains slot-based through `SlotRef` / slot enums, not byte-based accounting.
- Host overlay and certification consume the new model.
- Legacy byte-oriented bank telemetry fields and limits are removed from the canonical path.
## Escopo
### Included
- Add a visible slot-based `BankTelemetry` contract owned by `AssetManager`.
- Remove `BankStats` from the public bank telemetry contract path.
- Remove `BankPolicy` from the bank telemetry contract path and refactor internal code accordingly.
- Update bridge/consumer APIs so bank summaries come from `AssetManager` slot telemetry.
- Migrate certification from `max_gfx_bytes` / `max_audio_bytes` to slot-based limits for `GLYPH` and `SOUND`.
- Update host overlay to iterate `AssetManager` bank telemetry using canonical `GLYPH` and `SOUND` labels.
- Update specs/docs affected by the contract change.
### Excluded
- Adding new bank kinds beyond the currently supported `GLYPH` and `SOUND`.
- Reworking unrelated asset load/commit semantics.
- Redesigning generic slot-detail payloads beyond the accepted summary shape unless implementation requires a narrowly scoped helper.
- Add a canonical `BankTelemetry` type to the shared HAL asset contract.
- Add an `AssetBridge` method for bank telemetry summaries exposed by `AssetManager`.
- Refactor `AssetManager` in `crates/console/prometeu-drivers` to compute slot-first bank telemetry.
- Remove `BankStats` from the visible bank telemetry path and stop using it for overlay and certification.
- Remove `BankPolicy` as the main modeling surface used to describe visible bank telemetry.
- Keep detailed slot inspection on explicit slot references instead of bank byte summaries.
- Update runtime telemetry / certification to track `max_glyph_slots_used` and `max_sound_slots_used`.
- Update host overlay consumers to render bank occupancy from the new `AssetManager` summary.
- Update runtime specs so the published contract describes slot-first bank telemetry and slot-based certification.
## Execution Steps
## Fora de Escopo
### Step 1 - Introduce `BankTelemetry` in the exposed asset contract
- Reopening the base architectural decision about whether the visible summary belongs in `AssetManager`.
- Introducing new bank categories beyond the current `GLYPH` and `SOUNDS`.
- Reworking unrelated asset loading semantics, cartridge payload layout, or host UI design beyond adapting bank telemetry consumption.
- Removing all internal byte accounting if some private implementation detail still needs it temporarily.
## Plano de Execucao
### Step 1 - Lock the shared contract in HAL
**What:**
Define the new slot-based summary type and make it the canonical visible representation of bank telemetry.
Define the canonical shared types and trait surface for slot-first bank telemetry.
**How:**
Add `BankTelemetry` to the HAL asset domain and ensure canonical naming uses `GLYPH` and `SOUND`. Remove or deprecate exposed `BankStats` paths that conflict with the new contract.
Add `BankTelemetry` to `crates/console/prometeu-hal/src/asset.rs` using `bank_type`, `used_slots`, and `total_slots`. Update `crates/console/prometeu-hal/src/asset_bridge.rs` so the visible bridge exposes a bank telemetry summary method from `AssetManager`. Remove or deprecate `BankStats` from the visible bridge path so downstream callers stop depending on byte-oriented bank summaries.
**File(s):**
- `crates/console/prometeu-hal/src/asset.rs`
- `crates/console/prometeu-hal/src/asset_bridge.rs`
### Step 2 - Make `AssetManager` expose bank telemetry directly
**Depends on:**
- None
### Step 2 - Refactor AssetManager to produce slot-first summaries
**What:**
Implement `AssetManager` support for returning `Vec<BankTelemetry>` from live slot occupancy.
Move visible bank summary generation to a canonical `AssetManager` slot telemetry path.
**How:**
Compute `used_slots` from current slot occupancy and `total_slots` from the fixed slot arrays for `GLYPH` and `SOUND`. Keep implementation simple and derive the summary directly from the installed slots. Refactor any existing public bank-info consumers away from byte-oriented APIs.
Replace `bank_info(BankType) -> BankStats` with a summary path that reports only slot occupancy for each canonical bank type. Compute `used_slots` from the slot arrays already owned by `AssetManager` (`gfx_slots`, `sound_slots`) or equivalent canonical occupancy state. Keep detailed per-slot inspection in `slot_info(SlotRef)`. Remove `BankPolicy` and its byte counters as the visible modeling surface for bank telemetry; if any internal storage helper remains temporarily, it must not leak into the shared contract.
**File(s):**
- `crates/console/prometeu-drivers/src/asset.rs`
- `crates/console/prometeu-drivers/src/memory_banks.rs`
### Step 3 - Remove byte-based bank certification rules
**Depends on:**
- Step 1
### Step 3 - Migrate runtime and host consumers to the new summary
**What:**
Replace byte-based certification limits for banks with slot-based limits.
Replace every bank summary consumer that still expects byte-oriented telemetry.
**How:**
Remove `max_gfx_bytes` and `max_audio_bytes` from certification config and certifier checks. Introduce slot-based limits such as `max_glyph_slots_used` and `max_sound_slots_used`, and evaluate them against `BankTelemetry` summaries or equivalent slot-based data available at frame-end.
Update runtime and host callers to consume `BankTelemetry` from `AssetManager` instead of `BankStats`. Replace any direct `bank_info(BankType)` callsites with the new canonical summary path. Preserve slot-level inspection through `slot_info(SlotRef)` where detailed occupancy is needed. The host overlay must render bank occupancy from the `AssetManager` summary and must not reconstruct primary bank occupancy from bytes.
**File(s):**
- `crates/console/prometeu-system/src/virtual_machine_runtime/tick.rs`
- `crates/host/prometeu-host-desktop-winit/src/overlay.rs`
- Any additional callsites found through `rg 'bank_info|BankStats' crates`
**Depends on:**
- Step 2
### Step 4 - Replace certification byte limits with slot limits
**What:**
Move bank certification from byte ceilings to slot ceilings.
**How:**
Replace `max_gfx_bytes` and `max_audio_bytes` in telemetry / certification configuration and evaluation with slot-based limits such as `max_glyph_slots_used` and `max_sound_slots_used`. Update runtime accounting to record the maximum occupied slot count per bank during execution. Remove byte-limit evaluation from the bank certification path.
**File(s):**
- `crates/console/prometeu-hal/src/telemetry.rs`
- `crates/console/prometeu-system/src/virtual_machine_runtime/tick.rs`
### Step 4 - Update host overlay to consume slot telemetry
**Depends on:**
- Step 3
### Step 5 - Remove residual canonical-path names and semantics that conflict with DEC-0012
**What:**
Replace hardcoded bank presentation with iteration over `AssetManager` bank telemetry.
Eliminate remaining visible-path terminology and structures that preserve the old model.
**How:**
Render one bar per bank using `bank_type`, `used_slots`, and `total_slots`. Use canonical labels `GLYPH` and `SOUND`, and remove any byte-based bank presentation from the overlay.
Ensure no visible bank telemetry path still uses `GFX` / `AUD` as canonical names, `BankStats` as the summary type, or byte fields as the public bank occupancy contract. Keep `GLYPH` and `SOUNDS` aligned with the existing `BankType` enum. If bridges, docs, or helper APIs still expose the old path, remove or rename them in the same change set so the canonical path is unambiguous.
**File(s):**
- `crates/host/prometeu-host-desktop-winit/src/overlay.rs`
- Any host-side adapter/helper used to access asset telemetry
### Step 5 - Remove obsolete structures from the contract path
**What:**
Eliminate `BankPolicy` and `BankStats` from the exposed bank telemetry path.
**How:**
Refactor code so the new slot-based path is the only public contract. If internal implementation helpers must remain temporarily during migration, keep them private and make sure no consumer depends on them as contract.
**File(s):**
- `crates/console/prometeu-drivers/src/asset.rs`
- `crates/console/prometeu-hal/src/asset.rs`
- Related callers revealed during implementation
- `crates/console/prometeu-hal/src/asset_bridge.rs`
- `crates/console/prometeu-drivers/src/asset.rs`
- `crates/console/prometeu-system/src/virtual_machine_runtime/tick.rs`
- `crates/host/prometeu-host-desktop-winit/src/overlay.rs`
### Step 6 - Update normative documentation
**Depends on:**
- Step 4
### Step 6 - Update published specifications
**What:**
Align specifications with the new slot-based bank telemetry contract.
Align canonical docs with the implemented slot-first bank contract.
**How:**
Update relevant runtime documentation to describe bank occupancy in terms of `GLYPH` and `SOUND` slot usage, and remove outdated references to byte-based bank telemetry where they were treated as visible contract.
Update runtime specs to state that bank telemetry is exposed from `AssetManager`, that the published per-bank summary is slot-first, that detailed occupancy inspection remains slot-based, and that certification uses slot ceilings instead of byte ceilings. Remove residual published references to `max_gfx_bytes`, `max_audio_bytes`, or byte-oriented bank summaries in canonical specs.
**File(s):**
- `docs/specs/runtime/10-debug-inspection-and-profiling.md`
- `docs/specs/runtime/15-asset-management.md`
- Any additional touched runtime spec chapter
- `docs/specs/runtime/10-debug-inspection-and-profiling.md`
- Any additional runtime spec file that still documents byte-based bank certification or overlay bank summary consumption
## Test Requirements
**Depends on:**
- Step 5
## Criterios de Aceite
- [ ] `AssetManager` is the canonical visible source of bank telemetry.
- [ ] The visible bank summary contract uses only `bank_type`, `used_slots`, and `total_slots`.
- [ ] Canonical bank names in the visible contract are `GLYPH` and `SOUNDS`.
- [ ] `BankStats` is no longer used as the visible bank telemetry summary type.
- [ ] `BankPolicy` is not the main visible modeling surface for bank telemetry.
- [ ] Host overlay bank occupancy uses the new `AssetManager` bank summary.
- [ ] Detailed occupancy inspection remains slot-based via slot references or equivalent slot enums.
- [ ] `max_gfx_bytes` and `max_audio_bytes` are removed from bank certification.
- [ ] Slot-based bank certification limits exist and are enforced.
- [ ] Published runtime specs describe the slot-first contract and slot-based certification model.
## Tests / Validacao
### Unit Tests
- `AssetManager` reports correct `BankTelemetry` for empty and occupied `GLYPH` / `SOUND` slots.
- Certification slot limits trigger correctly for `GLYPH` and `SOUND`.
- Overlay rendering accepts generic bank telemetry iteration.
- Add or update tests in `crates/console/prometeu-drivers/src/asset.rs` to verify the bank summary reports correct `used_slots` and `total_slots` for `GLYPH` and `SOUNDS`.
- Add or update tests in `crates/console/prometeu-hal/src/telemetry.rs` to verify slot-based certification thresholds trigger correctly and byte-based limits no longer exist in the bank path.
- Keep slot inspection tests proving `slot_info(SlotRef)` remains the detailed inspection mechanism.
### Integration Tests
- `cargo check` for affected HAL, drivers, system, and host crates.
- Focused tests for asset bank telemetry and certification migration.
- Run `cargo test -p prometeu-drivers`.
- Run `cargo test -p prometeu-hal`.
- Run `cargo test -p prometeu-system`.
- Run `cargo test -p prometeu-host-desktop-winit` if overlay bank telemetry callsites are changed there.
- Run `cargo check` for the full touched workspace if cross-crate API changes make targeted tests insufficient.
### Manual Verification
- Run the desktop host and confirm banks appear as `GLYPH` and `SOUND`.
- Confirm bank bars track occupied slots instead of bytes.
- Confirm certification behavior still reports bank pressure using slot limits.
## Acceptance Criteria
- [ ] `AssetManager` exposes a visible `BankTelemetry` summary with `bank_type`, `used_slots`, and `total_slots`.
- [ ] Canonical bank names in the new contract are `GLYPH` and `SOUND`.
- [ ] Byte-based bank certification limits are removed and replaced by slot-based limits.
- [ ] Host overlay renders bank occupancy from generic slot telemetry rather than byte-oriented hardcoded bank logic.
- [ ] `BankPolicy` and `BankStats` are no longer part of the exposed bank telemetry contract path.
- Start the desktop host and confirm the overlay shows per-bank occupancy using slot counts, not bytes.
- Confirm the overlay labels use `GLYPH` and `SOUNDS` or approved presentation labels derived from those canonical bank types.
- Confirm certification output reports slot-limit violations and no longer reports `max_gfx_bytes` / `max_audio_bytes`.
- Confirm detailed slot inspection still works for both glyph and sound slots.
## Dependencies
- `DEC-0012` accepted and unchanged.
- `DEC-0012` remains accepted and unchanged.
- Existing `BankType` enum values remain `GLYPH` and `SOUNDS`.
- `AssetManager` remains the asset-domain owner for slot occupancy summaries.
## Risks
## Riscos
- Removing byte-based bank paths may have wider ripple effects than expected if old APIs are reused outside the overlay.
- The line between “removed from contract path” and “removed completely from implementation” must stay explicit during refactor to avoid accidental partial migration.
- Certification migration must stay synchronized with telemetry migration or the host and certifier will diverge semantically.
- Cross-crate trait changes in `AssetBridge` may create a wider refactor than the bank telemetry callsites initially suggest.
- Removing `BankStats` from the visible path may expose hidden byte-based assumptions in runtime telemetry, overlay formatting, or tests.
- Certification migration may fail partially if slot maxima are recorded in one layer while enforcement still reads old byte fields in another layer.
- If byte accounting is still needed internally during the refactor, the implementation can accidentally leak that detail back into public interfaces unless the change set is kept cohesive.

View File

@ -187,6 +187,23 @@ Limit:32KB
These data directly feed the certification.
### 7.1 Bank Occupancy Profiling
Bank occupancy diagnostics are slot-first.
The visible per-bank telemetry used by host inspection surfaces and certification is:
- `bank_type`
- `used_slots`
- `total_slots`
For the current runtime banks, the canonical bank names are:
- `GLYPH`
- `SOUNDS`
Byte-oriented bank occupancy is not the canonical visible profiling contract.
## 8 Breakpoints and Watchpoints
### 8.1 Breakpoints
@ -218,6 +235,17 @@ Execution can pause when:
## 9 Event and Fault Debugging
## 10 Certification Diagnostics
Certification diagnostics may enforce bank occupancy ceilings.
For bank residency, certification uses slot-based limits, such as:
- `max_glyph_slots_used`
- `max_sound_slots_used`
Bank certification MUST NOT depend on `max_gfx_bytes` or `max_audio_bytes`.
PROMETEU allows observing:
- event queue

View File

@ -169,7 +169,7 @@ For `TILES` v1:
The current runtime exposes bank types:
- `TILES`
- `GLYPH`
- `SOUNDS`
Assets are loaded into explicit slots identified by slot index at the public ABI boundary.
@ -184,6 +184,27 @@ SlotRef { bank_type, index }
That internal representation is derived from the resolved `AssetEntry`, not supplied by the caller.
### 5.1 Canonical Bank Telemetry
The canonical visible bank telemetry contract is exposed by `AssetManager`.
The per-bank summary is slot-first and uses this shape:
```text
BankTelemetry {
bank_type
used_slots
total_slots
}
```
Rules:
- the visible contract MUST NOT expose byte-oriented bank occupancy as the canonical summary;
- canonical bank names are `GLYPH` and `SOUNDS`;
- detailed occupancy inspection remains slot-based through slot references, not bank byte totals;
- any residual byte accounting MAY exist internally, but it is not part of the visible bank telemetry contract.
## 6 Load Lifecycle
The runtime asset manager exposes a staged lifecycle: