diff --git a/crates/host/prometeu-host-desktop-winit/src/runner.rs b/crates/host/prometeu-host-desktop-winit/src/runner.rs index 79f10c29..fd0468fe 100644 --- a/crates/host/prometeu-host-desktop-winit/src/runner.rs +++ b/crates/host/prometeu-host-desktop-winit/src/runner.rs @@ -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, + 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 { diff --git a/discussion/index.ndjson b/discussion/index.ndjson index fa97b69b..9d953d86 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -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-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"}]} @@ -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":"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-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"}]} diff --git a/discussion/lessons/DSC-0010-perf-host-desktop-frame-pacing-and-presentation/LSN-0036-frame-publication-and-host-invalidation-must-be-separate.md b/discussion/lessons/DSC-0010-perf-host-desktop-frame-pacing-and-presentation/LSN-0036-frame-publication-and-host-invalidation-must-be-separate.md new file mode 100644 index 00000000..62d3bde9 --- /dev/null +++ b/discussion/lessons/DSC-0010-perf-host-desktop-frame-pacing-and-presentation/LSN-0036-frame-publication-and-host-invalidation-must-be-separate.md @@ -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. diff --git a/discussion/workflow/agendas/AGD-0009-perf-host-desktop-frame-pacing-and-presentation.md b/discussion/workflow/agendas/AGD-0009-perf-host-desktop-frame-pacing-and-presentation.md deleted file mode 100644 index e2cf62f3..00000000 --- a/discussion/workflow/agendas/AGD-0009-perf-host-desktop-frame-pacing-and-presentation.md +++ /dev/null @@ -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. diff --git a/docs/specs/runtime/10-debug-inspection-and-profiling.md b/docs/specs/runtime/10-debug-inspection-and-profiling.md index 1c656380..4221ab0f 100644 --- a/docs/specs/runtime/10-debug-inspection-and-profiling.md +++ b/docs/specs/runtime/10-debug-inspection-and-profiling.md @@ -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 diff --git a/docs/specs/runtime/11-portability-and-cross-platform-execution.md b/docs/specs/runtime/11-portability-and-cross-platform-execution.md index dac986f4..82d6fe1a 100644 --- a/docs/specs/runtime/11-portability-and-cross-platform-execution.md +++ b/docs/specs/runtime/11-portability-and-cross-platform-execution.md @@ -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**: