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
5 changed files with 206 additions and 8 deletions
Showing only changes of commit f0663856c3 - Show all commits

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

@ -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":"in_progress","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":[{"id":"AGD-0009","file":"workflow/agendas/AGD-0009-perf-host-desktop-frame-pacing-and-presentation.md","status":"in_progress","created_at":"2026-03-27","updated_at":"2026-04-20"}],"decisions":[{"id":"DEC-0019","file":"DEC-0019-host-desktop-frame-pacing-and-presentation.md","status":"accepted","created_at":"2026-04-20","updated_at":"2026-04-20","ref_agenda":"AGD-0009"}],"plans":[{"id":"PLN-0036","file":"PLN-0036-host-desktop-frame-pacing-and-presentation.md","status":"review","created_at":"2026-04-20","updated_at":"2026-04-20","ref_decisions":["DEC-0019"]}],"lessons":[]} {"type":"discussion","id":"DSC-0010","status":"in_progress","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":[{"id":"AGD-0009","file":"workflow/agendas/AGD-0009-perf-host-desktop-frame-pacing-and-presentation.md","status":"in_progress","created_at":"2026-03-27","updated_at":"2026-04-20"}],"decisions":[{"id":"DEC-0019","file":"DEC-0019-host-desktop-frame-pacing-and-presentation.md","status":"accepted","created_at":"2026-04-20","updated_at":"2026-04-20","ref_agenda":"AGD-0009"}],"plans":[{"id":"PLN-0036","file":"PLN-0036-host-desktop-frame-pacing-and-presentation.md","status":"done","created_at":"2026-04-20","updated_at":"2026-04-20","ref_decisions":["DEC-0019"]}],"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":[]}
{"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

@ -2,9 +2,9 @@
id: PLN-0036 id: PLN-0036
ticket: perf-host-desktop-frame-pacing-and-presentation ticket: perf-host-desktop-frame-pacing-and-presentation
title: Plan - Host Desktop Frame Pacing and Presentation title: Plan - Host Desktop Frame Pacing and Presentation
status: review status: done
created: 2026-04-20 created: 2026-04-20
completed: completed: 2026-04-20
tags: [perf, host, desktop, frame-pacing, presentation, debug] tags: [perf, host, desktop, frame-pacing, presentation, 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**: