From f8b9dc7f771505548b8d5325ed22e1233bad29db Mon Sep 17 00:00:00 2001 From: "Galvan, Mark" Date: Fri, 17 Jul 2026 10:18:16 -0700 Subject: [PATCH] Calculate AnimationError and AnimationTime for generated frames Calculate animation metrics per displayed frame by distributing the application simulation interval across every generated and application display step between two application frames. Buffer rows until both animation context and display timing are available, while preserving ingest order for generated and not-displayed frames. Start a new animation timeline when the simulation source changes, omit animation metrics when no valid interval exists, and retain lookahead buffering so displayed duration remains accurate. --- .../CommonUtilities/CommonUtilities.vcxproj | 6 +- .../CommonUtilities.vcxproj.filters | 12 + .../mc/AnimationErrorTracker.cpp | 187 + .../mc/AnimationErrorTracker.h | 61 + .../CommonUtilities/mc/DisplayFrameQueue.cpp | 548 ++ .../CommonUtilities/mc/DisplayFrameQueue.h | 141 + .../CommonUtilities/mc/MetricsCalculator.cpp | 224 +- .../CommonUtilities/mc/MetricsCalculator.h | 38 +- .../mc/MetricsCalculatorAnimation.cpp | 150 +- .../mc/MetricsCalculatorDisplay.cpp | 57 +- .../mc/MetricsCalculatorInternal.h | 11 +- .../CommonUtilities/mc/MetricsTypes.h | 22 + .../CommonUtilities/mc/SwapChainState.cpp | 157 +- .../CommonUtilities/mc/SwapChainState.h | 6 +- .../CommonUtilities/mc/UnifiedSwapChain.cpp | 118 +- .../CommonUtilities/mc/UnifiedSwapChain.h | 48 +- .../PresentMonAPI2Tests/CsvHelper.h | 41 +- .../FrameMetricsSource.cpp | 20 +- IntelPresentMon/UnitTests/MetricsCore.cpp | 8445 ++++++----------- IntelPresentMon/UnitTests/SwapChainTests.cpp | 21 + PresentData/PresentMonTraceConsumer.cpp | 2 +- PresentMon/OutputThread.cpp | 95 +- 22 files changed, 4478 insertions(+), 5932 deletions(-) create mode 100644 IntelPresentMon/CommonUtilities/mc/AnimationErrorTracker.cpp create mode 100644 IntelPresentMon/CommonUtilities/mc/AnimationErrorTracker.h create mode 100644 IntelPresentMon/CommonUtilities/mc/DisplayFrameQueue.cpp create mode 100644 IntelPresentMon/CommonUtilities/mc/DisplayFrameQueue.h diff --git a/IntelPresentMon/CommonUtilities/CommonUtilities.vcxproj b/IntelPresentMon/CommonUtilities/CommonUtilities.vcxproj index 21ab16a4..23ff4ddf 100644 --- a/IntelPresentMon/CommonUtilities/CommonUtilities.vcxproj +++ b/IntelPresentMon/CommonUtilities/CommonUtilities.vcxproj @@ -1,4 +1,4 @@ - + @@ -73,6 +73,8 @@ + + @@ -165,6 +167,8 @@ + + diff --git a/IntelPresentMon/CommonUtilities/CommonUtilities.vcxproj.filters b/IntelPresentMon/CommonUtilities/CommonUtilities.vcxproj.filters index e957deba..8f8dc1af 100644 --- a/IntelPresentMon/CommonUtilities/CommonUtilities.vcxproj.filters +++ b/IntelPresentMon/CommonUtilities/CommonUtilities.vcxproj.filters @@ -315,6 +315,12 @@ Header Files + + Header Files + + + Header Files + Header Files @@ -518,6 +524,12 @@ Source Files + + Source Files + + + Source Files + Source Files diff --git a/IntelPresentMon/CommonUtilities/mc/AnimationErrorTracker.cpp b/IntelPresentMon/CommonUtilities/mc/AnimationErrorTracker.cpp new file mode 100644 index 00000000..78989061 --- /dev/null +++ b/IntelPresentMon/CommonUtilities/mc/AnimationErrorTracker.cpp @@ -0,0 +1,187 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: MIT +#include "AnimationErrorTracker.h" +#include "MetricsCalculator.h" +#include "MetricsTypes.h" +namespace pmon::util::metrics +{ + namespace + { + bool IsAppFrameType_(FrameType frameType) + { + return frameType == FrameType::Application || frameType == FrameType::NotSet; + } + + bool HasAppWork_(const FrameData& present) + { + if (present.displayed.Empty()) { + return true; + } + for (size_t i = 0; i < present.displayed.Size(); ++i) { + if (IsAppFrameType_(present.displayed[i].first)) { + return true; + } + } + return false; + } + + uint64_t PresentEndQpc_(const FrameData& present) + { + if (present.appPropagatedPresentStartTime != 0) { + return present.appPropagatedPresentStartTime + present.appPropagatedTimeInPresent; + } + return present.presentStartTime + present.timeInPresent; + } + + uint64_t ResolveCpuStartForAnimation_( + const SwapChainCoreState& chainState, + const FrameData* ingestPreviousPresent) + { + if (ingestPreviousPresent != nullptr && HasAppWork_(*ingestPreviousPresent)) { + return PresentEndQpc_(*ingestPreviousPresent); + } + if (chainState.lastAppPresent.has_value()) { + return PresentEndQpc_(chainState.lastAppPresent.value()); + } + return 0; + } + } + + bool AnimationErrorTracker::HasTimelineAnchor() const + { + return hasCurrentAnchor_; + } + bool AnimationErrorTracker::TryStartTimelineAtAnchor(const AppAnchor& anchor) + { + if (anchor.simStartTime == 0) { + return false; + } + currentAnchor_ = anchor; + currentAnchor_.animationTimeMs = 0.0; + timelineFirstSimStartTime_ = anchor.simStartTime; + hasCurrentAnchor_ = true; + return true; + } + void AnimationErrorTracker::SetCurrentAnchorAnimationTimeMs(double publishedAnimationTimeMs) + { + if (!hasCurrentAnchor_ || IsMissingFrameMetricValue(publishedAnimationTimeMs)) { + return; + } + currentAnchor_.animationTimeMs = publishedAnimationTimeMs; + } + bool AnimationErrorTracker::IsSourceTransition(const AppAnchor& anchor) const + { + return hasCurrentAnchor_ && currentAnchor_.source != anchor.source; + } + AnimationDisplayContext AnimationErrorTracker::StartTransitionTimelineAndBuildOriginContext_( + const AppAnchor& anchor) + { + AnimationDisplayContext context{}; + context.msAnimationError = MissingFrameMetricValue(); + context.msAnimationTime = 0.0; + context.source = anchor.source; + context.resolvedSimStartTime = anchor.simStartTime; + context.firstSimStartTime = anchor.simStartTime; + context.hasResolvedSimStart = anchor.simStartTime != 0; + TryStartTimelineAtAnchor(anchor); + return context; + } + std::vector AnimationErrorTracker::ResolveIntervalAndAdvanceAnchor( + const QpcConverter& qpc, + const AppAnchor& closingAnchor, + const std::vector& intervalRows) + { + std::vector contexts(intervalRows.size()); + if (!hasCurrentAnchor_ || closingAnchor.simStartTime == 0 || intervalRows.empty()) { + TryStartTimelineAtAnchor(closingAnchor); + return contexts; + } + if (IsSourceTransition(closingAnchor)) { + if (!contexts.empty()) { + contexts.back() = StartTransitionTimelineAndBuildOriginContext_(closingAnchor); + } + return contexts; + } + return ResolveSameSourceIntervalAndAdvanceAnchor_(qpc, closingAnchor, intervalRows); + } + std::vector AnimationErrorTracker::ResolveSameSourceIntervalAndAdvanceAnchor_( + const QpcConverter& qpc, + const AppAnchor& closingAnchor, + const std::vector& intervalRows) + { + std::vector contexts(intervalRows.size()); + if (closingAnchor.simStartTime <= currentAnchor_.simStartTime) { + AppAnchor reseededAnchor = closingAnchor; + reseededAnchor.animationTimeMs = currentAnchor_.animationTimeMs; + currentAnchor_ = reseededAnchor; + return contexts; + } + const double simStepTicks = + double(closingAnchor.simStartTime - currentAnchor_.simStartTime) / double(intervalRows.size()); + const double simStepMs = simStepTicks * qpc.GetMilliSecondsPerTick(); + for (size_t i = 0; i < intervalRows.size(); ++i) { + const auto& row = intervalRows[i]; + const double displayStepMs = + qpc.DeltaUnsignedMilliSeconds(row.previousDisplayedScreenTime, row.screenTime); + const double animationTimeMs = currentAnchor_.animationTimeMs + (simStepMs * double(i + 1)); + contexts[i].msAnimationError = + simStepMs == 0.0 || displayStepMs == 0.0 + ? MissingFrameMetricValue() + : simStepMs - displayStepMs; + contexts[i].msAnimationTime = animationTimeMs; + contexts[i].source = closingAnchor.source; + contexts[i].resolvedSimStartTime = + uint64_t(double(currentAnchor_.simStartTime) + (simStepTicks * double(i + 1))); + contexts[i].firstSimStartTime = timelineFirstSimStartTime_; + contexts[i].hasResolvedSimStart = true; + } + AppAnchor nextAnchor = closingAnchor; + nextAnchor.animationTimeMs = + currentAnchor_.animationTimeMs + (simStepMs * double(intervalRows.size())); + currentAnchor_ = nextAnchor; + return contexts; + } + AnimationErrorTracker::AppAnchor AnimationErrorTracker::ResolveAppAnchor( + const SwapChainCoreState& chainState, + const FrameData& present, + size_t displayIndex, + const FrameData* ingestPreviousPresent) const + { + AppAnchor anchor{}; + anchor.present = present; + anchor.displayIndex = displayIndex; + anchor.screenTime = present.displayed[displayIndex].second; + anchor.frameType = present.displayed[displayIndex].first; + anchor.source = ResolveSource_(present); + anchor.simStartTime = ResolveSimStart_( + chainState, + present, + anchor.source, + ingestPreviousPresent); + return anchor; + } + AnimationErrorSource AnimationErrorTracker::ResolveSource_(const FrameData& present) + { + if (present.appSimStartTime != 0) { + return AnimationErrorSource::AppProvider; + } + if (present.pclSimStartTime != 0) { + return AnimationErrorSource::PCLatency; + } + return AnimationErrorSource::CpuStart; + } + uint64_t AnimationErrorTracker::ResolveSimStart_( + const SwapChainCoreState& chainState, + const FrameData& present, + AnimationErrorSource source, + const FrameData* ingestPreviousPresent) + { + if (source == AnimationErrorSource::AppProvider) { + return present.appSimStartTime; + } + if (source == AnimationErrorSource::PCLatency) { + return present.pclSimStartTime; + } + return ResolveCpuStartForAnimation_(chainState, ingestPreviousPresent); + } +} diff --git a/IntelPresentMon/CommonUtilities/mc/AnimationErrorTracker.h b/IntelPresentMon/CommonUtilities/mc/AnimationErrorTracker.h new file mode 100644 index 00000000..1e21a763 --- /dev/null +++ b/IntelPresentMon/CommonUtilities/mc/AnimationErrorTracker.h @@ -0,0 +1,61 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: MIT +#pragma once +#include +#include "../qpc.h" +#include "MetricsTypes.h" +#include "SwapChainState.h" +namespace pmon::util::metrics +{ + // Animation tracker (design: "Animation Tracker" component). + class AnimationErrorTracker + { + public: + struct AppAnchor + { + FrameData present; + size_t displayIndex = 0; + uint64_t screenTime = 0; + FrameType frameType = FrameType::NotSet; + AnimationErrorSource source = AnimationErrorSource::CpuStart; + uint64_t simStartTime = 0; + double animationTimeMs = 0.0; + }; + bool HasTimelineAnchor() const; + // Start a new animation timeline at anchor. Returns false if the anchor + // has no resolved simulation start. + bool TryStartTimelineAtAnchor(const AppAnchor& anchor); + // Set the current anchor's accumulated animation time after its origin row + // has been published. + void SetCurrentAnchorAnimationTimeMs(double publishedAnimationTimeMs); + bool IsSourceTransition(const AppAnchor& anchor) const; + // Resolve animation contexts for intervalRows and advance the current + // timeline anchor according to same-source, transition, and invalid-interval + // rules. + std::vector ResolveIntervalAndAdvanceAnchor( + const QpcConverter& qpc, + const AppAnchor& closingAnchor, + const std::vector& intervalRows); + AppAnchor ResolveAppAnchor( + const SwapChainCoreState& chainState, + const FrameData& present, + size_t displayIndex, + const FrameData* ingestPreviousPresent = nullptr) const; + private: + static AnimationErrorSource ResolveSource_(const FrameData& present); + static uint64_t ResolveSimStart_( + const SwapChainCoreState& chainState, + const FrameData& present, + AnimationErrorSource source, + const FrameData* ingestPreviousPresent); + AnimationDisplayContext StartTransitionTimelineAndBuildOriginContext_( + const AppAnchor& anchor); + std::vector ResolveSameSourceIntervalAndAdvanceAnchor_( + const QpcConverter& qpc, + const AppAnchor& closingAnchor, + const std::vector& intervalRows); + bool hasCurrentAnchor_ = false; + AppAnchor currentAnchor_{}; + uint64_t timelineFirstSimStartTime_ = 0; + }; +} diff --git a/IntelPresentMon/CommonUtilities/mc/DisplayFrameQueue.cpp b/IntelPresentMon/CommonUtilities/mc/DisplayFrameQueue.cpp new file mode 100644 index 00000000..fcb5bada --- /dev/null +++ b/IntelPresentMon/CommonUtilities/mc/DisplayFrameQueue.cpp @@ -0,0 +1,548 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: MIT +#include "DisplayFrameQueue.h" +#include "MetricsCalculatorInternal.h" +#include +#include +namespace pmon::util::metrics +{ + // Ingest is called for every present and is responsible for expanding presents into display instances, + // attaching animation contexts from the AnimationErrorTracker, and buffering until the publish policy allows + // rows to be released. If frame-type metadata exists, each metadata entry becomes a row even when + // the present was not displayed. Displayed generated rows wait for an app anchor; displayed app rows + // close the prior interval and then wait for lookahead before release. + std::vector DisplayFrameQueue::Ingest( + const QpcConverter& qpc, + FrameData present, + AnimationErrorTracker& animation, + SwapChainCoreState& chain) + { + const FrameData* ingestPreviousPresent = + lastIngestedPresent_.has_value() ? &lastIngestedPresent_.value() : nullptr; + + // Ingest may move `present` (not-displayed path); keep a copy for lastIngestedPresent_ + // after processing while ingestPreviousPresent still refers to the prior ingest above. + FrameData presentForIngestHistory = present; + + const bool presentIsDisplayed = IsDisplayed_(present); + const size_t displayCount = present.displayed.Size(); + + std::vector ready; + if (displayCount == 0) { + IngestNotDisplayedRow_(MakeNotDisplayedRow_(std::move(present)), ready); + lastIngestedPresent_ = std::move(presentForIngestHistory); + return ready; + } + + for (size_t displayIndex = 0; displayIndex < displayCount; ++displayIndex) { + ReadyDisplayRow row = BuildDisplayInstanceRow_(present, displayIndex, chain); + + if (!presentIsDisplayed) { + row.isDisplayed = false; + row.previousDisplayedScreenTime = chain.lastDisplayedScreenTime; + row.screenTime = 0; + row.nextScreenTime = 0; + IngestNotDisplayedRow_(std::move(row), ready); + continue; + } + + ApplyNvV2Adjustment_(row); + + // This displayed instance is lookahead for rows that were complete except + // for nextScreenTime. + CompleteRowsWaitingForDisplayLookahead_(row.screenTime, ready); + + // Rows in the open app-to-app interval use the next displayed instance + // for their display duration, even before the interval closes. + CompleteOpenTimelineDisplayLookahead_(row.screenTime); + AcceptDisplayOrder_(row); + if (!row.isAppFrame) { + IngestGeneratedDisplayInstance_(std::move(row), ready); + } + else { + IngestAppAnchorDisplayInstance_( + qpc, + std::move(row), + animation, + chain, + ingestPreviousPresent, + ready); + } + } + lastIngestedPresent_ = std::move(presentForIngestHistory); + return ready; + } + + // NoteSeedPresent is used to track the most recent present that has been ingested, + // regardless of whether it's displayed or not. This is used by the AnimationErrorTracker + // to resolve anchors for app anchor rows that are ingested before the next app anchor + // closes the interval, as well as for generated rows that are ingested before any app + // anchor has been seen. + void DisplayFrameQueue::NoteSeedPresent(const FrameData& seedPresent) + { + lastIngestedPresent_ = seedPresent; + } + + void DisplayFrameQueue::Clear() + { + lastAcceptedScreenTime_ = 0; + lastAcceptedPresentStartTime_ = 0; + lastAcceptedFlipDelay_ = 0; + hasObservedAppAnchor_ = false; + openTimelineRows_.clear(); + closedIntervalAwaitingDisplayLookahead_.clear(); + preFirstAppAnchorRowsAwaitingDisplayLookahead_.clear(); + timelineOriginAwaitingDisplayLookahead_.reset(); + lastIngestedPresent_.reset(); + } + + // BuildDisplayInstanceRow_ takes the incoming present, display + // index and chain state to start building up a display ready row + ReadyDisplayRow DisplayFrameQueue::BuildDisplayInstanceRow_( + const FrameData& present, + size_t displayIndex, + const SwapChainCoreState& chain) const + { + const size_t displayCount = present.displayed.Size(); + const auto frameType = present.displayed[displayIndex].first; + const auto screenTime = present.displayed[displayIndex].second; + const auto nextScreenTime = (displayIndex + 1 < displayCount) + ? present.displayed[displayIndex + 1].second + : screenTime; + const auto previousScreenTime = lastAcceptedScreenTime_ != 0 + ? lastAcceptedScreenTime_ + : chain.lastDisplayedScreenTime; + ReadyDisplayRow row{}; + row.present = present; + row.displayIndex = displayIndex; + row.previousDisplayedScreenTime = previousScreenTime; + row.screenTime = screenTime; + // Note that nextScreenTime will be equal to screenTime if the + // display count is 1 + row.nextScreenTime = nextScreenTime; + row.isDisplayed = true; + row.isAppFrame = IsAppAnchor_(frameType); + row.frameType = frameType; + row.updateSwapChainAfterRow = false; + return row; + } + + void DisplayFrameQueue::AcceptDisplayOrder_(ReadyDisplayRow& row) + { + lastAcceptedScreenTime_ = row.screenTime; + lastAcceptedPresentStartTime_ = row.present.presentStartTime; + lastAcceptedFlipDelay_ = row.present.flipDelay; + } + + // Display timing has no meaning for a not-displayed row, so it is trivially + // complete. Displayed rows with a sibling display entry already have lookahead. + // The explicit PendingRow flag handles later lookahead, including valid + // zero-duration cases where nextScreenTime == screenTime. + bool DisplayFrameQueue::DisplayTimingComplete_(const ReadyDisplayRow& row) + { + if (!row.isDisplayed) { + return true; + } + return row.displayIndex + 1 < row.present.displayed.Size(); + } + + // Step 5 gate: the single place publishability (design: Row Readiness) is + // checked. A row may only leave the queue once its animation context and display + // timing are both resolved, even when the animation result is intentionally "not set". + bool DisplayFrameQueue::IsPublishable_(const PendingRow& pending) + { + return pending.animationComplete && pending.displayTimingComplete; + } + + // Step 4: explicit state-update role policy (design: "Displayed present with an + // app row" / "Displayed present without an app row"). This depends only on the row's own + // present structure, display index, and frame type, so it gives the same answer + // regardless of which other rows from this present have already released or are + // still buffered: + // isAppFrame -> the app row represents + // the present. + // present has no app anchor and this is the final -> the final display row + // display index of the present represents the present + // (e.g. PresentFrameType_Info + // generated-only presents). + // otherwise -> this row does not + // represent the present. + DisplayFrameQueue::StateUpdateRole DisplayFrameQueue::ComputeStateUpdateRole_(const ReadyDisplayRow& row) + { + if (row.isAppFrame) { + return StateUpdateRole::AdvancesSwapChainState; + } + if (!PresentHasAppAnchor_(row.present) && + row.displayIndex + 1 == row.present.displayed.Size()) { + return StateUpdateRole::AdvancesSwapChainState; + } + return StateUpdateRole::None; + } + + // Step 5: release. Consumes the state-update role assigned in step 4 -- derived + // from the row's own present/display metadata -- to decide whether + // MetricsCalculator applies this row's swap-chain state update. Never decided by + // which rows happened to release together. + void DisplayFrameQueue::Release_(PendingRow pending, std::vector& ready) + { + assert(IsPublishable_(pending) && "DisplayFrameQueue: released a row that is not publishable"); + pending.row.updateSwapChainAfterRow = + pending.stateUpdateRole == StateUpdateRole::AdvancesSwapChainState; + ready.push_back(std::move(pending.row)); + } + + // Pending collections can hold rows with mixed readiness (e.g. a closing app + // anchor still awaiting lookahead alongside rows that just became complete), so + // this filters explicitly instead of assuming every entry is publishable. + void DisplayFrameQueue::ReleaseAll_(std::vector& pending, std::vector& ready) + { + std::vector retained; + for (auto& row : pending) { + if (IsPublishable_(row)) { + Release_(std::move(row), ready); + } + else { + retained.push_back(std::move(row)); + } + } + pending = std::move(retained); + } + + // Step 2: complete display timing. Closed intervals and timeline origins wait for + // the next displayed frame after the closing/origin app anchor. This method + // supplies that lookahead and releases those rows. Pre-first-app-anchor rows are + // handled in their routing branch. + void DisplayFrameQueue::CompleteRowsWaitingForDisplayLookahead_( + uint64_t nextScreenTime, + std::vector& ready) + { + if (!closedIntervalAwaitingDisplayLookahead_.empty()) { + closedIntervalAwaitingDisplayLookahead_.back().row.nextScreenTime = nextScreenTime; + closedIntervalAwaitingDisplayLookahead_.back().displayTimingComplete = true; + ReleaseAll_(closedIntervalAwaitingDisplayLookahead_, ready); + } + + if (timelineOriginAwaitingDisplayLookahead_.has_value()) { + timelineOriginAwaitingDisplayLookahead_->row.nextScreenTime = nextScreenTime; + timelineOriginAwaitingDisplayLookahead_->displayTimingComplete = true; + if (IsPublishable_(*timelineOriginAwaitingDisplayLookahead_)) { + Release_(std::move(*timelineOriginAwaitingDisplayLookahead_), ready); + timelineOriginAwaitingDisplayLookahead_.reset(); + } + } + } + + void DisplayFrameQueue::CompleteOpenTimelineDisplayLookahead_(uint64_t nextScreenTime) + { + for (auto pending = openTimelineRows_.rbegin(); pending != openTimelineRows_.rend(); ++pending) { + if (pending->row.isDisplayed) { + if (!pending->displayTimingComplete) { + pending->row.nextScreenTime = nextScreenTime; + pending->displayTimingComplete = true; + } + return; + } + } + } + + // ------------------------------------------------------------------------- + // Step 1: ingest routing -- not-displayed rows + // ------------------------------------------------------------------------- + void DisplayFrameQueue::IngestNotDisplayedRow_( + ReadyDisplayRow row, + std::vector& ready) + { + // If there's an app anchor, hold not-displayed rows until the next app anchor closes the interval, + // at which point they'll be released with animation metrics. Otherwise, they can be emitted immediately + // with missing animation metrics. This handles cases where we receive generated frames without a prior app anchor, + // which is expected to be common when the provider starts after the app has already started presenting. + // + // A not-displayed present can carry several frame-type metadata rows (one row per + // entry); the same representative-row policy as displayed presents applies (design: + // "the emitted not-displayed row that represents the present"), so the role is + // computed the same way: the app-type entry if there is one, otherwise the final + // metadata entry. + const auto role = ComputeStateUpdateRole_(row); + const bool displayTimingComplete = DisplayTimingComplete_(row); + if (hasObservedAppAnchor_) { + openTimelineRows_.push_back( + PendingRow{ std::move(row), /*animationComplete*/ false, displayTimingComplete, role }); + } + else { + Release_(PendingRow{ std::move(row), /*animationComplete*/ true, displayTimingComplete, role }, ready); + } + } + + // ------------------------------------------------------------------------- + // Step 1: ingest routing -- generated display instances + // ------------------------------------------------------------------------- + void DisplayFrameQueue::IngestGeneratedDisplayInstance_( + ReadyDisplayRow row, + std::vector& ready) + { + if (!hasObservedAppAnchor_) { + // Design: trace start without prior app anchor. + if (!preFirstAppAnchorRowsAwaitingDisplayLookahead_.empty()) { + auto pendingOrigin = std::move(preFirstAppAnchorRowsAwaitingDisplayLookahead_.back()); + preFirstAppAnchorRowsAwaitingDisplayLookahead_.pop_back(); + pendingOrigin.row.nextScreenTime = row.screenTime; + pendingOrigin.displayTimingComplete = true; + Release_(std::move(pendingOrigin), ready); + } + const auto role = ComputeStateUpdateRole_(row); + const bool displayTimingComplete = DisplayTimingComplete_(row); + PendingRow pending{ std::move(row), /*animationComplete*/ true, displayTimingComplete, role }; + if (pending.displayTimingComplete) { + Release_(std::move(pending), ready); + } + else { + preFirstAppAnchorRowsAwaitingDisplayLookahead_.push_back(std::move(pending)); + } + return; + } + // Design: hold generated rows until the next app anchor closes the interval. + // Role is assigned now, before the row is released by anything: a generated + // row from a present that has no app anchor of its own (PresentFrameType_Info + // generated-only present) still represents that present once it is the final + // display row, even while it sits inside this open interval. + const auto role = ComputeStateUpdateRole_(row); + const bool displayTimingComplete = DisplayTimingComplete_(row); + openTimelineRows_.push_back( + PendingRow{ std::move(row), /*animationComplete*/ false, displayTimingComplete, role }); + } + + // ------------------------------------------------------------------------- + // Step 3: resolve animation at app anchors (design: App Anchor, Closed Interval) + // ------------------------------------------------------------------------- + void DisplayFrameQueue::IngestAppAnchorDisplayInstance_( + const QpcConverter& qpc, + ReadyDisplayRow row, + AnimationErrorTracker& animation, + SwapChainCoreState& chain, + const FrameData* ingestPreviousPresent, + std::vector& ready) + { + const auto anchor = animation.ResolveAppAnchor( + chain, + row.present, + row.displayIndex, + ingestPreviousPresent); + const bool isFirstObservedAppAnchor = !hasObservedAppAnchor_; + hasObservedAppAnchor_ = true; + + if (isFirstObservedAppAnchor) { + if (!preFirstAppAnchorRowsAwaitingDisplayLookahead_.empty()) { + auto pendingOrigin = std::move(preFirstAppAnchorRowsAwaitingDisplayLookahead_.back()); + preFirstAppAnchorRowsAwaitingDisplayLookahead_.pop_back(); + pendingOrigin.row.nextScreenTime = row.screenTime; + pendingOrigin.displayTimingComplete = true; + Release_(std::move(pendingOrigin), ready); + } + PublishFirstTimelineOrigin_(qpc, std::move(row), animation, chain, anchor, ready); + openTimelineRows_.clear(); + return; + } + if (animation.IsSourceTransition(anchor)) { + PublishSourceTransition_( + qpc, + std::move(row), + animation, + chain, + ingestPreviousPresent, + ready); + return; + } + std::vector closedInterval; + closedInterval.reserve(openTimelineRows_.size() + 1); + for (auto& pending : openTimelineRows_) { + closedInterval.push_back(std::move(pending)); + } + const auto role = ComputeStateUpdateRole_(row); + const bool displayTimingComplete = DisplayTimingComplete_(row); + closedInterval.push_back(PendingRow{ std::move(row), /*animationComplete*/ false, displayTimingComplete, role }); + ResolveIntervalAndHoldForLookahead_(qpc, animation, anchor, std::move(closedInterval), ready); + openTimelineRows_.clear(); + } + + void DisplayFrameQueue::PublishFirstTimelineOrigin_( + const QpcConverter& qpc, + ReadyDisplayRow row, + AnimationErrorTracker& animation, + const SwapChainCoreState& chain, + const AnimationErrorTracker::AppAnchor& anchor, + std::vector& ready) + { + row.animation.msAnimationError = MissingFrameMetricValue(); + row.animation.source = anchor.source; + row.animation.resolvedSimStartTime = anchor.simStartTime; + row.animation.firstSimStartTime = chain.firstAppSimStartTime != 0 + ? chain.firstAppSimStartTime + : qpc.GetSessionStartTimestamp(); + row.animation.hasResolvedSimStart = anchor.simStartTime != 0; + + if (animation.TryStartTimelineAtAnchor(anchor)) { + row.animation.msAnimationTime = CalculateAnimationTime( + qpc, + chain.firstAppSimStartTime, + anchor.simStartTime); + animation.SetCurrentAnchorAnimationTimeMs(row.animation.msAnimationTime); + } + else { + row.animation.msAnimationTime = MissingFrameMetricValue(); + row.animation.hasResolvedSimStart = false; + } + + const auto role = ComputeStateUpdateRole_(row); + const bool displayTimingComplete = DisplayTimingComplete_(row); + PendingRow pending{ std::move(row), /*animationComplete*/ true, displayTimingComplete, role }; + if (pending.displayTimingComplete) { + Release_(std::move(pending), ready); + } + else { + timelineOriginAwaitingDisplayLookahead_ = std::move(pending); + } + } + + // Resolving an interval makes every row in it animationComplete, but rows do not + // have to release together (design: Closed Interval / Row Readiness). Generated + // rows already display-timing-complete (they already got nextScreenTime from a + // sibling or later display instance) publish immediately here. The closing app + // anchor is usually still missing its own nextScreenTime lookahead and stays + // buffered until CompleteRowsWaitingForDisplayLookahead_ supplies it. + void DisplayFrameQueue::ResolveIntervalAndHoldForLookahead_( + const QpcConverter& qpc, + AnimationErrorTracker& animation, + const AnimationErrorTracker::AppAnchor& closingAnchor, + std::vector intervalRows, + std::vector& ready) + { + std::vector animationRows; + animationRows.reserve(intervalRows.size()); + for (const auto& pending : intervalRows) { + if (pending.row.isDisplayed) { + animationRows.push_back(pending.row); + } + } + + const auto contexts = animation.ResolveIntervalAndAdvanceAnchor(qpc, closingAnchor, animationRows); + size_t animationIndex = 0; + for (size_t i = 0; i < intervalRows.size(); ++i) { + if (intervalRows[i].row.isDisplayed) { + intervalRows[i].row.animation = contexts[animationIndex++]; + } + intervalRows[i].animationComplete = true; + } + + for (auto& pending : intervalRows) { + if (IsPublishable_(pending)) { + Release_(std::move(pending), ready); + } + else { + closedIntervalAwaitingDisplayLookahead_.push_back(std::move(pending)); + } + } + } + + void DisplayFrameQueue::PublishSourceTransition_( + const QpcConverter& qpc, + ReadyDisplayRow transitionAppRow, + AnimationErrorTracker& animation, + const SwapChainCoreState& chain, + const FrameData* ingestPreviousPresent, + std::vector& ready) + { + CompleteOpenTimelineDisplayLookahead_(transitionAppRow.screenTime); + // Abandoned by the transition: animation is intentionally not set for these rows. + // Each row's nextScreenTime is already known (chained from the next display + // instance as it arrived, or from the transition row above), but route release + // through ReleaseAll_ rather than asserting so an unexpected gap retains the row + // instead of publishing it. + for (auto& pending : openTimelineRows_) { + pending.animationComplete = true; + } + ReleaseAll_(openTimelineRows_, ready); + + const auto anchor = animation.ResolveAppAnchor( + chain, + transitionAppRow.present, + transitionAppRow.displayIndex, + ingestPreviousPresent); + std::vector transitionInterval; + transitionInterval.push_back(std::move(transitionAppRow)); + const auto contexts = animation.ResolveIntervalAndAdvanceAnchor(qpc, anchor, transitionInterval); + transitionInterval[0].animation = contexts[0]; + const auto role = ComputeStateUpdateRole_(transitionInterval[0]); + const bool displayTimingComplete = DisplayTimingComplete_(transitionInterval[0]); + PendingRow pendingOrigin{ + std::move(transitionInterval[0]), /*animationComplete*/ true, displayTimingComplete, role }; + if (pendingOrigin.displayTimingComplete) { + Release_(std::move(pendingOrigin), ready); + } + else { + timelineOriginAwaitingDisplayLookahead_ = std::move(pendingOrigin); + } + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + bool DisplayFrameQueue::IsAppAnchor_(FrameType frameType) + { + return frameType == FrameType::Application || frameType == FrameType::NotSet; + } + + bool DisplayFrameQueue::IsDisplayed_(const FrameData& present) + { + return present.finalState == PresentResult::Presented && !present.displayed.Empty(); + } + + bool DisplayFrameQueue::PresentHasAppAnchor_(const FrameData& present) + { + for (size_t i = 0; i < present.displayed.Size(); ++i) { + if (IsAppAnchor_(present.displayed[i].first)) { + return true; + } + } + return false; + } + + ReadyDisplayRow DisplayFrameQueue::MakeNotDisplayedRow_(FrameData present) + { + ReadyDisplayRow row{}; + row.present = std::move(present); + row.isDisplayed = false; + row.isAppFrame = true; + row.updateSwapChainAfterRow = true; + row.frameType = FrameType::NotSet; + return row; + } + + void DisplayFrameQueue::ApplyNvV2Adjustment_(ReadyDisplayRow& row) const + { + if (lastAcceptedPresentStartTime_ == row.present.presentStartTime || + row.present.displayed.Empty()) { + return; + } + FrameData previous{}; + previous.flipDelay = lastAcceptedFlipDelay_; + FrameData adjustedNext = row.present; + adjustedNext.displayed.Clear(); + adjustedNext.displayed.PushBack({ row.frameType, row.screenTime }); + uint64_t previousScreenTime = lastAcceptedScreenTime_; + uint64_t adjustedScreenTime = row.screenTime; + AdjustScreenTimeForCollapsedPresentNV( + previous, + &adjustedNext, + 0, + 0, + previousScreenTime, + adjustedScreenTime, + MetricsVersion::V2); + row.screenTime = adjustedScreenTime; + row.present.flipDelay = adjustedNext.flipDelay; + row.present.displayed[row.displayIndex].second = adjustedNext.displayed[0].second; + if (row.nextScreenTime < row.screenTime) { + row.nextScreenTime = row.screenTime; + } + } +} diff --git a/IntelPresentMon/CommonUtilities/mc/DisplayFrameQueue.h b/IntelPresentMon/CommonUtilities/mc/DisplayFrameQueue.h new file mode 100644 index 00000000..9768af09 --- /dev/null +++ b/IntelPresentMon/CommonUtilities/mc/DisplayFrameQueue.h @@ -0,0 +1,141 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: MIT +#pragma once +#include +#include +#include "../qpc.h" +#include "AnimationErrorTracker.h" +#include "MetricsTypes.h" +#include "SwapChainState.h" +namespace pmon::util::metrics +{ + // Display queue + // + // Each display instance moves through this lifecycle: + // 1. Ingest - expand FrameData.displayed into one row per display instance. + // 2. Complete display timing - supply nextScreenTime lookahead for a held row + // (CompleteRowsWaitingForDisplayLookahead_); see DisplayTimingComplete_. + // 3. Resolve animation - close an app-to-app interval at the next app anchor + // and attach the result from AnimationErrorTracker; see animationComplete. + // 4. Derive state-update role - decide, from the row's own present/display + // metadata, whether it advances long-lived swap-chain state when applied + // (ComputeStateUpdateRole_). + // 5. Release - emit the row once animationComplete && displayTimingComplete + // (IsPublishable_), writing the role into + // ReadyDisplayRow::updateSwapChainAfterRow (Release_ / ReleaseAll_). + class DisplayFrameQueue + { + public: + std::vector Ingest( + const QpcConverter& qpc, + FrameData present, + AnimationErrorTracker& animation, + SwapChainCoreState& chain); + void NoteSeedPresent(const FrameData& seedPresent); + void Clear(); + private: + // --- Steps 4-5: state-update role and release (design: "Row Readiness") --- + // Whether a held row's swap-chain state update, if any, advances long-lived + // swap-chain state when the row is applied. Derived explicitly from the + // row's own present/display metadata (see ComputeStateUpdateRole_) and + // consumed by Release_, which is the single place + // ReadyDisplayRow::updateSwapChainAfterRow is set. + enum class StateUpdateRole + { + None, + AdvancesSwapChainState, + }; + // Internal bookkeeping for a display instance held inside the queue. A row is + // only publishable once animationComplete and displayTimingComplete are both true. + struct PendingRow + { + ReadyDisplayRow row; + bool animationComplete = false; + bool displayTimingComplete = false; + StateUpdateRole stateUpdateRole = StateUpdateRole::None; + }; + static bool DisplayTimingComplete_(const ReadyDisplayRow& row); + static bool IsPublishable_(const PendingRow& pending); + // Explicit state-update role policy (design: "Displayed present with/without an + // app row"). Computed purely from this row's own present structure, display + // index, and frame type -- never from which other rows happen to be released + // in the same Ingest/ProcessPresent call. + static StateUpdateRole ComputeStateUpdateRole_(const ReadyDisplayRow& row); + // Single place every release path funnels through: only emits rows whose + // lifecycle state (design: Release Rule) is publishable, and is where the + // row's explicit state-update role is written into updateSwapChainAfterRow. + static void Release_(PendingRow pending, std::vector& ready); + static void ReleaseAll_(std::vector& pending, std::vector& ready); + + // --- Step 1: construct the display-instance row --- + ReadyDisplayRow BuildDisplayInstanceRow_( + const FrameData& present, + size_t displayIndex, + const SwapChainCoreState& chain) const; + void ApplyNvV2Adjustment_(ReadyDisplayRow& row) const; + void AcceptDisplayOrder_(ReadyDisplayRow& row); + // --- Step 2: complete display timing for already-held rows --- + // Supplies nextScreenTime lookahead to rows held only on displayTimingComplete, + // then releases the ones that become publishable. + void CompleteRowsWaitingForDisplayLookahead_( + uint64_t nextScreenTime, + std::vector& ready); + void CompleteOpenTimelineDisplayLookahead_(uint64_t nextScreenTime); + void IngestNotDisplayedRow_( + ReadyDisplayRow row, + std::vector& ready); + void IngestGeneratedDisplayInstance_( + ReadyDisplayRow row, + std::vector& ready); + // --- Step 3: resolve animation at app anchors --- + void IngestAppAnchorDisplayInstance_( + const QpcConverter& qpc, + ReadyDisplayRow row, + AnimationErrorTracker& animation, + SwapChainCoreState& chain, + const FrameData* ingestPreviousPresent, + std::vector& ready); + void PublishFirstTimelineOrigin_( + const QpcConverter& qpc, + ReadyDisplayRow row, + AnimationErrorTracker& animation, + const SwapChainCoreState& chain, + const AnimationErrorTracker::AppAnchor& anchor, + std::vector& ready); + void ResolveIntervalAndHoldForLookahead_( + const QpcConverter& qpc, + AnimationErrorTracker& animation, + const AnimationErrorTracker::AppAnchor& closingAnchor, + std::vector intervalRows, + std::vector& ready); + void PublishSourceTransition_( + const QpcConverter& qpc, + ReadyDisplayRow transitionAppRow, + AnimationErrorTracker& animation, + const SwapChainCoreState& chain, + const FrameData* ingestPreviousPresent, + std::vector& ready); + static bool IsAppAnchor_(FrameType frameType); + static bool IsDisplayed_(const FrameData& present); + static bool PresentHasAppAnchor_(const FrameData& present); + static ReadyDisplayRow MakeNotDisplayedRow_(FrameData present); + uint64_t lastAcceptedScreenTime_ = 0; + uint64_t lastAcceptedPresentStartTime_ = 0; + uint64_t lastAcceptedFlipDelay_ = 0; + bool hasObservedAppAnchor_ = false; + // Rows after an app anchor, in ingest order, until the next app anchor closes + // the interval. Only displayed rows participate in animation resolution. + std::vector openTimelineRows_; + // Closed interval rows waiting for the displayed frame after the closing app + // anchor. Already animation-complete; held only for displayTimingComplete. + std::vector closedIntervalAwaitingDisplayLookahead_; + // Trace start before first app anchor; animation intentionally complete-but-missing. + std::vector preFirstAppAnchorRowsAwaitingDisplayLookahead_; + // First app anchor (or source-transition origin) waiting for nextScreenTime. + // Already animation-complete. + std::optional timelineOriginAwaitingDisplayLookahead_; + // Last present that entered Ingest (including held rows). Used for anchor CpuStart + // when swap chain lastPresent has not advanced yet. + std::optional lastIngestedPresent_; + }; +} diff --git a/IntelPresentMon/CommonUtilities/mc/MetricsCalculator.cpp b/IntelPresentMon/CommonUtilities/mc/MetricsCalculator.cpp index 27acf9f2..14d1e33e 100644 --- a/IntelPresentMon/CommonUtilities/mc/MetricsCalculator.cpp +++ b/IntelPresentMon/CommonUtilities/mc/MetricsCalculator.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2026 Intel Corporation +// Copyright (C) 2026 Intel Corporation // SPDX-License-Identifier: MIT #include "MetricsCalculator.h" #include "MetricsCalculatorInternal.h" @@ -59,7 +59,7 @@ namespace pmon::util::metrics *d.newLastReceivedPclSimStart; } - // Accumulated PC latency input→frame-start time + // Accumulated PC latency input->frame-start time if (d.newAccumulatedInput2FrameStart) { chainState.accumulatedInput2FrameStartTime = @@ -91,9 +91,7 @@ namespace pmon::util::metrics std::vector ComputeMetricsForPresent( const QpcConverter& qpc, FrameData& present, - FrameData* nextDisplayed, - SwapChainCoreState& chainState, - MetricsVersion version) + SwapChainCoreState& chainState) { std::vector results; @@ -106,15 +104,11 @@ namespace pmon::util::metrics const uint64_t nextScreenTime = 0; const bool isDisplayed = false; - // Legacy-equivalent attribution: compute displayIndex/appIndex and derive isAppFrame. - const auto indexing = DisplayIndexing::Calculate(present, nextDisplayed); - const size_t displayIndex = indexing.startIndex; // Case 1 => 0 - const size_t appIndex = indexing.appIndex; - - const bool isAppFrame = (displayIndex == appIndex); const FrameType frameType = (displayCount > 0) - ? present.displayed[displayIndex].first + ? present.displayed[0].first : FrameType::NotSet; + const bool isAppFrame = + frameType == FrameType::Application || frameType == FrameType::NotSet; auto metrics = ComputeFrameMetrics( qpc, @@ -125,115 +119,81 @@ namespace pmon::util::metrics isDisplayed, isAppFrame, frameType, + AnimationDisplayContext{}, chainState); ApplyStateDeltas(chainState, metrics.stateDeltas); results.push_back(std::move(metrics)); - chainState.UpdateAfterPresent(present); - - return results; - } - - // V1: displayed presents are computed immediately (no look-ahead / no postponing). - // Emit exactly one row per present (legacy V1 behavior). - if (version == MetricsVersion::V1) { - const size_t displayIndex = 0; - uint64_t screenTime = present.displayed[displayIndex].second; - uint64_t nextScreenTime = 0; - - AdjustScreenTimeForCollapsedPresentNV( - present, - nextDisplayed, - chainState.lastDisplayedFlipDelay, - chainState.lastDisplayedScreenTime, - screenTime, - nextScreenTime, - version); - - // This is so msDisplayedTime comes back as 0 instead of garbage for V1 single-row output. - // TODO: Better option is to have display metrics be optional. Update metrics struct accordingly. - nextScreenTime = screenTime; - const auto indexing = DisplayIndexing::Calculate(present, nullptr); - const bool isAppFrame = (displayIndex == indexing.appIndex); - const bool isDisplayedInstance = isDisplayed && screenTime != 0; - const FrameType frameType = isDisplayedInstance ? present.displayed[displayIndex].first : FrameType::NotSet; - - auto metrics = ComputeFrameMetrics( - qpc, - present, - chainState.lastDisplayedScreenTime, - screenTime, - nextScreenTime, - isDisplayedInstance, - isAppFrame, - frameType, - chainState); - - ApplyStateDeltas(chainState, metrics.stateDeltas); - results.push_back(std::move(metrics)); + chainState.UpdateAfterPresentV1(present); - chainState.UpdateAfterPresent(present); return results; } - // There is at least one displayed frame to process - const auto indexing = DisplayIndexing::Calculate(present, nextDisplayed); - - // Determine if we should update the swap chain based on nextDisplayed - const bool shouldUpdateSwapChain = (nextDisplayed != nullptr); + const size_t displayIndex = 0; + uint64_t screenTime = present.displayed[displayIndex].second; + uint64_t nextScreenTime = 0; - for (size_t displayIndex = indexing.startIndex; displayIndex < indexing.endIndex; ++displayIndex) { - const uint64_t previousDisplayedScreenTime = - displayIndex > 0 - ? present.displayed[displayIndex - 1].second - : chainState.lastDisplayedScreenTime; - uint64_t screenTime = present.displayed[displayIndex].second; - uint64_t nextScreenTime = 0; - - if (displayIndex + 1 < displayCount) { - // Next display instance of the same present - nextScreenTime = present.displayed[displayIndex + 1].second; - } - else if (nextDisplayed != nullptr && !nextDisplayed->displayed.Empty()) { - // First display of the *next* presented frame - nextScreenTime = nextDisplayed->displayed[0].second; - } - else { - break; // No next screen time available - } - - AdjustScreenTimeForCollapsedPresentNV(present, nextDisplayed, 0, 0, screenTime, nextScreenTime, version); + AdjustScreenTimeForCollapsedPresentNV( + present, + nullptr, + chainState.lastDisplayedFlipDelay, + chainState.lastDisplayedScreenTime, + screenTime, + nextScreenTime, + MetricsVersion::V1); - const bool isAppFrame = (displayIndex == indexing.appIndex); - const bool isDisplayedInstance = isDisplayed && screenTime != 0 && nextScreenTime != 0; - const FrameType frameType = isDisplayedInstance ? present.displayed[displayIndex].first : FrameType::NotSet; + // This is so msDisplayedTime comes back as 0 instead of garbage for V1 single-row output. + // TODO: Better option is to have display metrics be optional. Update metrics struct accordingly. + nextScreenTime = screenTime; + const bool isDisplayedInstance = isDisplayed && screenTime != 0; + const FrameType frameType = isDisplayedInstance ? present.displayed[displayIndex].first : FrameType::NotSet; + const bool isAppFrame = + frameType == FrameType::Application || frameType == FrameType::NotSet; - auto metrics = ComputeFrameMetrics( - qpc, - present, - previousDisplayedScreenTime, - screenTime, - nextScreenTime, - isDisplayedInstance, - isAppFrame, - frameType, - chainState); + auto metrics = ComputeFrameMetrics( + qpc, + present, + chainState.lastDisplayedScreenTime, + screenTime, + nextScreenTime, + isDisplayedInstance, + isAppFrame, + frameType, + AnimationDisplayContext{}, + chainState); - ApplyStateDeltas(chainState, metrics.stateDeltas); + ApplyStateDeltas(chainState, metrics.stateDeltas); + results.push_back(std::move(metrics)); - results.push_back(std::move(metrics)); - } + chainState.UpdateAfterPresentV1(present); + return results; + } - // Matches old ReportMetricsHelper: - // - Case 2 (no nextDisplayed): no UpdateChain yet. - // - Case 3 (has nextDisplayed): this is the call that finally updates the chain. - if (shouldUpdateSwapChain) { - chainState.UpdateAfterPresent(present); + ComputedMetrics ComputeMetricsForReadyDisplayRow( + const QpcConverter& qpc, + const ReadyDisplayRow& row, + SwapChainCoreState& chainState) + { + auto metrics = ComputeFrameMetrics( + qpc, + row.present, + row.previousDisplayedScreenTime, + row.screenTime, + row.nextScreenTime, + row.isDisplayed, + row.isAppFrame, + row.frameType, + row.animation, + chainState); + + ApplyStateDeltas(chainState, metrics.stateDeltas); + if (row.updateSwapChainAfterRow) { + chainState.UpdateAfterReadyDisplayRow(row); } - return results; + return metrics; } // ============================================================================ @@ -248,6 +208,7 @@ namespace pmon::util::metrics bool isDisplayed, bool isAppFrame, FrameType frameType, + const AnimationDisplayContext& animation, const SwapChainCoreState& chain) { @@ -286,12 +247,7 @@ namespace pmon::util::metrics metrics); CalculateAnimationMetrics( - qpc, - chain, - present, - isDisplayed, - isAppFrame, - screenTime, + animation, metrics); CalculateInputLatencyMetrics( @@ -329,10 +285,26 @@ namespace pmon::util::metrics // ============================================================================ // 4) Exported helper definitions (declared in MetricsCalculator.h) // ============================================================================ + namespace + { + uint64_t PresentEndQpc_(const FrameData& present) + { + if (present.appPropagatedPresentStartTime != 0) { + return present.appPropagatedPresentStartTime + present.appPropagatedTimeInPresent; + } + return present.presentStartTime + present.timeInPresent; + } + } + uint64_t CalculateCPUStart( const SwapChainCoreState& chainState, - const FrameData& present) + const FrameData& present, + const FrameData* ingestPreviousPresent) { + if (ingestPreviousPresent != nullptr) { + return PresentEndQpc_(*ingestPreviousPresent); + } + uint64_t cpuStart = 0; if (chainState.lastAppPresent.has_value()) { const auto& lastAppPresent = chainState.lastAppPresent.value(); @@ -352,36 +324,4 @@ namespace pmon::util::metrics return cpuStart; } - // Helper: Calculate simulation start time (for animation error) - uint64_t CalculateAnimationErrorSimStartTime( - const SwapChainCoreState& chainState, - const FrameData& present, - AnimationErrorSource source) - { - uint64_t simStartTime = 0; - if (source == AnimationErrorSource::CpuStart) { - simStartTime = CalculateCPUStart(chainState, present); - } - else if (source == AnimationErrorSource::AppProvider) { - simStartTime = present.appSimStartTime; - } - else if (source == AnimationErrorSource::PCLatency) { - simStartTime = present.pclSimStartTime; - } - return simStartTime; - } - - // Helper: Calculate animation time - double CalculateAnimationTime( - const QpcConverter& qpc, - uint64_t firstAppSimStartTime, - uint64_t currentSimTime) - { - double animationTime = 0.0; - uint64_t firstSimStartTime = firstAppSimStartTime != 0 ? firstAppSimStartTime : qpc.GetSessionStartTimestamp(); - if (currentSimTime > firstSimStartTime) { - animationTime = qpc.DeltaUnsignedMilliSeconds(firstSimStartTime, currentSimTime); - } - return animationTime; - } } diff --git a/IntelPresentMon/CommonUtilities/mc/MetricsCalculator.h b/IntelPresentMon/CommonUtilities/mc/MetricsCalculator.h index aeda706a..8ac2f66e 100644 --- a/IntelPresentMon/CommonUtilities/mc/MetricsCalculator.h +++ b/IntelPresentMon/CommonUtilities/mc/MetricsCalculator.h @@ -26,24 +26,15 @@ namespace pmon::util::metrics } stateDeltas; }; - // Index calculation helper - struct DisplayIndexing { - size_t startIndex; // First display index to process - size_t endIndex; // One past last index - size_t appIndex; // Index of app frame (or SIZE_MAX if none) - bool hasNextDisplayed; - - static DisplayIndexing Calculate( - const FrameData& present, - const FrameData* nextDisplayed); - }; - std::vector ComputeMetricsForPresent( const QpcConverter& qpc, FrameData& present, - FrameData* nextDisplayed, - SwapChainCoreState& chainState, - MetricsVersion version = MetricsVersion::V2); + SwapChainCoreState& chainState); + + ComputedMetrics ComputeMetricsForReadyDisplayRow( + const QpcConverter& qpc, + const ReadyDisplayRow& row, + SwapChainCoreState& chainState); // === Pure Calculation Functions === @@ -56,22 +47,21 @@ namespace pmon::util::metrics bool isDisplayed, bool isAppFrame, FrameType frameType, + const AnimationDisplayContext& animation, const SwapChainCoreState& chain); - // Helper: Calculate CPU start time + // Helper: Calculate CPU start time for the current present. + // When ingestPreviousPresent is set (Ingest path), use that present's end instead of + // swap chain history, which only advances on Apply after held rows are released. uint64_t CalculateCPUStart( - const SwapChainCoreState& chainState, - const FrameData& present); - - // Helper: Calculate simulation start time (for animation error) - uint64_t CalculateAnimationErrorSimStartTime( const SwapChainCoreState& chainState, const FrameData& present, - AnimationErrorSource source); + const FrameData* ingestPreviousPresent = nullptr); - // Helper: Calculate animation time + // Elapsed animation time from firstAppSimStartTime (or session QPC) to currentSimTime. + // Returns missing when currentSimTime is not after the baseline (non-transition rows never emit 0). double CalculateAnimationTime( const QpcConverter& qpc, uint64_t firstAppSimStartTime, uint64_t currentSimTime); -} \ No newline at end of file +} diff --git a/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorAnimation.cpp b/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorAnimation.cpp index 49d8cb05..a59b7d03 100644 --- a/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorAnimation.cpp +++ b/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorAnimation.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2025 Intel Corporation +// Copyright (C) 2025 Intel Corporation // SPDX-License-Identifier: MIT #include "MetricsCalculator.h" #include "MetricsCalculatorInternal.h" @@ -10,119 +10,6 @@ namespace pmon::util::metrics { namespace { - struct AnimationSourceContext - { - AnimationErrorSource effectiveSource = AnimationErrorSource::CpuStart; - uint64_t currentSimStart = 0; - bool isTransitionFrame = false; - }; - - AnimationSourceContext ResolveAnimationSourceContext( - const SwapChainCoreState& chain, - const FrameData& present) - { - AnimationSourceContext context{}; - context.effectiveSource = chain.animationErrorSource; - - switch (chain.animationErrorSource) { - case AnimationErrorSource::AppProvider: - break; - - case AnimationErrorSource::PCLatency: - if (present.appSimStartTime != 0) { - context.effectiveSource = AnimationErrorSource::AppProvider; - context.isTransitionFrame = true; - } - break; - - case AnimationErrorSource::CpuStart: - if (present.appSimStartTime != 0) { - context.effectiveSource = AnimationErrorSource::AppProvider; - context.isTransitionFrame = true; - } - else if (present.pclSimStartTime != 0) { - context.effectiveSource = AnimationErrorSource::PCLatency; - context.isTransitionFrame = true; - } - break; - } - - context.currentSimStart = CalculateAnimationErrorSimStartTime( - chain, - present, - context.effectiveSource); - - return context; - } - - // ---- Animation metrics ---- - double ComputeAnimationError( - const QpcConverter& qpc, - const SwapChainCoreState& chain, - const FrameData& present, - bool isDisplayed, - bool isAppFrame, - uint64_t screenTime) - { - if (!isDisplayed || !isAppFrame) { - return MissingFrameMetricValue(); - } - - const auto sourceContext = ResolveAnimationSourceContext(chain, present); - - if (sourceContext.isTransitionFrame) { - return MissingFrameMetricValue(); - } - - if (sourceContext.currentSimStart == 0 || - chain.lastDisplayedSimStartTime == 0 || - sourceContext.currentSimStart <= chain.lastDisplayedSimStartTime || - chain.lastDisplayedAppScreenTime == 0) { - return MissingFrameMetricValue(); - } - - double simElapsed = qpc.DeltaUnsignedMilliSeconds( - chain.lastDisplayedSimStartTime, - sourceContext.currentSimStart); - double displayElapsed = qpc.DeltaUnsignedMilliSeconds(chain.lastDisplayedAppScreenTime, screenTime); - - if (simElapsed == 0.0 || displayElapsed == 0.0) { - return MissingFrameMetricValue(); - } - - return simElapsed - displayElapsed; - } - - - double ComputeAnimationTime( - const QpcConverter& qpc, - const SwapChainCoreState& chain, - const FrameData& present, - bool isDisplayed, - bool isAppFrame) - { - if (!isDisplayed || !isAppFrame) { - return MissingFrameMetricValue(); - } - - const auto sourceContext = ResolveAnimationSourceContext(chain, present); - - if (sourceContext.isTransitionFrame) { - return 0.0; - } - - if (sourceContext.effectiveSource != AnimationErrorSource::CpuStart && - sourceContext.currentSimStart == 0) { - return MissingFrameMetricValue(); - } - - if (sourceContext.currentSimStart == 0) { - return 0.0; - } - - return CalculateAnimationTime(qpc, chain.firstAppSimStartTime, sourceContext.currentSimStart); - } - double ComputePresentStartTimeMs( const QpcConverter& qpc, const FrameData& present) @@ -179,27 +66,24 @@ namespace pmon::util::metrics } void CalculateAnimationMetrics( - const QpcConverter& qpc, - const SwapChainCoreState& swapChain, - const FrameData& present, - bool isDisplayed, - bool isAppFrame, - uint64_t screenTime, + const AnimationDisplayContext& animation, FrameMetrics& metrics) { - metrics.msAnimationError = ComputeAnimationError( - qpc, - swapChain, - present, - isDisplayed, - isAppFrame, - screenTime); + metrics.msAnimationError = animation.msAnimationError; + metrics.msAnimationTime = animation.msAnimationTime; + } - metrics.msAnimationTime = ComputeAnimationTime( - qpc, - swapChain, - present, - isDisplayed, - isAppFrame); + double CalculateAnimationTime( + const QpcConverter& qpc, + uint64_t firstAppSimStartTime, + uint64_t currentSimTime) + { + const uint64_t firstSimStartTime = firstAppSimStartTime != 0 + ? firstAppSimStartTime + : qpc.GetSessionStartTimestamp(); + if (currentSimTime <= firstSimStartTime) { + return MissingFrameMetricValue(); + } + return qpc.DeltaUnsignedMilliSeconds(firstSimStartTime, currentSimTime); } } diff --git a/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorDisplay.cpp b/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorDisplay.cpp index 9776cd5b..da8332d9 100644 --- a/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorDisplay.cpp +++ b/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorDisplay.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2026 Intel Corporation +// Copyright (C) 2026 Intel Corporation // SPDX-License-Identifier: MIT #include "MetricsCalculator.h" #include "MetricsCalculatorInternal.h" @@ -135,61 +135,6 @@ namespace pmon::util::metrics } - DisplayIndexing DisplayIndexing::Calculate( - const FrameData& present, - const FrameData* nextDisplayed) - { - DisplayIndexing result{}; - - // Get display count - auto displayCount = present.displayed.Size(); // ConsoleAdapter/PresentSnapshot method - - // Check if displayed - bool displayed = present.finalState == PresentResult::Presented && displayCount > 0; - - // hasNextDisplayed - result.hasNextDisplayed = (nextDisplayed != nullptr); - - // Figure out range to process based on three cases: - // Case 1: Not displayed → empty range [0, 0) - // Case 2: Displayed, no next → process [0..N-2], postpone N-1 → range [0, N-1) - // Case 3: Displayed, with next → process postponed [N-1] → range [N-1, N) - - if (!displayed || displayCount == 0) { - // Case 1: Not displayed - result.startIndex = 0; - result.endIndex = 0; // Empty range - } - else if (nextDisplayed == nullptr) { - // Case 2: Postpone last display - result.startIndex = 0; - result.endIndex = displayCount - 1; // One past [N-2] = [N-1] (excludes last!) - } - else { - // Case 3: Process postponed last display - result.startIndex = displayCount - 1; - result.endIndex = displayCount; // One past [N-1] = [N] - } - - // appIndex - find first NotSet or Application frame - // Search from startIndex through ALL displays (not just the processing range) - result.appIndex = std::numeric_limits::max(); - if (displayCount > 0) { - for (size_t i = result.startIndex; i < displayCount; ++i) { - auto frameType = present.displayed[i].first; - if (frameType == FrameType::NotSet || frameType == FrameType::Application) { - result.appIndex = i; - break; - } - } - } - else { - result.appIndex = 0; - } - return result; - } - - void CalculateDisplayMetrics( const QpcConverter& qpc, const FrameData& present, diff --git a/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorInternal.h b/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorInternal.h index 04a839cc..4c7953d5 100644 --- a/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorInternal.h +++ b/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorInternal.h @@ -1,4 +1,4 @@ -// Copyright (C) 2026 Intel Corporation +// Copyright (C) 2026 Intel Corporation // SPDX-License-Identifier: MIT #pragma once @@ -31,12 +31,7 @@ namespace pmon::util::metrics FrameMetrics& metrics); void CalculateAnimationMetrics( - const QpcConverter& qpc, - const SwapChainCoreState& swapChain, - const FrameData& present, - bool isDisplayed, - bool isAppFrame, - uint64_t screenTime, + const AnimationDisplayContext& animation, FrameMetrics& metrics); void CalculateInputLatencyMetrics( @@ -80,4 +75,4 @@ namespace pmon::util::metrics uint64_t& screenTime, uint64_t& nextScreenTime, MetricsVersion version); -} \ No newline at end of file +} diff --git a/IntelPresentMon/CommonUtilities/mc/MetricsTypes.h b/IntelPresentMon/CommonUtilities/mc/MetricsTypes.h index 12e0d2c5..9583f103 100644 --- a/IntelPresentMon/CommonUtilities/mc/MetricsTypes.h +++ b/IntelPresentMon/CommonUtilities/mc/MetricsTypes.h @@ -186,4 +186,26 @@ namespace pmon::util::metrics { bool allowsTearing = false; PresentMode presentMode = {}; }; + + struct AnimationDisplayContext { + double msAnimationError = MissingFrameMetricValue(); + double msAnimationTime = MissingFrameMetricValue(); + AnimationErrorSource source = AnimationErrorSource::CpuStart; + uint64_t resolvedSimStartTime = 0; + uint64_t firstSimStartTime = 0; + bool hasResolvedSimStart = false; + }; + + struct ReadyDisplayRow { + FrameData present; + size_t displayIndex = 0; + uint64_t previousDisplayedScreenTime = 0; + uint64_t screenTime = 0; + uint64_t nextScreenTime = 0; + bool isDisplayed = false; + bool isAppFrame = false; + bool updateSwapChainAfterRow = true; + FrameType frameType = FrameType::NotSet; + AnimationDisplayContext animation; + }; } diff --git a/IntelPresentMon/CommonUtilities/mc/SwapChainState.cpp b/IntelPresentMon/CommonUtilities/mc/SwapChainState.cpp index 7d48a327..b5288dc6 100644 --- a/IntelPresentMon/CommonUtilities/mc/SwapChainState.cpp +++ b/IntelPresentMon/CommonUtilities/mc/SwapChainState.cpp @@ -1,12 +1,33 @@ -// Copyright (C) 2025 Intel Corporation +// Copyright (C) 2025 Intel Corporation // SPDX-License-Identifier: MIT #include "SwapChainState.h" #include namespace pmon::util::metrics { - void SwapChainCoreState::UpdateAfterPresent(const FrameData& present) + namespace { + bool IsAppFrameType_(FrameType frameType) + { + return frameType == FrameType::NotSet || frameType == FrameType::Application; + } + + size_t FindFirstAppDisplayIndex_(const FrameData& present) + { + const size_t displayCnt = present.displayed.Size(); + for (size_t i = 0; i < displayCnt; ++i) { + if (IsAppFrameType_(present.displayed[i].first)) { + return i; + } + } + return static_cast(-1); + } + } + + void SwapChainCoreState::UpdateAfterPresentV1(const FrameData& present) + { + // V1 spreadsheet present-level update path. V1 reports one row per present + // and intentionally uses the existing Displayed.Back()-based attribution rules. const auto finalState = present.finalState; const size_t displayCnt = present.displayed.Size(); @@ -14,8 +35,7 @@ namespace pmon::util::metrics { if (displayCnt > 0) { const size_t lastIdx = displayCnt - 1; const auto lastType = present.displayed[lastIdx].first; - const bool lastIsAppFrm = - (lastType == FrameType::NotSet || lastType == FrameType::Application); + const bool lastIsAppFrm = IsAppFrameType_(lastType); if (lastIsAppFrm) { const uint64_t lastScreenTime = present.displayed[lastIdx].second; @@ -87,7 +107,7 @@ namespace pmon::util::metrics { if (displayCnt > 0) { const size_t lastIdx = displayCnt - 1; const auto lastType = present.displayed[lastIdx].first; - if (lastType == FrameType::NotSet || lastType == FrameType::Application) { + if (IsAppFrameType_(lastType)) { lastAppPresent = present; } } @@ -108,4 +128,131 @@ namespace pmon::util::metrics { lastPresent = present; } + void SwapChainCoreState::UpdateAfterBootstrapPresentV2(const FrameData& present) + { + // V2 bootstrap state update used only for the first present on a swap chain. + // Search for the app index if the present has multiple entries in the display + // vector. + const auto finalState = present.finalState; + const size_t displayCnt = present.displayed.Size(); + const bool isDisplayed = finalState == PresentResult::Presented && displayCnt > 0; + const size_t appIdx = FindFirstAppDisplayIndex_(present); + + if (isDisplayed) { + const size_t lastIdx = displayCnt - 1; + lastDisplayedScreenTime = present.displayed[lastIdx].second; + lastDisplayedFlipDelay = present.flipDelay; + + if (appIdx != static_cast(-1)) { + const uint64_t appScreenTime = present.displayed[appIdx].second; + + if (animationErrorSource == AnimationErrorSource::AppProvider) { + if (present.appSimStartTime != 0) { + lastDisplayedSimStartTime = present.appSimStartTime; + if (firstAppSimStartTime == 0) { + firstAppSimStartTime = present.appSimStartTime; + } + lastDisplayedAppScreenTime = appScreenTime; + } + } + else if (animationErrorSource == AnimationErrorSource::PCLatency) { + if (present.appSimStartTime != 0) { + animationErrorSource = AnimationErrorSource::AppProvider; + firstAppSimStartTime = present.appSimStartTime; + lastDisplayedSimStartTime = present.appSimStartTime; + lastDisplayedAppScreenTime = appScreenTime; + } + else if (present.pclSimStartTime != 0) { + lastDisplayedSimStartTime = present.pclSimStartTime; + if (firstAppSimStartTime == 0) { + firstAppSimStartTime = present.pclSimStartTime; + } + lastDisplayedAppScreenTime = appScreenTime; + } + } + else { // AnimationErrorSource::CpuStart + if (present.appSimStartTime != 0) { + animationErrorSource = AnimationErrorSource::AppProvider; + firstAppSimStartTime = present.appSimStartTime; + lastDisplayedSimStartTime = present.appSimStartTime; + lastDisplayedAppScreenTime = appScreenTime; + } + else if (present.pclSimStartTime != 0) { + animationErrorSource = AnimationErrorSource::PCLatency; + firstAppSimStartTime = present.pclSimStartTime; + lastDisplayedSimStartTime = present.pclSimStartTime; + lastDisplayedAppScreenTime = appScreenTime; + } + else { + if (lastAppPresent.has_value()) { + const FrameData& lastApp = lastAppPresent.value(); + lastDisplayedSimStartTime = + lastApp.presentStartTime + lastApp.timeInPresent; + } + lastDisplayedAppScreenTime = appScreenTime; + } + } + + lastAppPresent = present; + } + } + else { + if (displayCnt == 0 || appIdx != static_cast(-1)) { + lastAppPresent = present; + } + } + + if (present.pclSimStartTime != 0) { + lastSimStartTime = present.pclSimStartTime; + } + else if (present.appSimStartTime != 0) { + lastSimStartTime = present.appSimStartTime; + } + + lastPresent = present; + } + + void SwapChainCoreState::UpdateAfterReadyDisplayRow(const ReadyDisplayRow& row) + { + const auto& present = row.present; + + if (row.isDisplayed) { + lastDisplayedScreenTime = row.screenTime; + lastDisplayedFlipDelay = present.flipDelay; + + if (row.isAppFrame) { + lastDisplayedAppScreenTime = row.screenTime; + lastAppPresent = present; + + if (row.animation.hasResolvedSimStart) { + animationErrorSource = row.animation.source; + lastDisplayedSimStartTime = row.animation.resolvedSimStartTime; + if (row.animation.firstSimStartTime != 0) { + firstAppSimStartTime = row.animation.firstSimStartTime; + } + else if (firstAppSimStartTime == 0) { + firstAppSimStartTime = row.animation.resolvedSimStartTime; + } + } + } + } + else { + // Not displayed: advance present/input history but leave last displayed + // screen time and flip delay unchanged. The screen is still showing + // whatever it last showed; a dropped present does not change that. + if (row.isAppFrame) { + lastAppPresent = present; + } + } + + if (present.pclSimStartTime != 0) { + lastSimStartTime = present.pclSimStartTime; + } + else if (present.appSimStartTime != 0) { + lastSimStartTime = present.appSimStartTime; + } + + lastPresent = present; + } + } // namespace pmon::util::metrics diff --git a/IntelPresentMon/CommonUtilities/mc/SwapChainState.h b/IntelPresentMon/CommonUtilities/mc/SwapChainState.h index 74965d06..a00b9f15 100644 --- a/IntelPresentMon/CommonUtilities/mc/SwapChainState.h +++ b/IntelPresentMon/CommonUtilities/mc/SwapChainState.h @@ -67,7 +67,9 @@ struct SwapChainCoreState { // NVIDIA Specific Tracking uint64_t lastDisplayedFlipDelay = 0; - void UpdateAfterPresent(const FrameData& present); + void UpdateAfterPresentV1(const FrameData& present); + void UpdateAfterBootstrapPresentV2(const FrameData& present); + void UpdateAfterReadyDisplayRow(const ReadyDisplayRow& row); }; -} \ No newline at end of file +} diff --git a/IntelPresentMon/CommonUtilities/mc/UnifiedSwapChain.cpp b/IntelPresentMon/CommonUtilities/mc/UnifiedSwapChain.cpp index b54cedb9..d54bb069 100644 --- a/IntelPresentMon/CommonUtilities/mc/UnifiedSwapChain.cpp +++ b/IntelPresentMon/CommonUtilities/mc/UnifiedSwapChain.cpp @@ -1,5 +1,6 @@ -// Copyright (C) 2025 Intel Corporation +// Copyright (C) 2025 Intel Corporation // SPDX-License-Identifier: MIT +#include "MetricsCalculator.h" #include "MetricsTypes.h" #include "UnifiedSwapChain.h" #include @@ -14,6 +15,12 @@ namespace pmon::util::metrics return swapChain.lastPresent.has_value() ? swapChain.lastPresent->presentStartTime : 0; } + uint64_t UnifiedSwapChain::GetLastPresentQpcOr(uint64_t fallbackQpc) const + { + const auto lastPresentQpc = GetLastPresentQpc(); + return lastPresentQpc != 0 ? lastPresentQpc : fallbackQpc; + } + bool UnifiedSwapChain::IsPrunableBefore(uint64_t minTimestampQpc) const { auto const last = GetLastPresentQpc(); @@ -24,7 +31,23 @@ namespace pmon::util::metrics { // Port of OutputThread.cpp::ReportMetrics() "Remove Repeated flips" pre-pass, // but applied to FrameData (so we do not mutate PresentEvent). + // + // Only apply when there is exactly one Repeated entry in the sequence. + // Two or more Repeated entries indicates a frame-generation shape + // (e.g., AMD AFMF: Repeated, Application, Repeated) where every Repeated + // is a distinct generated display instance that must reach the queue. auto& d = present.displayed; + + size_t repeatedCount = 0; + for (size_t i = 0; i < d.Size(); ++i) { + if (d[i].first == FrameType::Repeated) { + ++repeatedCount; + } + } + if (repeatedCount >= 2) { + return; + } + for (size_t i = 0; i + 1 < d.Size(); ) { const auto a = d[i].first; const auto b = d[i + 1].first; @@ -44,70 +67,47 @@ namespace pmon::util::metrics } } - void UnifiedSwapChain::SeedFromFirstPresent(const FrameData& present) + // ProcessPresent is the main entry point for processing a present through the + // metric calculation and publish policy. It returns the ProcessPresentRows that + // should be published. + std::vector UnifiedSwapChain::ProcessPresent( + const QpcConverter& qpc, + FrameData present) { - // Mirror console baseline behavior: - // first present just seeds history (no pending pipeline). - swapChain.UpdateAfterPresent(present); - } - - // UnifiedSwapChain.cpp - std::vector - UnifiedSwapChain::Enqueue(FrameData&& present, MetricsVersion version) - { - SanitizeDisplayedRepeatedPresents(present); - - std::vector out; - - // V1: FIFO (no buffering / no look-ahead). Every present is ready immediately. - if (version == MetricsVersion::V1) { - waitingDisplayed.reset(); - blocked.clear(); - out.push_back(ReadyItem{ std::move(present), nullptr, nullptr }); - return out; - } - - // Seed baseline + // If this is the first present for the swap chain, seed the swap chain state and display queue without + // applying any publish policy since some metrics require swap chain history to compute. if (!swapChain.lastPresent.has_value()) { - SeedFromFirstPresent(present); - return out; + SanitizeDisplayedRepeatedPresents(present); + swapChain.UpdateAfterBootstrapPresentV2(present); + displayQueue.NoteSeedPresent(present); + return {}; } - const bool isDisplayed = - (present.finalState == PresentResult::Presented) && - (!present.displayed.Empty()); - - if (isDisplayed) { - // 1) Finalize previously waiting displayed (if any), pointing at swapchain-owned next displayed. - if (waitingDisplayed.has_value()) { - FrameData prev = std::move(*waitingDisplayed); - waitingDisplayed = std::move(present); - - out.push_back(ReadyItem{ std::move(prev), nullptr, &*waitingDisplayed }); - } - else { - // First displayed: becomes the waitingDisplayed_. - waitingDisplayed = std::move(present); - } - - // 2) Release blocked not-displayed frames (owned, no look-ahead). - while (!blocked.empty()) { - out.push_back(ReadyItem{ std::move(blocked.front()), nullptr, nullptr }); - blocked.pop_front(); - } - - // 3) Current displayed is ready (all-but-last); provide a pointer so NV adjustments persist. - out.push_back(ReadyItem{ FrameData{}, &*waitingDisplayed, nullptr }); - return out; - } + // Enqueue the present and get any ready display rows that can be applied according to the publish policy. + auto ready = EnqueueReadyDisplayRows(qpc, std::move(present)); + return ApplyReadyDisplayRows(qpc, ready); + } - // Not displayed - if (waitingDisplayed.has_value()) { - blocked.push_back(std::move(present)); - return out; // nothing ready yet + std::vector UnifiedSwapChain::ApplyReadyDisplayRows( + const QpcConverter& qpc, + std::vector& ready) + { + std::vector applied; + applied.reserve(ready.size()); + for (auto& row : ready) { + ProcessPresentRow output{}; + output.present = row.present; + output.computed = ComputeMetricsForReadyDisplayRow(qpc, row, swapChain); + applied.push_back(std::move(output)); } + return applied; + } - out.push_back(ReadyItem{ std::move(present), nullptr, nullptr }); - return out; + std::vector UnifiedSwapChain::EnqueueReadyDisplayRows( + const QpcConverter& qpc, + FrameData present) + { + SanitizeDisplayedRepeatedPresents(present); + return displayQueue.Ingest(qpc, std::move(present), animationTracker, swapChain); } } diff --git a/IntelPresentMon/CommonUtilities/mc/UnifiedSwapChain.h b/IntelPresentMon/CommonUtilities/mc/UnifiedSwapChain.h index 46b92a00..c1687c76 100644 --- a/IntelPresentMon/CommonUtilities/mc/UnifiedSwapChain.h +++ b/IntelPresentMon/CommonUtilities/mc/UnifiedSwapChain.h @@ -3,34 +3,47 @@ #pragma once #include -#include #include -#include +#include "../qpc.h" +#include "AnimationErrorTracker.h" +#include "DisplayFrameQueue.h" +#include "MetricsCalculator.h" #include "SwapChainState.h" -#include "MetricsCalculator.h" // ComputeMetricsForPresent() namespace pmon::util::metrics { - struct UnifiedSwapChain + struct ProcessPresentRow { - struct ReadyItem - { - FrameData present; // owned (used when presentPtr==nullptr) - FrameData* presentPtr = nullptr; // points into waitingDisplayed_ (optional) - FrameData* nextDisplayedPtr = nullptr; // points into waitingDisplayed_ (optional) - }; + FrameData present; + ComputedMetrics computed; + }; + struct UnifiedSwapChain + { SwapChainCoreState swapChain; - // Seed without needing a QPC converter (needed for console GetPresentProcessInfo() early-return). - void SeedFromFirstPresent(const FrameData& present); + // Frame-generation pipeline entry point: seed first present, then Ingest, Apply, Publish. + // Console, middleware, and API paths all use this entry point. + std::vector ProcessPresent( + const QpcConverter& qpc, + FrameData present); - std::vector Enqueue(FrameData&& present, MetricsVersion version); + // Ingest only (display queue + animation tracker). Does not apply publish policy. + // Unit tests may call this with ComputeMetricsForReadyDisplayRow to apply without publishing. + std::vector EnqueueReadyDisplayRows( + const QpcConverter& qpc, + FrameData present); uint64_t GetLastPresentQpc() const; + uint64_t GetLastPresentQpcOr(uint64_t fallbackQpc) const; bool IsPrunableBefore(uint64_t minTimestampQpc) const; + // Applied before both the V1 (ComputeMetricsForPresent) and V2 (EnqueueReadyDisplayRows) + // paths so that a single Repeated entry adjacent to an Application is collapsed as in the + // original OutputThread "Remove Repeated flips" pre-pass. + static void SanitizeDisplayedRepeatedPresents(FrameData& present); + // Frame statistics float avgCPUDuration = 0.f; float avgGPUDuration = 0.f; @@ -42,8 +55,11 @@ namespace pmon::util::metrics double accumulatedInput2FrameStartTime = 0.f; private: - static void SanitizeDisplayedRepeatedPresents(FrameData& present); - std::optional waitingDisplayed; - std::deque blocked; + std::vector ApplyReadyDisplayRows( + const QpcConverter& qpc, + std::vector& ready); + + AnimationErrorTracker animationTracker; + DisplayFrameQueue displayQueue; }; } diff --git a/IntelPresentMon/PresentMonAPI2Tests/CsvHelper.h b/IntelPresentMon/PresentMonAPI2Tests/CsvHelper.h index a3576dd1..b3f2cd61 100644 --- a/IntelPresentMon/PresentMonAPI2Tests/CsvHelper.h +++ b/IntelPresentMon/PresentMonAPI2Tests/CsvHelper.h @@ -532,6 +532,18 @@ bool ValidateMetricValue(double expected, double actual) return Validate(expected, actual); } +void ValidateMetricOrThrow(Header columnId, size_t line, double gold, double actual) { + if (!ValidateMetricValue(gold, actual)) { + throw CsvValidationException(columnId, line, gold, actual); + } +} + +void ValidateFrameTypeOrThrow(Header columnId, size_t line, PM_FRAME_TYPE gold, PM_FRAME_TYPE test) { + if (!ValidateFrameType(gold, test)) { + throw CsvValidationException(columnId, line, gold, test); + } +} + std::optional CreateCsvFile(std::string& output_dir, std::string& processName) { // Setup csv file @@ -792,7 +804,6 @@ bool CsvParser::VerifyBlobAgainstCsv(const std::string& processName, const unsig // Go through all of the active headers and validate results for (const auto& pair : activeColHeadersMap_) { - bool columnsMatch = false; switch (pair.second) { case Header_Application: @@ -820,7 +831,7 @@ bool CsvParser::VerifyBlobAgainstCsv(const std::string& processName, const unsig ValidateOrThrow(pair.second, line_, v2MetricRow_.presentMode, presentMode); break; case Header_FrameType: - columnsMatch = ValidateFrameType(v2MetricRow_.frameType, frameType); + ValidateFrameTypeOrThrow(pair.second, line_, v2MetricRow_.frameType, frameType); break; case Header_TimeInSeconds: ValidateOrThrow(pair.second, line_, v2MetricRow_.presentStartQPC, timeQpc); @@ -829,13 +840,13 @@ bool CsvParser::VerifyBlobAgainstCsv(const std::string& processName, const unsig ValidateOrThrow(pair.second, line_, v2MetricRow_.cpuFrameQpc, cpuStartQpc); break; case Header_MsBetweenAppStart: - columnsMatch = ValidateMetricValue(v2MetricRow_.msBetweenAppStart, msBetweenAppStart); + ValidateMetricOrThrow(pair.second, line_, v2MetricRow_.msBetweenAppStart, msBetweenAppStart); break; case Header_MsCPUBusy: - columnsMatch = ValidateMetricValue(v2MetricRow_.msCpuBusy, msCpuBusy); + ValidateMetricOrThrow(pair.second, line_, v2MetricRow_.msCpuBusy, msCpuBusy); break; case Header_MsCPUWait: - columnsMatch = ValidateMetricValue(v2MetricRow_.msCpuWait, msCpuWait); + ValidateMetricOrThrow(pair.second, line_, v2MetricRow_.msCpuWait, msCpuWait); break; case Header_MsGPULatency: ValidateOrThrow(pair.second, line_, v2MetricRow_.msGpuLatency, msGpuLatency); @@ -850,34 +861,34 @@ bool CsvParser::VerifyBlobAgainstCsv(const std::string& processName, const unsig ValidateOrThrow(pair.second, line_, v2MetricRow_.msGpuWait, msGpuWait); break; case Header_MsBetweenSimulationStart: - columnsMatch = ValidateMetricValue(v2MetricRow_.msBetweenSimStart, msBetweenSimStartTime); + ValidateMetricOrThrow(pair.second, line_, v2MetricRow_.msBetweenSimStart, msBetweenSimStartTime); break; case Header_MsUntilDisplayed: - columnsMatch = ValidateMetricValue(v2MetricRow_.msUntilDisplayed, msUntilDisplayed); + ValidateMetricOrThrow(pair.second, line_, v2MetricRow_.msUntilDisplayed, msUntilDisplayed); break; case Header_MsBetweenDisplayChange: - columnsMatch = ValidateMetricValue(v2MetricRow_.msBetweenDisplayChange, msBetweenDisplayChange); + ValidateMetricOrThrow(pair.second, line_, v2MetricRow_.msBetweenDisplayChange, msBetweenDisplayChange); break; case Header_MsPCLatency: - columnsMatch = ValidateMetricValue(v2MetricRow_.msPcLatency, msPcLatency); + ValidateMetricOrThrow(pair.second, line_, v2MetricRow_.msPcLatency, msPcLatency); break; case Header_MsAnimationError: - columnsMatch = ValidateMetricValue(v2MetricRow_.msAnimationError, msAnimationError); + ValidateMetricOrThrow(pair.second, line_, v2MetricRow_.msAnimationError, msAnimationError); break; case Header_AnimationTime: - columnsMatch = ValidateMetricValue(v2MetricRow_.animationTime, animationTime); + ValidateMetricOrThrow(pair.second, line_, v2MetricRow_.animationTime, animationTime); break; case Header_MsFlipDelay: - columnsMatch = ValidateMetricValue(v2MetricRow_.msFlipDelay, msFrameDelay); + ValidateMetricOrThrow(pair.second, line_, v2MetricRow_.msFlipDelay, msFrameDelay); break; case Header_MsClickToPhotonLatency: - columnsMatch = ValidateMetricValue(v2MetricRow_.msClickToPhotonLatency, msClickToPhotonLatency); + ValidateMetricOrThrow(pair.second, line_, v2MetricRow_.msClickToPhotonLatency, msClickToPhotonLatency); break; case Header_MsAllInputToPhotonLatency: - columnsMatch = ValidateMetricValue(v2MetricRow_.msAllInputToPhotonLatency, msAllInputToPhotonLatency); + ValidateMetricOrThrow(pair.second, line_, v2MetricRow_.msAllInputToPhotonLatency, msAllInputToPhotonLatency); break; case Header_MsInstrumentedLatency: - columnsMatch = ValidateMetricValue(v2MetricRow_.msInstrumentedLatency, msInstrumentedLatency); + ValidateMetricOrThrow(pair.second, line_, v2MetricRow_.msInstrumentedLatency, msInstrumentedLatency); break; default: // Unknown header, skip validation diff --git a/IntelPresentMon/PresentMonMiddleware/FrameMetricsSource.cpp b/IntelPresentMon/PresentMonMiddleware/FrameMetricsSource.cpp index fe826187..09c8cb4f 100644 --- a/IntelPresentMon/PresentMonMiddleware/FrameMetricsSource.cpp +++ b/IntelPresentMon/PresentMonMiddleware/FrameMetricsSource.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2025 Intel Corporation +// Copyright (C) 2025 Intel Corporation #include "FrameMetricsSource.h" #include @@ -40,22 +40,8 @@ namespace pmon::mid void SwapChainState::ProcessFrame(const util::metrics::FrameData& frame, util::QpcConverter& qpc) { - auto frameCopy = frame; - auto ready = unified_.Enqueue(std::move(frameCopy), util::metrics::MetricsVersion::V2); - for (auto& item : ready) { - auto& present = item.presentPtr ? *item.presentPtr : item.present; - auto* nextPtr = item.nextDisplayedPtr; - - auto computed = util::metrics::ComputeMetricsForPresent( - qpc, - present, - nextPtr, - unified_.swapChain, - util::metrics::MetricsVersion::V2); - - for (auto& cm : computed) { - PushMetrics_(cm.metrics); - } + for (auto& row : unified_.ProcessPresent(qpc, frame)) { + PushMetrics_(row.computed.metrics); } } diff --git a/IntelPresentMon/UnitTests/MetricsCore.cpp b/IntelPresentMon/UnitTests/MetricsCore.cpp index aa96b534..2b4ac586 100644 --- a/IntelPresentMon/UnitTests/MetricsCore.cpp +++ b/IntelPresentMon/UnitTests/MetricsCore.cpp @@ -269,174 +269,7 @@ namespace MetricsCoreTests }; // ============================================================================ - // SECTION 2: DisplayIndexing Calculator - // ============================================================================ - - TEST_CLASS(DisplayIndexingTests) - { - public: - TEST_METHOD(Calculate_NoDisplayedFrames_ReturnsEmptyRange) - { - FrameData present{}; - // No displayed frames - present.displayed.Clear(); - - auto result = DisplayIndexing::Calculate(present, nullptr); - - Assert::AreEqual(size_t(0), result.startIndex); - Assert::AreEqual(size_t(0), result.endIndex); - Assert::AreEqual(size_t(0), result.appIndex); // No displays → appIndex = 0 - Assert::IsFalse(result.hasNextDisplayed); - } - - TEST_METHOD(Calculate_SingleDisplay_NoNext_Postponed) - { - FrameData present{}; - present.displayed.PushBack({ FrameType::Application, 1000 }); - present.finalState = PresentResult::Presented; - - auto result = DisplayIndexing::Calculate(present, nullptr); - - // Single display with no next = postponed (empty range) - Assert::AreEqual(size_t(0), result.startIndex); - Assert::AreEqual(size_t(0), result.endIndex); // Empty! Postponed - Assert::AreEqual(size_t(0), result.appIndex); // Would be 0 if processed - Assert::IsFalse(result.hasNextDisplayed); - } - - TEST_METHOD(Calculate_MultipleDisplays_NoNext_PostponeLast) - { - FrameData present{}; - present.displayed.PushBack({ FrameType::Application, 1000 }); - present.displayed.PushBack({ FrameType::Repeated, 2000 }); - present.displayed.PushBack({ FrameType::Repeated, 3000 }); - present.finalState = PresentResult::Presented; - - auto result = DisplayIndexing::Calculate(present, nullptr); - - // Process [0..1], postpone [2] - Assert::AreEqual(size_t(0), result.startIndex); - Assert::AreEqual(size_t(2), result.endIndex); // Excludes last! - Assert::AreEqual(size_t(0), result.appIndex); // App frame at index 0 - Assert::IsFalse(result.hasNextDisplayed); - } - - TEST_METHOD(Calculate_MultipleDisplays_WithNext_ProcessPostponed) - { - FrameData present{}; - present.displayed.PushBack({ FrameType::Application, 1000 }); - present.displayed.PushBack({ FrameType::Repeated, 2000 }); - present.displayed.PushBack({ FrameType::Repeated, 3000 }); - present.finalState = PresentResult::Presented; - - FrameData next{}; - next.displayed.PushBack({ FrameType::Application, 4000 }); - - auto result = DisplayIndexing::Calculate(present, &next); - - // Process only postponed last display [2] - Assert::AreEqual(size_t(2), result.startIndex); - Assert::AreEqual(size_t(3), result.endIndex); - Assert::AreEqual(SIZE_MAX, result.appIndex); // No app frame at [2], it's Repeated - Assert::IsTrue(result.hasNextDisplayed); - } - - TEST_METHOD(Calculate_NotDisplayed_ReturnsEmptyRange) - { - FrameData present{}; - present.displayed.PushBack({ FrameType::Application, 1000 }); - present.displayed.PushBack({ FrameType::Repeated, 2000 }); - // Don't set finalState = Presented, so displayed = false - - auto result = DisplayIndexing::Calculate(present, nullptr); - - // Not displayed → empty range - Assert::AreEqual(size_t(0), result.startIndex); - Assert::AreEqual(size_t(0), result.endIndex); - Assert::AreEqual(size_t(0), result.appIndex); // Fallback when displayCount > 0 but not displayed - Assert::IsFalse(result.hasNextDisplayed); - } - - TEST_METHOD(Calculate_FindsAppFrameIndex_Displayed) - { - FrameData present{}; - present.displayed.PushBack({ FrameType::Repeated, 1000 }); - present.displayed.PushBack({ FrameType::Application, 2000 }); - present.displayed.PushBack({ FrameType::Repeated, 3000 }); - present.finalState = PresentResult::Presented; - - auto result = DisplayIndexing::Calculate(present, nullptr); - - // Process [0..1], postpone [2] - Assert::AreEqual(size_t(0), result.startIndex); - Assert::AreEqual(size_t(2), result.endIndex); - Assert::AreEqual(size_t(1), result.appIndex); // App at index 1 - } - - TEST_METHOD(Calculate_FindsAppFrameIndex_NotDisplayed) - { - FrameData present{}; - present.displayed.PushBack({ FrameType::Repeated, 1000 }); - present.displayed.PushBack({ FrameType::Application, 2000 }); - present.displayed.PushBack({ FrameType::Repeated, 3000 }); - // Not displayed - - auto result = DisplayIndexing::Calculate(present, nullptr); - - // Not displayed → empty range - Assert::AreEqual(size_t(0), result.startIndex); - Assert::AreEqual(size_t(0), result.endIndex); - } - - TEST_METHOD(Calculate_AllRepeatedFrames_AppIndexInvalid) - { - FrameData present{}; - present.displayed.PushBack({ FrameType::Repeated, 1000 }); - present.displayed.PushBack({ FrameType::Repeated, 2000 }); - present.displayed.PushBack({ FrameType::Repeated, 3000 }); - present.finalState = PresentResult::Presented; - - auto result = DisplayIndexing::Calculate(present, nullptr); - - // Process [0..1], postpone [2] - Assert::AreEqual(size_t(0), result.startIndex); - Assert::AreEqual(size_t(2), result.endIndex); - Assert::AreEqual(SIZE_MAX, result.appIndex); // No app frame found - } - - TEST_METHOD(Calculate_MultipleAppFrames_FindsFirst) - { - FrameData present{}; - present.displayed.PushBack({ FrameType::Application, 1000 }); - present.displayed.PushBack({ FrameType::Application, 2000 }); - present.displayed.PushBack({ FrameType::Repeated, 3000 }); - present.finalState = PresentResult::Presented; - - auto result = DisplayIndexing::Calculate(present, nullptr); - - // Process [0..1], postpone [2] - Assert::AreEqual(size_t(0), result.startIndex); - Assert::AreEqual(size_t(2), result.endIndex); - Assert::AreEqual(size_t(0), result.appIndex); // First app frame - } - - TEST_METHOD(Calculate_WorksWithFrameData) - { - // Verify template works with FrameData - FrameData present{}; - present.displayed.PushBack({ FrameType::Application, 1000 }); - present.finalState = PresentResult::Presented; - - auto result = DisplayIndexing::Calculate(present, nullptr); - - Assert::AreEqual(size_t(0), result.startIndex); - Assert::AreEqual(size_t(0), result.endIndex); // Postponed [0], nothing processed - Assert::IsTrue(result.appIndex == 0); - } - }; - - // ============================================================================ - // SECTION 3: Helper Functions + // SECTION 2: Helper Functions // ============================================================================ TEST_CLASS(CalculateCPUStartTests) @@ -515,134 +348,34 @@ namespace MetricsCoreTests } }; - TEST_CLASS(CalculateAnimationErrorSimStartTimeTests) - { - public: - TEST_METHOD(UsesCpuStartSource) - { - QpcConverter qpc{ 10000000, 0 }; // 10 MHz for easy math - - SwapChainCoreState swapChain{}; - FrameData lastApp{}; - lastApp.presentStartTime = 1000; - lastApp.timeInPresent = 50; - swapChain.lastAppPresent = lastApp; - - FrameData current{}; - current.appSimStartTime = 5000; // Has appSim, but source is CpuStart - - auto result = CalculateAnimationErrorSimStartTime(swapChain, current, AnimationErrorSource::CpuStart); - - // Should use CPU start calculation: 1000 + 50 = 1050 - Assert::AreEqual(1050ull, result); - } - - TEST_METHOD(UsesAppProviderSource) - { - QpcConverter qpc{ 10000000, 0 }; - - SwapChainCoreState swapChain{}; - FrameData lastApp{}; - lastApp.presentStartTime = 1000; - lastApp.timeInPresent = 50; - swapChain.lastAppPresent = lastApp; - - FrameData current{}; - current.appSimStartTime = 5000; - - auto result = CalculateAnimationErrorSimStartTime(swapChain, current, AnimationErrorSource::AppProvider); - - // Should use appSimStartTime - Assert::AreEqual(5000ull, result); - } - - TEST_METHOD(UsesPCLatencySource) - { - QpcConverter qpc{ 10000000, 0 }; - - SwapChainCoreState swapChain{}; - FrameData lastApp{}; - lastApp.presentStartTime = 1000; - lastApp.timeInPresent = 50; - swapChain.lastAppPresent = lastApp; - - FrameData current{}; - current.pclSimStartTime = 6000; - - auto result = CalculateAnimationErrorSimStartTime(swapChain, current, AnimationErrorSource::PCLatency); - - // Should use pclSimStartTime - Assert::AreEqual(6000ull, result); - } - }; - - TEST_CLASS(CalculateAnimationTimeTests) + TEST_CLASS(AnimationErrorTrackerTests) { public: - TEST_METHOD(ComputesRelativeTime) - { - QpcConverter qpc{ 10000000, 0 }; // 10 MHz QPC frequency - - uint64_t firstSimStart = 1000; - uint64_t currentSimStart = 1500; // 500 ticks later - - auto result = CalculateAnimationTime(qpc, firstSimStart, currentSimStart); - - // 500 ticks at 10 MHz = 0.05 ms - AssertAreEqualWithinTolerance(0.05, result, 0.001); - } - - TEST_METHOD(HandlesZeroFirst) - { - QpcConverter qpc{ 10000000, 0 }; - - uint64_t firstSimStart = 0; // Not initialized yet - uint64_t currentSimStart = 1500; - - auto result = CalculateAnimationTime(qpc, firstSimStart, currentSimStart); - - // When first is 0, should return 0 - AssertAreEqualWithinTolerance(0.0, result, 0.001); - } - - TEST_METHOD(HandlesSameTimestamp) - { - QpcConverter qpc{ 10000000, 0 }; - - uint64_t firstSimStart = 1000; - uint64_t currentSimStart = 1000; // Same as first - - auto result = CalculateAnimationTime(qpc, firstSimStart, currentSimStart); - - // Same timestamp = 0 ms elapsed - AssertAreEqualWithinTolerance(0.0, result, 0.001); - } - - TEST_METHOD(HandlesLargeTimespan) + TEST_METHOD(ResolveInterval_SameSourceZeroDisplayElapsed_PublishesAnimationTimeAndMissingError) { - QpcConverter qpc{ 10000000, 0 }; // 10 MHz + QpcConverter qpc(1000, 0); + AnimationErrorTracker tracker{}; - uint64_t firstSimStart = 1000; - uint64_t currentSimStart = 1000 + (10000000 * 5); // +5 seconds in ticks + AnimationErrorTracker::AppAnchor anchor{}; + anchor.source = AnimationErrorSource::AppProvider; + anchor.simStartTime = 1000; + Assert::IsTrue(tracker.TryStartTimelineAtAnchor(anchor)); - auto result = CalculateAnimationTime(qpc, firstSimStart, currentSimStart); + AnimationErrorTracker::AppAnchor closingAnchor{}; + closingAnchor.source = AnimationErrorSource::AppProvider; + closingAnchor.simStartTime = 1016; - // 5 seconds = 5000 ms - AssertAreEqualWithinTolerance(5000.0, result, 0.1); - } - - TEST_METHOD(HandlesBackwardsTime) - { - QpcConverter qpc{ 10000000, 0 }; - - uint64_t firstSimStart = 2000; - uint64_t currentSimStart = 1000; // Earlier than first (unusual but possible) + ReadyDisplayRow row{}; + row.previousDisplayedScreenTime = 200; + row.screenTime = 200; - auto result = CalculateAnimationTime(qpc, firstSimStart, currentSimStart); + const auto contexts = tracker.ResolveIntervalAndAdvanceAnchor(qpc, closingAnchor, { row }); - // Should handle gracefully - returns negative or 0 depending on implementation - // This tests error handling - Assert::IsTrue(result <= 0.0); + Assert::AreEqual(size_t(1), contexts.size()); + Assert::IsFalse(HasMetricValue(contexts[0].msAnimationError)); + Assert::AreEqual(16.0, contexts[0].msAnimationTime, 0.0001); + Assert::AreEqual(uint64_t(1016), contexts[0].resolvedSimStartTime); + Assert::IsTrue(contexts[0].hasResolvedSimStart); } }; @@ -675,290 +408,6 @@ namespace MetricsCoreTests } -TEST_CLASS(UnifiedSwapChainTests) -{ -public: - TEST_METHOD(Enqueue_V2_SeedsFirstPresent_ReturnsNoReady) - { - UnifiedSwapChain u{}; - - // First present seeds baseline (no pipeline output). - auto seed = MakeFrame(PresentResult::Presented, 1'000'000, 10, 1'000'010, {}); - auto out = u.Enqueue(std::move(seed), MetricsVersion::V2); - - Assert::AreEqual(size_t(0), out.size()); - Assert::IsTrue(u.swapChain.lastPresent.has_value()); - Assert::AreEqual(uint64_t(1'000'000), u.GetLastPresentQpc()); - } - - TEST_METHOD(Enqueue_V2_NotDisplayed_NoWaiting_ReturnsSingleOwnedItem) - { - UnifiedSwapChain u{}; - (void)u.Enqueue(MakeFrame(PresentResult::Presented, 1'000'000, 10, 1'000'010, {}), MetricsVersion::V2); // seed - - auto p = MakeFrame(static_cast(9999), 2'000'000, 10, 2'000'010, {}); // not displayed - auto out = u.Enqueue(std::move(p), MetricsVersion::V2); - - Assert::AreEqual(size_t(1), out.size()); - Assert::IsNull(out[0].presentPtr); - Assert::IsNull(out[0].nextDisplayedPtr); - Assert::AreEqual(uint64_t(2'000'000), out[0].present.presentStartTime); - } - - TEST_METHOD(Enqueue_V2_Displayed_FirstDisplayed_ReturnsCurrentDisplayedPtrItemOnly) - { - UnifiedSwapChain u{}; - (void)u.Enqueue(MakeFrame(PresentResult::Presented, 1'000'000, 10, 1'000'010, {}), MetricsVersion::V2); // seed - - auto displayed = MakeFrame(PresentResult::Presented, 2'000'000, 10, 2'000'010, - { { FrameType::Application, 2'500'000 } }); - - auto out = u.Enqueue(std::move(displayed), MetricsVersion::V2); - - Assert::AreEqual(size_t(1), out.size()); - Assert::IsNotNull(out[0].presentPtr); - Assert::IsNull(out[0].nextDisplayedPtr); - Assert::AreEqual(uint64_t(2'000'000), out[0].presentPtr->presentStartTime); - } - - TEST_METHOD(Enqueue_V2_NotDisplayed_WithWaiting_IsBufferedUntilNextDisplayed) - { - UnifiedSwapChain u{}; - (void)u.Enqueue(MakeFrame(PresentResult::Presented, 1'000'000, 10, 1'000'010, {}), MetricsVersion::V2); // seed - - // First displayed => enters waitingDisplayed, produces current item (but may postpone metrics). - (void)u.Enqueue(MakeFrame(PresentResult::Presented, 2'000'000, 10, 2'000'010, - { { FrameType::Application, 2'500'000 } }), MetricsVersion::V2); - - // Not displayed while waiting => no ready work. - auto out1 = u.Enqueue(MakeFrame(static_cast(9999), 2'200'000, 10, 2'200'010, {}), MetricsVersion::V2); - Assert::AreEqual(size_t(0), out1.size()); - - // Next displayed => releases blocked. - auto out2 = u.Enqueue(MakeFrame(PresentResult::Presented, 3'000'000, 10, 3'000'010, - { { FrameType::Application, 3'500'000 } }), MetricsVersion::V2); - - // finalize previous + blocked + current - Assert::AreEqual(size_t(3), out2.size()); - Assert::AreEqual(uint64_t(2'000'000), out2[0].present.presentStartTime); // finalize previous - Assert::AreEqual(uint64_t(2'200'000), out2[1].present.presentStartTime); // released blocked - Assert::IsNotNull(out2[2].presentPtr); // current displayed - Assert::AreEqual(uint64_t(3'000'000), out2[2].presentPtr->presentStartTime); - } - - TEST_METHOD(Enqueue_V2_Displayed_WithWaiting_OrdersFinalizeThenBlockedThenCurrent) - { - UnifiedSwapChain u{}; - (void)u.Enqueue(MakeFrame(PresentResult::Presented, 1'000'000, 10, 1'000'010, {}), MetricsVersion::V2); // seed - - // Displayed A enters waiting - (void)u.Enqueue(MakeFrame(PresentResult::Presented, 2'000'000, 10, 2'000'010, - { { FrameType::Application, 2'500'000 } }), MetricsVersion::V2); - - // Buffer B and C - (void)u.Enqueue(MakeFrame(static_cast(9999), 2'100'000, 10, 2'100'010, {}), MetricsVersion::V2); - (void)u.Enqueue(MakeFrame(static_cast(9999), 2'200'000, 10, 2'200'010, {}), MetricsVersion::V2); - - // Next displayed D triggers: finalize A, then B,C, then current D - auto out = u.Enqueue(MakeFrame(PresentResult::Presented, 3'000'000, 10, 3'000'010, - { { FrameType::Application, 3'500'000 } }), MetricsVersion::V2); - - Assert::AreEqual(size_t(4), out.size()); - Assert::AreEqual(uint64_t(2'000'000), out[0].present.presentStartTime); - Assert::IsNotNull(out[0].nextDisplayedPtr); - Assert::AreEqual(uint64_t(3'000'000), out[0].nextDisplayedPtr->presentStartTime); - - Assert::AreEqual(uint64_t(2'100'000), out[1].present.presentStartTime); - Assert::AreEqual(uint64_t(2'200'000), out[2].present.presentStartTime); - - Assert::IsNotNull(out[3].presentPtr); - Assert::AreEqual(uint64_t(3'000'000), out[3].presentPtr->presentStartTime); - } - - TEST_METHOD(Enqueue_V2_SanitizeDisplayed_RemovesAppThenRepeated) - { - UnifiedSwapChain u{}; - (void)u.Enqueue(MakeFrame(PresentResult::Presented, 1'000'000, 10, 1'000'010, {}), MetricsVersion::V2); // seed - - auto p = MakeFrame(PresentResult::Presented, 2'000'000, 10, 2'000'010, - { { FrameType::Application, 2'500'000 }, - { FrameType::Repeated, 2'700'000 } }); - - auto out = u.Enqueue(std::move(p), MetricsVersion::V2); - - Assert::AreEqual(size_t(1), out.size()); - Assert::IsNotNull(out[0].presentPtr); - - Assert::AreEqual(size_t(1), out[0].presentPtr->displayed.Size()); - Assert::AreEqual((int)FrameType::Application, (int)out[0].presentPtr->displayed[0].first); - } - - TEST_METHOD(Enqueue_V2_SanitizeDisplayed_RemovesRepeatedThenApp) - { - UnifiedSwapChain u{}; - (void)u.Enqueue(MakeFrame(PresentResult::Presented, 1'000'000, 10, 1'000'010, {}), MetricsVersion::V2); // seed - - auto p = MakeFrame(PresentResult::Presented, 2'000'000, 10, 2'000'010, - { { FrameType::Repeated, 2'400'000 }, - { FrameType::Application, 2'500'000 } }); - - auto out = u.Enqueue(std::move(p), MetricsVersion::V2); - - Assert::AreEqual(size_t(1), out.size()); - Assert::IsNotNull(out[0].presentPtr); - - Assert::AreEqual(size_t(1), out[0].presentPtr->displayed.Size()); - Assert::AreEqual((int)FrameType::Application, (int)out[0].presentPtr->displayed[0].first); - } - - TEST_METHOD(Pipeline_V2_PostponedLastDisplayInstance_EmittedOnFinalize) - { - QpcConverter qpc(10'000'000, 0); - UnifiedSwapChain u{}; - - // Seed history - (void)u.Enqueue(MakeFrame(PresentResult::Presented, 1'000'000, 10, 1'000'010, {}), MetricsVersion::V2); - - // First displayed has two instances: app then repeated. - auto outA = u.Enqueue(MakeFrame(PresentResult::Presented, 2'000'000, 10, 2'000'010, - { { FrameType::Intel_XEFG, 2'500'000 }, - { FrameType::Application, 2'700'000 } }), - MetricsVersion::V2); - - Assert::AreEqual(size_t(1), outA.size()); - Assert::IsNotNull(outA[0].presentPtr); - - // Processing current displayed without next: should produce all-but-last => one result at 2'500'000. - auto resA = ComputeMetricsForPresent(qpc, *outA[0].presentPtr, nullptr, u.swapChain, MetricsVersion::V2); - Assert::AreEqual(size_t(1), resA.size()); - Assert::AreEqual(uint64_t(2'500'000), resA[0].metrics.screenTimeQpc); - - // Next displayed triggers finalize of the previous displayed (postponed last instance at 2'700'000). - auto outB = u.Enqueue(MakeFrame(PresentResult::Presented, 3'000'000, 10, 3'000'010, - { { FrameType::Application, 3'500'000 } }), - MetricsVersion::V2); - - Assert::AreEqual(size_t(2), outB.size()); - Assert::IsNotNull(outB[0].nextDisplayedPtr); - - auto resFinalize = ComputeMetricsForPresent(qpc, outB[0].present, outB[0].nextDisplayedPtr, u.swapChain, MetricsVersion::V2); - Assert::AreEqual(size_t(1), resFinalize.size()); - Assert::AreEqual(uint64_t(2'700'000), resFinalize[0].metrics.screenTimeQpc); - } - - TEST_METHOD(Pipeline_V2_NvCollapsed_AdjustmentPersistsViaNextDisplayedPtr) - { - QpcConverter qpc(10'000'000, 0); - UnifiedSwapChain u{}; - - (void)u.Enqueue(MakeFrame(PresentResult::Presented, 1'000'000, 10, 1'000'010, {}), MetricsVersion::V2); - - // First displayed: collapsed/runt-style (flipDelay set), screenTime is later than next's raw screenTime. - (void)u.Enqueue(MakeFrame(PresentResult::Presented, 4'000'000, 50'000, 4'100'000, - { { FrameType::Application, 5'500'000 } }, - 0, 0, 200'000), - MetricsVersion::V2); - - // Second displayed: raw screenTime earlier -> should be adjusted upward by NV2 when finalizing first. - auto out = u.Enqueue(MakeFrame(PresentResult::Presented, 5'000'000, 40'000, 5'100'000, - { { FrameType::Application, 5'000'000 } }, - 0, 0, 100'000), - MetricsVersion::V2); - - // Expect: finalize previous + current displayed - Assert::AreEqual(size_t(2), out.size()); - Assert::IsNotNull(out[0].nextDisplayedPtr); - Assert::IsNotNull(out[1].presentPtr); - - // Finalize first with look-ahead to second => mutates second via pointer. - (void)ComputeMetricsForPresent(qpc, out[0].present, out[0].nextDisplayedPtr, u.swapChain, MetricsVersion::V2); - - // Mutation must persist on swapchain-owned second frame. - Assert::AreEqual(uint64_t(5'500'000), out[1].presentPtr->displayed[0].second); - Assert::AreEqual(uint64_t(100'000 + (5'500'000 - 5'000'000)), out[1].presentPtr->flipDelay); - } - - TEST_METHOD(Pipeline_V1_NvCollapsed_AdjustsCurrentPresent) - { - QpcConverter qpc(10'000'000, 0); - UnifiedSwapChain u{}; - - // Establish previous displayed state (lastDisplayedScreenTime/flipDelay) via first displayed. - auto out1 = u.Enqueue(MakeFrame(PresentResult::Presented, 4'000'000, 50'000, 4'100'000, - { { FrameType::Application, 5'500'000 } }, - 0, 0, 200'000), - MetricsVersion::V1); - - Assert::AreEqual(size_t(1), out1.size()); - - (void)ComputeMetricsForPresent(qpc, out1[0].present, nullptr, u.swapChain, MetricsVersion::V1); - - // Second present has earlier raw screenTime; NV1 should adjust *current* present to lastDisplayedScreenTime. - auto out2 = u.Enqueue(MakeFrame(PresentResult::Presented, 5'000'000, 40'000, 5'100'000, - { { FrameType::Application, 5'000'000 } }, - 0, 0, 100'000), - MetricsVersion::V1); - - Assert::AreEqual(size_t(1), out2.size()); - - (void)ComputeMetricsForPresent(qpc, out2[0].present, nullptr, u.swapChain, MetricsVersion::V1); - - Assert::AreEqual(uint64_t(5'500'000), out2[0].present.displayed[0].second); - Assert::AreEqual(uint64_t(100'000 + (5'500'000 - 5'000'000)), out2[0].present.flipDelay); - } - - TEST_METHOD(Pipeline_V1_NoNvCollapse_DoesNotModifyCurrentPresent) - { - QpcConverter qpc(10'000'000, 0); - UnifiedSwapChain u{}; - - // Prior displayed state via first displayed. - auto out1 = u.Enqueue(MakeFrame(PresentResult::Presented, 4'000'000, 50'000, 4'100'000, - { { FrameType::Application, 5'000'000 } }, - 0, 0, 200'000), - MetricsVersion::V1); - (void)ComputeMetricsForPresent(qpc, out1[0].present, nullptr, u.swapChain, MetricsVersion::V1); - - // Current has later screenTime => no collapse. - auto out2 = u.Enqueue(MakeFrame(PresentResult::Presented, 5'000'000, 40'000, 5'100'000, - { { FrameType::Application, 6'000'000 } }, - 0, 0, 100'000), - MetricsVersion::V1); - - // Preserve originals for comparison. - const uint64_t origScreen = out2[0].present.displayed[0].second; - const uint64_t origFlipDelay = out2[0].present.flipDelay; - - (void)ComputeMetricsForPresent(qpc, out2[0].present, nullptr, u.swapChain, MetricsVersion::V1); - - Assert::AreEqual(origScreen, out2[0].present.displayed[0].second); - Assert::AreEqual(origFlipDelay, out2[0].present.flipDelay); - } - - TEST_METHOD(Enqueue_V1_ClearsV2Buffers_AndIsAlwaysReady) - { - UnifiedSwapChain u{}; - (void)u.Enqueue(MakeFrame(PresentResult::Presented, 1'000'000, 10, 1'000'010, {}), MetricsVersion::V2); // seed - - // Create V2 waitingDisplayed + blocked. - (void)u.Enqueue(MakeFrame(PresentResult::Presented, 2'000'000, 10, 2'000'010, - { { FrameType::Application, 2'500'000 } }), MetricsVersion::V2); - (void)u.Enqueue(MakeFrame(static_cast(9999), 2'200'000, 10, 2'200'010, {}), MetricsVersion::V2); - - // V1 enqueue must clear V2 buffers and return one ready item. - auto outV1 = u.Enqueue(MakeFrame(static_cast(9999), 2'300'000, 10, 2'300'010, {}), MetricsVersion::V1); - Assert::AreEqual(size_t(1), outV1.size()); - - // Next V2 displayed should behave as "no waiting/no blocked": returns only current displayed item. - auto outV2 = u.Enqueue(MakeFrame(PresentResult::Presented, 3'000'000, 10, 3'000'010, - { { FrameType::Application, 3'500'000 } }), MetricsVersion::V2); - - Assert::AreEqual(size_t(1), outV2.size()); - Assert::IsNotNull(outV2[0].presentPtr); - Assert::IsNull(outV2[0].nextDisplayedPtr); - } -}; - TEST_CLASS(ComputeMetricsForPresentTests) { @@ -969,7 +418,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) SwapChainCoreState chain{}; auto frame = MakeFrame(PresentResult::Presented, 10'000, 500, 10'500, {}); // Presented but no displays => not displayed path - auto metrics = ComputeMetricsForPresent(qpc, frame, nullptr, chain); + auto metrics = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), metrics.size(), L"Should produce exactly one metrics entry."); Assert::IsTrue(chain.lastPresent.has_value(), L"Chain should be updated for not displayed."); @@ -987,14 +436,14 @@ TEST_CLASS(ComputeMetricsForPresentTests) auto frame = MakeFrame(static_cast(9999), 1'000, 100, 1'200, { { FrameType::Application, 2'000 } }); - auto metrics = ComputeMetricsForPresent(qpc, frame, nullptr, chain); + auto metrics = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), metrics.size()); Assert::IsTrue(chain.lastPresent.has_value()); Assert::IsTrue(chain.lastAppPresent.has_value()); Assert::AreEqual(uint64_t(0), chain.lastDisplayedScreenTime, L"Not displayed path should not update displayed screen time."); } - TEST_METHOD(ComputeMetricsForPresent_DisplayedNoNext_SingleDisplay_PostponedChainNotUpdated) + TEST_METHOD(ComputeMetricsForPresent_DisplayedSingleDisplay_ProducesSingleMetricsAndUpdatesChain) { QpcConverter qpc(10'000'000, 0); SwapChainCoreState chain{}; @@ -1002,87 +451,43 @@ TEST_CLASS(ComputeMetricsForPresentTests) auto frame = MakeFrame(PresentResult::Presented, 5'000, 200, 5'500, { { FrameType::Application, 6'000 } }); - auto metrics = ComputeMetricsForPresent(qpc, frame, nullptr, chain); - - Assert::AreEqual(size_t(0), metrics.size(), L"Single display is postponed => zero metrics now."); - Assert::IsFalse(chain.lastPresent.has_value(), L"Chain should NOT be updated yet."); - Assert::IsFalse(chain.lastAppPresent.has_value()); - } - - TEST_METHOD(ComputeMetricsForPresent_DisplayedNoNext_MultipleDisplays_ProcessesAllButLast) - { - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState chain{}; - - auto frame = MakeFrame(PresentResult::Presented, 10'000, 300, 10'800, - { - { FrameType::Application, 11'000 }, - { FrameType::Repeated, 11'500 }, - { FrameType::Repeated, 12'000 } // postponed - }); - - auto metrics = ComputeMetricsForPresent(qpc, frame, nullptr, chain); - - Assert::AreEqual(size_t(2), metrics.size(), L"Should process all but last display."); - Assert::IsFalse(chain.lastPresent.has_value()); - Assert::IsFalse(chain.lastAppPresent.has_value()); - } - - TEST_METHOD(ComputeMetricsForPresent_DisplayedWithNext_ProcessesPostponedLastAndUpdatesChain) - { - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState chain{}; - - auto frame = MakeFrame(PresentResult::Presented, 10'000, 300, 10'800, - { - { FrameType::Application, 11'000 }, - { FrameType::Repeated, 11'500 }, - { FrameType::Repeated, 12'000 } - }, - 0, 0, 777); - - auto nextDisplayed = MakeFrame(PresentResult::Presented, 13'000, 250, 13'600, - { { FrameType::Application, 14'000 } }); - - // First call without nextDisplayed: postpone last - auto preMetrics = ComputeMetricsForPresent(qpc, frame, nullptr, chain); - Assert::AreEqual(size_t(2), preMetrics.size()); - Assert::IsFalse(chain.lastPresent.has_value()); + auto metrics = ComputeMetricsForPresent(qpc, frame, chain); - // Second call with nextDisplayed: process postponed last + update chain - auto postMetrics = ComputeMetricsForPresent(qpc, frame, &nextDisplayed, chain); - Assert::AreEqual(size_t(1), postMetrics.size(), L"Should process only the postponed last display this time."); + Assert::AreEqual(size_t(1), metrics.size(), L"Legacy present metrics emit one row immediately."); Assert::IsTrue(chain.lastPresent.has_value()); - Assert::AreEqual(uint64_t(12'000), chain.lastDisplayedScreenTime); - Assert::AreEqual(uint64_t(777), chain.lastDisplayedFlipDelay); + Assert::IsTrue(chain.lastAppPresent.has_value()); + Assert::AreEqual(uint64_t(6'000), metrics[0].metrics.screenTimeQpc); } - TEST_METHOD(ComputeMetricsForPresent_DisplayedWithNext_LastDisplayIsRepeated_DoesNotUpdateLastAppPresent) + TEST_METHOD(ProcessPresent_MultiDisplayAppAnchor_UsesAppDisplayRowAndUpdatesChain) { QpcConverter qpc(10'000'000, 0); - SwapChainCoreState chain{}; + UnifiedSwapChain swapChain{}; - // Previous app present for fallback usage. - FrameData prevApp = MakeFrame(PresentResult::Presented, 2'000, 100, 2'300, - { { FrameType::Application, 2'800 } }); - chain.lastAppPresent = prevApp; + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; - auto frame = MakeFrame(PresentResult::Presented, 4'000, 120, 4'300, - { - { FrameType::Application, 4'500 }, - { FrameType::Repeated, 4'900 } // last (Repeated) - }); + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); - auto nextDisplayed = MakeFrame(PresentResult::Presented, 5'000, 110, 5'250, - { { FrameType::Application, 5'600 } }); + FrameData frame{}; + frame.presentStartTime = 10'000; + frame.timeInPresent = 300; + frame.readyTime = 10'800; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 11'000 }); + frame.displayed.PushBack({ FrameType::Repeated, 11'500 }); + frame.displayed.PushBack({ FrameType::Repeated, 12'000 }); - auto metrics = ComputeMetricsForPresent(qpc, frame, &nextDisplayed, chain); - Assert::AreEqual(size_t(1), metrics.size()); + auto rows = swapChain.ProcessPresent(qpc, std::move(frame)); - Assert::IsTrue(chain.lastPresent.has_value()); - // lastAppPresent should remain previous since last display was Repeated - Assert::IsTrue(chain.lastAppPresent.has_value()); - Assert::AreEqual(uint64_t(2'000), chain.lastAppPresent->presentStartTime); + Assert::AreEqual(size_t(1), rows.size(), L"App anchor publishes when in-present lookahead is known."); + Assert::IsTrue(swapChain.swapChain.lastPresent.has_value()); + Assert::IsTrue(swapChain.swapChain.lastAppPresent.has_value()); + Assert::AreEqual(uint64_t(11'000), rows[0].computed.metrics.screenTimeQpc); + Assert::AreEqual(uint64_t(11'000), swapChain.swapChain.lastDisplayedScreenTime); } }; @@ -1098,7 +503,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) { { FrameType::Application, 1'500 } }, 10'000 /* appSimStartTime */); - chain.UpdateAfterPresent(frame); + chain.UpdateAfterPresentV1(frame); Assert::AreEqual(uint64_t(10'000), chain.lastDisplayedSimStartTime); Assert::AreEqual(uint64_t(10'000), chain.firstAppSimStartTime); @@ -1117,7 +522,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) { { FrameType::Application, 1'650 } }, 10'000 /* appSimStartTime */, 12'000 /* pclSimStart */); - chain.UpdateAfterPresent(frame); + chain.UpdateAfterPresentV1(frame); Assert::IsTrue(chain.animationErrorSource == AnimationErrorSource::AppProvider); Assert::AreEqual(uint64_t(40'000), chain.firstAppSimStartTime); @@ -1138,7 +543,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) { { FrameType::Application, 9'950 } }, 0 /* appSimStartTime */, 0 /* pclSimStart */); - chain.UpdateAfterPresent(frame); + chain.UpdateAfterPresentV1(frame); Assert::IsTrue(chain.animationErrorSource == AnimationErrorSource::AppProvider); Assert::AreEqual(uint64_t(40'000), chain.firstAppSimStartTime); @@ -1158,7 +563,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) { { FrameType::Application, 9'960 } }, 0 /* appSimStartTime */, 52'000 /* pclSimStart */); - chain.UpdateAfterPresent(frame); + chain.UpdateAfterPresentV1(frame); Assert::IsTrue(chain.animationErrorSource == AnimationErrorSource::AppProvider); Assert::AreEqual(uint64_t(40'000), chain.firstAppSimStartTime); @@ -1175,7 +580,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) { { FrameType::Application, 2'700 } }, 0 /* appSimStartTime */, 12'345 /* pclSimStart */); - chain.UpdateAfterPresent(frame); + chain.UpdateAfterPresentV1(frame); Assert::AreEqual(uint64_t(12'345), chain.lastDisplayedSimStartTime); Assert::AreEqual(uint64_t(12'345), chain.firstAppSimStartTime); @@ -1198,7 +603,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) { { FrameType::Application, 9'950 } }, 0 /* appSimStartTime */, 0 /* pclSimStart */); - chain.UpdateAfterPresent(frame); + chain.UpdateAfterPresentV1(frame); Assert::IsTrue(chain.animationErrorSource == AnimationErrorSource::PCLatency); Assert::AreEqual(uint64_t(30'000), chain.firstAppSimStartTime); @@ -1219,7 +624,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) { { FrameType::Application, 6'700 } }, 0, 0); - chain.UpdateAfterPresent(frame); + chain.UpdateAfterPresentV1(frame); // No appSimStartTime or pclSimStartTime, fallback uses previous app present CPU end: // 5'000 + 80 = 5'080 @@ -1237,7 +642,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) { { FrameType::Application, 7'900 } }, 20'000 /* appSimStartTime */); - chain.UpdateAfterPresent(frame); + chain.UpdateAfterPresentV1(frame); Assert::IsTrue(chain.animationErrorSource == AnimationErrorSource::AppProvider); Assert::AreEqual(uint64_t(20'000), chain.lastDisplayedSimStartTime); @@ -1255,7 +660,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) { { FrameType::Application, 8'300 } }, 20'000 /* appSimStartTime */, 30'000 /* pclSimStart */); - chain.UpdateAfterPresent(frame); + chain.UpdateAfterPresentV1(frame); Assert::IsTrue(chain.animationErrorSource == AnimationErrorSource::AppProvider); Assert::AreEqual(uint64_t(20'000), chain.lastDisplayedSimStartTime); @@ -1271,7 +676,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) { { FrameType::Application, 8'950 } }, 0 /* appSim */, 30'000 /* pclSim */); - chain.UpdateAfterPresent(frame); + chain.UpdateAfterPresentV1(frame); Assert::IsTrue(chain.animationErrorSource == AnimationErrorSource::PCLatency); Assert::AreEqual(uint64_t(30'000), chain.lastDisplayedSimStartTime); @@ -1290,7 +695,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) { { FrameType::Application, 9'950 } }, 40'000 /* appSimStartTime */, 0 /* pclSimStart */); - chain.UpdateAfterPresent(frame); + chain.UpdateAfterPresentV1(frame); Assert::IsTrue(chain.animationErrorSource == AnimationErrorSource::AppProvider); Assert::AreEqual(uint64_t(40'000), chain.firstAppSimStartTime); @@ -1310,7 +715,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) { { FrameType::Application, 10'950 } }, 40'000 /* appSimStartTime */, 50'000 /* pclSimStart */); - chain.UpdateAfterPresent(frame); + chain.UpdateAfterPresentV1(frame); Assert::IsTrue(chain.animationErrorSource == AnimationErrorSource::AppProvider); Assert::AreEqual(uint64_t(40'000), chain.firstAppSimStartTime); @@ -1332,7 +737,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) }, 0, 0, 1234 /* flipDelay */); - chain.UpdateAfterPresent(frame); + chain.UpdateAfterPresentV1(frame); Assert::AreEqual(uint64_t(11'000), chain.lastDisplayedScreenTime); Assert::AreEqual(uint64_t(1234), chain.lastDisplayedFlipDelay); @@ -1344,7 +749,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) auto frame = MakeFrame(PresentResult::Presented, 12'000, 40, 12'300, {}, 0, 0, 9999); - chain.UpdateAfterPresent(frame); + chain.UpdateAfterPresentV1(frame); Assert::AreEqual(uint64_t(0), chain.lastDisplayedScreenTime); Assert::AreEqual(uint64_t(0), chain.lastDisplayedFlipDelay); @@ -1352,552 +757,259 @@ TEST_CLASS(ComputeMetricsForPresentTests) TEST_METHOD(UpdateAfterPresent_NotPresented_DoesNotChangeLastDisplayedScreenTime) { - SwapChainCoreState chain{}; - // Seed previous displayed state - FrameData prev = MakeFrame(PresentResult::Presented, 1'000, 30, 1'200, - { { FrameType::Application, 1'500 } }); - chain.UpdateAfterPresent(prev); - Assert::AreEqual(uint64_t(1'500), chain.lastDisplayedScreenTime); + QpcConverter qpc(10'000'000, 0); + UnifiedSwapChain swapChain{}; + + FrameData seed{}; + seed.presentStartTime = 1'000; + seed.timeInPresent = 30; + seed.readyTime = 1'200; + seed.finalState = PresentResult::Presented; + seed.displayed.PushBack({ FrameType::Application, 1'500 }); - // Not presented frame with displays (ignored for displayed tracking) - auto frame = MakeFrame(static_cast(7777), 2'000, 25, 2'150, - { { FrameType::Application, 2'600 } }); + (void)swapChain.ProcessPresent(qpc, std::move(seed)); + Assert::AreEqual(uint64_t(1'500), swapChain.swapChain.lastDisplayedScreenTime); - chain.UpdateAfterPresent(frame); + FrameData notPresented{}; + notPresented.presentStartTime = 2'000; + notPresented.timeInPresent = 25; + notPresented.readyTime = 2'150; + notPresented.finalState = static_cast(7777); + notPresented.displayed.PushBack({ FrameType::Application, 2'600 }); - Assert::AreEqual(uint64_t(1'500), chain.lastDisplayedScreenTime, L"Should remain unchanged."); + (void)swapChain.ProcessPresent(qpc, std::move(notPresented)); + + Assert::AreEqual(uint64_t(1'500), swapChain.swapChain.lastDisplayedScreenTime, + L"Should remain unchanged."); } }; - TEST_CLASS(FrameTypeXefgAfmfIndexingTests) + + TEST_CLASS(UpdateAfterBootstrapPresentV2Tests) { public: - TEST_METHOD(DisplayIndexing_IntelXefg_Multi_NoNext_AppIndexIsLast) + TEST_METHOD(UpdateAfterBootstrapPresentV2_AppInMiddle_UsesAppDisplayForAppState) { - // 3x Intel_XEFG then a single Application - FrameData present = MakeFrame( - PresentResult::Presented, - 10'000, 500, 20'000, - { - { FrameType::Intel_XEFG, 11'000 }, - { FrameType::Intel_XEFG, 11'500 }, - { FrameType::Intel_XEFG, 12'000 }, - { FrameType::Application, 12'500 }, - }); + QpcConverter qpc(10'000'000, 0); + UnifiedSwapChain swapChain{}; - auto idx = DisplayIndexing::Calculate(present, nullptr); + FrameData bootstrapPresent{}; + bootstrapPresent.presentStartTime = 10'000; + bootstrapPresent.timeInPresent = 50; + bootstrapPresent.readyTime = 10'300; + bootstrapPresent.finalState = PresentResult::Presented; + bootstrapPresent.appSimStartTime = 40'000; + bootstrapPresent.flipDelay = 1234; + bootstrapPresent.displayed.PushBack({ FrameType::Repeated, 10'700 }); + bootstrapPresent.displayed.PushBack({ FrameType::Application, 10'800 }); + bootstrapPresent.displayed.PushBack({ FrameType::Repeated, 11'000 }); - // No nextDisplayed: process [0..N-2] => [0..3) - Assert::AreEqual(size_t(0), idx.startIndex); - Assert::AreEqual(size_t(3), idx.endIndex); - // App frame is at index 3 (outside processing range, postponed) - Assert::AreEqual(size_t(3), idx.appIndex); - Assert::IsFalse(idx.hasNextDisplayed); - } + auto rows = swapChain.ProcessPresent(qpc, std::move(bootstrapPresent)); + Assert::AreEqual(size_t(0), rows.size(), L"Bootstrap present seeds state only."); - TEST_METHOD(DisplayIndexing_AmdAfmf_Multi_WithNext_AppIndexProcessed) - { - // 3x AMD_AFMF then a single Application - FrameData present = MakeFrame( - PresentResult::Presented, - 20'000, 600, 30'000, - { - { FrameType::AMD_AFMF, 21'000 }, - { FrameType::AMD_AFMF, 21'500 }, - { FrameType::AMD_AFMF, 22'000 }, - { FrameType::Application, 22'500 }, - }); - - FrameData nextDisplayed = MakeFrame( - PresentResult::Presented, - 23'000, 400, 30'500, - { { FrameType::Application, 24'000 } }); - - auto idx = DisplayIndexing::Calculate(present, &nextDisplayed); - - // With nextDisplayed: process postponed last only => [N-1, N) => [3, 4) - Assert::AreEqual(size_t(3), idx.startIndex); - Assert::AreEqual(size_t(4), idx.endIndex); - Assert::AreEqual(size_t(3), idx.appIndex); - Assert::IsTrue(idx.hasNextDisplayed); + const auto& chain = swapChain.swapChain; + Assert::AreEqual(uint64_t(11'000), chain.lastDisplayedScreenTime); + Assert::AreEqual(uint64_t(1234), chain.lastDisplayedFlipDelay); + Assert::AreEqual(uint64_t(10'800), chain.lastDisplayedAppScreenTime); + Assert::AreEqual(uint64_t(40'000), chain.lastDisplayedSimStartTime); + Assert::AreEqual(uint64_t(40'000), chain.firstAppSimStartTime); + Assert::IsTrue(chain.animationErrorSource == AnimationErrorSource::AppProvider); + Assert::IsTrue(chain.lastAppPresent.has_value()); + Assert::IsTrue(chain.lastPresent.has_value()); } }; - TEST_CLASS(FrameTypeXefgAfmfMetricsTests) + TEST_CLASS(ProcessPresentXefgAfmfReleaseTests) { public: - TEST_METHOD(ComputeMetricsForPresent_IntelXefg_NoNext_AppNotProcessed_ChainNotUpdated) - { - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState chain{}; - - // 3x Intel_XEFG then 1 Application; no nextDisplayed - FrameData present = MakeFrame( - PresentResult::Presented, - 30'000, 700, 40'000, - { - { FrameType::Intel_XEFG, 31'000 }, - { FrameType::Intel_XEFG, 31'500 }, - { FrameType::Intel_XEFG, 32'000 }, - { FrameType::Application, 32'500 }, - }); - - auto metrics = ComputeMetricsForPresent(qpc, present, nullptr, chain); - - // Should process all but last => 3 metrics - Assert::AreEqual(size_t(3), metrics.size()); - // Chain update postponed until nextDisplayed - Assert::IsFalse(chain.lastPresent.has_value()); - Assert::IsFalse(chain.lastAppPresent.has_value()); - Assert::AreEqual(uint64_t(0), chain.lastDisplayedScreenTime); - Assert::AreEqual(uint64_t(0), chain.lastDisplayedFlipDelay); - } - - TEST_METHOD(ComputeMetricsForPresent_IntelXefg_Discarded_NoNext_ChainNotUpdated) - { - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState chain{}; - - // 3x Intel_XEFG then 1 Application; no nextDisplayed - FrameData present = MakeFrame( - PresentResult::Discarded, - 30'000, 700, 40'000, - { - { FrameType::Intel_XEFG, 0 }, - }); - - auto metrics = ComputeMetricsForPresent(qpc, present, nullptr, chain); - - // Should process 1 - Assert::AreEqual(size_t(1), metrics.size()); - // Chain update postponed until nextDisplayed - const auto& m = metrics[0].metrics; - Assert::IsTrue(FrameType::Intel_XEFG == m.frameType, L"FrameType should be Intel_XEFG"); - } - TEST_METHOD(ComputeMetricsForPresent_AmdAfmf_WithNext_AppProcessedAndUpdatesChain) - { - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState chain{}; - - // 3x AMD_AFMF then 1 Application; with nextDisplayed provided - FrameData present = MakeFrame( - PresentResult::Presented, - 40'000, 650, 50'000, - { - { FrameType::AMD_AFMF, 41'000 }, - { FrameType::AMD_AFMF, 41'400 }, - { FrameType::AMD_AFMF, 41'800 }, - { FrameType::Application, 42'200 }, - }, - 39'500, /* appSimStartTime*/ - 0, /* pclSimStartTime*/ - 999 /* flipDelay*/); - - FrameData nextDisplayed = MakeFrame( - PresentResult::Presented, - 43'000, 500, 50'500, - { { FrameType::Application, 44'000 } }); - - auto metrics = ComputeMetricsForPresent(qpc, present, &nextDisplayed, chain); - - // Should process only postponed last => 1 metrics - Assert::AreEqual(size_t(1), metrics.size()); - - // UpdateAfterPresent has run - Assert::IsTrue(chain.lastPresent.has_value()); - Assert::IsTrue(chain.lastAppPresent.has_value(), L"Last displayed is Application; lastAppPresent should be updated."); - Assert::AreEqual(uint64_t(42'200), chain.lastDisplayedScreenTime); - Assert::AreEqual(uint64_t(999), chain.lastDisplayedFlipDelay); - } - - TEST_METHOD(ComputeMetricsForPresent_IntelXefg_MultiPresentSequence_VaryingDisplayedVectorSizes) - { - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState chain{}; - chain.lastDisplayedScreenTime = 5; - - auto assertRow = [&](const auto& result, FrameType expectedFrameType, uint64_t expectedScreenTime, uint64_t expectedBetweenStart, uint64_t expectedDisplayedEnd, uint64_t expectedPresentStart, uint64_t expectedCpuStart, uint64_t expectedReadyTime) - { - const auto& metrics = result.metrics; - Assert::IsTrue(metrics.frameType == expectedFrameType); - Assert::AreEqual(expectedScreenTime, metrics.screenTimeQpc); - - double expectedBetween = qpc.DeltaUnsignedMilliSeconds(expectedBetweenStart, expectedScreenTime); - Assert::AreEqual(expectedBetween, metrics.msBetweenDisplayChange, 0.0001); - - double expectedDisplayedTime = qpc.DeltaUnsignedMilliSeconds(expectedScreenTime, expectedDisplayedEnd); - Assert::AreEqual(expectedDisplayedTime, metrics.msDisplayedTime, 0.0001); - - double expectedUntilDisplayed = qpc.DeltaUnsignedMilliSeconds(expectedPresentStart, expectedScreenTime); - Assert::AreEqual(expectedUntilDisplayed, metrics.msUntilDisplayed, 0.0001); - - double expectedDisplayLatency = qpc.DeltaUnsignedMilliSeconds(expectedCpuStart, expectedScreenTime); - Assert::AreEqual(expectedDisplayLatency, metrics.msDisplayLatency, 0.0001); - - double expectedReadyTimeToDisplayLatency = qpc.DeltaUnsignedMilliSeconds(expectedReadyTime, expectedScreenTime); - Assert::AreEqual(expectedReadyTimeToDisplayLatency, metrics.msReadyTimeToDisplayLatency, 0.0001); - - Assert::IsTrue(IsMissingFrameMetricValue(metrics.msFlipDelay), - L"msFlipDelay should be stored as missing (NaN)"); - }; - - FrameData frameA = MakeFrame( - PresentResult::Presented, - 1, - 1, - 1, - { - { FrameType::Intel_XEFG, 10 }, - { FrameType::Intel_XEFG, 20 }, - { FrameType::Intel_XEFG, 30 }, - { FrameType::Application, 40 }, - }); - - FrameData frameB = MakeFrame( - PresentResult::Presented, - 45, - 1, - 45, - { - { FrameType::Application, 55 }, - }); - - FrameData frameC = MakeFrame( - PresentResult::Presented, - 60, - 1, - 60, - { - { FrameType::Intel_XEFG, 70 }, - { FrameType::Application, 90 }, - }); - - FrameData frameD = MakeFrame( - PresentResult::Presented, - 95, - 1, - 95, - { - { FrameType::Intel_XEFG, 110 }, - { FrameType::Intel_XEFG, 130 }, - { FrameType::Application, 160 }, - }); - - FrameData frameE = MakeFrame( - PresentResult::Presented, - 170, - 1, - 170, - { - { FrameType::Application, 180 }, - }); - - auto resultsA0 = ComputeMetricsForPresent(qpc, frameA, nullptr, chain); - Assert::AreEqual(size_t(3), resultsA0.size(), L"Frame A should emit only generated rows until a later displayed present finalizes the trailing Application row."); - assertRow(resultsA0[0], FrameType::Intel_XEFG, 10, 5, 20, frameA.presentStartTime, 0, frameA.readyTime); - assertRow(resultsA0[1], FrameType::Intel_XEFG, 20, 10, 30, frameA.presentStartTime, 0, frameA.readyTime); - assertRow(resultsA0[2], FrameType::Intel_XEFG, 30, 20, 40, frameA.presentStartTime, 0, frameA.readyTime); - - auto resultsA1 = ComputeMetricsForPresent(qpc, frameA, &frameB, chain); - Assert::AreEqual(size_t(1), resultsA1.size(), L"Frame A's trailing Application row should emit only when Frame B is processed as the next displayed present."); - assertRow(resultsA1[0], FrameType::Application, 40, 30, 55, frameA.presentStartTime, 0, frameA.readyTime); - - auto resultsB0 = ComputeMetricsForPresent(qpc, frameB, nullptr, chain); - Assert::AreEqual(size_t(0), resultsB0.size(), L"A single Application display should stay postponed until a later displayed present is processed."); - - auto resultsB1 = ComputeMetricsForPresent(qpc, frameB, &frameC, chain); - Assert::AreEqual(size_t(1), resultsB1.size()); - assertRow(resultsB1[0], FrameType::Application, 55, 40, 70, frameB.presentStartTime, frameA.presentStartTime + frameA.timeInPresent, frameB.readyTime); - - auto resultsC0 = ComputeMetricsForPresent(qpc, frameC, nullptr, chain); - Assert::AreEqual(size_t(1), resultsC0.size(), L"Frame C should emit only its generated row before the trailing Application row is finalized."); - assertRow(resultsC0[0], FrameType::Intel_XEFG, 70, 55, 90, frameC.presentStartTime, frameB.presentStartTime + frameB.timeInPresent, frameC.readyTime); - - auto resultsC1 = ComputeMetricsForPresent(qpc, frameC, &frameD, chain); - Assert::AreEqual(size_t(1), resultsC1.size()); - assertRow(resultsC1[0], FrameType::Application, 90, 70, 110, frameC.presentStartTime, frameB.presentStartTime + frameB.timeInPresent, frameC.readyTime); - - auto resultsD0 = ComputeMetricsForPresent(qpc, frameD, nullptr, chain); - Assert::AreEqual(size_t(2), resultsD0.size(), L"Frame D should emit its generated rows and postpone only the trailing Application row."); - assertRow(resultsD0[0], FrameType::Intel_XEFG, 110, 90, 130, frameD.presentStartTime, frameC.presentStartTime + frameC.timeInPresent, frameD.readyTime); - assertRow(resultsD0[1], FrameType::Intel_XEFG, 130, 110, 160, frameD.presentStartTime, frameC.presentStartTime + frameC.timeInPresent, frameD.readyTime); - - auto resultsD1 = ComputeMetricsForPresent(qpc, frameD, &frameE, chain); - Assert::AreEqual(size_t(1), resultsD1.size(), L"Frame D's trailing Application row should emit only when a later displayed present is provided."); - assertRow(resultsD1[0], FrameType::Application, 160, 130, 180, frameD.presentStartTime, frameC.presentStartTime + frameC.timeInPresent, frameD.readyTime); - } - - TEST_METHOD(ComputeMetricsForPresent_IntelXefg_EqualScreenTimeGeneratedFrameSequence) + TEST_METHOD(IntelXefg_MultiEntryPresent_GeneratedRowsPublishImmediately_ClosingAppRowHeldWithoutLookahead) { + // AppA seed, then one present gen+gen+gen+AppB with no further display: timeline + // origin AppA publishes when the first gen supplies lookahead. AppB closes the + // interval, but Gen1..Gen3 already know their own nextScreenTime from sibling + // entries in the same present, so they publish immediately with resolved + // animation metrics. AppB is the last entry in its present, so it has no + // lookahead yet and stays held. QpcConverter qpc(10'000'000, 0); - SwapChainCoreState chain{}; - chain.lastDisplayedScreenTime = 200; - - FrameData priorApp{}; - priorApp.presentStartTime = 190; - priorApp.timeInPresent = 10; - chain.lastAppPresent = priorApp; - - FrameData frameX = MakeFrame( - PresentResult::Presented, - 205, - 15, - 208, - { - { FrameType::Intel_XEFG, 210 }, - { FrameType::Intel_XEFG, 210 }, - { FrameType::Intel_XEFG, 210 }, - { FrameType::Application, 240 }, - }, - 0, - 0, - 12); - - FrameData frameY = MakeFrame( - PresentResult::Presented, - 260, - 15, - 265, - { - { FrameType::Application, 300 }, - }); - - const uint64_t expectedCpuStart = priorApp.presentStartTime + priorApp.timeInPresent; + UnifiedSwapChain swapChain{}; - auto assertRow = [&](const auto& result, FrameType expectedFrameType, uint64_t expectedScreenTime, uint64_t expectedBetweenStart, uint64_t expectedDisplayedEnd) - { - const auto& metrics = result.metrics; - - Assert::IsTrue(metrics.frameType == expectedFrameType); - Assert::AreEqual(expectedScreenTime, metrics.screenTimeQpc); - - Assert::AreEqual( - qpc.DeltaUnsignedMilliSeconds(expectedBetweenStart, expectedScreenTime), - metrics.msBetweenDisplayChange, - 0.0001); - - Assert::AreEqual( - qpc.DeltaUnsignedMilliSeconds(expectedScreenTime, expectedDisplayedEnd), - metrics.msDisplayedTime, - 0.0001); - - Assert::AreEqual( - qpc.DeltaUnsignedMilliSeconds(frameX.presentStartTime, expectedScreenTime), - metrics.msUntilDisplayed, - 0.0001); - - Assert::AreEqual( - qpc.DeltaUnsignedMilliSeconds(expectedCpuStart, expectedScreenTime), - metrics.msDisplayLatency, - 0.0001); - - Assert::AreEqual( - qpc.DeltaUnsignedMilliSeconds(frameX.readyTime, expectedScreenTime), - metrics.msReadyTimeToDisplayLatency, - 0.0001); - - Assert::AreEqual( - qpc.DurationMilliSeconds(frameX.flipDelay), - metrics.msFlipDelay, - 0.0001); - }; + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; - auto resultsX0 = ComputeMetricsForPresent(qpc, frameX, nullptr, chain); - Assert::AreEqual(size_t(3), resultsX0.size(), L"Frame X should emit all three generated rows and postpone the trailing Application row."); - assertRow(resultsX0[0], FrameType::Intel_XEFG, 210, 200, 210); - assertRow(resultsX0[1], FrameType::Intel_XEFG, 210, 210, 210); - assertRow(resultsX0[2], FrameType::Intel_XEFG, 210, 210, 240); + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); - auto resultsX1 = ComputeMetricsForPresent(qpc, frameX, &frameY, chain); - Assert::AreEqual(size_t(1), resultsX1.size(), L"Frame X's trailing Application row should emit only when Frame Y is processed as the next displayed present."); - assertRow(resultsX1[0], FrameType::Application, 240, 210, 300); - } - - TEST_METHOD(ComputeMetricsForPresent_IntelXefg_GeneratedRowsRemainDisplayOnlyUntilTrailingApplicationFinalizes) - { - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState chain{}; - chain.animationErrorSource = AnimationErrorSource::AppProvider; - chain.lastDisplayedScreenTime = 5; - chain.lastDisplayedAppScreenTime = 5; - chain.lastSimStartTime = 80; - chain.firstAppSimStartTime = 80; - chain.lastDisplayedSimStartTime = 80; + FrameData seed{}; + seed.presentStartTime = 9'000; + seed.timeInPresent = 400; + seed.readyTime = 9'500; + seed.finalState = PresentResult::Presented; + seed.appSimStartTime = 8'000; + seed.displayed.PushBack({ FrameType::Application, 10'000 }); - FrameData priorApp = MakeFrame( - PresentResult::Presented, - 1, - 2, - 3, - { - { FrameType::Application, 5 }, - }); - chain.lastAppPresent = priorApp; + Assert::AreEqual(size_t(0), swapChain.ProcessPresent(qpc, std::move(seed)).size()); - FrameData frameA = MakeFrame( - PresentResult::Presented, - 6, - 4, - 8, - { - { FrameType::Intel_XEFG, 10 }, - { FrameType::Intel_XEFG, 20 }, - { FrameType::Intel_XEFG, 30 }, - { FrameType::Application, 40 }, - }, - 90, - 95, - 2); - frameA.gpuStartTime = 12; - frameA.gpuDuration = 6; - frameA.inputTime = 4; - frameA.mouseClickTime = 5; - frameA.appInputSample = { 3, InputDeviceType::Mouse }; - frameA.appSleepStartTime = 11; - frameA.appSleepEndTime = 12; - frameA.appRenderSubmitStartTime = 13; - - FrameData frameB = MakeFrame( - PresentResult::Presented, - 50, - 4, - 52, - { - { FrameType::Application, 55 }, - }); + FrameData present{}; + present.presentStartTime = 10'000; + present.timeInPresent = 500; + present.readyTime = 20'000; + present.finalState = PresentResult::Presented; + present.appSimStartTime = 9'500; + present.displayed.PushBack({ FrameType::Intel_XEFG, 11'000 }); + present.displayed.PushBack({ FrameType::Intel_XEFG, 11'500 }); + present.displayed.PushBack({ FrameType::Intel_XEFG, 12'000 }); + present.displayed.PushBack({ FrameType::Application, 12'500 }); + + auto rows = swapChain.ProcessPresent(qpc, std::move(present)); + + // Origin AppA, plus Gen1..Gen3: each already has nextScreenTime from a sibling + // display entry in this present, so they are display-timing-complete as soon as + // AppB closes the interval. AppB itself is the last entry in its present and is + // not included; it still needs lookahead from a later present. + Assert::AreEqual(size_t(4), rows.size()); + Assert::AreEqual((int)FrameType::Application, (int)rows[0].computed.metrics.frameType); + Assert::AreEqual(uint64_t(10'000), rows[0].computed.metrics.screenTimeQpc); + Assert::IsFalse(HasMetricValue(rows[0].computed.metrics.msAnimationError)); + Assert::AreEqual((int)FrameType::Intel_XEFG, (int)rows[1].computed.metrics.frameType); + Assert::AreEqual((int)FrameType::Intel_XEFG, (int)rows[2].computed.metrics.frameType); + Assert::AreEqual((int)FrameType::Intel_XEFG, (int)rows[3].computed.metrics.frameType); + Assert::AreEqual(uint64_t(11'000), rows[1].computed.metrics.screenTimeQpc); + Assert::AreEqual(uint64_t(11'500), rows[2].computed.metrics.screenTimeQpc); + Assert::AreEqual(uint64_t(12'000), rows[3].computed.metrics.screenTimeQpc); + Assert::IsTrue(HasMetricValue(rows[1].computed.metrics.msAnimationTime)); + Assert::IsTrue(HasMetricValue(rows[2].computed.metrics.msAnimationTime)); + Assert::IsTrue(HasMetricValue(rows[3].computed.metrics.msAnimationTime)); + } + + TEST_METHOD(AmdAfmf_MultiEntryPresent_ReleasesClosedIntervalGeneratedRowsImmediately_ClosingAppRowOnLookahead) + { + // AppA seed, then gen+gen+gen+AppB, then AppC for closing-app lookahead: the + // multi-gen present releases timeline origin AppA together with the 3x AMD_AFMF + // rows (their own nextScreenTime is already known from sibling entries in the + // same present); AppB itself releases alone once the next present supplies its + // lookahead. + QpcConverter qpc(10'000'000, 0); + UnifiedSwapChain swapChain{}; + + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; + + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); + + FrameData seed{}; + seed.presentStartTime = 19'000; + seed.timeInPresent = 400; + seed.readyTime = 19'500; + seed.finalState = PresentResult::Presented; + seed.appSimStartTime = 18'000; + seed.displayed.PushBack({ FrameType::Application, 20'000 }); + + Assert::AreEqual(size_t(0), swapChain.ProcessPresent(qpc, std::move(seed)).size()); - auto assertGeneratedDisplayOnlyRow = [&](const auto& result, uint64_t expectedScreenTime) - { - const auto& metrics = result.metrics; - - Assert::IsTrue(metrics.frameType == FrameType::Intel_XEFG); - Assert::AreEqual(expectedScreenTime, metrics.screenTimeQpc); - Assert::IsTrue(metrics.msDisplayLatency > 0.0); - Assert::IsTrue(metrics.msDisplayedTime > 0.0); - Assert::IsTrue(metrics.msUntilDisplayed > 0.0); - Assert::IsTrue(metrics.msBetweenDisplayChange > 0.0); - Assert::IsFalse(IsMissingFrameMetricValue(metrics.msReadyTimeToDisplayLatency), - L"msReadyTimeToDisplayLatency should not be stored as missing (NaN)"); - Assert::IsFalse(IsMissingFrameMetricValue(metrics.msFlipDelay), - L"msFlipDelay should not be stored as missing (NaN)"); - - Assert::AreEqual(0.0, metrics.msCPUBusy, 0.0001); - Assert::AreEqual(0.0, metrics.msCPUWait, 0.0001); - Assert::AreEqual(0.0, metrics.msCPUTime, 0.0001); - Assert::AreEqual(0.0, metrics.msGPULatency, 0.0001); - Assert::AreEqual(0.0, metrics.msGPUBusy, 0.0001); - Assert::AreEqual(0.0, metrics.msGPUWait, 0.0001); - Assert::AreEqual(0.0, metrics.msGPUTime, 0.0001); - Assert::AreEqual(0.0, metrics.msVideoBusy, 0.0001); - - Assert::IsTrue(IsMissingFrameMetricValue(metrics.msClickToPhotonLatency), - L"msClickToPhotonLatency should be stored as missing (NaN)"); - Assert::IsTrue(IsMissingFrameMetricValue(metrics.msAllInputPhotonLatency), - L"msAllInputPhotonLatency should be stored as missing (NaN)"); - Assert::IsTrue(IsMissingFrameMetricValue(metrics.msInstrumentedInputTime), - L"msInstrumentedInputTime should be stored as missing (NaN)"); - Assert::IsTrue(IsMissingFrameMetricValue(metrics.msAnimationError), - L"msAnimationError should be stored as missing (NaN)"); - Assert::IsTrue(IsMissingFrameMetricValue(metrics.msAnimationTime), - L"msAnimationTime should be stored as missing (NaN)"); - Assert::IsTrue(IsMissingFrameMetricValue(metrics.msInstrumentedSleep), - L"msInstrumentedSleep should be stored as missing (NaN)"); - Assert::IsTrue(IsMissingFrameMetricValue(metrics.msInstrumentedGpuLatency), - L"msInstrumentedGpuLatency should be stored as missing (NaN)"); - Assert::IsTrue(IsMissingFrameMetricValue(metrics.msBetweenSimStarts), - L"msBetweenSimStarts should be stored as missing (NaN)"); - Assert::IsTrue(IsMissingFrameMetricValue(metrics.msInstrumentedRenderLatency), - L"msInstrumentedRenderLatency should be stored as missing (NaN)"); - Assert::IsTrue(IsMissingFrameMetricValue(metrics.msInstrumentedLatency), - L"msInstrumentedLatency should be stored as missing (NaN)"); - }; - - auto generatedRows = ComputeMetricsForPresent(qpc, frameA, nullptr, chain); - - Assert::AreEqual(size_t(3), generatedRows.size(), L"Frame A should emit only generated rows before the trailing Application row is finalized."); - assertGeneratedDisplayOnlyRow(generatedRows[0], 10); - assertGeneratedDisplayOnlyRow(generatedRows[1], 20); - assertGeneratedDisplayOnlyRow(generatedRows[2], 30); - Assert::IsTrue(chain.lastAppPresent.has_value()); - Assert::AreEqual(priorApp.presentStartTime, chain.lastAppPresent->presentStartTime, - L"lastAppPresent must not advance while only generated rows are emitted."); - Assert::AreEqual(uint64_t(5), chain.lastDisplayedScreenTime, - L"lastDisplayedScreenTime must remain on the prior Application frame until finalization."); - - auto finalizedRows = ComputeMetricsForPresent(qpc, frameA, &frameB, chain); - - Assert::AreEqual(size_t(1), finalizedRows.size(), L"Frame A should emit only the postponed trailing Application row when Frame B finalizes it."); - - const auto& finalizedMetrics = finalizedRows[0].metrics; - Assert::IsTrue(finalizedMetrics.frameType == FrameType::Application); - Assert::AreEqual(uint64_t(40), finalizedMetrics.screenTimeQpc); - Assert::IsTrue(finalizedMetrics.msDisplayLatency > 0.0); - Assert::IsTrue(finalizedMetrics.msDisplayedTime > 0.0); - Assert::IsTrue(finalizedMetrics.msUntilDisplayed > 0.0); - Assert::IsTrue(finalizedMetrics.msBetweenDisplayChange > 0.0); - Assert::IsFalse(IsMissingFrameMetricValue(finalizedMetrics.msReadyTimeToDisplayLatency)); - Assert::IsFalse(IsMissingFrameMetricValue(finalizedMetrics.msFlipDelay)); - Assert::IsTrue(finalizedMetrics.msCPUBusy > 0.0); - Assert::IsTrue(finalizedMetrics.msCPUTime > 0.0); - Assert::IsTrue(finalizedMetrics.msGPUBusy > 0.0); - Assert::IsFalse(IsMissingFrameMetricValue(finalizedMetrics.msClickToPhotonLatency)); - Assert::IsFalse(IsMissingFrameMetricValue(finalizedMetrics.msAllInputPhotonLatency)); - Assert::IsFalse(IsMissingFrameMetricValue(finalizedMetrics.msInstrumentedInputTime)); - Assert::IsFalse(IsMissingFrameMetricValue(finalizedMetrics.msAnimationError)); - Assert::IsFalse(IsMissingFrameMetricValue(finalizedMetrics.msAnimationTime)); - Assert::IsFalse(IsMissingFrameMetricValue(finalizedMetrics.msInstrumentedLatency)); - Assert::IsFalse(IsMissingFrameMetricValue(finalizedMetrics.msInstrumentedRenderLatency)); - Assert::IsFalse(IsMissingFrameMetricValue(finalizedMetrics.msInstrumentedSleep)); - Assert::IsFalse(IsMissingFrameMetricValue(finalizedMetrics.msInstrumentedGpuLatency)); - Assert::IsFalse(IsMissingFrameMetricValue(finalizedMetrics.msBetweenSimStarts)); - Assert::IsTrue(chain.lastAppPresent.has_value()); - Assert::AreEqual(frameA.presentStartTime, chain.lastAppPresent->presentStartTime, - L"lastAppPresent must advance only after the trailing Application row is finalized."); - Assert::AreEqual(uint64_t(40), chain.lastDisplayedScreenTime, - L"lastDisplayedScreenTime should advance when the trailing Application row finalizes."); + FrameData present{}; + present.presentStartTime = 20'000; + present.timeInPresent = 600; + present.readyTime = 30'000; + present.finalState = PresentResult::Presented; + present.appSimStartTime = 19'500; + present.displayed.PushBack({ FrameType::AMD_AFMF, 21'000 }); + present.displayed.PushBack({ FrameType::AMD_AFMF, 21'500 }); + present.displayed.PushBack({ FrameType::AMD_AFMF, 22'000 }); + present.displayed.PushBack({ FrameType::Application, 22'500 }); + + auto afterMultiGenPresent = swapChain.ProcessPresent(qpc, std::move(present)); + Assert::AreEqual(size_t(4), afterMultiGenPresent.size()); + Assert::AreEqual((int)FrameType::Application, (int)afterMultiGenPresent[0].computed.metrics.frameType); + Assert::AreEqual(uint64_t(20'000), afterMultiGenPresent[0].computed.metrics.screenTimeQpc); + Assert::IsFalse(HasMetricValue(afterMultiGenPresent[0].computed.metrics.msAnimationError)); + Assert::AreEqual((int)FrameType::AMD_AFMF, (int)afterMultiGenPresent[1].computed.metrics.frameType); + Assert::AreEqual((int)FrameType::AMD_AFMF, (int)afterMultiGenPresent[2].computed.metrics.frameType); + Assert::AreEqual((int)FrameType::AMD_AFMF, (int)afterMultiGenPresent[3].computed.metrics.frameType); + Assert::AreEqual(uint64_t(21'000), afterMultiGenPresent[1].computed.metrics.screenTimeQpc); + Assert::AreEqual(uint64_t(21'500), afterMultiGenPresent[2].computed.metrics.screenTimeQpc); + Assert::AreEqual(uint64_t(22'000), afterMultiGenPresent[3].computed.metrics.screenTimeQpc); + Assert::IsTrue(HasMetricValue(afterMultiGenPresent[1].computed.metrics.msAnimationTime)); + Assert::IsTrue(HasMetricValue(afterMultiGenPresent[2].computed.metrics.msAnimationTime)); + Assert::IsTrue(HasMetricValue(afterMultiGenPresent[3].computed.metrics.msAnimationTime)); + + FrameData lookahead{}; + lookahead.presentStartTime = 23'000; + lookahead.timeInPresent = 400; + lookahead.readyTime = 30'500; + lookahead.finalState = PresentResult::Presented; + lookahead.appSimStartTime = 20'000; + lookahead.displayed.PushBack({ FrameType::Application, 24'000 }); + + auto afterLookaheadPresent = swapChain.ProcessPresent(qpc, std::move(lookahead)); + Assert::AreEqual(size_t(1), afterLookaheadPresent.size()); + Assert::AreEqual((int)FrameType::Application, (int)afterLookaheadPresent[0].computed.metrics.frameType); + Assert::AreEqual(uint64_t(22'500), afterLookaheadPresent[0].computed.metrics.screenTimeQpc); + Assert::IsTrue(HasMetricValue(afterLookaheadPresent[0].computed.metrics.msAnimationTime)); } }; + TEST_CLASS(DisplayedDroppedDisplayedSequenceTests) { public: TEST_METHOD(Displayed_Dropped_Displayed_Sequence_IsHandledAcrossCalls) { QpcConverter qpc(10'000'000, 0); - SwapChainCoreState chain{}; - - // A: displayed once, but no nextDisplayed yet => postponed - FrameData A = MakeFrame( - PresentResult::Presented, - 50'000, 400, 50'500, - { { FrameType::Application, 51'000 } }); - - auto mA_pre = ComputeMetricsForPresent(qpc, A, nullptr, chain); - Assert::AreEqual(size_t(0), mA_pre.size(), L"Single display postponed."); - Assert::IsFalse(chain.lastPresent.has_value(), L"Chain is not updated without nextDisplayed."); - - // B: dropped (not presented/displayed) - FrameData B = MakeFrame( - PresentResult::Discarded, - 52'000, 300, 52'400, - {} /* no displayed */); - - auto mB = ComputeMetricsForPresent(qpc, B, nullptr, chain); - Assert::AreEqual(size_t(1), mB.size(), L"Dropped frame goes through not-displayed path."); - Assert::IsTrue(chain.lastPresent.has_value(), L"Not-displayed path updates chain."); - Assert::IsTrue(chain.lastAppPresent.has_value(), L"Not-displayed frame becomes lastAppPresent."); - Assert::AreEqual(uint64_t(0), chain.lastDisplayedScreenTime, L"Not-displayed should leave lastDisplayedScreenTime at 0."); - - // C: displayed next; use it to process A's postponed last - FrameData C = MakeFrame( - PresentResult::Presented, - 53'000, 350, 53'400, - { { FrameType::Application, 54'000 } }); - - auto mA_post = ComputeMetricsForPresent(qpc, A, &C, chain); - Assert::AreEqual(size_t(1), mA_post.size(), L"Postponed last display of A processed with nextDisplayed."); - - // Chain updated based on A (last display instance) - Assert::IsTrue(chain.lastPresent.has_value()); - Assert::AreEqual(uint64_t(51'000), chain.lastDisplayedScreenTime); + UnifiedSwapChain swapChain{}; + + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; + + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); + + FrameData a{}; + a.presentStartTime = 50'000; + a.timeInPresent = 400; + a.readyTime = 50'500; + a.finalState = PresentResult::Presented; + a.appSimStartTime = 50'000; + a.displayed.PushBack({ FrameType::Application, 51'000 }); + + auto rowsA = swapChain.ProcessPresent(qpc, std::move(a)); + Assert::AreEqual(size_t(0), rowsA.size(), L"Timeline origin waits for display lookahead."); + Assert::AreEqual(uint64_t(0), swapChain.swapChain.lastDisplayedScreenTime); + + FrameData b{}; + b.presentStartTime = 52'000; + b.timeInPresent = 300; + b.readyTime = 52'400; + b.finalState = PresentResult::Discarded; + + auto rowsB = swapChain.ProcessPresent(qpc, std::move(b)); + Assert::AreEqual(size_t(0), rowsB.size(), L"Dropped present held after first app anchor is seeded."); + Assert::AreEqual(uint64_t(0), swapChain.swapChain.lastDisplayedScreenTime); + + FrameData c{}; + c.presentStartTime = 53'000; + c.timeInPresent = 350; + c.readyTime = 53'400; + c.finalState = PresentResult::Presented; + c.appSimStartTime = 52'000; + c.displayed.PushBack({ FrameType::Application, 54'000 }); + + auto rowsC = swapChain.ProcessPresent(qpc, std::move(c)); + Assert::IsTrue(rowsC.size() >= size_t(1), L"Lookahead publishes the held origin row."); + + bool publishedOriginA = false; + for (const auto& row : rowsC) { + if (row.computed.metrics.screenTimeQpc == 51'000) { + publishedOriginA = true; + break; + } + } + Assert::IsTrue(publishedOriginA); + Assert::AreEqual(uint64_t(51'000), swapChain.swapChain.lastDisplayedScreenTime); } }; @@ -1917,7 +1029,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) /*readyTime*/ 1'020'000, /*displayed*/{}); // no displayed frames => not-displayed path - auto firstMetrics = ComputeMetricsForPresent(qpc, first, nullptr, chain); + auto firstMetrics = ComputeMetricsForPresent(qpc, first, chain); // We should get exactly one metrics entry Assert::AreEqual(size_t(1), firstMetrics.size(), L"First not-displayed frame should produce one metrics entry."); @@ -1947,7 +1059,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) /*readyTime*/ 1'036'660, /*displayed*/{}); - auto secondMetrics = ComputeMetricsForPresent(qpc, second, nullptr, chain); + auto secondMetrics = ComputeMetricsForPresent(qpc, second, chain); Assert::AreEqual( size_t(1), @@ -1977,12 +1089,12 @@ TEST_CLASS(ComputeMetricsForPresentTests) PresentResult::Presented, /*presentStartTime*/ 1'000'000, // 0.1s /*timeInPresent*/ 200'000, // 0.02s - /*readyTime*/ 1'500'000, // 0.15s → 50 ms after start + /*readyTime*/ 1'500'000, // 0.15s -> 50 ms after start /*displayed*/{} // no displays => "not displayed" path ); first.gpuStartTime = 1'200'000; // 0.12s - auto firstMetricsList = ComputeMetricsForPresent(qpc, first, nullptr, chain); + auto firstMetricsList = ComputeMetricsForPresent(qpc, first, chain); Assert::AreEqual( size_t(1), firstMetricsList.size(), @@ -1996,8 +1108,8 @@ TEST_CLASS(ComputeMetricsForPresentTests) firstMetrics.timeInSeconds, L"timeInSeconds should come from QpcToSeconds(presentStartTime)."); - // No prior lastPresent → msBetweenPresents should be 0 - AssertAreEqualWithinTolerance( + // No prior lastPresent -> msBetweenPresents should be 0 + Assert::AreEqual( 0.0, firstMetrics.msBetweenPresents, 0.0001, @@ -2029,7 +1141,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) 0.0001, L"msUntilRenderStart should equal delta from PresentStartTime to GPUStartTime."); - // With no prior present, CalculateCPUStart should return 0 → cpuStartQpc == 0 + // With no prior present, CalculateCPUStart should return 0 -> cpuStartQpc == 0 Assert::AreEqual( uint64_t(0), firstMetrics.cpuStartQpc, @@ -2047,7 +1159,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) // ------------------------------------------------------------------------- // Second frame: also not displayed, later in time. // This should: - // - compute msBetweenPresents based on first→second start times + // - compute msBetweenPresents based on first->second start times // - keep msInPresentApi/msUntilRenderComplete consistent // - use CalculateCPUStart based on 'first' as lastAppPresent // ------------------------------------------------------------------------- @@ -2061,7 +1173,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) ); second.gpuStartTime = 1'220'000; // 0.122s - auto secondMetricsList = ComputeMetricsForPresent(qpc, second, nullptr, chain); + auto secondMetricsList = ComputeMetricsForPresent(qpc, second, chain); Assert::AreEqual( size_t(1), secondMetricsList.size(), @@ -2094,28 +1206,28 @@ TEST_CLASS(ComputeMetricsForPresentTests) expectedMsUntilRenderCompleteSecond, secondMetrics.msUntilRenderComplete, 0.0001, - L"Second frame msUntilRenderComplete should match start→ready delta."); - AssertAreEqualWithinTolerance( + L"Second frame msUntilRenderComplete should match start->ready delta."); + Assert::AreEqual( expectedMsUntilRenderStartSecond, secondMetrics.msUntilRenderStart, 0.0001, - L"Second frame msUntilRenderStart should match start→GPU start delta."); + L"Second frame msUntilRenderStart should match start->GPU start delta."); // cpuStartQpc for second should come from CalculateCPUStart: - // lastAppPresent == first (no propagated times) → first.start + first.timeInPresent + // lastAppPresent == first (no propagated times) -> first.start + first.timeInPresent uint64_t expectedCpuStartSecond = first.presentStartTime + first.timeInPresent; Assert::AreEqual( expectedCpuStartSecond, secondMetrics.cpuStartQpc, L"cpuStartQpc should match CalculateCPUStart from lastAppPresent."); } - TEST_METHOD(ComputeMetricsForPresent_DisplayedWithNext_BaseTimingAndCpuStart_AreCorrect) + TEST_METHOD(ComputeMetricsForPresent_Displayed_BaseTimingAndCpuStart_AreCorrect) { // 10 MHz QPC QpcConverter qpc(10'000'000, 0); SwapChainCoreState chain{}; - // Baseline frame: Presented but not displayed → not-displayed path + // Baseline frame: Presented but not displayed -> not-displayed path FrameData first = MakeFrame( PresentResult::Presented, /*presentStartTime*/ 1'000'000, @@ -2123,7 +1235,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) /*readyTime*/ 1'500'000, /*displayed*/{}); // no displays - auto firstMetricsList = ComputeMetricsForPresent(qpc, first, nullptr, chain); + auto firstMetricsList = ComputeMetricsForPresent(qpc, first, chain); Assert::AreEqual( size_t(1), firstMetricsList.size(), @@ -2136,7 +1248,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) return; } - // Second frame: Presented + one displayed instance, processed with a nextDisplayed + // Second frame: Presented + one displayed instance. FrameData second = MakeFrame( PresentResult::Presented, /*presentStartTime*/ 1'016'000, // slightly later than first @@ -2147,22 +1259,12 @@ TEST_CLASS(ComputeMetricsForPresentTests) }); second.gpuStartTime = 1'200'000; - // Dummy nextDisplayed with at least one display so the "with next" path is taken - FrameData nextDisplayed = MakeFrame( - PresentResult::Presented, - /*presentStartTime*/ 2'100'000, - /*timeInPresent*/ 100'000, - /*readyTime*/ 2'200'000, - DisplayedVector{ - { FrameType::Application, 2'300'000 } - }); - - auto secondMetricsList = ComputeMetricsForPresent(qpc, second, &nextDisplayed, chain); + auto secondMetricsList = ComputeMetricsForPresent(qpc, second, chain); Assert::AreEqual( size_t(1), secondMetricsList.size(), - L"Displayed-with-next frame should produce one metrics entry (postponed last display)."); + L"Displayed V1 frame should produce one metrics entry."); const auto& secondMetrics = secondMetricsList[0].metrics; @@ -2173,7 +1275,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) secondMetrics.timeInSeconds, L"timeInSeconds should match QpcToSeconds(presentStartTime) for displayed frame."); - // msBetweenPresents: lastPresent.start (first) → second.start + // msBetweenPresents: lastPresent.start (first) -> second.start double expectedBetween = qpc.DeltaUnsignedMilliSeconds(first.presentStartTime, second.presentStartTime); AssertAreEqualWithinTolerance( @@ -2190,26 +1292,26 @@ TEST_CLASS(ComputeMetricsForPresentTests) 0.0001, L"msInPresentApi should match QpcDeltaToMilliSeconds(timeInPresent) for displayed frame."); - // msUntilRenderComplete from start → ready + // msUntilRenderComplete from start -> ready double expectedMsUntilRenderCompleteSecond = qpc.DeltaUnsignedMilliSeconds(second.presentStartTime, second.readyTime); AssertAreEqualWithinTolerance( expectedMsUntilRenderCompleteSecond, secondMetrics.msUntilRenderComplete, 0.0001, - L"msUntilRenderComplete should match start→ready delta for displayed frame."); + L"msUntilRenderComplete should match start->ready delta for displayed frame."); - // msUntilRenderStart from start → GPU start + // msUntilRenderStart from start -> GPU start double expectedMsUntilRenderStartSecond = qpc.DeltaUnsignedMilliSeconds(second.presentStartTime, second.gpuStartTime); AssertAreEqualWithinTolerance( expectedMsUntilRenderStartSecond, secondMetrics.msUntilRenderStart, 0.0001, - L"msUntilRenderStart should match start→GPU start delta for displayed frame."); + L"msUntilRenderStart should match start->GPU start delta for displayed frame."); // cpuStartQpc should come from CalculateCPUStart using baseline frame as lastAppPresent: - // (no propagated times) → first.start + first.timeInPresent + // (no propagated times) -> first.start + first.timeInPresent uint64_t expectedCpuStartSecond = first.presentStartTime + first.timeInPresent; Assert::AreEqual( expectedCpuStartSecond, @@ -2230,7 +1332,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) frame.finalState = PresentResult::Presented; // No displayed entries - auto results = ComputeMetricsForPresent(qpc, frame, nullptr, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; AssertAreEqualWithinTolerance(0.0, m.msUntilDisplayed, 0.0001); @@ -2245,20 +1347,15 @@ TEST_CLASS(ComputeMetricsForPresentTests) frame.timeInPresent = 20'000; frame.readyTime = 2'050'000; frame.finalState = PresentResult::Presented; - // Single displayed; will be postponed unless nextDisplayed provided frame.displayed.PushBack({ FrameType::Application, 2'500'000 }); - FrameData next{}; // provide nextDisplayed to process postponed - next.finalState = PresentResult::Presented; - next.displayed.PushBack({ FrameType::Application, 3'000'000 }); - - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; double expected = qpc.DeltaUnsignedMilliSeconds(frame.presentStartTime, frame.displayed[0].second); AssertAreEqualWithinTolerance(expected, m.msUntilDisplayed, 0.0001); } - TEST_METHOD(DisplayedGeneratedFrame_AlsoReturnsDelta) + TEST_METHOD(DisplayedApplicationFrame_AlsoReturnsDelta) { QpcConverter qpc(10'000'000, 0); SwapChainCoreState chain{}; @@ -2268,14 +1365,9 @@ TEST_CLASS(ComputeMetricsForPresentTests) frame.timeInPresent = 15'000; frame.readyTime = 5'030'000; frame.finalState = PresentResult::Presented; - // Displayed generated frame (e.g., Repeated/Composed/Desktop depending on enum) - frame.displayed.PushBack({ FrameType::Intel_XEFG, 5'100'000 }); - - FrameData next{}; - next.finalState = PresentResult::Presented; - next.displayed.PushBack({ FrameType::Application, 6'000'000 }); + frame.displayed.PushBack({ FrameType::Application, 5'100'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; double expected = qpc.DeltaUnsignedMilliSeconds(frame.presentStartTime, frame.displayed[0].second); @@ -2296,7 +1388,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) frame.readyTime = 1'010'000; frame.finalState = PresentResult::Presented; - auto results = ComputeMetricsForPresent(qpc, frame, nullptr, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; AssertAreEqualWithinTolerance(0.0, m.msDisplayedTime, 0.0001); @@ -2305,7 +1397,15 @@ TEST_CLASS(ComputeMetricsForPresentTests) TEST_METHOD(DisplayedSingleDisplay_WithNextDisplay_ReturnsDeltaToNextScreenTime) { QpcConverter qpc(10'000'000, 0); - SwapChainCoreState chain{}; + UnifiedSwapChain swapChain{}; + + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; + + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); FrameData frame{}; frame.presentStartTime = 2'000'000; @@ -2314,50 +1414,86 @@ TEST_CLASS(ComputeMetricsForPresentTests) frame.finalState = PresentResult::Presented; frame.displayed.PushBack({ FrameType::Application, 2'500'000 }); + Assert::AreEqual(size_t(0), swapChain.ProcessPresent(qpc, std::move(frame)).size()); + FrameData next{}; + next.presentStartTime = 2'600'000; + next.timeInPresent = 15'000; + next.readyTime = 2'650'000; next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 2'800'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); - Assert::AreEqual(size_t(1), results.size()); - const auto& m = results[0].metrics; + auto rows = swapChain.ProcessPresent(qpc, std::move(next)); + Assert::AreEqual(size_t(1), rows.size()); + Assert::AreEqual(uint64_t(2'500'000), rows[0].computed.metrics.screenTimeQpc); double expected = qpc.DeltaUnsignedMilliSeconds(2'500'000, 2'800'000); - AssertAreEqualWithinTolerance(expected, m.msDisplayedTime, 0.0001); + AssertAreEqualWithinTolerance(expected, rows[0].computed.metrics.msDisplayedTime, 0.0001); } TEST_METHOD(DisplayedMultipleDisplays_ProcessEachWithNextScreenTime) { QpcConverter qpc(10'000'000, 0); - SwapChainCoreState chain{}; + UnifiedSwapChain swapChain{}; + + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; + + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); FrameData frame{}; frame.presentStartTime = 3'000'000; frame.timeInPresent = 30'000; frame.readyTime = 3'050'000; frame.finalState = PresentResult::Presented; + frame.appSimStartTime = 3'000'000; frame.displayed.PushBack({ FrameType::Application, 3'100'000 }); frame.displayed.PushBack({ FrameType::Repeated, 3'400'000 }); frame.displayed.PushBack({ FrameType::Repeated, 3'700'000 }); - FrameData next{}; - next.finalState = PresentResult::Presented; - next.displayed.PushBack({ FrameType::Application, 4'000'000 }); + auto originRows = swapChain.ProcessPresent(qpc, std::move(frame)); + Assert::AreEqual(size_t(1), originRows.size()); - auto results1 = ComputeMetricsForPresent(qpc, frame, nullptr, chain); - Assert::AreEqual(size_t(2), results1.size()); + double expectedOrigin = qpc.DeltaUnsignedMilliSeconds(3'100'000, 3'400'000); + AssertAreEqualWithinTolerance(expectedOrigin, originRows[0].computed.metrics.msDisplayedTime, 0.0001); - double expected0 = qpc.DeltaUnsignedMilliSeconds(3'100'000, 3'400'000); - AssertAreEqualWithinTolerance(expected0, results1[0].metrics.msDisplayedTime, 0.0001); + FrameData closingApp{}; + closingApp.presentStartTime = 3'800'000; + closingApp.timeInPresent = 25'000; + closingApp.readyTime = 3'850'000; + closingApp.finalState = PresentResult::Presented; + closingApp.appSimStartTime = 3'300'000; + closingApp.displayed.PushBack({ FrameType::Application, 4'000'000 }); - double expected1 = qpc.DeltaUnsignedMilliSeconds(3'400'000, 3'700'000); - AssertAreEqualWithinTolerance(expected1, results1[1].metrics.msDisplayedTime, 0.0001); + // Rep3400000 and Rep3700000 already know their own nextScreenTime from sibling + // display entries in the earlier present, so once closingApp closes the interval + // they publish immediately here; closingApp itself is the sole entry in its + // present and still needs lookahead. + auto closedIntervalRows = swapChain.ProcessPresent(qpc, std::move(closingApp)); + Assert::AreEqual(size_t(2), closedIntervalRows.size()); - auto results2 = ComputeMetricsForPresent(qpc, frame, &next, chain); - Assert::AreEqual(size_t(1), results2.size()); + double expectedFirstGen = qpc.DeltaUnsignedMilliSeconds(3'400'000, 3'700'000); + AssertAreEqualWithinTolerance(expectedFirstGen, closedIntervalRows[0].computed.metrics.msDisplayedTime, 0.0001); + + double expectedSecondGen = qpc.DeltaUnsignedMilliSeconds(3'700'000, 4'000'000); + AssertAreEqualWithinTolerance(expectedSecondGen, closedIntervalRows[1].computed.metrics.msDisplayedTime, 0.0001); + + FrameData lookahead{}; + lookahead.presentStartTime = 4'100'000; + lookahead.timeInPresent = 20'000; + lookahead.readyTime = 4'150'000; + lookahead.finalState = PresentResult::Presented; + lookahead.appSimStartTime = 3'320'000; + lookahead.displayed.PushBack({ FrameType::Application, 4'300'000 }); - double expected2 = qpc.DeltaUnsignedMilliSeconds(3'700'000, 4'000'000); - AssertAreEqualWithinTolerance(expected2, results2[0].metrics.msDisplayedTime, 0.0001); + auto intervalRows = swapChain.ProcessPresent(qpc, std::move(lookahead)); + Assert::AreEqual(size_t(1), intervalRows.size()); + + double expectedClosingApp = qpc.DeltaUnsignedMilliSeconds(4'000'000, 4'300'000); + AssertAreEqualWithinTolerance(expectedClosingApp, intervalRows[0].computed.metrics.msDisplayedTime, 0.0001); } }; @@ -2376,11 +1512,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) frame.finalState = PresentResult::Presented; frame.displayed.PushBack({ FrameType::Application, 5'500'000 }); - FrameData next{}; - next.finalState = PresentResult::Presented; - next.displayed.PushBack({ FrameType::Application, 6'000'000 }); - - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -2404,7 +1536,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 6'000'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -2424,7 +1556,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) frame.readyTime = 5'100'000; frame.finalState = PresentResult::Presented; - auto results = ComputeMetricsForPresent(qpc, frame, nullptr, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -2434,36 +1566,51 @@ TEST_CLASS(ComputeMetricsForPresentTests) TEST_METHOD(MultipleDisplays_EachComputesDeltaFromPreviousDisplayedEntry) { QpcConverter qpc(10'000'000, 0); - SwapChainCoreState chain{}; - chain.lastDisplayedScreenTime = 3'000'000; + UnifiedSwapChain swapChain{}; + + FrameData bootstrap{}; + bootstrap.presentStartTime = 2'500'000; + bootstrap.timeInPresent = 50'000; + bootstrap.readyTime = 2'600'000; + bootstrap.finalState = PresentResult::Presented; + bootstrap.appSimStartTime = 2'900'000; + bootstrap.displayed.PushBack({ FrameType::Application, 3'000'000 }); + + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); + Assert::AreEqual(uint64_t(3'000'000), swapChain.swapChain.lastDisplayedScreenTime); FrameData frame{}; frame.presentStartTime = 5'000'000; frame.timeInPresent = 50'000; frame.readyTime = 5'100'000; frame.finalState = PresentResult::Presented; + frame.appSimStartTime = 6'000'000; frame.displayed.PushBack({ FrameType::Intel_XEFG, 5'500'000 }); frame.displayed.PushBack({ FrameType::Intel_XEFG, 5'800'000 }); frame.displayed.PushBack({ FrameType::Application, 6'100'000 }); - FrameData next{}; - next.finalState = PresentResult::Presented; - next.displayed.PushBack({ FrameType::Application, 6'400'000 }); - - auto results1 = ComputeMetricsForPresent(qpc, frame, nullptr, chain); - Assert::AreEqual(size_t(2), results1.size()); + auto preAnchorRows = swapChain.ProcessPresent(qpc, std::move(frame)); + Assert::AreEqual(size_t(2), preAnchorRows.size()); double expected0 = qpc.DeltaUnsignedMilliSeconds(3'000'000, 5'500'000); - AssertAreEqualWithinTolerance(expected0, results1[0].metrics.msBetweenDisplayChange, 0.0001); + AssertAreEqualWithinTolerance(expected0, preAnchorRows[0].computed.metrics.msBetweenDisplayChange, 0.0001); double expected1 = qpc.DeltaUnsignedMilliSeconds(5'500'000, 5'800'000); - Assert::AreEqual(expected1, results1[1].metrics.msBetweenDisplayChange, 0.0001); + AssertAreEqualWithinTolerance(expected1, preAnchorRows[1].computed.metrics.msBetweenDisplayChange, 0.0001); - auto results2 = ComputeMetricsForPresent(qpc, frame, &next, chain); - Assert::AreEqual(size_t(1), results2.size()); + FrameData lookahead{}; + lookahead.presentStartTime = 6'200'000; + lookahead.timeInPresent = 30'000; + lookahead.readyTime = 6'250'000; + lookahead.finalState = PresentResult::Presented; + lookahead.appSimStartTime = 6'050'000; + lookahead.displayed.PushBack({ FrameType::Application, 6'400'000 }); + + auto originRows = swapChain.ProcessPresent(qpc, std::move(lookahead)); + Assert::AreEqual(size_t(1), originRows.size()); double expected2 = qpc.DeltaUnsignedMilliSeconds(5'800'000, 6'100'000); - Assert::AreEqual(expected2, results2[0].metrics.msBetweenDisplayChange, 0.0001); + AssertAreEqualWithinTolerance(expected2, originRows[0].computed.metrics.msBetweenDisplayChange, 0.0001); } }; @@ -2482,7 +1629,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) frame.flipDelay = 5'000; frame.finalState = PresentResult::Presented; - auto results = ComputeMetricsForPresent(qpc, frame, nullptr, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -2503,11 +1650,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) frame.finalState = PresentResult::Presented; frame.displayed.PushBack({ FrameType::Application, 7'500'000 }); - FrameData next{}; - next.finalState = PresentResult::Presented; - next.displayed.PushBack({ FrameType::Application, 8'000'000 }); - - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -2534,7 +1677,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 8'000'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -2553,13 +1696,13 @@ TEST_CLASS(ComputeMetricsForPresentTests) frame.readyTime = 7'100'000; frame.flipDelay = 50'000; frame.finalState = PresentResult::Presented; - frame.displayed.PushBack({ FrameType::Repeated, 7'500'000 }); + frame.displayed.PushBack({ FrameType::Application, 7'500'000 }); FrameData next{}; next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 8'000'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -2584,7 +1727,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) frame.readyTime = 9'100'000; frame.finalState = PresentResult::Presented; - auto results = ComputeMetricsForPresent(qpc, frame, nullptr, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -2603,11 +1746,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) frame.finalState = PresentResult::Presented; frame.displayed.PushBack({ FrameType::Application, 9'500'000 }); - FrameData next{}; - next.finalState = PresentResult::Presented; - next.displayed.PushBack({ FrameType::Application, 10'000'000 }); - - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -2617,32 +1756,61 @@ TEST_CLASS(ComputeMetricsForPresentTests) TEST_METHOD(DisplayedMultipleFrames_EachHasOwnScreenTime) { QpcConverter qpc(10'000'000, 0); - SwapChainCoreState chain{}; + UnifiedSwapChain swapChain{}; + + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; + + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); FrameData frame{}; frame.presentStartTime = 9'000'000; frame.timeInPresent = 90'000; frame.readyTime = 9'100'000; frame.finalState = PresentResult::Presented; + frame.appSimStartTime = 9'000'000; frame.displayed.PushBack({ FrameType::Application, 9'500'000 }); frame.displayed.PushBack({ FrameType::Repeated, 9'800'000 }); frame.displayed.PushBack({ FrameType::Repeated, 10'100'000 }); - FrameData next{}; - next.finalState = PresentResult::Presented; - next.displayed.PushBack({ FrameType::Application, 10'400'000 }); + auto originRows = swapChain.ProcessPresent(qpc, std::move(frame)); + Assert::AreEqual(size_t(1), originRows.size()); + Assert::AreEqual(uint64_t(9'500'000), originRows[0].computed.metrics.screenTimeQpc); - auto results1 = ComputeMetricsForPresent(qpc, frame, nullptr, chain); - Assert::AreEqual(size_t(2), results1.size()); - Assert::AreEqual(uint64_t(9'500'000), results1[0].metrics.screenTimeQpc); - Assert::AreEqual(uint64_t(9'800'000), results1[1].metrics.screenTimeQpc); + FrameData closingApp{}; + closingApp.presentStartTime = 10'200'000; + closingApp.timeInPresent = 30'000; + closingApp.readyTime = 10'250'000; + closingApp.finalState = PresentResult::Presented; + closingApp.appSimStartTime = 9'300'000; + closingApp.displayed.PushBack({ FrameType::Application, 10'400'000 }); - auto results2 = ComputeMetricsForPresent(qpc, frame, &next, chain); - Assert::AreEqual(size_t(1), results2.size()); - Assert::AreEqual(uint64_t(10'100'000), results2[0].metrics.screenTimeQpc); + // Rep9800000 and Rep10100000 already know their own nextScreenTime from sibling + // display entries in the earlier present, so once closingApp closes the interval + // they publish immediately here; closingApp itself is the sole entry in its + // present and still needs lookahead. + auto closedIntervalRows = swapChain.ProcessPresent(qpc, std::move(closingApp)); + Assert::AreEqual(size_t(2), closedIntervalRows.size()); + Assert::AreEqual(uint64_t(9'800'000), closedIntervalRows[0].computed.metrics.screenTimeQpc); + Assert::AreEqual(uint64_t(10'100'000), closedIntervalRows[1].computed.metrics.screenTimeQpc); + + FrameData lookahead{}; + lookahead.presentStartTime = 10'500'000; + lookahead.timeInPresent = 25'000; + lookahead.readyTime = 10'550'000; + lookahead.finalState = PresentResult::Presented; + lookahead.appSimStartTime = 9'320'000; + lookahead.displayed.PushBack({ FrameType::Application, 10'700'000 }); + + auto intervalRows = swapChain.ProcessPresent(qpc, std::move(lookahead)); + Assert::AreEqual(size_t(1), intervalRows.size()); + Assert::AreEqual(uint64_t(10'400'000), intervalRows[0].computed.metrics.screenTimeQpc); } - TEST_METHOD(DisplayedGeneratedFrame_EqualsGeneratedFrameScreenTime) + TEST_METHOD(V1_FirstDisplayNonAppFrame_UsesFirstDisplayScreenTime) { QpcConverter qpc(10'000'000, 0); SwapChainCoreState chain{}; @@ -2658,7 +1826,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 10'000'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -2668,11 +1836,11 @@ TEST_CLASS(ComputeMetricsForPresentTests) TEST_CLASS(NvCollapsedPresentTests) { public: - TEST_METHOD(NvCollapsedPresent_AdjustsNextScreenTimeAndFlipDelay) + TEST_METHOD(NvCollapsedPresent_V1_AdjustsCurrentScreenTimeAndFlipDelayFromPreviousFrame) { - // Mirrors AdjustScreenTimeForCollapsedPresentNV behavior: - // When current frame's screenTime > nextFrame's screenTime and current has flipDelay, - // the next frame's screenTime and flipDelay are adjusted upward. + // V1 correction uses prior displayed state. When the previous displayed + // screen time is greater than the current screen time, the current frame's + // screen time and flip delay are adjusted upward. QpcConverter qpc(10'000'000, 0); SwapChainCoreState chain{}; @@ -2688,36 +1856,30 @@ TEST_CLASS(ComputeMetricsForPresentTests) // First's screen time is 5'500'000 first.displayed.PushBack({ FrameType::Application, 5'500'000 }); - // Second frame (next displayed) + // Second frame. FrameData second{}; second.presentStartTime = 5'000'000; second.timeInPresent = 40'000; second.readyTime = 5'100'000; second.flipDelay = 100'000; // Original flip delay for second frame second.finalState = PresentResult::Presented; - // Second's raw screen time is 5'000'000, which is EARLIER than first's (5'500'000) - // This triggers NV2 adjustment + // Second's raw screen time is earlier than first's, so V1 correction applies. second.displayed.PushBack({ FrameType::Application, 5'000'000 }); - // Process first frame with second as nextDisplayed - auto resultsFirst = ComputeMetricsForPresent(qpc, first, &second, chain); + auto resultsFirst = ComputeMetricsForPresent(qpc, first, chain); Assert::AreEqual(size_t(1), resultsFirst.size()); - // Now process second frame (which should have been adjusted by NV2) - FrameData third{}; - third.finalState = PresentResult::Presented; - third.displayed.PushBack({ FrameType::Application, 6'000'000 }); - - auto resultsSecond = ComputeMetricsForPresent(qpc, second, &third, chain); + // Now process second frame, which should be adjusted by the V1 NV correction. + auto resultsSecond = ComputeMetricsForPresent(qpc, second, chain); Assert::AreEqual(size_t(1), resultsSecond.size()); const auto& secondMetrics = resultsSecond[0].metrics; - // NV2 adjustment: second's screenTime should be raised to first's screenTime - // when first.screenTime (5'500'000) > second.screenTime (5'000'000) + // V1 adjustment: second's screenTime should be raised to first's screenTime + // when first.screenTime (5'500'000) > second.screenTime (5'000'000). Assert::AreEqual(uint64_t(5'500'000), secondMetrics.screenTimeQpc, - L"NV2 should adjust second's screenTime to first's screenTime (5'500'000)"); + L"V1 should adjust second's screenTime to first's screenTime (5'500'000)"); - // NV2 adjustment: second's flipDelay should be increased by the difference + // V1 adjustment: second's flipDelay should be increased by the difference. // effectiveSecondFlipDelay = 100'000 + (5'500'000 - 5'000'000) = 100'000 + 500'000 = 600'000 uint64_t expectedEffectiveFlipDelaySecond = 100'000 + (5'500'000 - 5'000'000); double expectedMsFlipDelaySecond = qpc.DurationMilliSeconds(expectedEffectiveFlipDelaySecond); @@ -2726,7 +1888,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) L"msFlipDelay should be set for displayed frame"); if (HasMetricValue(secondMetrics.msFlipDelay)) { AssertAreEqualWithinTolerance(expectedMsFlipDelaySecond, secondMetrics.msFlipDelay, 0.0001, - L"NV2 should adjust second's flipDelay to account for screenTime catch-up"); + L"V1 should adjust second's flipDelay to account for screenTime catch-up"); } } @@ -2756,7 +1918,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 5'000'000 }); - auto results = ComputeMetricsForPresent(qpc, current, &next, chain); + auto results = ComputeMetricsForPresent(qpc, current, chain); Assert::AreEqual(size_t(1), results.size()); const auto& metrics = results[0].metrics; @@ -2801,23 +1963,19 @@ TEST_CLASS(ComputeMetricsForPresentTests) second.readyTime = 5'100'000; second.flipDelay = 50'000; second.finalState = PresentResult::Presented; - // Second screen time is equal to first (5'000'000), so NV2 should NOT adjust + // Second screen time is equal to first (5'000'000), so V1 should not adjust. second.displayed.PushBack({ FrameType::Application, 5'000'000 }); - auto resultsFirst = ComputeMetricsForPresent(qpc, first, &second, chain); + auto resultsFirst = ComputeMetricsForPresent(qpc, first, chain); Assert::AreEqual(size_t(1), resultsFirst.size()); - FrameData third{}; - third.finalState = PresentResult::Presented; - third.displayed.PushBack({ FrameType::Application, 6'000'000 }); - - auto resultsSecond = ComputeMetricsForPresent(qpc, second, &third, chain); + auto resultsSecond = ComputeMetricsForPresent(qpc, second, chain); Assert::AreEqual(size_t(1), resultsSecond.size()); const auto& secondMetrics = resultsSecond[0].metrics; - // NV2 should NOT adjust: second's screenTime should remain at 5'000'000 + // V1 should not adjust: second's screenTime should remain at 5'000'000. Assert::AreEqual(uint64_t(5'000'000), secondMetrics.screenTimeQpc, - L"NV2: when second.screenTime >= first.screenTime, no adjustment should occur"); + L"V1: when second.screenTime >= first.screenTime, no adjustment should occur"); // flipDelay should remain at original 50'000 double expectedMsFlipDelay = qpc.DurationMilliSeconds(50'000); @@ -2826,7 +1984,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) L"msFlipDelay should be set for displayed frame"); if (HasMetricValue(secondMetrics.msFlipDelay)) { AssertAreEqualWithinTolerance(expectedMsFlipDelay, secondMetrics.msFlipDelay, 0.0001, - L"NV2: when no collapse, flipDelay should remain unchanged"); + L"V1: when no collapse, flipDelay should remain unchanged"); } } @@ -2851,7 +2009,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) current.finalState = PresentResult::Presented; current.displayed.PushBack({ FrameType::Application, 5'000'000 }); - auto results = ComputeMetricsForPresent(qpc, current, nullptr, chain, MetricsVersion::V1); + auto results = ComputeMetricsForPresent(qpc, current, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -2882,8 +2040,8 @@ TEST_CLASS(ComputeMetricsForPresentTests) { // Scenario: Single displayed frame with well-separated timestamps. // cpuStart = 1'000'000, screenTime = 2'000'000 - // QPC frequency 10 MHz → 1'000'000 ticks = 0.1 ms - // Expected: msDisplayLatency ≈ 0.1 ms + // QPC frequency 10 MHz -> 1'000'000 ticks = 0.1 ms + // Expected: msDisplayLatency approx 0.1 ms QpcConverter qpc(10'000'000, 0); SwapChainCoreState chain{}; @@ -2905,7 +2063,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) priorApp.timeInPresent = 200'000; chain.lastAppPresent = priorApp; - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -2940,7 +2098,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) priorApp.timeInPresent = 300'000; chain.lastAppPresent = priorApp; - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -2964,7 +2122,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) frame.finalState = PresentResult::Presented; // No displayed entries - auto results = ComputeMetricsForPresent(qpc, frame, nullptr, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -2975,7 +2133,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) { // Scenario: No prior chain history; cpuStart defaults to 0. // cpuStart = 0, screenTime = 3'000'000 - // Expected: msDisplayLatency ≈ 0.3 ms + // Expected: msDisplayLatency approx 0.3 ms QpcConverter qpc(10'000'000, 0); SwapChainCoreState chain{}; @@ -2992,7 +2150,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 3'500'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -3011,7 +2169,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) // Scenario: Single displayed frame with GPU ready time before screen time. // readyTime = 1'500'000, screenTime = 2'000'000 // QPC 10 MHz: 500'000 ticks = 0.05 ms - // Expected: msReadyTimeToDisplayLatency ≈ 0.05 ms + // Expected: msReadyTimeToDisplayLatency approx 0.05 ms QpcConverter qpc(10'000'000, 0); SwapChainCoreState chain{}; @@ -3027,7 +2185,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 2'500'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -3057,7 +2215,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 2'500'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -3080,7 +2238,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) frame.finalState = PresentResult::Presented; // No displayed entries - auto results = ComputeMetricsForPresent(qpc, frame, nullptr, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -3091,7 +2249,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) { // Scenario: Ready time is set to a non-zero value before screen time. // readyTime = 70'000, screenTime = 2'000'000 - // Expected: msReadyTimeToDisplayLatency ≈ 0.193 ms + // Expected: msReadyTimeToDisplayLatency approx 0.193 ms QpcConverter qpc(10'000'000, 0); SwapChainCoreState chain{}; @@ -3107,7 +2265,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 2'500'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -3123,112 +2281,134 @@ TEST_CLASS(ComputeMetricsForPresentTests) public: TEST_METHOD(DisplayLatency_MultipleDisplays_EachComputesIndependently) { - // Scenario: Single FrameData with 3 displayed instances (e.g., frame interpolation). - // Display 0: screenTime = 2'000'000 - // Display 1: screenTime = 2'100'000 - // Display 2: screenTime = 2'200'000 - // cpuStart = 1'000'000 (same for all) - // QPC 10 MHz - // Expected: - // Metrics[0].msDisplayLatency ≈ 0.1 ms - // Metrics[1].msDisplayLatency ≈ 0.11 ms - // Metrics[2].msDisplayLatency ≈ 0.12 ms - + // cpuStart = 800'000 + 200'000 = 1'000'000 for each display instance on the multi-flip present. QpcConverter qpc(10'000'000, 0); - SwapChainCoreState chain{}; + UnifiedSwapChain swapChain{}; + + FrameData bootstrap{}; + bootstrap.presentStartTime = 800'000; + bootstrap.timeInPresent = 200'000; + bootstrap.readyTime = 900'000; + bootstrap.finalState = PresentResult::Presented; + bootstrap.displayed.PushBack({ FrameType::Application, 1'500'000 }); + + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); FrameData frame{}; frame.presentStartTime = 1'000'000; frame.timeInPresent = 50'000; frame.readyTime = 1'100'000; frame.finalState = PresentResult::Presented; + frame.appSimStartTime = 1'000'000; frame.displayed.PushBack({ FrameType::Application, 2'000'000 }); frame.displayed.PushBack({ FrameType::Repeated, 2'100'000 }); frame.displayed.PushBack({ FrameType::Repeated, 2'200'000 }); - FrameData next{}; - next.finalState = PresentResult::Presented; - next.displayed.PushBack({ FrameType::Application, 2'500'000 }); - - FrameData priorApp{}; - priorApp.presentStartTime = 800'000; - priorApp.timeInPresent = 200'000; - chain.lastAppPresent = priorApp; - - // First call without next: process [0..1] - auto results1 = ComputeMetricsForPresent(qpc, frame, nullptr, chain); - Assert::AreEqual(size_t(2), results1.size()); + auto originRows = swapChain.ProcessPresent(qpc, std::move(frame)); + Assert::AreEqual(size_t(1), originRows.size()); - // First display: cpuStart = 1'000'000, screenTime = 2'000'000 → 0.1 ms double expected0 = qpc.DeltaUnsignedMilliSeconds(1'000'000, 2'000'000); - AssertAreEqualWithinTolerance(expected0, results1[0].metrics.msDisplayLatency, 0.0001); - - // Second display: cpuStart = 1'000'000, screenTime = 2'100'000 → 0.11 ms - double expected1 = qpc.DeltaUnsignedMilliSeconds(1'000'000, 2'100'000); - AssertAreEqualWithinTolerance(expected1, results1[1].metrics.msDisplayLatency, 0.0001); - - // Second call with next: process [2] - auto results2 = ComputeMetricsForPresent(qpc, frame, &next, chain); - Assert::AreEqual(size_t(1), results2.size()); - - // Third display: cpuStart = 1'000'000, screenTime = 2'200'000 → 0.12 ms - double expected2 = qpc.DeltaUnsignedMilliSeconds(1'000'000, 2'200'000); - AssertAreEqualWithinTolerance(expected2, results2[0].metrics.msDisplayLatency, 0.0001); + AssertAreEqualWithinTolerance(expected0, originRows[0].computed.metrics.msDisplayLatency, 0.0001); + + FrameData closingApp{}; + closingApp.presentStartTime = 2'300'000; + closingApp.timeInPresent = 40'000; + closingApp.readyTime = 2'350'000; + closingApp.finalState = PresentResult::Presented; + closingApp.appSimStartTime = 1'050'000; + closingApp.displayed.PushBack({ FrameType::Application, 2'500'000 }); + + // Rep2100000 and Rep2200000 already know their own nextScreenTime from sibling + // display entries in the earlier present, so once closingApp closes the interval + // they publish immediately here; closingApp itself is the sole entry in its + // present and still needs lookahead. + auto closedIntervalRows = swapChain.ProcessPresent(qpc, std::move(closingApp)); + Assert::AreEqual(size_t(2), closedIntervalRows.size()); + + double expected1 = qpc.DeltaUnsignedMilliSeconds(1'050'000, 2'100'000); + AssertAreEqualWithinTolerance(expected1, closedIntervalRows[0].computed.metrics.msDisplayLatency, 0.0001); + + double expected2 = qpc.DeltaUnsignedMilliSeconds(1'050'000, 2'200'000); + AssertAreEqualWithinTolerance(expected2, closedIntervalRows[1].computed.metrics.msDisplayLatency, 0.0001); + + FrameData lookahead{}; + lookahead.presentStartTime = 2'400'000; + lookahead.timeInPresent = 30'000; + lookahead.readyTime = 2'450'000; + lookahead.finalState = PresentResult::Presented; + lookahead.appSimStartTime = 1'070'000; + lookahead.displayed.PushBack({ FrameType::Application, 2'600'000 }); + + auto intervalRows = swapChain.ProcessPresent(qpc, std::move(lookahead)); + Assert::AreEqual(size_t(1), intervalRows.size()); + Assert::AreEqual((int)FrameType::Application, (int)intervalRows[0].computed.metrics.frameType); } TEST_METHOD(ReadyTimeToDisplay_MultipleDisplays_IndependentDeltas) { - // Scenario: Multiple displays, each with different screenTime, but same readyTime. - // readyTime = 1'500'000 (single value for the frame) - // Display 0: screenTime = 2'000'000 - // Display 1: screenTime = 2'100'000 - // Display 2: screenTime = 2'200'000 - // QPC 10 MHz - // Expected: - // Metrics[0].msReadyTimeToDisplayLatency ≈ 0.05 ms (500'000 ticks) - // Metrics[1].msReadyTimeToDisplayLatency ≈ 0.06 ms (600'000 ticks) - // Metrics[2].msReadyTimeToDisplayLatency ≈ 0.07 ms (700'000 ticks) - + // Same readyTime on the multi-flip present; each display row uses its own screenTime. QpcConverter qpc(10'000'000, 0); - SwapChainCoreState chain{}; + UnifiedSwapChain swapChain{}; + + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; + + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); FrameData frame{}; frame.presentStartTime = 1'000'000; frame.timeInPresent = 50'000; frame.readyTime = 1'500'000; frame.finalState = PresentResult::Presented; + frame.appSimStartTime = 1'000'000; frame.displayed.PushBack({ FrameType::Application, 2'000'000 }); frame.displayed.PushBack({ FrameType::Intel_XEFG, 2'100'000 }); frame.displayed.PushBack({ FrameType::Intel_XEFG, 2'200'000 }); - FrameData next{}; - next.finalState = PresentResult::Presented; - next.displayed.PushBack({ FrameType::Application, 2'500'000 }); - - FrameData priorApp{}; - priorApp.presentStartTime = 800'000; - priorApp.timeInPresent = 200'000; - chain.lastAppPresent = priorApp; - - auto results = ComputeMetricsForPresent(qpc, frame, nullptr, chain); - Assert::AreEqual(size_t(2), results.size()); + auto originRows = swapChain.ProcessPresent(qpc, std::move(frame)); + Assert::AreEqual(size_t(1), originRows.size()); - // Display 0: readyTime = 1'500'000, screenTime = 2'000'000 → 0.05 ms double expected0 = qpc.DeltaUnsignedMilliSeconds(1'500'000, 2'000'000); - Assert::IsTrue(HasMetricValue(results[0].metrics.msReadyTimeToDisplayLatency)); - AssertAreEqualWithinTolerance(expected0, results[0].metrics.msReadyTimeToDisplayLatency, 0.0001); + Assert::IsTrue(HasMetricValue(originRows[0].computed.metrics.msReadyTimeToDisplayLatency)); + AssertAreEqualWithinTolerance(expected0, originRows[0].computed.metrics.msReadyTimeToDisplayLatency, 0.0001); + + FrameData closingApp{}; + closingApp.presentStartTime = 2'300'000; + closingApp.timeInPresent = 40'000; + closingApp.readyTime = 2'350'000; + closingApp.finalState = PresentResult::Presented; + closingApp.appSimStartTime = 1'050'000; + closingApp.displayed.PushBack({ FrameType::Application, 2'500'000 }); + + // Rep2100000 and Rep2200000 already know their own nextScreenTime from sibling + // display entries in the earlier present, so once closingApp closes the interval + // they publish immediately here; closingApp itself is the sole entry in its + // present and still needs lookahead. + auto closedIntervalRows = swapChain.ProcessPresent(qpc, std::move(closingApp)); + Assert::AreEqual(size_t(2), closedIntervalRows.size()); - // Display 0: readyTime = 1'500'000, screenTime = 2'000'000 → 0.05 ms double expected1 = qpc.DeltaUnsignedMilliSeconds(1'500'000, 2'100'000); - Assert::IsTrue(HasMetricValue(results[1].metrics.msReadyTimeToDisplayLatency)); - AssertAreEqualWithinTolerance(expected1, results[1].metrics.msReadyTimeToDisplayLatency, 0.0001); + Assert::IsTrue(HasMetricValue(closedIntervalRows[0].computed.metrics.msReadyTimeToDisplayLatency)); + AssertAreEqualWithinTolerance(expected1, closedIntervalRows[0].computed.metrics.msReadyTimeToDisplayLatency, 0.0001); - // Second call with next: process [2] - auto results2 = ComputeMetricsForPresent(qpc, frame, &next, chain); - Assert::AreEqual(size_t(1), results2.size()); double expected2 = qpc.DeltaUnsignedMilliSeconds(1'500'000, 2'200'000); - Assert::IsTrue(HasMetricValue(results2[0].metrics.msReadyTimeToDisplayLatency)); - AssertAreEqualWithinTolerance(expected2, results2[0].metrics.msReadyTimeToDisplayLatency, 0.0001); + Assert::IsTrue(HasMetricValue(closedIntervalRows[1].computed.metrics.msReadyTimeToDisplayLatency)); + AssertAreEqualWithinTolerance(expected2, closedIntervalRows[1].computed.metrics.msReadyTimeToDisplayLatency, 0.0001); + + FrameData lookahead{}; + lookahead.presentStartTime = 2'400'000; + lookahead.timeInPresent = 30'000; + lookahead.readyTime = 2'450'000; + lookahead.finalState = PresentResult::Presented; + lookahead.appSimStartTime = 1'070'000; + lookahead.displayed.PushBack({ FrameType::Application, 2'600'000 }); + + auto intervalRows = swapChain.ProcessPresent(qpc, std::move(lookahead)); + Assert::AreEqual(size_t(1), intervalRows.size()); + Assert::AreEqual((int)FrameType::Application, (int)intervalRows[0].computed.metrics.frameType); } }; @@ -3243,8 +2423,8 @@ TEST_CLASS(ComputeMetricsForPresentTests) // Display 1: screenTime = 3'000'000 // QPC 10 MHz // Assume the unified code applies NV adjustment - // Expected: msDisplayLatency ≈ 0.295 ms (using adjusted screenTime 4'000'000 − 1'050'000) - // Expected: msFlipDelay ≈ 0.103 ms (original 30'000 + adjustment) + // Expected: msDisplayLatency approx 0.295 ms (using adjusted screenTime 4'000'000 - 1'050'000) + // Expected: msFlipDelay approx 0.103 ms (original 30'000 + adjustment) QpcConverter qpc(10'000'000, 0); SwapChainCoreState chain{}; @@ -3266,20 +2446,13 @@ TEST_CLASS(ComputeMetricsForPresentTests) next1.finalState = PresentResult::Presented; next1.displayed.PushBack({ FrameType::Application, 3'000'000 }); - FrameData next2{}; - next2.presentStartTime = 3'000'000; - next2.timeInPresent = 50'000; - next2.readyTime = 3'100'000; - next2.finalState = PresentResult::Presented; - next2.displayed.PushBack({ FrameType::Application, 5'000'000 }); - // Set up chain with prior app present to establish cpuStart FrameData priorApp{}; priorApp.presentStartTime = 800'000; priorApp.timeInPresent = 200'000; chain.lastAppPresent = priorApp; - auto results1 = ComputeMetricsForPresent(qpc, frame, &next1, chain); + auto results1 = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results1.size()); // No adjust of first frame msDisplayLatency = 4'000'000 - 1'000'000 = 3'000'000 ticks = 0.3 ms @@ -3289,8 +2462,8 @@ TEST_CLASS(ComputeMetricsForPresentTests) Assert::IsTrue(HasMetricValue(results1[0].metrics.msFlipDelay)); AssertAreEqualWithinTolerance(expectedFlipDelay, results1[0].metrics.msFlipDelay, 0.0001); - auto results2 = ComputeMetricsForPresent(qpc, next1, &next2, chain); - Assert::AreEqual(size_t(1), results1.size()); + auto results2 = ComputeMetricsForPresent(qpc, next1, chain); + Assert::AreEqual(size_t(1), results2.size()); // After NV adjustment: screenTime = 4'000'000 -> set from NV FlipDelay adjustment // msDisplayLatency = 4'000'000 - 1'050'000 = 2'950'000 ticks = 0.295 ms @@ -3308,7 +2481,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) // Adjusted screenTime = 4'000'000 // readyTime = 2'100'000 // QPC 10 MHz - // Expected: msReadyTimeToDisplayLatency ≈ 0.19 ms (4'000'000 - 2'100'000 = 1'900'000 ticks) + // Expected: msReadyTimeToDisplayLatency approx 0.19 ms (4'000'000 - 2'100'000 = 1'900'000 ticks) QpcConverter qpc(10'000'000, 0); SwapChainCoreState chain{}; @@ -3330,20 +2503,13 @@ TEST_CLASS(ComputeMetricsForPresentTests) next1.finalState = PresentResult::Presented; next1.displayed.PushBack({ FrameType::Application, 3'000'000 }); - FrameData next2{}; - next2.presentStartTime = 3'000'000; - next2.timeInPresent = 50'000; - next2.readyTime = 3'100'000; - next2.finalState = PresentResult::Presented; - next2.displayed.PushBack({ FrameType::Application, 5'000'000 }); - // Set up chain with prior app present to establish cpuStart FrameData priorApp{}; priorApp.presentStartTime = 800'000; priorApp.timeInPresent = 200'000; chain.lastAppPresent = priorApp; - auto results1 = ComputeMetricsForPresent(qpc, frame, &next1, chain); + auto results1 = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results1.size()); // No adjust of first frame ready time = 4'000'000 - 1'100'000 = 2'900'000 ticks = 0.29 ms @@ -3351,8 +2517,8 @@ TEST_CLASS(ComputeMetricsForPresentTests) Assert::IsTrue(HasMetricValue(results1[0].metrics.msReadyTimeToDisplayLatency)); AssertAreEqualWithinTolerance(expectedReadyTimeLatency, results1[0].metrics.msReadyTimeToDisplayLatency, 0.0001); - auto results2 = ComputeMetricsForPresent(qpc, next1, &next2, chain); - Assert::AreEqual(size_t(1), results1.size()); + auto results2 = ComputeMetricsForPresent(qpc, next1, chain); + Assert::AreEqual(size_t(1), results2.size()); // After NV adjustment: ready time latency = 4'000'000 - 2'100'000 = 1'900'000 ticks = 0.19 ms double expectedReadyTimeLatency2 = qpc.DeltaUnsignedMilliSeconds(2'100'000, 4'000'000); @@ -3390,7 +2556,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) priorApp.timeInPresent = 500'000; chain.lastAppPresent = priorApp; - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -3421,7 +2587,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 2'500'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -3452,7 +2618,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 2'500'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -3470,7 +2636,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) // appPropagatedTimeInPresent = 150'000 // screenTime = 2'000'000 // Expected cpuStart = 800'000 + 150'000 = 950'000 - // Expected msDisplayLatency ≈ 0.1055 ms (2'000'000 − 950'000 = 1'050'000 ticks) + // Expected msDisplayLatency approx 0.1055 ms (2'000'000 - 950'000 = 1'050'000 ticks) QpcConverter qpc(10'000'000, 0); SwapChainCoreState chain{}; @@ -3493,7 +2659,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) priorApp.appPropagatedTimeInPresent = 150'000; chain.lastAppPresent = priorApp; - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -3535,7 +2701,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 1'400'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -3580,7 +2746,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 2'200'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -3616,7 +2782,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 6'300'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -3661,7 +2827,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 1'700'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -3704,7 +2870,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 2'000'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -3747,7 +2913,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 2'000'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -3790,7 +2956,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 1'800'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -3829,7 +2995,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 1'800'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -3843,6 +3009,27 @@ TEST_CLASS(ComputeMetricsForPresentTests) } }; + TEST_CLASS(UnifiedSwapChainPresentTimeTests) + { + public: + TEST_METHOD(NoPresentHistory_UsesCurrentPresentFallback) + { + UnifiedSwapChain swapChain{}; + + Assert::AreEqual(uint64_t(12345), swapChain.GetLastPresentQpcOr(12345)); + } + + TEST_METHOD(PresentHistory_UsesLastPresentTimestamp) + { + UnifiedSwapChain swapChain{}; + FrameData lastPresent{}; + lastPresent.presentStartTime = 67890; + swapChain.swapChain.lastPresent = lastPresent; + + Assert::AreEqual(uint64_t(67890), swapChain.GetLastPresentQpcOr(12345)); + } + }; + // ============================================================================ // GROUP B: CORE GPU METRICS (NON-VIDEO) // ============================================================================ @@ -3884,7 +3071,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 1'800'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -3929,7 +3116,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 1'800'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -3973,7 +3160,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 2'800'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -4016,7 +3203,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 1'700'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -4060,7 +3247,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 1'700'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -4105,7 +3292,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 1'700'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -4116,7 +3303,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) TEST_METHOD(GPUWait_BasicCase_BusyLessThanTotal) { - // gpuStartTime = 1'000'000, readyTime = 1'600'000 → total = 600'000 + // gpuStartTime = 1'000'000, readyTime = 1'600'000 -> total = 600'000 // gpuDuration (busy) = 500'000 // msGPUWait should be 100'000 ticks = 10 ms QpcConverter qpc(10'000'000, 0); @@ -4150,7 +3337,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 2'100'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -4165,7 +3352,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) TEST_METHOD(GPUWait_BusyEqualsTotal) { - // gpuStartTime = 1'000'000, readyTime = 1'600'000 → total = 600'000 + // gpuStartTime = 1'000'000, readyTime = 1'600'000 -> total = 600'000 // gpuDuration = 600'000 (fully busy) // msGPUWait should be 0 QpcConverter qpc(10'000'000, 0); @@ -4199,7 +3386,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 2'100'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -4211,7 +3398,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) TEST_METHOD(GPUWait_BusyGreaterThanTotal) { - // gpuStartTime = 1'000'000, readyTime = 1'600'000 → total = 600'000 + // gpuStartTime = 1'000'000, readyTime = 1'600'000 -> total = 600'000 // gpuDuration = 700'000 (impossible, but defensive) // msGPUWait should clamp to 0 QpcConverter qpc(10'000'000, 0); @@ -4245,7 +3432,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 2'100'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -4257,7 +3444,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) TEST_METHOD(GPUWait_WithAppPropagatedData) { - // appPropagatedGPUStartTime = 1'000'000, appPropagatedReadyTime = 1'550'000 → total = 550'000 + // appPropagatedGPUStartTime = 1'000'000, appPropagatedReadyTime = 1'550'000 -> total = 550'000 // appPropagatedGPUDuration = 450'000 // msGPUWait should be 100'000 ticks = 10 ms QpcConverter qpc(10'000'000, 0); @@ -4294,7 +3481,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 2'100'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -4347,7 +3534,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 1'700'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -4392,7 +3579,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 1'700'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -4438,7 +3625,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 1'700'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -4484,7 +3671,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 1'700'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -4495,48 +3682,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) AssertAreEqualWithinTolerance(expectedVideoBusy, m.msVideoBusy, 0.0001); } - TEST_METHOD(InterFrameEventMetrics_PsoCompile) - { - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState chain{}; - - FrameData priorApp{}; - priorApp.presentStartTime = 100'000; - priorApp.timeInPresent = 10'000; - priorApp.readyTime = 120'000; - priorApp.finalState = PresentResult::Presented; - priorApp.displayed.PushBack({ FrameType::Application, 130'000 }); - chain.lastAppPresent = priorApp; - - FrameData frame{}; - frame.presentStartTime = 200'000; - frame.timeInPresent = 10'000; - frame.readyTime = 220'000; - frame.finalState = PresentResult::Presented; - frame.displayed.PushBack({ FrameType::Application, 230'000 }); - frame.interFrameEventStats[(size_t)InterPresentActivity::Kind::D3D12PsoCompile] = { - 2, - 500'000, - 500'000, - }; - frame.interFrameEventWindowQpc = 1'000'000; - - FrameData next{}; - next.presentStartTime = 300'000; - next.timeInPresent = 10'000; - next.readyTime = 320'000; - next.finalState = PresentResult::Presented; - next.displayed.PushBack({ FrameType::Application, 330'000 }); - - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); - Assert::AreEqual(size_t(1), results.size()); - const auto& m = results[0].metrics; - Assert::AreEqual((uint64_t)2, m.psoCompileCount); - AssertAreEqualWithinTolerance(qpc.DurationMilliSeconds(500'000), m.msPsoCompileTime, 0.0001); - AssertAreEqualWithinTolerance(50.0, m.psoCompileBusyPercent, 0.0001); - } - - TEST_METHOD(VideoBusy_LargerThanGPUBusy) + TEST_METHOD(VideoBusy_LargerThanGPUBusy) { // msGPUBusy = 30 ms (computed from gpuDuration) // msVideoBusy = 50 ms (computed from gpuVideoDuration) @@ -4573,7 +3719,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 1'700'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -4616,7 +3762,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 1'700'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -4631,42 +3777,42 @@ TEST_CLASS(ComputeMetricsForPresentTests) TEST_METHOD(GeneratedFrameMetrics_NotAppFrame_CPUGPUMetricsZero) { - // Frame with only Repeated display type (not Application) QpcConverter qpc(10'000'000, 0); - SwapChainCoreState chain{}; + UnifiedSwapChain swapChain{}; - FrameData priorApp = MakeFrame( - PresentResult::Presented, - /*presentStartTime*/ 800'000, - /*timeInPresent*/ 200'000, - /*readyTime*/ 1'000'000, - /*displayed*/{}); + FrameData bootstrap{}; + bootstrap.presentStartTime = 800'000; + bootstrap.timeInPresent = 200'000; + bootstrap.readyTime = 1'000'000; + bootstrap.finalState = PresentResult::Presented; - chain.lastAppPresent = priorApp; + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); - // Current frame with only Repeated display (not Application) - FrameData frame{}; - frame.presentStartTime = 1'100'000; - frame.timeInPresent = 100'000; - frame.readyTime = 1'200'000; - frame.gpuStartTime = 1'150'000; - frame.gpuDuration = 200'000; - frame.finalState = PresentResult::Presented; - frame.displayed.PushBack({ FrameType::Repeated, 1'300'000 }); + FrameData generated{}; + generated.presentStartTime = 1'100'000; + generated.timeInPresent = 100'000; + generated.readyTime = 1'200'000; + generated.gpuStartTime = 1'150'000; + generated.gpuDuration = 200'000; + generated.finalState = PresentResult::Presented; + generated.displayed.PushBack({ FrameType::Repeated, 1'300'000 }); - // Next displayed frame (required to process current frame's display) - FrameData next{}; - next.presentStartTime = 1'500'000; - next.timeInPresent = 50'000; - next.readyTime = 1'600'000; - next.finalState = PresentResult::Presented; - next.displayed.PushBack({ FrameType::Application, 1'700'000 }); + auto heldGenerated = swapChain.ProcessPresent(qpc, std::move(generated)); + Assert::AreEqual(size_t(0), heldGenerated.size()); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); - Assert::AreEqual(size_t(1), results.size()); + FrameData firstAppAnchor{}; + firstAppAnchor.presentStartTime = 1'500'000; + firstAppAnchor.timeInPresent = 50'000; + firstAppAnchor.readyTime = 1'600'000; + firstAppAnchor.finalState = PresentResult::Presented; + firstAppAnchor.appSimStartTime = 1'000'000; + firstAppAnchor.displayed.PushBack({ FrameType::Application, 1'700'000 }); - const auto& m = results[0].metrics; - // Generated frames have no CPU attribution. + auto publishedRows = swapChain.ProcessPresent(qpc, std::move(firstAppAnchor)); + Assert::AreEqual(size_t(1), publishedRows.size()); + Assert::AreEqual((int)FrameType::Repeated, (int)publishedRows[0].computed.metrics.frameType); + + const auto& m = publishedRows[0].computed.metrics; Assert::IsTrue(IsMissingFrameMetricValue(m.msCPUBusy)); Assert::IsTrue(IsMissingFrameMetricValue(m.msCPUWait)); AssertAreEqualWithinTolerance(0.0, m.msGPULatency, 0.0001); @@ -4674,6 +3820,71 @@ TEST_CLASS(ComputeMetricsForPresentTests) AssertAreEqualWithinTolerance(0.0, m.msGPUWait, 0.0001); } + TEST_METHOD(NotDisplayedGeneratedFrame_PreservesFrameTypeAndDoesNotUpdateAppHistory) + { + QpcConverter qpc(10'000'000, 0); + UnifiedSwapChain swapChain{}; + + FrameData bootstrap{}; + bootstrap.presentStartTime = 800'000; + bootstrap.timeInPresent = 200'000; + bootstrap.readyTime = 1'000'000; + bootstrap.finalState = PresentResult::Presented; + bootstrap.displayed.PushBack({ FrameType::Application, 1'100'000 }); + + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); + Assert::IsTrue(swapChain.swapChain.lastAppPresent.has_value()); + Assert::AreEqual(uint64_t(800'000), swapChain.swapChain.lastAppPresent->presentStartTime); + + FrameData generated{}; + generated.presentStartTime = 1'100'000; + generated.timeInPresent = 100'000; + generated.readyTime = 1'200'000; + generated.gpuStartTime = 1'150'000; + generated.gpuDuration = 200'000; + generated.finalState = PresentResult::Discarded; + generated.displayed.PushBack({ FrameType::Repeated, 1'300'000 }); + + auto rows = swapChain.ProcessPresent(qpc, std::move(generated)); + Assert::AreEqual(size_t(1), rows.size()); + Assert::AreEqual((int)FrameType::Repeated, (int)rows[0].computed.metrics.frameType); + Assert::AreEqual(uint64_t(0), rows[0].computed.metrics.screenTimeQpc); + Assert::IsTrue(IsMissingFrameMetricValue(rows[0].computed.metrics.msCPUBusy)); + Assert::IsTrue(swapChain.swapChain.lastAppPresent.has_value()); + Assert::AreEqual(uint64_t(800'000), swapChain.swapChain.lastAppPresent->presentStartTime); + Assert::IsTrue(swapChain.swapChain.lastPresent.has_value()); + Assert::AreEqual(uint64_t(1'100'000), swapChain.swapChain.lastPresent->presentStartTime); + } + + TEST_METHOD(NotDisplayedPresent_MultipleFrameTypeEntries_ProducesRowPerEntry) + { + QpcConverter qpc(10'000'000, 0); + UnifiedSwapChain swapChain{}; + + FrameData bootstrap{}; + bootstrap.presentStartTime = 800'000; + bootstrap.timeInPresent = 200'000; + bootstrap.readyTime = 1'000'000; + bootstrap.finalState = PresentResult::Presented; + + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); + + FrameData dropped{}; + dropped.presentStartTime = 1'100'000; + dropped.timeInPresent = 100'000; + dropped.readyTime = 1'200'000; + dropped.finalState = PresentResult::Discarded; + dropped.displayed.PushBack({ FrameType::Intel_XEFG, 1'300'000 }); + dropped.displayed.PushBack({ FrameType::Application, 1'400'000 }); + + auto rows = swapChain.ProcessPresent(qpc, std::move(dropped)); + Assert::AreEqual(size_t(2), rows.size()); + Assert::AreEqual((int)FrameType::Intel_XEFG, (int)rows[0].computed.metrics.frameType); + Assert::AreEqual((int)FrameType::Application, (int)rows[1].computed.metrics.frameType); + Assert::AreEqual(uint64_t(0), rows[0].computed.metrics.screenTimeQpc); + Assert::AreEqual(uint64_t(0), rows[1].computed.metrics.screenTimeQpc); + } + TEST_METHOD(NotDisplayedFrame_AppFrameMetrics_Computed) { // Frame is not displayed (Discarded) but has CPU/GPU work @@ -4699,7 +3910,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) frame.finalState = PresentResult::Discarded; // No displayed entries - auto results = ComputeMetricsForPresent(qpc, frame, nullptr, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -4743,7 +3954,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 1'800'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -4784,7 +3995,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 1'800'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -4815,7 +4026,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 6'300'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -4846,7 +4057,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 6'300'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); // Verify chain state was updated @@ -4894,7 +4105,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 1'800'000'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -4940,7 +4151,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) next.finalState = PresentResult::Presented; next.displayed.PushBack({ FrameType::Application, 1'700'000 }); - auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + auto results = ComputeMetricsForPresent(qpc, frame, chain); Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; @@ -4975,7 +4186,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) nextA.finalState = PresentResult::Presented; nextA.displayed.PushBack({ FrameType::Application, 2'200'000 }); - auto resultsA = ComputeMetricsForPresent(qpc, frameA, &nextA, chain); + auto resultsA = ComputeMetricsForPresent(qpc, frameA, chain); Assert::AreEqual(size_t(1), resultsA.size()); AssertAreEqualWithinTolerance(0.0, resultsA[0].metrics.msVideoBusy, 0.0001); @@ -4997,2791 +4208,1253 @@ TEST_CLASS(ComputeMetricsForPresentTests) nextB.finalState = PresentResult::Presented; nextB.displayed.PushBack({ FrameType::Application, 3'200'000 }); - auto resultsB = ComputeMetricsForPresent(qpc, frameB, &nextB, chain); + auto resultsB = ComputeMetricsForPresent(qpc, frameB, chain); Assert::AreEqual(size_t(1), resultsB.size()); double expectedVideoBusy = qpc.DurationMilliSeconds(300'000); AssertAreEqualWithinTolerance(expectedVideoBusy, resultsB[0].metrics.msVideoBusy, 0.0001); } }; - TEST_CLASS(AnimationTime) + TEST_CLASS(AnimationFrameGenerationDesignTests) { public: - // ======================================================================== - // A1: AnimationTime_CpuStart_FirstFrame_ZeroWithoutAppSimStartTime - // ======================================================================== - TEST_METHOD(AnimationTime_CpuStart_FirstFrame_ZeroWithoutAppSimStartTime) - { - // Scenario: - // - SwapChainCoreState starts with CpuStart (default) - // - Current frame: displayed with appSimStartTime == 0 and pclSimStartTime == 0 - // - The existing body keeps CpuStart active when appSimStartTime is missing - // - // Expected Outcome: - // - msAnimationTime = 0.0 in this existing first-frame path - // - firstAppSimStartTime remains 0 in state - // - lastDisplayedSimStartTime remains 0 in state - // - Source remains CpuStart - // - This is legacy behavior and does not describe the new missing-source policy + static std::vector Process( + QpcConverter& qpc, + UnifiedSwapChain& swapChain, + FrameData frame) + { + std::vector metrics; + auto rows = swapChain.ProcessPresent(qpc, std::move(frame)); + metrics.reserve(rows.size()); + for (const auto& row : rows) { + metrics.push_back(row.computed.metrics); + } + return metrics; + } - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - // state.animationErrorSource defaults to CpuStart + TEST_METHOD(AppOnly_EmitsOriginThenIntervalOnLookahead) + { + QpcConverter qpc(1000, 0); + UnifiedSwapChain swapChain{}; - // Verify initial state - Assert::AreEqual(uint64_t(0), state.firstAppSimStartTime); - Assert::AreEqual(uint64_t(0), state.lastDisplayedSimStartTime); + (void)Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1, 1, 1, {})); + (void)Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 900, 100, 900, + { { FrameType::Application, 100 } }, 1000)); - // Create a displayed app frame WITHOUT appSimStartTime - FrameData frame{}; - frame.presentStartTime = 1'000'000; - frame.timeInPresent = 500; - frame.readyTime = 1'500'000; - frame.appSimStartTime = 0; // No app instrumentation yet - frame.pclSimStartTime = 0; - frame.finalState = PresentResult::Presented; - frame.displayed.PushBack({ FrameType::Application, 1'000'000 }); + Assert::AreEqual(size_t(1), Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1000, 16, 1000, + { { FrameType::Application, 116 } }, 1016)).size()); - // Verify frame setup - Assert::AreEqual(uint64_t(0), frame.appSimStartTime); - Assert::AreEqual(size_t(1), frame.displayed.Size()); + auto rows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1100, 16, 1100, + { { FrameType::Application, 132 } }, 1032)); - // Create nextDisplayed to allow processing - FrameData next{}; - next.presentStartTime = 2'000'000; - next.timeInPresent = 400; - next.readyTime = 2'500'000; - next.finalState = PresentResult::Presented; - next.displayed.PushBack({ FrameType::Application, 2'000'000 }); + Assert::AreEqual(size_t(1), rows.size()); + Assert::AreEqual((int)FrameType::Application, (int)rows[0].frameType); + Assert::AreEqual(16.0, rows[0].msAnimationTime, 0.0001); + Assert::AreEqual(0.0, rows[0].msAnimationError, 0.0001); + Assert::AreEqual(16.0, rows[0].msDisplayedTime, 0.0001); + } - // Action: Compute metrics for this frame - auto metricsVector = ComputeMetricsForPresent(qpc, frame, &next, state); + TEST_METHOD(AppOnly_CpuStartUsesBootstrapThenIngestPreviousWhenOriginHeld) + { + // CpuStart uses the previous present end as the simulation start. The + // first app anchor uses the bootstrap seed as its predecessor, then the + // next app anchor must use ingest-time previous present history while + // the prior rows are still held. + QpcConverter qpc(1000, 0); + UnifiedSwapChain swapChain{}; - // Assert: Should have one computed metric - Assert::AreEqual(size_t(1), metricsVector.size()); + (void)Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1, 1, 1, {})); + (void)Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 900, 100, 900, + { { FrameType::Application, 100 } })); - const ComputedMetrics& result = metricsVector[0]; + auto firstIntervalRows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1000, 16, 1000, + { { FrameType::Application, 116 } })); + Assert::AreEqual(size_t(1), firstIntervalRows.size()); + Assert::AreEqual(uint64_t(100), firstIntervalRows[0].screenTimeQpc); + Assert::AreEqual(0.0, firstIntervalRows[0].msAnimationTime, 0.0001); + Assert::IsFalse(HasMetricValue(firstIntervalRows[0].msAnimationError)); - // Assert: msAnimationTime should have value a value of zero - Assert::IsTrue(HasMetricValue(result.metrics.msAnimationTime), - L"msAnimationTime should have a value of zero"); - AssertAreEqualWithinTolerance(double(0.0), result.metrics.msAnimationTime, 0.0001); + auto bootstrapIntervalRows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1016, 16, 1016, + { { FrameType::Application, 132 } })); + Assert::AreEqual(size_t(1), bootstrapIntervalRows.size()); + Assert::AreEqual(uint64_t(116), bootstrapIntervalRows[0].screenTimeQpc); + Assert::AreEqual(998.0, bootstrapIntervalRows[0].msAnimationTime, 0.0001); + Assert::AreEqual(982.0, bootstrapIntervalRows[0].msAnimationError, 0.0001); - // Assert: firstAppSimStartTime in state should remain 0 - Assert::AreEqual(uint64_t(0), state.firstAppSimStartTime, - L"State: firstAppSimStartTime should remain 0 (no valid app sim start time detected)"); + auto rows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1032, 16, 1032, + { { FrameType::Application, 148 } })); - // Assert: lastDisplayedSimStartTime should remain 0 - Assert::AreEqual(uint64_t(0), state.lastDisplayedSimStartTime, - L"State: lastDisplayedSimStartTime should remain 0 (no valid app sim start time detected)"); + Assert::AreEqual(size_t(1), rows.size()); + Assert::AreEqual(uint64_t(132), rows[0].screenTimeQpc); + Assert::AreEqual(1014.0, rows[0].msAnimationTime, 0.0001); + Assert::AreEqual(0.0, rows[0].msAnimationError, 0.0001); } - // ======================================================================== - // A2: AnimationTime_AppProvider_TransitionFrame_FirstValidAppSimStart - // ======================================================================== - TEST_METHOD(AnimationTime_AppProvider_TransitionFrame_FirstValidAppSimStart) - { - // Scenario: - // - Start with CpuStart source (default) - // - Frame 1: App data arrives, triggers source switch to AppProvider - // - Expected: msAnimationTime = 0 (first frame with valid app sim start) - // - // Expected Outcome: - // - msAnimationTime = 0 (first frame with app instrumentation) - // - firstAppSimStartTime is set to qpc(100) - // - Source switches to AppProvider after UpdateChain - - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - // state.animationErrorSource defaults to CpuStart - - // Create a displayed app frame WITH appSimStartTime for the first time - FrameData frame{}; - frame.presentStartTime = 1'000'000; - frame.timeInPresent = 500; - frame.readyTime = 1'500'000; - frame.appSimStartTime = 100; // First valid app sim start - frame.pclSimStartTime = 0; - frame.finalState = PresentResult::Presented; - frame.displayed.PushBack({ FrameType::Application, 1'000'000 }); - - // Create nextDisplayed - FrameData frame2{}; - frame2.presentStartTime = 2'000'000; - frame2.timeInPresent = 400; - frame2.readyTime = 2'500'000; - frame2.finalState = PresentResult::Presented; - frame2.displayed.PushBack({ FrameType::Application, 2'000'000 }); - - // Action: Compute metrics - auto metricsVector = ComputeMetricsForPresent(qpc, frame, &frame2, state); - - // Assert - Assert::AreEqual(size_t(1), metricsVector.size()); - const ComputedMetrics& result = metricsVector[0]; - - Assert::IsTrue(HasMetricValue(result.metrics.msAnimationTime), - L"msAnimationTime should have a value"); - AssertAreEqualWithinTolerance(double(0.0), result.metrics.msAnimationTime, 0.0001, - L"msAnimationTime should be 0 on first frame with CpuStart source and no history"); - - // Assert: State should be updated - Assert::AreEqual(uint64_t(100), state.firstAppSimStartTime, - L"State: firstAppSimStartTime should be set to first valid app sim start"); - Assert::AreEqual(uint64_t(100), state.lastDisplayedSimStartTime, - L"State: lastDisplayedSimStartTime should be set to current frame's app sim start"); - Assert::IsTrue( - state.animationErrorSource == AnimationErrorSource::AppProvider, - L"Animation source should transition to AppProvider after first appSimStartTime."); - } - // ======================================================================== - // A3: AnimationTime_AppProvider_SecondFrame_IncrementsCorrectly - // ======================================================================== - TEST_METHOD(AnimationTime_AppProvider_SecondFrame_IncrementsCorrectly) + TEST_METHOD(TraceStart_PreFirstAppAnchorGeneratedRows_EmitWithAnimationNotSet) { - // Scenario: - // - Frame 1: First app data, establishes firstAppSimStartTime = 100, switches to AppProvider - // - Frame 2: appSimStartTime = 150 (50 QPC ticks later) - // - QPC frequency = 10 MHz → 50 ticks = 5 µs = 0.005 ms - // - // Expected Outcome: - // - msAnimationTime ≈ 0.005 ms (elapsed sim time from first to current) - - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - // Start with CpuStart, will switch to AppProvider after frame1 + QpcConverter qpc(1000, 0); + UnifiedSwapChain swapChain{}; - // Frame 1: First app data - FrameData frame1{}; - frame1.presentStartTime = 500'000; - frame1.timeInPresent = 300; - frame1.appSimStartTime = 100; - frame1.finalState = PresentResult::Presented; - frame1.displayed.PushBack({ FrameType::Application, 900'000 }); - - FrameData next1{}; - next1.presentStartTime = 1'500'000; - next1.finalState = PresentResult::Presented; - next1.displayed.PushBack({ FrameType::Application, 1'500'000 }); + (void)Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1, 1, 1, {})); - ComputeMetricsForPresent(qpc, frame1, &next1, state); - // After this, source is AppProvider, firstAppSimStartTime = 100 + Assert::AreEqual(size_t(0), Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 800, 1, 800, + { { FrameType::Intel_XEFG, 100 } })).size()); - // Frame 2: Second app frame with incremented sim start - FrameData frame{}; - frame.presentStartTime = 1'000'000; - frame.timeInPresent = 500; - frame.readyTime = 1'500'000; - frame.appSimStartTime = 150; // 50 ticks later - frame.pclSimStartTime = 0; - frame.finalState = PresentResult::Presented; - frame.displayed.PushBack({ FrameType::Application, 1'000'000 }); + auto gen1Rows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 810, 1, 810, + { { FrameType::Intel_XEFG, 108 } })); + Assert::AreEqual(size_t(1), gen1Rows.size()); + Assert::AreEqual((int)FrameType::Intel_XEFG, (int)gen1Rows[0].frameType); + Assert::AreEqual(uint64_t(100), gen1Rows[0].screenTimeQpc); + Assert::AreEqual(8.0, gen1Rows[0].msDisplayedTime, 0.0001); + Assert::IsFalse(HasMetricValue(gen1Rows[0].msAnimationError)); + Assert::IsFalse(HasMetricValue(gen1Rows[0].msAnimationTime)); - // Create nextDisplayed - FrameData next{}; - next.presentStartTime = 2'000'000; - next.timeInPresent = 400; - next.readyTime = 2'500'000; - next.finalState = PresentResult::Presented; - next.displayed.PushBack({ FrameType::Application, 2'000'000 }); + auto gen2Rows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 820, 1, 820, + { { FrameType::Intel_XEFG, 116 } })); + Assert::AreEqual(size_t(1), gen2Rows.size()); + Assert::AreEqual(uint64_t(108), gen2Rows[0].screenTimeQpc); + Assert::IsFalse(HasMetricValue(gen2Rows[0].msAnimationError)); + Assert::IsFalse(HasMetricValue(gen2Rows[0].msAnimationTime)); - // Action: Compute metrics - auto metricsVector = ComputeMetricsForPresent(qpc, frame, &next, state); + auto samePresentRows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 825, 1, 825, + { + { FrameType::Intel_XEFG, 120 }, + { FrameType::Intel_XEFG, 128 }, + })); + Assert::AreEqual(size_t(2), samePresentRows.size()); + Assert::AreEqual(uint64_t(116), samePresentRows[0].screenTimeQpc); + Assert::AreEqual(uint64_t(120), samePresentRows[1].screenTimeQpc); + Assert::IsFalse(HasMetricValue(samePresentRows[0].msAnimationError)); + Assert::IsFalse(HasMetricValue(samePresentRows[1].msAnimationError)); + + auto afterFirstAnchorRows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 900, 1, 900, + { { FrameType::Application, 136 } }, 1000)); + Assert::AreEqual(size_t(1), afterFirstAnchorRows.size()); + Assert::AreEqual((int)FrameType::Intel_XEFG, (int)afterFirstAnchorRows[0].frameType); + Assert::AreEqual(uint64_t(128), afterFirstAnchorRows[0].screenTimeQpc); + Assert::AreEqual(8.0, afterFirstAnchorRows[0].msDisplayedTime, 0.0001); + Assert::IsFalse(HasMetricValue(afterFirstAnchorRows[0].msAnimationError)); + Assert::IsFalse(HasMetricValue(afterFirstAnchorRows[0].msAnimationTime)); + + auto originAnchorRows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 910, 1, 910, + { { FrameType::Intel_XEFG, 144 } })); + Assert::AreEqual(size_t(1), originAnchorRows.size()); + Assert::AreEqual((int)FrameType::Application, (int)originAnchorRows[0].frameType); + Assert::AreEqual(uint64_t(136), originAnchorRows[0].screenTimeQpc); + Assert::AreEqual(8.0, originAnchorRows[0].msDisplayedTime, 0.0001); + Assert::AreEqual(0.0, originAnchorRows[0].msAnimationTime, 0.0001); + Assert::IsFalse(HasMetricValue(originAnchorRows[0].msAnimationError)); + } + + TEST_METHOD(TraceStart_FirstCpuStartAnchor_WithOnlyGeneratedHistory_PublishesMissingAnimationTime) + { + QpcConverter qpc(1000, 0); + UnifiedSwapChain swapChain{}; + + FrameData bootstrap{}; + bootstrap.presentStartTime = 790; + bootstrap.timeInPresent = 10; + bootstrap.readyTime = 790; + bootstrap.finalState = PresentResult::Presented; + bootstrap.displayed.PushBack({ FrameType::Intel_XEFG, 100 }); + (void)Process(qpc, swapChain, std::move(bootstrap)); + + FrameData firstGenerated{}; + firstGenerated.presentStartTime = 800; + firstGenerated.timeInPresent = 10; + firstGenerated.readyTime = 800; + firstGenerated.finalState = PresentResult::Presented; + firstGenerated.displayed.PushBack({ FrameType::Intel_XEFG, 108 }); + Assert::AreEqual(size_t(0), Process(qpc, swapChain, std::move(firstGenerated)).size()); + + FrameData secondGenerated{}; + secondGenerated.presentStartTime = 810; + secondGenerated.timeInPresent = 10; + secondGenerated.readyTime = 810; + secondGenerated.finalState = PresentResult::Presented; + secondGenerated.displayed.PushBack({ FrameType::Intel_XEFG, 116 }); + auto preAnchorRows = Process(qpc, swapChain, std::move(secondGenerated)); + Assert::AreEqual(size_t(1), preAnchorRows.size()); + Assert::IsFalse(HasMetricValue(preAnchorRows[0].msAnimationTime)); + + FrameData firstAppAnchor{}; + firstAppAnchor.presentStartTime = 900; + firstAppAnchor.timeInPresent = 10; + firstAppAnchor.readyTime = 900; + firstAppAnchor.finalState = PresentResult::Presented; + firstAppAnchor.displayed.PushBack({ FrameType::Application, 124 }); + auto finalPreAnchorRows = Process(qpc, swapChain, std::move(firstAppAnchor)); + Assert::AreEqual(size_t(1), finalPreAnchorRows.size()); + Assert::IsFalse(HasMetricValue(finalPreAnchorRows[0].msAnimationTime)); + + FrameData lookahead{}; + lookahead.presentStartTime = 910; + lookahead.timeInPresent = 10; + lookahead.readyTime = 910; + lookahead.finalState = PresentResult::Presented; + lookahead.displayed.PushBack({ FrameType::Intel_XEFG, 132 }); + auto originRows = Process(qpc, swapChain, std::move(lookahead)); + Assert::AreEqual(size_t(1), originRows.size()); + Assert::AreEqual((int)FrameType::Application, (int)originRows[0].frameType); + Assert::AreEqual(uint64_t(124), originRows[0].screenTimeQpc); + + // The origin app anchor has no provider or PC-latency timestamp, and + // the only prior observed frames are generated. Do not derive a CPU + // simulation start from generated-frame history. + Assert::IsFalse(HasMetricValue(originRows[0].msAnimationTime)); + Assert::IsFalse(HasMetricValue(originRows[0].msAnimationError)); + } + + TEST_METHOD(FirstAppAnchor_PublishesTimelineOriginWithAnimationTimeFromSession) + { + QpcConverter qpc(1000, 0); + UnifiedSwapChain swapChain{}; + + (void)Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1, 1, 1, {})); + + Assert::AreEqual(size_t(0), Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 800, 1, 800, + { { FrameType::Application, 100 } }, 1000)).size()); + + auto heldThenReleased = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 810, 1, 810, + { { FrameType::Intel_XEFG, 108 } })); + Assert::AreEqual(size_t(1), heldThenReleased.size()); + Assert::AreEqual((int)FrameType::Application, (int)heldThenReleased[0].frameType); + Assert::AreEqual(uint64_t(100), heldThenReleased[0].screenTimeQpc); + Assert::AreEqual(8.0, heldThenReleased[0].msDisplayedTime, 0.0001); + Assert::AreEqual(0.0, heldThenReleased[0].msAnimationTime, 0.0001); + Assert::IsFalse(HasMetricValue(heldThenReleased[0].msAnimationError)); + + UnifiedSwapChain swapChain2{}; + (void)Process(qpc, swapChain2, MakeFrame(PresentResult::Presented, 1, 1, 1, {})); + + auto samePresentLookahead = Process(qpc, swapChain2, MakeFrame(PresentResult::Presented, 800, 1, 800, + { + { FrameType::Application, 100 }, + { FrameType::Intel_XEFG, 108 }, + }, + 1000)); + Assert::AreEqual(size_t(1), samePresentLookahead.size()); + Assert::AreEqual((int)FrameType::Application, (int)samePresentLookahead[0].frameType); + Assert::AreEqual(8.0, samePresentLookahead[0].msDisplayedTime, 0.0001); + Assert::AreEqual(0.0, samePresentLookahead[0].msAnimationTime, 0.0001); + Assert::IsFalse(HasMetricValue(samePresentLookahead[0].msAnimationError)); + } + + TEST_METHOD(PublishPolicy_FirstPresentOnSwapChainProducesNoOutput) + { + QpcConverter qpc(1000, 0); + UnifiedSwapChain swapChain{}; + + const auto firstPresentFrame = MakeFrame(PresentResult::Presented, 900, 100, 900, + { { FrameType::Application, 100 }, { FrameType::Intel_XEFG, 108 } }, + 1000); + + UnifiedSwapChain ingestAndApplyOnly{}; + auto readyOnFirstPresent = ingestAndApplyOnly.EnqueueReadyDisplayRows(qpc, firstPresentFrame); + Assert::IsTrue(readyOnFirstPresent.size() > 0); + + auto publishedOnFirstPresent = swapChain.ProcessPresent(qpc, firstPresentFrame); + Assert::AreEqual(size_t(0), publishedOnFirstPresent.size()); + + auto publishedOnSecondPresent = swapChain.ProcessPresent(qpc, MakeFrame(PresentResult::Presented, 1000, 1, 1000, + { { FrameType::Application, 116 }, { FrameType::Intel_XEFG, 124 } }, + 1016)); + Assert::AreEqual(size_t(1), publishedOnSecondPresent.size()); + Assert::AreEqual((int)FrameType::Application, (int)publishedOnSecondPresent[0].computed.metrics.frameType); + } + + TEST_METHOD(GenAndApp_OnSecondPresent_GenIncludedInFirstClosedInterval) + { + // Seed present does not ingest; app+gen on first process emits only app row (gen held); + // second process emits gen row with animation time from first present, and app row with animation + // time from second present. This verifies that the gen frame is included in the first closed + // interval, even though it was not ingested at the time of the first present. + QpcConverter qpc(1000, 0); + UnifiedSwapChain swapChain{}; + + (void)Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1, 1, 1, {})); + + Assert::AreEqual(size_t(1), Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 900, 100, 900, + { { FrameType::Application, 100 }, { FrameType::Intel_XEFG, 108 } }, + 1000)).size()); + + auto rows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1000, 1, 1000, + { { FrameType::Application, 116 }, { FrameType::Intel_XEFG, 124 } }, + 1016)); + + Assert::AreEqual(size_t(2), rows.size()); + Assert::AreEqual((int)FrameType::Intel_XEFG, (int)rows[0].frameType); + Assert::AreEqual((int)FrameType::Application, (int)rows[1].frameType); + Assert::AreEqual(uint64_t(108), rows[0].screenTimeQpc); + Assert::AreEqual(uint64_t(116), rows[1].screenTimeQpc); + Assert::AreEqual(8.0, rows[0].msAnimationTime, 0.0001); + Assert::AreEqual(16.0, rows[1].msAnimationTime, 0.0001); + Assert::AreEqual(0.0, rows[0].msAnimationError, 0.0001); + Assert::AreEqual(0.0, rows[1].msAnimationError, 0.0001); + Assert::AreEqual(8.0, rows[1].msDisplayedTime, 0.0001); + } + + TEST_METHOD(PresentFrameTypeInfo_GeneratedOnlyPresentsWaitForNextAppAndLookahead) + { + QpcConverter qpc(1000, 0); + UnifiedSwapChain swapChain{}; + + (void)Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1, 1, 1, {})); + (void)Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 900, 100, 900, + { { FrameType::Application, 100 } }, 1000)); + + Assert::AreEqual(size_t(1), Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1000, 1, 1000, + { { FrameType::Intel_XEFG, 106 } })).size()); + Assert::AreEqual(size_t(0), Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1001, 1, 1001, + { { FrameType::Intel_XEFG, 112 } })).size()); + Assert::AreEqual(size_t(0), Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1002, 1, 1002, + { { FrameType::Intel_XEFG, 118 } })).size()); + + // AppB closes the interval [Gen106, Gen112, Gen118, AppB]. Each generated row + // already learned its own nextScreenTime from the next display instance as it + // arrived, so they publish immediately with resolved animation metrics. AppB is + // the only entry in its own present, so it still needs lookahead. + auto closedIntervalRows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1100, 1, 1100, + { { FrameType::Application, 124 } }, 1024)); + + Assert::AreEqual(size_t(3), closedIntervalRows.size()); + Assert::AreEqual((int)FrameType::Intel_XEFG, (int)closedIntervalRows[0].frameType); + Assert::AreEqual((int)FrameType::Intel_XEFG, (int)closedIntervalRows[1].frameType); + Assert::AreEqual((int)FrameType::Intel_XEFG, (int)closedIntervalRows[2].frameType); + Assert::AreEqual(uint64_t(106), closedIntervalRows[0].screenTimeQpc); + Assert::AreEqual(uint64_t(112), closedIntervalRows[1].screenTimeQpc); + Assert::AreEqual(uint64_t(118), closedIntervalRows[2].screenTimeQpc); + Assert::AreEqual(6.0, closedIntervalRows[0].msAnimationTime, 0.0001); + Assert::AreEqual(12.0, closedIntervalRows[1].msAnimationTime, 0.0001); + Assert::AreEqual(18.0, closedIntervalRows[2].msAnimationTime, 0.0001); + + // AppB still needs lookahead from a later displayed frame before it can publish. + auto rows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1101, 1, 1101, + { { FrameType::Intel_XEFG, 132 } })); + + Assert::AreEqual(size_t(1), rows.size()); + Assert::AreEqual((int)FrameType::Application, (int)rows[0].frameType); + Assert::AreEqual(uint64_t(124), rows[0].screenTimeQpc); + Assert::AreEqual(24.0, rows[0].msAnimationTime, 0.0001); + Assert::AreEqual(8.0, rows[0].msDisplayedTime, 0.0001); + } - Assert::AreEqual(size_t(1), metricsVector.size()); - const ComputedMetrics& result = metricsVector[0]; + TEST_METHOD(FlipFrameTypeInfo_AppInMiddle_OnlyClosedIntervalRowsEmit) + { + QpcConverter qpc(1000, 0); + UnifiedSwapChain swapChain{}; + + (void)Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1, 1, 1, {})); + (void)Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 900, 100, 900, + { + { FrameType::Intel_XEFG, 90 }, + { FrameType::Application, 100 }, + { FrameType::Intel_XEFG, 108 }, + }, + 1000)); - // Assert: msAnimationTime should be 0.005 ms - Assert::IsTrue(HasMetricValue(result.metrics.msAnimationTime)); - double expectedMs = qpc.DeltaUnsignedMilliSeconds(100, 150); - AssertAreEqualWithinTolerance(expectedMs, result.metrics.msAnimationTime, 0.0001, - L"msAnimationTime should reflect elapsed time from first app sim start"); + auto rows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1000, 1, 1000, + { + { FrameType::Intel_XEFG, 116 }, + { FrameType::Application, 124 }, + { FrameType::Intel_XEFG, 132 }, + }, + 1024)); + + Assert::AreEqual(size_t(3), rows.size()); + Assert::AreEqual(uint64_t(108), rows[0].screenTimeQpc); + Assert::AreEqual(uint64_t(116), rows[1].screenTimeQpc); + Assert::AreEqual(uint64_t(124), rows[2].screenTimeQpc); + Assert::AreEqual(8.0, rows[0].msAnimationTime, 0.0001); + Assert::AreEqual(16.0, rows[1].msAnimationTime, 0.0001); + Assert::AreEqual(24.0, rows[2].msAnimationTime, 0.0001); + Assert::AreEqual(8.0, rows[2].msDisplayedTime, 0.0001); + } + + TEST_METHOD(FlipFrameTypeInfo_RepeatedAppInMiddle_GeneratedRowsIncludedInInterval) + { + // Regression: SanitizeDisplayedRepeatedPresents was collapsing + // [Repeated, Application, Repeated] -> [Application] because the loop + // removed rep+app then app+rep in two consecutive passes. Sequences with + // two or more Repeated entries are AMD AFMF frame-generation shapes and + // must not be sanitized. + QpcConverter qpc(1000, 0); + UnifiedSwapChain swapChain{}; + + (void)Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1, 1, 1, {})); + (void)Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 900, 100, 900, + { + { FrameType::Repeated, 90 }, + { FrameType::Application, 100 }, + { FrameType::Repeated, 108 }, + }, + 1000)); - // Assert: firstAppSimStartTime unchanged - Assert::AreEqual(uint64_t(100), state.firstAppSimStartTime, - L"firstAppSimStartTime should remain at first value"); + auto rows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1000, 1, 1000, + { + { FrameType::Repeated, 116 }, + { FrameType::Application, 124 }, + { FrameType::Repeated, 132 }, + }, + 1024)); - // Assert: lastDisplayedSimStartTime updated - Assert::AreEqual(uint64_t(150), state.lastDisplayedSimStartTime, - L"lastDisplayedSimStartTime should be updated to current frame's app sim start"); + Assert::AreEqual(size_t(3), rows.size()); + Assert::AreEqual((int)FrameType::Repeated, (int)rows[0].frameType); + Assert::AreEqual((int)FrameType::Repeated, (int)rows[1].frameType); + Assert::AreEqual((int)FrameType::Application, (int)rows[2].frameType); + Assert::AreEqual(uint64_t(108), rows[0].screenTimeQpc); + Assert::AreEqual(uint64_t(116), rows[1].screenTimeQpc); + Assert::AreEqual(uint64_t(124), rows[2].screenTimeQpc); + Assert::AreEqual(8.0, rows[0].msAnimationTime, 0.0001); + Assert::AreEqual(16.0, rows[1].msAnimationTime, 0.0001); + Assert::AreEqual(24.0, rows[2].msAnimationTime, 0.0001); + Assert::AreEqual(8.0, rows[2].msDisplayedTime, 0.0001); } - // ======================================================================== - // A4: AnimationTime_AppProvider_ThreeFrames_CumulativeElapsedTime - // ======================================================================== - TEST_METHOD(AnimationTime_AppProvider_ThreeFrames_CumulativeElapsedTime) + + TEST_METHOD(CpuStartFallback_UsesPreviousPresentEndWhenProviderAndPclAreUnavailable) { - // Scenario: - // - Start with CpuStart as the animation source. - // - Frame 1: first displayed app frame with appSimStartTime = 100. - // This SEEDS animation state (firstAppSimStartTime), but - // does not yet emit msAnimationTime. - // - Frame 2: displayed app frame with appSimStartTime = 150. - // - Frame 3: displayed app frame with appSimStartTime = 250. - // - // QPC frequency = 10 MHz. - // - // Expected outcome: - // - firstAppSimStartTime is latched to 100 and never changes. - // - Frame 1: msAnimationTime == nullopt (just seeds state). - // - Frame 2: msAnimationTime = (150 - 100) / 10e6. - // - Frame 3: msAnimationTime = (250 - 100) / 10e6. + // With no app-provider or PC-latency sim start, CpuStart falls back to + // previous present end. The bootstrap seed is a valid first predecessor. + QpcConverter qpc(1000, 0); + UnifiedSwapChain swapChain{}; - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - // animationErrorSource defaults to CpuStart; will transition to AppProvider - // when the first displayed frame with appSimStartTime arrives. - - // -------------------------------------------------------------------- - // Frame 1: first valid app sim start, displayed - // -------------------------------------------------------------------- - FrameData frame1{}; - frame1.presentStartTime = 1'000'000; - frame1.timeInPresent = 500; - frame1.readyTime = 1'500'000; - frame1.appSimStartTime = 100; - frame1.pclSimStartTime = 0; - frame1.finalState = PresentResult::Presented; - frame1.displayed.PushBack({ FrameType::Application, 1'000'000 }); + (void)Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1, 1, 1, {})); + (void)Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 900, 100, 900, + { { FrameType::Application, 100 } })); - FrameData next1{}; - next1.presentStartTime = 2'000'000; - next1.timeInPresent = 400; - next1.readyTime = 2'500'000; - next1.finalState = PresentResult::Presented; - next1.displayed.PushBack({ FrameType::Application, 2'000'000 }); - - auto metrics1 = ComputeMetricsForPresent(qpc, frame1, &next1, state); - Assert::AreEqual(size_t(1), metrics1.size()); - - Assert::IsTrue( - HasMetricValue(metrics1[0].metrics.msAnimationTime), - L"First AppProvider frame should seed firstAppSimStartTime and animation time should be zero"); - AssertAreEqualWithinTolerance(double(0.0), metrics1[0].metrics.msAnimationTime, 0.0001, - L"msAnimationTime should be 0 on first frame with CpuStart source and no history"); - - // After processing frame1, the chain should have latched sim start and - // switched to AppProvider. - Assert::AreEqual(uint64_t(100), state.firstAppSimStartTime); - Assert::AreEqual(uint64_t(100), state.lastDisplayedSimStartTime); - Assert::IsTrue( - state.animationErrorSource == AnimationErrorSource::AppProvider, - L"Animation source should transition to AppProvider after first appSimStartTime."); - - // -------------------------------------------------------------------- - // Frame 2: second displayed app frame - // -------------------------------------------------------------------- - FrameData frame2{}; - frame2.presentStartTime = 3'000'000; - frame2.timeInPresent = 500; - frame2.readyTime = 3'500'000; - frame2.appSimStartTime = 150; - frame2.pclSimStartTime = 0; - frame2.finalState = PresentResult::Presented; - frame2.displayed.PushBack({ FrameType::Application, 3'000'000 }); - - FrameData next2{}; - next2.presentStartTime = 4'000'000; - next2.timeInPresent = 400; - next2.readyTime = 4'500'000; - next2.finalState = PresentResult::Presented; - next2.displayed.PushBack({ FrameType::Application, 4'000'000 }); - - auto metrics2 = ComputeMetricsForPresent(qpc, frame2, &next2, state); - Assert::AreEqual(size_t(1), metrics2.size()); - Assert::IsTrue( - HasMetricValue(metrics2[0].metrics.msAnimationTime), - L"Second displayed app frame should report msAnimationTime."); - - double expected2 = qpc.DeltaUnsignedMilliSeconds(100, 150); - AssertAreEqualWithinTolerance( - expected2, - metrics2[0].metrics.msAnimationTime, - 0.0001, - L"Frame 2's msAnimationTime should be relative to firstAppSimStartTime (100 → 150)."); - - // firstAppSimStartTime should stay anchored at 100 - Assert::AreEqual(uint64_t(100), state.firstAppSimStartTime, L"firstAppSimStartTime should not change."); - // lastDisplayedSimStartTime should now reflect frame2 - Assert::AreEqual(uint64_t(150), state.lastDisplayedSimStartTime); - - // -------------------------------------------------------------------- - // Frame 3: third displayed app frame - // -------------------------------------------------------------------- - FrameData frame3{}; - frame3.presentStartTime = 5'000'000; - frame3.timeInPresent = 500; - frame3.readyTime = 5'500'000; - frame3.appSimStartTime = 250; - frame3.pclSimStartTime = 0; - frame3.finalState = PresentResult::Presented; - frame3.displayed.PushBack({ FrameType::Application, 5'000'000 }); - - FrameData next3{}; - next3.presentStartTime = 6'000'000; - next3.timeInPresent = 400; - next3.readyTime = 6'500'000; - next3.finalState = PresentResult::Presented; - next3.displayed.PushBack({ FrameType::Application, 6'000'000 }); - - auto metrics3 = ComputeMetricsForPresent(qpc, frame3, &next3, state); - Assert::AreEqual(size_t(1), metrics3.size()); - Assert::IsTrue( - HasMetricValue(metrics3[0].metrics.msAnimationTime), - L"Third displayed app frame should report msAnimationTime."); - - double expected3 = qpc.DeltaUnsignedMilliSeconds(100, 250); - AssertAreEqualWithinTolerance( - expected3, - metrics3[0].metrics.msAnimationTime, - 0.0001, - L"Frame 3's msAnimationTime should be relative to original firstAppSimStartTime (100 → 250)."); + auto bootstrapIntervalRows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1000, 16, 1000, + { { FrameType::Application, 116 } })); + Assert::AreEqual(size_t(1), bootstrapIntervalRows.size()); + Assert::AreEqual(uint64_t(100), bootstrapIntervalRows[0].screenTimeQpc); + Assert::AreEqual(0.0, bootstrapIntervalRows[0].msAnimationTime, 0.0001); + Assert::IsFalse(HasMetricValue(bootstrapIntervalRows[0].msAnimationError)); - // firstAppSimStartTime remains the original seed - Assert::AreEqual(uint64_t(100), state.firstAppSimStartTime, L"firstAppSimStartTime should remain at 100."); - // lastDisplayedSimStartTime should now reflect frame3 - Assert::AreEqual(uint64_t(250), state.lastDisplayedSimStartTime); + auto rows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1100, 16, 1100, + { { FrameType::Application, 132 } })); + + Assert::AreEqual(size_t(1), rows.size()); + Assert::AreEqual(uint64_t(116), rows[0].screenTimeQpc); + Assert::AreEqual(998.0, rows[0].msAnimationTime, 0.0001); + Assert::AreEqual(982.0, rows[0].msAnimationError, 0.0001); + Assert::AreEqual(16.0, rows[0].msDisplayedTime, 0.0001); } - // ======================================================================== - // A5: AnimationTime_AppProvider_SkippedFrame_StaysConsistent - // ======================================================================== - TEST_METHOD(AnimationTime_AppProvider_SkippedFrame_StaysConsistent) + + TEST_METHOD(SourceTransition_CpuStartToAppProvider_StartsNewTimeline) { - // We want to verify: - // - Animation source is AppProvider (AppSimStartTime-based). - // - A discarded (not displayed) frame with AppSimStartTime: - // * Produces no animation metrics. - // * Does NOT change firstAppSimStartTime / lastDisplayedSimStartTime / - // lastDisplayedAppScreenTime. - // - A later displayed app frame still computes msAnimationTime correctly - // based on the original firstAppSimStartTime. + QpcConverter qpc(1000, 0); + UnifiedSwapChain swapChain{}; - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; + (void)Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1, 1, 1, {})); + (void)Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 900, 100, 900, + { { FrameType::Application, 100 } })); - // Seed state as if we already had one displayed app frame at sim = 100. - state.animationErrorSource = AnimationErrorSource::AppProvider; - state.firstAppSimStartTime = 100; - state.lastDisplayedSimStartTime = 100; - state.lastDisplayedAppScreenTime = 1'000'000; // prior displayed screen time - - // -------------------------------------------------------------------- - // Frame 1: Discarded / not displayed, but with AppSimStartTime = 150 - // -------------------------------------------------------------------- - FrameData frameDropped{}; - frameDropped.presentStartTime = 2'000'000; - frameDropped.timeInPresent = 50'000; - frameDropped.readyTime = 2'050'000; - frameDropped.appSimStartTime = 150; - frameDropped.finalState = PresentResult::Discarded; - // No displayed entries -> not displayed - - auto droppedResults = ComputeMetricsForPresent(qpc, frameDropped, nullptr, state); - Assert::AreEqual(size_t(1), droppedResults.size()); - const auto& droppedMetrics = droppedResults[0].metrics; - - // Discarded / not-displayed frame must NOT produce animation metrics. - Assert::IsFalse(HasMetricValue(droppedMetrics.msAnimationTime), - L"Discarded frame should not have msAnimationTime"); - Assert::IsFalse(HasMetricValue(droppedMetrics.msAnimationError), - L"Discarded frame should not have msAnimationError"); - - // And it must NOT disturb the animation anchors from prior displayed frames. - Assert::AreEqual(uint64_t(100), state.firstAppSimStartTime, - L"firstAppSimStartTime should be unchanged by discarded frame"); - Assert::AreEqual(uint64_t(100), state.lastDisplayedSimStartTime, - L"lastDisplayedSimStartTime should be unchanged by discarded frame"); - Assert::AreEqual(uint64_t(1'000'000), state.lastDisplayedAppScreenTime, - L"lastDisplayedAppScreenTime should be unchanged by discarded frame"); - - // -------------------------------------------------------------------- - // Frame 2: Next displayed app frame with AppSimStartTime = 200 - // -------------------------------------------------------------------- - FrameData frameDisplayed{}; - frameDisplayed.presentStartTime = 3'000'000; - frameDisplayed.timeInPresent = 50'000; - frameDisplayed.readyTime = 3'050'000; - frameDisplayed.appSimStartTime = 200; - frameDisplayed.finalState = PresentResult::Presented; - frameDisplayed.displayed.PushBack({ FrameType::Application, 3'500'000 }); - - // Dummy "next" frame – just to exercise the Case 3 path so that - // UpdateAfterPresent() is called for frameDisplayed. - FrameData frameNext{}; - frameNext.presentStartTime = 4'000'000; - frameNext.timeInPresent = 50'000; - frameNext.readyTime = 4'050'000; - frameNext.appSimStartTime = 250; - frameNext.finalState = PresentResult::Presented; - frameNext.displayed.PushBack({ FrameType::Application, 4'500'000 }); - - auto displayedResults = ComputeMetricsForPresent(qpc, frameDisplayed, &frameNext, state); - Assert::AreEqual(size_t(1), displayedResults.size()); - const auto& displayedMetrics = displayedResults[0].metrics; - - Assert::IsTrue(HasMetricValue(displayedMetrics.msAnimationTime), - L"Displayed app frame should have msAnimationTime"); - - // Animation time should be based purely on the AppProvider sim times: - // firstAppSimStartTime = 100, currentSim = 200. - const double expected = qpc.DeltaUnsignedMilliSeconds(100, 200); - AssertAreEqualWithinTolerance(expected, displayedMetrics.msAnimationTime, 0.0001); - - // After processing a displayed app frame via the Case 3 path, - // state should now reflect that frame as the last displayed. - Assert::AreEqual(uint64_t(100), state.firstAppSimStartTime, - L"firstAppSimStartTime should remain the first displayed AppSimStartTime"); - Assert::AreEqual(uint64_t(200), state.lastDisplayedSimStartTime, - L"lastDisplayedSimStartTime should track the most recent DISPLAYED AppSimStartTime"); - Assert::AreEqual(uint64_t(3'500'000), state.lastDisplayedAppScreenTime, - L"lastDisplayedAppScreenTime should track the most recent displayed screen time"); - } - // ======================================================================== - // A6: AnimationTime_AppProvider_MissingApp_NoPcl_IsMissingAndStateUnchanged - // ======================================================================== - TEST_METHOD(AnimationTime_AppProvider_MissingApp_NoPcl_IsMissingAndStateUnchanged) - { - // Scenario: - // - State already uses AppProvider with existing animation anchors. - // - Current displayed frame has neither appSimStartTime nor pclSimStartTime. - // - AppProvider must remain active; no fallback or demotion is allowed. - // - // Expected Outcome: - // - msAnimationTime is missing (NaN), not 0.0. - // - animationErrorSource remains AppProvider. - // - firstAppSimStartTime, lastDisplayedSimStartTime, and - // lastDisplayedAppScreenTime remain unchanged. + Assert::AreEqual(size_t(1), Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1000, 16, 1000, + { { FrameType::Application, 116 } }, 2000)).size()); - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - state.animationErrorSource = AnimationErrorSource::AppProvider; - state.firstAppSimStartTime = 40'000; - state.lastDisplayedSimStartTime = 41'000; - state.lastDisplayedAppScreenTime = 8'800; + auto transitionRows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1100, 16, 1100, + { { FrameType::Application, 132 } }, 2016)); - FrameData frame{}; - frame.presentStartTime = 1'200'000; - frame.timeInPresent = 100'000; - frame.readyTime = 1'300'000; - frame.appSimStartTime = 0; - frame.pclSimStartTime = 0; - frame.finalState = PresentResult::Presented; - frame.displayed.PushBack({ FrameType::Application, 9'950 }); + Assert::AreEqual(size_t(1), transitionRows.size()); + Assert::IsFalse(HasMetricValue(transitionRows[0].msAnimationError)); + Assert::AreEqual(0.0, transitionRows[0].msAnimationTime, 0.0001); - FrameData next{}; - next.presentStartTime = 1'600'000; - next.timeInPresent = 50'000; - next.readyTime = 1'700'000; - next.finalState = PresentResult::Presented; - next.displayed.PushBack({ FrameType::Application, 10'950 }); + auto rows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1200, 16, 1200, + { { FrameType::Application, 148 } }, 2032)); - auto metricsVector = ComputeMetricsForPresent(qpc, frame, &next, state); + Assert::AreEqual(size_t(1), rows.size()); + Assert::AreEqual(16.0, rows[0].msAnimationTime, 0.0001); + Assert::AreEqual(0.0, rows[0].msAnimationError, 0.0001); + } - Assert::AreEqual(size_t(1), metricsVector.size()); + TEST_METHOD(SourceTransition_PendingGeneratedRowsAndNewTimelineOriginPublish) + { + QpcConverter qpc(1000, 0); + UnifiedSwapChain swapChain{}; - const ComputedMetrics& result = metricsVector[0]; + (void)Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1, 1, 1, {})); + Assert::AreEqual(size_t(0), Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 900, 100, 900, + { { FrameType::Application, 100 } })).size()); - Assert::IsFalse(HasMetricValue(result.metrics.msAnimationTime), - L"msAnimationTime should be missing when AppProvider remains active but no sim start is available."); - Assert::IsTrue(IsMissingFrameMetricValue(result.metrics.msAnimationTime), - L"msAnimationTime should be stored as missing (NaN)"); + (void)Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 910, 1, 910, + { { FrameType::Intel_XEFG, 108 } })); - Assert::IsTrue(state.animationErrorSource == AnimationErrorSource::AppProvider, - L"animationErrorSource should remain AppProvider when no transition is allowed."); - Assert::AreEqual(uint64_t(40'000), state.firstAppSimStartTime, - L"firstAppSimStartTime should remain unchanged."); - Assert::AreEqual(uint64_t(41'000), state.lastDisplayedSimStartTime, - L"lastDisplayedSimStartTime should remain unchanged when the active source is missing."); - Assert::AreEqual(uint64_t(8'800), state.lastDisplayedAppScreenTime, - L"lastDisplayedAppScreenTime should remain unchanged when the active source is missing."); - } - // ======================================================================== - // A7: AnimationTime_AppProvider_MissingApp_WithPcl_IsMissingAndStateUnchanged - // ======================================================================== - TEST_METHOD(AnimationTime_AppProvider_MissingApp_WithPcl_IsMissingAndStateUnchanged) - { - // Scenario: - // - State already uses AppProvider with existing animation anchors. - // - Current displayed frame has pclSimStartTime but no appSimStartTime. - // - AppProvider must remain active; AppProvider -> PCLatency is not allowed. - // - // Expected Outcome: - // - msAnimationTime is missing (NaN), not 0.0. - // - animationErrorSource remains AppProvider, proving no demotion to PCLatency. - // - firstAppSimStartTime, lastDisplayedSimStartTime, and - // lastDisplayedAppScreenTime remain unchanged. + Assert::AreEqual(size_t(0), Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1010, 1, 1010, + { { FrameType::Intel_XEFG, 116 } })).size()); - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - state.animationErrorSource = AnimationErrorSource::AppProvider; - state.firstAppSimStartTime = 40'000; - state.lastDisplayedSimStartTime = 41'000; - state.lastDisplayedAppScreenTime = 8'800; + auto transitionRows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1020, 1, 1020, + { { FrameType::Application, 132 } }, 2000)); - FrameData frame{}; - frame.presentStartTime = 1'200'000; - frame.timeInPresent = 100'000; - frame.readyTime = 1'300'000; - frame.appSimStartTime = 0; - frame.pclSimStartTime = 42'000; - frame.finalState = PresentResult::Presented; - frame.displayed.PushBack({ FrameType::Application, 9'950 }); + Assert::AreEqual(size_t(2), transitionRows.size()); + Assert::AreEqual((int)FrameType::Intel_XEFG, (int)transitionRows[0].frameType); + Assert::AreEqual((int)FrameType::Intel_XEFG, (int)transitionRows[1].frameType); + Assert::AreEqual(uint64_t(108), transitionRows[0].screenTimeQpc); + Assert::AreEqual(uint64_t(116), transitionRows[1].screenTimeQpc); + Assert::AreEqual(8.0, transitionRows[0].msDisplayedTime, 0.0001); + Assert::AreEqual(16.0, transitionRows[1].msDisplayedTime, 0.0001); + Assert::IsFalse(HasMetricValue(transitionRows[0].msAnimationError)); + Assert::IsFalse(HasMetricValue(transitionRows[0].msAnimationTime)); + Assert::IsFalse(HasMetricValue(transitionRows[1].msAnimationError)); + Assert::IsFalse(HasMetricValue(transitionRows[1].msAnimationTime)); - FrameData next{}; - next.presentStartTime = 1'600'000; - next.timeInPresent = 50'000; - next.readyTime = 1'700'000; - next.finalState = PresentResult::Presented; - next.displayed.PushBack({ FrameType::Application, 10'950 }); + auto newOriginRows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1030, 1, 1030, + { { FrameType::Application, 148 } }, 2016)); + Assert::AreEqual(size_t(1), newOriginRows.size()); + Assert::AreEqual((int)FrameType::Application, (int)newOriginRows[0].frameType); + Assert::AreEqual(uint64_t(132), newOriginRows[0].screenTimeQpc); + Assert::AreEqual(16.0, newOriginRows[0].msDisplayedTime, 0.0001); + Assert::AreEqual(0.0, newOriginRows[0].msAnimationTime, 0.0001); + Assert::IsFalse(HasMetricValue(newOriginRows[0].msAnimationError)); + } - auto metricsVector = ComputeMetricsForPresent(qpc, frame, &next, state); + TEST_METHOD(SourceTransition_PclToAppProvider_StartsNewTimeline) + { + QpcConverter qpc(1000, 0); + UnifiedSwapChain swapChain{}; - Assert::AreEqual(size_t(1), metricsVector.size()); + (void)Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1, 1, 1, {})); + (void)Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 900, 100, 900, + { { FrameType::Application, 100 } }, 0, 1000)); - const ComputedMetrics& result = metricsVector[0]; + Assert::AreEqual(size_t(1), Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1000, 16, 1000, + { { FrameType::Application, 116 } }, 2000, 1016)).size()); - Assert::IsFalse(HasMetricValue(result.metrics.msAnimationTime), - L"msAnimationTime should be missing when AppProvider remains active but only pclSimStartTime is present."); - Assert::IsTrue(IsMissingFrameMetricValue(result.metrics.msAnimationTime), - L"msAnimationTime should be stored as missing (NaN) when AppProvider remains active but only pclSimStartTime is present."); + auto transitionRows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1100, 16, 1100, + { { FrameType::Application, 132 } }, 2016)); - Assert::IsTrue(state.animationErrorSource == AnimationErrorSource::AppProvider, - L"animationErrorSource should remain AppProvider when appSimStartTime is missing."); - Assert::IsFalse(state.animationErrorSource == AnimationErrorSource::PCLatency, - L"AppProvider must not transition to PCLatency during normal runtime when only pclSimStartTime is available."); - Assert::AreEqual(uint64_t(40'000), state.firstAppSimStartTime, - L"firstAppSimStartTime should remain unchanged."); - Assert::AreEqual(uint64_t(41'000), state.lastDisplayedSimStartTime, - L"lastDisplayedSimStartTime should remain unchanged instead of switching to the frame's pclSimStartTime."); - Assert::IsTrue(state.lastDisplayedSimStartTime != frame.pclSimStartTime, - L"lastDisplayedSimStartTime should not be replaced by pclSimStartTime when AppProvider stays active."); - Assert::AreEqual(uint64_t(8'800), state.lastDisplayedAppScreenTime, - L"lastDisplayedAppScreenTime should remain unchanged when the active source is missing."); + Assert::AreEqual(size_t(1), transitionRows.size()); + Assert::IsFalse(HasMetricValue(transitionRows[0].msAnimationError)); + Assert::AreEqual(0.0, transitionRows[0].msAnimationTime, 0.0001); } - // ======================================================================== - // B1: AnimationTime_CpuStart_FirstFrame_ZeroWithoutPclSimStartTime - // ======================================================================== - TEST_METHOD(AnimationTime_CpuStart_FirstFrame_ZeroWithoutPclSimStartTime) + + TEST_METHOD(NonMonotonicSimStart_PreservesAccumulatedAnimationTime) { - // Scenario: - // - SwapChainCoreState starts with CpuStart (default) - // - Current frame: displayed with pclSimStartTime == 0 and appSimStartTime == 0 - // - The existing body keeps CpuStart active when pclSimStartTime is missing - // - // Expected Outcome: - // - msAnimationTime = 0.0 in this existing first-frame path - // - firstAppSimStartTime remains 0 in state - // - lastDisplayedSimStartTime remains 0 in state - // - Source remains CpuStart - // - This is legacy behavior and does not describe the new missing-source policy + QpcConverter qpc(1000, 0); + UnifiedSwapChain swapChain{}; - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - // state.animationErrorSource defaults to CpuStart + (void)Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1, 1, 1, {})); + (void)Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 900, 1, 900, + { { FrameType::Application, 100 } }, 1000)); + Assert::AreEqual(size_t(1), Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1000, 1, 1000, + { { FrameType::Application, 116 } }, 1016)).size()); - // Verify initial state - Assert::AreEqual(uint64_t(0), state.firstAppSimStartTime); - Assert::AreEqual(uint64_t(0), state.lastDisplayedSimStartTime); + auto firstRows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1010, 1, 1010, + { { FrameType::Application, 132 } }, 1008)); + Assert::AreEqual(size_t(1), firstRows.size()); + Assert::AreEqual(16.0, firstRows[0].msAnimationTime, 0.0001); - // Create a displayed app frame WITHOUT pclSimStartTime - FrameData frame{}; - frame.presentStartTime = 1'000'000; - frame.timeInPresent = 500; - frame.readyTime = 1'500'000; - frame.appSimStartTime = 0; - frame.pclSimStartTime = 0; // No PC latency instrumentation yet - frame.finalState = PresentResult::Presented; - frame.displayed.PushBack({ FrameType::Application, 1'000'000 }); + auto invalidRows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1020, 1, 1020, + { { FrameType::Application, 148 } }, 1008)); + Assert::AreEqual(size_t(1), invalidRows.size()); + Assert::IsFalse(HasMetricValue(invalidRows[0].msAnimationError)); + Assert::IsFalse(HasMetricValue(invalidRows[0].msAnimationTime)); - // Verify frame setup - Assert::AreEqual(uint64_t(0), frame.pclSimStartTime); - Assert::AreEqual(size_t(1), frame.displayed.Size()); + auto secondInvalidRows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1030, 1, 1030, + { { FrameType::Application, 164 } }, 1024)); + Assert::AreEqual(size_t(1), secondInvalidRows.size()); + Assert::IsFalse(HasMetricValue(secondInvalidRows[0].msAnimationError)); + Assert::IsFalse(HasMetricValue(secondInvalidRows[0].msAnimationTime)); - // Create nextDisplayed to allow processing - FrameData next{}; - next.presentStartTime = 2'000'000; - next.timeInPresent = 400; - next.readyTime = 2'500'000; - next.finalState = PresentResult::Presented; - next.displayed.PushBack({ FrameType::Application, 2'000'000 }); + auto rows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1040, 1, 1040, + { { FrameType::Application, 180 } }, 1040)); + Assert::AreEqual(size_t(1), rows.size()); + Assert::AreEqual(32.0, rows[0].msAnimationTime, 0.0001); + Assert::AreEqual(0.0, rows[0].msAnimationError, 0.0001); + } - // Action: Compute metrics for this frame - auto metricsVector = ComputeMetricsForPresent(qpc, frame, &next, state); + TEST_METHOD(DroppedFrame_BeforeFirstAnchor_DoesNotCorruptPreviousDisplayedScreenTime) + { + // Regression: UpdateAfterReadyDisplayRow zeroed lastDisplayedScreenTime for + // not-displayed rows. A displayed app frame processed as the swap chain seed + // present sets lastDisplayedScreenTime via the display queue; a subsequent + // dropped frame (before the first animation anchor) must not zero it. + QpcConverter qpc(1000, 0); + UnifiedSwapChain swapChain{}; - // Assert: Should have one computed metric - Assert::AreEqual(size_t(1), metricsVector.size()); + (void)Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 900, 100, 900, + { { FrameType::Application, 100 } }, 1000)); - const ComputedMetrics& result = metricsVector[0]; + // Dropped present before the first animation anchor. No anchor exists yet, + // so this is emitted immediately and UpdateAfterReadyDisplayRow is called. + // lastDisplayedScreenTime must remain 100 after this. + (void)Process(qpc, swapChain, MakeFrame(PresentResult::Discarded, 950, 10, 950, {})); - // Assert: msAnimationTime should be zero - Assert::IsTrue(HasMetricValue(result.metrics.msAnimationTime), - L"msAnimationTime should be 0 whentransitioning"); - AssertAreEqualWithinTolerance(double(0.0), result.metrics.msAnimationTime, 0.0001); + // The first queued app anchor. Its previousDisplayedScreenTime must be + // derived from the seed (100), not from the zeroed-out state (0). + Assert::AreEqual(size_t(0), Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1000, 16, 1000, + { { FrameType::Application, 116 } }, 1016)).size()); - // Assert: firstAppSimStartTime in state should remain 0 - Assert::AreEqual(uint64_t(0), state.firstAppSimStartTime, - L"State: firstAppSimStartTime should remain 0 (no valid pcl sim start time detected)"); + auto rows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1100, 16, 1100, + { { FrameType::Application, 132 } }, 1032)); - // Assert: lastDisplayedSimStartTime should remain 0 - Assert::AreEqual(uint64_t(0), state.lastDisplayedSimStartTime, - L"State: lastDisplayedSimStartTime should remain 0 (no valid pcl sim start time detected)"); + Assert::AreEqual(size_t(1), rows.size()); + // simStep = (1032 - 1016) / 1 = 16ms; displayStep = delta(116, 132) = 16ms + Assert::AreEqual(0.0, rows[0].msAnimationError, 0.0001); + Assert::AreEqual(16.0, rows[0].msAnimationTime, 0.0001); } - // ======================================================================== - // B2: AnimationTime_PCLatency_TransitionFrame_FirstValidPclSimStart - // ======================================================================== - TEST_METHOD(AnimationTime_PCLatency_TransitionFrame_FirstValidPclSimStart) + TEST_METHOD(DroppedFrames_DoNotEnterAnimationIntervals) { - // Scenario: - // - Start with CpuStart source (default) - // - Frame 1: PCL data arrives, triggers source switch to PCLatency - // - Expected: msAnimationTime = 0 (first frame with valid pcl sim start) - // - // Expected Outcome: - // - msAnimationTime = 0 (first frame with PCL instrumentation) - // - firstAppSimStartTime is set to qpc(100) - // - Source switches to PCLatency after UpdateChain - - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - // state.animationErrorSource defaults to CpuStart - - // Create a displayed app frame WITH pclSimStartTime for the first time - FrameData frame{}; - frame.presentStartTime = 1'000'000; - frame.timeInPresent = 500; - frame.readyTime = 1'500'000; - frame.appSimStartTime = 0; - frame.pclSimStartTime = 100; // First valid pcl sim start - frame.finalState = PresentResult::Presented; - frame.displayed.PushBack({ FrameType::Application, 1'000'000 }); - - // Create nextDisplayed - FrameData frame2{}; - frame2.presentStartTime = 2'000'000; - frame2.timeInPresent = 400; - frame2.readyTime = 2'500'000; - frame2.finalState = PresentResult::Presented; - frame2.displayed.PushBack({ FrameType::Application, 2'000'000 }); + QpcConverter qpc(1000, 0); + UnifiedSwapChain swapChain{}; - // Action: Compute metrics - auto metricsVector = ComputeMetricsForPresent(qpc, frame, &frame2, state); + (void)Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1, 1, 1, {})); + (void)Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 900, 100, 900, + { { FrameType::Application, 100 } }, 1000)); + Assert::AreEqual(size_t(0), Process(qpc, swapChain, MakeFrame(PresentResult::Discarded, 950, 10, 950, + {}, 1008)).size()); + // The second application frame closes the interval. The origin app frame + // and discarded frame are both publishable now; the second application + // frame still needs display lookahead from the next displayed frame. + auto closedIntervalRows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1000, 16, 1000, + { { FrameType::Application, 116 } }, 1016)); - // Assert - Assert::AreEqual(size_t(1), metricsVector.size()); - const ComputedMetrics& result = metricsVector[0]; + Assert::AreEqual(size_t(2), closedIntervalRows.size()); + Assert::IsFalse(HasMetricValue(closedIntervalRows[1].msAnimationTime)); - // Assert: msAnimationTime should have a value of zero - Assert::IsTrue(HasMetricValue(result.metrics.msAnimationTime), - L"msAnimationTime should have a value"); + // The second application frame now has lookahead and is emitted by itself. + auto rows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1100, 16, 1100, + { { FrameType::Application, 132 } }, 1032)); - // Assert: State should be updated - Assert::AreEqual(uint64_t(100), state.firstAppSimStartTime, - L"State: firstAppSimStartTime should be set to first valid pcl sim start"); - Assert::AreEqual(uint64_t(100), state.lastDisplayedSimStartTime, - L"State: lastDisplayedSimStartTime should be set to current frame's pcl sim start"); - Assert::IsTrue( - state.animationErrorSource == AnimationErrorSource::PCLatency, - L"Animation source should transition to PCLatency after first appSimStartTime."); + Assert::AreEqual(size_t(1), rows.size()); + Assert::AreEqual(16.0, rows[0].msAnimationTime, 0.0001); + Assert::AreEqual(0.0, rows[0].msAnimationError, 0.0001); } - // ======================================================================== - // B3: AnimationTime_PCLatency_SecondFrame_IncrementsCorrectly - // ======================================================================== - TEST_METHOD(AnimationTime_PCLatency_SecondFrame_IncrementsCorrectly) + TEST_METHOD(GeneratedThenNotDisplayedApplication_PublishesInPresentOrder) { - // Scenario: - // - Frame 2: pclSimStartTime = 200 (100 QPC ticks later) - // - QPC frequency = 10 MHz → 100 ticks = 10 µs = 0.01 ms - // - // Expected Outcome: - // - msAnimationTime ≈ 0.01 ms (elapsed sim time from first to current) + QpcConverter qpc(1000, 0); + UnifiedSwapChain swapChain{}; - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - // Start with CpuStart, will switch to PCLatency after frame1 + (void)Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1, 1, 1, {})); + (void)Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 900, 1, 900, + { { FrameType::Application, 100 } }, 1000)); - // Frame 1: First PCL data - FrameData frame1{}; - frame1.presentStartTime = 500'000; - frame1.timeInPresent = 300; - frame1.pclSimStartTime = 100; - frame1.finalState = PresentResult::Presented; - frame1.displayed.PushBack({ FrameType::Application, 900'000 }); + auto originRows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1000, 1, 1000, + { { FrameType::Intel_XEFG, 108 } })); + Assert::AreEqual(size_t(1), originRows.size()); + Assert::AreEqual(uint64_t(900), originRows[0].presentStartQpc); - FrameData next1{}; - next1.presentStartTime = 1'500'000; - next1.finalState = PresentResult::Presented; - next1.displayed.PushBack({ FrameType::Application, 1'500'000 }); + Assert::AreEqual(size_t(0), Process(qpc, swapChain, MakeFrame(PresentResult::Discarded, 1010, 1, 1010, + { { FrameType::Application, 0 } }, 1008)).size()); - ComputeMetricsForPresent(qpc, frame1, &next1, state); - // After this, source is PCLatency, firstAppSimStartTime = 100 + auto rows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1100, 1, 1100, + { { FrameType::Application, 116 } }, 1016)); - // Frame 2: Second PCL frame with incremented sim start - FrameData frame{}; - frame.presentStartTime = 1'000'000; - frame.timeInPresent = 500; - frame.readyTime = 1'500'000; - frame.appSimStartTime = 0; - frame.pclSimStartTime = 200; // 100 ticks later - frame.finalState = PresentResult::Presented; - frame.displayed.PushBack({ FrameType::Application, 1'000'000 }); + Assert::AreEqual(size_t(2), rows.size()); + Assert::AreEqual((int)FrameType::Intel_XEFG, (int)rows[0].frameType); + Assert::AreEqual(uint64_t(1000), rows[0].presentStartQpc); + Assert::AreEqual(100.0, rows[0].msBetweenPresents, 0.0001); + Assert::AreEqual((int)FrameType::Application, (int)rows[1].frameType); + Assert::AreEqual(uint64_t(1010), rows[1].presentStartQpc); + Assert::AreEqual(10.0, rows[1].msBetweenPresents, 0.0001); + } - // Create nextDisplayed - FrameData next{}; - next.presentStartTime = 2'000'000; - next.timeInPresent = 400; - next.readyTime = 2'500'000; - next.finalState = PresentResult::Presented; - next.displayed.PushBack({ FrameType::Application, 2'000'000 }); + TEST_METHOD(NvCollapsedAdjustment_AdjustsNextReadyDisplayRow) + { + QpcConverter qpc(10'000'000, 0); + UnifiedSwapChain swapChain{}; + + (void)swapChain.EnqueueReadyDisplayRows(qpc, MakeFrame(PresentResult::Presented, 1'000'000, 10, 1'000'010, {})); + (void)swapChain.EnqueueReadyDisplayRows(qpc, MakeFrame(PresentResult::Presented, 4'000'000, 50'000, 4'100'000, + { { FrameType::Application, 5'500'000 } }, + 1'000'000, + 0, + 200'000)); - // Action: Compute metrics - auto metricsVector = ComputeMetricsForPresent(qpc, frame, &next, state); + auto releasedOriginRows = swapChain.EnqueueReadyDisplayRows(qpc, MakeFrame(PresentResult::Presented, 5'000'000, 40'000, 5'100'000, + { { FrameType::Application, 5'000'000 } }, + 1'000'100, + 0, + 100'000)); + Assert::AreEqual(size_t(1), releasedOriginRows.size()); + Assert::AreEqual(uint64_t(5'500'000), releasedOriginRows[0].screenTime); - Assert::AreEqual(size_t(1), metricsVector.size()); - const ComputedMetrics& result = metricsVector[0]; + auto rows = swapChain.EnqueueReadyDisplayRows(qpc, MakeFrame(PresentResult::Presented, 6'000'000, 40'000, 6'100'000, + { { FrameType::Application, 6'000'000 } }, + 1'000'200)); - // Assert: msAnimationTime should be 0.01 ms - Assert::IsTrue(HasMetricValue(result.metrics.msAnimationTime)); - double expectedMs = qpc.DeltaUnsignedMilliSeconds(100, 200); - AssertAreEqualWithinTolerance(expectedMs, result.metrics.msAnimationTime, 0.0001, - L"msAnimationTime should reflect elapsed time from first pcl sim start"); + Assert::AreEqual(size_t(1), rows.size()); + Assert::AreEqual(uint64_t(5'500'000), rows[0].screenTime); + Assert::AreEqual(uint64_t(600'000), rows[0].present.flipDelay); + Assert::AreEqual(uint64_t(5'500'000), rows[0].present.displayed[0].second); + } + }; - // Assert: firstAppSimStartTime unchanged - Assert::AreEqual(uint64_t(100), state.firstAppSimStartTime, - L"firstAppSimStartTime should remain at first value"); + // ============================================================================ + // SECTION: Row-Level Publish Characterization Tests (target behavior) + // + // Design/AnimationErrorFrameGenerationDesign.md: rows in a resolved + // app-to-app interval publish independently once their own animation and + // display timing are complete; they do not all wait for the closing app + // anchor's own display-duration lookahead. These tests describe that + // target behavior and are expected to fail until the publish policy in + // DisplayFrameQueue is changed (see Design/AnimationErrorFrameGenerationAgentPrompts.md, + // Agent 3). Do not weaken or delete these to make them pass; update the + // production code instead. + // ============================================================================ - // Assert: lastDisplayedSimStartTime updated - Assert::AreEqual(uint64_t(200), state.lastDisplayedSimStartTime, - L"lastDisplayedSimStartTime should be updated to current frame's pcl sim start"); + TEST_CLASS(RowLevelPublishCharacterizationTests) + { + public: + static std::vector Process( + QpcConverter& qpc, + UnifiedSwapChain& swapChain, + FrameData frame) + { + std::vector metrics; + auto rows = swapChain.ProcessPresent(qpc, std::move(frame)); + metrics.reserve(rows.size()); + for (const auto& row : rows) { + metrics.push_back(row.computed.metrics); + } + return metrics; } - // ======================================================================== - // B4: AnimationTime_PCLatency_ThreeFrames_CumulativeElapsedTime - // ======================================================================== - TEST_METHOD(AnimationTime_PCLatency_ThreeFrames_CumulativeElapsedTime) + TEST_METHOD(GeneratedRowsInSamePresentAsClosingAnchor_PublishImmediately_ClosingAnchorAwaitsLookahead) { - // Scenario: - // - Start with CpuStart as the animation source. - // - Frame 1: first displayed app frame with appSimStartTime = 100. - // This SEEDS animation state (firstAppSimStartTime), but - // does not yet emit msAnimationTime. - // - Frame 2: displayed app frame with appSimStartTime = 150. - // - Frame 3: displayed app frame with appSimStartTime = 250. - // - // QPC frequency = 10 MHz. - // - // Expected outcome: - // - firstAppSimStartTime is latched to 100 and never changes. - // - Frame 1: msAnimationTime == nullopt (just seeds state). - // - Frame 2: msAnimationTime = (150 - 100) / 10e6. - // - Frame 3: msAnimationTime = (250 - 100) / 10e6. - + // PresentA: Gen1, Gen2, Gen3, AppClose all in one present. Gen1..Gen3 already + // know their own nextScreenTime from sibling display entries in the same present, + // so once AppClose closes the interval they should publish immediately with + // resolved animation metrics. AppClose is the last display entry in its present, + // so it still needs the next displayed frame (in a later present) before it can + // publish. QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - // animationErrorSource defaults to CpuStart; will transition to AppProvider - // when the first displayed frame with appSimStartTime arrives. - - // -------------------------------------------------------------------- - // Frame 1: first valid app sim start, displayed - // -------------------------------------------------------------------- - FrameData frame1{}; - frame1.presentStartTime = 1'000'000; - frame1.timeInPresent = 500; - frame1.readyTime = 1'500'000; - frame1.appSimStartTime = 0; - frame1.pclSimStartTime = 100; - frame1.finalState = PresentResult::Presented; - frame1.displayed.PushBack({ FrameType::Application, 1'000'000 }); + UnifiedSwapChain swapChain{}; - FrameData next1{}; - next1.presentStartTime = 2'000'000; - next1.timeInPresent = 400; - next1.readyTime = 2'500'000; - next1.finalState = PresentResult::Presented; - next1.displayed.PushBack({ FrameType::Application, 2'000'000 }); - - auto metrics1 = ComputeMetricsForPresent(qpc, frame1, &next1, state); - Assert::AreEqual(size_t(1), metrics1.size()); - - // First displayed app frame seeds state; animation time is reported as zero. - Assert::IsTrue( - HasMetricValue(metrics1[0].metrics.msAnimationTime), - L"msAnimationTime should report a value even when transitioning"); - - // After processing frame1, the chain should have latched sim start and - // switched to PCLatency. - Assert::AreEqual(uint64_t(100), state.firstAppSimStartTime); - Assert::AreEqual(uint64_t(100), state.lastDisplayedSimStartTime); - Assert::IsTrue( - state.animationErrorSource == AnimationErrorSource::PCLatency, - L"Animation source should transition to PCLatency after first appSimStartTime."); - - // -------------------------------------------------------------------- - // Frame 2: second displayed app frame - // -------------------------------------------------------------------- - FrameData frame2{}; - frame2.presentStartTime = 3'000'000; - frame2.timeInPresent = 500; - frame2.readyTime = 3'500'000; - frame2.appSimStartTime = 0; - frame2.pclSimStartTime = 150; - frame2.finalState = PresentResult::Presented; - frame2.displayed.PushBack({ FrameType::Application, 3'000'000 }); - - FrameData next2{}; - next2.presentStartTime = 4'000'000; - next2.timeInPresent = 400; - next2.readyTime = 4'500'000; - next2.finalState = PresentResult::Presented; - next2.displayed.PushBack({ FrameType::Application, 4'000'000 }); - - auto metrics2 = ComputeMetricsForPresent(qpc, frame2, &next2, state); - Assert::AreEqual(size_t(1), metrics2.size()); - Assert::IsTrue( - HasMetricValue(metrics1[0].metrics.msAnimationTime), - L"Second displayed app frame should report msAnimationTime."); - AssertAreEqualWithinTolerance(double(0.0), metrics1[0].metrics.msAnimationTime, 0.0001); - double expected2 = qpc.DeltaUnsignedMilliSeconds(100, 150); - AssertAreEqualWithinTolerance( - expected2, - metrics2[0].metrics.msAnimationTime, - 0.0001, - L"Frame 2's msAnimationTime should be relative to firstAppSimStartTime (100 → 150)."); - - // firstAppSimStartTime should stay anchored at 100 - Assert::AreEqual(uint64_t(100), state.firstAppSimStartTime, L"firstAppSimStartTime should not change."); - // lastDisplayedSimStartTime should now reflect frame2 - Assert::AreEqual(uint64_t(150), state.lastDisplayedSimStartTime); - - // -------------------------------------------------------------------- - // Frame 3: third displayed app frame - // -------------------------------------------------------------------- - FrameData frame3{}; - frame3.presentStartTime = 5'000'000; - frame3.timeInPresent = 500; - frame3.readyTime = 5'500'000; - frame3.appSimStartTime = 0; - frame3.pclSimStartTime = 250; - frame3.finalState = PresentResult::Presented; - frame3.displayed.PushBack({ FrameType::Application, 5'000'000 }); - - FrameData next3{}; - next3.presentStartTime = 6'000'000; - next3.timeInPresent = 400; - next3.readyTime = 6'500'000; - next3.finalState = PresentResult::Presented; - next3.displayed.PushBack({ FrameType::Application, 6'000'000 }); - - auto metrics3 = ComputeMetricsForPresent(qpc, frame3, &next3, state); - Assert::AreEqual(size_t(1), metrics3.size()); - Assert::IsTrue( - HasMetricValue(metrics3[0].metrics.msAnimationTime), - L"Third displayed app frame should report msAnimationTime."); - - double expected3 = qpc.DeltaUnsignedMilliSeconds(100, 250); - AssertAreEqualWithinTolerance( - expected3, - metrics3[0].metrics.msAnimationTime, - 0.0001, - L"Frame 3's msAnimationTime should be relative to original firstAppSimStartTime (100 → 250)."); - - // firstAppSimStartTime remains the original seed - Assert::AreEqual(uint64_t(100), state.firstAppSimStartTime, L"firstAppSimStartTime should remain at 100."); - // lastDisplayedSimStartTime should now reflect frame3 - Assert::AreEqual(uint64_t(250), state.lastDisplayedSimStartTime); - } - // ======================================================================== - // B5: AnimationTime_PCLatency_SkippedFrame_StaysConsistent - // ======================================================================== - TEST_METHOD(AnimationTime_PCLatency_SkippedFrame_StaysConsistent) - { - // Scenario: - // - Chain is configured to use PCLatency as the animation source. - // - Frame 1: displayed, pclSimStartTime = 100 → seeds animation state. - // - Frame 2: discarded (not displayed), pclSimStartTime = 200 → should NOT - // produce animation time and should NOT advance animation state. - // - Frame 3: displayed again, pclSimStartTime = 300 → animation time should be - // measured from the original 100, skipping the discarded frame. - // - // Expectations: - // - Frame 1: msAnimationTime is std::nullopt, but firstAppSimStartTime and - // lastDisplayedSimStartTime are set to 100. - // - Frame 2: msAnimationTime is std::nullopt and state remains 100/100. - // - Frame 3: msAnimationTime == Delta(100, 300) and lastDisplayedSimStartTime == 300. - - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState chain{}; - - // - // Frame 1: first displayed PCL frame - // - FrameData frame1{}; - frame1.presentStartTime = 1'000'000; - frame1.timeInPresent = 10'000; - frame1.readyTime = 1'010'000; - frame1.pclSimStartTime = 100; - frame1.finalState = PresentResult::Presented; - frame1.displayed.PushBack({ FrameType::Application, 2'000'000 }); - - FrameData next1{}; - next1.presentStartTime = 3'000'000; - next1.timeInPresent = 10'000; - next1.readyTime = 3'010'000; - next1.finalState = PresentResult::Presented; - next1.displayed.PushBack({ FrameType::Application, 4'000'000 }); - - auto metrics1 = ComputeMetricsForPresent(qpc, frame1, &next1, chain); - Assert::AreEqual(size_t(1), metrics1.size()); - - // First displayed frame seeds animation state; animation time will be reported - // still as we are transitioning to PCLatency. - Assert::IsTrue( - HasMetricValue(metrics1[0].metrics.msAnimationTime), - L"Animation Time will be reported"); - AssertAreEqualWithinTolerance(double(0.0), metrics1[0].metrics.msAnimationTime, 0.0001); - - Assert::AreEqual(uint64_t(100), chain.firstAppSimStartTime); - Assert::AreEqual(uint64_t(100), chain.lastDisplayedSimStartTime); - Assert::IsTrue(AnimationErrorSource::PCLatency == chain.animationErrorSource); - - // - // Frame 2: discarded (not displayed) but has a PCL sim start - // - FrameData frame2{}; - frame2.presentStartTime = 5'000'000; - frame2.timeInPresent = 10'000; - frame2.readyTime = 5'010'000; - frame2.pclSimStartTime = 200; // Has PCL sim start but not displayed - frame2.finalState = PresentResult::Discarded; // Not presented → not displayed - - auto metrics2 = ComputeMetricsForPresent(qpc, frame2, nullptr, chain); - Assert::AreEqual(size_t(1), metrics2.size()); - - // Not displayed → no animation time, and animation state should not advance - Assert::IsFalse( - HasMetricValue(metrics2[0].metrics.msAnimationTime), - L"Non-displayed frame should not report animation time."); - - Assert::AreEqual(uint64_t(100), chain.firstAppSimStartTime, - L"firstAppSimStartTime must remain anchored to Frame 1 after skipped frame."); - Assert::AreEqual(uint64_t(100), chain.lastDisplayedSimStartTime, - L"lastDisplayedSimStartTime must remain anchored to Frame 1 after skipped frame."); - - // - // Frame 3: displayed again after the skipped frame - // - FrameData frame3{}; - frame3.presentStartTime = 6'000'000; - frame3.timeInPresent = 10'000; - frame3.readyTime = 6'010'000; - frame3.pclSimStartTime = 300; - frame3.finalState = PresentResult::Presented; - frame3.displayed.PushBack({ FrameType::Application, 7'000'000 }); - - FrameData next3{}; - next3.presentStartTime = 8'000'000; - next3.timeInPresent = 10'000; - next3.readyTime = 8'010'000; - next3.finalState = PresentResult::Presented; - next3.displayed.PushBack({ FrameType::Application, 9'000'000 }); - - auto metrics3 = ComputeMetricsForPresent(qpc, frame3, &next3, chain); - Assert::AreEqual(size_t(1), metrics3.size()); - Assert::IsTrue( - HasMetricValue(metrics3[0].metrics.msAnimationTime), - L"Displayed frame with valid PCL sim start should report animation time."); - - double expected3 = qpc.DeltaUnsignedMilliSeconds(100, 300); - AssertAreEqualWithinTolerance( - expected3, - metrics3[0].metrics.msAnimationTime, - 0.0001, - L"Frame 3's msAnimationTime should be measured from Frame 1's PCL sim start, skipping Frame 2."); - - Assert::AreEqual(uint64_t(100), chain.firstAppSimStartTime, - L"firstAppSimStartTime should remain at Frame 1's value."); - Assert::AreEqual(uint64_t(300), chain.lastDisplayedSimStartTime, - L"lastDisplayedSimStartTime should advance to Frame 3's PCL sim start."); - } - // ======================================================================== - // B6: AnimationTime_PCLatency_MissingPclAndAppSimStart_NoDemotionOrFallback - // ======================================================================== - TEST_METHOD(AnimationTime_PCLatency_MissingPclAndAppSimStart_NoDemotionOrFallback) - { - // Scenario: - // - Start with CpuStart source. - // - Frame 1: displayed PCL data establishes PCLatency as the active source. - // - Frame 2: displayed frame has neither pclSimStartTime nor appSimStartTime. - // - No demotion or fallback is allowed, so the source and anchors stay unchanged. - // - // Expected Outcome: - // - msAnimationTime is missing (NaN), not 0.0. - // - animationErrorSource remains PCLatency. - // - firstAppSimStartTime, lastDisplayedSimStartTime, and - // lastDisplayedAppScreenTime remain unchanged. - - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - - // Frame 1: First PCL data - FrameData frame1{}; - frame1.presentStartTime = 500'000; - frame1.timeInPresent = 300; - frame1.pclSimStartTime = 100; - frame1.finalState = PresentResult::Presented; - frame1.displayed.PushBack({ FrameType::Application, 900'000 }); - - FrameData next1{}; - next1.presentStartTime = 1'500'000; - next1.finalState = PresentResult::Presented; - next1.displayed.PushBack({ FrameType::Application, 1'500'000 }); - - auto metrics1 = ComputeMetricsForPresent(qpc, frame1, &next1, state); - Assert::AreEqual(size_t(1), metrics1.size()); - Assert::AreEqual(uint64_t(100), state.firstAppSimStartTime); - Assert::AreEqual(uint64_t(100), state.lastDisplayedSimStartTime); - Assert::AreEqual(uint64_t(900'000), state.lastDisplayedAppScreenTime); - Assert::IsTrue(state.animationErrorSource == AnimationErrorSource::PCLatency); - - // Frame 2: displayed frame is missing both PCL and AppProvider sim start data. - FrameData frame{}; - frame.presentStartTime = 1'200'000; - frame.timeInPresent = 100'000; - frame.readyTime = 1'300'000; - frame.appSimStartTime = 0; - frame.pclSimStartTime = 0; - frame.finalState = PresentResult::Presented; - frame.displayed.PushBack({ FrameType::Application, 1'400'000 }); - - // Create nextDisplayed - FrameData next{}; - next.presentStartTime = 1'600'000; - next.timeInPresent = 50'000; - next.readyTime = 1'700'000; - next.finalState = PresentResult::Presented; - next.displayed.PushBack({ FrameType::Application, 1'800'000 }); - - // Action: Compute metrics - auto metricsVector = ComputeMetricsForPresent(qpc, frame, &next, state); - - // Assert: Should have one computed metric - Assert::AreEqual(size_t(1), metricsVector.size()); - - const ComputedMetrics& result = metricsVector[0]; - - // Assert: missing active-source data does not trigger a fallback or reset. - Assert::IsFalse(HasMetricValue(result.metrics.msAnimationTime), - L"msAnimationTime should be missing when PCLatency remains active but no sim start is available."); - - // Assert: State should remain PCLatency with prior anchors intact. - Assert::IsTrue(state.animationErrorSource == AnimationErrorSource::PCLatency, - L"animationErrorSource should remain PCLatency when no demotion is allowed."); - - Assert::AreEqual(uint64_t(100), state.firstAppSimStartTime, - L"firstAppSimStartTime should remain unchanged."); - Assert::AreEqual(uint64_t(100), state.lastDisplayedSimStartTime, - L"lastDisplayedSimStartTime should remain unchanged when the active source is missing."); - Assert::AreEqual(uint64_t(900'000), state.lastDisplayedAppScreenTime, - L"lastDisplayedAppScreenTime should remain unchanged when the active source is missing."); - } - - // ======================================================================== - // B7: AnimationTime_PCLatency_TransitionToAppProvider_AppOnly_ZeroAndReseeds - // ======================================================================== - TEST_METHOD(AnimationTime_PCLatency_TransitionToAppProvider_AppOnly_ZeroAndReseeds) - { - // Scenario: - // - Chain is already using PCLatency with a prior PCL-derived anchor. - // - Current displayed frame has appSimStartTime but no pclSimStartTime. - // - PCLatency -> AppProvider is an allowed upgrade. - // - // Expected Outcome: - // - msAnimationTime = 0.0 on the transition frame. - // - animationErrorSource upgrades to AppProvider. - // - firstAppSimStartTime and lastDisplayedSimStartTime reseed to appSimStartTime. - // - lastDisplayedAppScreenTime updates to the displayed app screen time. - - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - state.animationErrorSource = AnimationErrorSource::PCLatency; - state.firstAppSimStartTime = 300; - state.lastDisplayedSimStartTime = 320; - state.lastDisplayedAppScreenTime = 900'000; - - FrameData frame{}; - frame.presentStartTime = 1'000'000; - frame.timeInPresent = 500; - frame.readyTime = 1'500'000; - frame.appSimStartTime = 700; - frame.pclSimStartTime = 0; - frame.finalState = PresentResult::Presented; - frame.displayed.PushBack({ FrameType::Application, 1'900'000 }); - - FrameData next{}; - next.presentStartTime = 2'000'000; - next.timeInPresent = 400; - next.readyTime = 2'500'000; - next.finalState = PresentResult::Presented; - next.displayed.PushBack({ FrameType::Application, 2'900'000 }); - - auto metricsVector = ComputeMetricsForPresent(qpc, frame, &next, state); - - Assert::AreEqual(size_t(1), metricsVector.size()); - const ComputedMetrics& result = metricsVector[0]; - - Assert::IsTrue(HasMetricValue(result.metrics.msAnimationTime), - L"Transition frame should report msAnimationTime."); - AssertAreEqualWithinTolerance(double(0.0), result.metrics.msAnimationTime, 0.0001, - L"msAnimationTime should be 0.0 on the PCLatency to AppProvider transition frame."); - - Assert::IsTrue(state.animationErrorSource == AnimationErrorSource::AppProvider, - L"animationErrorSource should transition to AppProvider."); - Assert::AreEqual(uint64_t(700), state.firstAppSimStartTime, - L"firstAppSimStartTime should reseed to the current frame's appSimStartTime."); - Assert::AreEqual(uint64_t(700), state.lastDisplayedSimStartTime, - L"lastDisplayedSimStartTime should update to the current frame's appSimStartTime."); - Assert::AreEqual(uint64_t(1'900'000), state.lastDisplayedAppScreenTime, - L"lastDisplayedAppScreenTime should update to the displayed app screen time."); - } - - // ======================================================================== - // B8: AnimationTime_PCLatency_TransitionToAppProvider_BothPresent_ZeroAndReseedsToApp - // ======================================================================== - TEST_METHOD(AnimationTime_PCLatency_TransitionToAppProvider_BothPresent_ZeroAndReseedsToApp) - { - // Scenario: - // - Chain is already using PCLatency with a prior PCL-derived anchor. - // - Current displayed frame has both appSimStartTime and pclSimStartTime. - // - PCLatency -> AppProvider is an allowed upgrade, and AppProvider wins. - // - // Expected Outcome: - // - msAnimationTime = 0.0 on the transition frame. - // - animationErrorSource upgrades to AppProvider. - // - firstAppSimStartTime and lastDisplayedSimStartTime reseed to appSimStartTime, - // not pclSimStartTime. - // - lastDisplayedAppScreenTime updates to the displayed app screen time. - - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - state.animationErrorSource = AnimationErrorSource::PCLatency; - state.firstAppSimStartTime = 300; - state.lastDisplayedSimStartTime = 320; - state.lastDisplayedAppScreenTime = 900'000; - - FrameData frame{}; - frame.presentStartTime = 1'000'000; - frame.timeInPresent = 500; - frame.readyTime = 1'500'000; - frame.appSimStartTime = 700; - frame.pclSimStartTime = 950; - frame.finalState = PresentResult::Presented; - frame.displayed.PushBack({ FrameType::Application, 1'950'000 }); - - FrameData next{}; - next.presentStartTime = 2'000'000; - next.timeInPresent = 400; - next.readyTime = 2'500'000; - next.finalState = PresentResult::Presented; - next.displayed.PushBack({ FrameType::Application, 2'950'000 }); - - auto metricsVector = ComputeMetricsForPresent(qpc, frame, &next, state); - - Assert::AreEqual(size_t(1), metricsVector.size()); - const ComputedMetrics& result = metricsVector[0]; - - Assert::IsTrue(HasMetricValue(result.metrics.msAnimationTime), - L"Transition frame should report msAnimationTime."); - AssertAreEqualWithinTolerance(double(0.0), result.metrics.msAnimationTime, 0.0001, - L"msAnimationTime should be 0.0 on the PCLatency to AppProvider transition frame when both sources are present."); - - Assert::IsTrue(state.animationErrorSource == AnimationErrorSource::AppProvider, - L"animationErrorSource should transition to AppProvider when both AppProvider and PCLatency data are present."); - Assert::AreEqual(uint64_t(700), state.firstAppSimStartTime, - L"firstAppSimStartTime should reseed to appSimStartTime for the new AppProvider timeline."); - Assert::AreEqual(uint64_t(700), state.lastDisplayedSimStartTime, - L"lastDisplayedSimStartTime should update to appSimStartTime, not pclSimStartTime."); - Assert::AreNotEqual(uint64_t(950), state.firstAppSimStartTime, - L"firstAppSimStartTime should not reseed from pclSimStartTime when AppProvider is available."); - Assert::AreNotEqual(uint64_t(950), state.lastDisplayedSimStartTime, - L"lastDisplayedSimStartTime should prove AppProvider wins over PCLatency in the both-present case."); - Assert::AreEqual(uint64_t(1'950'000), state.lastDisplayedAppScreenTime, - L"lastDisplayedAppScreenTime should update to the displayed app screen time."); - } - - // ======================================================================== - // D1: AnimationTime_CpuStart_FirstFrame_ZeroWithoutHistory - // ======================================================================== - TEST_METHOD(AnimationTime_CpuStart_FirstFrame_ZeroWithoutHistory) - { - // Scenario: - // - SwapChainCoreState is in initial state (no prior frames) - // - Current frame: displayed, displayIndex == appIndex - // - animationErrorSource = CpuStart - // - No lastAppPresent or lastPresent in chain - // - // Expected Outcome: - // - msAnimationTime = std::nullopt (cpuStart = 0, cannot initialize firstAppSimStartTime) - // - firstAppSimStartTime remains 0 in state - // - lastDisplayedSimStartTime remains 0 in state - - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - // state.animationErrorSource defaults to CpuStart - - // Verify initial state - Assert::AreEqual(uint64_t(0), state.firstAppSimStartTime); - Assert::AreEqual(uint64_t(0), state.lastDisplayedSimStartTime); - - // Create a displayed app frame with CpuStart source - FrameData frame{}; - frame.presentStartTime = 1'000'000; - frame.timeInPresent = 500; - frame.readyTime = 1'500'000; - frame.appSimStartTime = 0; - frame.pclSimStartTime = 0; - frame.finalState = PresentResult::Presented; - frame.displayed.PushBack({ FrameType::Application, 1'000'000 }); - - // Verify frame setup - Assert::AreEqual(size_t(1), frame.displayed.Size()); - - // Create nextDisplayed to allow processing - FrameData next{}; - next.presentStartTime = 2'000'000; - next.timeInPresent = 400; - next.readyTime = 2'500'000; - next.finalState = PresentResult::Presented; - next.displayed.PushBack({ FrameType::Application, 2'000'000 }); - - // Action: Compute metrics for this frame - auto metricsVector = ComputeMetricsForPresent(qpc, frame, &next, state); - - // Assert: Should have one computed metric - Assert::AreEqual(size_t(1), metricsVector.size()); - - const ComputedMetrics& result = metricsVector[0]; - - Assert::IsTrue(HasMetricValue(result.metrics.msAnimationTime), - L"msAnimationTime should have a value"); - AssertAreEqualWithinTolerance(double(0.0), result.metrics.msAnimationTime, 0.0001, - L"msAnimationTime should be 0 on first frame with CpuStart source and no history"); - // Assert: State should not be updated - Assert::AreEqual(uint64_t(0), state.firstAppSimStartTime, - L"State: firstAppSimStartTime should remain 0 (no valid CPU start available)"); - - // Assert: lastDisplayedSimStartTime should remain 0 - Assert::AreEqual(uint64_t(0), state.lastDisplayedSimStartTime, - L"State: lastDisplayedSimStartTime should remain 0"); - } - - // ======================================================================== - // D2: AnimationTime_CpuStart_TransitionFrame_FirstValidCpuStart - // ======================================================================== - TEST_METHOD(AnimationTime_CpuStart_TransitionFrame_FirstValidCpuStart) - { - // Scenario: - // - Prior state: firstAppSimStartTime == 0 (no prior CPU start) - // - Chain has lastAppPresent with valid timing data - // - Current frame: displayed, displayIndex == appIndex - // - cpuStart = lastAppPresent.presentStartTime + lastAppPresent.timeInPresent = 1'000'000 - // - animationErrorSource = CpuStart - // - // Expected Outcome: - // - msAnimationTime = 0 (first frame with valid CPU start) - // - lastDisplayedSimStartTime is updated to cpuStart (1'000'000) - - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - // state.animationErrorSource defaults to CpuStart - - // Set up prior app present for CPU start calculation - FrameData priorApp{}; - priorApp.presentStartTime = 800'000; - priorApp.timeInPresent = 200'000; - priorApp.readyTime = 1'000'000; - priorApp.finalState = PresentResult::Presented; - priorApp.displayed.PushBack({ FrameType::Application, 1'100'000 }); - - state.lastAppPresent = priorApp; - - // Create a displayed app frame with CpuStart source - FrameData frame{}; - frame.presentStartTime = 1'200'000; - frame.timeInPresent = 100'000; - frame.readyTime = 1'300'000; - frame.appSimStartTime = 0; - frame.pclSimStartTime = 0; - frame.finalState = PresentResult::Presented; - frame.displayed.PushBack({ FrameType::Application, 1'400'000 }); - - // Create nextDisplayed - FrameData next{}; - next.presentStartTime = 1'600'000; - next.timeInPresent = 50'000; - next.readyTime = 1'700'000; - next.finalState = PresentResult::Presented; - next.displayed.PushBack({ FrameType::Application, 1'800'000 }); - - // Action: Compute metrics - auto metricsVector = ComputeMetricsForPresent(qpc, frame, &next, state); - - // Assert - Assert::AreEqual(size_t(1), metricsVector.size()); - const ComputedMetrics& result = metricsVector[0]; - - // Assert: msAnimationTime should be 0 (first transition frame) - Assert::IsTrue(HasMetricValue(result.metrics.msAnimationTime), - L"msAnimationTime should have a value on first valid CPU start"); - AssertAreEqualWithinTolerance(0.0, result.metrics.msAnimationTime, 0.0001, - L"msAnimationTime should be 0 on first transition frame"); - - // Assert: State should be updated with CPU start - // cpuStart = 800'000 + 200'000 = 1'000'000 - uint64_t expectedCpuStart = 800'000 + 200'000; - Assert::AreEqual(expectedCpuStart, state.lastDisplayedSimStartTime, - L"State: lastDisplayedSimStartTime should be set to CPU start value"); - } - - // ======================================================================== - // D3: AnimationTime_CpuStart_IncreasesAcrossFramesWithoutProvider - // ======================================================================== - TEST_METHOD(AnimationTime_CpuStart_IncreasesAcrossFramesWithoutProvider) - { - // Scenario: - // - animationErrorSource = CpuStart - // - No App or PCL sim start timestamps on any frame. - // - We seed lastAppPresent so the first tested frame already has - // a non-zero CpuStart. - // - We then process two displayed frames and expect: - // * Both report msAnimationTime (has_value()). - // * Frame 2's msAnimationTime > Frame 1's msAnimationTime. - // * firstAppSimStartTime stays 0 (no provider events yet). - - QpcConverter qpc(10'000'000, 500'000); - SwapChainCoreState state{}; - state.animationErrorSource = AnimationErrorSource::CpuStart; - - // Seed lastAppPresent so CalculateCPUStart() for frame1 is non-zero. - // cpuStart1 = prior.presentStartTime + prior.timeInPresent - FrameData prior{}; - prior.presentStartTime = 1'000'000; - prior.timeInPresent = 100'000; // cpuStart1 = 1'100'000 - prior.readyTime = 1'200'000; - prior.finalState = PresentResult::Presented; - prior.displayed.PushBack({ FrameType::Application, 1'300'000 }); - state.lastAppPresent = prior; - - // Sanity: no provider sim-start yet. - state.firstAppSimStartTime = 0; - state.lastDisplayedSimStartTime = 0; - - // -------------------------------------------------------------------- - // Frame 1: Presented + displayed, no App/PCL sim start - // -------------------------------------------------------------------- - FrameData frame1{}; - frame1.presentStartTime = 2'000'000; - frame1.timeInPresent = 80'000; - frame1.readyTime = 2'100'000; - frame1.appSimStartTime = 0; - frame1.pclSimStartTime = 0; - frame1.finalState = PresentResult::Presented; - frame1.displayed.PushBack({ FrameType::Application, 2'500'000 }); - - FrameData next1{}; - next1.presentStartTime = 3'000'000; - next1.timeInPresent = 50'000; - next1.readyTime = 3'100'000; - next1.finalState = PresentResult::Presented; - next1.displayed.PushBack({ FrameType::Application, 3'500'000 }); - - auto metrics1 = ComputeMetricsForPresent(qpc, frame1, &next1, state); - Assert::AreEqual(size_t(1), metrics1.size()); - const auto& m1 = metrics1[0].metrics; - - Assert::IsTrue(HasMetricValue(m1.msAnimationTime), - L"CpuStart animation should report msAnimationTime even without App/PCL provider."); - const double anim1 = m1.msAnimationTime; - Assert::IsTrue(anim1 > 0.0, - L"First CpuStart-driven frame should have a positive animation time relative to session/start anchor."); - - // No provider yet → firstAppSimStartTime must remain 0. - Assert::AreEqual(uint64_t(0), state.firstAppSimStartTime, - L"firstAppSimStartTime should not be set until App/PCL provider events arrive."); - - // After frame1, lastAppPresent is now frame1; CpuStart for frame2 will be later. - // -------------------------------------------------------------------- - // Frame 2: later Presented + displayed frame, still no App/PCL provider - // -------------------------------------------------------------------- - FrameData frame2{}; - frame2.presentStartTime = 4'000'000; - frame2.timeInPresent = 120'000; - frame2.readyTime = 4'200'000; - frame2.appSimStartTime = 0; - frame2.pclSimStartTime = 0; - frame2.finalState = PresentResult::Presented; - frame2.displayed.PushBack({ FrameType::Application, 4'600'000 }); - - FrameData next2{}; - next2.presentStartTime = 5'000'000; - next2.timeInPresent = 50'000; - next2.readyTime = 5'100'000; - next2.finalState = PresentResult::Presented; - next2.displayed.PushBack({ FrameType::Application, 5'500'000 }); - - auto metrics2 = ComputeMetricsForPresent(qpc, frame2, &next2, state); - Assert::AreEqual(size_t(1), metrics2.size()); - const auto& m2 = metrics2[0].metrics; - - Assert::IsTrue(HasMetricValue(m2.msAnimationTime), - L"Second CpuStart-driven frame should also report msAnimationTime."); - const double anim2 = m2.msAnimationTime; - - Assert::IsTrue(anim2 > anim1, - L"CpuStart-based animation time should increase across frames as CpuStart advances."); - - // Still no provider events → anchor remains "non-provider", so firstAppSimStartTime should be 0. - Assert::AreEqual(uint64_t(0), state.firstAppSimStartTime, - L"firstAppSimStartTime should still be 0 without App/PCL provider data."); - } - }; - - // ============================================================================ - // SECTION: Animation Error Tests - // ============================================================================ - - TEST_CLASS(AnimationErrorTests) - { - public: - // Section B: Animation Error – AppProvider Source - - TEST_METHOD(AnimationError_AppProvider_NoLastDisplayedFrame_Nullopt) - { - // Scenario: First app-displayed frame with appSimStartTime - // Expected: msAnimationError = std::nullopt (no prior frame data) - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - state.animationErrorSource = AnimationErrorSource::AppProvider; - - FrameData present{}; - present.presentStartTime = 1000; - present.timeInPresent = 100; - present.appSimStartTime = 150; - present.finalState = PresentResult::Presented; - present.displayed.PushBack({ FrameType::Application, 200 }); - - FrameData nextPresent{}; - nextPresent.presentStartTime = 2000; - nextPresent.finalState = PresentResult::Presented; - nextPresent.displayed.PushBack({ FrameType::Application, 2100 }); - - auto results = ComputeMetricsForPresent(qpc, present, &nextPresent, state); - - Assert::AreEqual(size_t(1), results.size()); - Assert::IsFalse(HasMetricValue(results[0].metrics.msAnimationError), - L"msAnimationError should be nullopt without prior displayed frame"); - } - - TEST_METHOD(AnimationError_AppProvider_TwoFrames_PositiveError) - { - // Scenario: Two frames with sim elapsed = 50, display elapsed = 50 - // Expected: msAnimationError = 0 ms (cadences match) - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - state.animationErrorSource = AnimationErrorSource::AppProvider; - - // Frame 1 setup - FrameData frame1{}; - frame1.presentStartTime = 1000; - frame1.timeInPresent = 100; - frame1.appSimStartTime = 100; - frame1.finalState = PresentResult::Presented; - frame1.displayed.PushBack({ FrameType::Application, 1000 }); - - FrameData frame2{}; - frame2.presentStartTime = 2000; - frame2.timeInPresent = 100; - frame2.appSimStartTime = 150; // sim elapsed = 50 - frame2.finalState = PresentResult::Presented; - frame2.displayed.PushBack({ FrameType::Application, 1050 }); // display elapsed = 50 - - // Process frame 1 - FrameData dummyNext{}; - dummyNext.finalState = PresentResult::Presented; - dummyNext.displayed.PushBack({ FrameType::Application, 2000 }); - auto results1 = ComputeMetricsForPresent(qpc, frame1, &dummyNext, state); - - // Process frame 2 - FrameData frame3{}; - frame3.finalState = PresentResult::Presented; - frame3.displayed.PushBack({ FrameType::Application, 3000 }); - auto results2 = ComputeMetricsForPresent(qpc, frame2, &frame3, state); - - Assert::AreEqual(size_t(1), results2.size()); - Assert::IsTrue(HasMetricValue(results2[0].metrics.msAnimationError)); - AssertAreEqualWithinTolerance(0.0, results2[0].metrics.msAnimationError, 0.0001, - L"msAnimationError should be 0 when sim and display cadences match"); - } - - TEST_METHOD(AnimationError_AppProvider_TwoFrames_SimSlowerThanDisplay) - { - // Scenario: sim elapsed = 40 ticks, display elapsed = 50 ticks - // Expected: msAnimationError = -0.001 ms (sim slower) - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - state.animationErrorSource = AnimationErrorSource::AppProvider; - - FrameData frame1{}; - frame1.presentStartTime = 1000; - frame1.timeInPresent = 100; - frame1.appSimStartTime = 100; - frame1.finalState = PresentResult::Presented; - frame1.displayed.PushBack({ FrameType::Application, 1000 }); - - FrameData frame2{}; - frame2.presentStartTime = 2000; - frame2.timeInPresent = 100; - frame2.appSimStartTime = 140; // sim elapsed = 40 - frame2.finalState = PresentResult::Presented; - frame2.displayed.PushBack({ FrameType::Application, 1050 }); // display elapsed = 50 - - FrameData dummyNext{}; - dummyNext.finalState = PresentResult::Presented; - dummyNext.displayed.PushBack({ FrameType::Application, 2000 }); - ComputeMetricsForPresent(qpc, frame1, &dummyNext, state); - - FrameData frame3{}; - frame3.finalState = PresentResult::Presented; - frame3.displayed.PushBack({ FrameType::Application, 3000 }); - auto results = ComputeMetricsForPresent(qpc, frame2, &frame3, state); - - Assert::IsTrue(HasMetricValue(results[0].metrics.msAnimationError)); - double simElapsed = qpc.DeltaUnsignedMilliSeconds(100, 140); // 0.004 ms - double displayElapsed = qpc.DeltaUnsignedMilliSeconds(1000, 1050); // 0.005 ms - double expected = simElapsed - displayElapsed; // -0.001 ms - AssertAreEqualWithinTolerance(expected, results[0].metrics.msAnimationError, 0.0001); - } - - TEST_METHOD(AnimationError_AppProvider_TwoFrames_SimFasterThanDisplay) - { - // Scenario: sim elapsed = 60 ticks, display elapsed = 50 ticks - // Expected: msAnimationError = +0.001 ms (sim faster) - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - state.animationErrorSource = AnimationErrorSource::AppProvider; - - FrameData frame1{}; - frame1.presentStartTime = 1000; - frame1.timeInPresent = 100; - frame1.appSimStartTime = 100; - frame1.finalState = PresentResult::Presented; - frame1.displayed.PushBack({ FrameType::Application, 1000 }); - - FrameData frame2{}; - frame2.presentStartTime = 2000; - frame2.timeInPresent = 100; - frame2.appSimStartTime = 160; // sim elapsed = 60 - frame2.finalState = PresentResult::Presented; - frame2.displayed.PushBack({ FrameType::Application, 1050 }); // display elapsed = 50 - - FrameData dummyNext{}; - dummyNext.finalState = PresentResult::Presented; - dummyNext.displayed.PushBack({ FrameType::Application, 2000 }); - ComputeMetricsForPresent(qpc, frame1, &dummyNext, state); - - FrameData frame3{}; - frame3.finalState = PresentResult::Presented; - frame3.displayed.PushBack({ FrameType::Application, 3000 }); - auto results = ComputeMetricsForPresent(qpc, frame2, &frame3, state); - - Assert::IsTrue(HasMetricValue(results[0].metrics.msAnimationError)); - double simElapsed = qpc.DeltaUnsignedMilliSeconds(100, 160); // 0.006 ms - double displayElapsed = qpc.DeltaUnsignedMilliSeconds(1000, 1050); // 0.005 ms - double expected = simElapsed - displayElapsed; // +0.001 ms - AssertAreEqualWithinTolerance(expected, results[0].metrics.msAnimationError, 0.0001); - } - - TEST_METHOD(AnimationError_AppProvider_BackwardsSimStartTime_Nullopt) - { - // Scenario: Current sim start goes backward in time - // Expected: msAnimationError = std::nullopt - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - state.animationErrorSource = AnimationErrorSource::AppProvider; - - FrameData frame1{}; - frame1.presentStartTime = 1000; - frame1.timeInPresent = 100; - frame1.appSimStartTime = 150; - frame1.finalState = PresentResult::Presented; - frame1.displayed.PushBack({ FrameType::Application, 1050 }); - - FrameData frame2{}; - frame2.presentStartTime = 2000; - frame2.timeInPresent = 100; - frame2.appSimStartTime = 140; // backwards! - frame2.finalState = PresentResult::Presented; - frame2.displayed.PushBack({ FrameType::Application, 1100 }); - - FrameData dummyNext{}; - dummyNext.finalState = PresentResult::Presented; - dummyNext.displayed.PushBack({ FrameType::Application, 2000 }); - ComputeMetricsForPresent(qpc, frame1, &dummyNext, state); - - FrameData frame3{}; - frame3.finalState = PresentResult::Presented; - frame3.displayed.PushBack({ FrameType::Application, 3000 }); - auto results = ComputeMetricsForPresent(qpc, frame2, &frame3, state); - - Assert::IsFalse(HasMetricValue(results[0].metrics.msAnimationError), - L"msAnimationError should be nullopt when sim start goes backward"); - } - - TEST_METHOD(AnimationError_AppProvider_CurrentSimStartTimeZero_Nullopt) - { - // Scenario: Current frame has no app instrumentation (appSimStartTime = 0) - // Expected: msAnimationError = std::nullopt - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState swapChain{}; - swapChain.animationErrorSource = AnimationErrorSource::AppProvider; - - FrameData frame1{}; - frame1.presentStartTime = 1000; - frame1.timeInPresent = 100; - frame1.appSimStartTime = 100; - frame1.finalState = PresentResult::Presented; - frame1.displayed.PushBack({ FrameType::Application, 1000 }); - - FrameData frame2{}; - frame2.presentStartTime = 2000; - frame2.timeInPresent = 100; - frame2.appSimStartTime = 0; // no instrumentation - frame2.finalState = PresentResult::Presented; - frame2.displayed.PushBack({ FrameType::Application, 1050 }); - - FrameData dummyNext{}; - dummyNext.finalState = PresentResult::Presented; - dummyNext.displayed.PushBack({ FrameType::Application, 2000 }); - ComputeMetricsForPresent(qpc, frame1, &dummyNext, swapChain); - - Assert::IsTrue(swapChain.animationErrorSource == AnimationErrorSource::AppProvider, - L"animationErrorSource should be AppProvider before processing the missing-data frame."); - Assert::AreEqual(uint64_t(100), swapChain.firstAppSimStartTime, - L"firstAppSimStartTime should be seeded from the prior displayed AppProvider frame."); - Assert::AreEqual(uint64_t(100), swapChain.lastDisplayedSimStartTime, - L"lastDisplayedSimStartTime should reflect the prior displayed AppProvider frame before the missing-data frame is processed."); - Assert::AreEqual(uint64_t(1000), swapChain.lastDisplayedAppScreenTime, - L"lastDisplayedAppScreenTime should be seeded from the prior displayed AppProvider frame."); - - FrameData frame3{}; - frame3.finalState = PresentResult::Presented; - frame3.displayed.PushBack({ FrameType::Application, 3000 }); - auto results = ComputeMetricsForPresent(qpc, frame2, &frame3, swapChain); - - Assert::IsFalse(HasMetricValue(results[0].metrics.msAnimationError), - L"msAnimationError should be nullopt without valid sim start time"); - Assert::IsTrue(swapChain.animationErrorSource == AnimationErrorSource::AppProvider, - L"animationErrorSource should remain AppProvider when no transition is allowed."); - Assert::AreEqual(uint64_t(100), swapChain.firstAppSimStartTime, - L"firstAppSimStartTime should remain unchanged when the active source is missing."); - Assert::AreEqual(uint64_t(100), swapChain.lastDisplayedSimStartTime, - L"lastDisplayedSimStartTime should remain unchanged when the active source is missing."); - Assert::AreEqual(uint64_t(1000), swapChain.lastDisplayedAppScreenTime, - L"lastDisplayedAppScreenTime should remain unchanged when the active source is missing."); - } - - TEST_METHOD(AnimationError_AppProvider_BothPresent_UsesAppNotPcl) - { - // Scenario: AppProvider is already active and the current displayed frame has both - // appSimStartTime and pclSimStartTime. - // Expected: msAnimationError uses the app timeline and the source remains AppProvider. - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - state.animationErrorSource = AnimationErrorSource::AppProvider; - - FrameData frame1{}; - frame1.presentStartTime = 1000; - frame1.timeInPresent = 100; - frame1.appSimStartTime = 100; - frame1.finalState = PresentResult::Presented; - frame1.displayed.PushBack({ FrameType::Application, 1000 }); - - FrameData frame2{}; - frame2.presentStartTime = 2000; - frame2.timeInPresent = 100; - frame2.appSimStartTime = 140; - frame2.pclSimStartTime = 180; - frame2.finalState = PresentResult::Presented; - frame2.displayed.PushBack({ FrameType::Application, 1050 }); - - FrameData dummyNext{}; - dummyNext.finalState = PresentResult::Presented; - dummyNext.displayed.PushBack({ FrameType::Application, 2000 }); - ComputeMetricsForPresent(qpc, frame1, &dummyNext, state); - - Assert::IsTrue(state.animationErrorSource == AnimationErrorSource::AppProvider, - L"animationErrorSource should already be AppProvider before processing the frame with both timestamps."); - - FrameData frame3{}; - frame3.finalState = PresentResult::Presented; - frame3.displayed.PushBack({ FrameType::Application, 3000 }); - auto results = ComputeMetricsForPresent(qpc, frame2, &frame3, state); - - Assert::AreEqual(size_t(1), results.size()); - Assert::IsTrue(HasMetricValue(results[0].metrics.msAnimationError), - L"msAnimationError should be computed when AppProvider remains active and appSimStartTime is present."); - - double appExpected = qpc.DeltaUnsignedMilliSeconds(100, 140) - - qpc.DeltaUnsignedMilliSeconds(1000, 1050); - double pclExpected = qpc.DeltaUnsignedMilliSeconds(100, 180) - - qpc.DeltaUnsignedMilliSeconds(1000, 1050); - - AssertAreEqualWithinTolerance(-0.001, appExpected, 0.0001, - L"Test setup should produce a distinct app-based animation error."); - AssertAreEqualWithinTolerance(0.003, pclExpected, 0.0001, - L"Test setup should produce a different hypothetical pcl-based animation error."); - AssertAreEqualWithinTolerance(appExpected, results[0].metrics.msAnimationError, 0.0001, - L"msAnimationError should use app timing when AppProvider is authoritative."); - Assert::IsTrue(state.animationErrorSource == AnimationErrorSource::AppProvider, - L"animationErrorSource should remain AppProvider after processing a frame with both timestamps."); - } - - TEST_METHOD(AnimationError_AppProvider_ZeroDisplayDelta_ErrorIsSimElapsed) - { - // Scenario: Display elapsed = 0 (same screen time) - // Expected: msAnimationError = simElapsed - 0 - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - state.animationErrorSource = AnimationErrorSource::AppProvider; - - FrameData frame1{}; - frame1.presentStartTime = 1000; - frame1.timeInPresent = 100; - frame1.appSimStartTime = 100; - frame1.finalState = PresentResult::Presented; - frame1.displayed.PushBack({ FrameType::Application, 1000 }); - - FrameData frame2{}; - frame2.presentStartTime = 2000; - frame2.timeInPresent = 100; - frame2.appSimStartTime = 150; // sim elapsed = 50 - frame2.finalState = PresentResult::Presented; - frame2.displayed.PushBack({ FrameType::Application, 1000 }); // same screen time! - - FrameData dummyNext{}; - dummyNext.finalState = PresentResult::Presented; - dummyNext.displayed.PushBack({ FrameType::Application, 2000 }); - ComputeMetricsForPresent(qpc, frame1, &dummyNext, state); - - FrameData frame3{}; - frame3.finalState = PresentResult::Presented; - frame3.displayed.PushBack({ FrameType::Application, 3000 }); - auto results = ComputeMetricsForPresent(qpc, frame2, &frame3, state); - - Assert::IsFalse(HasMetricValue(results[0].metrics.msAnimationError)); - } - - // Section C: Animation Error – PCLatency Source - - TEST_METHOD(AnimationError_PCLatency_TwoFrames_ValidPclSimStart) - { - // Scenario: PCL source, sim elapsed = 40, display elapsed = 50 - // Expected: msAnimationError = -0.001 ms - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - state.animationErrorSource = AnimationErrorSource::PCLatency; - - FrameData frame1{}; - frame1.presentStartTime = 1000; - frame1.timeInPresent = 100; - frame1.pclSimStartTime = 100; - frame1.finalState = PresentResult::Presented; - frame1.displayed.PushBack({ FrameType::Application, 1000 }); - - FrameData frame2{}; - frame2.presentStartTime = 2000; - frame2.timeInPresent = 100; - frame2.pclSimStartTime = 140; // PCL sim elapsed = 40 - frame2.finalState = PresentResult::Presented; - frame2.displayed.PushBack({ FrameType::Application, 1050 }); // display elapsed = 50 - - FrameData dummyNext{}; - dummyNext.finalState = PresentResult::Presented; - dummyNext.displayed.PushBack({ FrameType::Application, 2000 }); - ComputeMetricsForPresent(qpc, frame1, &dummyNext, state); - - FrameData frame3{}; - frame3.finalState = PresentResult::Presented; - frame3.displayed.PushBack({ FrameType::Application, 3000 }); - auto results = ComputeMetricsForPresent(qpc, frame2, &frame3, state); - - Assert::IsTrue(HasMetricValue(results[0].metrics.msAnimationError)); - double simElapsed = qpc.DeltaUnsignedMilliSeconds(100, 140); // 0.004 ms - double displayElapsed = qpc.DeltaUnsignedMilliSeconds(1000, 1050); // 0.005 ms - double expected = simElapsed - displayElapsed; // -0.001 ms - AssertAreEqualWithinTolerance(expected, results[0].metrics.msAnimationError, 0.0001); - } - - TEST_METHOD(AnimationError_PCLatency_CurrentPclSimStartZero_Nullopt) - { - // Scenario: PCL source, but current frame has pclSimStartTime = 0 - // and no appSimStartTime. - // Expected: msAnimationError = std::nullopt with no transition and - // no anchor clearing while PCLatency remains active. - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - state.animationErrorSource = AnimationErrorSource::PCLatency; - - FrameData frame1{}; - frame1.presentStartTime = 1000; - frame1.timeInPresent = 100; - frame1.pclSimStartTime = 100; - frame1.finalState = PresentResult::Presented; - frame1.displayed.PushBack({ FrameType::Application, 1000 }); - - FrameData frame2{}; - frame2.presentStartTime = 2000; - frame2.timeInPresent = 100; - frame2.pclSimStartTime = 0; // PCL unavailable - frame2.appSimStartTime = 0; // app unavailable - frame2.finalState = PresentResult::Presented; - frame2.displayed.PushBack({ FrameType::Application, 1050 }); - - FrameData dummyNext{}; - dummyNext.finalState = PresentResult::Presented; - dummyNext.displayed.PushBack({ FrameType::Application, 2000 }); - auto seedResults = ComputeMetricsForPresent(qpc, frame1, &dummyNext, state); - - Assert::AreEqual(size_t(1), seedResults.size()); - Assert::IsTrue(state.animationErrorSource == AnimationErrorSource::PCLatency, - L"animationErrorSource should be seeded to PCLatency by the prior displayed PCL frame."); - Assert::AreEqual(uint64_t(100), state.firstAppSimStartTime, - L"firstAppSimStartTime should be seeded from the prior displayed PCL frame."); - Assert::AreEqual(uint64_t(100), state.lastDisplayedSimStartTime, - L"lastDisplayedSimStartTime should be seeded from the prior displayed PCL frame."); - Assert::AreEqual(uint64_t(1000), state.lastDisplayedAppScreenTime, - L"lastDisplayedAppScreenTime should be seeded from the prior displayed PCL frame."); - - FrameData frame3{}; - frame3.finalState = PresentResult::Presented; - frame3.displayed.PushBack({ FrameType::Application, 3000 }); - auto results = ComputeMetricsForPresent(qpc, frame2, &frame3, state); - - Assert::AreEqual(size_t(1), results.size()); - Assert::IsFalse(HasMetricValue(results[0].metrics.msAnimationError), - L"msAnimationError should be nullopt when PCL source unavailable"); - Assert::IsTrue(state.animationErrorSource == AnimationErrorSource::PCLatency, - L"animationErrorSource should remain PCLatency when no transition is allowed."); - Assert::AreEqual(uint64_t(100), state.firstAppSimStartTime, - L"firstAppSimStartTime should remain unchanged when the active source is missing."); - Assert::AreEqual(uint64_t(100), state.lastDisplayedSimStartTime, - L"lastDisplayedSimStartTime should remain unchanged when the active source is missing."); - Assert::AreEqual(uint64_t(1000), state.lastDisplayedAppScreenTime, - L"lastDisplayedAppScreenTime should remain unchanged when the active source is missing."); - } - - TEST_METHOD(AnimationError_PCLatency_TransitionFromZero_FirstValidPclSimStart) - { - // Scenario: First frame with valid PCL sim start - // Expected: msAnimationError = std::nullopt (no prior PCL frame) - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - state.animationErrorSource = AnimationErrorSource::PCLatency; - - FrameData present{}; - present.presentStartTime = 1000; - present.timeInPresent = 100; - present.pclSimStartTime = 100; // first valid PCL - present.finalState = PresentResult::Presented; - present.displayed.PushBack({ FrameType::Application, 1000 }); - - FrameData nextPresent{}; - nextPresent.finalState = PresentResult::Presented; - nextPresent.displayed.PushBack({ FrameType::Application, 2000 }); - - auto results = ComputeMetricsForPresent(qpc, present, &nextPresent, state); - - Assert::IsFalse(HasMetricValue(results[0].metrics.msAnimationError), - L"msAnimationError should be nullopt on first valid PCL frame"); - } - - TEST_METHOD(AnimationError_PCLatency_TransitionToAppProvider_AppOnly_Nullopt) - { - // Scenario: Active source is PCLatency and the current displayed frame - // has appSimStartTime but no pclSimStartTime. - // Expected: This is an allowed upgrade to AppProvider, so - // msAnimationError is nullopt and state reseeds to AppProvider. - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - state.animationErrorSource = AnimationErrorSource::PCLatency; - state.firstAppSimStartTime = 300; - state.lastDisplayedSimStartTime = 320; - state.lastDisplayedAppScreenTime = 900'000; - - FrameData frame{}; - frame.presentStartTime = 1'000'000; - frame.timeInPresent = 500; - frame.readyTime = 1'500'000; - frame.pclSimStartTime = 0; - frame.appSimStartTime = 700; - frame.finalState = PresentResult::Presented; - frame.displayed.PushBack({ FrameType::Application, 1'900'000 }); - - FrameData next{}; - next.presentStartTime = 2'000'000; - next.timeInPresent = 400; - next.readyTime = 2'500'000; - next.finalState = PresentResult::Presented; - next.displayed.PushBack({ FrameType::Application, 2'900'000 }); - - auto results = ComputeMetricsForPresent(qpc, frame, &next, state); - - Assert::AreEqual(size_t(1), results.size()); - Assert::IsFalse(HasMetricValue(results[0].metrics.msAnimationError), - L"msAnimationError should be nullopt on the PCLatency to AppProvider transition frame when only appSimStartTime is present."); - Assert::IsTrue(state.animationErrorSource == AnimationErrorSource::AppProvider, - L"animationErrorSource should transition to AppProvider when appSimStartTime becomes available while PCLatency is active."); - Assert::AreEqual(uint64_t(700), state.firstAppSimStartTime, - L"firstAppSimStartTime should reseed to appSimStartTime for the new AppProvider timeline."); - Assert::AreEqual(uint64_t(700), state.lastDisplayedSimStartTime, - L"lastDisplayedSimStartTime should reseed to appSimStartTime on the AppProvider transition frame."); - Assert::AreEqual(uint64_t(1'900'000), state.lastDisplayedAppScreenTime, - L"lastDisplayedAppScreenTime should update to the displayed app screen time on transition."); - } - - TEST_METHOD(AnimationError_PCLatency_TransitionToAppProvider_BothPresent_Nullopt) - { - // Scenario: Active source is PCLatency and the current displayed frame - // has both appSimStartTime and pclSimStartTime. - // Expected: This is an allowed upgrade to AppProvider, so - // msAnimationError is nullopt and state reseeds to AppProvider. - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - state.animationErrorSource = AnimationErrorSource::PCLatency; - state.firstAppSimStartTime = 300; - state.lastDisplayedSimStartTime = 320; - state.lastDisplayedAppScreenTime = 900'000; - - FrameData frame{}; - frame.presentStartTime = 1'000'000; - frame.timeInPresent = 500; - frame.readyTime = 1'500'000; - frame.pclSimStartTime = 950; - frame.appSimStartTime = 700; - frame.finalState = PresentResult::Presented; - frame.displayed.PushBack({ FrameType::Application, 1'950'000 }); - - FrameData next{}; - next.presentStartTime = 2'000'000; - next.timeInPresent = 400; - next.readyTime = 2'500'000; - next.finalState = PresentResult::Presented; - next.displayed.PushBack({ FrameType::Application, 2'950'000 }); - - auto results = ComputeMetricsForPresent(qpc, frame, &next, state); - - Assert::AreEqual(size_t(1), results.size()); - Assert::IsFalse(HasMetricValue(results[0].metrics.msAnimationError), - L"msAnimationError should be nullopt on the PCLatency to AppProvider transition frame when both sources are present."); - Assert::IsTrue(state.animationErrorSource == AnimationErrorSource::AppProvider, - L"animationErrorSource should transition to AppProvider when appSimStartTime becomes available while PCLatency is active."); - Assert::AreEqual(uint64_t(700), state.firstAppSimStartTime, - L"firstAppSimStartTime should reseed to appSimStartTime for the new AppProvider timeline."); - Assert::AreEqual(uint64_t(700), state.lastDisplayedSimStartTime, - L"lastDisplayedSimStartTime should reseed to appSimStartTime on the AppProvider transition frame."); - Assert::AreNotEqual(uint64_t(950), state.firstAppSimStartTime, - L"firstAppSimStartTime should not reseed from pclSimStartTime when AppProvider is available."); - Assert::AreNotEqual(uint64_t(950), state.lastDisplayedSimStartTime, - L"lastDisplayedSimStartTime should prove AppProvider wins over PCLatency in the both-present transition case."); - Assert::AreEqual(uint64_t(1'950'000), state.lastDisplayedAppScreenTime, - L"lastDisplayedAppScreenTime should update to the displayed app screen time on transition."); - } - - // Section D: Animation Error – CpuStart Source - - TEST_METHOD(AnimationError_CpuStart_ComputedFromCpuPresent) - { - // Scenario: CpuStart source, CPU-derived sim times - // Expected: Error computed from CPU present end times - // Note: Need baseline frame to establish lastDisplayedSimStartTime - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - state.animationErrorSource = AnimationErrorSource::CpuStart; - - // Baseline frame to establish initial state - FrameData frame1{}; - frame1.presentStartTime = 800; - frame1.timeInPresent = 100; // CPU sim start for next frame = 900 - frame1.finalState = PresentResult::Presented; - frame1.displayed.PushBack({ FrameType::Application, 1900 }); - - FrameData frame2{}; - frame2.presentStartTime = 1000; - frame2.timeInPresent = 100; // CPU sim start for next frame = 1100 - frame2.finalState = PresentResult::Presented; - frame2.displayed.PushBack({ FrameType::Application, 2000 }); - - FrameData frame3{}; - frame3.presentStartTime = 1200; - frame3.timeInPresent = 100; // CPU sim start = 1300, elapsed from 1100 = 200 - frame3.finalState = PresentResult::Presented; - frame3.displayed.PushBack({ FrameType::Application, 2050 }); // display elapsed from 2000 = 50 - - FrameData dummyNext1{}; - dummyNext1.finalState = PresentResult::Presented; - dummyNext1.displayed.PushBack({ FrameType::Application, 2500 }); - ComputeMetricsForPresent(qpc, frame1, &dummyNext1, state); - - FrameData dummyNext2{}; - dummyNext2.finalState = PresentResult::Presented; - dummyNext2.displayed.PushBack({ FrameType::Application, 3000 }); - ComputeMetricsForPresent(qpc, frame2, &dummyNext2, state); - - FrameData frame4{}; - frame4.finalState = PresentResult::Presented; - frame4.displayed.PushBack({ FrameType::Application, 4000 }); - auto results = ComputeMetricsForPresent(qpc, frame3, &frame4, state); - - Assert::IsTrue(HasMetricValue(results[0].metrics.msAnimationError)); - double simElapsed = qpc.DeltaUnsignedMilliSeconds(1100, 1300); // 0.020 ms - double displayElapsed = qpc.DeltaUnsignedMilliSeconds(2000, 2050); // 0.005 ms - double expected = simElapsed - displayElapsed; // 0.015 ms - AssertAreEqualWithinTolerance(expected, results[0].metrics.msAnimationError, 0.0001); - } - - TEST_METHOD(AnimationError_CpuStart_Frame2DisplayIsGreaterThanFrame1Display) - { - // Scenario: CpuStart source, CPU-derived sim times - // Frame 2 has a display time earlier than Frame 1 - // Expected: NA reported for animation error - // Note: Need baseline frame to establish lastDisplayedSimStartTime - // and lastDisplayedScreenTime - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - state.animationErrorSource = AnimationErrorSource::CpuStart; - state.lastDisplayedScreenTime = 55454524262; - state.lastDisplayedSimStartTime = 55454168764; - state.lastDisplayedAppScreenTime = 55454524262; - FrameData frame3{}; - frame3.presentStartTime = 55454299820; - frame3.timeInPresent = 24537; - state.lastPresent = frame3; - - FrameData frame1{}; - frame1.presentStartTime = 55454457377; - frame1.timeInPresent = 2411; - frame1.finalState = PresentResult::Presented; - frame1.displayed.PushBack({ FrameType::Application, 55454512384 }); - - FrameData frame2{}; - frame2.presentStartTime = 55454612236; - frame2.timeInPresent = 3056; - frame2.finalState = PresentResult::Presented; - frame2.displayed.PushBack({ FrameType::Application, 55454615330 }); - - ComputeMetricsForPresent(qpc, frame1, nullptr, state); - auto results = ComputeMetricsForPresent(qpc, frame1, &frame2, state); - - Assert::IsFalse(HasMetricValue(results[0].metrics.msAnimationError)); - } - - TEST_METHOD(AnimationError_CpuStart_TransitionToAppProvider_Nullopt) - { - // Scenario: Source switches from CpuStart to AppProvider mid-stream - // Expected: msAnimationError = std::nullopt (first frame of new source) - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - state.animationErrorSource = AnimationErrorSource::CpuStart; - - FrameData frame1{}; - frame1.presentStartTime = 1000; - frame1.timeInPresent = 100; - frame1.finalState = PresentResult::Presented; - frame1.displayed.PushBack({ FrameType::Application, 2000 }); - - FrameData frame2{}; - frame2.presentStartTime = 2000; - frame2.timeInPresent = 100; - frame2.appSimStartTime = 100; // app instrumentation appears - frame2.finalState = PresentResult::Presented; - frame2.displayed.PushBack({ FrameType::Application, 2050 }); - - FrameData dummyNext{}; - dummyNext.finalState = PresentResult::Presented; - dummyNext.displayed.PushBack({ FrameType::Application, 3000 }); - ComputeMetricsForPresent(qpc, frame1, &dummyNext, state); - - FrameData frame3{}; - frame3.finalState = PresentResult::Presented; - frame3.displayed.PushBack({ FrameType::Application, 4000 }); - auto results = ComputeMetricsForPresent(qpc, frame2, &frame3, state); - - Assert::IsFalse(HasMetricValue(results[0].metrics.msAnimationError), - L"msAnimationError should be nullopt on source transition"); - Assert::IsTrue(state.animationErrorSource == AnimationErrorSource::AppProvider, - L"Source should auto-switch to AppProvider"); - } - - TEST_METHOD(AnimationError_CpuStart_BothPresent_TransitionsDirectlyToAppProvider_Nullopt) - { - // Scenario: Active source is CpuStart and the current displayed frame - // has both appSimStartTime and pclSimStartTime. - // Expected: This is treated as a direct transition to AppProvider, - // so msAnimationError is nullopt and AppProvider state is reseeded - // from appSimStartTime, not pclSimStartTime. - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - state.animationErrorSource = AnimationErrorSource::CpuStart; - state.firstAppSimStartTime = 300; - state.lastDisplayedSimStartTime = 320; - state.lastDisplayedAppScreenTime = 900'000; - - FrameData frame{}; - frame.presentStartTime = 1'000'000; - frame.timeInPresent = 500; - frame.readyTime = 1'500'000; - frame.appSimStartTime = 700; - frame.pclSimStartTime = 950; - frame.finalState = PresentResult::Presented; - frame.displayed.PushBack({ FrameType::Application, 1'950'000 }); - - FrameData next{}; - next.presentStartTime = 2'000'000; - next.timeInPresent = 400; - next.readyTime = 2'500'000; - next.finalState = PresentResult::Presented; - next.displayed.PushBack({ FrameType::Application, 2'950'000 }); + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); - auto results = ComputeMetricsForPresent(qpc, frame, &next, state); - - Assert::AreEqual(size_t(1), results.size()); - Assert::IsFalse(HasMetricValue(results[0].metrics.msAnimationError), - L"msAnimationError should be nullopt on the CpuStart to AppProvider transition frame when both sources are present."); - Assert::IsTrue(state.animationErrorSource == AnimationErrorSource::AppProvider, - L"animationErrorSource should transition directly to AppProvider when both appSimStartTime and pclSimStartTime are present while CpuStart is active."); - Assert::IsFalse(state.animationErrorSource == AnimationErrorSource::PCLatency, - L"CpuStart should transition directly to AppProvider, not to PCLatency, when both sources are present."); - Assert::AreEqual(uint64_t(700), state.firstAppSimStartTime, - L"firstAppSimStartTime should reseed to appSimStartTime for the new AppProvider timeline."); - Assert::AreEqual(uint64_t(700), state.lastDisplayedSimStartTime, - L"lastDisplayedSimStartTime should reseed to appSimStartTime on the AppProvider transition frame."); - Assert::AreNotEqual(uint64_t(950), state.firstAppSimStartTime, - L"firstAppSimStartTime should not reseed from pclSimStartTime when AppProvider is available."); - Assert::AreNotEqual(uint64_t(950), state.lastDisplayedSimStartTime, - L"lastDisplayedSimStartTime should prove AppProvider wins over PCLatency in the both-present CpuStart transition case."); - Assert::AreEqual(uint64_t(1'950'000), state.lastDisplayedAppScreenTime, - L"lastDisplayedAppScreenTime should update to the displayed app screen time on transition."); - } - - // Section E: Disabled or Edge Cases - - TEST_METHOD(AnimationError_NotAppDisplayed_BothNullopt) - { - // Scenario: Frame not app-displayed (wrong displayIndex) - // Expected: Both animation metrics = std::nullopt - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - state.animationErrorSource = AnimationErrorSource::AppProvider; - state.firstAppSimStartTime = 100; - state.lastDisplayedSimStartTime = 100; + FrameData seed{}; + seed.presentStartTime = 9'000; + seed.timeInPresent = 400; + seed.readyTime = 9'500; + seed.finalState = PresentResult::Presented; + seed.appSimStartTime = 8'000; + seed.displayed.PushBack({ FrameType::Application, 10'000 }); + Assert::AreEqual(size_t(0), swapChain.ProcessPresent(qpc, std::move(seed)).size()); FrameData present{}; - present.presentStartTime = 1000; - present.timeInPresent = 100; - present.appSimStartTime = 150; + present.presentStartTime = 10'000; + present.timeInPresent = 500; + present.readyTime = 20'000; present.finalState = PresentResult::Presented; - present.displayed.PushBack({ FrameType::Repeated, 2000 }); // Not Application! - - FrameData nextPresent{}; - nextPresent.finalState = PresentResult::Presented; - nextPresent.displayed.PushBack({ FrameType::Application, 3000 }); - - auto results = ComputeMetricsForPresent(qpc, present, &nextPresent, state); - - Assert::AreEqual(size_t(1), results.size()); - Assert::IsFalse(HasMetricValue(results[0].metrics.msAnimationError), - L"msAnimationError should be nullopt for non-app frames"); - Assert::IsFalse(HasMetricValue(results[0].metrics.msAnimationTime), - L"msAnimationTime should be nullopt for non-app frames"); - } + present.appSimStartTime = 9'500; + present.displayed.PushBack({ FrameType::Intel_XEFG, 11'000 }); + present.displayed.PushBack({ FrameType::Intel_XEFG, 11'500 }); + present.displayed.PushBack({ FrameType::Intel_XEFG, 12'000 }); + present.displayed.PushBack({ FrameType::Application, 12'500 }); + + auto rows = swapChain.ProcessPresent(qpc, std::move(present)); + + // Timeline origin (seed's AppA) plus the three generated rows of the closed + // interval. The closing app row (12'500) is not included: it has no lookahead yet. + Assert::AreEqual(size_t(4), rows.size()); + Assert::AreEqual((int)FrameType::Application, (int)rows[0].computed.metrics.frameType); + Assert::AreEqual(uint64_t(10'000), rows[0].computed.metrics.screenTimeQpc); + Assert::AreEqual((int)FrameType::Intel_XEFG, (int)rows[1].computed.metrics.frameType); + Assert::AreEqual((int)FrameType::Intel_XEFG, (int)rows[2].computed.metrics.frameType); + Assert::AreEqual((int)FrameType::Intel_XEFG, (int)rows[3].computed.metrics.frameType); + Assert::AreEqual(uint64_t(11'000), rows[1].computed.metrics.screenTimeQpc); + Assert::AreEqual(uint64_t(11'500), rows[2].computed.metrics.screenTimeQpc); + Assert::AreEqual(uint64_t(12'000), rows[3].computed.metrics.screenTimeQpc); + Assert::IsTrue(HasMetricValue(rows[1].computed.metrics.msAnimationTime)); + Assert::IsTrue(HasMetricValue(rows[2].computed.metrics.msAnimationTime)); + Assert::IsTrue(HasMetricValue(rows[3].computed.metrics.msAnimationTime)); + + FrameData lookahead{}; + lookahead.presentStartTime = 23'000; + lookahead.timeInPresent = 400; + lookahead.readyTime = 30'000; + lookahead.finalState = PresentResult::Presented; + lookahead.appSimStartTime = 20'000; + lookahead.displayed.PushBack({ FrameType::Application, 24'000 }); + + auto afterLookahead = swapChain.ProcessPresent(qpc, std::move(lookahead)); + Assert::AreEqual(size_t(1), afterLookahead.size()); + Assert::AreEqual((int)FrameType::Application, (int)afterLookahead[0].computed.metrics.frameType); + Assert::AreEqual(uint64_t(12'500), afterLookahead[0].computed.metrics.screenTimeQpc); + Assert::IsTrue(HasMetricValue(afterLookahead[0].computed.metrics.msAnimationTime)); + } + + TEST_METHOD(SeparatePresentGeneratedRows_PublishOnIntervalClose_ClosingAppAnchorWaitsForOwnLookahead) + { + // Design/AnimationErrorFrameGenerationAgentPrompts.md Agent 1, target scenario: + // AppA @ 100, sim 1000 + // Gen1 @ 108 + // Gen2 @ 116 + // AppB @ 124, sim 1024 + // After AppB arrives: Gen1 and Gen2 publish with animation metrics; AppB does + // not publish yet because no later displayed frame is available. After a later + // displayed frame arrives, AppB publishes with animation metrics and complete + // msDisplayedTime. + QpcConverter qpc(1000, 0); + UnifiedSwapChain swapChain{}; + + (void)Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1, 1, 1, {})); + + // AppA is the timeline origin; it is held until Gen1 supplies its lookahead. + Assert::AreEqual(size_t(0), Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 900, 100, 900, + { { FrameType::Application, 100 } }, 1000)).size()); + + // Gen1 completes AppA's lookahead, so AppA publishes here; Gen1 itself is queued. + Assert::AreEqual(size_t(1), Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1000, 1, 1000, + { { FrameType::Intel_XEFG, 108 } })).size()); + + // Gen2 is queued alongside Gen1; nothing publishes yet. + Assert::AreEqual(size_t(0), Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1001, 1, 1001, + { { FrameType::Intel_XEFG, 116 } })).size()); + + // AppB closes the interval [Gen1, Gen2, AppB]. Gen1 and Gen2 already have their + // own display timing complete (each supplied the next row's nextScreenTime as it + // arrived) and should publish now with resolved animation metrics. AppB is the + // only entry in its own present, so it still needs lookahead and must not publish. + auto afterAppB = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1100, 1, 1100, + { { FrameType::Application, 124 } }, 1024)); + + Assert::AreEqual(size_t(2), afterAppB.size()); + Assert::AreEqual((int)FrameType::Intel_XEFG, (int)afterAppB[0].frameType); + Assert::AreEqual((int)FrameType::Intel_XEFG, (int)afterAppB[1].frameType); + Assert::AreEqual(uint64_t(108), afterAppB[0].screenTimeQpc); + Assert::AreEqual(uint64_t(116), afterAppB[1].screenTimeQpc); + Assert::IsTrue(HasMetricValue(afterAppB[0].msAnimationTime)); + Assert::IsTrue(HasMetricValue(afterAppB[1].msAnimationTime)); + + for (const auto& row : afterAppB) { + Assert::IsFalse(row.frameType == FrameType::Application, + L"AppB must not publish before its own display-duration lookahead is known."); + } - TEST_METHOD(AnimationError_FirstFrameEver_BothMissingMetric) - { - // Scenario: Very first frame, no prior state - // Expected: Both animation metrics = std::QuietNaN - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; // all zeros - state.animationErrorSource = AnimationErrorSource::AppProvider; + // The next displayed frame supplies AppB's lookahead; AppB now publishes with + // complete animation metrics and msDisplayedTime. + auto afterLookahead = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1101, 1, 1101, + { { FrameType::Intel_XEFG, 132 } })); + + Assert::AreEqual(size_t(1), afterLookahead.size()); + Assert::AreEqual((int)FrameType::Application, (int)afterLookahead[0].frameType); + Assert::AreEqual(uint64_t(124), afterLookahead[0].screenTimeQpc); + Assert::IsTrue(HasMetricValue(afterLookahead[0].msAnimationTime)); + Assert::AreEqual(8.0, afterLookahead[0].msDisplayedTime, 0.0001); + } + + TEST_METHOD(MultiDisplayAcrossTwoPresents_IntervalBoundaryExcludesPriorAndNextIntervalRows) + { + // Design/AnimationErrorFrameGenerationAgentPrompts.md Agent 1, multi-display hard case: + // PresentA: gen1 @ 90, appA @ 100, gen2 @ 108, gen3 @ 116 + // PresentB: gen4 @ 124, gen5 @ 132, appB @ 140, gen6 @ 148 + // The interval from appA to appB is gen2, gen3, gen4, gen5, appB + // (displayIntervalCount = 5). gen1 belongs to the prior interval (closed when + // appA arrived) and gen6 belongs to the next interval (it only supplies appB's + // display-duration lookahead); neither must receive this interval's animation + // metrics. gen6 is split into its own present (rather than appended to PresentB) + // so that appB's own lookahead is not trivially available within the same Ingest + // call as the interval close -- this is what exposes the target behavior: gen2..gen5 + // must publish as soon as the interval resolves, without waiting for appB's lookahead. + QpcConverter qpc(1000, 0); + UnifiedSwapChain swapChain{}; + + (void)Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1, 1, 1, {})); + + // Origin app anchor before PresentA; held until gen1 supplies its lookahead. + Assert::AreEqual(size_t(0), Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 700, 50, 700, + { { FrameType::Application, 80 } }, 900)).size()); + + auto presentARows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 800, 50, 800, + { + { FrameType::Intel_XEFG, 90 }, + { FrameType::Application, 100 }, + { FrameType::Intel_XEFG, 108 }, + { FrameType::Intel_XEFG, 116 }, + }, + 1000)); + + // Origin publishes (gen1 supplied its lookahead). gen1 and appA also already have + // their own nextScreenTime from sibling entries in PresentA, so the + // origin-to-appA interval (gen1, appA) resolves and publishes immediately too. + // gen2 and gen3 (after appA) are not part of that closed interval and do not + // publish yet. + Assert::AreEqual(size_t(3), presentARows.size()); + Assert::AreEqual((int)FrameType::Application, (int)presentARows[0].frameType); + Assert::AreEqual(uint64_t(80), presentARows[0].screenTimeQpc); + Assert::AreEqual((int)FrameType::Intel_XEFG, (int)presentARows[1].frameType); + Assert::AreEqual(uint64_t(90), presentARows[1].screenTimeQpc); + Assert::AreEqual((int)FrameType::Application, (int)presentARows[2].frameType); + Assert::AreEqual(uint64_t(100), presentARows[2].screenTimeQpc); + + // PresentB: gen4, gen5, appB. appB is the last entry of its own present, so it has + // no lookahead yet. gen2, gen3 (held from PresentA), gen4, and gen5 all already have + // complete display timing once appB closes the interval and should publish now. + auto presentBRows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 900, 50, 900, + { + { FrameType::Intel_XEFG, 124 }, + { FrameType::Intel_XEFG, 132 }, + { FrameType::Application, 140 }, + }, + 1500)); + + // appA -> appB interval is exactly gen2, gen3, gen4, gen5, appB (displayIntervalCount = 5). + // Each consecutive screen time is 8 ticks apart and simStep = (1500 - 1000) / 5 = 100, + // so every row in the interval has msAnimationError = 100 - 8 = 92 and msAnimationTime + // advances by 100 per row from appA's published animation time (100). Only appB + // (140) is missing here: it still needs lookahead. + Assert::AreEqual(size_t(4), presentBRows.size()); + Assert::AreEqual((int)FrameType::Intel_XEFG, (int)presentBRows[0].frameType); + Assert::AreEqual((int)FrameType::Intel_XEFG, (int)presentBRows[1].frameType); + Assert::AreEqual((int)FrameType::Intel_XEFG, (int)presentBRows[2].frameType); + Assert::AreEqual((int)FrameType::Intel_XEFG, (int)presentBRows[3].frameType); + Assert::AreEqual(uint64_t(108), presentBRows[0].screenTimeQpc); + Assert::AreEqual(uint64_t(116), presentBRows[1].screenTimeQpc); + Assert::AreEqual(uint64_t(124), presentBRows[2].screenTimeQpc); + Assert::AreEqual(uint64_t(132), presentBRows[3].screenTimeQpc); + Assert::AreEqual(200.0, presentBRows[0].msAnimationTime, 0.0001); + Assert::AreEqual(300.0, presentBRows[1].msAnimationTime, 0.0001); + Assert::AreEqual(400.0, presentBRows[2].msAnimationTime, 0.0001); + Assert::AreEqual(500.0, presentBRows[3].msAnimationTime, 0.0001); + Assert::AreEqual(92.0, presentBRows[0].msAnimationError, 0.0001); + Assert::AreEqual(92.0, presentBRows[1].msAnimationError, 0.0001); + Assert::AreEqual(92.0, presentBRows[2].msAnimationError, 0.0001); + Assert::AreEqual(92.0, presentBRows[3].msAnimationError, 0.0001); + + for (const auto& row : presentBRows) { + Assert::IsFalse(row.frameType == FrameType::Application, + L"appB must not publish before its own display-duration lookahead is known."); + } - FrameData present{}; - present.presentStartTime = 1000; - present.timeInPresent = 100; - present.appSimStartTime = 0; // no instrumentation - present.pclSimStartTime = 0; - present.finalState = PresentResult::Presented; - present.displayed.PushBack({ FrameType::Application, 2000 }); + // gen6 supplies appB's lookahead. appB now publishes alone, with the same + // interval's resolved animation metrics (it must not pick up metrics from the + // next interval that gen6 will eventually close). + auto presentCRows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1000, 50, 1000, + { { FrameType::Intel_XEFG, 148 } })); + + Assert::AreEqual(size_t(1), presentCRows.size()); + Assert::AreEqual((int)FrameType::Application, (int)presentCRows[0].frameType); + Assert::AreEqual(uint64_t(140), presentCRows[0].screenTimeQpc); + Assert::AreEqual(600.0, presentCRows[0].msAnimationTime, 0.0001); + Assert::AreEqual(92.0, presentCRows[0].msAnimationError, 0.0001); + + // gen6 itself is still buffered: it has its own display timing but has not closed + // an interval against the next app anchor yet, so it must not have published. + auto afterGen6Only = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1100, 50, 1100, + { { FrameType::Intel_XEFG, 156 } })); + Assert::AreEqual(size_t(0), afterGen6Only.size()); + } + + TEST_METHOD(MultiDisplayWithSamePresentLookahead_ClosingAnchorUsesSiblingGeneratedRow_NextIntervalRowExcludedFromMetrics) + { + // Design/AnimationErrorFrameGenerationAgentPrompts.md Agent 1, multi-display hard + // case, exact shape: + // PresentA: gen1 @ 90, appA @ 100, gen2 @ 108, gen3 @ 116 + // PresentB: gen4 @ 124, gen5 @ 132, appB @ 140, gen6 @ 148 + // Here gen6 shares PresentB with appB and supplies appB's display-duration + // lookahead directly (no further present needed). The interval from appA to appB + // is still exactly gen2, gen3, gen4, gen5, appB (displayIntervalCount = 5); gen6 + // belongs to the next interval and must not receive this interval's resolved + // animation metrics even though it is the row that completes appB's display timing. + QpcConverter qpc(1000, 0); + UnifiedSwapChain swapChain{}; + + (void)Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1, 1, 1, {})); + + // Origin app anchor before PresentA; held until gen1 supplies its lookahead. + Assert::AreEqual(size_t(0), Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 700, 50, 700, + { { FrameType::Application, 80 } }, 900)).size()); + + auto presentARows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 800, 50, 800, + { + { FrameType::Intel_XEFG, 90 }, + { FrameType::Application, 100 }, + { FrameType::Intel_XEFG, 108 }, + { FrameType::Intel_XEFG, 116 }, + }, + 1000)); + Assert::AreEqual(size_t(3), presentARows.size()); - FrameData nextPresent{}; - nextPresent.finalState = PresentResult::Presented; - nextPresent.displayed.PushBack({ FrameType::Application, 3000 }); + auto presentBRows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 900, 50, 900, + { + { FrameType::Intel_XEFG, 124 }, + { FrameType::Intel_XEFG, 132 }, + { FrameType::Application, 140 }, + { FrameType::Intel_XEFG, 148 }, + }, + 1500)); + + // gen6 (148) completes appB's display timing within the same present, so the + // whole appA -> appB interval (gen2, gen3, gen4, gen5, appB) is both + // animation-complete and display-timing-complete by the end of this Ingest call + // and publishes here. simStep = (1500 - 1000) / 5 = 100, each consecutive screen + // time is 8 ticks apart, and animation time advances from appA's published + // animation time (100), so every row has msAnimationError = 100 - 8 = 92. + Assert::AreEqual(size_t(5), presentBRows.size()); + Assert::AreEqual((int)FrameType::Intel_XEFG, (int)presentBRows[0].frameType); + Assert::AreEqual((int)FrameType::Intel_XEFG, (int)presentBRows[1].frameType); + Assert::AreEqual((int)FrameType::Intel_XEFG, (int)presentBRows[2].frameType); + Assert::AreEqual((int)FrameType::Intel_XEFG, (int)presentBRows[3].frameType); + Assert::AreEqual((int)FrameType::Application, (int)presentBRows[4].frameType); + Assert::AreEqual(uint64_t(108), presentBRows[0].screenTimeQpc); + Assert::AreEqual(uint64_t(116), presentBRows[1].screenTimeQpc); + Assert::AreEqual(uint64_t(124), presentBRows[2].screenTimeQpc); + Assert::AreEqual(uint64_t(132), presentBRows[3].screenTimeQpc); + Assert::AreEqual(uint64_t(140), presentBRows[4].screenTimeQpc); + Assert::AreEqual(200.0, presentBRows[0].msAnimationTime, 0.0001); + Assert::AreEqual(300.0, presentBRows[1].msAnimationTime, 0.0001); + Assert::AreEqual(400.0, presentBRows[2].msAnimationTime, 0.0001); + Assert::AreEqual(500.0, presentBRows[3].msAnimationTime, 0.0001); + Assert::AreEqual(600.0, presentBRows[4].msAnimationTime, 0.0001); + Assert::AreEqual(92.0, presentBRows[0].msAnimationError, 0.0001); + Assert::AreEqual(92.0, presentBRows[1].msAnimationError, 0.0001); + Assert::AreEqual(92.0, presentBRows[2].msAnimationError, 0.0001); + Assert::AreEqual(92.0, presentBRows[3].msAnimationError, 0.0001); + Assert::AreEqual(92.0, presentBRows[4].msAnimationError, 0.0001); + + // gen6 (148) is excluded: despite supplying appB's lookahead, it must not publish + // with this interval's animation metrics -- it belongs to the next interval. + for (const auto& row : presentBRows) { + Assert::AreNotEqual(uint64_t(148), row.screenTimeQpc, + L"gen6 only supplies appB's lookahead; it must not publish with this interval's metrics."); + } - auto results = ComputeMetricsForPresent(qpc, present, &nextPresent, state); + // gen6 itself is still buffered: it has its own display timing (it supplied + // appB's lookahead) but has not closed an interval against the next app anchor + // yet, so it must not have published with this or any animation metrics. + auto afterGen6Only = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1000, 50, 1000, + { { FrameType::Intel_XEFG, 156 } })); + Assert::AreEqual(size_t(0), afterGen6Only.size()); + } + + TEST_METHOD(GeneratedRowReleasedBeforeSamePresentAppRow_DoesNotBecomeAppHistoryAnchor) + { + // A present's generated row can become display-timing-complete (and therefore + // publish) before that same present's own app row, which still needs lookahead. + // The generated row must not advance app-history swap-chain state (lastAppPresent, + // lastDisplayedAppScreenTime, lastDisplayedSimStartTime, animationErrorSource); + // those must only advance once the present's actual app row is applied. Separately, + // lastPresent/lastDisplayedScreenTime must still advance to whichever row actually + // carries the explicit state-update role for its own present (the gen-only present + // 800's sole row, then PresentB's app row), never to a present whose role-bearing + // row has not released yet. + QpcConverter qpc(1000, 0); + UnifiedSwapChain swapChain{}; + + (void)Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1, 1, 1, {})); + + // Origin app anchor; held until Gen1 (next present) supplies its lookahead. + Assert::AreEqual(size_t(0), Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 700, 50, 700, + { { FrameType::Application, 100 } }, 1000)).size()); + + // Gen1 completes the origin's lookahead; origin publishes with app-history state + // (lastAppPresent etc.) reflecting screenTime 100 / sim 1000. + Assert::AreEqual(size_t(1), Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 800, 50, 800, + { { FrameType::Intel_XEFG, 108 } })).size()); + + Assert::IsTrue(swapChain.swapChain.lastAppPresent.has_value()); + Assert::AreEqual(uint64_t(700), swapChain.swapChain.lastAppPresent.value().presentStartTime); + Assert::AreEqual(uint64_t(100), swapChain.swapChain.lastDisplayedAppScreenTime); + Assert::AreEqual(uint64_t(1000), swapChain.swapChain.lastDisplayedSimStartTime); + Assert::AreEqual(uint64_t(700), swapChain.swapChain.lastPresent.value().presentStartTime); + Assert::AreEqual(uint64_t(100), swapChain.swapChain.lastDisplayedScreenTime); + Assert::IsTrue(swapChain.swapChain.animationErrorSource == AnimationErrorSource::AppProvider); + + // Gen1 (present 800) is itself a displayed present with no app row of its own, + // so once it is the row that actually releases, it represents present 800 for + // lastPresent/lastDisplayedScreenTime even though it must not touch app-history + // state (lastAppPresent etc., asserted above). It has not released yet here + // (still buffered awaiting display lookahead from PresentB below), so lastPresent + // must still be the origin present, not 800. + Assert::AreEqual(uint64_t(700), swapChain.swapChain.lastPresent.value().presentStartTime, + L"Gen1 has not released yet; lastPresent must not advance past the origin present."); + + // PresentB carries Gen2 then its own app row (App2, sim 1024) as the last entry. + // Gen2 already knows its own nextScreenTime (App2 follows it in the same present), + // so once App2 closes the interval, Gen2 should publish immediately. App2 itself + // is the last display entry in PresentB, so it still needs lookahead. + auto presentBRows = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 900, 50, 900, + { { FrameType::Intel_XEFG, 116 }, { FrameType::Application, 124 } }, 1024)); + + Assert::AreEqual(size_t(2), presentBRows.size(), + L"Gen1 and Gen2 publish on interval close even though App2 still awaits lookahead."); + for (const auto& row : presentBRows) { + Assert::IsFalse(row.frameType == FrameType::Application, + L"App2 must not publish before its own display-duration lookahead is known."); + } - Assert::IsFalse(HasMetricValue(results[0].metrics.msAnimationError)); - Assert::IsFalse(HasMetricValue(results[0].metrics.msAnimationTime)); + // App2 has not been applied yet: app-history state must still reflect the origin + // present, not PresentB, even though PresentB's generated rows already released. + Assert::AreEqual(uint64_t(700), swapChain.swapChain.lastAppPresent.value().presentStartTime, + L"A generated row must not become the app history anchor for a present whose app row publishes later."); + Assert::AreEqual(uint64_t(100), swapChain.swapChain.lastDisplayedAppScreenTime); + Assert::AreEqual(uint64_t(1000), swapChain.swapChain.lastDisplayedSimStartTime); + Assert::IsTrue(swapChain.swapChain.animationErrorSource == AnimationErrorSource::AppProvider, + L"A non-role-bearing generated row (Gen2) must not disturb animationErrorSource."); + + // Gen1 (present 800) has now released and has no app row of its own, so it + // legitimately represents present 800 for lastPresent/lastDisplayedScreenTime. + // Gen2 (present 900) released alongside it but PresentB has its own app row + // (App2), so Gen2's role is None and it must not move lastPresent/ + // lastDisplayedScreenTime past Gen1 to PresentB/116. + Assert::AreEqual(uint64_t(800), swapChain.swapChain.lastPresent.value().presentStartTime, + L"Gen1 has no app row in its own present, so it represents present 800 once released."); + Assert::AreEqual(uint64_t(108), swapChain.swapChain.lastDisplayedScreenTime, + L"Gen2 (PresentB) must not advance lastDisplayedScreenTime ahead of App2."); + + // The next displayed frame supplies App2's lookahead; App2 publishes alone and now + // correctly becomes the app history anchor for PresentB. + auto afterLookahead = Process(qpc, swapChain, MakeFrame(PresentResult::Presented, 1000, 50, 1000, + { { FrameType::Intel_XEFG, 132 } })); + + Assert::AreEqual(size_t(1), afterLookahead.size()); + Assert::AreEqual((int)FrameType::Application, (int)afterLookahead[0].frameType); + Assert::AreEqual(uint64_t(124), afterLookahead[0].screenTimeQpc); + + Assert::AreEqual(uint64_t(900), swapChain.swapChain.lastAppPresent.value().presentStartTime); + Assert::AreEqual(uint64_t(124), swapChain.swapChain.lastDisplayedAppScreenTime); + Assert::AreEqual(uint64_t(1024), swapChain.swapChain.lastDisplayedSimStartTime); + Assert::AreEqual(uint64_t(900), swapChain.swapChain.lastPresent.value().presentStartTime, + L"App2 represents PresentB once it releases, so lastPresent now advances to it."); + Assert::AreEqual(uint64_t(124), swapChain.swapChain.lastDisplayedScreenTime); + Assert::IsTrue(swapChain.swapChain.animationErrorSource == AnimationErrorSource::AppProvider); } + }; - TEST_METHOD(AnimationError_BackwardsScreenTime_ErrorStillComputed) - { - // Scenario: Screen time goes backward - // Expected: nullopt for animation error - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - state.animationErrorSource = AnimationErrorSource::AppProvider; - - FrameData frame1{}; - frame1.presentStartTime = 1000; - frame1.timeInPresent = 100; - frame1.appSimStartTime = 100; - frame1.finalState = PresentResult::Presented; - frame1.displayed.PushBack({ FrameType::Application, 1100 }); - - FrameData frame2{}; - frame2.presentStartTime = 2000; - frame2.timeInPresent = 100; - frame2.appSimStartTime = 150; // sim elapsed = 50 - frame2.finalState = PresentResult::Presented; - frame2.displayed.PushBack({ FrameType::Application, 1050 }); // screen time backward! - - FrameData dummyNext{}; - dummyNext.finalState = PresentResult::Presented; - dummyNext.displayed.PushBack({ FrameType::Application, 2000 }); - ComputeMetricsForPresent(qpc, frame1, &dummyNext, state); - - FrameData frame3{}; - frame3.finalState = PresentResult::Presented; - frame3.displayed.PushBack({ FrameType::Application, 3000 }); - auto results = ComputeMetricsForPresent(qpc, frame2, &frame3, state); + // ============================================================================ + // SECTION: Input Latency Tests + // ============================================================================ - Assert::IsFalse(HasMetricValue(results[0].metrics.msAnimationError), - L"Error should be nullopt with backwards screen time"); - } + TEST_CLASS(InputLatencyTests) + { + public: + // ======================================================================== + // V1: ComputeMetricsForPresent (one row per call) + // ======================================================================== - TEST_METHOD(AnimationError_VeryLargeCadenceMismatch_LargeError) + TEST_METHOD(InputLatency_ClickToPhoton_DisplayedFrame_UsesOwnClickTime) { - // Scenario: Sim running much faster than display - // Expected: Large positive error - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - state.animationErrorSource = AnimationErrorSource::AppProvider; - - FrameData frame1{}; - frame1.presentStartTime = 1000; - frame1.timeInPresent = 100; - frame1.appSimStartTime = 100; - frame1.finalState = PresentResult::Presented; - frame1.displayed.PushBack({ FrameType::Application, 1000 }); - - FrameData frame2{}; - frame2.presentStartTime = 2000; - frame2.timeInPresent = 100; - frame2.appSimStartTime = 500; // sim elapsed = 400 ticks - frame2.finalState = PresentResult::Presented; - frame2.displayed.PushBack({ FrameType::Application, 1010 }); // display elapsed = 10 ticks - - FrameData dummyNext{}; - dummyNext.finalState = PresentResult::Presented; - dummyNext.displayed.PushBack({ FrameType::Application, 2000 }); - ComputeMetricsForPresent(qpc, frame1, &dummyNext, state); - - FrameData frame3{}; - frame3.finalState = PresentResult::Presented; - frame3.displayed.PushBack({ FrameType::Application, 3000 }); - auto results = ComputeMetricsForPresent(qpc, frame2, &frame3, state); - - Assert::IsTrue(HasMetricValue(results[0].metrics.msAnimationError)); - double simElapsed = qpc.DeltaUnsignedMilliSeconds(100, 500); // 0.040 ms - double displayElapsed = qpc.DeltaUnsignedMilliSeconds(1000, 1010); // 0.001 ms - double expected = simElapsed - displayElapsed; // 0.039 ms - AssertAreEqualWithinTolerance(expected, results[0].metrics.msAnimationError, 0.0001, - L"Large cadence mismatch should produce large positive error"); - } - - TEST_METHOD(AnimationError_RepeatedFrameType_BothNullopt) - { - // Scenario: Frame displayed but type is Repeated, not Application - // Expected: Animation metrics should be nullopt QpcConverter qpc(10'000'000, 0); SwapChainCoreState state{}; - state.animationErrorSource = AnimationErrorSource::AppProvider; - state.firstAppSimStartTime = 100; - state.lastDisplayedSimStartTime = 100; - FrameData present{}; - present.presentStartTime = 1000; - present.timeInPresent = 100; - present.appSimStartTime = 150; - present.finalState = PresentResult::Presented; - present.displayed.PushBack({ FrameType::Repeated, 2000 }); + FrameData p1{}; + p1.presentStartTime = 500'000; + p1.timeInPresent = 100'000; + p1.mouseClickTime = 400'000; + p1.inputTime = 0; + p1.appSimStartTime = 450'000; + p1.finalState = PresentResult::Presented; + p1.displayed.PushBack({ FrameType::Application, 1'000'000 }); - FrameData nextPresent{}; - nextPresent.finalState = PresentResult::Presented; - nextPresent.displayed.PushBack({ FrameType::Application, 3000 }); + auto p1_results = ComputeMetricsForPresent(qpc, p1, state); - auto results = ComputeMetricsForPresent(qpc, present, &nextPresent, state); + Assert::AreEqual(size_t(1), p1_results.size()); + Assert::IsTrue(HasMetricValue(p1_results[0].metrics.msClickToPhotonLatency)); - Assert::IsFalse(HasMetricValue(results[0].metrics.msAnimationError), - L"msAnimationError should be nullopt for Repeated frame type"); - Assert::IsFalse(HasMetricValue(results[0].metrics.msAnimationTime), - L"msAnimationTime should be nullopt for Repeated frame type"); + double expected = qpc.DeltaUnsignedMilliSeconds(400'000, 1'000'000); + AssertAreEqualWithinTolerance(expected, p1_results[0].metrics.msClickToPhotonLatency, 0.0001); + Assert::AreEqual(uint64_t(0), state.lastReceivedNotDisplayedMouseClickTime); } - TEST_METHOD(AnimationError_MultipleDisplayInstances_OnlyLastAppIndex) + TEST_METHOD(InputLatency_ClickToPhoton_DisplayedFrame_UsesOwnClickTime_ProcessPresent) { - // Scenario: Present has 3 display instances, only middle one is Application - // Expected: Animation computed only for Application display instance QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - state.animationErrorSource = AnimationErrorSource::AppProvider; - - // Setup prior frame - FrameData frame1{}; - frame1.presentStartTime = 1000; - frame1.timeInPresent = 100; - frame1.appSimStartTime = 100; - frame1.finalState = PresentResult::Presented; - frame1.displayed.PushBack({ FrameType::Application, 1000 }); - - // Frame with multiple display instances - FrameData frame2{}; - frame2.presentStartTime = 2000; - frame2.timeInPresent = 100; - frame2.appSimStartTime = 150; - frame2.finalState = PresentResult::Presented; - frame2.displayed.PushBack({ FrameType::Repeated, 2000 }); // [0] - frame2.displayed.PushBack({ FrameType::Application, 2050 }); // [1] - appIndex - frame2.displayed.PushBack({ FrameType::Repeated, 2100 }); // [2] - - FrameData dummyNext{}; - dummyNext.finalState = PresentResult::Presented; - dummyNext.displayed.PushBack({ FrameType::Application, 3000 }); - ComputeMetricsForPresent(qpc, frame1, &dummyNext, state); - - // Process frame2 without next (should process [0] and [1]) - auto resultsPartial = ComputeMetricsForPresent(qpc, frame2, nullptr, state); - Assert::AreEqual(size_t(2), resultsPartial.size()); - - // First display instance (Repeated) - no animation metrics - Assert::IsFalse(HasMetricValue(resultsPartial[0].metrics.msAnimationError), - L"Display [0] (Repeated) should not have animation error"); - - // Second display instance (Application) - has animation metrics - Assert::IsTrue(HasMetricValue(resultsPartial[1].metrics.msAnimationError), - L"Display [1] (Application) should have animation error"); - - double simElapsed = qpc.DeltaUnsignedMilliSeconds(100, 150); - double displayElapsed = qpc.DeltaUnsignedMilliSeconds(1000, 2050); - double expected = simElapsed - displayElapsed; - AssertAreEqualWithinTolerance(expected, resultsPartial[1].metrics.msAnimationError, 0.0001); - } - TEST_METHOD(Animation_AppProvider_PendingSequence_P1P2P3) - { - // This test mimics the real ReportMetrics pipeline for a single swapchain: - // - // P1 arrives: - // - ComputeMetricsForPresent(P1, nullptr, state) // Case 2, pending = P1 - // - // P2 arrives: - // - ComputeMetricsForPresent(P1, &P2, state) // Case 3, finalize P1 - // - ComputeMetricsForPresent(P2, nullptr, state) // Case 2, pending = P2 - // - // P3 arrives: - // - ComputeMetricsForPresent(P2, &P3, state) // Case 3, finalize P2 - // - ComputeMetricsForPresent(P3, nullptr, state) // Case 2, pending = P3 - // - // We verify: - // - P1's final metrics: no animation error/time (it seeds animation state). - // - P2's final metrics: non-null msAnimationTime & msAnimationError == 0.0. - // - SwapChainCoreState's animation fields evolve correctly: - // firstAppSimStartTime = 100 - // lastDisplayedSimStartTime = 200 after P2 - // lastDisplayedAppScreenTime = P2's screenTime - // - // QPC frequency = 10 MHz. - // App sim times: P1=100, P2=200, P3=300 - // Screen times: P1=1'000'000, P2=1'100'000, P3=1'200'000 - // -> sim Δ P1→P2: 100 ticks = 0.01 ms - // -> display Δ P1→P2: 100'000 ticks = 0.01 ms - // => animation error for P2 = 0.0 ms - // => animation time for P2 = (200 - 100) / 10e6 = 0.01 ms + UnifiedSwapChain swapChain{}; - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; + + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); - // -------------------------------------------------------------------- - // P1: first displayed app frame with AppProvider data - // -------------------------------------------------------------------- FrameData p1{}; p1.presentStartTime = 500'000; - p1.timeInPresent = 10'000; - p1.readyTime = 510'000; - p1.appSimStartTime = 475'000; - p1.pclSimStartTime = 0; + p1.timeInPresent = 100'000; + p1.mouseClickTime = 400'000; + p1.inputTime = 0; + p1.appSimStartTime = 450'000; p1.finalState = PresentResult::Presented; p1.displayed.PushBack({ FrameType::Application, 1'000'000 }); - // Arrival of P1 -> Case 2 (no nextDisplayed yet), becomes pending - auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, state); - Assert::AreEqual(size_t(0), p1_phase1.size(), - L"First call for P1 with next=nullptr should produce no metrics (pending only)."); - - // Animation state should still be in CpuStart mode; no provider seeded yet. - Assert::IsTrue(state.animationErrorSource == AnimationErrorSource::CpuStart); - Assert::AreEqual(uint64_t(0), state.firstAppSimStartTime); - Assert::AreEqual(uint64_t(0), state.lastDisplayedSimStartTime); - Assert::AreEqual(uint64_t(0), state.lastDisplayedAppScreenTime); + Assert::AreEqual(size_t(0), swapChain.ProcessPresent(qpc, std::move(p1)).size()); - // -------------------------------------------------------------------- - // P2: second displayed app frame - // -------------------------------------------------------------------- FrameData p2{}; - p2.presentStartTime = 600'000; - p2.timeInPresent = 10'000; - p2.readyTime = 610'000; - p2.appSimStartTime = 575'000; - p2.pclSimStartTime = 0; + p2.presentStartTime = 1'050'000; + p2.timeInPresent = 50'000; + p2.mouseClickTime = 0; + p2.inputTime = 0; p2.finalState = PresentResult::Presented; p2.displayed.PushBack({ FrameType::Application, 1'100'000 }); - // Arrival of P2: - // 1) Flush pending P1 using P2 as nextDisplayed -> Case 3, finalize P1. - auto p1_final = ComputeMetricsForPresent(qpc, p1, &p2, state); - Assert::AreEqual(size_t(1), p1_final.size()); - const auto& p1_metrics = p1_final[0].metrics; - - // P1 is the FIRST provider-driven frame, so it should only seed the state: - // no animation error or time yet. - Assert::IsFalse(HasMetricValue(p1_metrics.msAnimationError), - L"P1 should not report animation error; it seeds the animation state."); - Assert::IsTrue(HasMetricValue(p1_metrics.msAnimationTime), - L"P1 should report back 0.0."); - AssertAreEqualWithinTolerance(double(0.0), p1_metrics.msAnimationTime, 0.0001); - - // UpdateAfterPresent should have run for P1 and switched to AppProvider: - Assert::IsTrue(state.animationErrorSource == AnimationErrorSource::AppProvider, - L"Animation source should transition to AppProvider after P1."); - Assert::AreEqual(uint64_t(475'000), state.firstAppSimStartTime, - L"firstAppSimStartTime should latch P1's appSimStartTime."); - Assert::AreEqual(uint64_t(475'000), state.lastDisplayedSimStartTime, - L"lastDisplayedSimStartTime should match P1's appSimStartTime."); - Assert::AreEqual(uint64_t(1'000'000), state.lastDisplayedAppScreenTime, - L"lastDisplayedAppScreenTime should match P1's screenTime."); - - // 2) Now process P2's arrival as pending: Case 2 with next=nullptr - auto p2_phase1 = ComputeMetricsForPresent(qpc, p2, nullptr, state); - Assert::AreEqual(size_t(0), p2_phase1.size(), - L"First call for P2 with next=nullptr should produce no metrics (pending only)."); - - // -------------------------------------------------------------------- - // P3: third displayed app frame - // -------------------------------------------------------------------- - FrameData p3{}; - p3.presentStartTime = 700'000; - p3.timeInPresent = 10'000; - p3.readyTime = 710'000; - p3.appSimStartTime = 675'000; - p3.pclSimStartTime = 0; - p3.finalState = PresentResult::Presented; - p3.displayed.PushBack({ FrameType::Application, 1'200'000 }); - - // Arrival of P3: - // 1) Flush pending P2 using P3 as nextDisplayed -> Case 3, finalize P2. - auto p2_final = ComputeMetricsForPresent(qpc, p2, &p3, state); - Assert::AreEqual(size_t(1), p2_final.size()); - const auto& p2_metrics = p2_final[0].metrics; - - // For P2: - // - previous displayed sim start = P1.appSimStartTime = 100 - // - current sim start = P2.appSimStartTime = 200 - // - previous screen time = P1.screenTime = 1'000'000 - // - current screen time = P2.screenTime = 1'100'000 - // => simElapsed = 100 ticks → 0.01 ms - // => displayElapsed = 100'000 ticks → 0.01 ms - // => animationError = 0.0 ms - // => animationTime = (200 - 100) ticks from firstAppSimStartTime -> 0.01 ms - - Assert::IsTrue(HasMetricValue(p2_metrics.msAnimationError), - L"P2 should report animation error."); - Assert::IsTrue(HasMetricValue(p2_metrics.msAnimationTime), - L"P2 should report animation time."); - - double expectedError = 0.0; - AssertAreEqualWithinTolerance(expectedError, p2_metrics.msAnimationError, 0.0001, - L"P2's msAnimationError should be 0.0 when sim and display deltas match."); - - double expectedAnim = qpc.DeltaUnsignedMilliSeconds(475'000, 575'000); - AssertAreEqualWithinTolerance(expectedAnim, p2_metrics.msAnimationTime, 0.0001, - L"P2's msAnimationTime should be based on firstAppSimStartTime (100) to current sim (200)."); - - // After finalizing P2, chain state should now reflect P2 as "last displayed" - Assert::AreEqual(uint64_t(475'000), state.firstAppSimStartTime, - L"firstAppSimStartTime should remain anchored to P1."); - Assert::AreEqual(uint64_t(575'000), state.lastDisplayedSimStartTime, - L"lastDisplayedSimStartTime should advance to P2's appSimStartTime."); - Assert::AreEqual(uint64_t(1'100'000), state.lastDisplayedAppScreenTime, - L"lastDisplayedAppScreenTime should advance to P2's screenTime."); - - // 2) Finally, process P3 as the new pending (Case 2 with next=nullptr). - auto p3_phase1 = ComputeMetricsForPresent(qpc, p3, nullptr, state); - Assert::AreEqual(size_t(0), p3_phase1.size(), - L"First call for P3 with next=nullptr should produce no metrics (pending only)."); - } - // ======================================================================== - // A7: Animation_AppProvider_PendingSequence_P2Discarded_SkipsAnimation - // ======================================================================== - TEST_METHOD(Animation_AppProvider_PendingSequence_P2Discarded_SkipsAnimation) - { - // This test mimics the real ReportMetrics pipeline for a single swapchain - // when a middle frame (P2) is discarded and never displayed. - // - // P1 arrives (displayed, has AppProvider sim start): - // - ComputeMetricsForPresent(P1, nullptr, state) // Case 2, pending = P1 - // - // P2 arrives (DISCARDED, not displayed, but with appSimStartTime): - // - ComputeMetricsForPresent(P2, nullptr, state) // Case 1, not displayed - // * Should produce a single metrics entry with NO animation time/error - // * Must NOT change firstAppSimStartTime / lastDisplayedSimStartTime - // - // P3 arrives (displayed, has AppProvider sim start): - // - ComputeMetricsForPresent(P1, &P3, state) // Case 3, finalize P1 - // * P1 is the FIRST provider-driven displayed frame, so it seeds state. - // * P1 should not report animation time/error. - // * State switches to AppProvider and latches P1's sim + screen times. - // - ComputeMetricsForPresent(P3, nullptr, state) // Case 2, pending = P3 - // - // App sim times are in QPC domain: - // P1.appSimStartTime = 475'000 - // P2.appSimStartTime = 575'000 - // P3.appSimStartTime = 675'000 - // - // Screen times: - // P1.screenTime = 1'000'000 - // P2 has no screenTime (discarded, not displayed) - // P3.screenTime = 1'100'000 - // - // We verify: - // - P2's metrics have no msAnimationTime / msAnimationError. - // - P2 does not change firstAppSimStartTime / lastDisplayedSimStartTime. - // - After finalizing P1 with P3 as nextDisplayed: - // * animationErrorSource == AppProvider - // * firstAppSimStartTime == 475'000 - // * lastDisplayedSimStartTime == 475'000 - // * lastDisplayedAppScreenTime == 1'000'000 - - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; - - // -------------------------------------------------------------------- - // P1: first displayed app frame with AppProvider data - // -------------------------------------------------------------------- - FrameData p1{}; - p1.presentStartTime = 500'000; - p1.timeInPresent = 10'000; - p1.readyTime = 510'000; - p1.appSimStartTime = 475'000; // APC-provided sim start (QPC ticks) - p1.pclSimStartTime = 0; - p1.finalState = PresentResult::Presented; - p1.displayed.PushBack({ FrameType::Application, 1'000'000 }); // screen time - - // P1 arrives -> Case 2 (no nextDisplayed yet), becomes pending - auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, state); - Assert::AreEqual(size_t(0), p1_phase1.size(), - L"First call for P1 with next=nullptr should produce no metrics (pending only)."); - - // Still in CpuStart mode; no provider-seeded animation state yet. - Assert::IsTrue(state.animationErrorSource == AnimationErrorSource::CpuStart); - Assert::AreEqual(uint64_t(0), state.firstAppSimStartTime); - Assert::AreEqual(uint64_t(0), state.lastDisplayedSimStartTime); - Assert::AreEqual(uint64_t(0), state.lastDisplayedAppScreenTime); - - // -------------------------------------------------------------------- - // P2: discarded, not displayed, but with AppProvider sim start - // -------------------------------------------------------------------- - FrameData p2{}; - p2.presentStartTime = 600'000; - p2.timeInPresent = 10'000; - p2.readyTime = 610'000; - p2.appSimStartTime = 575'000; // APC timestamp, but this frame is not displayed - p2.pclSimStartTime = 0; - p2.finalState = PresentResult::Discarded; - // No displayed entries -> not displayed - - auto p2_results = ComputeMetricsForPresent(qpc, p2, nullptr, state); - Assert::AreEqual(size_t(1), p2_results.size(), - L"Discarded frame should produce a single not-displayed metrics entry."); - - const auto& p2_metrics = p2_results[0].metrics; + auto rows = swapChain.ProcessPresent(qpc, std::move(p2)); + Assert::AreEqual(size_t(1), rows.size()); + Assert::AreEqual(uint64_t(1'000'000), rows[0].computed.metrics.screenTimeQpc); + Assert::IsTrue(HasMetricValue(rows[0].computed.metrics.msClickToPhotonLatency)); - // Discarded / not-displayed frame must NOT produce animation metrics. - Assert::IsFalse(HasMetricValue(p2_metrics.msAnimationTime), - L"P2 (discarded) should not have msAnimationTime."); - Assert::IsFalse(HasMetricValue(p2_metrics.msAnimationError), - L"P2 (discarded) should not have msAnimationError."); - - // And it must NOT disturb animation anchors, since it's not displayed. - Assert::AreEqual(uint64_t(0), state.firstAppSimStartTime, - L"P2 must not set firstAppSimStartTime; only displayed App/PCL frames do that."); - Assert::AreEqual(uint64_t(0), state.lastDisplayedSimStartTime, - L"P2 must not change lastDisplayedSimStartTime when not displayed."); - Assert::AreEqual(uint64_t(0), state.lastDisplayedAppScreenTime, - L"P2 must not change lastDisplayedAppScreenTime when not displayed."); - - // (Optional sanity: lastSimStartTime may track P2's appSimStartTime, which is fine for - // simulation plumbing but not for animation anchors.) - - // -------------------------------------------------------------------- - // P3: next displayed app frame - // -------------------------------------------------------------------- - FrameData p3{}; - p3.presentStartTime = 700'000; - p3.timeInPresent = 10'000; - p3.readyTime = 710'000; - p3.appSimStartTime = 675'000; // another +100'000 ticks in sim space - p3.pclSimStartTime = 0; - p3.finalState = PresentResult::Presented; - p3.displayed.PushBack({ FrameType::Application, 1'100'000 }); // next screen time - - // P3 arrives: - // 1) Flush pending P1 using P3 as nextDisplayed -> Case 3, finalize P1. - auto p1_final = ComputeMetricsForPresent(qpc, p1, &p3, state); - Assert::AreEqual(size_t(1), p1_final.size()); - const auto& p1_metrics = p1_final[0].metrics; - - // P1 is the FIRST displayed frame with AppProvider sim start. - // It should only seed animation state; no error/time yet. - Assert::IsFalse(HasMetricValue(p1_metrics.msAnimationError), - L"P1 should not report animation error; it seeds the animation state."); - Assert::IsTrue(HasMetricValue(p1_metrics.msAnimationTime), - L"P1 should have an animation time of 0.0."); - AssertAreEqualWithinTolerance(double(0.0), p1_metrics.msAnimationTime, 0.0001); - - // After finalizing P1, we must now be in AppProvider mode with anchors from P1. - Assert::IsTrue(state.animationErrorSource == AnimationErrorSource::AppProvider, - L"Animation source should transition to AppProvider after first displayed AppSimStart frame (P1)."); - Assert::AreEqual(uint64_t(475'000), state.firstAppSimStartTime, - L"firstAppSimStartTime should latch P1's appSimStartTime."); - Assert::AreEqual(uint64_t(475'000), state.lastDisplayedSimStartTime, - L"lastDisplayedSimStartTime should match P1's appSimStartTime after P1 is finalized."); - Assert::AreEqual(uint64_t(1'000'000), state.lastDisplayedAppScreenTime, - L"lastDisplayedAppScreenTime should match P1's screenTime."); - - // 2) Process P3 as the new pending frame (Case 2 with next=nullptr). - auto p3_phase1 = ComputeMetricsForPresent(qpc, p3, nullptr, state); - Assert::AreEqual(size_t(0), p3_phase1.size(), - L"First call for P3 with next=nullptr should produce no metrics (pending only)."); - - // State remains anchored to P1 until a later frame finalizes P3 with a true nextDisplayed. - Assert::AreEqual(uint64_t(475'000), state.firstAppSimStartTime, - L"firstAppSimStartTime should remain anchored to P1 after P3's pending pass."); - Assert::AreEqual(uint64_t(475'000), state.lastDisplayedSimStartTime, - L"lastDisplayedSimStartTime should still reflect P1 until P3 is finalized."); + double expected = qpc.DeltaUnsignedMilliSeconds(400'000, 1'000'000); + AssertAreEqualWithinTolerance(expected, rows[0].computed.metrics.msClickToPhotonLatency, 0.0001); + Assert::AreEqual(uint64_t(0), swapChain.swapChain.lastReceivedNotDisplayedMouseClickTime); } - }; - // ============================================================================ - // SECTION: Input Latency Tests - // ============================================================================ - TEST_CLASS(InputLatencyTests) - { - public: - // ======================================================================== - // Test 1: ClickToPhoton - displayed frame uses its own click - // ======================================================================== - TEST_METHOD(InputLatency_ClickToPhoton_DisplayedFrame_UsesOwnClickTime) + TEST_METHOD(InputLatency_ClickToPhoton_DroppedFrame_CarriesClickToNextDisplayed) { - // Scenario: - // - P1 (displayed frame) has its own mouseClickTime = 400'000 - // - P1 computes msClickToPhotonLatency from click to its own display time - // - No pending click should remain in the chain - // - // Expected: - // - P1's msClickToPhotonLatency uses P1's own click (400'000 -> 1'000'000) - // - state.lastReceivedNotDisplayedMouseClickTime == 0 - QpcConverter qpc(10'000'000, 0); SwapChainCoreState state{}; - // P1: displayed app frame with its own click FrameData p1{}; - p1.presentStartTime = 500'000; - p1.timeInPresent = 100'000; + p1.presentStartTime = 300'000; + p1.timeInPresent = 50'000; p1.mouseClickTime = 400'000; p1.inputTime = 0; - p1.appSimStartTime = 450'000; - p1.finalState = PresentResult::Presented; - p1.displayed.PushBack({ FrameType::Application, 1'000'000 }); + p1.finalState = PresentResult::Discarded; - // P2: next displayed frame FrameData p2{}; - p2.presentStartTime = 1'050'000; - p2.timeInPresent = 50'000; + p2.presentStartTime = 900'000; + p2.timeInPresent = 100'000; p2.mouseClickTime = 0; p2.inputTime = 0; p2.finalState = PresentResult::Presented; - p2.displayed.PushBack({ FrameType::Application, 1'100'000 }); - - // P1 arrives (pending) - auto p1_pending = ComputeMetricsForPresent(qpc, p1, nullptr, state); - Assert::AreEqual(size_t(0), p1_pending.size(), L"P1 pending should be empty"); + p2.displayed.PushBack({ FrameType::Application, 1'000'000 }); - // P2 arrives, finalizes P1 - auto p1_final = ComputeMetricsForPresent(qpc, p1, &p2, state); - auto p2_pending = ComputeMetricsForPresent(qpc, p2, nullptr, state); + auto p1_results = ComputeMetricsForPresent(qpc, p1, state); + Assert::AreEqual(size_t(1), p1_results.size()); + Assert::IsTrue(IsMissingFrameMetricValue(p1_results[0].metrics.msClickToPhotonLatency)); + Assert::AreEqual(uint64_t(400'000), state.lastReceivedNotDisplayedMouseClickTime); - // Assertions for P1 - Assert::AreEqual(size_t(1), p1_final.size()); - Assert::IsTrue(HasMetricValue(p1_final[0].metrics.msClickToPhotonLatency), - L"P1 should have msClickToPhotonLatency"); + auto p2_results = ComputeMetricsForPresent(qpc, p2, state); + Assert::AreEqual(size_t(1), p2_results.size()); + Assert::IsTrue(HasMetricValue(p2_results[0].metrics.msClickToPhotonLatency)); double expected = qpc.DeltaUnsignedMilliSeconds(400'000, 1'000'000); - AssertAreEqualWithinTolerance(expected, p1_final[0].metrics.msClickToPhotonLatency, 0.0001, - L"P1's click-to-photon should use its own click time"); - - // Verify no pending click remains - Assert::AreEqual(uint64_t(0), state.lastReceivedNotDisplayedMouseClickTime, - L"No pending click should remain after P1 used its own click"); + AssertAreEqualWithinTolerance(expected, p2_results[0].metrics.msClickToPhotonLatency, 0.0001); + Assert::AreEqual(uint64_t(0), state.lastReceivedNotDisplayedMouseClickTime); } - // ======================================================================== - // Test 2: ClickToPhoton - dropped frame carries click to next displayed - // ======================================================================== - TEST_METHOD(InputLatency_ClickToPhoton_DroppedFrame_CarriesClickToNextDisplayed) + TEST_METHOD(InputLatency_ClickToPhoton_DroppedFrame_CarriesClickToNextDisplayed_ProcessPresent) { - // Scenario: - // - P1 (dropped, not displayed) has mouseClickTime = 400'000 - // - P1 does not produce msClickToPhotonLatency - // - P1 stores click in lastReceivedNotDisplayedMouseClickTime - // - P2 (displayed, no own click) uses the stored click from P1 - // - // Expected: - // - P1: msClickToPhotonLatency is missing (stored internally as NaN) - // - After P1: state.lastReceivedNotDisplayedMouseClickTime == 400'000 - // - P2: msClickToPhotonLatency uses stored click (400'000 -> 1'000'000) - // - After P2: state.lastReceivedNotDisplayedMouseClickTime == 0 (consumed) - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; + UnifiedSwapChain swapChain{}; + + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; + + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); - // P1: dropped frame with click FrameData p1{}; p1.presentStartTime = 300'000; p1.timeInPresent = 50'000; p1.mouseClickTime = 400'000; p1.inputTime = 0; p1.finalState = PresentResult::Discarded; - // displayed is empty (not displayed) - // P2: first displayed frame (no own click) + auto p1_rows = swapChain.ProcessPresent(qpc, std::move(p1)); + Assert::AreEqual(size_t(1), p1_rows.size()); + Assert::IsTrue(IsMissingFrameMetricValue(p1_rows[0].computed.metrics.msClickToPhotonLatency)); + Assert::AreEqual(uint64_t(400'000), swapChain.swapChain.lastReceivedNotDisplayedMouseClickTime); + FrameData p2{}; p2.presentStartTime = 900'000; p2.timeInPresent = 100'000; @@ -7790,81 +5463,104 @@ TEST_CLASS(ComputeMetricsForPresentTests) p2.finalState = PresentResult::Presented; p2.displayed.PushBack({ FrameType::Application, 1'000'000 }); - // P3: later frame to finalize P2 + Assert::AreEqual(size_t(0), swapChain.ProcessPresent(qpc, std::move(p2)).size()); + FrameData p3{}; p3.presentStartTime = 1'050'000; p3.timeInPresent = 50'000; p3.finalState = PresentResult::Presented; p3.displayed.PushBack({ FrameType::Application, 1'100'000 }); - // P1 arrives (dropped) - auto p1_results = ComputeMetricsForPresent(qpc, p1, nullptr, state); + auto p3_rows = swapChain.ProcessPresent(qpc, std::move(p3)); + Assert::AreEqual(size_t(1), p3_rows.size()); + Assert::AreEqual(uint64_t(1'000'000), p3_rows[0].computed.metrics.screenTimeQpc); + Assert::IsTrue(HasMetricValue(p3_rows[0].computed.metrics.msClickToPhotonLatency)); - // Assertions for P1 - Assert::AreEqual(size_t(1), p1_results.size()); - Assert::IsTrue(IsMissingFrameMetricValue(p1_results[0].metrics.msClickToPhotonLatency), - L"P1 (dropped) should store missing click-to-photon as NaN"); - Assert::AreEqual(uint64_t(400'000), state.lastReceivedNotDisplayedMouseClickTime, - L"P1's click should be stored as pending"); + double expected = qpc.DeltaUnsignedMilliSeconds(400'000, 1'000'000); + AssertAreEqualWithinTolerance(expected, p3_rows[0].computed.metrics.msClickToPhotonLatency, 0.0001); + Assert::AreEqual(uint64_t(0), swapChain.swapChain.lastReceivedNotDisplayedMouseClickTime); + } - // P2 arrives (pending) - auto p2_pending = ComputeMetricsForPresent(qpc, p2, nullptr, state); + TEST_METHOD(InputLatency_AllInputPhoton_MultipleDroppedFrames_LastInputWins) + { + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; - // P3 arrives, finalizes P2 - auto p2_final = ComputeMetricsForPresent(qpc, p2, &p3, state); - auto p3_pending = ComputeMetricsForPresent(qpc, p3, nullptr, state); + FrameData p1{}; + p1.presentStartTime = 200'000; + p1.timeInPresent = 50'000; + p1.inputTime = 300'000; + p1.mouseClickTime = 0; + p1.finalState = PresentResult::Discarded; - // Assertions for P2 - Assert::AreEqual(size_t(1), p2_final.size()); - Assert::IsTrue(HasMetricValue(p2_final[0].metrics.msClickToPhotonLatency), - L"P2 should have msClickToPhotonLatency using P1's stored click"); + FrameData p2{}; + p2.presentStartTime = 400'000; + p2.timeInPresent = 50'000; + p2.inputTime = 450'000; + p2.mouseClickTime = 0; + p2.finalState = PresentResult::Discarded; - double expected = qpc.DeltaUnsignedMilliSeconds(400'000, 1'000'000); - AssertAreEqualWithinTolerance(expected, p2_final[0].metrics.msClickToPhotonLatency, 0.0001, - L"P2's click-to-photon should use P1's stored click"); + FrameData p3{}; + p3.presentStartTime = 900'000; + p3.timeInPresent = 100'000; + p3.inputTime = 0; + p3.mouseClickTime = 0; + p3.finalState = PresentResult::Presented; + p3.displayed.PushBack({ FrameType::Application, 1'000'000 }); + + auto p1_results = ComputeMetricsForPresent(qpc, p1, state); + Assert::AreEqual(size_t(1), p1_results.size()); + Assert::IsTrue(IsMissingFrameMetricValue(p1_results[0].metrics.msAllInputPhotonLatency)); + Assert::AreEqual(uint64_t(300'000), state.lastReceivedNotDisplayedAllInputTime); - // Optional: verify pending click is consumed - Assert::AreEqual(uint64_t(0), state.lastReceivedNotDisplayedMouseClickTime, - L"Pending click should be consumed after P2 uses it"); + auto p2_results = ComputeMetricsForPresent(qpc, p2, state); + Assert::AreEqual(size_t(1), p2_results.size()); + Assert::IsTrue(IsMissingFrameMetricValue(p2_results[0].metrics.msAllInputPhotonLatency)); + Assert::AreEqual(uint64_t(450'000), state.lastReceivedNotDisplayedAllInputTime); + + auto p3_results = ComputeMetricsForPresent(qpc, p3, state); + Assert::AreEqual(size_t(1), p3_results.size()); + Assert::IsTrue(HasMetricValue(p3_results[0].metrics.msAllInputPhotonLatency)); + + double expected = qpc.DeltaUnsignedMilliSeconds(450'000, 1'000'000); + AssertAreEqualWithinTolerance(expected, p3_results[0].metrics.msAllInputPhotonLatency, 0.0001); } - // ======================================================================== - // Test 3: AllInputPhoton - multiple dropped frames, last input wins - // ======================================================================== - TEST_METHOD(InputLatency_AllInputPhoton_MultipleDroppedFrames_LastInputWins) + TEST_METHOD(InputLatency_AllInputPhoton_MultipleDroppedFrames_LastInputWins_ProcessPresent) { - // Scenario: - // - P1 (dropped) has inputTime = 300'000 - // - P2 (dropped) has inputTime = 450'000 (should override P1) - // - P3 (displayed, no own input) uses the last stored input (450'000) - // - // Expected: - // - P1: msAllInputPhotonLatency is missing (stored internally as NaN), state stores 300'000 - // - P2: msAllInputPhotonLatency is missing (stored internally as NaN), state updates to 450'000 - // - P3: msAllInputPhotonLatency uses 450'000 (last wins) - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; + UnifiedSwapChain swapChain{}; + + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; + + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); - // P1: dropped with first input FrameData p1{}; p1.presentStartTime = 200'000; p1.timeInPresent = 50'000; p1.inputTime = 300'000; p1.mouseClickTime = 0; p1.finalState = PresentResult::Discarded; - // displayed empty - // P2: dropped with later input (overrides P1) FrameData p2{}; p2.presentStartTime = 400'000; p2.timeInPresent = 50'000; p2.inputTime = 450'000; p2.mouseClickTime = 0; p2.finalState = PresentResult::Discarded; - // displayed empty - // P3: first displayed frame (no own input) + auto p1_rows = swapChain.ProcessPresent(qpc, std::move(p1)); + Assert::AreEqual(size_t(1), p1_rows.size()); + Assert::AreEqual(uint64_t(300'000), swapChain.swapChain.lastReceivedNotDisplayedAllInputTime); + + auto p2_rows = swapChain.ProcessPresent(qpc, std::move(p2)); + Assert::AreEqual(size_t(1), p2_rows.size()); + Assert::AreEqual(uint64_t(450'000), swapChain.swapChain.lastReceivedNotDisplayedAllInputTime); + FrameData p3{}; p3.presentStartTime = 900'000; p3.timeInPresent = 100'000; @@ -7873,73 +5569,79 @@ TEST_CLASS(ComputeMetricsForPresentTests) p3.finalState = PresentResult::Presented; p3.displayed.PushBack({ FrameType::Application, 1'000'000 }); - // P4: later frame to finalize P3 + Assert::AreEqual(size_t(0), swapChain.ProcessPresent(qpc, std::move(p3)).size()); + FrameData p4{}; p4.presentStartTime = 1'050'000; p4.timeInPresent = 50'000; p4.finalState = PresentResult::Presented; p4.displayed.PushBack({ FrameType::Application, 1'100'000 }); - // P1 arrives (dropped) - auto p1_results = ComputeMetricsForPresent(qpc, p1, nullptr, state); - Assert::AreEqual(size_t(1), p1_results.size()); - Assert::IsTrue(IsMissingFrameMetricValue(p1_results[0].metrics.msAllInputPhotonLatency), - L"P1 (dropped) should store missing all-input-to-photon as NaN"); - Assert::AreEqual(uint64_t(300'000), state.lastReceivedNotDisplayedAllInputTime, - L"P1's input should be stored"); + auto p4_rows = swapChain.ProcessPresent(qpc, std::move(p4)); + Assert::AreEqual(size_t(1), p4_rows.size()); + Assert::AreEqual(uint64_t(1'000'000), p4_rows[0].computed.metrics.screenTimeQpc); + Assert::IsTrue(HasMetricValue(p4_rows[0].computed.metrics.msAllInputPhotonLatency)); - // P2 arrives (dropped, overrides P1) - auto p2_results = ComputeMetricsForPresent(qpc, p2, nullptr, state); - Assert::AreEqual(size_t(1), p2_results.size()); - Assert::IsTrue(IsMissingFrameMetricValue(p2_results[0].metrics.msAllInputPhotonLatency), - L"P2 (dropped) should store missing all-input-to-photon as NaN"); - Assert::AreEqual(uint64_t(450'000), state.lastReceivedNotDisplayedAllInputTime, - L"P2's input should override P1's stored input (last wins)"); + double expected = qpc.DeltaUnsignedMilliSeconds(450'000, 1'000'000); + AssertAreEqualWithinTolerance(expected, p4_rows[0].computed.metrics.msAllInputPhotonLatency, 0.0001); + } + + TEST_METHOD(InputLatency_AllInputPhoton_DisplayedFrame_WithOwnInput_OverridesPending) + { + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + + FrameData p0{}; + p0.presentStartTime = 200'000; + p0.timeInPresent = 50'000; + p0.inputTime = 300'000; + p0.mouseClickTime = 0; + p0.finalState = PresentResult::Discarded; - // P3 arrives (pending) - auto p3_pending = ComputeMetricsForPresent(qpc, p3, nullptr, state); + FrameData p1{}; + p1.presentStartTime = 900'000; + p1.timeInPresent = 100'000; + p1.inputTime = 500'000; + p1.mouseClickTime = 0; + p1.finalState = PresentResult::Presented; + p1.displayed.PushBack({ FrameType::Application, 1'000'000 }); - // P4 arrives, finalizes P3 - auto p3_final = ComputeMetricsForPresent(qpc, p3, &p4, state); - auto p4_pending = ComputeMetricsForPresent(qpc, p4, nullptr, state); + auto p0_results = ComputeMetricsForPresent(qpc, p0, state); + Assert::AreEqual(size_t(1), p0_results.size()); + Assert::AreEqual(uint64_t(300'000), state.lastReceivedNotDisplayedAllInputTime); - // Assertions for P3 - Assert::AreEqual(size_t(1), p3_final.size()); - Assert::IsTrue(HasMetricValue(p3_final[0].metrics.msAllInputPhotonLatency), - L"P3 should have msAllInputPhotonLatency using last stored input"); + auto p1_results = ComputeMetricsForPresent(qpc, p1, state); + Assert::AreEqual(size_t(1), p1_results.size()); + Assert::IsTrue(HasMetricValue(p1_results[0].metrics.msAllInputPhotonLatency)); - double expected = qpc.DeltaUnsignedMilliSeconds(450'000, 1'000'000); - AssertAreEqualWithinTolerance(expected, p3_final[0].metrics.msAllInputPhotonLatency, 0.0001, - L"P3's all-input-to-photon should use P2's input (last wins)"); + double expected = qpc.DeltaUnsignedMilliSeconds(500'000, 1'000'000); + AssertAreEqualWithinTolerance(expected, p1_results[0].metrics.msAllInputPhotonLatency, 0.0001); } - // ======================================================================== - // Test 4: AllInputPhoton - displayed frame with own input overrides pending - // ======================================================================== - TEST_METHOD(InputLatency_AllInputPhoton_DisplayedFrame_WithOwnInput_OverridesPending) + TEST_METHOD(InputLatency_AllInputPhoton_DisplayedFrame_WithOwnInput_OverridesPending_ProcessPresent) { - // Scenario: - // - P0 (dropped) seeds pending input = 300'000 - // - P1 (displayed) has its own inputTime = 500'000 - // - P1's own input should override the pending 300'000 - // - // Expected: - // - P0: state.lastReceivedNotDisplayedAllInputTime == 300'000 - // - P1: msAllInputPhotonLatency uses P1's own input (500'000 -> 1'000'000) + QpcConverter qpc(10'000'000, 0); + UnifiedSwapChain swapChain{}; + + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); - // P0: dropped, seeds pending input FrameData p0{}; p0.presentStartTime = 200'000; p0.timeInPresent = 50'000; p0.inputTime = 300'000; p0.mouseClickTime = 0; p0.finalState = PresentResult::Discarded; - // displayed empty - // P1: displayed with its own input + auto p0_rows = swapChain.ProcessPresent(qpc, std::move(p0)); + Assert::AreEqual(size_t(1), p0_rows.size()); + Assert::AreEqual(uint64_t(300'000), swapChain.swapChain.lastReceivedNotDisplayedAllInputTime); + FrameData p1{}; p1.presentStartTime = 900'000; p1.timeInPresent = 100'000; @@ -7948,33 +5650,21 @@ TEST_CLASS(ComputeMetricsForPresentTests) p1.finalState = PresentResult::Presented; p1.displayed.PushBack({ FrameType::Application, 1'000'000 }); - // P2: later frame to finalize P1 + Assert::AreEqual(size_t(0), swapChain.ProcessPresent(qpc, std::move(p1)).size()); + FrameData p2{}; p2.presentStartTime = 1'050'000; p2.timeInPresent = 50'000; p2.finalState = PresentResult::Presented; p2.displayed.PushBack({ FrameType::Application, 1'100'000 }); - // P0 arrives (dropped) - auto p0_results = ComputeMetricsForPresent(qpc, p0, nullptr, state); - Assert::AreEqual(uint64_t(300'000), state.lastReceivedNotDisplayedAllInputTime, - L"P0's input should be stored as pending"); - - // P1 arrives (pending) - auto p1_pending = ComputeMetricsForPresent(qpc, p1, nullptr, state); - - // P2 arrives, finalizes P1 - auto p1_final = ComputeMetricsForPresent(qpc, p1, &p2, state); - auto p2_pending = ComputeMetricsForPresent(qpc, p2, nullptr, state); - - // Assertions for P1 - Assert::AreEqual(size_t(1), p1_final.size()); - Assert::IsTrue(HasMetricValue(p1_final[0].metrics.msAllInputPhotonLatency), - L"P1 should have msAllInputPhotonLatency using its own input"); + auto p2_rows = swapChain.ProcessPresent(qpc, std::move(p2)); + Assert::AreEqual(size_t(1), p2_rows.size()); + Assert::AreEqual(uint64_t(1'000'000), p2_rows[0].computed.metrics.screenTimeQpc); + Assert::IsTrue(HasMetricValue(p2_rows[0].computed.metrics.msAllInputPhotonLatency)); double expected = qpc.DeltaUnsignedMilliSeconds(500'000, 1'000'000); - AssertAreEqualWithinTolerance(expected, p1_final[0].metrics.msAllInputPhotonLatency, 0.0001, - L"P1's all-input-to-photon should use its own input (500'000), not pending (300'000)"); + AssertAreEqualWithinTolerance(expected, p2_rows[0].computed.metrics.msAllInputPhotonLatency, 0.0001); } // ======================================================================== @@ -8024,12 +5714,8 @@ TEST_CLASS(ComputeMetricsForPresentTests) p3.finalState = PresentResult::Presented; p3.displayed.PushBack({ FrameType::Application, 1'200'000 }); - // P1 arrives (pending) - auto p1_pending = ComputeMetricsForPresent(qpc, p1, nullptr, state); - - // P2 arrives, finalizes P1 (switches to AppProvider) - auto p1_final = ComputeMetricsForPresent(qpc, p1, &p2, state); - auto p2_pending = ComputeMetricsForPresent(qpc, p2, nullptr, state); + (void)ComputeMetricsForPresent(qpc, p1, state); + auto p2Metrics = ComputeMetricsForPresent(qpc, p2, state); // Verify state after P1 Assert::IsTrue(state.animationErrorSource == AnimationErrorSource::AppProvider, @@ -8037,386 +5723,203 @@ TEST_CLASS(ComputeMetricsForPresentTests) Assert::AreEqual(uint64_t(475'000), state.firstAppSimStartTime, L"firstAppSimStartTime should be set to P1's appSimStartTime"); - // P3 arrives, finalizes P2 - auto p2_final = ComputeMetricsForPresent(qpc, p2, &p3, state); - auto p3_pending = ComputeMetricsForPresent(qpc, p3, nullptr, state); + (void)ComputeMetricsForPresent(qpc, p3, state); // Assertions for P2 - Assert::AreEqual(size_t(1), p2_final.size()); - - // Verify P2 has animation time (sanity check we're in AppProvider mode) - Assert::IsTrue(HasMetricValue(p2_final[0].metrics.msAnimationTime), - L"P2 should have msAnimationTime (AppProvider mode)"); + Assert::AreEqual(size_t(1), p2Metrics.size()); // Verify msInstrumentedInputTime is present - Assert::IsTrue(HasMetricValue(p2_final[0].metrics.msInstrumentedInputTime), + Assert::IsTrue(HasMetricValue(p2Metrics[0].metrics.msInstrumentedInputTime), L"P2 should have msInstrumentedInputTime"); // Calculate expected: app input -> p2 screen double expectedInstr = qpc.DeltaUnsignedMilliSeconds(500'000, 1'100'000); - AssertAreEqualWithinTolerance(expectedInstr, p2_final[0].metrics.msInstrumentedInputTime, 0.0001, + AssertAreEqualWithinTolerance(expectedInstr, p2Metrics[0].metrics.msInstrumentedInputTime, 0.0001, L"msInstrumentedInputTime should be P2 app input time to P2 screen time"); } }; TEST_CLASS(PcLatencyTests) { public: + // V1: ComputeMetricsForPresent (nullptr next). V2: *_ProcessPresent duplicates. TEST_METHOD(PcLatency_PendingSequence_DroppedDroppedDisplayed_P0P1P2P3) { - // This test mimics the real ReportMetrics pipeline for a single swapchain, - // focusing on the PC Latency accumulation across *dropped* frames, and its - // completion when the corresponding frame finally reaches the screen. - // - // We use four presents: - // - // P0: DROPPED - // - PclInputPingTime = 10'000 - // - PclSimStartTime = 20'000 - // -> initializes accumulatedInput2FrameStartTime with Δ(PING0, SIM0). - // - // P1: DROPPED - // - PclInputPingTime = 0 - // - PclSimStartTime = 30'000 - // -> extends accumulatedInput2FrameStartTime with Δ(SIM0, SIM1). - // - // P2: DISPLAYED - // - PclInputPingTime = 0 - // - PclSimStartTime = 40'000 - // - ScreenTime = 50'000 - // - // P3: DISPLAYED - // - no PCL data; used only as "nextDisplayed" for P2 to mimic ReportMetrics. - // - // QPC frequency = 10 MHz. - // - // Timing (ticks): - // PING0 = 10'000 - // SIM0 = 20'000 - // SIM1 = 30'000 - // SIM2 = 40'000 - // SCR2 = 50'000 - // - // Δ(PING0, SIM0) = 10'000 ticks = 1.0 ms - // Δ(SIM0, SIM1) = 10'000 ticks = 1.0 ms - // Δ(SIM1, SIM2) = 10'000 ticks = 1.0 ms - // => full input→frame-start for this chain = 3.0 ms - // - // Δ(SIM2, SCR2) = 10'000 ticks = 1.0 ms - // - // Legacy behavior: - // - AccumulatedInput2FrameStartTime builds to 3.0 ms over P0/P1/P2. - // - EMA seeds from that full 3.0 ms sample when P2 finally completes. - // - PC Latency for P2 ≈ 3.0 ms (input→frame-start) + 1.0 ms (sim→screen). - // - // The pipeline calls we mimic: - // - // P0 arrives (dropped): - // - ComputeMetricsForPresent(P0, nullptr, state) // Case 1, not displayed - // - // P1 arrives (dropped): - // - ComputeMetricsForPresent(P1, nullptr, state) // Case 1, not displayed - // - // P2 arrives (displayed): - // - ComputeMetricsForPresent(P2, nullptr, state) // Case 2, pending only (no metrics yet) - // - // P3 arrives (displayed): - // - ComputeMetricsForPresent(P2, &P3, state) // Case 3, finalize P2 - // - ComputeMetricsForPresent(P3, nullptr, state) // Case 2, pending = P3 - // - // We verify: - // - P0 & P1 produce no msPcLatency (dropped frames). - // - accumulatedInput2FrameStartTime grows after P0 and P1. - // - P2's first call (next=nullptr) produces no metrics and does NOT - // disturb the accumulatedInput2FrameStartTime. - // - The final P2 call (with next=P3) produces a non-null msPcLatency, - // and resets accumulatedInput2FrameStartTime and lastReceivedNotDisplayedPclSimStart - // to 0, matching the legacy PCL behavior. - // QpcConverter qpc(10'000'000, 0); SwapChainCoreState state{}; - const uint32_t PROCESS_ID = 1234; - const uint64_t SWAPCHAIN = 0xABC0; - - // -------------------------------------------------------------------- - // P0: DROPPED, first PCL frame with Ping+Sim - // -------------------------------------------------------------------- FrameData p0{}; - p0.processId = PROCESS_ID; - p0.swapChainAddress = SWAPCHAIN; - p0.presentStartTime = 0; - p0.timeInPresent = 0; - p0.readyTime = 0; - p0.appSimStartTime = 0; - - p0.pclInputPingTime = 10'000; // PING0 - p0.pclSimStartTime = 20'000; // SIM0 - + p0.pclInputPingTime = 10'000; + p0.pclSimStartTime = 20'000; p0.finalState = PresentResult::Discarded; - p0.displayed.Clear(); // not displayed - // P0 arrival -> Case 1 (not displayed), process immediately. - auto p0_metrics_list = ComputeMetricsForPresent(qpc, p0, nullptr, state); - Assert::AreEqual(size_t(1), p0_metrics_list.size(), - L"P0: not-displayed present should produce a single metrics record."); + auto p0_metrics_list = ComputeMetricsForPresent(qpc, p0, state); + Assert::AreEqual(size_t(1), p0_metrics_list.size()); + Assert::IsFalse(HasMetricValue(p0_metrics_list[0].metrics.msPcLatency)); + Assert::IsTrue(state.accumulatedInput2FrameStartTime > 0.0); + Assert::AreEqual(uint64_t(20'000), state.lastReceivedNotDisplayedPclSimStart); + const double accumAfterP0 = state.accumulatedInput2FrameStartTime; - const auto& p0_metrics = p0_metrics_list[0].metrics; + FrameData p1{}; + p1.pclInputPingTime = 0; + p1.pclSimStartTime = 30'000; + p1.finalState = PresentResult::Discarded; - // Dropped frames never report PC latency directly. - Assert::IsFalse(HasMetricValue(p0_metrics.msPcLatency), - L"P0: dropped frame should not report msPcLatency."); + auto p1_metrics_list = ComputeMetricsForPresent(qpc, p1, state); + Assert::AreEqual(size_t(1), p1_metrics_list.size()); + Assert::IsFalse(HasMetricValue(p1_metrics_list[0].metrics.msPcLatency)); + Assert::IsTrue(state.accumulatedInput2FrameStartTime > accumAfterP0); + Assert::AreEqual(uint64_t(30'000), state.lastReceivedNotDisplayedPclSimStart); + const double accumAfterP1 = state.accumulatedInput2FrameStartTime; - // Accumulator should have been initialized from Ping0 -> Sim0. - Assert::IsTrue(state.accumulatedInput2FrameStartTime > 0.0, - L"P0: accumulatedInput2FrameStartTime should be initialized and > 0."); - Assert::AreEqual(uint64_t(20'000), state.lastReceivedNotDisplayedPclSimStart, - L"P0: lastReceivedNotDisplayedPclSimStart should match P0's pclSimStartTime (20'000)."); + FrameData p2{}; + p2.pclInputPingTime = 0; + p2.pclSimStartTime = 40'000; + p2.finalState = PresentResult::Presented; + p2.displayed.PushBack({ FrameType::Application, 50'000 }); - const double accumAfterP0 = state.accumulatedInput2FrameStartTime; + auto p2_results = ComputeMetricsForPresent(qpc, p2, state); + Assert::AreEqual(size_t(1), p2_results.size()); + const auto& p2_metrics = p2_results[0].metrics; - // -------------------------------------------------------------------- - // P1: DROPPED, continuation of same PCL chain (Sim only) - // -------------------------------------------------------------------- - FrameData p1{}; - p1.processId = PROCESS_ID; - p1.swapChainAddress = SWAPCHAIN; - p1.presentStartTime = 0; - p1.timeInPresent = 0; - p1.readyTime = 0; - p1.appSimStartTime = 0; + Assert::IsTrue(accumAfterP1 > 0.0); + Assert::IsTrue(HasMetricValue(p2_metrics.msPcLatency)); + Assert::IsTrue(p2_metrics.msPcLatency > 0.0); + AssertAreEqualWithinTolerance(0.0, state.accumulatedInput2FrameStartTime, 1e-9); + Assert::AreEqual(uint64_t{ 0 }, state.lastReceivedNotDisplayedPclSimStart); + } - p1.pclInputPingTime = 0; // no new ping - p1.pclSimStartTime = 30'000; // SIM1 + TEST_METHOD(PcLatency_PendingSequence_DroppedDroppedDisplayed_P0P1P2P3_ProcessPresent) + { + QpcConverter qpc(10'000'000, 0); + UnifiedSwapChain swapChain{}; - p1.finalState = PresentResult::Discarded; - p1.displayed.Clear(); // not displayed + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; - auto p1_metrics_list = ComputeMetricsForPresent(qpc, p1, nullptr, state); - Assert::AreEqual(size_t(1), p1_metrics_list.size(), - L"P1: not-displayed present should produce a single metrics record."); + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); - const auto& p1_metrics = p1_metrics_list[0].metrics; + FrameData p0{}; + p0.pclInputPingTime = 10'000; + p0.pclSimStartTime = 20'000; + p0.finalState = PresentResult::Discarded; - Assert::IsFalse(HasMetricValue(p1_metrics.msPcLatency), - L"P1: dropped frame should not report msPcLatency."); + auto p0_rows = swapChain.ProcessPresent(qpc, std::move(p0)); + Assert::AreEqual(size_t(1), p0_rows.size()); + Assert::IsFalse(HasMetricValue(p0_rows[0].computed.metrics.msPcLatency)); + Assert::IsTrue(swapChain.swapChain.accumulatedInput2FrameStartTime > 0.0); + const double accumAfterP0 = swapChain.swapChain.accumulatedInput2FrameStartTime; - // Accumulator should have grown: now includes SIM0->SIM1 as well. - Assert::IsTrue(state.accumulatedInput2FrameStartTime > accumAfterP0, - L"P1: accumulatedInput2FrameStartTime should be greater than after P0."); - Assert::AreEqual(uint64_t(30'000), state.lastReceivedNotDisplayedPclSimStart, - L"P1: lastReceivedNotDisplayedPclSimStart should match P1's pclSimStartTime (30'000)."); + FrameData p1{}; + p1.pclInputPingTime = 0; + p1.pclSimStartTime = 30'000; + p1.finalState = PresentResult::Discarded; - const double accumAfterP1 = state.accumulatedInput2FrameStartTime; + auto p1_rows = swapChain.ProcessPresent(qpc, std::move(p1)); + Assert::AreEqual(size_t(1), p1_rows.size()); + Assert::IsTrue(swapChain.swapChain.accumulatedInput2FrameStartTime > accumAfterP0); + const double accumAfterP1 = swapChain.swapChain.accumulatedInput2FrameStartTime; - // -------------------------------------------------------------------- - // P2: DISPLAYED, Sim only – this is the frame where the pending input - // will finally be visible on-screen. However, the metrics for P2 are - // only finalized when we know P3 (nextDisplayed), just like in the - // ReportMetrics pipeline. - // -------------------------------------------------------------------- FrameData p2{}; - p2.processId = PROCESS_ID; - p2.swapChainAddress = SWAPCHAIN; - p2.presentStartTime = 0; - p2.timeInPresent = 0; - p2.readyTime = 0; - p2.appSimStartTime = 0; + p2.pclInputPingTime = 0; + p2.pclSimStartTime = 40'000; + p2.finalState = PresentResult::Presented; + p2.displayed.PushBack({ FrameType::Application, 50'000 }); - p2.pclInputPingTime = 0; // no new ping - p2.pclSimStartTime = 40'000; // SIM2 + Assert::AreEqual(size_t(0), swapChain.ProcessPresent(qpc, std::move(p2)).size()); + AssertAreEqualWithinTolerance(accumAfterP1, swapChain.swapChain.accumulatedInput2FrameStartTime, 1e-9); - p2.finalState = PresentResult::Presented; - p2.displayed.Clear(); - p2.displayed.PushBack({ FrameType::Application, 50'000 }); // SCR2 - - // P2 arrival: Case 2 (displayed, no nextDisplayed), becomes pending. - auto p2_phase1 = ComputeMetricsForPresent(qpc, p2, nullptr, state); - Assert::AreEqual(size_t(0), p2_phase1.size(), - L"P2 (phase 1): first call with nextDisplayed=nullptr should produce no metrics (pending only)."); - - // The pending call MUST NOT disturb the accumulated PCL chain. - AssertAreEqualWithinTolerance(accumAfterP1, state.accumulatedInput2FrameStartTime, 1e-9, - L"P2 (phase 1): accumulatedInput2FrameStartTime should remain unchanged while pending."); - Assert::AreEqual(uint64_t(30'000), state.lastReceivedNotDisplayedPclSimStart, - L"P2 (phase 1): lastReceivedNotDisplayedPclSimStart should remain at P1's sim start (30'000)."); - - // -------------------------------------------------------------------- - // P3: DISPLAYED, used only as nextDisplayed when finalizing P2. - // -------------------------------------------------------------------- FrameData p3{}; - p3.processId = PROCESS_ID; - p3.swapChainAddress = SWAPCHAIN; - p3.presentStartTime = 0; - p3.timeInPresent = 0; - p3.readyTime = 0; - p3.appSimStartTime = 0; + p3.finalState = PresentResult::Presented; + p3.displayed.PushBack({ FrameType::Application, 60'000 }); - p3.pclInputPingTime = 0; - p3.pclSimStartTime = 0; // no PCL for P3 itself + auto p3_rows = swapChain.ProcessPresent(qpc, std::move(p3)); + Assert::AreEqual(size_t(1), p3_rows.size()); + Assert::AreEqual(uint64_t(50'000), p3_rows[0].computed.metrics.screenTimeQpc); - p3.finalState = PresentResult::Presented; - p3.displayed.Clear(); - p3.displayed.PushBack({ FrameType::Application, 60'000 }); // some later screen time - - // P3 arrival: - // 1) Flush pending P2 using P3 as nextDisplayed -> Case 3, finalize P2. - auto p2_final = ComputeMetricsForPresent(qpc, p2, &p3, state); - Assert::AreEqual(size_t(1), p2_final.size(), - L"P2 (final): expected exactly one metrics record when flushing with nextDisplayed=P3."); - const auto& p2_metrics = p2_final[0].metrics; - - // 2) Now process P3's arrival as pending -> Case 2 with next=nullptr. - auto p3_phase1 = ComputeMetricsForPresent(qpc, p3, nullptr, state); - Assert::AreEqual(size_t(0), p3_phase1.size(), - L"P3 (phase 1): first call with nextDisplayed=nullptr should produce no metrics (pending only)."); - - // -------------------------------------------------------------------- - // Assertions for the P2 finalization (this is where PC Latency must appear). - // -------------------------------------------------------------------- - - // Precondition: we had a non-zero accumulated input→frame-start before finalizing P2. - Assert::IsTrue(accumAfterP1 > 0.0, - L"Precondition: expected non-zero accumulatedInput2FrameStartTime before P2 finalization."); - - // 1) PC Latency should be populated and positive for P2 when it finally - // reaches the screen after the dropped chain. - Assert::IsTrue(HasMetricValue(p2_metrics.msPcLatency), - L"P2 (final): msPcLatency should be populated for the displayed frame completing the dropped PCL chain."); - Assert::IsTrue(p2_metrics.msPcLatency > 0.0, - L"P2 (final): msPcLatency should be positive."); - - // 2) After completion, the accumulated input→frame-start time and the - // last-not-displayed PCL sim start should be reset to zero, matching - // the legacy PCL behavior. - AssertAreEqualWithinTolerance(0.0, state.accumulatedInput2FrameStartTime, 1e-9, - L"P2 (final): accumulatedInput2FrameStartTime should be reset to 0 after completion."); - Assert::AreEqual(uint64_t{ 0 }, state.lastReceivedNotDisplayedPclSimStart, - L"P2 (final): lastReceivedNotDisplayedPclSimStart should be reset to 0 after completion."); + Assert::IsTrue(HasMetricValue(p3_rows[0].computed.metrics.msPcLatency)); + Assert::IsTrue(p3_rows[0].computed.metrics.msPcLatency > 0.0); + AssertAreEqualWithinTolerance(0.0, swapChain.swapChain.accumulatedInput2FrameStartTime, 1e-9); + Assert::AreEqual(uint64_t{ 0 }, swapChain.swapChain.lastReceivedNotDisplayedPclSimStart); } TEST_METHOD(PcLatency_NoPclData_AllFrames_NoLatency) { - // Scenario: - // - P0 is dropped with no PC Latency timestamps. - // - P1 and P2 are displayed app frames but likewise carry no pclSimStartTime/pclInputPingTime. - // - We run the ReportMetrics-style scheduling: dropped frames are processed immediately, - // displayed frames are first queued (Case 2) and then finalized by the arrival of the - // next displayed present (Case 3). - // QPC plan (ticks at 10 MHz): - // - Screen times: SCR1 = 100'000, SCR2 = 120'000, SCR3 = 140'000. - // Expectations: - // - Every metrics record reports msPcLatency.has_value() == false. - // - accumulatedInput2FrameStartTime remains 0.0 throughout the sequence. - // - lastReceivedNotDisplayedPclSimStart never departs from 0 since no PCL timestamps exist. - QpcConverter qpc(10'000'000, 0); SwapChainCoreState state{}; - const uint32_t PROCESS_ID = 77; - const uint64_t SWAPCHAIN = 0x11AAu; + FrameData p0{}; + p0.finalState = PresentResult::Discarded; + + auto p0_results = ComputeMetricsForPresent(qpc, p0, state); + Assert::AreEqual(size_t(1), p0_results.size()); + Assert::IsFalse(HasMetricValue(p0_results[0].metrics.msPcLatency)); + AssertAreEqualWithinTolerance(0.0, state.accumulatedInput2FrameStartTime, 0.0001); + + FrameData p1{}; + p1.finalState = PresentResult::Presented; + p1.displayed.PushBack({ FrameType::Application, 100'000 }); + + auto p1_results = ComputeMetricsForPresent(qpc, p1, state); + Assert::AreEqual(size_t(1), p1_results.size()); + Assert::IsFalse(HasMetricValue(p1_results[0].metrics.msPcLatency)); + AssertAreEqualWithinTolerance(0.0, state.accumulatedInput2FrameStartTime, 0.0001); + + FrameData p2{}; + p2.finalState = PresentResult::Presented; + p2.displayed.PushBack({ FrameType::Application, 120'000 }); + + auto p2_results = ComputeMetricsForPresent(qpc, p2, state); + Assert::AreEqual(size_t(1), p2_results.size()); + Assert::IsFalse(HasMetricValue(p2_results[0].metrics.msPcLatency)); + AssertAreEqualWithinTolerance(0.0, state.accumulatedInput2FrameStartTime, 0.0001); + } + + TEST_METHOD(PcLatency_NoPclData_AllFrames_NoLatency_ProcessPresent) + { + QpcConverter qpc(10'000'000, 0); + UnifiedSwapChain swapChain{}; + + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; + + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); - // -------------------------------------------------------------------- - // P0: dropped frame without any PCL data - // -------------------------------------------------------------------- FrameData p0{}; - p0.processId = PROCESS_ID; - p0.swapChainAddress = SWAPCHAIN; - p0.pclInputPingTime = 0; - p0.pclSimStartTime = 0; p0.finalState = PresentResult::Discarded; + auto p0_rows = swapChain.ProcessPresent(qpc, std::move(p0)); + Assert::AreEqual(size_t(1), p0_rows.size()); + Assert::IsFalse(HasMetricValue(p0_rows[0].computed.metrics.msPcLatency)); - auto p0_results = ComputeMetricsForPresent(qpc, p0, nullptr, state); - Assert::AreEqual(size_t(1), p0_results.size(), - L"P0 (dropped) should emit one metrics record."); - Assert::IsFalse(HasMetricValue(p0_results[0].metrics.msPcLatency), - L"P0 should not report msPcLatency without PCL data."); - AssertAreEqualWithinTolerance(0.0, state.accumulatedInput2FrameStartTime, 0.0001, - L"P0 should not modify accumulatedInput2FrameStartTime when there is no PCL data."); - Assert::AreEqual(uint64_t(0), state.lastReceivedNotDisplayedPclSimStart, - L"P0 should leave lastReceivedNotDisplayedPclSimStart at 0."); - - // -------------------------------------------------------------------- - // P1: displayed frame without PCL data (pending, then finalized by P2) - // -------------------------------------------------------------------- FrameData p1{}; - p1.processId = PROCESS_ID; - p1.swapChainAddress = SWAPCHAIN; p1.finalState = PresentResult::Presented; p1.displayed.PushBack({ FrameType::Application, 100'000 }); + Assert::AreEqual(size_t(0), swapChain.ProcessPresent(qpc, std::move(p1)).size()); - auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, state); - Assert::AreEqual(size_t(0), p1_phase1.size(), - L"P1 pending pass should not emit metrics."); - AssertAreEqualWithinTolerance(0.0, state.accumulatedInput2FrameStartTime, 0.0001, - L"State.accumulatedInput2FrameStartTime must remain 0 after P1 pending pass."); - Assert::AreEqual(uint64_t(0), state.lastReceivedNotDisplayedPclSimStart, - L"lastReceivedNotDisplayedPclSimStart should remain 0 after P1 pending pass."); - - // -------------------------------------------------------------------- - // P2: displayed frame without PCL data (finalizes P1, then becomes pending) - // -------------------------------------------------------------------- FrameData p2{}; - p2.processId = PROCESS_ID; - p2.swapChainAddress = SWAPCHAIN; p2.finalState = PresentResult::Presented; p2.displayed.PushBack({ FrameType::Application, 120'000 }); + auto p2_rows = swapChain.ProcessPresent(qpc, std::move(p2)); + Assert::AreEqual(size_t(1), p2_rows.size()); + Assert::IsFalse(HasMetricValue(p2_rows[0].computed.metrics.msPcLatency)); + AssertAreEqualWithinTolerance(0.0, swapChain.swapChain.accumulatedInput2FrameStartTime, 0.0001); - auto p1_final = ComputeMetricsForPresent(qpc, p1, &p2, state); - Assert::AreEqual(size_t(1), p1_final.size(), - L"Finalizing P1 should emit exactly one metrics record."); - Assert::IsFalse(HasMetricValue(p1_final[0].metrics.msPcLatency), - L"P1 final metrics should not report msPcLatency without PCL data."); - AssertAreEqualWithinTolerance(0.0, state.accumulatedInput2FrameStartTime, 0.0001, - L"Accumulated input-to-frame-start time must remain 0 after finalizing P1."); - Assert::AreEqual(uint64_t(0), state.lastReceivedNotDisplayedPclSimStart, - L"lastReceivedNotDisplayedPclSimStart should remain 0 after finalizing P1."); - - auto p2_phase1 = ComputeMetricsForPresent(qpc, p2, nullptr, state); - Assert::AreEqual(size_t(0), p2_phase1.size(), - L"P2 pending pass should not emit metrics."); - - // -------------------------------------------------------------------- - // P3: helper displayed frame to flush P2 - // -------------------------------------------------------------------- FrameData p3{}; - p3.processId = PROCESS_ID; - p3.swapChainAddress = SWAPCHAIN; p3.finalState = PresentResult::Presented; p3.displayed.PushBack({ FrameType::Application, 140'000 }); - - auto p2_final = ComputeMetricsForPresent(qpc, p2, &p3, state); - Assert::AreEqual(size_t(1), p2_final.size(), - L"Finalizing P2 should emit exactly one metrics record."); - Assert::IsFalse(HasMetricValue(p2_final[0].metrics.msPcLatency), - L"P2 final metrics should not report msPcLatency without PCL data."); - AssertAreEqualWithinTolerance(0.0, state.accumulatedInput2FrameStartTime, 0.0001, - L"Accumulated input-to-frame-start time must still be 0 after P2."); - Assert::AreEqual(uint64_t(0), state.lastReceivedNotDisplayedPclSimStart, - L"lastReceivedNotDisplayedPclSimStart should remain 0 through the entire sequence."); - - auto p3_phase1 = ComputeMetricsForPresent(qpc, p3, nullptr, state); - Assert::AreEqual(size_t(0), p3_phase1.size(), - L"P3 pending pass is only for completeness and should not emit metrics."); + auto p3_rows = swapChain.ProcessPresent(qpc, std::move(p3)); + Assert::AreEqual(size_t(1), p3_rows.size()); + Assert::IsFalse(HasMetricValue(p3_rows[0].computed.metrics.msPcLatency)); } TEST_METHOD(PcLatency_SingleDisplayed_DirectSample_FirstEma) { - // Scenario: - // - Single displayed frame P0 provides both pclInputPingTime and pclSimStartTime. - // - There is no subsequent present, so we exercise Case 2 (nextDisplayed == nullptr) - // with two display samples on P0 to mirror the ReportMetrics behavior where the - // last display instance is deferred. - // - The first metrics record must report msPcLatency immediately and seed the EMA. - // Timing (ticks at 10 MHz): - // - Ping0 = 10'000, Sim0 = 20'000 (Δ = 1.0 ms) - // - Display samples: SCR0 = 50'000, SCR0b = 60'000 (provides nextScreenTime). - // Expectations: - // - msPcLatency.has_value() == true and > 0. - // - accumulatedInput2FrameStartTime remains 0 (no dropped chain). - // - Input2FrameStartTimeEma equals CalculateEma(0.0, Δ(PING0,SIM0), 0.1). - // - msPcLatency equals EMA + Δ(SIM0, SCR0), proving pclSimStartTime was used. - QpcConverter qpc(10'000'000, 0); SwapChainCoreState state{}; @@ -8425,122 +5928,141 @@ TEST_CLASS(ComputeMetricsForPresentTests) p0.pclSimStartTime = 20'000; p0.finalState = PresentResult::Presented; p0.displayed.PushBack({ FrameType::Application, 50'000 }); - p0.displayed.PushBack({ FrameType::Application, 60'000 }); - auto p0_results = ComputeMetricsForPresent(qpc, p0, nullptr, state); - Assert::AreEqual(size_t(1), p0_results.size(), - L"P0 should emit metrics immediately when nextDisplayed == nullptr and two display samples exist."); + auto p0_results = ComputeMetricsForPresent(qpc, p0, state); + Assert::AreEqual(size_t(1), p0_results.size()); const auto& p0_metrics = p0_results[0].metrics; - Assert::IsTrue(HasMetricValue(p0_metrics.msPcLatency), - L"P0 should report msPcLatency for a direct PCL sample."); - Assert::IsTrue(p0_metrics.msPcLatency > 0.0, - L"P0 msPcLatency should be positive."); - AssertAreEqualWithinTolerance(0.0, state.accumulatedInput2FrameStartTime, 0.0001, - L"Direct PCL sample should not touch accumulatedInput2FrameStartTime."); - Assert::AreEqual(uint64_t(0), state.lastReceivedNotDisplayedPclSimStart, - L"No dropped frames occurred, so there should be no pending pclSimStart."); + Assert::IsTrue(HasMetricValue(p0_metrics.msPcLatency)); + Assert::IsTrue(p0_metrics.msPcLatency > 0.0); + AssertAreEqualWithinTolerance(0.0, state.accumulatedInput2FrameStartTime, 0.0001); double deltaPingSim = qpc.DeltaUnsignedMilliSeconds(10'000, 20'000); double expectedEma = pmon::util::CalculateEma(0.0, deltaPingSim, 0.1); - AssertAreEqualWithinTolerance(expectedEma, state.Input2FrameStartTimeEma, 0.0001, - L"Input2FrameStartTimeEma should be seeded from the first Δ(PING,SIM)."); + Assert::AreEqual(expectedEma, state.Input2FrameStartTimeEma, 0.0001); double expectedLatency = expectedEma + qpc.DeltaSignedMilliSeconds(20'000, 50'000); - AssertAreEqualWithinTolerance(expectedLatency, p0_metrics.msPcLatency, 0.0001, - L"msPcLatency should use pclSimStartTime (not lastSimStartTime) plus the seeded EMA."); + AssertAreEqualWithinTolerance(expectedLatency, p0_metrics.msPcLatency, 0.0001); } - TEST_METHOD(PcLatency_TwoDisplayed_DirectSamples_UpdateEma) + TEST_METHOD(PcLatency_SingleDisplayed_DirectSample_FirstEma_ProcessPresent) { - // Scenario: - // - Two displayed frames (P0, P1) each provide direct PCL samples (Ping + Sim). - // - We mimic the ReportMetrics scheduling: - // P0 arrives -> pending only (Case 2). - // P1 arrives -> finalize P0 with nextDisplayed = P1 (Case 3), then queue P1 (Case 2). - // P2 helper -> finalize P1 (Case 3) to observe the EMA update. - // Expectations: - // - P0 final metrics report msPcLatency and seed the EMA. - // - P1 pending call emits no metrics. - // - After P1 is finalized, Input2FrameStartTimeEma changes (≠ value after P0) and stays > 0. - // - accumulatedInput2FrameStartTime stays 0 because no dropped chain exists. + QpcConverter qpc(10'000'000, 0); + UnifiedSwapChain swapChain{}; + + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; + + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); + + FrameData p0{}; + p0.pclInputPingTime = 10'000; + p0.pclSimStartTime = 20'000; + p0.finalState = PresentResult::Presented; + p0.displayed.PushBack({ FrameType::Application, 50'000 }); + + Assert::AreEqual(size_t(0), swapChain.ProcessPresent(qpc, std::move(p0)).size()); + + FrameData p1{}; + p1.finalState = PresentResult::Presented; + p1.displayed.PushBack({ FrameType::Application, 60'000 }); + + auto rows = swapChain.ProcessPresent(qpc, std::move(p1)); + Assert::AreEqual(size_t(1), rows.size()); + Assert::AreEqual(uint64_t(50'000), rows[0].computed.metrics.screenTimeQpc); + + const auto& p0_metrics = rows[0].computed.metrics; + Assert::IsTrue(HasMetricValue(p0_metrics.msPcLatency)); + Assert::IsTrue(p0_metrics.msPcLatency > 0.0); + + double deltaPingSim = qpc.DeltaUnsignedMilliSeconds(10'000, 20'000); + double expectedEma = pmon::util::CalculateEma(0.0, deltaPingSim, 0.1); + Assert::AreEqual(expectedEma, swapChain.swapChain.Input2FrameStartTimeEma, 0.0001); + double expectedLatency = expectedEma + qpc.DeltaSignedMilliSeconds(20'000, 50'000); + AssertAreEqualWithinTolerance(expectedLatency, p0_metrics.msPcLatency, 0.0001); + } + + TEST_METHOD(PcLatency_TwoDisplayed_DirectSamples_UpdateEma) + { QpcConverter qpc(10'000'000, 0); SwapChainCoreState state{}; - // -------------------------------------------------------------------- - // P0: first displayed frame with direct PCL sample - // -------------------------------------------------------------------- FrameData p0{}; p0.pclInputPingTime = 10'000; p0.pclSimStartTime = 20'000; p0.finalState = PresentResult::Presented; p0.displayed.PushBack({ FrameType::Application, 50'000 }); - auto p0_phase1 = ComputeMetricsForPresent(qpc, p0, nullptr, state); - Assert::AreEqual(size_t(0), p0_phase1.size(), - L"P0 pending pass should not emit metrics."); + auto p0_results = ComputeMetricsForPresent(qpc, p0, state); + Assert::AreEqual(size_t(1), p0_results.size()); + Assert::IsTrue(HasMetricValue(p0_results[0].metrics.msPcLatency)); + double emaAfterP0 = state.Input2FrameStartTimeEma; + Assert::IsTrue(emaAfterP0 > 0.0); - // -------------------------------------------------------------------- - // P1: second displayed frame with direct PCL sample - // -------------------------------------------------------------------- FrameData p1{}; p1.pclInputPingTime = 30'000; p1.pclSimStartTime = 40'000; p1.finalState = PresentResult::Presented; p1.displayed.PushBack({ FrameType::Application, 70'000 }); - auto p0_final = ComputeMetricsForPresent(qpc, p0, &p1, state); - Assert::AreEqual(size_t(1), p0_final.size(), - L"Finalizing P0 with nextDisplayed=P1 should emit exactly one metrics record."); - Assert::IsTrue(HasMetricValue(p0_final[0].metrics.msPcLatency), - L"P0 should report msPcLatency when finalized."); - double emaAfterP0 = state.Input2FrameStartTimeEma; - Assert::IsTrue(emaAfterP0 > 0.0, - L"EMA after P0 should be positive."); - AssertAreEqualWithinTolerance(0.0, state.accumulatedInput2FrameStartTime, 0.0001, - L"Accumulated input-to-frame-start time should remain zero after P0."); - - auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, state); - Assert::AreEqual(size_t(0), p1_phase1.size(), - L"P1 pending pass should not emit metrics."); - - // -------------------------------------------------------------------- - // P2: helper displayed frame to flush P1 - // -------------------------------------------------------------------- + auto p1_results = ComputeMetricsForPresent(qpc, p1, state); + Assert::AreEqual(size_t(1), p1_results.size()); + Assert::IsTrue(HasMetricValue(p1_results[0].metrics.msPcLatency)); + double emaAfterP1 = state.Input2FrameStartTimeEma; + Assert::IsTrue(emaAfterP1 > 0.0); + Assert::IsTrue(emaAfterP1 != emaAfterP0); + AssertAreEqualWithinTolerance(0.0, state.accumulatedInput2FrameStartTime, 0.0001); + } + + TEST_METHOD(PcLatency_TwoDisplayed_DirectSamples_UpdateEma_ProcessPresent) + { + QpcConverter qpc(10'000'000, 0); + UnifiedSwapChain swapChain{}; + + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; + + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); + + FrameData p0{}; + p0.pclInputPingTime = 10'000; + p0.pclSimStartTime = 20'000; + p0.finalState = PresentResult::Presented; + p0.displayed.PushBack({ FrameType::Application, 50'000 }); + + Assert::AreEqual(size_t(0), swapChain.ProcessPresent(qpc, std::move(p0)).size()); + + FrameData p1{}; + p1.pclInputPingTime = 30'000; + p1.pclSimStartTime = 40'000; + p1.finalState = PresentResult::Presented; + p1.displayed.PushBack({ FrameType::Application, 70'000 }); + + auto p0_rows = swapChain.ProcessPresent(qpc, std::move(p1)); + Assert::AreEqual(size_t(1), p0_rows.size()); + Assert::IsTrue(HasMetricValue(p0_rows[0].computed.metrics.msPcLatency)); + double emaAfterP0 = swapChain.swapChain.Input2FrameStartTimeEma; + FrameData p2{}; p2.finalState = PresentResult::Presented; p2.displayed.PushBack({ FrameType::Application, 90'000 }); - auto p1_final = ComputeMetricsForPresent(qpc, p1, &p2, state); - Assert::AreEqual(size_t(1), p1_final.size(), - L"Finalizing P1 should emit exactly one metrics record."); - Assert::IsTrue(HasMetricValue(p1_final[0].metrics.msPcLatency), - L"P1 should report msPcLatency when finalized."); - double emaAfterP1 = state.Input2FrameStartTimeEma; - Assert::IsTrue(emaAfterP1 > 0.0, - L"EMA after P1 should stay positive."); - Assert::IsTrue(emaAfterP1 != emaAfterP0, - L"EMA after P1 must differ from the first-sample EMA after P0."); - AssertAreEqualWithinTolerance(0.0, state.accumulatedInput2FrameStartTime, 0.0001, - L"No dropped chain should mean accumulatedInput2FrameStartTime stays at 0."); - - auto p2_phase1 = ComputeMetricsForPresent(qpc, p2, nullptr, state); - Assert::AreEqual(size_t(0), p2_phase1.size(), - L"P2 pending pass is only to mirror the pipeline; it should emit no metrics."); + auto p1_rows = swapChain.ProcessPresent(qpc, std::move(p2)); + Assert::AreEqual(size_t(1), p1_rows.size()); + Assert::IsTrue(HasMetricValue(p1_rows[0].computed.metrics.msPcLatency)); + double emaAfterP1 = swapChain.swapChain.Input2FrameStartTimeEma; + Assert::IsTrue(emaAfterP1 != emaAfterP0); } TEST_METHOD(PcLatency_Dropped_DirectPcl_InitializesAccum) { - // Scenario: - // - A dropped frame P0 carries both pclInputPingTime and pclSimStartTime. - // - Without any displayed frame to consume it, the PC Latency accumulator should - // be initialized to Δ(PING0, SIM0) while msPcLatency remains absent. - // Expectations: - // - P0's metrics do not expose msPcLatency (it was dropped). - // - accumulatedInput2FrameStartTime equals Δ(PING0, SIM0) and > 0. - // - lastReceivedNotDisplayedPclSimStart latches SIM0. - QpcConverter qpc(10'000'000, 0); SwapChainCoreState state{}; @@ -8549,32 +6071,44 @@ TEST_CLASS(ComputeMetricsForPresentTests) p0.pclSimStartTime = 20'000; p0.finalState = PresentResult::Discarded; - auto p0_results = ComputeMetricsForPresent(qpc, p0, nullptr, state); - Assert::AreEqual(size_t(1), p0_results.size(), - L"Dropped frames should emit one metrics record immediately."); - Assert::IsFalse(HasMetricValue(p0_results[0].metrics.msPcLatency), - L"Dropped frames must not report msPcLatency."); + auto p0_results = ComputeMetricsForPresent(qpc, p0, state); + Assert::AreEqual(size_t(1), p0_results.size()); + Assert::IsFalse(HasMetricValue(p0_results[0].metrics.msPcLatency)); double expectedAccum = qpc.DeltaUnsignedMilliSeconds(10'000, 20'000); - Assert::IsTrue(state.accumulatedInput2FrameStartTime > 0.0, - L"Accumulated input-to-frame-start time should be initialized."); - AssertAreEqualWithinTolerance(expectedAccum, state.accumulatedInput2FrameStartTime, 0.0001, - L"Accumulator should equal Δ(PING0, SIM0)."); - Assert::AreEqual(uint64_t(20'000), state.lastReceivedNotDisplayedPclSimStart, - L"lastReceivedNotDisplayedPclSimStart should track P0's pclSimStartTime."); + Assert::AreEqual(expectedAccum, state.accumulatedInput2FrameStartTime, 0.0001); + Assert::AreEqual(uint64_t(20'000), state.lastReceivedNotDisplayedPclSimStart); } - TEST_METHOD(PcLatency_DroppedChain_SimOnly_ExtendsAccum) + TEST_METHOD(PcLatency_Dropped_DirectPcl_InitializesAccum_ProcessPresent) { - // Scenario: - // - P0 (dropped) has both Ping and Sim, seeding the accumulator. - // - P1 (dropped) has only pclSimStartTime and should extend the accumulator by the - // delta between SIM0 and SIM1. - // Expectations: - // - P1 still reports no msPcLatency. - // - accumulatedInput2FrameStartTime after P1 > accumulated time after P0. - // - lastReceivedNotDisplayedPclSimStart equals SIM1. + QpcConverter qpc(10'000'000, 0); + UnifiedSwapChain swapChain{}; + + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; + + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); + + FrameData p0{}; + p0.pclInputPingTime = 10'000; + p0.pclSimStartTime = 20'000; + p0.finalState = PresentResult::Discarded; + + auto rows = swapChain.ProcessPresent(qpc, std::move(p0)); + Assert::AreEqual(size_t(1), rows.size()); + Assert::IsFalse(HasMetricValue(rows[0].computed.metrics.msPcLatency)); + + double expectedAccum = qpc.DeltaUnsignedMilliSeconds(10'000, 20'000); + Assert::AreEqual(expectedAccum, swapChain.swapChain.accumulatedInput2FrameStartTime, 0.0001); + Assert::AreEqual(uint64_t(20'000), swapChain.swapChain.lastReceivedNotDisplayedPclSimStart); + } + TEST_METHOD(PcLatency_DroppedChain_SimOnly_ExtendsAccum) + { QpcConverter qpc(10'000'000, 0); SwapChainCoreState state{}; @@ -8583,9 +6117,8 @@ TEST_CLASS(ComputeMetricsForPresentTests) p0.pclSimStartTime = 20'000; p0.finalState = PresentResult::Discarded; - auto p0_results = ComputeMetricsForPresent(qpc, p0, nullptr, state); + auto p0_results = ComputeMetricsForPresent(qpc, p0, state); Assert::AreEqual(size_t(1), p0_results.size()); - Assert::IsFalse(HasMetricValue(p0_results[0].metrics.msPcLatency)); double accumAfterP0 = state.accumulatedInput2FrameStartTime; FrameData p1{}; @@ -8593,27 +6126,45 @@ TEST_CLASS(ComputeMetricsForPresentTests) p1.pclSimStartTime = 30'000; p1.finalState = PresentResult::Discarded; - auto p1_results = ComputeMetricsForPresent(qpc, p1, nullptr, state); - Assert::AreEqual(size_t(1), p1_results.size(), - L"Second dropped frame should emit one metrics record."); - Assert::IsFalse(HasMetricValue(p1_results[0].metrics.msPcLatency), - L"Dropped frames never report msPcLatency."); - Assert::IsTrue(state.accumulatedInput2FrameStartTime > accumAfterP0, - L"Accumulator should grow when a sim-only dropped frame follows an existing chain."); - Assert::AreEqual(uint64_t(30'000), state.lastReceivedNotDisplayedPclSimStart, - L"Sim-only dropped frames still update lastReceivedNotDisplayedPclSimStart."); + auto p1_results = ComputeMetricsForPresent(qpc, p1, state); + Assert::AreEqual(size_t(1), p1_results.size()); + Assert::IsFalse(HasMetricValue(p1_results[0].metrics.msPcLatency)); + Assert::IsTrue(state.accumulatedInput2FrameStartTime > accumAfterP0); + Assert::AreEqual(uint64_t(30'000), state.lastReceivedNotDisplayedPclSimStart); } - TEST_METHOD(PcLatency_Dropped_SimOnly_NoAccum_NoEffect) + TEST_METHOD(PcLatency_DroppedChain_SimOnly_ExtendsAccum_ProcessPresent) { - // Scenario: - // - A single dropped frame P0 only provides pclSimStartTime (no ping) and there is - // no existing accumulator. - // Expectations: - // - msPcLatency remains absent. - // - accumulatedInput2FrameStartTime stays at 0 (chain not started). - // - lastReceivedNotDisplayedPclSimStart updates to SIM0 for possible future chaining. + QpcConverter qpc(10'000'000, 0); + UnifiedSwapChain swapChain{}; + + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; + + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); + + FrameData p0{}; + p0.pclInputPingTime = 10'000; + p0.pclSimStartTime = 20'000; + p0.finalState = PresentResult::Discarded; + + (void)swapChain.ProcessPresent(qpc, std::move(p0)); + const double accumAfterP0 = swapChain.swapChain.accumulatedInput2FrameStartTime; + + FrameData p1{}; + p1.pclSimStartTime = 30'000; + p1.finalState = PresentResult::Discarded; + (void)swapChain.ProcessPresent(qpc, std::move(p1)); + Assert::IsTrue(swapChain.swapChain.accumulatedInput2FrameStartTime > accumAfterP0); + Assert::AreEqual(uint64_t(30'000), swapChain.swapChain.lastReceivedNotDisplayedPclSimStart); + } + + TEST_METHOD(PcLatency_Dropped_SimOnly_NoAccum_NoEffect) + { QpcConverter qpc(10'000'000, 0); SwapChainCoreState state{}; @@ -8622,28 +6173,37 @@ TEST_CLASS(ComputeMetricsForPresentTests) p0.pclSimStartTime = 25'000; p0.finalState = PresentResult::Discarded; - auto p0_results = ComputeMetricsForPresent(qpc, p0, nullptr, state); + auto p0_results = ComputeMetricsForPresent(qpc, p0, state); Assert::AreEqual(size_t(1), p0_results.size()); Assert::IsFalse(HasMetricValue(p0_results[0].metrics.msPcLatency)); - AssertAreEqualWithinTolerance(0.0, state.accumulatedInput2FrameStartTime, 0.0001, - L"Accumulator should remain 0 when a sim-only drop has no pending chain."); - Assert::AreEqual(uint64_t(25'000), state.lastReceivedNotDisplayedPclSimStart, - L"Sim-only drop should remember its pclSimStartTime even if no accumulator exists yet."); + AssertAreEqualWithinTolerance(0.0, state.accumulatedInput2FrameStartTime, 0.0001); + Assert::AreEqual(uint64_t(25'000), state.lastReceivedNotDisplayedPclSimStart); } - TEST_METHOD(PcLatency_Displayed_SimOnly_NoAccum_UsesExistingEma) + TEST_METHOD(PcLatency_Dropped_SimOnly_NoAccum_NoEffect_ProcessPresent) { - // Scenario: - // - P0 is displayed with a direct PCL sample, seeding the EMA. - // - P1 is displayed with only pclSimStartTime (no ping) and there is no accumulated chain. - // - We follow the full pipeline: - // P0 pending, then finalized by P1 (Case 3). - // P1 pending, then finalized by helper P2. - // Expectations: - // - P1 final metrics report msPcLatency despite having no new ping. - // - accumulatedInput2FrameStartTime stays at 0 (no dropped chain was active). - // - Input2FrameStartTimeEma remains positive (not reset to 0). + QpcConverter qpc(10'000'000, 0); + UnifiedSwapChain swapChain{}; + + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); + + FrameData p0{}; + p0.pclSimStartTime = 25'000; + p0.finalState = PresentResult::Discarded; + + (void)swapChain.ProcessPresent(qpc, std::move(p0)); + AssertAreEqualWithinTolerance(0.0, swapChain.swapChain.accumulatedInput2FrameStartTime, 0.0001); + Assert::AreEqual(uint64_t(25'000), swapChain.swapChain.lastReceivedNotDisplayedPclSimStart); + } + + TEST_METHOD(PcLatency_Displayed_SimOnly_NoAccum_UsesExistingEma) + { QpcConverter qpc(10'000'000, 0); SwapChainCoreState state{}; @@ -8653,59 +6213,107 @@ TEST_CLASS(ComputeMetricsForPresentTests) p0.finalState = PresentResult::Presented; p0.displayed.PushBack({ FrameType::Application, 50'000 }); - auto p0_phase1 = ComputeMetricsForPresent(qpc, p0, nullptr, state); - Assert::AreEqual(size_t(0), p0_phase1.size()); + auto p0_results = ComputeMetricsForPresent(qpc, p0, state); + Assert::AreEqual(size_t(1), p0_results.size()); + Assert::IsTrue(HasMetricValue(p0_results[0].metrics.msPcLatency)); + Assert::IsTrue(state.Input2FrameStartTimeEma > 0.0); FrameData p1{}; - p1.pclInputPingTime = 0; p1.pclSimStartTime = 35'000; p1.finalState = PresentResult::Presented; p1.displayed.PushBack({ FrameType::Application, 70'000 }); - auto p0_final = ComputeMetricsForPresent(qpc, p0, &p1, state); - Assert::AreEqual(size_t(1), p0_final.size()); - Assert::IsTrue(HasMetricValue(p0_final[0].metrics.msPcLatency)); - double emaAfterP0 = state.Input2FrameStartTimeEma; - Assert::IsTrue(emaAfterP0 > 0.0); + auto p1_results = ComputeMetricsForPresent(qpc, p1, state); + Assert::AreEqual(size_t(1), p1_results.size()); + Assert::IsTrue(HasMetricValue(p1_results[0].metrics.msPcLatency)); + AssertAreEqualWithinTolerance(0.0, state.accumulatedInput2FrameStartTime, 0.0001); + Assert::IsTrue(state.Input2FrameStartTimeEma > 0.0); + } + + TEST_METHOD(PcLatency_Displayed_SimOnly_NoAccum_UsesExistingEma_ProcessPresent) + { + QpcConverter qpc(10'000'000, 0); + UnifiedSwapChain swapChain{}; + + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; + + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); + + FrameData p0{}; + p0.pclInputPingTime = 10'000; + p0.pclSimStartTime = 20'000; + p0.finalState = PresentResult::Presented; + p0.displayed.PushBack({ FrameType::Application, 50'000 }); + + Assert::AreEqual(size_t(0), swapChain.ProcessPresent(qpc, std::move(p0)).size()); + + FrameData p1{}; + p1.pclSimStartTime = 35'000; + p1.finalState = PresentResult::Presented; + p1.displayed.PushBack({ FrameType::Application, 70'000 }); - auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, state); - Assert::AreEqual(size_t(0), p1_phase1.size()); + auto p0_rows = swapChain.ProcessPresent(qpc, std::move(p1)); + Assert::AreEqual(size_t(1), p0_rows.size()); + Assert::IsTrue(HasMetricValue(p0_rows[0].computed.metrics.msPcLatency)); FrameData p2{}; p2.finalState = PresentResult::Presented; p2.displayed.PushBack({ FrameType::Application, 90'000 }); - auto p1_final = ComputeMetricsForPresent(qpc, p1, &p2, state); - Assert::AreEqual(size_t(1), p1_final.size()); - const auto& p1_metrics = p1_final[0].metrics; - Assert::IsTrue(HasMetricValue(p1_metrics.msPcLatency), - L"P1 should report msPcLatency despite missing pclInputPingTime."); - Assert::IsTrue(p1_metrics.msPcLatency > 0.0, - L"P1 msPcLatency should stay positive."); - AssertAreEqualWithinTolerance(0.0, state.accumulatedInput2FrameStartTime, 0.0001, - L"No dropped chain means the accumulator must stay zero."); - Assert::IsTrue(state.Input2FrameStartTimeEma > 0.0, - L"EMA should not be reset when a sim-only displayed frame uses existing history."); + auto p1_rows = swapChain.ProcessPresent(qpc, std::move(p2)); + Assert::AreEqual(size_t(1), p1_rows.size()); + Assert::IsTrue(HasMetricValue(p1_rows[0].computed.metrics.msPcLatency)); + AssertAreEqualWithinTolerance(0.0, swapChain.swapChain.accumulatedInput2FrameStartTime, 0.0001); + } + + TEST_METHOD(PcLatency_Displayed_NoPclSim_UsesLastSimStart) + { + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + + FrameData p0{}; + p0.pclInputPingTime = 10'000; + p0.pclSimStartTime = 30'000; + p0.finalState = PresentResult::Presented; + p0.displayed.PushBack({ FrameType::Application, 70'000 }); + + auto p0_results = ComputeMetricsForPresent(qpc, p0, state); + Assert::AreEqual(size_t(1), p0_results.size()); + Assert::IsTrue(HasMetricValue(p0_results[0].metrics.msPcLatency)); + double emaAfterP0 = state.Input2FrameStartTimeEma; + uint64_t fallbackSimStart = state.lastSimStartTime; + Assert::AreEqual(uint64_t(30'000), fallbackSimStart); + + FrameData p1{}; + p1.finalState = PresentResult::Presented; + p1.displayed.PushBack({ FrameType::Application, 90'000 }); + + auto p1_results = ComputeMetricsForPresent(qpc, p1, state); + Assert::AreEqual(size_t(1), p1_results.size()); + const auto& p1_metrics = p1_results[0].metrics; + Assert::IsTrue(HasMetricValue(p1_metrics.msPcLatency)); + AssertAreEqualWithinTolerance(emaAfterP0, state.Input2FrameStartTimeEma, 0.0001); - auto p2_phase1 = ComputeMetricsForPresent(qpc, p2, nullptr, state); - Assert::AreEqual(size_t(0), p2_phase1.size()); + double expectedLatency = emaAfterP0 + qpc.DeltaSignedMilliSeconds(fallbackSimStart, 90'000); + AssertAreEqualWithinTolerance(expectedLatency, p1_metrics.msPcLatency, 0.0001); } - TEST_METHOD(PcLatency_Displayed_NoPclSim_UsesLastSimStart) + TEST_METHOD(PcLatency_Displayed_NoPclSim_UsesLastSimStart_ProcessPresent) { - // Scenario: - // - P0 is displayed with a full PCL sample to seed both the EMA and lastSimStartTime. - // - P1 is displayed without any PCL timestamps (pclSimStartTime == 0, pclInputPingTime == 0). - // - P1 should still produce msPcLatency by combining the existing EMA with the fallback - // state.lastSimStartTime recorded after P0. - // Call schedule mirrors ReportMetrics: P0 pending, finalized by P1; P1 pending, finalized by P2. - // Expectations: - // - P1 final metrics report msPcLatency.has_value() == true. - // - Input2FrameStartTimeEma is unchanged from the P0 sample (no new data). - // - msPcLatency equals EMA_after_P0 + Δ(lastSimStartTime_after_P0, SCR1), proving the fallback path. + QpcConverter qpc(10'000'000, 0); + UnifiedSwapChain swapChain{}; - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState state{}; + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; + + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); FrameData p0{}; p0.pclInputPingTime = 10'000; @@ -8713,60 +6321,31 @@ TEST_CLASS(ComputeMetricsForPresentTests) p0.finalState = PresentResult::Presented; p0.displayed.PushBack({ FrameType::Application, 70'000 }); - auto p0_phase1 = ComputeMetricsForPresent(qpc, p0, nullptr, state); - Assert::AreEqual(size_t(0), p0_phase1.size()); + Assert::AreEqual(size_t(0), swapChain.ProcessPresent(qpc, std::move(p0)).size()); FrameData p1{}; - p1.pclInputPingTime = 0; - p1.pclSimStartTime = 0; p1.finalState = PresentResult::Presented; p1.displayed.PushBack({ FrameType::Application, 90'000 }); - auto p0_final = ComputeMetricsForPresent(qpc, p0, &p1, state); - Assert::AreEqual(size_t(1), p0_final.size()); - Assert::IsTrue(HasMetricValue(p0_final[0].metrics.msPcLatency)); - double emaAfterP0 = state.Input2FrameStartTimeEma; - uint64_t fallbackSimStart = state.lastSimStartTime; - Assert::IsTrue(emaAfterP0 > 0.0, - L"EMA must be initialized after the first direct sample."); - Assert::AreEqual(uint64_t(30'000), fallbackSimStart, - L"lastSimStartTime should latch P0's pclSimStartTime when it is displayed."); - - auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, state); - Assert::AreEqual(size_t(0), p1_phase1.size()); + auto p0_rows = swapChain.ProcessPresent(qpc, std::move(p1)); + Assert::AreEqual(size_t(1), p0_rows.size()); + double emaAfterP0 = swapChain.swapChain.Input2FrameStartTimeEma; + uint64_t fallbackSimStart = swapChain.swapChain.lastSimStartTime; FrameData p2{}; p2.finalState = PresentResult::Presented; p2.displayed.PushBack({ FrameType::Application, 110'000 }); - auto p1_final = ComputeMetricsForPresent(qpc, p1, &p2, state); - Assert::AreEqual(size_t(1), p1_final.size()); - const auto& p1_metrics = p1_final[0].metrics; - Assert::IsTrue(HasMetricValue(p1_metrics.msPcLatency), - L"P1 should still report msPcLatency using the fallback lastSimStartTime."); - AssertAreEqualWithinTolerance(emaAfterP0, state.Input2FrameStartTimeEma, 0.0001, - L"EMA should remain unchanged when no new PCL sample exists."); - double expectedLatency = emaAfterP0 + qpc.DeltaSignedMilliSeconds(fallbackSimStart, 90'000); - AssertAreEqualWithinTolerance(expectedLatency, p1_metrics.msPcLatency, 0.0001, - L"msPcLatency should use the stored EMA plus the delta from lastSimStartTime to screen time."); - AssertAreEqualWithinTolerance(0.0, state.accumulatedInput2FrameStartTime, 0.0001, - L"Accumulator should remain zero in this scenario."); + auto p1_rows = swapChain.ProcessPresent(qpc, std::move(p2)); + Assert::AreEqual(size_t(1), p1_rows.size()); + Assert::AreEqual(uint64_t(90'000), p1_rows[0].computed.metrics.screenTimeQpc); - auto p2_phase1 = ComputeMetricsForPresent(qpc, p2, nullptr, state); - Assert::AreEqual(size_t(0), p2_phase1.size()); + double expectedLatency = emaAfterP0 + qpc.DeltaSignedMilliSeconds(fallbackSimStart, 90'000); + AssertAreEqualWithinTolerance(expectedLatency, p1_rows[0].computed.metrics.msPcLatency, 0.0001); } TEST_METHOD(PcLatency_Dropped_DirectPcl_OverwritesOldAccum) { - // Scenario: - // - Dropped frames P0 (Ping+Sim) and P1 (Sim only) create an accumulated chain A_old. - // - A new dropped frame P2 arrives with its own Ping+Sim and should overwrite (not extend) - // the accumulator, effectively starting a brand new chain. - // Expectations: - // - Accumulator after P2 equals Δ(PING2, SIM2) exactly (no residue from A_old). - // - lastReceivedNotDisplayedPclSimStart equals SIM2. - // - P2 still reports no msPcLatency. - QpcConverter qpc(10'000'000, 0); SwapChainCoreState state{}; @@ -8774,517 +6353,439 @@ TEST_CLASS(ComputeMetricsForPresentTests) p0.pclInputPingTime = 10'000; p0.pclSimStartTime = 20'000; p0.finalState = PresentResult::Discarded; - ComputeMetricsForPresent(qpc, p0, nullptr, state); + ComputeMetricsForPresent(qpc, p0, state); FrameData p1{}; - p1.pclInputPingTime = 0; p1.pclSimStartTime = 30'000; p1.finalState = PresentResult::Discarded; - ComputeMetricsForPresent(qpc, p1, nullptr, state); + ComputeMetricsForPresent(qpc, p1, state); - double accumBeforeP2 = state.accumulatedInput2FrameStartTime; - Assert::IsTrue(accumBeforeP2 > 0.0, - L"Precondition: accumulator should already be non-zero before introducing P2."); + Assert::IsTrue(state.accumulatedInput2FrameStartTime > 0.0); FrameData p2{}; p2.pclInputPingTime = 100'000; p2.pclSimStartTime = 120'000; p2.finalState = PresentResult::Discarded; - auto p2_results = ComputeMetricsForPresent(qpc, p2, nullptr, state); + auto p2_results = ComputeMetricsForPresent(qpc, p2, state); Assert::AreEqual(size_t(1), p2_results.size()); Assert::IsFalse(HasMetricValue(p2_results[0].metrics.msPcLatency)); double expectedAccum = qpc.DeltaUnsignedMilliSeconds(100'000, 120'000); - AssertAreEqualWithinTolerance(expectedAccum, state.accumulatedInput2FrameStartTime, 0.0001, - L"New dropped frame with Ping+Sim should overwrite the accumulator with its own delta."); - Assert::AreEqual(uint64_t(120'000), state.lastReceivedNotDisplayedPclSimStart, - L"lastReceivedNotDisplayedPclSimStart should latch the newest sim start."); + AssertAreEqualWithinTolerance(expectedAccum, state.accumulatedInput2FrameStartTime, 0.0001); + Assert::AreEqual(uint64_t(120'000), state.lastReceivedNotDisplayedPclSimStart); } - TEST_METHOD(PcLatency_IncompleteDroppedChain_DoesNotAffectDirectSample) + TEST_METHOD(PcLatency_Dropped_DirectPcl_OverwritesOldAccum_ProcessPresent) { - // Scenario: - // - D0 (dropped, Ping+Sim) followed by D1 (dropped, Sim-only) builds an incomplete chain. - // - No displayed frame consumes it before a new direct-sample present P0 arrives. - // - P0 should be treated as a fresh direct measurement: EMA behaves like a first sample - // from P0 alone and the stale accumulator is cleared. - // Expectations: - // - D0/D1 never report msPcLatency. - // - P0 final metrics report msPcLatency.has_value() == true. - // - Input2FrameStartTimeEma after P0 equals CalculateEma(0.0, Δ(P0.Ping, P0.Sim), 0.1). - // - accumulatedInput2FrameStartTime resets to 0 after the displayed frame completes. + QpcConverter qpc(10'000'000, 0); + UnifiedSwapChain swapChain{}; + + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; + + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); + + FrameData p0{}; + p0.pclInputPingTime = 10'000; + p0.pclSimStartTime = 20'000; + p0.finalState = PresentResult::Discarded; + (void)swapChain.ProcessPresent(qpc, std::move(p0)); + + FrameData p1{}; + p1.pclSimStartTime = 30'000; + p1.finalState = PresentResult::Discarded; + (void)swapChain.ProcessPresent(qpc, std::move(p1)); + + FrameData p2{}; + p2.pclInputPingTime = 100'000; + p2.pclSimStartTime = 120'000; + p2.finalState = PresentResult::Discarded; + + (void)swapChain.ProcessPresent(qpc, std::move(p2)); + + double expectedAccum = qpc.DeltaUnsignedMilliSeconds(100'000, 120'000); + AssertAreEqualWithinTolerance(expectedAccum, swapChain.swapChain.accumulatedInput2FrameStartTime, 0.0001); + Assert::AreEqual(uint64_t(120'000), swapChain.swapChain.lastReceivedNotDisplayedPclSimStart); + } + TEST_METHOD(PcLatency_IncompleteDroppedChain_DoesNotAffectDirectSample) + { QpcConverter qpc(10'000'000, 0); SwapChainCoreState state{}; - // Dropped chain D0 -> D1 (incomplete) FrameData d0{}; d0.pclInputPingTime = 10'000; d0.pclSimStartTime = 20'000; d0.finalState = PresentResult::Discarded; - auto d0_results = ComputeMetricsForPresent(qpc, d0, nullptr, state); + auto d0_results = ComputeMetricsForPresent(qpc, d0, state); Assert::AreEqual(size_t(1), d0_results.size()); Assert::IsFalse(HasMetricValue(d0_results[0].metrics.msPcLatency)); FrameData d1{}; - d1.pclInputPingTime = 0; d1.pclSimStartTime = 30'000; d1.finalState = PresentResult::Discarded; - auto d1_results = ComputeMetricsForPresent(qpc, d1, nullptr, state); + auto d1_results = ComputeMetricsForPresent(qpc, d1, state); Assert::AreEqual(size_t(1), d1_results.size()); Assert::IsFalse(HasMetricValue(d1_results[0].metrics.msPcLatency)); - double accumBeforeDisplayed = state.accumulatedInput2FrameStartTime; - Assert::IsTrue(accumBeforeDisplayed > 0.0, - L"Incomplete chain should leave a non-zero accumulator."); + Assert::IsTrue(state.accumulatedInput2FrameStartTime > 0.0); + + FrameData p0{}; + p0.pclInputPingTime = 100'000; + p0.pclSimStartTime = 120'000; + p0.finalState = PresentResult::Presented; + p0.displayed.PushBack({ FrameType::Application, 150'000 }); + + auto p0_results = ComputeMetricsForPresent(qpc, p0, state); + Assert::AreEqual(size_t(1), p0_results.size()); + const auto& p0_metrics = p0_results[0].metrics; + Assert::IsTrue(HasMetricValue(p0_metrics.msPcLatency)); + + double expectedFirstEma = pmon::util::CalculateEma(0.0, + qpc.DeltaUnsignedMilliSeconds(100'000, 120'000), + 0.1); + AssertAreEqualWithinTolerance(expectedFirstEma, state.Input2FrameStartTimeEma, 0.0001); + AssertAreEqualWithinTolerance(0.0, state.accumulatedInput2FrameStartTime, 0.0001); + Assert::AreEqual(uint64_t(0), state.lastReceivedNotDisplayedPclSimStart); + } + + TEST_METHOD(PcLatency_IncompleteDroppedChain_DoesNotAffectDirectSample_ProcessPresent) + { + QpcConverter qpc(10'000'000, 0); + UnifiedSwapChain swapChain{}; + + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; + + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); + + FrameData d0{}; + d0.pclInputPingTime = 10'000; + d0.pclSimStartTime = 20'000; + d0.finalState = PresentResult::Discarded; + (void)swapChain.ProcessPresent(qpc, std::move(d0)); + + FrameData d1{}; + d1.pclSimStartTime = 30'000; + d1.finalState = PresentResult::Discarded; + (void)swapChain.ProcessPresent(qpc, std::move(d1)); + Assert::IsTrue(swapChain.swapChain.accumulatedInput2FrameStartTime > 0.0); - // Displayed P0 with a brand-new direct sample FrameData p0{}; p0.pclInputPingTime = 100'000; p0.pclSimStartTime = 120'000; p0.finalState = PresentResult::Presented; p0.displayed.PushBack({ FrameType::Application, 150'000 }); - auto p0_phase1 = ComputeMetricsForPresent(qpc, p0, nullptr, state); - Assert::AreEqual(size_t(0), p0_phase1.size()); + Assert::AreEqual(size_t(0), swapChain.ProcessPresent(qpc, std::move(p0)).size()); FrameData p1{}; p1.finalState = PresentResult::Presented; p1.displayed.PushBack({ FrameType::Application, 180'000 }); - auto p0_final = ComputeMetricsForPresent(qpc, p0, &p1, state); - Assert::AreEqual(size_t(1), p0_final.size()); - const auto& p0_metrics = p0_final[0].metrics; - Assert::IsTrue(HasMetricValue(p0_metrics.msPcLatency), - L"Displayed frame with direct PCL data must report msPcLatency."); - Assert::IsTrue(p0_metrics.msPcLatency > 0.0, - L"msPcLatency should be positive for P0."); + auto rows = swapChain.ProcessPresent(qpc, std::move(p1)); + Assert::AreEqual(size_t(1), rows.size()); + Assert::IsTrue(HasMetricValue(rows[0].computed.metrics.msPcLatency)); double expectedFirstEma = pmon::util::CalculateEma(0.0, qpc.DeltaUnsignedMilliSeconds(100'000, 120'000), 0.1); - AssertAreEqualWithinTolerance(expectedFirstEma, state.Input2FrameStartTimeEma, 0.0001, - L"EMA after P0 should match a first-sample EMA that ignores stale accumulation."); - AssertAreEqualWithinTolerance(0.0, state.accumulatedInput2FrameStartTime, 0.0001, - L"Accumulator must be cleared once the displayed frame consumes the chain."); - Assert::AreEqual(uint64_t(0), state.lastReceivedNotDisplayedPclSimStart, - L"Pending pclSimStart markers should be cleared once the chain completes."); - - auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, state); - Assert::AreEqual(size_t(0), p1_phase1.size()); + AssertAreEqualWithinTolerance(expectedFirstEma, swapChain.swapChain.Input2FrameStartTimeEma, 0.0001); + AssertAreEqualWithinTolerance(0.0, swapChain.swapChain.accumulatedInput2FrameStartTime, 0.0001); + Assert::AreEqual(uint64_t(0), swapChain.swapChain.lastReceivedNotDisplayedPclSimStart); } }; TEST_CLASS(InstrumentedMetricsTests) { public: + // V1: ComputeMetricsForPresent (nullptr next). V2: *_ProcessPresent duplicates. TEST_METHOD(InstrumentedCpuGpu_AppFrame_FullData_UsesPclSimStart) { - // This test verifies the "instrumented CPU/GPU" metrics on an application frame: - // - // - msInstrumentedSleep - // - msInstrumentedGpuLatency - // - msBetweenSimStarts (PCL sim preferred over App sim) - // - // We construct: - // - // QPC frequency = 10 MHz - // - // Pre-state in swapChain: - // lastSimStartTime = 10'000 (this represents the previous frame's sim start) - // - // P0 (the frame under test) – APP FRAME: - // appSleepStartTime = 1'000 - // appSleepEndTime = 11'000 // Δsleep = 10'000 ticks - // appSimStartTime =100'000 // should NOT be used for between-sim-starts - // pclSimStartTime = 20'000 // PCL sim should win for between-sim-starts - // gpuStartTime = 21'000 // GPU start time used for GPU latency - // displayed = one Application entry at screenTime = 50'000 - // - // Derived deltas: - // sleep Δ: 11'000 - 1'000 = 10'000 ticks - // PCL sim Δ: 20'000 - 10'000 = 10'000 ticks - // GPU latency Δ: 21'000 - 11'000 = 10'000 ticks - // - // With QPC = 10 MHz, 10'000 ticks = 0.001 ms. - // - // Call pattern (ReportMetrics-style for a single displayed app frame): - // - // P0 arrives: - // ComputeMetricsForPresent(P0, nullptr, chain) // Case 2, pending only - // - // P1 arrives later: - // ComputeMetricsForPresent(P0, &P1, chain) // Case 3, finalize P0 - // ComputeMetricsForPresent(P1, nullptr, chain) // pending P1 (ignored in this test) - // - // We verify on P0's final metrics: - // - msInstrumentedSleep has a value and matches Δ(appSleepStart, appSleepEnd). - // - msInstrumentedGpuLatency has a value and matches Δ(appSleepEnd, gpuStartTime). - // - msBetweenSimStarts has a value and matches Δ(lastSimStartTime, P0.pclSimStartTime), - // proving PCL sim is preferred over App sim for between-sim-starts. - QpcConverter qpc(10'000'000, 0); SwapChainCoreState chain{}; - // Seed lastSimStartTime to simulate a previous frame. chain.lastSimStartTime = 10'000; chain.animationErrorSource = AnimationErrorSource::PCLatency; - const uint32_t PROCESS_ID = 1234; - const uint64_t SWAPCHAIN = 0xABC0; - - // -------------------------------------------------------------------- - // P0: Application frame with full instrumented CPU/GPU data - // -------------------------------------------------------------------- FrameData p0{}; - p0.processId = PROCESS_ID; - p0.swapChainAddress = SWAPCHAIN; - p0.presentStartTime = 0; p0.timeInPresent = 0; p0.readyTime = 0; - - // Instrumented CPU / sim: p0.appSleepStartTime = 1'000; p0.appSleepEndTime = 11'000; - p0.appSimStartTime = 100'000; // should NOT be used for between-sim-starts - p0.pclSimStartTime = 20'000; // should be used instead - - // GPU start time for GPU latency: + p0.appSimStartTime = 100'000; + p0.pclSimStartTime = 20'000; p0.gpuStartTime = 21'000; + p0.finalState = PresentResult::Presented; + p0.displayed.PushBack({ FrameType::Application, 50'000 }); + + auto p0_results = ComputeMetricsForPresent(qpc, p0, chain); + Assert::AreEqual(size_t(1), p0_results.size()); + + const auto& m0 = p0_results[0].metrics; + double expectedSleepMs = qpc.DeltaUnsignedMilliSeconds(1'000, 11'000); + double expectedGpuMs = qpc.DeltaUnsignedMilliSeconds(11'000, 21'000); + double expectedBetween = qpc.DeltaUnsignedMilliSeconds(10'000, 20'000); + + Assert::IsTrue(HasMetricValue(m0.msInstrumentedSleep)); + Assert::AreEqual(expectedSleepMs, m0.msInstrumentedSleep, 1e-6); + Assert::IsTrue(HasMetricValue(m0.msInstrumentedGpuLatency)); + Assert::AreEqual(expectedGpuMs, m0.msInstrumentedGpuLatency, 1e-6); + Assert::IsTrue(HasMetricValue(m0.msBetweenSimStarts)); + AssertAreEqualWithinTolerance(expectedBetween, m0.msBetweenSimStarts, 1e-6); + } + + TEST_METHOD(InstrumentedCpuGpu_AppFrame_FullData_UsesPclSimStart_ProcessPresent) + { + QpcConverter qpc(10'000'000, 0); + UnifiedSwapChain swapChain{}; + + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; + + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); + swapChain.swapChain.lastSimStartTime = 10'000; + swapChain.swapChain.animationErrorSource = AnimationErrorSource::PCLatency; - // Mark as displayed Application frame + FrameData p0{}; + p0.appSleepStartTime = 1'000; + p0.appSleepEndTime = 11'000; + p0.appSimStartTime = 100'000; + p0.pclSimStartTime = 20'000; + p0.gpuStartTime = 21'000; p0.finalState = PresentResult::Presented; - p0.displayed.Clear(); - p0.displayed.PushBack({ FrameType::Application, 50'000 }); // screenTime = 50'000 + p0.displayed.PushBack({ FrameType::Application, 50'000 }); - // First call: P0 arrives, becomes pending (no metrics yet). - auto p0_phase1 = ComputeMetricsForPresent(qpc, p0, nullptr, chain); - Assert::AreEqual(size_t(0), p0_phase1.size(), - L"P0 (phase 1): pending-only call with nextDisplayed=nullptr should produce no metrics."); + Assert::AreEqual(size_t(0), swapChain.ProcessPresent(qpc, std::move(p0)).size()); - // -------------------------------------------------------------------- - // P1: simple next displayed app frame (used only as nextDisplayed for P0) - // -------------------------------------------------------------------- FrameData p1{}; - p1.processId = PROCESS_ID; - p1.swapChainAddress = SWAPCHAIN; + p1.finalState = PresentResult::Presented; + p1.displayed.PushBack({ FrameType::Application, 60'000 }); - p1.presentStartTime = 0; - p1.timeInPresent = 0; - p1.readyTime = 0; + auto rows = swapChain.ProcessPresent(qpc, std::move(p1)); + Assert::AreEqual(size_t(1), rows.size()); + Assert::AreEqual(uint64_t(50'000), rows[0].computed.metrics.screenTimeQpc); - p1.finalState = PresentResult::Presented; - p1.displayed.Clear(); - p1.displayed.PushBack({ FrameType::Application, 60'000 }); // later display, not important - - // Second call: P1 arrives, finalize P0 using P1 as nextDisplayed (Case 3). - auto p0_final = ComputeMetricsForPresent(qpc, p0, &p1, chain); - Assert::AreEqual(size_t(1), p0_final.size(), - L"P0 (final): expected exactly one metrics record when flushed with nextDisplayed=P1."); - - const auto& m0 = p0_final[0].metrics; - - // -------------------------------------------------------------------- - // Assertions for P0's instrumented CPU/GPU metrics - // -------------------------------------------------------------------- - // Expected values based on our chosen QPC times: - double expectedSleepMs = qpc.DeltaUnsignedMilliSeconds(1'000, 11'000); // 10'000 ticks - double expectedGpuMs = qpc.DeltaUnsignedMilliSeconds(11'000, 21'000); // 10'000 ticks - double expectedBetween = qpc.DeltaUnsignedMilliSeconds(10'000, 20'000); // 10'000 ticks - - // 1) Instrumented sleep - Assert::IsTrue(HasMetricValue(m0.msInstrumentedSleep), - L"P0: msInstrumentedSleep should have a value for valid AppSleepStart/End."); - AssertAreEqualWithinTolerance(expectedSleepMs, m0.msInstrumentedSleep, 1e-6, - L"P0: msInstrumentedSleep did not match expected Δ(AppSleepStart, AppSleepEnd)."); - - // 2) Instrumented GPU latency (start = AppSleepEndTime since it is non-zero) - Assert::IsTrue(HasMetricValue(m0.msInstrumentedGpuLatency), - L"P0: msInstrumentedGpuLatency should have a value when InstrumentedStartTime and gpuStartTime are valid."); - AssertAreEqualWithinTolerance(expectedGpuMs, m0.msInstrumentedGpuLatency, 1e-6, - L"P0: msInstrumentedGpuLatency did not match expected Δ(AppSleepEndTime, gpuStartTime)."); - - // 3) Between sim starts: PCL sim (20'000) must win over App sim (100'000) - Assert::IsTrue(HasMetricValue(m0.msBetweenSimStarts), - L"P0: msBetweenSimStarts should have a value when lastSimStartTime and PclSimStartTime are non-zero."); - AssertAreEqualWithinTolerance(expectedBetween, m0.msBetweenSimStarts, 1e-6, - L"P0: msBetweenSimStarts should be based on PCL sim start, not App sim start."); + const auto& m0 = rows[0].computed.metrics; + double expectedSleepMs = qpc.DeltaUnsignedMilliSeconds(1'000, 11'000); + double expectedGpuMs = qpc.DeltaUnsignedMilliSeconds(11'000, 21'000); + double expectedBetween = qpc.DeltaUnsignedMilliSeconds(10'000, 20'000); + + Assert::IsTrue(HasMetricValue(m0.msInstrumentedSleep)); + Assert::AreEqual(expectedSleepMs, m0.msInstrumentedSleep, 1e-6); + Assert::IsTrue(HasMetricValue(m0.msInstrumentedGpuLatency)); + Assert::AreEqual(expectedGpuMs, m0.msInstrumentedGpuLatency, 1e-6); + Assert::IsTrue(HasMetricValue(m0.msBetweenSimStarts)); + AssertAreEqualWithinTolerance(expectedBetween, m0.msBetweenSimStarts, 1e-6); } + TEST_METHOD(InstrumentedDisplay_AppFrame_FullData_ComputesAll) { - // This test verifies the "instrumented display" metrics on a displayed - // application frame: - // - // - msInstrumentedRenderLatency - // - msReadyTimeToDisplayLatency - // - msInstrumentedLatency (total app-instrumented latency) - // - // New invariant (unified metrics): - // These metrics are only computed when: - // - the frame is an Application frame (isAppFrame == true), AND - // - the frame is displayed (isDisplayed == true). - // - // We construct: - // - // QPC frequency = 10 MHz - // - // P0 (the frame under test) – DISPLAYED APP FRAME: - // appRenderSubmitStartTime = 10'000 - // readyTime = 20'000 - // appSleepEndTime = 5'000 // used as InstrumentedStartTime - // screenTime = 30'000 (Application entry in displayed) - // - // Derived deltas: - // render latency: 30'000 - 10'000 = 20'000 ticks - // ready→display: 30'000 - 20'000 = 10'000 ticks - // total inst. latency: 30'000 - 5'000 = 25'000 ticks - // - // With QPC = 10 MHz: - // 10'000 ticks = 0.001 ms - // 20'000 ticks = 0.002 ms - // 25'000 ticks = 0.0025 ms - // - // Call pattern (mirroring ReportMetrics for a displayed app frame): - // - // P0 arrives: - // ComputeMetricsForPresent(P0, nullptr, chain) // Case 2, pending only - // - // P1 arrives: - // ComputeMetricsForPresent(P0, &P1, chain) // Case 3, finalize P0 - // ComputeMetricsForPresent(P1, nullptr, chain) // pending P1 (ignored) - // - // We verify on P0's final metrics: - // - msInstrumentedRenderLatency has a value and matches Δ(appRenderSubmitStartTime, screenTime). - // - msReadyTimeToDisplayLatency has a value and matches Δ(readyTime, screenTime). - // - msInstrumentedLatency has a value and matches Δ(appSleepEndTime, screenTime). - QpcConverter qpc(10'000'000, 0); SwapChainCoreState chain{}; - const uint32_t PROCESS_ID = 1234; - const uint64_t SWAPCHAIN = 0xABC0; - - // -------------------------------------------------------------------- - // P0: Displayed Application frame with full instrumented display data - // -------------------------------------------------------------------- FrameData p0{}; - p0.processId = PROCESS_ID; - p0.swapChainAddress = SWAPCHAIN; - p0.presentStartTime = 0; p0.timeInPresent = 0; - p0.readyTime = 20'000; // ReadyTime - - // Instrumented markers + p0.readyTime = 20'000; p0.appRenderSubmitStartTime = 10'000; p0.appSleepEndTime = 5'000; - p0.appSimStartTime = 0; // not needed in this test + p0.appSimStartTime = 0; + p0.finalState = PresentResult::Presented; + p0.displayed.PushBack({ FrameType::Application, 30'000 }); + + auto p0_results = ComputeMetricsForPresent(qpc, p0, chain); + Assert::AreEqual(size_t(1), p0_results.size()); + + const auto& m0 = p0_results[0].metrics; + double expectedRenderMs = qpc.DeltaUnsignedMilliSeconds(10'000, 30'000); + double expectedReadyMs = qpc.DeltaUnsignedMilliSeconds(20'000, 30'000); + double expectedTotalMs = qpc.DeltaUnsignedMilliSeconds(5'000, 30'000); + + Assert::IsTrue(HasMetricValue(m0.msInstrumentedRenderLatency)); + Assert::AreEqual(expectedRenderMs, m0.msInstrumentedRenderLatency, 1e-6); + Assert::IsTrue(HasMetricValue(m0.msReadyTimeToDisplayLatency)); + Assert::AreEqual(expectedReadyMs, m0.msReadyTimeToDisplayLatency, 1e-6); + Assert::IsTrue(HasMetricValue(m0.msInstrumentedLatency)); + Assert::AreEqual(expectedTotalMs, m0.msInstrumentedLatency, 1e-6); + } + + TEST_METHOD(InstrumentedDisplay_AppFrame_FullData_ComputesAll_ProcessPresent) + { + QpcConverter qpc(10'000'000, 0); + UnifiedSwapChain swapChain{}; + + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; - // Mark as displayed Application frame with a single screen time. + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); + + FrameData p0{}; + p0.readyTime = 20'000; + p0.appRenderSubmitStartTime = 10'000; + p0.appSleepEndTime = 5'000; p0.finalState = PresentResult::Presented; - p0.displayed.Clear(); - p0.displayed.PushBack({ FrameType::Application, 30'000 }); // screenTime = 30'000 + p0.displayed.PushBack({ FrameType::Application, 30'000 }); - // First call: P0 arrives, becomes pending. - auto p0_phase1 = ComputeMetricsForPresent(qpc, p0, nullptr, chain); - Assert::AreEqual(size_t(0), p0_phase1.size(), - L"P0 (phase 1): pending-only call with nextDisplayed=nullptr should produce no metrics."); + Assert::AreEqual(size_t(0), swapChain.ProcessPresent(qpc, std::move(p0)).size()); - // -------------------------------------------------------------------- - // P1: Next displayed app frame (used only as nextDisplayed for P0) - // -------------------------------------------------------------------- FrameData p1{}; - p1.processId = PROCESS_ID; - p1.swapChainAddress = SWAPCHAIN; + p1.finalState = PresentResult::Presented; + p1.displayed.PushBack({ FrameType::Application, 40'000 }); - p1.presentStartTime = 0; - p1.timeInPresent = 0; - p1.readyTime = 0; + auto rows = swapChain.ProcessPresent(qpc, std::move(p1)); + Assert::AreEqual(size_t(1), rows.size()); + Assert::AreEqual(uint64_t(30'000), rows[0].computed.metrics.screenTimeQpc); - p1.finalState = PresentResult::Presented; - p1.displayed.Clear(); - p1.displayed.PushBack({ FrameType::Application, 40'000 }); // later display - - // Second call: finalize P0 with nextDisplayed=P1 - auto p0_final = ComputeMetricsForPresent(qpc, p0, &p1, chain); - Assert::AreEqual(size_t(1), p0_final.size(), - L"P0 (final): expected exactly one metrics record when flushed with nextDisplayed=P1."); - - const auto& m0 = p0_final[0].metrics; - - // For completeness, process P1 as pending (not used in this test). - auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, chain); - Assert::AreEqual(size_t(0), p1_phase1.size(), - L"P1 (phase 1): first call with nextDisplayed=nullptr should produce no metrics (pending only)."); - - // -------------------------------------------------------------------- - // Assertions for P0's instrumented display metrics - // -------------------------------------------------------------------- - double expectedRenderMs = qpc.DeltaUnsignedMilliSeconds(10'000, 30'000); // 20'000 ticks - double expectedReadyMs = qpc.DeltaUnsignedMilliSeconds(20'000, 30'000); // 10'000 ticks - double expectedTotalMs = qpc.DeltaUnsignedMilliSeconds(5'000, 30'000); // 25'000 ticks - - // Render latency - Assert::IsTrue(HasMetricValue(m0.msInstrumentedRenderLatency), - L"P0: msInstrumentedRenderLatency should have a value for a displayed app frame with AppRenderSubmitStartTime."); - AssertAreEqualWithinTolerance(expectedRenderMs, m0.msInstrumentedRenderLatency, 1e-6, - L"P0: msInstrumentedRenderLatency did not match expected Δ(AppRenderSubmitStartTime, screenTime)."); - - // Ready-to-display latency - Assert::IsTrue(HasMetricValue(m0.msReadyTimeToDisplayLatency), - L"P0: msReadyTimeToDisplayLatency should have a value when ReadyTime and screenTime are valid."); - AssertAreEqualWithinTolerance(expectedReadyMs, m0.msReadyTimeToDisplayLatency, 1e-6, - L"P0: msReadyTimeToDisplayLatency did not match expected Δ(ReadyTime, screenTime)."); - - // Total instrumented latency: from appSleepEndTime to screenTime - Assert::IsTrue(HasMetricValue(m0.msInstrumentedLatency), - L"P0: msInstrumentedLatency should have a value when there is a valid instrumented start time."); - AssertAreEqualWithinTolerance(expectedTotalMs, m0.msInstrumentedLatency, 1e-6, - L"P0: msInstrumentedLatency did not match expected Δ(AppSleepEndTime, screenTime)."); + const auto& m0 = rows[0].computed.metrics; + double expectedRenderMs = qpc.DeltaUnsignedMilliSeconds(10'000, 30'000); + double expectedReadyMs = qpc.DeltaUnsignedMilliSeconds(20'000, 30'000); + double expectedTotalMs = qpc.DeltaUnsignedMilliSeconds(5'000, 30'000); + + Assert::IsTrue(HasMetricValue(m0.msInstrumentedRenderLatency)); + Assert::AreEqual(expectedRenderMs, m0.msInstrumentedRenderLatency, 1e-6); + Assert::IsTrue(HasMetricValue(m0.msReadyTimeToDisplayLatency)); + Assert::AreEqual(expectedReadyMs, m0.msReadyTimeToDisplayLatency, 1e-6); + Assert::IsTrue(HasMetricValue(m0.msInstrumentedLatency)); + Assert::AreEqual(expectedTotalMs, m0.msInstrumentedLatency, 1e-6); } TEST_METHOD(InstrumentedCpuGpu_AppFrame_NoSleep_UsesAppSimStart) { - // Scenario: - // - Validate the instrumented CPU/GPU metrics when the application never enters an - // instrumented sleep, forcing GPU latency to fall back to appSimStart. - // - Also ensure msBetweenSimStarts uses the stored lastSimStartTime → appSimStart delta - // when no PCL sim timestamp is present. - // - // QPC frequency: 10 MHz. - // - // Pre-state: - // chain.lastSimStartTime = 40'000 (represents the previous frame's sim start). - // - // P0 (displayed Application frame): - // appSleepStartTime = 0 - // appSleepEndTime = 0 - // appSimStartTime = 70'000 (used for GPU latency + between-sim-starts) - // pclSimStartTime = 0 (forces AppSim fallback) - // gpuStartTime = 90'000 - // screenTime = 120'000 (Application entry) - // Δ(sim start vs last) = 30'000 ticks, Δ(sim start → gpu) = 20'000 ticks. - // - // Call pattern (Case 2/3): - // P0 pending → Compute(..., nullptr) - // P1 arrives → Compute(P0, &P1) to finalize P0, then Compute(P1, nullptr) to seed next pending. - // - // Expectations on P0 final metrics: - // - msInstrumentedSleep has no value (no sleep interval). - // - msInstrumentedGpuLatency uses appSimStartTime (70'000 → 90'000). - // - msBetweenSimStarts computes 40'000 → 70'000. - QpcConverter qpc(10'000'000, 0); SwapChainCoreState chain{}; chain.lastSimStartTime = 40'000; chain.animationErrorSource = AnimationErrorSource::AppProvider; - const uint32_t PROCESS_ID = 4321; - const uint64_t SWAPCHAIN = 0x2222; + FrameData p0{}; + p0.appSimStartTime = 70'000; + p0.gpuStartTime = 90'000; + p0.finalState = PresentResult::Presented; + p0.displayed.PushBack({ FrameType::Application, 120'000 }); + + auto p0_results = ComputeMetricsForPresent(qpc, p0, chain); + Assert::AreEqual(size_t(1), p0_results.size()); + + const auto& m0 = p0_results[0].metrics; + Assert::IsFalse(HasMetricValue(m0.msInstrumentedSleep)); + Assert::IsTrue(HasMetricValue(m0.msInstrumentedGpuLatency)); + Assert::IsTrue(HasMetricValue(m0.msBetweenSimStarts)); + + double expectedGpuMs = qpc.DeltaUnsignedMilliSeconds(70'000, 90'000); + double expectedBetweenMs = qpc.DeltaUnsignedMilliSeconds(40'000, 70'000); + + Assert::AreEqual(expectedGpuMs, m0.msInstrumentedGpuLatency, 1e-6); + Assert::AreEqual(expectedBetweenMs, m0.msBetweenSimStarts, 1e-6); + } + + TEST_METHOD(InstrumentedCpuGpu_AppFrame_NoSleep_UsesAppSimStart_ProcessPresent) + { + QpcConverter qpc(10'000'000, 0); + UnifiedSwapChain swapChain{}; + + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; + + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); + swapChain.swapChain.lastSimStartTime = 40'000; + swapChain.swapChain.animationErrorSource = AnimationErrorSource::AppProvider; - // P0: displayed Application frame with no sleep range but valid appSimStart FrameData p0{}; - p0.processId = PROCESS_ID; - p0.swapChainAddress = SWAPCHAIN; p0.appSimStartTime = 70'000; p0.gpuStartTime = 90'000; p0.finalState = PresentResult::Presented; - p0.displayed.Clear(); p0.displayed.PushBack({ FrameType::Application, 120'000 }); - auto p0_phase1 = ComputeMetricsForPresent(qpc, p0, nullptr, chain); - Assert::AreEqual(size_t(0), p0_phase1.size(), - L"P0 (phase 1) should stay pending when nextDisplayed is unavailable."); + Assert::AreEqual(size_t(0), swapChain.ProcessPresent(qpc, std::move(p0)).size()); - // P1: minimal next displayed application frame FrameData p1{}; - p1.processId = PROCESS_ID; - p1.swapChainAddress = SWAPCHAIN; p1.finalState = PresentResult::Presented; - p1.displayed.Clear(); p1.displayed.PushBack({ FrameType::Application, 150'000 }); - auto p0_final = ComputeMetricsForPresent(qpc, p0, &p1, chain); - Assert::AreEqual(size_t(1), p0_final.size(), - L"P0 (final) should emit exactly one metrics record once nextDisplayed is provided."); + auto rows = swapChain.ProcessPresent(qpc, std::move(p1)); + Assert::AreEqual(size_t(1), rows.size()); + Assert::AreEqual(uint64_t(120'000), rows[0].computed.metrics.screenTimeQpc); - const auto& m0 = p0_final[0].metrics; - Assert::IsFalse(HasMetricValue(m0.msInstrumentedSleep), - L"P0: Instrumented sleep must be absent when the app never emitted sleep markers."); - Assert::IsTrue(HasMetricValue(m0.msInstrumentedGpuLatency), - L"P0: GPU latency should fall back to AppSimStart when no sleep end exists."); - Assert::IsTrue(HasMetricValue(m0.msBetweenSimStarts), - L"P0: Between-sim-starts should use the stored lastSimStartTime when AppSimStart is valid."); + const auto& m0 = rows[0].computed.metrics; + Assert::IsFalse(HasMetricValue(m0.msInstrumentedSleep)); + Assert::IsTrue(HasMetricValue(m0.msInstrumentedGpuLatency)); + Assert::IsTrue(HasMetricValue(m0.msBetweenSimStarts)); double expectedGpuMs = qpc.DeltaUnsignedMilliSeconds(70'000, 90'000); double expectedBetweenMs = qpc.DeltaUnsignedMilliSeconds(40'000, 70'000); - AssertAreEqualWithinTolerance(expectedGpuMs, m0.msInstrumentedGpuLatency, 1e-6, - L"P0: msInstrumentedGpuLatency should measure Δ(AppSimStartTime, gpuStartTime)."); - AssertAreEqualWithinTolerance(expectedBetweenMs, m0.msBetweenSimStarts, 1e-6, - L"P0: msBetweenSimStarts should use AppSimStart when no PCL sim exists."); - - auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, chain); - Assert::AreEqual(size_t(0), p1_phase1.size(), - L"P1 (phase 1) remains pending for completeness."); + Assert::AreEqual(expectedGpuMs, m0.msInstrumentedGpuLatency, 1e-6); + Assert::AreEqual(expectedBetweenMs, m0.msBetweenSimStarts, 1e-6); } TEST_METHOD(InstrumentedCpuGpu_AppFrame_NoSleepNoSim_NoInstrumentedCpuGpu) { - // Scenario: - // - Displayed Application frame with no instrumented sleep markers and neither appSimStartTime - // nor pclSimStartTime populated. GPU start exists, but there is no instrumented start anchor. - // - // QPC frequency: 10 MHz. - // Pre-state: chain.lastSimStartTime = 55'000. - // P0 fields: appSleepStart=0, appSleepEnd=0, appSimStart=0, pclSimStart=0, gpuStart=80'000, - // screenTime=100'000 (Application display). - // Derived deltas: none are valid because the start markers are zero. - // - // Call pattern (Case 2/3): - // P0 pending → Compute(..., nullptr) - // P1 arrives → Compute(P0, &P1) to flush, then Compute(P1, nullptr) for completeness. - // - // Expectations: all three instrumented CPU metrics stay std::nullopt for P0. - QpcConverter qpc(10'000'000, 0); SwapChainCoreState chain{}; chain.lastSimStartTime = 55'000; - const uint32_t PROCESS_ID = 9876; - const uint64_t SWAPCHAIN = 0xEF00; + FrameData p0{}; + p0.gpuStartTime = 80'000; + p0.finalState = PresentResult::Presented; + p0.displayed.PushBack({ FrameType::Application, 100'000 }); + + auto p0_results = ComputeMetricsForPresent(qpc, p0, chain); + Assert::AreEqual(size_t(1), p0_results.size()); + + const auto& m0 = p0_results[0].metrics; + Assert::IsFalse(HasMetricValue(m0.msInstrumentedSleep)); + Assert::IsFalse(HasMetricValue(m0.msInstrumentedGpuLatency)); + Assert::IsFalse(HasMetricValue(m0.msBetweenSimStarts)); + } + + TEST_METHOD(InstrumentedCpuGpu_AppFrame_NoSleepNoSim_NoInstrumentedCpuGpu_ProcessPresent) + { + QpcConverter qpc(10'000'000, 0); + UnifiedSwapChain swapChain{}; + + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; + + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); + swapChain.swapChain.lastSimStartTime = 55'000; FrameData p0{}; - p0.processId = PROCESS_ID; - p0.swapChainAddress = SWAPCHAIN; p0.gpuStartTime = 80'000; p0.finalState = PresentResult::Presented; - p0.displayed.Clear(); p0.displayed.PushBack({ FrameType::Application, 100'000 }); - auto p0_phase1 = ComputeMetricsForPresent(qpc, p0, nullptr, chain); - Assert::AreEqual(size_t(0), p0_phase1.size()); + Assert::AreEqual(size_t(0), swapChain.ProcessPresent(qpc, std::move(p0)).size()); FrameData p1{}; - p1.processId = PROCESS_ID; - p1.swapChainAddress = SWAPCHAIN; p1.finalState = PresentResult::Presented; p1.displayed.PushBack({ FrameType::Application, 120'000 }); - auto p0_final = ComputeMetricsForPresent(qpc, p0, &p1, chain); - Assert::AreEqual(size_t(1), p0_final.size()); - - const auto& m0 = p0_final[0].metrics; - Assert::IsFalse(HasMetricValue(m0.msInstrumentedSleep), - L"P0: sleep metrics require both start and end markers."); - Assert::IsFalse(HasMetricValue(m0.msInstrumentedGpuLatency), - L"P0: GPU latency must remain off without an instrumented start time."); - Assert::IsFalse(HasMetricValue(m0.msBetweenSimStarts), - L"P0: between-sim-starts cannot be computed without a new sim start."); + auto rows = swapChain.ProcessPresent(qpc, std::move(p1)); + Assert::AreEqual(size_t(1), rows.size()); - auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, chain); - Assert::AreEqual(size_t(0), p1_phase1.size()); + const auto& m0 = rows[0].computed.metrics; + Assert::IsFalse(HasMetricValue(m0.msInstrumentedSleep)); + Assert::IsFalse(HasMetricValue(m0.msInstrumentedGpuLatency)); + Assert::IsFalse(HasMetricValue(m0.msBetweenSimStarts)); } TEST_METHOD(InstrumentedCpuGpu_AppFrame_NotDisplayed_StillComputed) @@ -9298,10 +6799,10 @@ TEST_CLASS(ComputeMetricsForPresentTests) // Pre-state: chain.lastSimStartTime = 5'000. // P0 fields: appSleepStart=10'000, appSleepEnd=25'000, appSimStart=30'000, // gpuStart=45'000, no displayed entries (finalState = Discarded). - // Derived deltas: sleep Δ = 15'000 ticks, GPU latency Δ = 20'000 ticks, - // between-sim-starts Δ = 30'000 ticks. + // Derived deltas: sleep Delta = 15'000 ticks, GPU latency Delta = 20'000 ticks, + // between-sim-starts Delta = 30'000 ticks. // - // Call pattern: Case 1 (pure dropped) → single ComputeMetricsForPresent call with nextDisplayed == nullptr. + // Call pattern: Case 1 (pure dropped) -> single ComputeMetricsForPresent call. // // Expectations: instrumented sleep/GPU/betweenSimStarts all have values with the deltas above, // and display instrumented metrics remain std::nullopt. @@ -9318,7 +6819,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) p0.gpuStartTime = 45'000; p0.finalState = PresentResult::Discarded; - auto p0_results = ComputeMetricsForPresent(qpc, p0, nullptr, chain); + auto p0_results = ComputeMetricsForPresent(qpc, p0, chain); Assert::AreEqual(size_t(1), p0_results.size(), L"Dropped frames should emit their metrics immediately (Case 1)."); @@ -9333,89 +6834,98 @@ TEST_CLASS(ComputeMetricsForPresentTests) Assert::IsTrue(HasMetricValue(m0.msInstrumentedGpuLatency)); AssertAreEqualWithinTolerance(expectedGpuMs, m0.msInstrumentedGpuLatency, 1e-6); - Assert::IsTrue(HasMetricValue(m0.msBetweenSimStarts)); - AssertAreEqualWithinTolerance(expectedBetweenMs, m0.msBetweenSimStarts, 1e-6); + Assert::IsTrue(HasMetricValue(m0.msBetweenSimStarts)); + AssertAreEqualWithinTolerance(expectedBetweenMs, m0.msBetweenSimStarts, 1e-6); + + Assert::IsFalse(HasMetricValue(m0.msInstrumentedRenderLatency), + L"Display-dependent metrics must stay off for non-displayed frames."); + Assert::IsFalse(HasMetricValue(m0.msReadyTimeToDisplayLatency)); + Assert::IsFalse(HasMetricValue(m0.msInstrumentedLatency)); + } + + TEST_METHOD(InstrumentedCpuGpu_V1_FirstDisplayNonAppFrame_Ignored) + { + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + chain.lastSimStartTime = 60'000; + + FrameData p0{}; + p0.appSleepStartTime = 11'000; + p0.appSleepEndTime = 21'000; + p0.appSimStartTime = 70'000; + p0.pclSimStartTime = 72'000; + p0.gpuStartTime = 90'000; + p0.finalState = PresentResult::Presented; + p0.displayed.PushBack({ FrameType::Repeated, 120'000 }); + + auto p0_results = ComputeMetricsForPresent(qpc, p0, chain); + Assert::AreEqual(size_t(1), p0_results.size()); - Assert::IsFalse(HasMetricValue(m0.msInstrumentedRenderLatency), - L"Display-dependent metrics must stay off for non-displayed frames."); - Assert::IsFalse(HasMetricValue(m0.msReadyTimeToDisplayLatency)); - Assert::IsFalse(HasMetricValue(m0.msInstrumentedLatency)); + const auto& m0 = p0_results[0].metrics; + Assert::IsFalse(HasMetricValue(m0.msInstrumentedSleep)); + Assert::IsFalse(HasMetricValue(m0.msInstrumentedGpuLatency)); + Assert::IsFalse(HasMetricValue(m0.msBetweenSimStarts)); } - TEST_METHOD(InstrumentedCpuGpu_NonAppFrame_Ignored) + TEST_METHOD(InstrumentedCpuGpu_NonAppFrame_Ignored_ProcessPresent) { - // Scenario: - // - Displayed frame whose sole display entry is FrameType::Repeated, so DisplayIndexing never - // marks an Application display. - // - Even with instrumented CPU/GPU markers present, msInstrumentedSleep/GpuLatency/BetweenSimStarts - // must remain unset because the display instance is not an app frame. - // - // QPC frequency: 10 MHz. - // Pre-state: chain.lastSimStartTime = 60'000. - // P0 fields: appSleepStart=11'000, appSleepEnd=21'000, appSimStart=70'000, pclSimStart=72'000, - // gpuStart=90'000, displayed[0] = (Repeated, 120'000). - // Derived deltas (that should be ignored): sleep Δ = 10'000 ticks, GPU Δ = 69'000 ticks, - // between-sim-starts Δ = 10'000 ticks. - // - // Call pattern: Case 2/3 (P0 pending, finalized by a synthetic P1, then P1 pending). - // - // Expectations: all instrumented CPU metrics remain std::nullopt for P0. - - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState chain{}; - chain.lastSimStartTime = 60'000; + QpcConverter qpc(10'000'000, 0); + UnifiedSwapChain swapChain{}; const uint32_t PROCESS_ID = 5555; const uint64_t SWAPCHAIN = 0xDEADBEEF; + FrameData bootstrap{}; + bootstrap.processId = PROCESS_ID; + bootstrap.swapChainAddress = SWAPCHAIN; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; + + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); + swapChain.swapChain.lastSimStartTime = 60'000; + FrameData p0{}; p0.processId = PROCESS_ID; p0.swapChainAddress = SWAPCHAIN; + p0.presentStartTime = 100'000; + p0.timeInPresent = 10'000; + p0.readyTime = 110'000; p0.appSleepStartTime = 11'000; p0.appSleepEndTime = 21'000; p0.appSimStartTime = 70'000; p0.pclSimStartTime = 72'000; p0.gpuStartTime = 90'000; p0.finalState = PresentResult::Presented; - p0.displayed.Clear(); p0.displayed.PushBack({ FrameType::Repeated, 120'000 }); - auto p0_phase1 = ComputeMetricsForPresent(qpc, p0, nullptr, chain); - Assert::AreEqual(size_t(0), p0_phase1.size()); + auto heldRepeated = swapChain.ProcessPresent(qpc, std::move(p0)); + Assert::AreEqual(size_t(0), heldRepeated.size()); FrameData p1{}; p1.processId = PROCESS_ID; p1.swapChainAddress = SWAPCHAIN; + p1.presentStartTime = 130'000; + p1.timeInPresent = 5'000; + p1.readyTime = 140'000; p1.finalState = PresentResult::Presented; + p1.appSimStartTime = 80'000; p1.displayed.PushBack({ FrameType::Application, 150'000 }); - auto p0_final = ComputeMetricsForPresent(qpc, p0, &p1, chain); - Assert::AreEqual(size_t(1), p0_final.size()); - const auto& m0 = p0_final[0].metrics; + auto publishedRows = swapChain.ProcessPresent(qpc, std::move(p1)); + Assert::AreEqual(size_t(1), publishedRows.size()); + Assert::AreEqual((int)FrameType::Repeated, (int)publishedRows[0].computed.metrics.frameType); + const auto& m0 = publishedRows[0].computed.metrics; Assert::IsFalse(HasMetricValue(m0.msInstrumentedSleep), L"Non-app displays must not emit instrumented CPU metrics."); Assert::IsFalse(HasMetricValue(m0.msInstrumentedGpuLatency)); Assert::IsFalse(HasMetricValue(m0.msBetweenSimStarts)); - - auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, chain); - Assert::AreEqual(size_t(0), p1_phase1.size()); } TEST_METHOD(InstrumentedDisplay_AppFrame_NoRenderSubmit_RenderLatencyOff) { - // Scenario: - // - Displayed Application frame missing appRenderSubmitStartTime but with readyTime + sleep end. - // - // QPC frequency: 10 MHz. - // P0 fields: readyTime = 80'000, appSleepEndTime = 50'000, no render submit, screenTime = 100'000. - // Derived deltas: ready→display Δ = 20'000 ticks, total latency Δ = 50'000 ticks. - // - // Call pattern: Case 2/3 (P0 pending, finalized by P1, then P1 pending). - // - // Expectations: msInstrumentedRenderLatency = nullopt, while msReadyTimeToDisplayLatency and - // msInstrumentedLatency match the deltas above. - QpcConverter qpc(10'000'000, 0); SwapChainCoreState chain{}; @@ -9425,52 +6935,61 @@ TEST_CLASS(ComputeMetricsForPresentTests) p0.finalState = PresentResult::Presented; p0.displayed.PushBack({ FrameType::Application, 100'000 }); - auto p0_phase1 = ComputeMetricsForPresent(qpc, p0, nullptr, chain); - Assert::AreEqual(size_t(0), p0_phase1.size()); + auto p0_results = ComputeMetricsForPresent(qpc, p0, chain); + Assert::AreEqual(size_t(1), p0_results.size()); + const auto& m0 = p0_results[0].metrics; + + double expectedReadyMs = qpc.DeltaUnsignedMilliSeconds(80'000, 100'000); + double expectedTotalMs = qpc.DeltaUnsignedMilliSeconds(50'000, 100'000); + + Assert::IsFalse(HasMetricValue(m0.msInstrumentedRenderLatency)); + Assert::IsTrue(HasMetricValue(m0.msReadyTimeToDisplayLatency)); + AssertAreEqualWithinTolerance(expectedReadyMs, m0.msReadyTimeToDisplayLatency, 1e-6); + Assert::IsTrue(HasMetricValue(m0.msInstrumentedLatency)); + AssertAreEqualWithinTolerance(expectedTotalMs, m0.msInstrumentedLatency, 1e-6); + } + + TEST_METHOD(InstrumentedDisplay_AppFrame_NoRenderSubmit_RenderLatencyOff_ProcessPresent) + { + QpcConverter qpc(10'000'000, 0); + UnifiedSwapChain swapChain{}; + + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; + + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); + + FrameData p0{}; + p0.readyTime = 80'000; + p0.appSleepEndTime = 50'000; + p0.finalState = PresentResult::Presented; + p0.displayed.PushBack({ FrameType::Application, 100'000 }); + + Assert::AreEqual(size_t(0), swapChain.ProcessPresent(qpc, std::move(p0)).size()); FrameData p1{}; p1.finalState = PresentResult::Presented; p1.displayed.PushBack({ FrameType::Application, 130'000 }); - auto p0_final = ComputeMetricsForPresent(qpc, p0, &p1, chain); - Assert::AreEqual(size_t(1), p0_final.size()); - const auto& m0 = p0_final[0].metrics; + auto rows = swapChain.ProcessPresent(qpc, std::move(p1)); + Assert::AreEqual(size_t(1), rows.size()); + const auto& m0 = rows[0].computed.metrics; double expectedReadyMs = qpc.DeltaUnsignedMilliSeconds(80'000, 100'000); double expectedTotalMs = qpc.DeltaUnsignedMilliSeconds(50'000, 100'000); - Assert::IsFalse(HasMetricValue(m0.msInstrumentedRenderLatency), - L"Render latency must remain off without appRenderSubmitStartTime."); + Assert::IsFalse(HasMetricValue(m0.msInstrumentedRenderLatency)); Assert::IsTrue(HasMetricValue(m0.msReadyTimeToDisplayLatency)); AssertAreEqualWithinTolerance(expectedReadyMs, m0.msReadyTimeToDisplayLatency, 1e-6); - Assert::IsTrue(HasMetricValue(m0.msInstrumentedLatency)); AssertAreEqualWithinTolerance(expectedTotalMs, m0.msInstrumentedLatency, 1e-6); - - auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, chain); - Assert::AreEqual(size_t(0), p1_phase1.size()); } TEST_METHOD(InstrumentedDisplay_AppFrame_NoSleep_UsesAppSimStart) { - // Scenario: - // - Displayed Application frame lacks appSleepEndTime but provides appSimStartTime. - // - // QPC timeline (10 MHz): - // P0.appRenderSubmitStartTime = 10'000 - // P0.appSimStartTime = 5'000 - // P0.readyTime = 30'000 - // P0.screenTime = 60'000 - // Derived deltas: - // - Render latency: 50'000 ticks - // - Ready→display: 30'000 ticks - // - Total instrumented latency (AppSim→screen): 55'000 ticks - // - // Call pattern: Case 2/3. - // - // Expectations: render + ready latencies computed normally; msInstrumentedLatency should fall back - // to AppSimStartTime since sleep end is missing. - QpcConverter qpc(10'000'000, 0); SwapChainCoreState chain{}; @@ -9482,16 +7001,51 @@ TEST_CLASS(ComputeMetricsForPresentTests) p0.finalState = PresentResult::Presented; p0.displayed.PushBack({ FrameType::Application, 60'000 }); - auto p0_phase1 = ComputeMetricsForPresent(qpc, p0, nullptr, chain); - Assert::AreEqual(size_t(0), p0_phase1.size()); + auto p0_results = ComputeMetricsForPresent(qpc, p0, chain); + Assert::AreEqual(size_t(1), p0_results.size()); + const auto& m0 = p0_results[0].metrics; + + double expectedRenderMs = qpc.DeltaUnsignedMilliSeconds(10'000, 60'000); + double expectedReadyMs = qpc.DeltaUnsignedMilliSeconds(30'000, 60'000); + double expectedTotalMs = qpc.DeltaUnsignedMilliSeconds(5'000, 60'000); + + Assert::IsTrue(HasMetricValue(m0.msInstrumentedRenderLatency)); + AssertAreEqualWithinTolerance(expectedRenderMs, m0.msInstrumentedRenderLatency, 1e-6); + Assert::IsTrue(HasMetricValue(m0.msReadyTimeToDisplayLatency)); + AssertAreEqualWithinTolerance(expectedReadyMs, m0.msReadyTimeToDisplayLatency, 1e-6); + Assert::IsTrue(HasMetricValue(m0.msInstrumentedLatency)); + AssertAreEqualWithinTolerance(expectedTotalMs, m0.msInstrumentedLatency, 1e-6); + } + + TEST_METHOD(InstrumentedDisplay_AppFrame_NoSleep_UsesAppSimStart_ProcessPresent) + { + QpcConverter qpc(10'000'000, 0); + UnifiedSwapChain swapChain{}; + + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; + + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); + + FrameData p0{}; + p0.appRenderSubmitStartTime = 10'000; + p0.appSimStartTime = 5'000; + p0.readyTime = 30'000; + p0.finalState = PresentResult::Presented; + p0.displayed.PushBack({ FrameType::Application, 60'000 }); + + Assert::AreEqual(size_t(0), swapChain.ProcessPresent(qpc, std::move(p0)).size()); FrameData p1{}; p1.finalState = PresentResult::Presented; p1.displayed.PushBack({ FrameType::Application, 90'000 }); - auto p0_final = ComputeMetricsForPresent(qpc, p0, &p1, chain); - Assert::AreEqual(size_t(1), p0_final.size()); - const auto& m0 = p0_final[0].metrics; + auto rows = swapChain.ProcessPresent(qpc, std::move(p1)); + Assert::AreEqual(size_t(1), rows.size()); + const auto& m0 = rows[0].computed.metrics; double expectedRenderMs = qpc.DeltaUnsignedMilliSeconds(10'000, 60'000); double expectedReadyMs = qpc.DeltaUnsignedMilliSeconds(30'000, 60'000); @@ -9502,28 +7056,11 @@ TEST_CLASS(ComputeMetricsForPresentTests) Assert::IsTrue(HasMetricValue(m0.msReadyTimeToDisplayLatency)); AssertAreEqualWithinTolerance(expectedReadyMs, m0.msReadyTimeToDisplayLatency, 1e-6); Assert::IsTrue(HasMetricValue(m0.msInstrumentedLatency)); - AssertAreEqualWithinTolerance(expectedTotalMs, m0.msInstrumentedLatency, 1e-6, - L"Total latency should fall back to AppSimStartTime when sleep end is missing."); - - auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, chain); - Assert::AreEqual(size_t(0), p1_phase1.size()); + AssertAreEqualWithinTolerance(expectedTotalMs, m0.msInstrumentedLatency, 1e-6); } TEST_METHOD(InstrumentedDisplay_AppFrame_NoSleepNoSim_NoTotalLatency) { - // Scenario: - // - Displayed Application frame has render submit + ready markers but neither appSleepEndTime - // nor appSimStartTime, so the total instrumented latency should be disabled. - // - // QPC values (10 MHz): appRenderSubmitStartTime = 12'000, readyTime = 32'000, screenTime = 70'000. - // Derived deltas: - // - Render latency Δ = 58'000 ticks - // - Ready→display Δ = 38'000 ticks - // - // Call pattern: Case 2/3. - // - // Expectations: render + ready metrics populated with the deltas above; msInstrumentedLatency is nullopt. - QpcConverter qpc(10'000'000, 0); SwapChainCoreState chain{}; @@ -9533,16 +7070,48 @@ TEST_CLASS(ComputeMetricsForPresentTests) p0.finalState = PresentResult::Presented; p0.displayed.PushBack({ FrameType::Application, 70'000 }); - auto p0_phase1 = ComputeMetricsForPresent(qpc, p0, nullptr, chain); - Assert::AreEqual(size_t(0), p0_phase1.size()); + auto p0_results = ComputeMetricsForPresent(qpc, p0, chain); + Assert::AreEqual(size_t(1), p0_results.size()); + const auto& m0 = p0_results[0].metrics; + + double expectedRenderMs = qpc.DeltaUnsignedMilliSeconds(12'000, 70'000); + double expectedReadyMs = qpc.DeltaUnsignedMilliSeconds(32'000, 70'000); + + Assert::IsTrue(HasMetricValue(m0.msInstrumentedRenderLatency)); + AssertAreEqualWithinTolerance(expectedRenderMs, m0.msInstrumentedRenderLatency, 1e-6); + Assert::IsTrue(HasMetricValue(m0.msReadyTimeToDisplayLatency)); + AssertAreEqualWithinTolerance(expectedReadyMs, m0.msReadyTimeToDisplayLatency, 1e-6); + Assert::IsFalse(HasMetricValue(m0.msInstrumentedLatency)); + } + + TEST_METHOD(InstrumentedDisplay_AppFrame_NoSleepNoSim_NoTotalLatency_ProcessPresent) + { + QpcConverter qpc(10'000'000, 0); + UnifiedSwapChain swapChain{}; + + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; + + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); + + FrameData p0{}; + p0.appRenderSubmitStartTime = 12'000; + p0.readyTime = 32'000; + p0.finalState = PresentResult::Presented; + p0.displayed.PushBack({ FrameType::Application, 70'000 }); + + Assert::AreEqual(size_t(0), swapChain.ProcessPresent(qpc, std::move(p0)).size()); FrameData p1{}; p1.finalState = PresentResult::Presented; p1.displayed.PushBack({ FrameType::Application, 90'000 }); - auto p0_final = ComputeMetricsForPresent(qpc, p0, &p1, chain); - Assert::AreEqual(size_t(1), p0_final.size()); - const auto& m0 = p0_final[0].metrics; + auto rows = swapChain.ProcessPresent(qpc, std::move(p1)); + Assert::AreEqual(size_t(1), rows.size()); + const auto& m0 = rows[0].computed.metrics; double expectedRenderMs = qpc.DeltaUnsignedMilliSeconds(12'000, 70'000); double expectedReadyMs = qpc.DeltaUnsignedMilliSeconds(32'000, 70'000); @@ -9551,52 +7120,69 @@ TEST_CLASS(ComputeMetricsForPresentTests) AssertAreEqualWithinTolerance(expectedRenderMs, m0.msInstrumentedRenderLatency, 1e-6); Assert::IsTrue(HasMetricValue(m0.msReadyTimeToDisplayLatency)); AssertAreEqualWithinTolerance(expectedReadyMs, m0.msReadyTimeToDisplayLatency, 1e-6); - Assert::IsFalse(HasMetricValue(m0.msInstrumentedLatency), - L"Total instrumented latency must stay off without an instrumented start."); - - auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, chain); - Assert::AreEqual(size_t(0), p1_phase1.size()); + Assert::IsFalse(HasMetricValue(m0.msInstrumentedLatency)); } - TEST_METHOD(InstrumentedDisplay_NonAppFrame_Ignored) + TEST_METHOD(InstrumentedDisplay_V1_FirstDisplayNonAppFrame_Ignored) { - // Scenario: - // - Displayed frame whose first (and only) display entry is FrameType::Repeated, so the - // DisplayIndexing logic never flags an application frame for this present. - // - // QPC values (10 MHz): appRenderSubmitStartTime = 10'000, readyTime = 30'000, appSleepEndTime = 5'000, - // screenTime = 60'000. These deltas should all be ignored. - // - // Call pattern: Case 2/3. - // - // Expectations: all instrumented display metrics remain std::nullopt for P0. - - QpcConverter qpc(10'000'000, 0); + QpcConverter qpc(10'000'000, 0); SwapChainCoreState chain{}; FrameData p0{}; + p0.readyTime = 30'000; p0.appRenderSubmitStartTime = 10'000; + p0.appSleepEndTime = 5'000; + p0.finalState = PresentResult::Presented; + p0.displayed.PushBack({ FrameType::Repeated, 60'000 }); + + auto p0_results = ComputeMetricsForPresent(qpc, p0, chain); + Assert::AreEqual(size_t(1), p0_results.size()); + + const auto& m0 = p0_results[0].metrics; + Assert::IsFalse(HasMetricValue(m0.msInstrumentedRenderLatency)); + Assert::IsFalse(HasMetricValue(m0.msInstrumentedLatency)); + } + + TEST_METHOD(InstrumentedDisplay_NonAppFrame_Ignored_ProcessPresent) + { + QpcConverter qpc(10'000'000, 0); + UnifiedSwapChain swapChain{}; + + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; + + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); + + FrameData p0{}; + p0.presentStartTime = 50'000; + p0.timeInPresent = 8'000; p0.readyTime = 30'000; + p0.appRenderSubmitStartTime = 10'000; p0.appSleepEndTime = 5'000; p0.finalState = PresentResult::Presented; p0.displayed.PushBack({ FrameType::Repeated, 60'000 }); - auto p0_phase1 = ComputeMetricsForPresent(qpc, p0, nullptr, chain); - Assert::AreEqual(size_t(0), p0_phase1.size()); + auto heldRepeated = swapChain.ProcessPresent(qpc, std::move(p0)); + Assert::AreEqual(size_t(0), heldRepeated.size()); FrameData p1{}; + p1.presentStartTime = 70'000; + p1.timeInPresent = 5'000; + p1.readyTime = 80'000; p1.finalState = PresentResult::Presented; + p1.appSimStartTime = 40'000; p1.displayed.PushBack({ FrameType::Application, 90'000 }); - auto p0_final = ComputeMetricsForPresent(qpc, p0, &p1, chain); - Assert::AreEqual(size_t(1), p0_final.size()); - const auto& m0 = p0_final[0].metrics; + auto publishedRows = swapChain.ProcessPresent(qpc, std::move(p1)); + Assert::AreEqual(size_t(1), publishedRows.size()); + Assert::AreEqual((int)FrameType::Repeated, (int)publishedRows[0].computed.metrics.frameType); + const auto& m0 = publishedRows[0].computed.metrics; Assert::IsFalse(HasMetricValue(m0.msInstrumentedRenderLatency)); Assert::IsFalse(HasMetricValue(m0.msInstrumentedLatency)); - - auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, chain); - Assert::AreEqual(size_t(0), p1_phase1.size()); } TEST_METHOD(InstrumentedDisplay_AppFrame_NotDisplayed_NoDisplayMetrics) @@ -9608,7 +7194,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) // QPC values: appRenderSubmitStart = 9'000, readyTime = 19'000, appSleepEnd = 4'000, // appSimStart = 2'000. No displayed entries, so screenTime is undefined. // - // Call pattern: Case 1 (single call, nextDisplayed == nullptr). + // Call pattern: Case 1 (single call). // // Expectations: msInstrumentedRenderLatency / msReadyTimeToDisplayLatency / msInstrumentedLatency are nullopt. @@ -9622,7 +7208,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) p0.appSimStartTime = 2'000; p0.finalState = PresentResult::Discarded; - auto p0_results = ComputeMetricsForPresent(qpc, p0, nullptr, chain); + auto p0_results = ComputeMetricsForPresent(qpc, p0, chain); Assert::AreEqual(size_t(1), p0_results.size()); const auto& m0 = p0_results[0].metrics; @@ -9633,86 +7219,83 @@ TEST_CLASS(ComputeMetricsForPresentTests) TEST_METHOD(InstrumentedInput_DroppedAppFrame_PendingProviderInput_ConsumedOnDisplay) { - // Scenario: - // - P0 is a dropped Application frame that carries an App provider input sample at 20'000 ticks. - // - P1 is the next displayed Application frame (screenTime = 70'000) without its own sample. - // - P2 is a synthetic follower to flush P1, mirroring the ReportMetrics Case 2/3 pattern. - // - // QPC-derived delta: 70'000 - 20'000 = 50'000 ticks = 5 ms (with 10 MHz QPC). - // - // Call pattern: - // - P0 (Case 1) → Compute(..., nullptr) populates lastReceivedNotDisplayedAppProviderInputTime. - // - P1 pending → Compute(P1, nullptr). - // - P1 final → Compute(P1, &P2) produces metrics. - // - // Expectations: - // - Cached provider input equals 20'000 after P0. - // - P1 reports msInstrumentedInputTime = 5 ms and clears all pending caches afterward. - QpcConverter qpc(10'000'000, 0); SwapChainCoreState chain{}; const uint64_t pendingInputTime = 20'000; - // P0: Dropped Application frame with provider input. FrameData p0{}; p0.appInputSample = { pendingInputTime, InputDeviceType::Mouse }; p0.finalState = PresentResult::Discarded; - auto p0_results = ComputeMetricsForPresent(qpc, p0, nullptr, chain); + auto p0_results = ComputeMetricsForPresent(qpc, p0, chain); Assert::AreEqual(size_t(1), p0_results.size()); - Assert::IsTrue(IsMissingFrameMetricValue(p0_results[0].metrics.msInstrumentedInputTime), - L"Dropped provider input should remain missing until a displayed frame consumes it."); - Assert::AreEqual(pendingInputTime, chain.lastReceivedNotDisplayedAppProviderInputTime, - L"Dropped provider input should be cached until a displayed frame consumes it."); + Assert::IsTrue(IsMissingFrameMetricValue(p0_results[0].metrics.msInstrumentedInputTime)); + Assert::AreEqual(pendingInputTime, chain.lastReceivedNotDisplayedAppProviderInputTime); + + FrameData p1{}; + p1.finalState = PresentResult::Presented; + p1.displayed.PushBack({ FrameType::Application, 70'000 }); + + auto p1_results = ComputeMetricsForPresent(qpc, p1, chain); + Assert::AreEqual(size_t(1), p1_results.size()); + const auto& m1 = p1_results[0].metrics; + + Assert::IsTrue(HasMetricValue(m1.msInstrumentedInputTime)); + double expectedInputMs = qpc.DeltaUnsignedMilliSeconds(pendingInputTime, 70'000); + AssertAreEqualWithinTolerance(expectedInputMs, m1.msInstrumentedInputTime, 1e-6); + + Assert::AreEqual(uint64_t(0), chain.lastReceivedNotDisplayedAppProviderInputTime); + Assert::AreEqual(uint64_t(0), chain.lastReceivedNotDisplayedAllInputTime); + Assert::AreEqual(uint64_t(0), chain.lastReceivedNotDisplayedMouseClickTime); + } + + TEST_METHOD(InstrumentedInput_DroppedAppFrame_PendingProviderInput_ConsumedOnDisplay_ProcessPresent) + { + QpcConverter qpc(10'000'000, 0); + UnifiedSwapChain swapChain{}; + + const uint64_t pendingInputTime = 20'000; + + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; + + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); + + FrameData p0{}; + p0.appInputSample = { pendingInputTime, InputDeviceType::Mouse }; + p0.finalState = PresentResult::Discarded; + + auto p0_rows = swapChain.ProcessPresent(qpc, std::move(p0)); + Assert::AreEqual(size_t(1), p0_rows.size()); + Assert::AreEqual(pendingInputTime, swapChain.swapChain.lastReceivedNotDisplayedAppProviderInputTime); - // P1: Displayed Application frame without its own AppInputSample. FrameData p1{}; p1.finalState = PresentResult::Presented; p1.displayed.PushBack({ FrameType::Application, 70'000 }); - auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, chain); - Assert::AreEqual(size_t(0), p1_phase1.size()); + Assert::AreEqual(size_t(0), swapChain.ProcessPresent(qpc, std::move(p1)).size()); - // P2: Simple next displayed frame to flush P1. FrameData p2{}; p2.finalState = PresentResult::Presented; p2.displayed.PushBack({ FrameType::Application, 90'000 }); - auto p1_final = ComputeMetricsForPresent(qpc, p1, &p2, chain); - Assert::AreEqual(size_t(1), p1_final.size()); - const auto& m1 = p1_final[0].metrics; + auto p2_rows = swapChain.ProcessPresent(qpc, std::move(p2)); + Assert::AreEqual(size_t(1), p2_rows.size()); + Assert::AreEqual(uint64_t(70'000), p2_rows[0].computed.metrics.screenTimeQpc); - Assert::IsTrue(HasMetricValue(m1.msInstrumentedInputTime), - L"P1 should consume the cached provider input time once it is displayed."); + const auto& m1 = p2_rows[0].computed.metrics; + Assert::IsTrue(HasMetricValue(m1.msInstrumentedInputTime)); double expectedInputMs = qpc.DeltaUnsignedMilliSeconds(pendingInputTime, 70'000); AssertAreEqualWithinTolerance(expectedInputMs, m1.msInstrumentedInputTime, 1e-6); - - Assert::AreEqual(uint64_t(0), chain.lastReceivedNotDisplayedAppProviderInputTime, - L"Pending provider input cache must be cleared after consumption."); - Assert::AreEqual(uint64_t(0), chain.lastReceivedNotDisplayedAllInputTime); - Assert::AreEqual(uint64_t(0), chain.lastReceivedNotDisplayedMouseClickTime); - - auto p2_phase1 = ComputeMetricsForPresent(qpc, p2, nullptr, chain); - Assert::AreEqual(size_t(0), p2_phase1.size()); + Assert::AreEqual(uint64_t(0), swapChain.swapChain.lastReceivedNotDisplayedAppProviderInputTime); } TEST_METHOD(InstrumentedInput_DisplayedAppFrame_WithOwnSample_IgnoresPending) { - // Scenario: - // - P0 (dropped) seeds pending provider input at 10'000 ticks. - // - P1 (displayed Application) carries its own sample at 15'000 ticks and displays at 60'000. - // - P2 finalizes P1. - // - // QPC-derived deltas: - // - Pending path would have produced 50'000 ticks, but we expect 45'000 ticks from P1's own sample. - // - // Call pattern: identical to Test 10 (Case 1 for P0, Case 2/3 for P1). - // - // Expectations: - // - Cached pending input updated after P0. - // - P1 final metrics use Δ(15'000, 60'000) only and clear the pending cache. - QpcConverter qpc(10'000'000, 0); SwapChainCoreState chain{}; @@ -9723,7 +7306,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) p0.appInputSample = { pendingInputTime, InputDeviceType::Keyboard }; p0.finalState = PresentResult::Discarded; - auto p0_results = ComputeMetricsForPresent(qpc, p0, nullptr, chain); + auto p0_results = ComputeMetricsForPresent(qpc, p0, chain); Assert::AreEqual(size_t(1), p0_results.size()); Assert::AreEqual(pendingInputTime, chain.lastReceivedNotDisplayedAppProviderInputTime); @@ -9732,82 +7315,142 @@ TEST_CLASS(ComputeMetricsForPresentTests) p1.finalState = PresentResult::Presented; p1.displayed.PushBack({ FrameType::Application, 60'000 }); - auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, chain); - Assert::AreEqual(size_t(0), p1_phase1.size()); + auto p1_results = ComputeMetricsForPresent(qpc, p1, chain); + Assert::AreEqual(size_t(1), p1_results.size()); + const auto& m1 = p1_results[0].metrics; + + double expectedInputMs = qpc.DeltaUnsignedMilliSeconds(directInputTime, 60'000); + Assert::IsTrue(HasMetricValue(m1.msInstrumentedInputTime)); + AssertAreEqualWithinTolerance(expectedInputMs, m1.msInstrumentedInputTime, 1e-6); + Assert::AreEqual(uint64_t(0), chain.lastReceivedNotDisplayedAppProviderInputTime); + } + + TEST_METHOD(InstrumentedInput_DisplayedAppFrame_WithOwnSample_IgnoresPending_ProcessPresent) + { + QpcConverter qpc(10'000'000, 0); + UnifiedSwapChain swapChain{}; + + const uint64_t pendingInputTime = 10'000; + const uint64_t directInputTime = 15'000; + + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; + + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); + + FrameData p0{}; + p0.appInputSample = { pendingInputTime, InputDeviceType::Keyboard }; + p0.finalState = PresentResult::Discarded; + + (void)swapChain.ProcessPresent(qpc, std::move(p0)); + Assert::AreEqual(pendingInputTime, swapChain.swapChain.lastReceivedNotDisplayedAppProviderInputTime); + + FrameData p1{}; + p1.appInputSample = { directInputTime, InputDeviceType::Mouse }; + p1.finalState = PresentResult::Presented; + p1.displayed.PushBack({ FrameType::Application, 60'000 }); + + Assert::AreEqual(size_t(0), swapChain.ProcessPresent(qpc, std::move(p1)).size()); FrameData p2{}; p2.finalState = PresentResult::Presented; p2.displayed.PushBack({ FrameType::Application, 80'000 }); - auto p1_final = ComputeMetricsForPresent(qpc, p1, &p2, chain); - Assert::AreEqual(size_t(1), p1_final.size()); - const auto& m1 = p1_final[0].metrics; + auto p2_rows = swapChain.ProcessPresent(qpc, std::move(p2)); + Assert::AreEqual(size_t(1), p2_rows.size()); + Assert::AreEqual(uint64_t(60'000), p2_rows[0].computed.metrics.screenTimeQpc); + const auto& m1 = p2_rows[0].computed.metrics; double expectedInputMs = qpc.DeltaUnsignedMilliSeconds(directInputTime, 60'000); Assert::IsTrue(HasMetricValue(m1.msInstrumentedInputTime)); - AssertAreEqualWithinTolerance(expectedInputMs, m1.msInstrumentedInputTime, 1e-6, - L"P1 must prefer its own input marker over pending values."); + AssertAreEqualWithinTolerance(expectedInputMs, m1.msInstrumentedInputTime, 1e-6); + Assert::AreEqual(uint64_t(0), swapChain.swapChain.lastReceivedNotDisplayedAppProviderInputTime); + } + + TEST_METHOD(InstrumentedInput_V1_FirstDisplayNonAppFrame_DoesNotAffectInstrumentedInputTime) + { + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + const uint64_t ignoredInputTime = 25'000; + FrameData p0{}; + p0.appInputSample = { ignoredInputTime, InputDeviceType::Mouse }; + p0.finalState = PresentResult::Presented; + p0.displayed.PushBack({ FrameType::Repeated, 50'000 }); + + auto p0_results = ComputeMetricsForPresent(qpc, p0, chain); + Assert::AreEqual(size_t(1), p0_results.size()); Assert::AreEqual(uint64_t(0), chain.lastReceivedNotDisplayedAppProviderInputTime); - auto p2_phase1 = ComputeMetricsForPresent(qpc, p2, nullptr, chain); - Assert::AreEqual(size_t(0), p2_phase1.size()); + FrameData p1{}; + p1.finalState = PresentResult::Presented; + p1.displayed.PushBack({ FrameType::Application, 80'000 }); + + auto p1_results = ComputeMetricsForPresent(qpc, p1, chain); + Assert::AreEqual(size_t(1), p1_results.size()); + Assert::IsTrue(IsMissingFrameMetricValue(p1_results[0].metrics.msInstrumentedInputTime)); } - TEST_METHOD(InstrumentedInput_NonAppFrame_DoesNotAffectInstrumentedInputTime) + TEST_METHOD(InstrumentedInput_NonAppFrame_DoesNotAffectInstrumentedInputTime_ProcessPresent) { - // Scenario: - // - P0 is a displayed frame with FrameType::Repeated at screenTime = 50'000 and a provider - // input sample at 25'000 ticks. Because it is not an Application frame, it must NOT seed the - // pending provider cache. - // - P1 is the next displayed Application frame (screenTime = 80'000) without its own sample. - // - P2 finalizes P1. - // - // QPC expectation: since no pending sample exists, msInstrumentedInputTime for P1 must be missing (NaN). - // - // Call pattern: Case 2/3 for both P0 and P1 (since both are displayed frames). - // - // Expectations: - // - After P0 finalization, chain.lastReceivedNotDisplayedAppProviderInputTime remains 0. - // - P1 final metrics leave msInstrumentedInputTime unset. - - QpcConverter qpc(10'000'000, 0); - SwapChainCoreState chain{}; + QpcConverter qpc(10'000'000, 0); + UnifiedSwapChain swapChain{}; const uint64_t ignoredInputTime = 25'000; + FrameData bootstrap{}; + bootstrap.presentStartTime = 1; + bootstrap.timeInPresent = 1; + bootstrap.readyTime = 1; + bootstrap.finalState = PresentResult::Presented; + + (void)swapChain.ProcessPresent(qpc, std::move(bootstrap)); + FrameData p0{}; + p0.presentStartTime = 40'000; + p0.timeInPresent = 5'000; + p0.readyTime = 45'000; p0.appInputSample = { ignoredInputTime, InputDeviceType::Mouse }; p0.finalState = PresentResult::Presented; p0.displayed.PushBack({ FrameType::Repeated, 50'000 }); - auto p0_phase1 = ComputeMetricsForPresent(qpc, p0, nullptr, chain); - Assert::AreEqual(size_t(0), p0_phase1.size()); + auto heldRepeated = swapChain.ProcessPresent(qpc, std::move(p0)); + Assert::AreEqual(size_t(0), heldRepeated.size()); FrameData p1{}; + p1.presentStartTime = 60'000; + p1.timeInPresent = 5'000; + p1.readyTime = 70'000; p1.finalState = PresentResult::Presented; + p1.appSimStartTime = 55'000; p1.displayed.PushBack({ FrameType::Application, 80'000 }); - auto p0_final = ComputeMetricsForPresent(qpc, p0, &p1, chain); - Assert::AreEqual(size_t(1), p0_final.size()); - Assert::AreEqual(uint64_t(0), chain.lastReceivedNotDisplayedAppProviderInputTime, + auto repeatedRows = swapChain.ProcessPresent(qpc, std::move(p1)); + Assert::AreEqual(size_t(1), repeatedRows.size()); + Assert::AreEqual((int)FrameType::Repeated, (int)repeatedRows[0].computed.metrics.frameType); + Assert::AreEqual(uint64_t(0), swapChain.swapChain.lastReceivedNotDisplayedAppProviderInputTime, L"Non-app frames should not seed the pending provider input cache."); - auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, chain); - Assert::AreEqual(size_t(0), p1_phase1.size()); - FrameData p2{}; + p2.presentStartTime = 85'000; + p2.timeInPresent = 5'000; + p2.readyTime = 95'000; p2.finalState = PresentResult::Presented; + p2.appSimStartTime = 75'000; p2.displayed.PushBack({ FrameType::Application, 100'000 }); - auto p1_final = ComputeMetricsForPresent(qpc, p1, &p2, chain); - Assert::AreEqual(size_t(1), p1_final.size()); - const auto& m1 = p1_final[0].metrics; + auto originRows = swapChain.ProcessPresent(qpc, std::move(p2)); + Assert::AreEqual(size_t(1), originRows.size()); + Assert::AreEqual((int)FrameType::Application, (int)originRows[0].computed.metrics.frameType); + Assert::AreEqual(uint64_t(80'000), originRows[0].computed.metrics.screenTimeQpc); + + const auto& m1 = originRows[0].computed.metrics; Assert::IsTrue(IsMissingFrameMetricValue(m1.msInstrumentedInputTime), L"P1 should report missing instrumented input latency as NaN when no app-frame sample exists."); - - auto p2_phase1 = ComputeMetricsForPresent(qpc, p2, nullptr, chain); - Assert::AreEqual(size_t(0), p2_phase1.size()); } }; } diff --git a/IntelPresentMon/UnitTests/SwapChainTests.cpp b/IntelPresentMon/UnitTests/SwapChainTests.cpp index 4eef8ab4..6ce74190 100644 --- a/IntelPresentMon/UnitTests/SwapChainTests.cpp +++ b/IntelPresentMon/UnitTests/SwapChainTests.cpp @@ -248,5 +248,26 @@ namespace MetricsCoreTests Assert::AreEqual(uint64_t(1234), swapChainOne.lastSimStartTime); //Assert::AreEqual(presents[1], *swapChainOne.lastPresent); } + + TEST_METHOD(UpdateAfterReadyDisplayRow_NotDisplayed_PreservesLastDisplayedState) + { + // Regression: the not-displayed branch was resetting lastDisplayedScreenTime + // and lastDisplayedFlipDelay to 0. A dropped present does not change what is + // currently on screen, so those fields must be preserved. + pmon::util::metrics::SwapChainCoreState chain{}; + chain.lastDisplayedScreenTime = 12345; + chain.lastDisplayedFlipDelay = 99; + + pmon::util::metrics::ReadyDisplayRow row{}; + row.isDisplayed = false; + row.isAppFrame = true; + + chain.UpdateAfterReadyDisplayRow(row); + + Assert::AreEqual(uint64_t(12345), chain.lastDisplayedScreenTime, + L"lastDisplayedScreenTime must not be cleared by a not-displayed row."); + Assert::AreEqual(uint64_t(99), chain.lastDisplayedFlipDelay, + L"lastDisplayedFlipDelay must not be cleared by a not-displayed row."); + } }; } diff --git a/PresentData/PresentMonTraceConsumer.cpp b/PresentData/PresentMonTraceConsumer.cpp index c49df7b2..0a05137e 100644 --- a/PresentData/PresentMonTraceConsumer.cpp +++ b/PresentData/PresentMonTraceConsumer.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2017-2026 Intel Corporation +// Copyright (C) 2017-2026 Intel Corporation // Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved // SPDX-License-Identifier: MIT diff --git a/PresentMon/OutputThread.cpp b/PresentMon/OutputThread.cpp index 2b61aef4..2cfdd645 100644 --- a/PresentMon/OutputThread.cpp +++ b/PresentMon/OutputThread.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2017-2024 Intel Corporation +// Copyright (C) 2017-2024 Intel Corporation // Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved // SPDX-License-Identifier: MIT @@ -352,16 +352,11 @@ static bool GetPresentProcessInfo( return true; } - auto chain = &processInfo->mSwapChain[presentEvent->SwapChainAddress]; - if (!chain->mUnifiedSwapChain.swapChain.lastPresent.has_value()) { - using namespace pmon::util::metrics; - chain->mUnifiedSwapChain.SeedFromFirstPresent(FrameData::CopyFrameData(presentEvent)); - return true; - } - *outProcessInfo = processInfo; + auto chain = &processInfo->mSwapChain[presentEvent->SwapChainAddress]; *outChain = chain; - *outPresentTime = chain->mUnifiedSwapChain.GetLastPresentQpc(); + *outPresentTime = chain->mUnifiedSwapChain.GetLastPresentQpcOr( + presentEvent->PresentStartTime); return false; } @@ -485,65 +480,61 @@ static void ProcessEvents( continue; } - auto ready = chain->mUnifiedSwapChain.Enqueue(pmon::util::metrics::FrameData::CopyFrameData(presentEvent), - args.mUseV1Metrics ? pmon::util::metrics::MetricsVersion::V1 : pmon::util::metrics::MetricsVersion::V2); - // Do we need to emit metrics for this present? const bool emit = (isRecording || computeAvg); - for (auto& it : ready) { - // Build FrameData copies for the unified calculator state-advance (and V2 metrics). - using namespace pmon::util::metrics; + using namespace pmon::util::metrics; - FrameData& frame = (it.presentPtr != nullptr) ? *it.presentPtr : it.present; - FrameData* nextPtr = it.nextDisplayedPtr; + const bool seedPresentOnly = !chain->mUnifiedSwapChain.swapChain.lastPresent.has_value(); - if (args.mUseV1Metrics) { - // V1: compute immediately (no look-ahead) and emit legacy V1 CSV. - auto computed = ComputeMetricsForPresent(qpc, frame, nullptr, chain->mUnifiedSwapChain.swapChain, MetricsVersion::V1); + if (args.mUseV1Metrics) { + FrameData frame = FrameData::CopyFrameData(presentEvent); + UnifiedSwapChain::SanitizeDisplayedRepeatedPresents(frame); + auto computed = ComputeMetricsForPresent(qpc, frame, chain->mUnifiedSwapChain.swapChain); - if (emit) { - for (auto const& cm : computed) { - auto const m1 = ToFrameMetrics1(cm.metrics); + if (emit && !seedPresentOnly) { + for (auto const& cm : computed) { + auto const m1 = ToFrameMetrics1(cm.metrics); - if (isRecording) { - UpdateCsv(pmSession, processInfo, frame, m1); - } + if (isRecording) { + UpdateCsv(pmSession, processInfo, frame, m1); + } - if (computeAvg) { - UpdateAverage(&chain->mUnifiedSwapChain.avgCPUDuration, m1.msBetweenPresents); - UpdateAverage(&chain->mUnifiedSwapChain.avgGPUDuration, m1.msGPUDuration); + if (computeAvg) { + UpdateAverage(&chain->mUnifiedSwapChain.avgCPUDuration, m1.msBetweenPresents); + UpdateAverage(&chain->mUnifiedSwapChain.avgGPUDuration, m1.msGPUDuration); - if (m1.msUntilDisplayed > 0) { - UpdateAverage(&chain->mUnifiedSwapChain.avgDisplayLatency, m1.msUntilDisplayed); - if (m1.msBetweenDisplayChange > 0) { - UpdateAverage(&chain->mUnifiedSwapChain.avgDisplayedTime, m1.msBetweenDisplayChange); - } + if (m1.msUntilDisplayed > 0) { + UpdateAverage(&chain->mUnifiedSwapChain.avgDisplayLatency, m1.msUntilDisplayed); + if (m1.msBetweenDisplayChange > 0) { + UpdateAverage(&chain->mUnifiedSwapChain.avgDisplayedTime, m1.msBetweenDisplayChange); } } } } } - else { - // V2 unified metrics: compute + advance together - auto computed = ComputeMetricsForPresent(qpc, frame, nextPtr, chain->mUnifiedSwapChain.swapChain, MetricsVersion::V2); + } + else { + auto processed = chain->mUnifiedSwapChain.ProcessPresent( + qpc, + FrameData::CopyFrameData(presentEvent)); - if (emit) { - for (auto const& cm : computed) { - auto const& m = cm.metrics; + for (auto const& row : processed) { + auto const& frame = row.present; + auto const& m = row.computed.metrics; - if (isRecording) { - UpdateCsv(pmSession, processInfo, frame, m); - } + if (emit) { + if (isRecording) { + UpdateCsv(pmSession, processInfo, frame, m); + } - if (computeAvg) { - UpdateAverage(&chain->mUnifiedSwapChain.avgCPUDuration, m.msCPUBusy + m.msCPUWait); - if (m.msUntilDisplayed > 0) { - UpdateAverage(&chain->mUnifiedSwapChain.avgDisplayLatency, m.msDisplayLatency); - UpdateAverage(&chain->mUnifiedSwapChain.avgDisplayedTime, m.msDisplayedTime); - UpdateAverage(&chain->mUnifiedSwapChain.avgMsUntilDisplayed, m.msUntilDisplayed); - UpdateAverage(&chain->mUnifiedSwapChain.avgMsBetweenDisplayChange, m.msBetweenDisplayChange); - } + if (computeAvg) { + UpdateAverage(&chain->mUnifiedSwapChain.avgCPUDuration, m.msCPUBusy + m.msCPUWait); + if (m.msUntilDisplayed > 0) { + UpdateAverage(&chain->mUnifiedSwapChain.avgDisplayLatency, m.msDisplayLatency); + UpdateAverage(&chain->mUnifiedSwapChain.avgDisplayedTime, m.msDisplayedTime); + UpdateAverage(&chain->mUnifiedSwapChain.avgMsUntilDisplayed, m.msUntilDisplayed); + UpdateAverage(&chain->mUnifiedSwapChain.avgMsBetweenDisplayChange, m.msBetweenDisplayChange); } } } @@ -667,4 +658,4 @@ void StopOutputThread() DeleteCriticalSection(&gRecordingToggleCS); } -} \ No newline at end of file +}