dev/perf-host-desktop-frame-pacing-and-presentation #19

Merged
bquarkz merged 4 commits from dev/perf-host-desktop-frame-pacing-and-presentation into master 2026-04-20 12:19:51 +00:00
6 changed files with 267 additions and 86 deletions

View File

@ -20,6 +20,72 @@ use winit::event_loop::{ActiveEventLoop, ControlFlow};
use winit::keyboard::{KeyCode, PhysicalKey}; use winit::keyboard::{KeyCode, PhysicalKey};
use winit::window::{Window, WindowAttributes, WindowId}; use winit::window::{Window, WindowAttributes, WindowId};
const IDLE_HOST_POLL_DT: Duration = Duration::from_millis(100);
#[derive(Debug, Clone)]
struct PresentationState {
latest_published_frame: u64,
last_presented_frame: Option<u64>,
host_invalidated: bool,
redraw_requested: bool,
}
impl Default for PresentationState {
fn default() -> Self {
Self {
latest_published_frame: 0,
last_presented_frame: None,
host_invalidated: true,
redraw_requested: false,
}
}
}
impl PresentationState {
fn note_published_frame(&mut self, frame_index: u64) {
if frame_index > self.latest_published_frame {
self.latest_published_frame = frame_index;
}
}
fn invalidate_host_surface(&mut self) {
self.host_invalidated = true;
}
fn needs_redraw(&self) -> bool {
self.host_invalidated || self.last_presented_frame != Some(self.latest_published_frame)
}
fn should_request_redraw(&mut self) -> bool {
if self.needs_redraw() && !self.redraw_requested {
self.redraw_requested = true;
return true;
}
false
}
fn mark_presented(&mut self) {
self.redraw_requested = false;
self.host_invalidated = false;
self.last_presented_frame = Some(self.latest_published_frame);
}
}
fn desired_control_flow(
now: Instant,
machine_running: bool,
accumulator: Duration,
frame_target_dt: Duration,
) -> ControlFlow {
if machine_running {
let remaining = frame_target_dt.saturating_sub(accumulator);
ControlFlow::WaitUntil(now + remaining)
} else {
ControlFlow::WaitUntil(now + IDLE_HOST_POLL_DT)
}
}
/// The Desktop implementation of the PROMETEU Runtime. /// The Desktop implementation of the PROMETEU Runtime.
/// ///
/// This struct acts as the physical "chassis" of the virtual console. It is /// This struct acts as the physical "chassis" of the virtual console. It is
@ -69,6 +135,8 @@ pub struct HostRunner {
audio: HostAudio, audio: HostAudio,
/// Last known pause state to sync with audio. /// Last known pause state to sync with audio.
last_paused_state: bool, last_paused_state: bool,
/// Tracks whether a new logical frame or host-only surface invalidation requires presentation.
presentation: PresentationState,
} }
impl HostRunner { impl HostRunner {
@ -105,6 +173,7 @@ impl HostRunner {
overlay_enabled: false, overlay_enabled: false,
audio: HostAudio::new(), audio: HostAudio::new(),
last_paused_state: false, last_paused_state: false,
presentation: PresentationState::default(),
} }
} }
@ -123,6 +192,20 @@ impl HostRunner {
w.request_redraw(); w.request_redraw();
} }
} }
fn invalidate_host_surface(&mut self) {
self.presentation.invalidate_host_surface();
}
fn request_redraw_if_needed(&mut self) {
if self.presentation.should_request_redraw() {
self.request_redraw();
}
}
fn machine_running(&self) -> bool {
!self.firmware.os.paused && !self.debugger.waiting_for_start
}
} }
impl ApplicationHandler for HostRunner { impl ApplicationHandler for HostRunner {
@ -157,7 +240,9 @@ impl ApplicationHandler for HostRunner {
eprintln!("[HostAudio] Disabled: {}", err); eprintln!("[HostAudio] Disabled: {}", err);
} }
event_loop.set_control_flow(ControlFlow::Poll); self.invalidate_host_surface();
self.request_redraw_if_needed();
event_loop.set_control_flow(ControlFlow::Wait);
} }
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) { fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
@ -168,11 +253,15 @@ impl ApplicationHandler for HostRunner {
WindowEvent::Resized(size) => { WindowEvent::Resized(size) => {
self.resize_surface(size.width, size.height); self.resize_surface(size.width, size.height);
self.invalidate_host_surface();
self.request_redraw_if_needed();
} }
WindowEvent::ScaleFactorChanged { .. } => { WindowEvent::ScaleFactorChanged { .. } => {
let size = self.window().inner_size(); let size = self.window().inner_size();
self.resize_surface(size.width, size.height); self.resize_surface(size.width, size.height);
self.invalidate_host_surface();
self.request_redraw_if_needed();
} }
WindowEvent::RedrawRequested => { WindowEvent::RedrawRequested => {
@ -198,6 +287,8 @@ impl ApplicationHandler for HostRunner {
if pixels.render().is_err() { if pixels.render().is_err() {
event_loop.exit(); event_loop.exit();
} else {
self.presentation.mark_presented();
} }
} }
@ -207,11 +298,15 @@ impl ApplicationHandler for HostRunner {
if is_down && code == KeyCode::KeyD && self.debugger.waiting_for_start { if is_down && code == KeyCode::KeyD && self.debugger.waiting_for_start {
self.debugger.waiting_for_start = false; self.debugger.waiting_for_start = false;
self.invalidate_host_surface();
self.request_redraw_if_needed();
println!("[Debugger] Execution started!"); println!("[Debugger] Execution started!");
} }
if is_down && code == KeyCode::F1 { if is_down && code == KeyCode::F1 {
self.overlay_enabled = !self.overlay_enabled; self.overlay_enabled = !self.overlay_enabled;
self.invalidate_host_surface();
self.request_redraw_if_needed();
} }
} }
} }
@ -222,7 +317,11 @@ impl ApplicationHandler for HostRunner {
/// Called by `winit` when the application is idle and ready to perform updates. /// Called by `winit` when the application is idle and ready to perform updates.
/// This is where the core execution loop lives. /// This is where the core execution loop lives.
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
let was_debugger_connected = self.debugger.stream.is_some();
let was_waiting_for_start = self.debugger.waiting_for_start;
let was_paused = self.firmware.os.paused;
// 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);
@ -283,6 +382,15 @@ impl ApplicationHandler for HostRunner {
self.stats.record_frame(); self.stats.record_frame();
} }
self.presentation.note_published_frame(self.firmware.os.logical_frame_index);
if was_debugger_connected != self.debugger.stream.is_some()
|| was_waiting_for_start != self.debugger.waiting_for_start
|| was_paused != self.firmware.os.paused
{
self.invalidate_host_surface();
}
// 4. Feedback and Synchronization. // 4. Feedback and Synchronization.
self.audio.update_stats(&mut self.stats); self.audio.update_stats(&mut self.stats);
@ -298,9 +406,15 @@ impl ApplicationHandler for HostRunner {
}; };
self.log_sink.process_events(new_events); self.log_sink.process_events(new_events);
// 5. Request redraw so the host surface can present the latest machine frame // 5. Request redraw only when a new logical frame was published or the
// and, when enabled, compose the overlay in the host-only RGBA surface. // host-owned presentation surface became invalid.
self.request_redraw(); self.request_redraw_if_needed();
event_loop.set_control_flow(desired_control_flow(
Instant::now(),
self.machine_running(),
self.accumulator,
self.frame_target_dt,
));
} }
} }
@ -313,6 +427,70 @@ mod tests {
use std::io::{Read, Write}; use std::io::{Read, Write};
use std::net::TcpStream; use std::net::TcpStream;
#[test]
fn presentation_state_requires_initial_redraw_only_once() {
let mut state = PresentationState::default();
assert!(state.should_request_redraw());
assert!(!state.should_request_redraw());
state.mark_presented();
assert!(!state.should_request_redraw());
}
#[test]
fn presentation_state_requests_redraw_for_new_frame_publication() {
let mut state = PresentationState::default();
state.mark_presented();
state.note_published_frame(1);
assert!(state.should_request_redraw());
state.mark_presented();
assert_eq!(state.last_presented_frame, Some(1));
}
#[test]
fn presentation_state_requests_redraw_for_host_invalidation_without_new_frame() {
let mut state = PresentationState::default();
state.mark_presented();
state.invalidate_host_surface();
assert!(state.should_request_redraw());
state.mark_presented();
assert!(!state.needs_redraw());
}
#[test]
fn desired_control_flow_waits_until_next_logical_deadline_while_running() {
let now = Instant::now();
let flow =
desired_control_flow(now, true, Duration::from_millis(5), Duration::from_millis(16));
match flow {
ControlFlow::WaitUntil(deadline) => {
assert_eq!(deadline.duration_since(now), Duration::from_millis(11));
}
other => panic!("unexpected control flow: {:?}", other),
}
}
#[test]
fn desired_control_flow_uses_idle_poll_when_machine_is_not_running() {
let now = Instant::now();
let flow =
desired_control_flow(now, false, Duration::from_millis(5), Duration::from_millis(16));
match flow {
ControlFlow::WaitUntil(deadline) => {
assert_eq!(deadline.duration_since(now), IDLE_HOST_POLL_DT);
}
other => panic!("unexpected control flow: {:?}", other),
}
}
#[test] #[test]
fn host_debugger_maps_cert_events_from_host_owned_sources() { fn host_debugger_maps_cert_events_from_host_owned_sources() {
let telemetry = TelemetryFrame { let telemetry = TelemetryFrame {

View File

@ -1,4 +1,4 @@
{"type":"meta","next_id":{"DSC":29,"AGD":29,"DEC":19,"PLN":36,"LSN":36,"CLSN":1}} {"type":"meta","next_id":{"DSC":29,"AGD":29,"DEC":20,"PLN":37,"LSN":37,"CLSN":1}}
{"type":"discussion","id":"DSC-0023","status":"done","ticket":"perf-full-migration-to-atomic-telemetry","title":"Agenda - [PERF] Full Migration to Atomic Telemetry","created_at":"2026-04-10","updated_at":"2026-04-10","tags":["perf","runtime","telemetry"],"agendas":[{"id":"AGD-0021","file":"workflow/agendas/AGD-0021-full-migration-to-atomic-telemetry.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"decisions":[{"id":"DEC-0008","file":"workflow/decisions/DEC-0008-full-migration-to-atomic-telemetry.md","status":"accepted","created_at":"2026-04-10","updated_at":"2026-04-10"}],"plans":[{"id":"PLN-0007","file":"workflow/plans/PLN-0007-full-migration-to-atomic-telemetry.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"lessons":[{"id":"LSN-0028","file":"lessons/DSC-0023-perf-full-migration-to-atomic-telemetry/LSN-0028-converging-to-single-atomic-telemetry-source.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]} {"type":"discussion","id":"DSC-0023","status":"done","ticket":"perf-full-migration-to-atomic-telemetry","title":"Agenda - [PERF] Full Migration to Atomic Telemetry","created_at":"2026-04-10","updated_at":"2026-04-10","tags":["perf","runtime","telemetry"],"agendas":[{"id":"AGD-0021","file":"workflow/agendas/AGD-0021-full-migration-to-atomic-telemetry.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"decisions":[{"id":"DEC-0008","file":"workflow/decisions/DEC-0008-full-migration-to-atomic-telemetry.md","status":"accepted","created_at":"2026-04-10","updated_at":"2026-04-10"}],"plans":[{"id":"PLN-0007","file":"workflow/plans/PLN-0007-full-migration-to-atomic-telemetry.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"lessons":[{"id":"LSN-0028","file":"lessons/DSC-0023-perf-full-migration-to-atomic-telemetry/LSN-0028-converging-to-single-atomic-telemetry-source.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
{"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"}]}
@ -12,7 +12,7 @@
{"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":"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-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":"done","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-04-20","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0036","file":"lessons/DSC-0010-perf-host-desktop-frame-pacing-and-presentation/LSN-0036-frame-publication-and-host-invalidation-must-be-separate.md","status":"done","created_at":"2026-04-20","updated_at":"2026-04-20"}]}
{"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":"done","ticket":"perf-runtime-introspection-syscalls","title":"Agenda - [PERF] Runtime Introspection Syscalls","created_at":"2026-03-27","updated_at":"2026-04-19","tags":["perf","runtime","syscall","telemetry","debug","asset"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0034","file":"lessons/DSC-0012-perf-runtime-introspection-syscalls/LSN-0034-host-owned-debug-boundaries.md","status":"done","created_at":"2026-04-19","updated_at":"2026-04-19"}]} {"type":"discussion","id":"DSC-0012","status":"done","ticket":"perf-runtime-introspection-syscalls","title":"Agenda - [PERF] Runtime Introspection Syscalls","created_at":"2026-03-27","updated_at":"2026-04-19","tags":["perf","runtime","syscall","telemetry","debug","asset"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0034","file":"lessons/DSC-0012-perf-runtime-introspection-syscalls/LSN-0034-host-owned-debug-boundaries.md","status":"done","created_at":"2026-04-19","updated_at":"2026-04-19"}]}
{"type":"discussion","id":"DSC-0013","status":"done","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":[],"decisions":[],"plans":[],"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":"done","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":[],"decisions":[],"plans":[],"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"}]}

View File

@ -0,0 +1,62 @@
---
id: LSN-0036
ticket: perf-host-desktop-frame-pacing-and-presentation
title: Frame Publication and Host Invalidation Must Be Separate
created: 2026-04-20
tags: [performance, host, desktop, presentation, frame-pacing, debug]
---
# Frame Publication and Host Invalidation Must Be Separate
## Context
The desktop host was waking aggressively with `ControlFlow::Poll`, requesting redraw continuously, and converting the full RGB565 framebuffer to RGBA8 on every redraw even when the machine had not published a new logical frame.
That design hid real presentation cost, wasted CPU on stable screens, and blurred an important boundary: the runtime owns logical frame production, while the host owns window invalidation, overlays, and debugger presentation.
## Key Decisions
### Published logical frames are the canonical machine-side redraw trigger
**What:**
The host presentation loop now treats "a new logical frame was published" as the primary reason to redraw the machine image.
**Why:**
PROMETEU portability and timing depend on logical frames, not on a host polling loop. If the host redraws just to discover whether a frame changed, presentation cost stops reflecting actual machine behavior.
**Trade-offs:**
This requires an explicit or at least stable host-side way to observe frame publication. The extra state tracking is worth it because it keeps redraw policy honest and testable.
### Host-only invalidation is a separate redraw cause
**What:**
Resize, expose, overlay toggle, and debugger-visible transitions remain valid redraw causes, but they are treated as host-owned invalidation rather than machine frame production.
**Why:**
The host still has to repair or recomposite its visible surface when the OS window changes or a technical HUD becomes visible. That need is real, but it must not be confused with "the machine emitted another frame."
**Trade-offs:**
The host needs separate invalidation bookkeeping. In return, paused or stable machine state no longer forces continuous framebuffer conversion.
## Patterns and Algorithms
- Use two independent signals in presentation code:
- latest published logical frame;
- host-owned surface invalidation.
- Request redraw only when one of those signals changes and only once per pending update.
- After presentation completes, mark the current logical frame as presented and clear host invalidation.
- When the machine is running, wait until the next logical deadline instead of spinning continuously.
- When the machine is paused or waiting for debugger start, allow low-frequency host wakeups or OS events, but keep the last valid image until a real invalidation occurs.
## Pitfalls
- Do not use redraw polling as a substitute for a missing publication signal. That only hides the architectural gap.
- Do not let host overlays imply extra logical frame production. Host HUDs may change the visible surface without changing machine state.
- Do not promote host-only invalidation into guest-visible ABI. The runtime-host handshake may need internal state, but the cartridge contract does not.
- Do not reopen render-backend architecture just to fix pacing. Dirty regions or GPU offload are separate optimization questions.
## Takeaways
- A stable screen is a first-class state and should not cost continuous presentation work.
- Frame production and host invalidation are different events and should remain different in code.
- Event-driven redraw policy is easier to test, cheaper to run, and more faithful to the machine contract than polling-driven presentation.

View File

@ -1,79 +0,0 @@
---
id: AGD-0009
ticket: perf-host-desktop-frame-pacing-and-presentation
title: Agenda - [PERF] Host Desktop Frame Pacing and Presentation
status: open
created: 2026-03-27
resolved:
decision:
tags: []
---
# Agenda - [PERF] Host Desktop Frame Pacing and Presentation
## Problema
O host desktop ainda roda em modo agressivo de polling e apresentacao continua.
Hoje o loop usa `ControlFlow::Poll`, pede redraw incondicionalmente e converte o framebuffer inteiro de RGB565 para RGBA8 a cada `RedrawRequested`, mesmo quando nao ha novo frame logico.
## Dor
- CPU fica ocupada sem ganho visual.
- port de referencia no desktop mascara problemas de pacing em hardware barato.
- a conta de energia/temperatura piora mesmo quando a VM esta ociosa.
## Hotspots Atuais
- [runner.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/host/prometeu-host-desktop-winit/src/runner.rs#L252)
- [runner.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/host/prometeu-host-desktop-winit/src/runner.rs#L270)
- [runner.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/host/prometeu-host-desktop-winit/src/runner.rs#L390)
## Alvo da Discussao
Fechar uma politica de pacing/apresentacao host-driven que nao desperdice CPU quando nao existe frame novo.
## O Que Precisa Ser Definido
1. Gatilho de redraw.
Decidir se redraw acontece:
- apenas com logical frame pronto;
- por deadline de vsync;
- por dirty flag do front buffer;
- por evento externo relevante.
2. Politica do event loop.
Decidir entre:
- `Wait`;
- `WaitUntil`;
- `Poll` apenas em modo debug/profiling.
3. Conversao de framebuffer.
Definir se a conversao RGB565 -> RGBA8:
- continua full-frame;
- passa a ser dirty-region;
- sai da CPU e vai para shader/path especifico do host.
4. Modo ocioso.
Delimitar comportamento quando VM esta pausada, em breakpoint ou sem cart.
## Open Questions de Arquitetura
1. O host desktop deve ser referencia conservadora de energia ou apenas shell de desenvolvimento?
2. O runtime precisa expor um sinal explicito de "novo frame disponivel" para o host?
3. Existe necessidade real de redraw continuo quando o overlay esta desligado?
## Dependencias
- `../specs/01-time-model-and-cycles.md`
- `../specs/10-debug-inspection-and-profiling.md`
- `../specs/11-portability-and-cross-platform-execution.md`
## Criterio de Saida Desta Agenda
Pode virar PR quando houver decisao escrita sobre:
- politica de control flow do host;
- criterio canonico de redraw;
- estrategia de conversao/apresentacao de framebuffer;
- comportamento de idle/pause/debug.

View File

@ -131,6 +131,16 @@ The host may:
- observe buffers separately - observe buffers separately
- identify excessive redraw - identify excessive redraw
Host-owned overlay or inspection surfaces MUST observe the last published logical frame rather than force continuous redraw to probe for changes.
When overlay, debugger, or other host-only diagnostics become visually stable:
- the host MUST preserve the last valid visible image;
- the host MUST NOT keep recomposing the surface by continuous polling alone;
- additional redraw is only justified by a newly published logical frame or a host-owned invalidation event such as overlay toggle, resize, expose, or explicit debugger-visible state transition.
Paused or breakpointed execution does not grant permission to swap logical machine buffers just to sustain a host-only HUD.
## 6 Time Profiling (Cycles) ## 6 Time Profiling (Cycles)
### 6.1 Per-Frame Measurement ### 6.1 Per-Frame Measurement

View File

@ -127,6 +127,14 @@ The platform layer:
- **may overlay technical HUDs without modifying the logical framebuffer** - **may overlay technical HUDs without modifying the logical framebuffer**
- may transport the logical framebuffer into a host presentation surface where a host-only overlay layer is composed - may transport the logical framebuffer into a host presentation surface where a host-only overlay layer is composed
Host presentation SHOULD be driven by published logical frames and explicit host-owned invalidation, not by perpetual redraw polling.
In particular:
- a stable logical frame MAY remain visible across multiple host wakeups without recomposition;
- a host MAY redraw the same logical frame again when its own surface is invalidated by resize, expose, or host-only overlay/debug changes;
- a host MUST NOT invent intermediate logical frames or require continuous redraw merely to discover whether a new logical frame exists.
## 9 Debug and Inspection Isolation ## 9 Debug and Inspection Isolation
To preserve portability and certification purity, technical inspection tools (like the Debug Overlay) are moved to the Host layer. To preserve portability and certification purity, technical inspection tools (like the Debug Overlay) are moved to the Host layer.
@ -144,6 +152,8 @@ Inspection is facilitated by a lockless, push-based atomic interface:
2. **Asynchronous Observation:** The Host layer reads snapshots of these counters at its own display frequency. 2. **Asynchronous Observation:** The Host layer reads snapshots of these counters at its own display frequency.
3. **Loop Purity:** This ensures that the VM execution loop remains deterministic and free from synchronization overhead (locks) that could vary across host architectures. 3. **Loop Purity:** This ensures that the VM execution loop remains deterministic and free from synchronization overhead (locks) that could vary across host architectures.
Reading host-owned telemetry does not imply a perpetual presentation loop. The host may wake on its own schedule for inspection purposes while still presenting the logical framebuffer only when a published frame or host-owned invalidation requires it.
## 10 File System and Persistence ## 10 File System and Persistence
PROMETEU defines a **sandbox logical filesystem**: PROMETEU defines a **sandbox logical filesystem**: