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

View File

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

View File

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

View File

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

View File

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

View File

@ -486,8 +486,17 @@ impl NativeInterface for VirtualMachineRuntime {
1 => BankType::SOUNDS, 1 => BankType::SOUNDS,
_ => return Err(VmFault::Trap(TRAP_TYPE, "Invalid asset type".to_string())), _ => return Err(VmFault::Trap(TRAP_TYPE, "Invalid asset type".to_string())),
}; };
let json = let telemetry = hw
serde_json::to_string(&hw.assets().bank_info(asset_type)).unwrap_or_default(); .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); ret.push_string(json);
Ok(()) Ok(())
} }

View File

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

View File

@ -272,6 +272,16 @@ impl HostDebugger {
tel.host_cpu_time_us, tel.host_cpu_time_us,
cert_config.max_host_cpu_us_per_frame.unwrap_or(0), 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), _ => ("unknown".to_string(), 0, 0),
}; };
@ -302,12 +312,10 @@ impl HostDebugger {
cycles_budget: tel.cycles_budget, cycles_budget: tel.cycles_budget,
host_cpu_time_us: tel.host_cpu_time_us, host_cpu_time_us: tel.host_cpu_time_us,
violations: tel.violations, violations: tel.violations,
gfx_used_bytes: tel.gfx_used_bytes, glyph_slots_used: tel.glyph_slots_used,
gfx_inflight_bytes: tel.gfx_inflight_bytes, glyph_slots_total: tel.glyph_slots_total,
gfx_slots_occupied: tel.gfx_slots_occupied, sound_slots_used: tel.sound_slots_used,
audio_used_bytes: tel.audio_used_bytes, sound_slots_total: tel.sound_slots_total,
audio_inflight_bytes: tel.audio_inflight_bytes,
audio_slots_occupied: tel.audio_slots_occupied,
}); });
self.last_telemetry_frame = current_frame; self.last_telemetry_frame = current_frame;
} }

View File

@ -42,14 +42,30 @@ pub(crate) struct OverlaySnapshot {
footer: Vec<OverlayMetric>, 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 { pub(crate) fn capture_snapshot(stats: &HostStats, firmware: &Firmware) -> OverlaySnapshot {
let tel = firmware.os.atomic_telemetry.snapshot(); let tel = firmware.os.atomic_telemetry.snapshot();
let recent_logs = firmware.os.log_service.get_recent(10); 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(); let mut footer = Vec::new();
if violations_count > 0 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 { footer.push(OverlayMetric {
label: "CERT", 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 }) .or(if tel.heap_max_bytes > 0 { Some(tel.heap_max_bytes) } else { None })
.unwrap_or(OVERLAY_HEAP_FALLBACK_BYTES); .unwrap_or(OVERLAY_HEAP_FALLBACK_BYTES);
let heap_ratio = ratio(tel.heap_used_bytes as u64, heap_total_bytes as u64); 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 glyph_ratio = ratio(tel.glyph_slots_used as u64, tel.glyph_slots_total as u64);
let audio_ratio = ratio(tel.audio_slots_occupied as u64, 16); let sound_ratio = ratio(tel.sound_slots_used as u64, tel.sound_slots_total as u64);
OverlaySnapshot { OverlaySnapshot {
rows: vec![ 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()), value: format!("{:.2}ms", stats.average_host_cpu_ms()),
warn: false, warn: false,
}, },
OverlayMetric { OverlayMetric { label: "STEPS", value: tel.vm_steps.to_string(), warn: false },
label: "STEPS",
value: tel.vm_steps.to_string(),
warn: false,
},
), ),
( (
OverlayMetric { OverlayMetric { label: "SYSC", value: tel.syscalls.to_string(), warn: false },
label: "SYSC", OverlayMetric { label: "LOGS", value: tel.logs_count.to_string(), warn: false },
value: tel.syscalls.to_string(),
warn: false,
},
OverlayMetric {
label: "LOGS",
value: tel.logs_count.to_string(),
warn: false,
},
), ),
], ],
bars: vec![ bars: vec![
@ -136,36 +140,27 @@ pub(crate) fn capture_snapshot(stats: &HostStats, firmware: &Firmware) -> Overla
label: "BUDGET", label: "BUDGET",
value: if tel.cycles_budget > 0 { value: if tel.cycles_budget > 0 {
format!( format!(
"{:.1}K/{:.1}K {:.1}%", "{:.1}K/{:.1}K",
tel.cycles_used as f64 / 1000.0, tel.cycles_used as f64 / 1000.0,
tel.cycles_budget as f64 / 1000.0, tel.cycles_budget as f64 / 1000.0
cycles_ratio * 100.0
) )
} else { } else {
"0.0K/0.0K 0.0%".to_string() "0.0K/0.0K".to_string()
}, },
ratio: cycles_ratio, ratio: cycles_ratio,
warn: cycles_ratio >= 0.9, warn: cycles_ratio >= 0.9,
}, },
OverlayBar { OverlayBar {
label: "GFX", label: "GLYPH",
value: format!( value: format!("{} / {} slots", tel.glyph_slots_used, tel.glyph_slots_total),
"{} / 16 slots {}K", ratio: glyph_ratio,
tel.gfx_slots_occupied, warn: tel.glyph_slots_total > 0 && tel.glyph_slots_used >= tel.glyph_slots_total,
tel.gfx_used_bytes.div_ceil(1024)
),
ratio: gfx_ratio,
warn: tel.gfx_inflight_bytes > 0,
}, },
OverlayBar { OverlayBar {
label: "AUD", label: "SOUNDS",
value: format!( value: format!("{} / {} slots", tel.sound_slots_used, tel.sound_slots_total),
"{} / 16 slots {}K", ratio: sound_ratio,
tel.audio_slots_occupied, warn: tel.sound_slots_total > 0 && tel.sound_slots_used >= tel.sound_slots_total,
tel.audio_used_bytes.div_ceil(1024)
),
ratio: audio_ratio,
warn: tel.audio_inflight_bytes > 0,
}, },
], ],
footer, footer,
@ -178,44 +173,58 @@ pub(crate) fn draw_overlay(
frame_height: usize, frame_height: usize,
snapshot: &OverlaySnapshot, 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_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); fill_rect_alpha(&mut canvas, panel_rect, BG);
stroke_rect(frame, frame_width, frame_height, PANEL_X, PANEL_Y, PANEL_WIDTH, panel_height, BORDER); stroke_rect(&mut canvas, panel_rect, BORDER);
let mut y = PANEL_Y + PANEL_PADDING; let mut y = PANEL_Y + PANEL_PADDING;
for (left, right) in &snapshot.rows { 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; y += LINE_HEIGHT;
} }
for bar in &snapshot.bars { for bar in &snapshot.bars {
let color = if bar.warn { WARN } else { TEXT }; 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(
draw_text(frame, frame_width, frame_height, PANEL_X + 48, y, &bar.value, color); 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; y += LINE_HEIGHT - 2;
let bar_x = PANEL_X + PANEL_PADDING; 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; let fill_width = ((BAR_WIDTH as f32) * bar.ratio.clamp(0.0, 1.0)).round() as usize;
fill_rect( fill_rect(
frame, &mut canvas,
frame_width, Rect { x: bar_x, y, width: fill_width, height: BAR_HEIGHT },
frame_height,
bar_x,
y,
fill_width,
BAR_HEIGHT,
if bar.warn { BAR_WARN } else { BAR_FILL }, 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; y += BAR_HEIGHT + 6;
} }
for line in &snapshot.footer { for line in &snapshot.footer {
let color = if line.warn { WARN } else { TEXT }; 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(
draw_text(frame, frame_width, frame_height, PANEL_X + 48, y, &line.value, color); 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; y += LINE_HEIGHT;
} }
} }
@ -260,7 +269,15 @@ fn truncate_value(value: &str, max_len: usize) -> String {
upper 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; let mut cursor_x = x;
for ch in text.chars() { for ch in text.chars() {
if ch == '\n' { 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); let glyph = glyph_bits(ch);
for (row, bits) in glyph.iter().enumerate() { for (row, bits) in glyph.iter().enumerate() {
for col in 0..5 { for col in 0..5 {
if bits & (1 << (4 - col)) != 0 { 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( fn fill_rect_alpha(canvas: &mut FrameCanvas<'_>, rect: Rect, color: [u8; 4]) {
frame: &mut [u8], let max_x = (rect.x + rect.width).min(canvas.width);
frame_width: usize, let max_y = (rect.y + rect.height).min(canvas.height);
frame_height: usize, for py in rect.y..max_y {
x: usize, for px in rect.x..max_x {
y: usize, blend_pixel(canvas.frame, canvas.width, px, py, color);
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( fn fill_rect(canvas: &mut FrameCanvas<'_>, rect: Rect, color: [u8; 4]) {
frame: &mut [u8], let max_x = (rect.x + rect.width).min(canvas.width);
frame_width: usize, let max_y = (rect.y + rect.height).min(canvas.height);
frame_height: usize, for py in rect.y..max_y {
x: usize, for px in rect.x..max_x {
y: usize, write_pixel(canvas.frame, canvas.width, px, py, color);
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 stroke_rect( fn stroke_rect(canvas: &mut FrameCanvas<'_>, rect: Rect, color: [u8; 4]) {
frame: &mut [u8], if rect.width == 0 || rect.height == 0 {
frame_width: usize,
frame_height: usize,
x: usize,
y: usize,
width: usize,
height: usize,
color: [u8; 4],
) {
if width == 0 || height == 0 {
return; return;
} }
fill_rect(frame, frame_width, frame_height, x, y, width, 1, color); fill_rect(canvas, Rect { x: rect.x, y: rect.y, width: rect.width, height: 1 }, color);
fill_rect(frame, frame_width, frame_height, x, y + height.saturating_sub(1), width, 1, color); fill_rect(
fill_rect(frame, frame_width, frame_height, x, y, 1, height, color); canvas,
fill_rect(frame, frame_width, frame_height, x + width.saturating_sub(1), y, 1, height, color); 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]) { fn blend_pixel(frame: &mut [u8], frame_width: usize, x: usize, y: usize, color: [u8; 4]) {
@ -426,7 +442,8 @@ mod tests {
OverlayMetric { label: "STEPS", value: "420".to_string(), warn: false }, OverlayMetric { label: "STEPS", value: "420".to_string(), warn: false },
), ),
], ],
bars: vec![OverlayBar { bars: vec![
OverlayBar {
label: "HEAP", label: "HEAP",
value: "1024K / 8192K".to_string(), value: "1024K / 8192K".to_string(),
ratio: 0.125, ratio: 0.125,
@ -434,10 +451,11 @@ mod tests {
}, },
OverlayBar { OverlayBar {
label: "BUDGET", label: "BUDGET",
value: "9.0K/10.0K 90.0%".to_string(), value: "9.0K/10.0K".to_string(),
ratio: 0.9, ratio: 0.9,
warn: true, warn: true,
}], },
],
footer: vec![OverlayMetric { footer: vec![OverlayMetric {
label: "CRASH", label: "CRASH",
value: "VM PANIC".to_string(), value: "VM PANIC".to_string(),

View File

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

View File

@ -45,8 +45,10 @@ impl HostStats {
pub fn record_host_cpu_time(&mut self, us: u64) { 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_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_cursor =
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 + 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 { 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-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-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-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-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-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"}]} {"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`. 2. O contrato exposto deve usar somente `used_slots` e `total_slots`.
3. `BankPolicy` e `BankStats` devem ser removidos por completo. 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. 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`. 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: Nomenclatura canônica acordada para a camada genérica:
- `GLYPH` - `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. `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** **Accepted**
## Contexto ## 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`; - o contrato visível é `slot-first`;
- a telemetria de banks deve usar somente o enum do tipo do bank; - o resumo visível por bank usa apenas `bank_type`, `used_slots` e `total_slots`;
- a semântica operacional deve ser `used_slots / total_slots`; - o detalhamento operacional ocorre por enum de slot;
- `GFX` e `AUD` não são nomes canônicos do domínio; os nomes corretos são `GLYPH` e `SOUND`; - `GLYPH` e `SOUNDS` são os nomes canônicos do domínio;
- a certificação de banks deve deixar de usar limites em bytes e passar a usar limites por slots. - `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 ## Decisao
1. O contrato visível de telemetria de banks MUST be exposed by `AssetManager`. 1. O contrato visível de telemetria de banks MUST ser exposto diretamente por `AssetManager`.
2. O resumo de bank MUST use the following structure shape: 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 ```rust
pub struct BankTelemetry { 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 visível de telemetria de banks MUST ser `slot-first` e MUST NOT depender de contagens de bytes.
4. O contrato de telemetria de banks MUST be slot-first and MUST NOT depend on byte counts. 5. `bank_type` MUST usar nomes canônicos do domínio. Para os banks atuais, os nomes canônicos são `GLYPH` e `SOUNDS`.
5. `BankPolicy` e `BankStats` MUST be removed completely from the bank telemetry contract path. 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.
6. O host overlay MUST consume `AssetManager` bank telemetry instead of hardcoded bank-specific logic. 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.
7. As regras de certificação `max_gfx_bytes` e `max_audio_bytes` MUST be removed. 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.
8. A certificação de banks MUST migrate to slot-based limits, using canonical bank-specific slot limits such as: 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_glyph_slots_used`
- `max_sound_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 ## Rationale
- Slots são a unidade operacional correta para entender ocupação de banks. - Slots são a unidade operacional real percebida por host, overlay e depuração de banks.
- O `AssetManager` já é o lugar que conhece carregamento, commit e ocupação prática dos 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 elimina uma fonte de telemetria considerada enganosa. - Remover bytes do contrato visível evita uma telemetria enganosa e elimina a ambiguidade que a agenda abriu para resolver.
- `GLYPH` e `SOUND` preservam a linguagem correta do domínio e evitam apelidos frágeis na interface. - Fixar `GLYPH` e `SOUNDS` como nomes canônicos evita contaminar o contrato com apelidos de apresentação.
- Certificação por slots mantém coerência entre contrato exposto, overlay e limites técnicos. - 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 ## Invariantes / Contrato
- A telemetria de banks exposta ao restante do sistema sai do `AssetManager`. - A telemetria de banks exposta ao restante do sistema sai de `AssetManager`.
- Cada entrada de bank telemetria informa somente tipo do bank, slots usados e slots totais. - O contrato visível principal não reside em `MemoryBanks`.
- O contrato canônico usa `GLYPH` e `SOUND`. - 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. - O overlay não depende de bytes para mostrar ocupação de banks.
- A certificação de banks não depende de bytes. - A certificação de banks não depende de bytes.
## Impactos ## Impactos
- **AssetManager:** precisa expor `Vec<BankTelemetry>` ou equivalente direto. - **AssetManager:** deve expor `Vec<BankTelemetry>` ou equivalente direto como superfície canônica visível.
- **HAL / Bridges:** precisam alinhar interfaces consumidoras ao contrato slot-first. - **MemoryBanks:** pode continuar como suporte interno, mas não deve concentrar o contrato visível principal de telemetria.
- **Overlay:** deve iterar a telemetria de banks do `AssetManager`. - **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. - **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 ## Referencias
- `AGD-0024`: Generic Memory Bank Slot Contract. - `AGD-0024`: Generic Memory Bank Slot Contract.
- `DEC-0010`: Overlay Log Metric Uses Last Completed Frame. - `DEC-0010`: Overlay Log Metric Uses Last Completed Frame.
## Propagacao Necessaria ## Propagacao Necessaria
1. Criar plano de execução para refatorar `AssetManager`, HAL, overlay e certificação. 1. O plano de execução deve refatorar `AssetManager`, `MemoryBanks`, HAL/bridges, overlay e certificação sem reabrir a arquitetura base.
2. Remover o caminho de telemetria de banks baseado em bytes. 2. O caminho de telemetria de banks baseado em bytes deve ser removido do contrato visível.
3. Migrar limites de certificação de banks para slots. 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 id: PLN-0010
ticket: generic-memory-bank-slot-contract ticket: generic-memory-bank-slot-contract
title: PR/Plan - Asset Manager Bank Telemetry Slot Contract title: PR/Plan - Asset Manager Bank Telemetry Slot Contract
status: open status: in_progress
created: 2026-04-10 created: 2026-04-10
completed: 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 ## Alvo
pub struct BankTelemetry {
pub bank_type: BankType,
pub used_slots: usize,
pub total_slots: usize,
}
```
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 ### 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 - Add a canonical `BankTelemetry` type to the shared HAL asset contract.
- Adding new bank kinds beyond the currently supported `GLYPH` and `SOUND`. - Add an `AssetBridge` method for bank telemetry summaries exposed by `AssetManager`.
- Reworking unrelated asset load/commit semantics. - Refactor `AssetManager` in `crates/console/prometeu-drivers` to compute slot-first bank telemetry.
- Redesigning generic slot-detail payloads beyond the accepted summary shape unless implementation requires a narrowly scoped helper. - 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:** **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:** **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):** **File(s):**
- `crates/console/prometeu-hal/src/asset.rs` - `crates/console/prometeu-hal/src/asset.rs`
- `crates/console/prometeu-hal/src/asset_bridge.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:** **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:** **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):** **File(s):**
- `crates/console/prometeu-drivers/src/asset.rs` - `crates/console/prometeu-drivers/src/asset.rs`
- `crates/console/prometeu-drivers/src/memory_banks.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:** **What:**
Replace byte-based certification limits for banks with slot-based limits. Replace every bank summary consumer that still expects byte-oriented telemetry.
**How:** **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):** **File(s):**
- `crates/console/prometeu-hal/src/telemetry.rs` - `crates/console/prometeu-hal/src/telemetry.rs`
- `crates/console/prometeu-system/src/virtual_machine_runtime/tick.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:** **What:**
Replace hardcoded bank presentation with iteration over `AssetManager` bank telemetry. Eliminate remaining visible-path terminology and structures that preserve the old model.
**How:** **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):** **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` - `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:** **What:**
Align specifications with the new slot-based bank telemetry contract. Align canonical docs with the implemented slot-first bank contract.
**How:** **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):** **File(s):**
- `docs/specs/runtime/10-debug-inspection-and-profiling.md`
- `docs/specs/runtime/15-asset-management.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 ### Unit Tests
- `AssetManager` reports correct `BankTelemetry` for empty and occupied `GLYPH` / `SOUND` slots.
- Certification slot limits trigger correctly for `GLYPH` and `SOUND`. - 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`.
- Overlay rendering accepts generic bank telemetry iteration. - 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 ### 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 ### 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 - 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.
- [ ] `AssetManager` exposes a visible `BankTelemetry` summary with `bank_type`, `used_slots`, and `total_slots`. - Confirm certification output reports slot-limit violations and no longer reports `max_gfx_bytes` / `max_audio_bytes`.
- [ ] Canonical bank names in the new contract are `GLYPH` and `SOUND`. - Confirm detailed slot inspection still works for both glyph and sound slots.
- [ ] 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.
## Dependencies ## 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. - Cross-crate trait changes in `AssetBridge` may create a wider refactor than the bank telemetry callsites initially suggest.
- The line between “removed from contract path” and “removed completely from implementation” must stay explicit during refactor to avoid accidental partial migration. - Removing `BankStats` from the visible path may expose hidden byte-based assumptions in runtime telemetry, overlay formatting, or tests.
- Certification migration must stay synchronized with telemetry migration or the host and certifier will diverge semantically. - 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. 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 Breakpoints and Watchpoints
### 8.1 Breakpoints ### 8.1 Breakpoints
@ -218,6 +235,17 @@ Execution can pause when:
## 9 Event and Fault Debugging ## 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: PROMETEU allows observing:
- event queue - event queue

View File

@ -169,7 +169,7 @@ For `TILES` v1:
The current runtime exposes bank types: The current runtime exposes bank types:
- `TILES` - `GLYPH`
- `SOUNDS` - `SOUNDS`
Assets are loaded into explicit slots identified by slot index at the public ABI boundary. 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. 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 ## 6 Load Lifecycle
The runtime asset manager exposes a staged lifecycle: The runtime asset manager exposes a staged lifecycle: