implements PLN-0097

This commit is contained in:
bQUARKz 2026-06-05 09:58:41 +01:00
parent e4c1390614
commit 8cd218dc04
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
4 changed files with 240 additions and 2 deletions

View File

@ -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);
}

View File

@ -1,3 +1,5 @@
#[cfg(test)]
mod async_render_contract_tests;
mod dispatch;
mod frame_scheduler;
mod lifecycle;

View File

@ -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"}]}

View File

@ -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]