654 lines
23 KiB
Rust
654 lines
23 KiB
Rust
use prometeu_hal::app_mode::AppMode;
|
|
use prometeu_hal::telemetry::AtomicTelemetry;
|
|
use prometeu_hal::{
|
|
FrameId, Game2DFramePacket, RenderOwnership, RenderSubmission, RenderSubmissionPacket,
|
|
ShellUiFramePacket,
|
|
};
|
|
use std::sync::atomic::Ordering;
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum RenderTransitionState {
|
|
Idle,
|
|
Pending { from: RenderOwnership, to: RenderOwnership },
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
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);
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
|
pub struct RenderTelemetrySnapshot {
|
|
pub produced_submissions: u64,
|
|
pub replaced_before_consume: u64,
|
|
pub consumed_submissions: u64,
|
|
pub presented_frames: u64,
|
|
pub repeated_presents: u64,
|
|
pub render_errors: u64,
|
|
pub present_errors: u64,
|
|
pub stale_epoch_discards: u64,
|
|
pub shutdown_discards: u64,
|
|
pub last_produced_frame_id: u64,
|
|
pub last_consumed_frame_id: u64,
|
|
pub last_presented_frame_id: u64,
|
|
pub last_dropped_frame_id: u64,
|
|
pub last_error_frame_id: u64,
|
|
pub active_render_epoch: u64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
struct RenderTelemetry {
|
|
snapshot: RenderTelemetrySnapshot,
|
|
}
|
|
|
|
impl RenderTelemetry {
|
|
fn record_produced(&mut self, frame_id: FrameId) {
|
|
self.snapshot.produced_submissions = self.snapshot.produced_submissions.wrapping_add(1);
|
|
self.snapshot.last_produced_frame_id = frame_id.get();
|
|
}
|
|
|
|
fn record_replaced_before_consume(&mut self, frame_id: FrameId) {
|
|
self.snapshot.replaced_before_consume =
|
|
self.snapshot.replaced_before_consume.wrapping_add(1);
|
|
self.snapshot.last_dropped_frame_id = frame_id.get();
|
|
}
|
|
|
|
fn record_consumed(&mut self, frame_id: FrameId) {
|
|
self.snapshot.consumed_submissions = self.snapshot.consumed_submissions.wrapping_add(1);
|
|
self.snapshot.last_consumed_frame_id = frame_id.get();
|
|
}
|
|
|
|
fn record_presented(&mut self, frame_id: FrameId) {
|
|
self.snapshot.presented_frames = self.snapshot.presented_frames.wrapping_add(1);
|
|
self.snapshot.last_presented_frame_id = frame_id.get();
|
|
}
|
|
|
|
fn record_repeated_present(&mut self, frame_id: FrameId) {
|
|
self.snapshot.repeated_presents = self.snapshot.repeated_presents.wrapping_add(1);
|
|
self.snapshot.last_presented_frame_id = frame_id.get();
|
|
}
|
|
|
|
fn record_render_error(&mut self, frame_id: FrameId) {
|
|
self.snapshot.render_errors = self.snapshot.render_errors.wrapping_add(1);
|
|
self.snapshot.last_error_frame_id = frame_id.get();
|
|
}
|
|
|
|
fn record_present_error(&mut self, frame_id: FrameId) {
|
|
self.snapshot.present_errors = self.snapshot.present_errors.wrapping_add(1);
|
|
self.snapshot.last_error_frame_id = frame_id.get();
|
|
}
|
|
|
|
fn record_stale_epoch_discard(&mut self, frame_id: FrameId) {
|
|
self.snapshot.stale_epoch_discards = self.snapshot.stale_epoch_discards.wrapping_add(1);
|
|
self.snapshot.last_dropped_frame_id = frame_id.get();
|
|
}
|
|
|
|
fn record_shutdown_discard(&mut self, frame_id: FrameId) {
|
|
self.snapshot.shutdown_discards = self.snapshot.shutdown_discards.wrapping_add(1);
|
|
self.snapshot.last_dropped_frame_id = frame_id.get();
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct RenderHandoff {
|
|
pending: Option<RenderSubmission>,
|
|
last_consumed: Option<RenderSubmission>,
|
|
replaced_before_consume: u64,
|
|
discarded_pending: u64,
|
|
stale_epoch_discards: u64,
|
|
}
|
|
|
|
impl RenderHandoff {
|
|
pub fn publish(&mut self, submission: RenderSubmission) {
|
|
if self.pending.replace(submission).is_some() {
|
|
self.replaced_before_consume = self.replaced_before_consume.wrapping_add(1);
|
|
}
|
|
}
|
|
|
|
pub fn pending_submission(&self) -> Option<&RenderSubmission> {
|
|
self.pending.as_ref()
|
|
}
|
|
|
|
pub fn latest_submission(&self) -> Option<&RenderSubmission> {
|
|
self.pending.as_ref().or(self.last_consumed.as_ref())
|
|
}
|
|
|
|
pub fn take_latest(&mut self) -> Option<RenderSubmission> {
|
|
self.pending.take()
|
|
}
|
|
|
|
pub fn record_consumed(&mut self, submission: RenderSubmission) {
|
|
self.last_consumed = Some(submission);
|
|
}
|
|
|
|
pub fn record_stale_epoch_discard(&mut self, submission: RenderSubmission) {
|
|
self.stale_epoch_discards = self.stale_epoch_discards.wrapping_add(1);
|
|
self.last_consumed = Some(submission);
|
|
}
|
|
|
|
pub fn discard_pending(&mut self) -> Option<RenderSubmission> {
|
|
let discarded = self.pending.take();
|
|
if discarded.is_some() {
|
|
self.discarded_pending = self.discarded_pending.wrapping_add(1);
|
|
}
|
|
discarded
|
|
}
|
|
|
|
pub fn replaced_before_consume(&self) -> u64 {
|
|
self.replaced_before_consume
|
|
}
|
|
|
|
pub fn discarded_pending(&self) -> u64 {
|
|
self.discarded_pending
|
|
}
|
|
|
|
pub fn stale_epoch_discards(&self) -> u64 {
|
|
self.stale_epoch_discards
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct RenderManager {
|
|
active_app_mode: AppMode,
|
|
active_app_id: u32,
|
|
active_ownership: RenderOwnership,
|
|
active_policy: RenderPolicy,
|
|
next_frame_id: FrameId,
|
|
handoff: RenderHandoff,
|
|
telemetry: RenderTelemetry,
|
|
transition_state: RenderTransitionState,
|
|
}
|
|
|
|
impl RenderManager {
|
|
pub fn new(active_app_mode: AppMode) -> Self {
|
|
Self {
|
|
active_app_mode,
|
|
active_app_id: 0,
|
|
active_ownership: RenderOwnership::new(0, active_app_mode, 0),
|
|
active_policy: RenderPolicy::for_app_mode(active_app_mode),
|
|
next_frame_id: FrameId::ZERO,
|
|
handoff: RenderHandoff::default(),
|
|
telemetry: RenderTelemetry::default(),
|
|
transition_state: RenderTransitionState::Idle,
|
|
}
|
|
}
|
|
|
|
pub fn active_app_mode(&self) -> AppMode {
|
|
self.active_app_mode
|
|
}
|
|
|
|
pub fn active_ownership(&self) -> RenderOwnership {
|
|
self.active_ownership
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
pub fn latest_complete_submission(&self) -> Option<&RenderSubmission> {
|
|
self.handoff.latest_submission()
|
|
}
|
|
|
|
pub fn pending_submission(&self) -> Option<&RenderSubmission> {
|
|
self.handoff.pending_submission()
|
|
}
|
|
|
|
pub fn replaced_before_consume(&self) -> u64 {
|
|
self.handoff.replaced_before_consume()
|
|
}
|
|
|
|
pub fn discarded_pending(&self) -> u64 {
|
|
self.handoff.discarded_pending()
|
|
}
|
|
|
|
pub fn stale_epoch_discards(&self) -> u64 {
|
|
self.handoff.stale_epoch_discards()
|
|
}
|
|
|
|
pub fn discard_pending_submission(&mut self) -> Option<RenderSubmission> {
|
|
let discarded = self.handoff.discard_pending();
|
|
if let Some(submission) = discarded.as_ref() {
|
|
self.telemetry.record_shutdown_discard(submission.frame_id);
|
|
}
|
|
discarded
|
|
}
|
|
|
|
pub fn render_telemetry(&self) -> RenderTelemetrySnapshot {
|
|
let mut snapshot = self.telemetry.snapshot;
|
|
snapshot.active_render_epoch = self.active_ownership.epoch;
|
|
snapshot
|
|
}
|
|
|
|
pub fn sync_telemetry(&self, telemetry: &AtomicTelemetry) {
|
|
let snapshot = self.render_telemetry();
|
|
telemetry.produced_submissions.store(snapshot.produced_submissions, Ordering::Relaxed);
|
|
telemetry
|
|
.replaced_before_consume
|
|
.store(snapshot.replaced_before_consume, Ordering::Relaxed);
|
|
telemetry.consumed_submissions.store(snapshot.consumed_submissions, Ordering::Relaxed);
|
|
telemetry.presented_frames.store(snapshot.presented_frames, Ordering::Relaxed);
|
|
telemetry.repeated_presents.store(snapshot.repeated_presents, Ordering::Relaxed);
|
|
telemetry.render_errors.store(snapshot.render_errors, Ordering::Relaxed);
|
|
telemetry.present_errors.store(snapshot.present_errors, Ordering::Relaxed);
|
|
telemetry.stale_epoch_discards.store(snapshot.stale_epoch_discards, Ordering::Relaxed);
|
|
telemetry.shutdown_discards.store(snapshot.shutdown_discards, Ordering::Relaxed);
|
|
telemetry.last_produced_frame_id.store(snapshot.last_produced_frame_id, Ordering::Relaxed);
|
|
telemetry.last_consumed_frame_id.store(snapshot.last_consumed_frame_id, Ordering::Relaxed);
|
|
telemetry
|
|
.last_presented_frame_id
|
|
.store(snapshot.last_presented_frame_id, Ordering::Relaxed);
|
|
telemetry.last_dropped_frame_id.store(snapshot.last_dropped_frame_id, Ordering::Relaxed);
|
|
telemetry.last_error_frame_id.store(snapshot.last_error_frame_id, Ordering::Relaxed);
|
|
telemetry.active_render_epoch.store(snapshot.active_render_epoch, Ordering::Relaxed);
|
|
}
|
|
|
|
pub fn record_repeated_present(&mut self, frame_id: FrameId) {
|
|
self.telemetry.record_repeated_present(frame_id);
|
|
}
|
|
|
|
pub fn record_render_error(&mut self, frame_id: FrameId) {
|
|
self.telemetry.record_render_error(frame_id);
|
|
}
|
|
|
|
pub fn record_present_error(&mut self, frame_id: FrameId) {
|
|
self.telemetry.record_present_error(frame_id);
|
|
}
|
|
|
|
pub fn set_active_app_mode(&mut self, app_mode: AppMode) {
|
|
self.transition_render_owner(app_mode, self.active_app_id);
|
|
}
|
|
|
|
pub fn transition_render_owner(&mut self, app_mode: AppMode, app_id: u32) {
|
|
if self.active_app_mode == app_mode && self.active_app_id == app_id {
|
|
return;
|
|
}
|
|
|
|
let previous = self.active_ownership;
|
|
self.active_app_mode = app_mode;
|
|
self.active_app_id = app_id;
|
|
self.active_ownership =
|
|
RenderOwnership::new(self.active_ownership.epoch.wrapping_add(1), app_mode, app_id);
|
|
self.active_policy = RenderPolicy::for_app_mode(app_mode);
|
|
self.transition_state =
|
|
RenderTransitionState::Pending { from: previous, to: self.active_ownership };
|
|
}
|
|
|
|
pub fn acknowledge_transition(&mut self) {
|
|
self.transition_state = RenderTransitionState::Idle;
|
|
}
|
|
|
|
pub fn close_frame_with_packet(
|
|
&mut self,
|
|
packet: RenderSubmissionPacket,
|
|
) -> Result<&RenderSubmission, RenderSubmissionError> {
|
|
let frame_id = self.next_frame_id;
|
|
let submission = match (self.active_app_mode, packet) {
|
|
(AppMode::Game, RenderSubmissionPacket::Game2D(packet)) => {
|
|
RenderSubmission::game2d(frame_id, packet).with_ownership(self.active_ownership)
|
|
}
|
|
(AppMode::Shell, RenderSubmissionPacket::ShellUi(packet)) => {
|
|
RenderSubmission::shell_ui(frame_id, packet).with_ownership(self.active_ownership)
|
|
}
|
|
(active, _) => {
|
|
return Err(RenderSubmissionError::PacketAppModeMismatch { active });
|
|
}
|
|
};
|
|
|
|
self.next_frame_id = self.next_frame_id.next();
|
|
if let Some(replaced) = self.handoff.pending_submission() {
|
|
self.telemetry.record_replaced_before_consume(replaced.frame_id);
|
|
}
|
|
self.telemetry.record_produced(frame_id);
|
|
self.handoff.publish(submission);
|
|
Ok(self.handoff.pending_submission().expect("submission was just stored"))
|
|
}
|
|
|
|
pub fn close_compat_frame(&mut self) -> &RenderSubmission {
|
|
let packet = match self.active_app_mode {
|
|
AppMode::Game => RenderSubmissionPacket::Game2D(Game2DFramePacket::default()),
|
|
AppMode::Shell => RenderSubmissionPacket::ShellUi(ShellUiFramePacket::new(Vec::new())),
|
|
};
|
|
|
|
self.close_frame_with_packet(packet).expect("compat packet matches active app mode")
|
|
}
|
|
|
|
pub fn publish_latest<S: RenderSurface>(&mut self, surface: &mut S) -> bool {
|
|
let Some(submission) = self.handoff.take_latest() else {
|
|
return false;
|
|
};
|
|
if submission.ownership != self.active_ownership {
|
|
self.telemetry.record_stale_epoch_discard(submission.frame_id);
|
|
self.handoff.record_stale_epoch_discard(submission);
|
|
return false;
|
|
}
|
|
self.telemetry.record_consumed(submission.frame_id);
|
|
surface.consume_submission(&submission);
|
|
self.telemetry.record_presented(submission.frame_id);
|
|
self.handoff.record_consumed(submission);
|
|
true
|
|
}
|
|
}
|
|
|
|
impl Default for RenderManager {
|
|
fn default() -> Self {
|
|
Self::new(AppMode::Game)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::AtomicU32;
|
|
|
|
#[derive(Default)]
|
|
struct RecordingSurface {
|
|
seen: Vec<FrameId>,
|
|
}
|
|
|
|
impl RenderSurface for RecordingSurface {
|
|
fn consume_submission(&mut self, submission: &RenderSubmission) {
|
|
self.seen.push(submission.frame_id);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn frame_closure_assigns_monotonic_frame_ids() {
|
|
let mut manager = RenderManager::new(AppMode::Game);
|
|
|
|
let first = manager.close_compat_frame().frame_id;
|
|
let second = manager.close_compat_frame().frame_id;
|
|
|
|
assert_eq!(first, FrameId::new(0));
|
|
assert_eq!(second, FrameId::new(1));
|
|
}
|
|
|
|
#[test]
|
|
fn latest_complete_submission_wins_without_queue_growth() {
|
|
let mut manager = RenderManager::new(AppMode::Game);
|
|
|
|
manager.close_compat_frame();
|
|
manager.close_compat_frame();
|
|
|
|
let latest = manager.latest_complete_submission().expect("latest submission");
|
|
assert_eq!(latest.frame_id, FrameId::new(1));
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_packet_that_does_not_match_active_app_mode() {
|
|
let mut manager = RenderManager::new(AppMode::Game);
|
|
|
|
let err = manager
|
|
.close_frame_with_packet(RenderSubmissionPacket::ShellUi(ShellUiFramePacket::new(
|
|
Vec::new(),
|
|
)))
|
|
.expect_err("shell packet is invalid in game mode");
|
|
|
|
assert_eq!(err, RenderSubmissionError::PacketAppModeMismatch { active: AppMode::Game });
|
|
}
|
|
|
|
#[test]
|
|
fn app_mode_switch_records_noop_transition_placeholder() {
|
|
let mut manager = RenderManager::new(AppMode::Game);
|
|
let previous = manager.active_ownership();
|
|
|
|
manager.set_active_app_mode(AppMode::Shell);
|
|
|
|
assert_eq!(manager.active_app_mode(), AppMode::Shell);
|
|
assert_eq!(
|
|
manager.transition_state(),
|
|
RenderTransitionState::Pending {
|
|
from: previous,
|
|
to: RenderOwnership::new(1, AppMode::Shell, 0)
|
|
}
|
|
);
|
|
|
|
manager.acknowledge_transition();
|
|
assert_eq!(manager.transition_state(), RenderTransitionState::Idle);
|
|
}
|
|
|
|
#[test]
|
|
fn transition_render_owner_increments_epoch_for_same_mode_app_swap() {
|
|
let mut manager = RenderManager::new(AppMode::Game);
|
|
|
|
manager.transition_render_owner(AppMode::Game, 7);
|
|
let first_owner = manager.active_ownership();
|
|
manager.transition_render_owner(AppMode::Game, 8);
|
|
|
|
assert_eq!(first_owner, RenderOwnership::new(1, AppMode::Game, 7));
|
|
assert_eq!(manager.active_ownership(), RenderOwnership::new(2, AppMode::Game, 8));
|
|
}
|
|
|
|
#[test]
|
|
fn close_frame_stamps_submission_with_active_ownership() {
|
|
let mut manager = RenderManager::new(AppMode::Game);
|
|
manager.transition_render_owner(AppMode::Game, 42);
|
|
|
|
let submission = manager.close_compat_frame();
|
|
|
|
assert_eq!(submission.ownership, RenderOwnership::new(1, AppMode::Game, 42));
|
|
}
|
|
|
|
#[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);
|
|
let mut surface = RecordingSurface::default();
|
|
|
|
assert!(!manager.publish_latest(&mut surface));
|
|
manager.close_compat_frame();
|
|
assert!(manager.publish_latest(&mut surface));
|
|
|
|
assert_eq!(surface.seen, vec![FrameId::new(0)]);
|
|
}
|
|
|
|
#[test]
|
|
fn render_handoff_takes_owned_latest_submission() {
|
|
let mut handoff = RenderHandoff::default();
|
|
|
|
assert!(handoff.take_latest().is_none());
|
|
handoff.publish(RenderSubmission::game2d(FrameId::new(7), Game2DFramePacket::default()));
|
|
|
|
let submission = handoff.take_latest().expect("pending submission");
|
|
assert_eq!(submission.frame_id, FrameId::new(7));
|
|
assert!(handoff.take_latest().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn render_handoff_counts_replacement_before_consume() {
|
|
let mut handoff = RenderHandoff::default();
|
|
|
|
handoff.publish(RenderSubmission::game2d(FrameId::new(1), Game2DFramePacket::default()));
|
|
handoff.publish(RenderSubmission::game2d(FrameId::new(2), Game2DFramePacket::default()));
|
|
|
|
assert_eq!(handoff.replaced_before_consume(), 1);
|
|
assert_eq!(handoff.take_latest().expect("latest").frame_id, FrameId::new(2));
|
|
}
|
|
|
|
#[test]
|
|
fn render_handoff_discards_pending_submission() {
|
|
let mut handoff = RenderHandoff::default();
|
|
|
|
assert!(handoff.discard_pending().is_none());
|
|
handoff.publish(RenderSubmission::game2d(FrameId::new(3), Game2DFramePacket::default()));
|
|
|
|
let discarded = handoff.discard_pending().expect("discarded pending");
|
|
assert_eq!(discarded.frame_id, FrameId::new(3));
|
|
assert_eq!(handoff.discarded_pending(), 1);
|
|
assert!(handoff.take_latest().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn publish_latest_consumes_pending_but_keeps_observable_last_submission() {
|
|
let mut manager = RenderManager::new(AppMode::Game);
|
|
let mut surface = RecordingSurface::default();
|
|
|
|
manager.close_compat_frame();
|
|
assert!(manager.pending_submission().is_some());
|
|
assert!(manager.publish_latest(&mut surface));
|
|
|
|
assert!(manager.pending_submission().is_none());
|
|
assert_eq!(
|
|
manager.latest_complete_submission().expect("last consumed").frame_id,
|
|
FrameId::ZERO
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn publish_latest_discards_stale_epoch_before_present() {
|
|
let mut manager = RenderManager::new(AppMode::Game);
|
|
let mut surface = RecordingSurface::default();
|
|
|
|
manager.close_compat_frame();
|
|
manager.transition_render_owner(AppMode::Shell, 1);
|
|
|
|
assert!(!manager.publish_latest(&mut surface));
|
|
assert!(surface.seen.is_empty());
|
|
assert_eq!(manager.stale_epoch_discards(), 1);
|
|
assert_eq!(
|
|
manager.latest_complete_submission().expect("stale consumed for diagnostics").ownership,
|
|
RenderOwnership::new(0, AppMode::Game, 0)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn render_telemetry_counts_produced_replaced_consumed_and_presented() {
|
|
let mut manager = RenderManager::new(AppMode::Game);
|
|
let mut surface = RecordingSurface::default();
|
|
|
|
manager.close_compat_frame();
|
|
manager.close_compat_frame();
|
|
assert!(manager.publish_latest(&mut surface));
|
|
|
|
let telemetry = manager.render_telemetry();
|
|
assert_eq!(telemetry.produced_submissions, 2);
|
|
assert_eq!(telemetry.replaced_before_consume, 1);
|
|
assert_eq!(telemetry.consumed_submissions, 1);
|
|
assert_eq!(telemetry.presented_frames, 1);
|
|
assert_eq!(telemetry.last_produced_frame_id, 1);
|
|
assert_eq!(telemetry.last_dropped_frame_id, 0);
|
|
assert_eq!(telemetry.last_consumed_frame_id, 1);
|
|
assert_eq!(telemetry.last_presented_frame_id, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn render_telemetry_counts_stale_repeat_and_error_hooks() {
|
|
let mut manager = RenderManager::new(AppMode::Game);
|
|
let mut surface = RecordingSurface::default();
|
|
|
|
manager.close_compat_frame();
|
|
manager.transition_render_owner(AppMode::Shell, 1);
|
|
assert!(!manager.publish_latest(&mut surface));
|
|
manager.record_repeated_present(FrameId::new(9));
|
|
manager.record_render_error(FrameId::new(10));
|
|
manager.record_present_error(FrameId::new(11));
|
|
|
|
let telemetry = manager.render_telemetry();
|
|
assert_eq!(telemetry.stale_epoch_discards, 1);
|
|
assert_eq!(telemetry.repeated_presents, 1);
|
|
assert_eq!(telemetry.render_errors, 1);
|
|
assert_eq!(telemetry.present_errors, 1);
|
|
assert_eq!(telemetry.last_presented_frame_id, 9);
|
|
assert_eq!(telemetry.last_error_frame_id, 11);
|
|
}
|
|
|
|
#[test]
|
|
fn render_telemetry_syncs_to_atomic_telemetry() {
|
|
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, 5);
|
|
manager.close_compat_frame();
|
|
assert!(manager.publish_latest(&mut surface));
|
|
manager.sync_telemetry(&atomic);
|
|
|
|
let snapshot = atomic.snapshot();
|
|
assert_eq!(snapshot.produced_submissions, 1);
|
|
assert_eq!(snapshot.consumed_submissions, 1);
|
|
assert_eq!(snapshot.presented_frames, 1);
|
|
assert_eq!(snapshot.last_presented_frame_id, 0);
|
|
assert_eq!(snapshot.active_render_epoch, 1);
|
|
}
|
|
}
|