diff --git a/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs b/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs index 453b63f3..715953db 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs @@ -14,6 +14,46 @@ pub enum RenderSubmissionError { PacketAppModeMismatch { active: AppMode }, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RenderPacingPolicy { + FrameScheduled, + LifecycleDriven, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RenderExecutionPolicy { + LocalSynchronous, + WorkerCapableWithLocalFallback, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RenderPolicy { + pub app_mode: AppMode, + pub pacing: RenderPacingPolicy, + pub execution: RenderExecutionPolicy, +} + +impl RenderPolicy { + pub const fn for_app_mode(app_mode: AppMode) -> Self { + match app_mode { + AppMode::Game => Self { + app_mode, + pacing: RenderPacingPolicy::FrameScheduled, + execution: RenderExecutionPolicy::WorkerCapableWithLocalFallback, + }, + AppMode::Shell => Self { + app_mode, + pacing: RenderPacingPolicy::LifecycleDriven, + execution: RenderExecutionPolicy::LocalSynchronous, + }, + } + } + + pub const fn uses_frame_scheduler(self) -> bool { + matches!(self.pacing, RenderPacingPolicy::FrameScheduled) + } +} + pub trait RenderSurface { fn consume_submission(&mut self, submission: &RenderSubmission); } @@ -69,6 +109,7 @@ impl RenderHandoff { #[derive(Debug, Clone)] pub struct RenderManager { active_app_mode: AppMode, + active_policy: RenderPolicy, next_frame_id: FrameId, handoff: RenderHandoff, transition_state: RenderTransitionState, @@ -78,6 +119,7 @@ impl RenderManager { pub fn new(active_app_mode: AppMode) -> Self { Self { active_app_mode, + active_policy: RenderPolicy::for_app_mode(active_app_mode), next_frame_id: FrameId::ZERO, handoff: RenderHandoff::default(), transition_state: RenderTransitionState::Idle, @@ -88,6 +130,14 @@ impl RenderManager { self.active_app_mode } + pub fn active_render_policy(&self) -> RenderPolicy { + self.active_policy + } + + pub fn policy_for_app_mode(app_mode: AppMode) -> RenderPolicy { + RenderPolicy::for_app_mode(app_mode) + } + pub fn transition_state(&self) -> RenderTransitionState { self.transition_state } @@ -119,6 +169,7 @@ impl RenderManager { let previous = self.active_app_mode; self.active_app_mode = app_mode; + self.active_policy = RenderPolicy::for_app_mode(app_mode); self.transition_state = RenderTransitionState::Pending { from: previous, to: app_mode }; } @@ -239,6 +290,35 @@ mod tests { assert_eq!(manager.transition_state(), RenderTransitionState::Idle); } + #[test] + fn render_policy_maps_game_to_frame_scheduled_worker_capable() { + let policy = RenderPolicy::for_app_mode(AppMode::Game); + + assert_eq!(policy.pacing, RenderPacingPolicy::FrameScheduled); + assert_eq!(policy.execution, RenderExecutionPolicy::WorkerCapableWithLocalFallback); + assert!(policy.uses_frame_scheduler()); + } + + #[test] + fn render_policy_maps_shell_to_lifecycle_driven_local_sync() { + let policy = RenderPolicy::for_app_mode(AppMode::Shell); + + assert_eq!(policy.pacing, RenderPacingPolicy::LifecycleDriven); + assert_eq!(policy.execution, RenderExecutionPolicy::LocalSynchronous); + assert!(!policy.uses_frame_scheduler()); + } + + #[test] + fn active_render_policy_follows_app_mode_transition() { + let mut manager = RenderManager::new(AppMode::Game); + + assert_eq!(manager.active_render_policy(), RenderPolicy::for_app_mode(AppMode::Game)); + + manager.set_active_app_mode(AppMode::Shell); + + assert_eq!(manager.active_render_policy(), RenderPolicy::for_app_mode(AppMode::Shell)); + } + #[test] fn publish_latest_hands_submission_to_surface() { let mut manager = RenderManager::new(AppMode::Game); diff --git a/crates/console/prometeu-system/src/services/vm_runtime/tests.rs b/crates/console/prometeu-system/src/services/vm_runtime/tests.rs index b931c981..66ed5bb4 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/tests.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/tests.rs @@ -484,6 +484,8 @@ fn tick_shell_profile_closes_gfxui_commands_into_shell_packet() { assert_eq!(packet.commands.len(), 1); assert!(matches!(packet.commands[0], prometeu_hal::GfxUiCommand::Clear { .. })); assert_eq!(hardware.gfx.front_buffer()[0], Color::from_raw(0x11223344).raw()); + assert_eq!(runtime.frame_scheduler.completed_game_frames(), 0); + assert_eq!(runtime.frame_scheduler.active_game_frame_id(), None); } #[test] @@ -833,6 +835,7 @@ fn tick_game_submissions_are_immutable_and_latest_complete_wins() { prometeu_hal::Gfx2dCommand::Clear { color } if color == Color::from_raw(0x11223344) )); assert_eq!(hardware.gfx.front_buffer()[0], Color::from_raw(0x11223344).raw()); + assert_eq!(runtime.frame_scheduler.completed_game_frames(), 1); let second_packet = prometeu_hal::Game2DFramePacket::new( Default::default(), diff --git a/crates/console/prometeu-system/src/services/vm_runtime/tick.rs b/crates/console/prometeu-system/src/services/vm_runtime/tick.rs index e242b805..7046fa4b 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/tick.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/tick.rs @@ -141,7 +141,9 @@ impl VirtualMachineRuntime { self.update_fs(log_service, fs, fs_state); if !self.logical_frame_active { - if self.current_cartridge_app_mode == AppMode::Game { + if RenderManager::policy_for_app_mode(self.current_cartridge_app_mode) + .uses_frame_scheduler() + { self.frame_scheduler .authorize_game_frame() .expect("logical frame cannot start while another Game frame is active"); @@ -322,7 +324,9 @@ impl VirtualMachineRuntime { self.logical_frame_index += 1; self.logical_frame_active = false; self.logical_frame_remaining_cycles = 0; - if self.current_cartridge_app_mode == AppMode::Game { + if RenderManager::policy_for_app_mode(self.current_cartridge_app_mode) + .uses_frame_scheduler() + { self.frame_scheduler.complete_game_frame(); } @@ -335,7 +339,8 @@ impl VirtualMachineRuntime { self.debug_step_request = false; } } else if run.reason == LogicalFrameEndingReason::BudgetExhausted - && self.current_cartridge_app_mode == AppMode::Game + && RenderManager::policy_for_app_mode(self.current_cartridge_app_mode) + .uses_frame_scheduler() { self.frame_scheduler.record_logical_frame_overrun(); } diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 35c3813e..0d6a4ff7 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,6 +1,6 @@ {"type":"meta","next_id":{"DSC":42,"AGD":42,"DEC":32,"PLN":98,"LSN":48,"CLSN":1}} {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} -{"type":"discussion","id":"DSC-0040","status":"in_progress","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-05","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[{"id":"AGD-0040","file":"AGD-0040-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-04","updated_at":"2026-06-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-06-05","ref_agenda":"AGD-0040"}],"plans":[{"id":"PLN-0087","file":"PLN-0087-fix-game2d-packet-overlay-composition.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0088","file":"PLN-0088-define-read-only-render-resource-boundary.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0089","file":"PLN-0089-introduce-render-handoff-abstraction.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0090","file":"PLN-0090-add-render-frame-pacing-contract.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0091","file":"PLN-0091-implement-appmode-render-policy-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0092","file":"PLN-0092-add-render-epoch-ownership-transitions.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0093","file":"PLN-0093-add-render-telemetry-counters-and-events.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0094","file":"PLN-0094-specify-async-render-boundary.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0095","file":"PLN-0095-prototype-local-render-worker-capability.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0096","file":"PLN-0096-define-render-shutdown-and-failure-handling.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0097","file":"PLN-0097-add-async-render-integration-test-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0040","status":"in_progress","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-05","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[{"id":"AGD-0040","file":"AGD-0040-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-04","updated_at":"2026-06-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-06-05","ref_agenda":"AGD-0040"}],"plans":[{"id":"PLN-0087","file":"PLN-0087-fix-game2d-packet-overlay-composition.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0088","file":"PLN-0088-define-read-only-render-resource-boundary.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0089","file":"PLN-0089-introduce-render-handoff-abstraction.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0090","file":"PLN-0090-add-render-frame-pacing-contract.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0091","file":"PLN-0091-implement-appmode-render-policy-matrix.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0092","file":"PLN-0092-add-render-epoch-ownership-transitions.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0093","file":"PLN-0093-add-render-telemetry-counters-and-events.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0094","file":"PLN-0094-specify-async-render-boundary.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0095","file":"PLN-0095-prototype-local-render-worker-capability.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0096","file":"PLN-0096-define-render-shutdown-and-failure-handling.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0097","file":"PLN-0097-add-async-render-integration-test-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]}],"lessons":[]} {"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0091-implement-appmode-render-policy-matrix.md b/discussion/workflow/plans/PLN-0091-implement-appmode-render-policy-matrix.md index 94be136b..9585762f 100644 --- a/discussion/workflow/plans/PLN-0091-implement-appmode-render-policy-matrix.md +++ b/discussion/workflow/plans/PLN-0091-implement-appmode-render-policy-matrix.md @@ -2,7 +2,7 @@ id: PLN-0091 ticket: vm-render-parallel-execution-boundary title: Implement AppMode Render Policy Matrix -status: open +status: done created: 2026-06-05 ref_decisions: [DEC-0031] tags: [runtime, appmode, shell, game, policy]