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 +}