implements PLN-0097
This commit is contained in:
parent
e4c1390614
commit
8cd218dc04
@ -0,0 +1,236 @@
|
||||
use super::FrameScheduler;
|
||||
use super::frame_scheduler::FrameAuthorizationError;
|
||||
use super::render_manager::{
|
||||
LocalRenderWorker, RenderConsumeOutcome, RenderConsumerPath, RenderExecutionPolicy,
|
||||
RenderPacingPolicy, RenderPolicy, RenderRuntimeCapabilities, RenderSurface,
|
||||
};
|
||||
use prometeu_hal::app_mode::AppMode;
|
||||
use prometeu_hal::telemetry::AtomicTelemetry;
|
||||
use prometeu_hal::{
|
||||
FrameId, Game2DFramePacket, RenderOwnership, RenderSubmission, RenderSubmissionPacket,
|
||||
ShellUiFramePacket,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicU32;
|
||||
|
||||
use super::render_manager::RenderManager;
|
||||
|
||||
struct ContractMatrixRow {
|
||||
invariant: &'static str,
|
||||
validations: &'static [&'static str],
|
||||
}
|
||||
|
||||
const ASYNC_RENDER_CONTRACT_MATRIX: &[ContractMatrixRow] = &[
|
||||
ContractMatrixRow {
|
||||
invariant: "complete packet and AppMode policy are explicit",
|
||||
validations: &["matrix_packet_policy_and_pacing_are_mode_specific"],
|
||||
},
|
||||
ContractMatrixRow {
|
||||
invariant: "handoff is single-slot latest-wins in sync and worker-capable paths",
|
||||
validations: &["matrix_sync_and_worker_paths_share_latest_wins_handoff_contract"],
|
||||
},
|
||||
ContractMatrixRow {
|
||||
invariant: "render consumers see resource banks through read-only access",
|
||||
validations: &[
|
||||
"prometeu-drivers::render_resource_access_traits_are_thread_safe",
|
||||
"prometeu-drivers::render_resource_access_exposes_read_only_glyph_and_scene_banks",
|
||||
],
|
||||
},
|
||||
ContractMatrixRow {
|
||||
invariant: "Game pacing is frame scheduled and does not catch up skipped frames",
|
||||
validations: &["matrix_packet_policy_and_pacing_are_mode_specific"],
|
||||
},
|
||||
ContractMatrixRow {
|
||||
invariant: "ownership epoch prevents obsolete work from presenting",
|
||||
validations: &["matrix_epoch_stale_discard_and_telemetry_sync_remain_consistent"],
|
||||
},
|
||||
ContractMatrixRow {
|
||||
invariant: "telemetry exposes production, drops, consumes, presents, repeats, and errors",
|
||||
validations: &[
|
||||
"matrix_epoch_stale_discard_and_telemetry_sync_remain_consistent",
|
||||
"matrix_shutdown_failure_and_repeat_are_observable",
|
||||
],
|
||||
},
|
||||
ContractMatrixRow {
|
||||
invariant: "shutdown discards pending work and bounded local worker failure is contained",
|
||||
validations: &["matrix_shutdown_failure_and_repeat_are_observable"],
|
||||
},
|
||||
ContractMatrixRow {
|
||||
invariant: "active-scene Game2D overlay is composed after layer blit",
|
||||
validations: &[
|
||||
"prometeu-drivers::publish_game2d_submission_applies_gfx2d_overlay_after_active_scene",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
#[derive(Default)]
|
||||
struct RecordingSurface {
|
||||
seen: Vec<FrameId>,
|
||||
}
|
||||
|
||||
impl RenderSurface for RecordingSurface {
|
||||
fn consume_submission(&mut self, submission: &RenderSubmission) {
|
||||
self.seen.push(submission.frame_id);
|
||||
}
|
||||
}
|
||||
|
||||
struct PanickingSurface;
|
||||
|
||||
impl RenderSurface for PanickingSurface {
|
||||
fn consume_submission(&mut self, _submission: &RenderSubmission) {
|
||||
panic!("matrix present failure");
|
||||
}
|
||||
}
|
||||
|
||||
fn close_game(manager: &mut RenderManager) {
|
||||
manager
|
||||
.close_frame_with_packet(RenderSubmissionPacket::Game2D(Game2DFramePacket::default()))
|
||||
.expect("game packet must match Game mode");
|
||||
}
|
||||
|
||||
fn close_shell(manager: &mut RenderManager) {
|
||||
manager
|
||||
.close_frame_with_packet(RenderSubmissionPacket::ShellUi(ShellUiFramePacket::new(
|
||||
Vec::new(),
|
||||
)))
|
||||
.expect("shell packet must match Shell mode");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn async_render_contract_matrix_rows_have_validation() {
|
||||
assert!(ASYNC_RENDER_CONTRACT_MATRIX.len() >= 7);
|
||||
for row in ASYNC_RENDER_CONTRACT_MATRIX {
|
||||
assert!(!row.invariant.is_empty());
|
||||
assert!(!row.validations.is_empty(), "missing validation for {}", row.invariant);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matrix_packet_policy_and_pacing_are_mode_specific() {
|
||||
let game_policy = RenderPolicy::for_app_mode(AppMode::Game);
|
||||
assert_eq!(game_policy.pacing, RenderPacingPolicy::FrameScheduled);
|
||||
assert_eq!(game_policy.execution, RenderExecutionPolicy::WorkerCapableWithLocalFallback);
|
||||
assert_eq!(
|
||||
game_policy.resolve_consumer_path(RenderRuntimeCapabilities { game_render_worker: true }),
|
||||
RenderConsumerPath::LocalWorkerPrototype
|
||||
);
|
||||
|
||||
let shell_policy = RenderPolicy::for_app_mode(AppMode::Shell);
|
||||
assert_eq!(shell_policy.pacing, RenderPacingPolicy::LifecycleDriven);
|
||||
assert_eq!(shell_policy.execution, RenderExecutionPolicy::LocalSynchronous);
|
||||
assert_eq!(
|
||||
shell_policy.resolve_consumer_path(RenderRuntimeCapabilities { game_render_worker: true }),
|
||||
RenderConsumerPath::LocalSynchronous
|
||||
);
|
||||
|
||||
let mut game_manager = RenderManager::new(AppMode::Game);
|
||||
close_game(&mut game_manager);
|
||||
assert!(
|
||||
game_manager
|
||||
.close_frame_with_packet(RenderSubmissionPacket::ShellUi(ShellUiFramePacket::new(
|
||||
Vec::new()
|
||||
)))
|
||||
.is_err()
|
||||
);
|
||||
|
||||
let mut shell_manager = RenderManager::new(AppMode::Shell);
|
||||
close_shell(&mut shell_manager);
|
||||
assert!(
|
||||
shell_manager
|
||||
.close_frame_with_packet(RenderSubmissionPacket::Game2D(Game2DFramePacket::default()))
|
||||
.is_err()
|
||||
);
|
||||
|
||||
let mut scheduler = FrameScheduler::default();
|
||||
assert_eq!(scheduler.authorize_game_frame(), Ok(FrameId::ZERO));
|
||||
scheduler.record_logical_frame_overrun();
|
||||
assert_eq!(
|
||||
scheduler.authorize_game_frame(),
|
||||
Err(FrameAuthorizationError::GameFrameAlreadyActive { active_frame_id: FrameId::ZERO })
|
||||
);
|
||||
assert_eq!(scheduler.complete_game_frame(), Some(FrameId::ZERO));
|
||||
assert_eq!(scheduler.authorize_game_frame(), Ok(FrameId::new(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matrix_sync_and_worker_paths_share_latest_wins_handoff_contract() {
|
||||
fn consume_with(path: RenderConsumerPath) -> (RenderConsumeOutcome, Vec<FrameId>, u64) {
|
||||
let mut manager = RenderManager::new(AppMode::Game);
|
||||
let mut surface = RecordingSurface::default();
|
||||
|
||||
close_game(&mut manager);
|
||||
close_game(&mut manager);
|
||||
|
||||
let outcome = match path {
|
||||
RenderConsumerPath::LocalSynchronous => manager.consume_latest(&mut surface),
|
||||
RenderConsumerPath::LocalWorkerPrototype => {
|
||||
let mut worker = LocalRenderWorker;
|
||||
worker.consume_latest(&mut manager, &mut surface)
|
||||
}
|
||||
};
|
||||
|
||||
(outcome, surface.seen, manager.render_telemetry().replaced_before_consume)
|
||||
}
|
||||
|
||||
for path in [RenderConsumerPath::LocalSynchronous, RenderConsumerPath::LocalWorkerPrototype] {
|
||||
let (outcome, seen, replaced) = consume_with(path);
|
||||
assert_eq!(outcome, RenderConsumeOutcome::Presented { frame_id: FrameId::new(1) });
|
||||
assert_eq!(seen, vec![FrameId::new(1)]);
|
||||
assert_eq!(replaced, 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matrix_epoch_stale_discard_and_telemetry_sync_remain_consistent() {
|
||||
let mut manager = RenderManager::new(AppMode::Game);
|
||||
let mut surface = RecordingSurface::default();
|
||||
let atomic = AtomicTelemetry::new(Arc::new(AtomicU32::new(0)));
|
||||
|
||||
manager.transition_render_owner(AppMode::Game, 7);
|
||||
close_game(&mut manager);
|
||||
manager.transition_render_owner(AppMode::Game, 8);
|
||||
|
||||
assert_eq!(
|
||||
manager.consume_latest(&mut surface),
|
||||
RenderConsumeOutcome::DiscardedStaleEpoch { frame_id: FrameId::ZERO }
|
||||
);
|
||||
assert!(surface.seen.is_empty());
|
||||
|
||||
manager.sync_telemetry(&atomic);
|
||||
let snapshot = atomic.snapshot();
|
||||
assert_eq!(snapshot.produced_submissions, 1);
|
||||
assert_eq!(snapshot.stale_epoch_discards, 1);
|
||||
assert_eq!(snapshot.presented_frames, 0);
|
||||
assert_eq!(snapshot.last_dropped_frame_id, 0);
|
||||
assert_eq!(snapshot.active_render_epoch, 2);
|
||||
assert_eq!(manager.active_ownership(), RenderOwnership::new(2, AppMode::Game, 8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matrix_shutdown_failure_and_repeat_are_observable() {
|
||||
let mut manager = RenderManager::new(AppMode::Game);
|
||||
|
||||
close_game(&mut manager);
|
||||
let discarded = manager.request_shutdown().expect("pending frame discarded");
|
||||
assert_eq!(discarded.frame_id, FrameId::ZERO);
|
||||
|
||||
manager.resume_consumer();
|
||||
close_game(&mut manager);
|
||||
let mut surface = PanickingSurface;
|
||||
assert_eq!(
|
||||
manager.consume_latest(&mut surface),
|
||||
RenderConsumeOutcome::PresentFailed {
|
||||
frame_id: FrameId::new(1),
|
||||
message: "matrix present failure".to_string()
|
||||
}
|
||||
);
|
||||
manager.record_repeated_present(FrameId::new(1));
|
||||
|
||||
let telemetry = manager.render_telemetry();
|
||||
assert_eq!(telemetry.shutdown_discards, 1);
|
||||
assert_eq!(telemetry.present_errors, 1);
|
||||
assert_eq!(telemetry.presented_frames, 0);
|
||||
assert_eq!(telemetry.repeated_presents, 1);
|
||||
assert_eq!(telemetry.last_error_frame_id, 1);
|
||||
assert_eq!(telemetry.last_presented_frame_id, 1);
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
#[cfg(test)]
|
||||
mod async_render_contract_tests;
|
||||
mod dispatch;
|
||||
mod frame_scheduler;
|
||||
mod lifecycle;
|
||||
|
||||
@ -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":"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":"done","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":"done","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":"done","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":"done","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":"done","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":"done","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":"done","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":"done","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":"done","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":"done","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":"done","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"}]}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
id: PLN-0097
|
||||
ticket: vm-render-parallel-execution-boundary
|
||||
title: Add Async Render Integration Test Matrix
|
||||
status: open
|
||||
status: done
|
||||
created: 2026-06-05
|
||||
ref_decisions: [DEC-0031]
|
||||
tags: [runtime, renderer, tests, integration]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user