[PERF] Host Desktop Frame Pacing and Presentation

This commit is contained in:
bQUARKz 2026-04-20 09:52:59 +01:00
parent f622387f4a
commit f0663856c3
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
5 changed files with 206 additions and 8 deletions

View File

@ -20,6 +20,72 @@ use winit::event_loop::{ActiveEventLoop, ControlFlow};
use winit::keyboard::{KeyCode, PhysicalKey};
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.
///
/// This struct acts as the physical "chassis" of the virtual console. It is
@ -69,6 +135,8 @@ pub struct HostRunner {
audio: HostAudio,
/// Last known pause state to sync with audio.
last_paused_state: bool,
/// Tracks whether a new logical frame or host-only surface invalidation requires presentation.
presentation: PresentationState,
}
impl HostRunner {
@ -105,6 +173,7 @@ impl HostRunner {
overlay_enabled: false,
audio: HostAudio::new(),
last_paused_state: false,
presentation: PresentationState::default(),
}
}
@ -123,6 +192,20 @@ impl HostRunner {
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 {
@ -157,7 +240,9 @@ impl ApplicationHandler for HostRunner {
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) {
@ -168,11 +253,15 @@ impl ApplicationHandler for HostRunner {
WindowEvent::Resized(size) => {
self.resize_surface(size.width, size.height);
self.invalidate_host_surface();
self.request_redraw_if_needed();
}
WindowEvent::ScaleFactorChanged { .. } => {
let size = self.window().inner_size();
self.resize_surface(size.width, size.height);
self.invalidate_host_surface();
self.request_redraw_if_needed();
}
WindowEvent::RedrawRequested => {
@ -198,6 +287,8 @@ impl ApplicationHandler for HostRunner {
if pixels.render().is_err() {
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 {
self.debugger.waiting_for_start = false;
self.invalidate_host_surface();
self.request_redraw_if_needed();
println!("[Debugger] Execution started!");
}
if is_down && code == KeyCode::F1 {
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.
/// 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.
self.debugger.check_commands(&mut self.firmware, &mut self.hardware);
@ -283,6 +382,15 @@ impl ApplicationHandler for HostRunner {
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.
self.audio.update_stats(&mut self.stats);
@ -298,9 +406,15 @@ impl ApplicationHandler for HostRunner {
};
self.log_sink.process_events(new_events);
// 5. Request redraw so the host surface can present the latest machine frame
// and, when enabled, compose the overlay in the host-only RGBA surface.
self.request_redraw();
// 5. Request redraw only when a new logical frame was published or the
// host-owned presentation surface became invalid.
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::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]
fn host_debugger_maps_cert_events_from_host_owned_sources() {
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-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-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-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"}]}

View File

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

View File

@ -131,6 +131,16 @@ The host may:
- observe buffers separately
- 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.1 Per-Frame Measurement

View File

@ -127,6 +127,14 @@ The platform layer:
- **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
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
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.
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
PROMETEU defines a **sandbox logical filesystem**: