dev/perf-runtime-telemetry-hot-path (#13)
All checks were successful
Intrepid/Prometeu/Runtime/pipeline/head This commit looks good

Reviewed-on: #13
Co-authored-by: bQUARKz <bquarkz@gmail.com>
Co-committed-by: bQUARKz <bquarkz@gmail.com>
This commit is contained in:
bQUARKz 2026-04-10 08:32:00 +00:00 committed by bquarkz
parent 5e80ea46b8
commit c65362c186
24 changed files with 622 additions and 664 deletions

View File

@ -12,14 +12,22 @@ use prometeu_hal::sample::Sample;
use prometeu_hal::sound_bank::SoundBank; use prometeu_hal::sound_bank::SoundBank;
use std::collections::HashMap; use std::collections::HashMap;
use std::io::Read; use std::io::Read;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, RwLock}; use std::sync::{Arc, Mutex, RwLock};
use std::thread; use std::thread;
use std::time::Instant; use std::time::Instant;
type ResidentMap<T> = HashMap<AssetId, ResidentEntry<T>>;
type StagedValue<T> = (Arc<T>, usize);
type StagingMap<T> = HashMap<HandleId, StagedValue<T>>;
type AssetTable = HashMap<AssetId, AssetEntry>;
type HandleTable = HashMap<HandleId, LoadHandleInfo>;
const GLYPH_BANK_PALETTE_COUNT_V1: usize = 64; const GLYPH_BANK_PALETTE_COUNT_V1: usize = 64;
const GLYPH_BANK_COLORS_PER_PALETTE: usize = 16; const GLYPH_BANK_COLORS_PER_PALETTE: usize = 16;
const GLYPH_BANK_PALETTE_BYTES_V1: usize = const GLYPH_BANK_PALETTE_BYTES_V1: usize =
GLYPH_BANK_PALETTE_COUNT_V1 * GLYPH_BANK_COLORS_PER_PALETTE * std::mem::size_of::<u16>(); GLYPH_BANK_PALETTE_COUNT_V1 * GLYPH_BANK_COLORS_PER_PALETTE * size_of::<u16>();
/// Resident metadata for a decoded/materialized asset inside a BankPolicy. /// Resident metadata for a decoded/materialized asset inside a BankPolicy.
#[derive(Debug)] #[derive(Debug)]
@ -53,10 +61,26 @@ impl<T> ResidentEntry<T> {
/// This is internal to the AssetManager and not visible to peripherals. /// This is internal to the AssetManager and not visible to peripherals.
pub struct BankPolicy<T> { pub struct BankPolicy<T> {
/// Dedup table: asset_id -> resident entry (value + telemetry). /// Dedup table: asset_id -> resident entry (value + telemetry).
resident: Arc<RwLock<HashMap<AssetId, ResidentEntry<T>>>>, pub resident: Arc<RwLock<ResidentMap<T>>>,
/// Staging area: handle -> value ready to commit. /// Staging area: handle -> value ready to commit.
staging: Arc<RwLock<HashMap<HandleId, Arc<T>>>>, pub staging: Arc<RwLock<StagingMap<T>>>,
/// Total bytes currently in resident storage.
pub used_bytes: Arc<AtomicUsize>,
/// Bytes in staging awaiting commit.
pub inflight_bytes: Arc<AtomicUsize>,
}
impl<T> Clone for BankPolicy<T> {
fn clone(&self) -> Self {
Self {
resident: Arc::clone(&self.resident),
staging: Arc::clone(&self.staging),
used_bytes: Arc::clone(&self.used_bytes),
inflight_bytes: Arc::clone(&self.inflight_bytes),
}
}
} }
impl<T> BankPolicy<T> { impl<T> BankPolicy<T> {
@ -64,6 +88,8 @@ impl<T> BankPolicy<T> {
Self { Self {
resident: Arc::new(RwLock::new(HashMap::new())), resident: Arc::new(RwLock::new(HashMap::new())),
staging: Arc::new(RwLock::new(HashMap::new())), staging: Arc::new(RwLock::new(HashMap::new())),
used_bytes: Arc::new(AtomicUsize::new(0)),
inflight_bytes: Arc::new(AtomicUsize::new(0)),
} }
} }
@ -87,30 +113,38 @@ impl<T> BankPolicy<T> {
None => { None => {
let entry = ResidentEntry::new(Arc::clone(&value), bytes); let entry = ResidentEntry::new(Arc::clone(&value), bytes);
map.insert(asset_id, entry); map.insert(asset_id, entry);
self.used_bytes.fetch_add(bytes, Ordering::Relaxed);
value value
} }
} }
} }
/// Place a value into staging for a given handle. /// Place a value into staging for a given handle.
pub fn stage(&self, handle: HandleId, value: Arc<T>) { pub fn stage(&self, handle: HandleId, value: Arc<T>, bytes: usize) {
self.staging.write().unwrap().insert(handle, value); self.staging.write().unwrap().insert(handle, (value, bytes));
self.inflight_bytes.fetch_add(bytes, Ordering::Relaxed);
} }
/// Take staged value (used by commit path). /// Take staged value (used by commit path).
pub fn take_staging(&self, handle: HandleId) -> Option<Arc<T>> { pub fn take_staging(&self, handle: HandleId) -> Option<StagedValue<T>> {
self.staging.write().unwrap().remove(&handle) let entry = self.staging.write().unwrap().remove(&handle);
if let Some((_, bytes)) = entry.as_ref() {
self.inflight_bytes.fetch_sub(*bytes, Ordering::Relaxed);
}
entry
} }
pub fn clear(&self) { pub fn clear(&self) {
self.resident.write().unwrap().clear(); self.resident.write().unwrap().clear();
self.staging.write().unwrap().clear(); self.staging.write().unwrap().clear();
self.used_bytes.store(0, Ordering::Relaxed);
self.inflight_bytes.store(0, Ordering::Relaxed);
} }
} }
pub struct AssetManager { pub struct AssetManager {
assets: Arc<RwLock<HashMap<AssetId, AssetEntry>>>, assets: Arc<RwLock<AssetTable>>,
handles: Arc<RwLock<HashMap<HandleId, LoadHandleInfo>>>, handles: Arc<RwLock<HandleTable>>,
next_handle_id: Mutex<HandleId>, next_handle_id: Mutex<HandleId>,
assets_data: Arc<RwLock<AssetsPayloadSource>>, assets_data: Arc<RwLock<AssetsPayloadSource>>,
@ -127,6 +161,11 @@ 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>>,
} }
@ -263,6 +302,8 @@ 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)),
@ -379,7 +420,7 @@ impl AssetManager {
let already_resident = match entry.bank_type { let already_resident = match entry.bank_type {
BankType::GLYPH => { BankType::GLYPH => {
if let Some(bank) = self.gfx_policy.get_resident(asset_id) { if let Some(bank) = self.gfx_policy.get_resident(asset_id) {
self.gfx_policy.stage(handle_id, bank); self.gfx_policy.stage(handle_id, bank, entry.decoded_size as usize);
true true
} else { } else {
false false
@ -387,7 +428,7 @@ impl AssetManager {
} }
BankType::SOUNDS => { BankType::SOUNDS => {
if let Some(bank) = self.sound_policy.get_resident(asset_id) { if let Some(bank) = self.sound_policy.get_resident(asset_id) {
self.sound_policy.stage(handle_id, bank); self.sound_policy.stage(handle_id, bank, entry.decoded_size as usize);
true true
} else { } else {
false false
@ -414,10 +455,8 @@ impl AssetManager {
let entry_clone = entry.clone(); let entry_clone = entry.clone();
// Capture policies for the worker thread // Capture policies for the worker thread
let gfx_policy_resident = Arc::clone(&self.gfx_policy.resident); let gfx_policy = self.gfx_policy.clone();
let gfx_policy_staging = Arc::clone(&self.gfx_policy.staging); let sound_policy = self.sound_policy.clone();
let sound_policy_resident = Arc::clone(&self.sound_policy.resident);
let sound_policy_staging = Arc::clone(&self.sound_policy.staging);
thread::spawn(move || { thread::spawn(move || {
// Update status to LOADING // Update status to LOADING
@ -439,22 +478,17 @@ impl AssetManager {
let result = Self::perform_load_glyph_bank(&entry_clone, assets_data); let result = Self::perform_load_glyph_bank(&entry_clone, assets_data);
if let Ok(tilebank) = result { if let Ok(tilebank) = result {
let bank_arc = Arc::new(tilebank); let bank_arc = Arc::new(tilebank);
let resident_arc = { let resident_arc = gfx_policy.put_resident(
let mut map = gfx_policy_resident.write().unwrap(); asset_id,
if let Some(existing) = map.get_mut(&asset_id) { bank_arc,
existing.last_used = Instant::now();
existing.loads += 1;
Arc::clone(&existing.value)
} else {
let entry = ResidentEntry::new(
Arc::clone(&bank_arc),
entry_clone.decoded_size as usize, entry_clone.decoded_size as usize,
); );
map.insert(asset_id, entry); gfx_policy.stage(
bank_arc handle_id,
} resident_arc,
}; entry_clone.decoded_size as usize,
gfx_policy_staging.write().unwrap().insert(handle_id, resident_arc); );
let mut handles_map = handles.write().unwrap(); let mut handles_map = handles.write().unwrap();
if let Some(h) = handles_map.get_mut(&handle_id) { if let Some(h) = handles_map.get_mut(&handle_id) {
if h.status == LoadStatus::LOADING { if h.status == LoadStatus::LOADING {
@ -472,22 +506,17 @@ impl AssetManager {
let result = Self::perform_load_sound_bank(&entry_clone, assets_data); let result = Self::perform_load_sound_bank(&entry_clone, assets_data);
if let Ok(soundbank) = result { if let Ok(soundbank) = result {
let bank_arc = Arc::new(soundbank); let bank_arc = Arc::new(soundbank);
let resident_arc = { let resident_arc = sound_policy.put_resident(
let mut map = sound_policy_resident.write().unwrap(); asset_id,
if let Some(existing) = map.get_mut(&asset_id) { bank_arc,
existing.last_used = Instant::now();
existing.loads += 1;
Arc::clone(&existing.value)
} else {
let entry = ResidentEntry::new(
Arc::clone(&bank_arc),
entry_clone.decoded_size as usize, entry_clone.decoded_size as usize,
); );
map.insert(asset_id, entry); sound_policy.stage(
bank_arc handle_id,
} resident_arc,
}; entry_clone.decoded_size as usize,
sound_policy_staging.write().unwrap().insert(handle_id, resident_arc); );
let mut handles_map = handles.write().unwrap(); let mut handles_map = handles.write().unwrap();
if let Some(h) = handles_map.get_mut(&handle_id) { if let Some(h) = handles_map.get_mut(&handle_id) {
if h.status == LoadStatus::LOADING { if h.status == LoadStatus::LOADING {
@ -699,20 +728,26 @@ impl AssetManager {
if h.status == LoadStatus::READY { if h.status == LoadStatus::READY {
match h.slot.asset_type { match h.slot.asset_type {
BankType::GLYPH => { BankType::GLYPH => {
if let Some(bank) = self.gfx_policy.take_staging(handle_id) { if let Some((bank, _)) = self.gfx_policy.take_staging(handle_id) {
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;
} }
} }
BankType::SOUNDS => { BankType::SOUNDS => {
if let Some(bank) = self.sound_policy.take_staging(handle_id) { if let Some((bank, _)) = self.sound_policy.take_staging(handle_id) {
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;
@ -727,38 +762,9 @@ impl AssetManager {
pub fn bank_info(&self, kind: BankType) -> BankStats { pub fn bank_info(&self, kind: BankType) -> BankStats {
match kind { match kind {
BankType::GLYPH => { BankType::GLYPH => {
let mut used_bytes = 0; let used_bytes = self.gfx_policy.used_bytes.load(Ordering::Relaxed);
{ let inflight_bytes = self.gfx_policy.inflight_bytes.load(Ordering::Relaxed);
let resident = self.gfx_policy.resident.read().unwrap(); let slots_occupied = self.gfx_slots_occupied.load(Ordering::Relaxed);
for entry in resident.values() {
used_bytes += entry.bytes;
}
}
let mut inflight_bytes = 0;
{
let staging = self.gfx_policy.staging.read().unwrap();
let assets = self.assets.read().unwrap();
let handles = self.handles.read().unwrap();
for (handle_id, _) in staging.iter() {
if let Some(h) = handles.get(handle_id) {
if let Some(entry) = assets.get(&h._asset_id) {
inflight_bytes += entry.decoded_size as usize;
}
}
}
}
let mut slots_occupied = 0;
{
let slots = self.gfx_slots.read().unwrap();
for s in slots.iter() {
if s.is_some() {
slots_occupied += 1;
}
}
}
BankStats { BankStats {
total_bytes: 16 * 1024 * 1024, total_bytes: 16 * 1024 * 1024,
@ -770,38 +776,9 @@ impl AssetManager {
} }
} }
BankType::SOUNDS => { BankType::SOUNDS => {
let mut used_bytes = 0; let used_bytes = self.sound_policy.used_bytes.load(Ordering::Relaxed);
{ let inflight_bytes = self.sound_policy.inflight_bytes.load(Ordering::Relaxed);
let resident = self.sound_policy.resident.read().unwrap(); let slots_occupied = self.sound_slots_occupied.load(Ordering::Relaxed);
for entry in resident.values() {
used_bytes += entry.bytes;
}
}
let mut inflight_bytes = 0;
{
let staging = self.sound_policy.staging.read().unwrap();
let assets = self.assets.read().unwrap();
let handles = self.handles.read().unwrap();
for (handle_id, _) in staging.iter() {
if let Some(h) = handles.get(handle_id) {
if let Some(entry) = assets.get(&h._asset_id) {
inflight_bytes += entry.decoded_size as usize;
}
}
}
}
let mut slots_occupied = 0;
{
let slots = self.sound_slots.read().unwrap();
for s in slots.iter() {
if s.is_some() {
slots_occupied += 1;
}
}
}
BankStats { BankStats {
total_bytes: 32 * 1024 * 1024, total_bytes: 32 * 1024 * 1024,
@ -865,6 +842,8 @@ 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);
@ -1049,8 +1028,8 @@ mod tests {
assert_eq!(am.status(handle2), LoadStatus::READY); assert_eq!(am.status(handle2), LoadStatus::READY);
let staging = am.gfx_policy.staging.read().unwrap(); let staging = am.gfx_policy.staging.read().unwrap();
let bank1 = staging.get(&handle1).unwrap(); let bank1 = &staging.get(&handle1).unwrap().0;
let bank2 = staging.get(&handle2).unwrap(); let bank2 = &staging.get(&handle2).unwrap().0;
assert!(Arc::ptr_eq(bank1, bank2)); assert!(Arc::ptr_eq(bank1, bank2));
} }
@ -1208,4 +1187,60 @@ mod tests {
assert_eq!(am.status(handle), LoadStatus::CANCELED); assert_eq!(am.status(handle), LoadStatus::CANCELED);
assert_eq!(am.commit(handle), AssetOpStatus::InvalidState); assert_eq!(am.commit(handle), AssetOpStatus::InvalidState);
} }
#[test]
fn test_asset_telemetry_incremental() {
let banks = Arc::new(MemoryBanks::new());
let gfx_installer = Arc::clone(&banks) as Arc<dyn GlyphBankPoolInstaller>;
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
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(
vec![test_glyph_asset_entry("test_glyphs", width, height)],
AssetsPayloadSource::from_bytes(data),
gfx_installer,
sound_installer,
);
// 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);
// Loading
let handle = am.load(0, 0).expect("load must allocate handle");
// While LOADING or READY, it should be in inflight_bytes
let start = Instant::now();
while am.status(handle) != LoadStatus::READY && start.elapsed().as_secs() < 5 {
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);
// 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);
// 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);
}
} }

View File

@ -5,11 +5,12 @@ pub struct LogService {
events: VecDeque<LogEvent>, events: VecDeque<LogEvent>,
capacity: usize, capacity: usize,
next_seq: u64, next_seq: u64,
pub logs_count: u32,
} }
impl LogService { impl LogService {
pub fn new(capacity: usize) -> Self { pub fn new(capacity: usize) -> Self {
Self { events: VecDeque::with_capacity(capacity), capacity, next_seq: 0 } Self { events: VecDeque::with_capacity(capacity), capacity, next_seq: 0, logs_count: 0 }
} }
pub fn log( pub fn log(
@ -34,6 +35,11 @@ impl LogService {
msg, msg,
}); });
self.next_seq += 1; self.next_seq += 1;
self.logs_count += 1;
}
pub fn reset_count(&mut self) {
self.logs_count = 0;
} }
pub fn get_recent(&self, n: usize) -> Vec<LogEvent> { pub fn get_recent(&self, n: usize) -> Vec<LogEvent> {

View File

@ -96,6 +96,62 @@ pub struct SyscallRegistryEntry {
pub meta: SyscallMeta, pub meta: SyscallMeta,
} }
impl SyscallRegistryEntry {
/// Starts the builder with mandatory fields and sensible default values.
pub const fn builder(syscall: Syscall, module: &'static str, name: &'static str) -> Self {
Self {
syscall,
meta: SyscallMeta {
id: syscall as u32,
module,
name,
version: 1, // Default for new syscalls
arg_slots: 0,
ret_slots: 0,
caps: 0,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 1,
},
}
}
pub const fn version(mut self, n: u16) -> Self {
self.meta.version = n;
self
}
pub const fn args(mut self, n: u8) -> Self {
self.meta.arg_slots = n;
self
}
pub const fn rets(mut self, n: u16) -> Self {
self.meta.ret_slots = n;
self
}
pub const fn caps(mut self, caps: CapFlags) -> Self {
self.meta.caps = caps;
self
}
pub const fn non_deterministic(mut self) -> Self {
self.meta.determinism = Determinism::NonDeterministic;
self
}
pub const fn may_allocate(mut self) -> Self {
self.meta.may_allocate = true;
self
}
pub const fn cost(mut self, cost: u32) -> Self {
self.meta.cost_hint = cost;
self
}
}
pub fn meta_for(syscall: Syscall) -> &'static SyscallMeta { pub fn meta_for(syscall: Syscall) -> &'static SyscallMeta {
registry::meta_for(syscall) registry::meta_for(syscall)
} }

View File

@ -1,53 +1,27 @@
use super::entry; use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
use crate::syscalls::{Determinism, Syscall, SyscallRegistryEntry, caps};
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[ pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
entry( SyscallRegistryEntry::builder(Syscall::AssetLoad, "asset", "load")
Syscall::AssetLoad, .args(2)
"asset", .rets(2)
"load", .caps(caps::ASSET)
1, .non_deterministic()
2, .cost(20),
2, SyscallRegistryEntry::builder(Syscall::AssetStatus, "asset", "status")
caps::ASSET, .args(1)
Determinism::NonDeterministic, .rets(1)
false, .caps(caps::ASSET)
20, .non_deterministic(),
), SyscallRegistryEntry::builder(Syscall::AssetCommit, "asset", "commit")
entry( .args(1)
Syscall::AssetStatus, .rets(1)
"asset", .caps(caps::ASSET)
"status", .non_deterministic()
1, .cost(20),
1, SyscallRegistryEntry::builder(Syscall::AssetCancel, "asset", "cancel")
1, .args(1)
caps::ASSET, .rets(1)
Determinism::NonDeterministic, .caps(caps::ASSET)
false, .non_deterministic()
1, .cost(20),
),
entry(
Syscall::AssetCommit,
"asset",
"commit",
1,
1,
1,
caps::ASSET,
Determinism::NonDeterministic,
false,
20,
),
entry(
Syscall::AssetCancel,
"asset",
"cancel",
1,
1,
1,
caps::ASSET,
Determinism::NonDeterministic,
false,
20,
),
]; ];

View File

@ -1,29 +1,14 @@
use super::entry; use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
use crate::syscalls::{Determinism, Syscall, SyscallRegistryEntry, caps};
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[ pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
entry( SyscallRegistryEntry::builder(Syscall::AudioPlaySample, "audio", "play_sample")
Syscall::AudioPlaySample, .args(5)
"audio", .rets(1)
"play_sample", .caps(caps::AUDIO)
1, .cost(5),
5, SyscallRegistryEntry::builder(Syscall::AudioPlay, "audio", "play")
1, .args(7)
caps::AUDIO, .rets(1)
Determinism::Deterministic, .caps(caps::AUDIO)
false, .cost(5),
5,
),
entry(
Syscall::AudioPlay,
"audio",
"play",
1,
7,
1,
caps::AUDIO,
Determinism::Deterministic,
false,
5,
),
]; ];

View File

@ -1,29 +1,12 @@
use super::entry; use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
use crate::syscalls::{Determinism, Syscall, SyscallRegistryEntry, caps};
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[ pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
entry( SyscallRegistryEntry::builder(Syscall::BankInfo, "bank", "info")
Syscall::BankInfo, .args(1)
"bank", .rets(1)
"info", .caps(caps::BANK),
1, SyscallRegistryEntry::builder(Syscall::BankSlotInfo, "bank", "slot_info")
1, .args(2)
1, .rets(1)
caps::BANK, .caps(caps::BANK),
Determinism::Deterministic,
false,
1,
),
entry(
Syscall::BankSlotInfo,
"bank",
"slot_info",
1,
2,
1,
caps::BANK,
Determinism::Deterministic,
false,
1,
),
]; ];

View File

@ -1,150 +1,68 @@
use super::entry; use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
use crate::syscalls::{Determinism, Syscall, SyscallRegistryEntry, caps};
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[ pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
entry( SyscallRegistryEntry::builder(Syscall::FsOpen, "fs", "open")
Syscall::FsOpen, .args(1)
"fs", .rets(1)
"open", .caps(caps::FS)
1, .non_deterministic()
1, .cost(20),
1, SyscallRegistryEntry::builder(Syscall::FsRead, "fs", "read")
caps::FS, .args(1)
Determinism::NonDeterministic, .rets(1)
false, .caps(caps::FS)
20, .non_deterministic()
), .cost(20),
entry( SyscallRegistryEntry::builder(Syscall::FsWrite, "fs", "write")
Syscall::FsRead, .args(2)
"fs", .rets(1)
"read", .caps(caps::FS)
1, .non_deterministic()
1, .cost(20),
1, SyscallRegistryEntry::builder(Syscall::FsClose, "fs", "close").args(1).caps(caps::FS).cost(5),
caps::FS, SyscallRegistryEntry::builder(Syscall::FsListDir, "fs", "list_dir")
Determinism::NonDeterministic, .args(1)
false, .rets(1)
20, .caps(caps::FS)
), .non_deterministic()
entry( .cost(20),
Syscall::FsWrite, SyscallRegistryEntry::builder(Syscall::FsExists, "fs", "exists").args(1).rets(1).caps(caps::FS),
"fs", SyscallRegistryEntry::builder(Syscall::FsDelete, "fs", "delete")
"write", .args(1)
1, .caps(caps::FS)
2, .non_deterministic()
1, .cost(20),
caps::FS, SyscallRegistryEntry::builder(Syscall::MemSlotCount, "mem", "slot_count")
Determinism::NonDeterministic, .rets(2)
false, .caps(caps::FS),
20, SyscallRegistryEntry::builder(Syscall::MemSlotStat, "mem", "slot_stat")
), .args(1)
entry(Syscall::FsClose, "fs", "close", 1, 1, 0, caps::FS, Determinism::Deterministic, false, 5), .rets(5)
entry( .caps(caps::FS)
Syscall::FsListDir, .non_deterministic()
"fs", .cost(5),
"list_dir", SyscallRegistryEntry::builder(Syscall::MemSlotRead, "mem", "slot_read")
1, .args(3)
1, .rets(3)
1, .caps(caps::FS)
caps::FS, .non_deterministic()
Determinism::NonDeterministic, .cost(20),
false, SyscallRegistryEntry::builder(Syscall::MemSlotWrite, "mem", "slot_write")
20, .args(3)
), .rets(2)
entry( .caps(caps::FS)
Syscall::FsExists, .non_deterministic()
"fs", .cost(20),
"exists", SyscallRegistryEntry::builder(Syscall::MemSlotCommit, "mem", "slot_commit")
1, .args(1)
1, .rets(1)
1, .caps(caps::FS)
caps::FS, .non_deterministic()
Determinism::Deterministic, .cost(20),
false, SyscallRegistryEntry::builder(Syscall::MemSlotClear, "mem", "slot_clear")
1, .args(1)
), .rets(1)
entry( .caps(caps::FS)
Syscall::FsDelete, .non_deterministic()
"fs", .cost(20),
"delete",
1,
1,
0,
caps::FS,
Determinism::NonDeterministic,
false,
20,
),
entry(
Syscall::MemSlotCount,
"mem",
"slot_count",
1,
0,
2,
caps::FS,
Determinism::Deterministic,
false,
1,
),
entry(
Syscall::MemSlotStat,
"mem",
"slot_stat",
1,
1,
5,
caps::FS,
Determinism::NonDeterministic,
false,
5,
),
entry(
Syscall::MemSlotRead,
"mem",
"slot_read",
1,
3,
3,
caps::FS,
Determinism::NonDeterministic,
false,
20,
),
entry(
Syscall::MemSlotWrite,
"mem",
"slot_write",
1,
3,
2,
caps::FS,
Determinism::NonDeterministic,
false,
20,
),
entry(
Syscall::MemSlotCommit,
"mem",
"slot_commit",
1,
1,
1,
caps::FS,
Determinism::NonDeterministic,
false,
20,
),
entry(
Syscall::MemSlotClear,
"mem",
"slot_clear",
1,
1,
1,
caps::FS,
Determinism::NonDeterministic,
false,
20,
),
]; ];

View File

@ -1,113 +1,41 @@
use super::entry; use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
use crate::syscalls::{Determinism, Syscall, SyscallRegistryEntry, caps};
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[ pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
entry( SyscallRegistryEntry::builder(Syscall::GfxClear, "gfx", "clear")
Syscall::GfxClear, .args(1)
"gfx", .caps(caps::GFX)
"clear", .cost(20),
1, SyscallRegistryEntry::builder(Syscall::GfxFillRect, "gfx", "fill_rect")
1, .args(5)
0, .caps(caps::GFX)
caps::GFX, .cost(20),
Determinism::Deterministic, SyscallRegistryEntry::builder(Syscall::GfxDrawLine, "gfx", "draw_line")
false, .args(5)
20, .caps(caps::GFX)
), .cost(5),
entry( SyscallRegistryEntry::builder(Syscall::GfxDrawCircle, "gfx", "draw_circle")
Syscall::GfxFillRect, .args(4)
"gfx", .caps(caps::GFX)
"fill_rect", .cost(5),
1, SyscallRegistryEntry::builder(Syscall::GfxDrawDisc, "gfx", "draw_disc")
5, .args(5)
0, .caps(caps::GFX)
caps::GFX, .cost(5),
Determinism::Deterministic, SyscallRegistryEntry::builder(Syscall::GfxDrawSquare, "gfx", "draw_square")
false, .args(6)
20, .caps(caps::GFX)
), .cost(5),
entry( SyscallRegistryEntry::builder(Syscall::GfxSetSprite, "gfx", "set_sprite")
Syscall::GfxDrawLine, .args(10)
"gfx", .rets(1)
"draw_line", .caps(caps::GFX)
1, .cost(5),
5, SyscallRegistryEntry::builder(Syscall::GfxDrawText, "gfx", "draw_text")
0, .args(4)
caps::GFX, .caps(caps::GFX)
Determinism::Deterministic, .cost(20),
false, SyscallRegistryEntry::builder(Syscall::GfxClear565, "gfx", "clear_565")
5, .args(1)
), .caps(caps::GFX)
entry( .cost(20),
Syscall::GfxDrawCircle,
"gfx",
"draw_circle",
1,
4,
0,
caps::GFX,
Determinism::Deterministic,
false,
5,
),
entry(
Syscall::GfxDrawDisc,
"gfx",
"draw_disc",
1,
5,
0,
caps::GFX,
Determinism::Deterministic,
false,
5,
),
entry(
Syscall::GfxDrawSquare,
"gfx",
"draw_square",
1,
6,
0,
caps::GFX,
Determinism::Deterministic,
false,
5,
),
entry(
Syscall::GfxSetSprite,
"gfx",
"set_sprite",
1,
10,
1,
caps::GFX,
Determinism::Deterministic,
false,
5,
),
entry(
Syscall::GfxDrawText,
"gfx",
"draw_text",
1,
4,
0,
caps::GFX,
Determinism::Deterministic,
false,
20,
),
entry(
Syscall::GfxClear565,
"gfx",
"clear_565",
1,
1,
0,
caps::GFX,
Determinism::Deterministic,
false,
20,
),
]; ];

View File

@ -1,29 +1,14 @@
use super::entry; use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
use crate::syscalls::{Determinism, Syscall, SyscallRegistryEntry, caps};
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[ pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
entry( SyscallRegistryEntry::builder(Syscall::LogWrite, "log", "write")
Syscall::LogWrite, .args(2)
"log", .caps(caps::LOG)
"write", .non_deterministic()
1, .cost(5),
2, SyscallRegistryEntry::builder(Syscall::LogWriteTag, "log", "write_tag")
0, .args(3)
caps::LOG, .caps(caps::LOG)
Determinism::NonDeterministic, .non_deterministic()
false, .cost(5),
5,
),
entry(
Syscall::LogWriteTag,
"log",
"write_tag",
1,
3,
0,
caps::LOG,
Determinism::NonDeterministic,
false,
5,
),
]; ];

View File

@ -6,36 +6,7 @@ mod gfx;
mod log; mod log;
mod system; mod system;
use super::{CapFlags, Determinism, Syscall, SyscallMeta, SyscallRegistryEntry}; use super::SyscallRegistryEntry;
pub(crate) const fn entry(
syscall: Syscall,
module: &'static str,
name: &'static str,
version: u16,
arg_slots: u8,
ret_slots: u16,
caps: CapFlags,
determinism: Determinism,
may_allocate: bool,
cost_hint: u32,
) -> SyscallRegistryEntry {
SyscallRegistryEntry {
syscall,
meta: SyscallMeta {
id: syscall as u32,
module,
name,
version,
arg_slots,
ret_slots,
caps,
determinism,
may_allocate,
cost_hint,
},
}
}
pub(crate) fn all_entries() -> impl Iterator<Item = &'static SyscallRegistryEntry> { pub(crate) fn all_entries() -> impl Iterator<Item = &'static SyscallRegistryEntry> {
system::ENTRIES system::ENTRIES

View File

@ -1,29 +1,11 @@
use super::entry; use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
use crate::syscalls::{Determinism, Syscall, SyscallRegistryEntry, caps};
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[ pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
entry( SyscallRegistryEntry::builder(Syscall::SystemHasCart, "system", "has_cart")
Syscall::SystemHasCart, .rets(1)
"system", .caps(caps::SYSTEM),
"has_cart", SyscallRegistryEntry::builder(Syscall::SystemRunCart, "system", "run_cart")
1, .caps(caps::SYSTEM)
0, .non_deterministic()
1, .cost(50),
caps::SYSTEM,
Determinism::Deterministic,
false,
1,
),
entry(
Syscall::SystemRunCart,
"system",
"run_cart",
1,
0,
0,
caps::SYSTEM,
Determinism::NonDeterministic,
false,
50,
),
]; ];

View File

@ -20,6 +20,13 @@ pub struct TelemetryFrame {
pub audio_used_bytes: usize, pub audio_used_bytes: usize,
pub audio_inflight_bytes: usize, pub audio_inflight_bytes: usize,
pub audio_slots_occupied: u32, pub audio_slots_occupied: u32,
// RAM (Heap)
pub heap_used_bytes: usize,
pub heap_max_bytes: usize,
// Log Pressure
pub logs_count: u32,
} }
#[derive(Debug, Clone, Copy, Default)] #[derive(Debug, Clone, Copy, Default)]
@ -28,6 +35,10 @@ 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_audio_bytes: Option<usize>,
pub max_heap_bytes: Option<usize>,
pub max_logs_per_frame: Option<u32>,
} }
pub struct Certifier { pub struct Certifier {
@ -51,6 +62,7 @@ impl Certifier {
let mut violations = 0; let mut violations = 0;
// 1. Cycles
if let Some(budget) = self.config.cycles_budget_per_frame if let Some(budget) = self.config.cycles_budget_per_frame
&& telemetry.cycles_used > budget && telemetry.cycles_used > budget
{ {
@ -68,6 +80,7 @@ impl Certifier {
violations += 1; violations += 1;
} }
// 2. Syscalls
if let Some(limit) = self.config.max_syscalls_per_frame if let Some(limit) = self.config.max_syscalls_per_frame
&& telemetry.syscalls > limit && telemetry.syscalls > limit
{ {
@ -85,6 +98,7 @@ impl Certifier {
violations += 1; violations += 1;
} }
// 3. CPU Time
if let Some(limit) = self.config.max_host_cpu_us_per_frame if let Some(limit) = self.config.max_host_cpu_us_per_frame
&& telemetry.host_cpu_time_us > limit && telemetry.host_cpu_time_us > limit
{ {
@ -102,6 +116,75 @@ impl Certifier {
violations += 1; violations += 1;
} }
// 4. GFX Memory
if let Some(limit) = self.config.max_gfx_bytes
&& telemetry.gfx_used_bytes > limit
{
log_service.log(
ts_ms,
telemetry.frame_index,
LogLevel::Warn,
LogSource::Pos,
0xCA04,
format!(
"Cert: GFX bank exceeded memory limit ({} > {})",
telemetry.gfx_used_bytes, limit
),
);
violations += 1;
}
// 5. Audio Memory
if let Some(limit) = self.config.max_audio_bytes
&& telemetry.audio_used_bytes > limit
{
log_service.log(
ts_ms,
telemetry.frame_index,
LogLevel::Warn,
LogSource::Pos,
0xCA05,
format!(
"Cert: Audio bank exceeded memory limit ({} > {})",
telemetry.audio_used_bytes, limit
),
);
violations += 1;
}
// 6. Heap Memory
if let Some(limit) = self.config.max_heap_bytes
&& telemetry.heap_used_bytes > limit
{
log_service.log(
ts_ms,
telemetry.frame_index,
LogLevel::Warn,
LogSource::Pos,
0xCA06,
format!(
"Cert: Heap memory exceeded limit ({} > {})",
telemetry.heap_used_bytes, limit
),
);
violations += 1;
}
// 7. Log Pressure
if let Some(limit) = self.config.max_logs_per_frame
&& telemetry.logs_count > limit
{
log_service.log(
ts_ms,
telemetry.frame_index,
LogLevel::Warn,
LogSource::Pos,
0xCA07,
format!("Cert: Log pressure exceeded limit ({} > {})", telemetry.logs_count, limit),
);
violations += 1;
}
violations violations
} }
} }
@ -118,6 +201,8 @@ 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),
..Default::default()
}; };
let cert = Certifier::new(config); let cert = Certifier::new(config);
let mut ls = LogService::new(10); let mut ls = LogService::new(10);
@ -126,13 +211,15 @@ 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;
let violations = cert.evaluate(&tel, &mut ls, 1000); let violations = cert.evaluate(&tel, &mut ls, 1000);
assert_eq!(violations, 2); assert_eq!(violations, 3);
let logs = ls.get_recent(10); let logs = ls.get_recent(10);
assert_eq!(logs.len(), 2); 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"));
} }
} }

View File

@ -37,13 +37,14 @@ pub struct VirtualMachineRuntime {
pub certifier: Certifier, pub certifier: Certifier,
pub paused: bool, pub paused: bool,
pub debug_step_request: bool, pub debug_step_request: bool,
pub inspection_active: bool,
pub(crate) needs_prepare_entry_call: bool, pub(crate) needs_prepare_entry_call: bool,
pub(crate) boot_time: Instant, pub(crate) boot_time: Instant,
} }
impl VirtualMachineRuntime { impl VirtualMachineRuntime {
pub const CYCLES_PER_LOGICAL_FRAME: u64 = 5_000_000; pub const CYCLES_PER_LOGICAL_FRAME: u64 = 1_500_000;
pub const SLICE_PER_TICK: u64 = 5_000_000; pub const SLICE_PER_TICK: u64 = 1_500_000;
pub const MAX_LOG_LEN: usize = 256; pub const MAX_LOG_LEN: usize = 256;
pub const MAX_LOGS_PER_FRAME: u32 = 10; pub const MAX_LOGS_PER_FRAME: u32 = 10;
} }

View File

@ -30,6 +30,7 @@ impl VirtualMachineRuntime {
certifier: Certifier::new(cap_config.unwrap_or_default()), certifier: Certifier::new(cap_config.unwrap_or_default()),
paused: false, paused: false,
debug_step_request: false, debug_step_request: false,
inspection_active: false,
needs_prepare_entry_call: false, needs_prepare_entry_call: false,
boot_time, boot_time,
}; };
@ -104,6 +105,7 @@ impl VirtualMachineRuntime {
self.paused = false; self.paused = false;
self.debug_step_request = false; self.debug_step_request = false;
self.inspection_active = false;
self.needs_prepare_entry_call = false; self.needs_prepare_entry_call = false;
} }

View File

@ -4,6 +4,7 @@ use prometeu_hal::asset::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;
impl VirtualMachineRuntime { impl VirtualMachineRuntime {
pub fn debug_step_instruction( pub fn debug_step_instruction(
@ -128,6 +129,26 @@ impl VirtualMachineRuntime {
|| run.reason == LogicalFrameEndingReason::EndOfRom || run.reason == LogicalFrameEndingReason::EndOfRom
{ {
hw.gfx_mut().render_all(); hw.gfx_mut().render_all();
// 1. Snapshot full telemetry at logical frame end (O(1) with atomic counters)
let gfx_stats = hw.assets().bank_info(BankType::GLYPH);
self.telemetry_current.gfx_used_bytes = gfx_stats.used_bytes;
self.telemetry_current.gfx_inflight_bytes = gfx_stats.inflight_bytes;
self.telemetry_current.gfx_slots_occupied = gfx_stats.slots_occupied as u32;
let audio_stats = hw.assets().bank_info(BankType::SOUNDS);
self.telemetry_current.audio_used_bytes = audio_stats.used_bytes;
self.telemetry_current.audio_inflight_bytes = audio_stats.inflight_bytes;
self.telemetry_current.audio_slots_occupied =
audio_stats.slots_occupied as u32;
self.telemetry_current.heap_used_bytes =
vm.heap().used_bytes.load(Ordering::Relaxed);
self.telemetry_current.heap_max_bytes = 0; // Not yet capped
self.telemetry_current.logs_count = self.log_service.logs_count;
self.log_service.reset_count();
self.telemetry_current.host_cpu_time_us = self.telemetry_current.host_cpu_time_us =
start.elapsed().as_micros() as u64; start.elapsed().as_micros() as u64;
@ -166,6 +187,8 @@ impl VirtualMachineRuntime {
self.last_frame_cpu_time_us = start.elapsed().as_micros() as u64; self.last_frame_cpu_time_us = start.elapsed().as_micros() as u64;
// 2. High-frequency telemetry update (only if inspection is active)
if self.inspection_active {
let gfx_stats = hw.assets().bank_info(BankType::GLYPH); let gfx_stats = hw.assets().bank_info(BankType::GLYPH);
self.telemetry_current.gfx_used_bytes = gfx_stats.used_bytes; self.telemetry_current.gfx_used_bytes = gfx_stats.used_bytes;
self.telemetry_current.gfx_inflight_bytes = gfx_stats.inflight_bytes; self.telemetry_current.gfx_inflight_bytes = gfx_stats.inflight_bytes;
@ -176,6 +199,10 @@ impl VirtualMachineRuntime {
self.telemetry_current.audio_inflight_bytes = audio_stats.inflight_bytes; self.telemetry_current.audio_inflight_bytes = audio_stats.inflight_bytes;
self.telemetry_current.audio_slots_occupied = audio_stats.slots_occupied as u32; self.telemetry_current.audio_slots_occupied = audio_stats.slots_occupied as u32;
self.telemetry_current.heap_used_bytes = vm.heap().used_bytes.load(Ordering::Relaxed);
self.telemetry_current.logs_count = self.log_service.logs_count;
}
if !self.logical_frame_active if !self.logical_frame_active
&& self.telemetry_last.frame_index == self.logical_frame_index.wrapping_sub(1) && self.telemetry_last.frame_index == self.logical_frame_index.wrapping_sub(1)
{ {
@ -187,6 +214,9 @@ impl VirtualMachineRuntime {
self.telemetry_last.audio_used_bytes = self.telemetry_current.audio_used_bytes; self.telemetry_last.audio_used_bytes = self.telemetry_current.audio_used_bytes;
self.telemetry_last.audio_inflight_bytes = self.telemetry_current.audio_inflight_bytes; self.telemetry_last.audio_inflight_bytes = self.telemetry_current.audio_inflight_bytes;
self.telemetry_last.audio_slots_occupied = self.telemetry_current.audio_slots_occupied; self.telemetry_last.audio_slots_occupied = self.telemetry_current.audio_slots_occupied;
self.telemetry_last.heap_used_bytes = self.telemetry_current.heap_used_bytes;
self.telemetry_last.heap_max_bytes = self.telemetry_current.heap_max_bytes;
self.telemetry_last.logs_count = self.telemetry_current.logs_count;
} }
None None

View File

@ -2,6 +2,9 @@ use crate::call_frame::CallFrame;
use crate::object::{ObjectHeader, ObjectKind}; use crate::object::{ObjectHeader, ObjectKind};
use prometeu_bytecode::{HeapRef, Value}; use prometeu_bytecode::{HeapRef, Value};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
/// Internal stored object: header plus opaque payload bytes. /// Internal stored object: header plus opaque payload bytes.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct StoredObject { pub struct StoredObject {
@ -22,6 +25,30 @@ pub struct StoredObject {
pub coroutine: Option<CoroutineData>, pub coroutine: Option<CoroutineData>,
} }
impl StoredObject {
/// Returns the approximate memory footprint of this object in bytes.
pub fn bytes(&self) -> usize {
let mut total = std::mem::size_of::<ObjectHeader>();
total += self.payload.capacity();
if let Some(elems) = &self.array_elems {
total += std::mem::size_of::<Vec<Value>>();
total += elems.capacity() * std::mem::size_of::<Value>();
}
if let Some(env) = &self.closure_env {
total += std::mem::size_of::<Vec<Value>>();
total += env.capacity() * std::mem::size_of::<Value>();
}
if let Some(coro) = &self.coroutine {
total += std::mem::size_of::<CoroutineData>();
total += coro.stack.capacity() * std::mem::size_of::<Value>();
total += coro.frames.capacity() * std::mem::size_of::<CallFrame>();
}
total
}
}
/// Execution state of a coroutine. /// Execution state of a coroutine.
#[derive(Debug, Copy, Clone, Eq, PartialEq)] #[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum CoroutineState { pub enum CoroutineState {
@ -49,14 +76,22 @@ pub struct Heap {
objects: Vec<Option<StoredObject>>, objects: Vec<Option<StoredObject>>,
// Reclaimed slots available for deterministic reuse (LIFO). // Reclaimed slots available for deterministic reuse (LIFO).
free_list: Vec<usize>, free_list: Vec<usize>,
/// Total bytes currently used by all objects in the heap.
pub used_bytes: Arc<AtomicUsize>,
} }
impl Heap { impl Heap {
pub fn new() -> Self { pub fn new() -> Self {
Self { objects: Vec::new(), free_list: Vec::new() } Self {
objects: Vec::new(),
free_list: Vec::new(),
used_bytes: Arc::new(AtomicUsize::new(0)),
}
} }
fn insert_object(&mut self, obj: StoredObject) -> HeapRef { fn insert_object(&mut self, obj: StoredObject) -> HeapRef {
self.used_bytes.fetch_add(obj.bytes(), Ordering::Relaxed);
if let Some(idx) = self.free_list.pop() { if let Some(idx) = self.free_list.pop() {
debug_assert!(self.objects.get(idx).is_some_and(|slot| slot.is_none())); debug_assert!(self.objects.get(idx).is_some_and(|slot| slot.is_none()));
self.objects[idx] = Some(obj); self.objects[idx] = Some(obj);
@ -363,6 +398,7 @@ impl Heap {
obj.header.set_marked(false); obj.header.set_marked(false);
} else { } else {
// Unreachable: reclaim by dropping and turning into tombstone. // Unreachable: reclaim by dropping and turning into tombstone.
self.used_bytes.fetch_sub(obj.bytes(), Ordering::Relaxed);
*slot = None; *slot = None;
self.free_list.push(idx); self.free_list.push(idx);
} }

View File

@ -146,10 +146,10 @@ impl Verifier {
let func_code = &code[func_start..func_end]; let func_code = &code[func_start..func_end];
// Funções vazias (sem qualquer byte de código) são consideradas válidas no verificador. // Empty functions (no code bytes) are considered valid in the verifier.
// Elas não consomem nem produzem valores na pilha e não possuem fluxo interno. // They do not consume or produce values on the stack and have no internal flow.
// Observação: se uma função vazia for chamada em tempo de execução e retorno/efeitos // Note: if an empty function is called at runtime and return/effects
// forem esperados, caberá ao gerador de código/linker impedir tal situação. // are expected, it is the responsibility of the code generator/linker to prevent this situation.
if func_code.is_empty() { if func_code.is_empty() {
return Ok(0); return Ok(0);
} }

View File

@ -140,6 +140,11 @@ impl VirtualMachine {
self.operand_stack[start..].iter().rev().cloned().collect() self.operand_stack[start..].iter().rev().cloned().collect()
} }
/// Returns a reference to the VM's heap.
pub fn heap(&self) -> &Heap {
&self.heap
}
/// Returns true if the VM has executed a HALT and is not currently running. /// Returns true if the VM has executed a HALT and is not currently running.
pub fn is_halted(&self) -> bool { pub fn is_halted(&self) -> bool {
self.halted self.halted

View File

@ -129,7 +129,7 @@ impl HostRunner {
let color_bg = Color::INDIGO; // Dark blue to stand out let color_bg = Color::INDIGO; // Dark blue to stand out
let color_warn = Color::RED; let color_warn = Color::RED;
self.hardware.gfx.fill_rect(5, 5, 175, 100, color_bg); self.hardware.gfx.fill_rect(5, 5, 175, 130, color_bg);
self.hardware.gfx.draw_text( self.hardware.gfx.draw_text(
10, 10,
10, 10,
@ -187,8 +187,16 @@ impl HostRunner {
); );
} }
self.hardware.gfx.draw_text(
10,
82,
&format!("RAM: {}KB", tel.heap_used_bytes / 1024),
color_text,
);
self.hardware.gfx.draw_text(10, 90, &format!("LOGS: {}", tel.logs_count), color_text);
let cert_color = if tel.violations > 0 { color_warn } else { color_text }; let cert_color = if tel.violations > 0 { color_warn } else { color_text };
self.hardware.gfx.draw_text(10, 82, &format!("CERT LAST: {}", tel.violations), cert_color); self.hardware.gfx.draw_text(10, 98, &format!("CERT LAST: {}", tel.violations), cert_color);
if tel.violations > 0 if tel.violations > 0
&& let Some(event) = self && let Some(event) = self
@ -204,7 +212,7 @@ impl HostRunner {
if msg.len() > 30 { if msg.len() > 30 {
msg.truncate(30); msg.truncate(30);
} }
self.hardware.gfx.draw_text(10, 90, &msg, color_warn); self.hardware.gfx.draw_text(10, 106, &msg, color_warn);
} }
if let Some(report) = self.firmware.os.last_crash_report.as_ref() { if let Some(report) = self.firmware.os.last_crash_report.as_ref() {
@ -212,7 +220,7 @@ impl HostRunner {
if msg.len() > 30 { if msg.len() > 30 {
msg.truncate(30); msg.truncate(30);
} }
self.hardware.gfx.draw_text(10, 98, &msg, color_warn); self.hardware.gfx.draw_text(10, 114, &msg, color_warn);
} }
} }
} }
@ -311,6 +319,9 @@ impl ApplicationHandler for HostRunner {
// 1. Process pending debug commands from the network. // 1. Process pending debug commands from the network.
self.debugger.check_commands(&mut self.firmware, &mut self.hardware); self.debugger.check_commands(&mut self.firmware, &mut self.hardware);
// Sync inspection mode state.
self.firmware.os.inspection_active = self.overlay_enabled || self.debugger.stream.is_some();
// 2. Maintain filesystem connection if it was lost (e.g., directory removed). // 2. Maintain filesystem connection if it was lost (e.g., directory removed).
if let Some(root) = &self.fs_root { if let Some(root) = &self.fs_root {
use prometeu_system::fs::FsState; use prometeu_system::fs::FsState;

View File

@ -1,4 +1,4 @@
{"type":"meta","next_id":{"DSC":23,"AGD":21,"DEC":7,"PLN":6,"LSN":26,"CLSN":1}} {"type":"meta","next_id":{"DSC":23,"AGD":21,"DEC":7,"PLN":6,"LSN":27,"CLSN":1}}
{"type":"discussion","id":"DSC-0020","status":"done","ticket":"jenkins-gitea-integration","title":"Jenkins Gitea Integration and Relocation","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["ci","jenkins","gitea"],"agendas":[{"id":"AGD-0018","file":"workflow/agendas/AGD-0018-jenkins-gitea-integration-and-relocation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"decisions":[{"id":"DEC-0003","file":"workflow/decisions/DEC-0003-jenkins-gitea-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0003","file":"workflow/plans/PLN-0003-jenkins-gitea-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0021","file":"lessons/DSC-0020-jenkins-gitea-integration/LSN-0021-jenkins-gitea-integration.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]} {"type":"discussion","id":"DSC-0020","status":"done","ticket":"jenkins-gitea-integration","title":"Jenkins Gitea Integration and Relocation","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["ci","jenkins","gitea"],"agendas":[{"id":"AGD-0018","file":"workflow/agendas/AGD-0018-jenkins-gitea-integration-and-relocation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"decisions":[{"id":"DEC-0003","file":"workflow/decisions/DEC-0003-jenkins-gitea-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0003","file":"workflow/plans/PLN-0003-jenkins-gitea-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0021","file":"lessons/DSC-0020-jenkins-gitea-integration/LSN-0021-jenkins-gitea-integration.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]}
{"type":"discussion","id":"DSC-0021","status":"done","ticket":"asset-entry-codec-enum-with-metadata","title":"Asset Entry Codec Enum Contract","created_at":"2026-04-09","updated_at":"2026-04-09","tags":["asset","runtime","codec","metadata"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0024","file":"lessons/DSC-0021-asset-entry-codec-enum-contract/LSN-0024-string-on-the-wire-enum-in-runtime.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]} {"type":"discussion","id":"DSC-0021","status":"done","ticket":"asset-entry-codec-enum-with-metadata","title":"Asset Entry Codec Enum Contract","created_at":"2026-04-09","updated_at":"2026-04-09","tags":["asset","runtime","codec","metadata"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0024","file":"lessons/DSC-0021-asset-entry-codec-enum-contract/LSN-0024-string-on-the-wire-enum-in-runtime.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]}
{"type":"discussion","id":"DSC-0022","status":"done","ticket":"tile-bank-vs-glyph-bank-domain-naming","title":"Glyph Bank Domain Naming Contract","created_at":"2026-04-09","updated_at":"2026-04-10","tags":["gfx","runtime","naming","domain-model"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0025","file":"lessons/DSC-0022-glyph-bank-domain-naming/LSN-0025-rename-artifact-by-meaning-not-by-token.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]} {"type":"discussion","id":"DSC-0022","status":"done","ticket":"tile-bank-vs-glyph-bank-domain-naming","title":"Glyph Bank Domain Naming Contract","created_at":"2026-04-09","updated_at":"2026-04-10","tags":["gfx","runtime","naming","domain-model"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0025","file":"lessons/DSC-0022-glyph-bank-domain-naming/LSN-0025-rename-artifact-by-meaning-not-by-token.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
@ -9,7 +9,7 @@
{"type":"discussion","id":"DSC-0005","status":"open","ticket":"system-fault-semantics-and-control-surface","title":"Agenda - System Fault Semantics and Control Surface","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0004","file":"workflow/agendas/AGD-0004-system-fault-semantics-and-control-surface.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0005","status":"open","ticket":"system-fault-semantics-and-control-surface","title":"Agenda - System Fault Semantics and Control Surface","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0004","file":"workflow/agendas/AGD-0004-system-fault-semantics-and-control-surface.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0006","status":"open","ticket":"vm-owned-random-service","title":"Agenda - VM-Owned Random Service","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0005","file":"workflow/agendas/AGD-0005-vm-owned-random-service.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0006","status":"open","ticket":"vm-owned-random-service","title":"Agenda - VM-Owned Random Service","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0005","file":"workflow/agendas/AGD-0005-vm-owned-random-service.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0007","status":"open","ticket":"app-home-filesystem-surface-and-semantics","title":"Agenda - App Home Filesystem Surface and Semantics","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0006","file":"workflow/agendas/AGD-0006-app-home-filesystem-surface-and-semantics.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0007","status":"open","ticket":"app-home-filesystem-surface-and-semantics","title":"Agenda - App Home Filesystem Surface and Semantics","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0006","file":"workflow/agendas/AGD-0006-app-home-filesystem-surface-and-semantics.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0008","status":"open","ticket":"perf-runtime-telemetry-hot-path","title":"Agenda - [PERF] Runtime Telemetry Hot Path","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0007","file":"workflow/agendas/AGD-0007-perf-runtime-telemetry-hot-path.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0008","status":"done","ticket":"perf-runtime-telemetry-hot-path","title":"Agenda - [PERF] Runtime Telemetry Hot Path","created_at":"2026-03-27","updated_at":"2026-04-10","tags":[],"agendas":[{"id":"AGD-0007","file":"workflow/agendas/AGD-0007-perf-runtime-telemetry-hot-path.md","status":"done","created_at":"2026-03-27","updated_at":"2026-04-10"}],"decisions":[{"id":"DEC-0005","file":"workflow/decisions/DEC-0005-perf-push-based-telemetry-model.md","status":"accepted","created_at":"2026-04-10","updated_at":"2026-04-10"}],"plans":[{"id":"PLN-0005","file":"workflow/plans/PLN-0005-perf-push-based-telemetry-implementation.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"lessons":[{"id":"LSN-0026","file":"lessons/DSC-0008-perf-runtime-telemetry-hot-path/LSN-0026-push-based-telemetry-model.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
{"type":"discussion","id":"DSC-0009","status":"open","ticket":"perf-async-background-work-lanes-for-assets-and-fs","title":"Agenda - [PERF] Async Background Work Lanes for Assets and FS","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0008","file":"workflow/agendas/AGD-0008-perf-async-background-work-lanes-for-assets-and-fs.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0009","status":"open","ticket":"perf-async-background-work-lanes-for-assets-and-fs","title":"Agenda - [PERF] Async Background Work Lanes for Assets and FS","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0008","file":"workflow/agendas/AGD-0008-perf-async-background-work-lanes-for-assets-and-fs.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0010","status":"open","ticket":"perf-host-desktop-frame-pacing-and-presentation","title":"Agenda - [PERF] Host Desktop Frame Pacing and Presentation","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0009","file":"workflow/agendas/AGD-0009-perf-host-desktop-frame-pacing-and-presentation.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0010","status":"open","ticket":"perf-host-desktop-frame-pacing-and-presentation","title":"Agenda - [PERF] Host Desktop Frame Pacing and Presentation","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0009","file":"workflow/agendas/AGD-0009-perf-host-desktop-frame-pacing-and-presentation.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-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":[]}

View File

@ -0,0 +1,37 @@
---
id: LSN-0026
ticket: perf-runtime-telemetry-hot-path
title: Push-based Telemetry Model
created: 2026-04-10
tags: [performance, telemetry, atomics]
---
# Push-based Telemetry Model
The PROMETEU telemetry system evolved from an on-demand scan model (pull) to an incremental counter model (push), aiming to minimize the impact on the runtime's hot path.
## The Original Problem
Previously, at every host tick, the runtime requested memory usage information from the asset banks. This resulted in:
- $O(n)$ scans over resource maps.
- Multiple read lock acquisitions in every tick.
- Unnecessary overhead on handheld hardware, where every microsecond counts.
## The Solution: Push Model with Atomics
The implemented solution uses `AtomicUsize` in drivers and the VM to maintain the system state in real-time with $O(1)$ read and write cost:
1. **Drivers (Assets):** Atomic counters in each `BankPolicy` are updated during `load`, `commit`, and `cancel`.
2. **VM (Heap):** A `used_bytes` counter in the `Heap` struct tracks allocations and deallocations (sweep).
3. **System (Logs):** The `LogService` tracks log pressure emitted in each frame.
## Two Levels of Observability
To balance performance and debugging, the collection was divided:
- **Frame Snapshot (Always):** Automatic capture at the end of each logical frame. Irrelevant cost ($O(1)$). Serves the `Certifier` and historical logs.
- **Host Tick (On-Demand):** Detailed collection in every tick only occurs if `inspection_active` is enabled (e.g., F1 Overlay on).
## Lessons Learned
- **Trigger Decoupling:** We should not use the `Certifier` state to enable visual debugging features (like the overlay), as they have different purposes and costs.
- **Eventual Consistency is Sufficient:** For telemetry metrics, it is not necessary to lock the system to obtain an exact value every nanosecond. Relaxed atomic reading is sufficient and much more performant.
- **Cost Isolation:** Moving the aggregation logic to the driver simplifies the runtime and ensures that the telemetry cost is paid only during state mutations, rather than repeatedly during stable execution.

View File

@ -1,77 +0,0 @@
---
id: AGD-0007
ticket: perf-runtime-telemetry-hot-path
title: Agenda - [PERF] Runtime Telemetry Hot Path
status: open
created: 2026-03-27
resolved:
decision:
tags: []
---
# Agenda - [PERF] Runtime Telemetry Hot Path
## Problema
O runtime cobra telemetria de asset bank no caminho quente de todo host tick.
Hoje, `tick()` consulta `bank_info()` para `gfx` e `audio` mesmo quando nenhum logical frame foi fechado. O custo de observabilidade acaba sendo pago continuamente pela execucao normal.
## Dor
- CPU e locks sao gastos em todos os ticks, nao apenas quando a metrica muda.
- hardware barato sofre mais com trabalho pequeno e repetitivo do que com picos raros.
- overlay, stats e certifier acabam puxando custo estrutural para o core do runtime.
## Hotspots Atuais
- [tick.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-system/src/virtual_machine_runtime/tick.rs#L167)
- [asset.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-drivers/src/asset.rs#L618)
## Alvo da Discussao
Remover varredura e agregacao lock-heavy do hot path do tick sem perder observabilidade util.
## O Que Precisa Ser Definido
1. Modelo de metrica.
Decidir o que passa a ser contador incremental e o que continua sendo snapshot sob demanda.
2. Frequencia de coleta.
Decidir se atualizacao acontece:
- no fechamento do logical frame;
- apenas com overlay/debug ativo;
- por amostragem periodica;
- por evento de mutacao (`load`, `commit`, `cancel`).
3. Responsabilidade da agregacao.
Delimitar se a verdade dos bytes/slots fica:
- no `AssetManager`;
- no runtime;
- em uma camada propria de telemetry cache.
4. Garantia de consistencia.
Decidir qual grau de defasagem e aceitavel para handheld barato:
- exato em tempo real;
- eventual por frame;
- eventual por tick de debug.
## Open Questions de Arquitetura
1. O certifier realmente precisa de snapshot de bank a cada tick?
2. O overlay pode ler uma versao resumida da telemetria em vez de recalcular tudo?
3. Vale manter caminho "preciso" so para testes/debug e caminho "barato" para runtime normal?
## Dependencias
- `../specs/10-debug-inspection-and-profiling.md`
- `../specs/16a-syscall-policies.md`
## Criterio de Saida Desta Agenda
Pode virar PR quando houver decisao escrita sobre:
- metrica incremental vs snapshot;
- ponto canonico de atualizacao da telemetria;
- custo maximo aceitavel no hot path do tick;
- comportamento de overlay/certifier sobre dados defasados.

View File

@ -52,19 +52,22 @@ Isolar o overlay de debug do custo medido do console sem perder utilidade para d
## Open Questions de Arquitetura ## Open Questions de Arquitetura
1. O overlay precisa ser representativo do hardware final ou apenas ferramenta de desktop? 1. O overlay precisa ser representativo do hardware final ou apenas ferramenta de desktop?
Não, como é HUD técnico, pode e deve ser renderizado pelo Host nativo para melhor legibilidade.
2. Vale um modo "perf puro" onde overlay nunca toca no framebuffer do console? 2. Vale um modo "perf puro" onde overlay nunca toca no framebuffer do console?
Sim. O isolamento garante que o `gfx` emulado esteja 100% livre para o jogo durante a medição.
3. O host deve oferecer toggles separados para stats, logs e overlay visual? 3. O host deve oferecer toggles separados para stats, logs e overlay visual?
Sim. O `HostRunner` deve expor controles granulares via `inspection_active`.
4. Como melhorar a legibilidade e estética (Glyphs/Transparência)?
Migrar a renderização do HUD para o Host Nativo (Winit/Pixels), permitindo o uso de fontes TrueType (monospaced) nítidas e Alpha Blending real para transparência no fundo do painel.
## Dependencias ## Dependencias
- `../specs/10-debug-inspection-and-profiling.md` - `../specs/10-debug-inspection-and-profiling.md`
- `../specs/11-portability-and-cross-platform-execution.md` - `../specs/11-portability-and-cross-platform-execution.md`
## Criterio de Saida Desta Agenda ## Sugestao / Recomendacao
Pode virar PR quando houver decisao escrita sobre: 1. **Migração para Camada Host Nativa:** Renderizar o HUD de debug em uma surface separada ou via pipeline nativo do Host (depois do upscaling do framebuffer do console).
2. **Fontes TrueType (Mono):** Substituir os glyphs bitmapped rudimentares por uma fonte nativa de alta qualidade e nítida.
- onde o overlay e composto; 3. **Composição Alpha:** Permitir fundo semi-transparente para o overlay para não bloquear a visão do jogo.
- politica de cache de texto/glyphs; 4. **Acionamento Explícito:** Host deve gerenciar `inspection_active: true` no runtime apenas quando o HUD ou Debugger estiverem ativos.
- como o custo do overlay aparece na telemetria;
- overhead maximo aceitavel em modo debug.