diff --git a/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorAnimation.cpp b/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorAnimation.cpp index 1fcc19c4..d8f5518d 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,8 +10,53 @@ 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 ---- - std::optional ComputeAnimationError( + double ComputeAnimationError( const QpcConverter& qpc, const SwapChainCoreState& chain, const FrameData& present, @@ -20,30 +65,36 @@ namespace pmon::util::metrics uint64_t screenTime) { if (!isDisplayed || !isAppFrame) { - return std::nullopt; + return MissingFrameMetricValue(); } - uint64_t currentSimStart = CalculateAnimationErrorSimStartTime(chain, present, chain.animationErrorSource); + const auto sourceContext = ResolveAnimationSourceContext(chain, present); + + if (sourceContext.isTransitionFrame) { + return MissingFrameMetricValue(); + } - if (currentSimStart == 0 || + if (sourceContext.currentSimStart == 0 || chain.lastDisplayedSimStartTime == 0 || - currentSimStart <= chain.lastDisplayedSimStartTime || + sourceContext.currentSimStart <= chain.lastDisplayedSimStartTime || chain.lastDisplayedAppScreenTime == 0) { - return std::nullopt; + return MissingFrameMetricValue(); } - double simElapsed = qpc.DeltaUnsignedMilliSeconds(chain.lastDisplayedSimStartTime, currentSimStart); + double simElapsed = qpc.DeltaUnsignedMilliSeconds( + chain.lastDisplayedSimStartTime, + sourceContext.currentSimStart); double displayElapsed = qpc.DeltaUnsignedMilliSeconds(chain.lastDisplayedAppScreenTime, screenTime); if (simElapsed == 0.0 || displayElapsed == 0.0) { - return std::nullopt; + return MissingFrameMetricValue(); } return simElapsed - displayElapsed; } - std::optional ComputeAnimationTime( + double ComputeAnimationTime( const QpcConverter& qpc, const SwapChainCoreState& chain, const FrameData& present, @@ -51,25 +102,25 @@ namespace pmon::util::metrics bool isAppFrame) { if (!isDisplayed || !isAppFrame) { - return std::nullopt; + return MissingFrameMetricValue(); } - bool isFirstProviderSimTime = - chain.animationErrorSource == AnimationErrorSource::CpuStart && - chain.firstAppSimStartTime == 0 && - (present.appSimStartTime != 0 || present.pclSimStartTime != 0); - if (isFirstProviderSimTime) { - // Seed only: no animation time yet. UpdateAfterPresent will flip us - // into AppProvider/PCL and latch firstAppSimStartTime. + const auto sourceContext = ResolveAnimationSourceContext(chain, present); + + if (sourceContext.isTransitionFrame) { return 0.0; } - uint64_t currentSimStart = CalculateAnimationErrorSimStartTime(chain, present, chain.animationErrorSource); - if (currentSimStart == 0) { + if (sourceContext.effectiveSource != AnimationErrorSource::CpuStart && + sourceContext.currentSimStart == 0) { + return MissingFrameMetricValue(); + } + + if (sourceContext.currentSimStart == 0) { return 0.0; } - return CalculateAnimationTime(qpc, chain.firstAppSimStartTime, currentSimStart); + return CalculateAnimationTime(qpc, chain.firstAppSimStartTime, sourceContext.currentSimStart); } double ComputePresentStartTimeMs( diff --git a/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorCpuGpu.cpp b/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorCpuGpu.cpp index b73325a4..d411a262 100644 --- a/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorCpuGpu.cpp +++ b/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorCpuGpu.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" @@ -18,7 +18,7 @@ namespace pmon::util::metrics bool isAppPresent) { if (!isAppPresent) { - return 0.0; + return MissingFrameMetricValue(); } const auto cpuStart = CalculateCPUStart(swapChain, present); @@ -42,7 +42,7 @@ namespace pmon::util::metrics bool isAppPresent) { if (!isAppPresent) { - return 0.0; + return MissingFrameMetricValue(); } if (present.appPropagatedTimeInPresent != 0) { diff --git a/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorDisplay.cpp b/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorDisplay.cpp index 268a8282..95713c35 100644 --- a/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorDisplay.cpp +++ b/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorDisplay.cpp @@ -49,7 +49,7 @@ namespace pmon::util::metrics } - std::optional ComputeMsFlipDelay( + double ComputeMsFlipDelay( const QpcConverter& qpc, const FrameData& present, bool isDisplayed) @@ -57,7 +57,7 @@ namespace pmon::util::metrics if (isDisplayed && present.flipDelay != 0) { return qpc.DurationMilliSeconds(present.flipDelay); } - return std::nullopt; + return MissingFrameMetricValue(); } @@ -75,7 +75,7 @@ namespace pmon::util::metrics } - std::optional ComputeMsReadyTimeToDisplayLatency( + double ComputeMsReadyTimeToDisplayLatency( const QpcConverter& qpc, const FrameData& present, bool isDisplayed, @@ -84,7 +84,7 @@ namespace pmon::util::metrics if (isDisplayed && present.readyTime != 0) { return qpc.DeltaUnsignedMilliSeconds(present.readyTime, screenTime); } - return std::nullopt; + return MissingFrameMetricValue(); } diff --git a/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorInput.cpp b/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorInput.cpp index 63532729..6b56ce8b 100644 --- a/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorInput.cpp +++ b/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorInput.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" @@ -11,7 +11,7 @@ namespace pmon::util::metrics namespace { // ---- Input latency metrics ---- - std::optional ComputeClickToPhotonLatency( + double ComputeClickToPhotonLatency( const QpcConverter& qpc, const SwapChainCoreState& chain, const FrameData& present, @@ -22,7 +22,7 @@ namespace pmon::util::metrics { // Only app frames participate in click-to-photon. if (!isAppFrame) { - return std::nullopt; + return MissingFrameMetricValue(); } uint64_t inputTime = 0; @@ -34,7 +34,7 @@ namespace pmon::util::metrics if (!isDisplayed) { // Not displayed: stash the click for a future displayed frame. stateDeltas.lastReceivedNotDisplayedMouseClickTime = inputTime; - return std::nullopt; + return MissingFrameMetricValue(); } else { stateDeltas.shouldResetInputTimes = true; @@ -49,14 +49,14 @@ namespace pmon::util::metrics // If we still have no inputTime, nothing to compute. if (inputTime == 0) { - return std::nullopt; + return MissingFrameMetricValue(); } return qpc.DeltaUnsignedMilliSeconds(inputTime, screenTime); } - std::optional ComputeAllInputToPhotonLatency( + double ComputeAllInputToPhotonLatency( const QpcConverter& qpc, const SwapChainCoreState& chain, const FrameData& present, @@ -67,7 +67,7 @@ namespace pmon::util::metrics { // Only app frames participate in click-to-photon. if (!isAppFrame) { - return std::nullopt; + return MissingFrameMetricValue(); } uint64_t inputTime = 0; @@ -79,7 +79,7 @@ namespace pmon::util::metrics if (!isDisplayed) { // Not displayed: stash the click for a future displayed frame. stateDeltas.lastReceivedNotDisplayedAllInputTime = inputTime; - return std::nullopt; + return MissingFrameMetricValue(); } else { stateDeltas.shouldResetInputTimes = true; @@ -94,13 +94,13 @@ namespace pmon::util::metrics // If we still have no inputTime, nothing to compute. if (inputTime == 0) { - return std::nullopt; + return MissingFrameMetricValue(); } return qpc.DeltaUnsignedMilliSeconds(inputTime, screenTime); } - std::optional ComputeInstrumentedInputToPhotonLatency( + double ComputeInstrumentedInputToPhotonLatency( const QpcConverter& qpc, const SwapChainCoreState& chain, const FrameData& present, @@ -111,7 +111,7 @@ namespace pmon::util::metrics { // Only app frames participate in click-to-photon. if (!isAppFrame) { - return std::nullopt; + return MissingFrameMetricValue(); } uint64_t inputTime = 0; @@ -123,7 +123,7 @@ namespace pmon::util::metrics if (!isDisplayed) { // Not displayed: stash the click for a future displayed frame. stateDeltas.lastReceivedNotDisplayedAppProviderInputTime = inputTime; - return std::nullopt; + return MissingFrameMetricValue(); } else { stateDeltas.shouldResetInputTimes = true; @@ -138,7 +138,7 @@ namespace pmon::util::metrics // If we still have no inputTime, nothing to compute. if (inputTime == 0) { - return std::nullopt; + return MissingFrameMetricValue(); } return qpc.DeltaUnsignedMilliSeconds(inputTime, screenTime); diff --git a/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorInstrumented.cpp b/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorInstrumented.cpp index 2b891feb..e97df78f 100644 --- a/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorInstrumented.cpp +++ b/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorInstrumented.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" @@ -11,7 +11,7 @@ namespace pmon::util::metrics namespace { // ---- Instrumented metrics ---- - std::optional ComputeInstrumentedLatency( + double ComputeInstrumentedLatency( const QpcConverter& qpc, const FrameData& present, bool isDisplayed, @@ -19,7 +19,7 @@ namespace pmon::util::metrics uint64_t screenTime) { if (!isDisplayed || !isAppFrame) { - return std::nullopt; + return MissingFrameMetricValue(); } auto instrumentedStartTime = present.appSleepEndTime != 0 ? @@ -27,13 +27,13 @@ namespace pmon::util::metrics if (instrumentedStartTime == 0) { // No instrumented start time: nothing to compute. - return std::nullopt; + return MissingFrameMetricValue(); } return qpc.DeltaUnsignedMilliSeconds(instrumentedStartTime, screenTime); } - std::optional ComputeInstrumentedRenderLatency( + double ComputeInstrumentedRenderLatency( const QpcConverter& qpc, const FrameData& present, bool isDisplayed, @@ -41,41 +41,41 @@ namespace pmon::util::metrics uint64_t screenTime) { if (!isDisplayed || !isAppFrame) { - return std::nullopt; + return MissingFrameMetricValue(); } if (present.appRenderSubmitStartTime == 0) { // No app provided render submit start time: nothing to compute. - return std::nullopt; + return MissingFrameMetricValue(); } return qpc.DeltaUnsignedMilliSeconds(present.appRenderSubmitStartTime, screenTime); } - std::optional ComputeInstrumentedSleep( + double ComputeInstrumentedSleep( const QpcConverter& qpc, const FrameData& present, bool isAppFrame) { if (!isAppFrame) { - return std::nullopt; + return MissingFrameMetricValue(); } if (present.appSleepStartTime == 0 || present.appSleepEndTime == 0) { // No app provided sleep times: nothing to compute. - return std::nullopt; + return MissingFrameMetricValue(); } return qpc.DeltaUnsignedMilliSeconds(present.appSleepStartTime, present.appSleepEndTime); } - std::optional ComputeInstrumentedGpuLatency( + double ComputeInstrumentedGpuLatency( const QpcConverter& qpc, const FrameData& present, bool isAppFrame) { if (!isAppFrame) { - return std::nullopt; + return MissingFrameMetricValue(); } auto instrumentedStartTime = present.appSleepEndTime != 0 ? @@ -83,26 +83,26 @@ namespace pmon::util::metrics if (instrumentedStartTime == 0) { // No provider sleep end or sim start time: nothing to compute. - return std::nullopt; + return MissingFrameMetricValue(); } if (present.gpuStartTime == 0) { // No GPU start time: nothing to compute. - return std::nullopt; + return MissingFrameMetricValue(); } return qpc.DeltaUnsignedMilliSeconds(instrumentedStartTime, present.gpuStartTime); } // ---- Simulation metrics ---- - std::optional ComputeMsBetweenSimulationStarts( + double ComputeMsBetweenSimulationStarts( const QpcConverter& qpc, const SwapChainCoreState& chain, const FrameData& present, bool isAppFrame) { if (!isAppFrame) { - return std::nullopt; + return MissingFrameMetricValue(); } // The current sim start time is only dependent on the current frame's simulation start times. @@ -121,13 +121,13 @@ namespace pmon::util::metrics currentSimStartTime); } else { - return std::nullopt; + return MissingFrameMetricValue(); } } } - std::optional CalculatePcLatency( + double CalculatePcLatency( const QpcConverter& qpc, const SwapChainCoreState& chain, const FrameData& present, @@ -156,7 +156,7 @@ namespace pmon::util::metrics } stateDeltas.newLastReceivedPclSimStart = present.pclSimStartTime; } - return std::nullopt; + return MissingFrameMetricValue(); } // Check to see if we have a valid PC Latency sim start time @@ -203,7 +203,7 @@ namespace pmon::util::metrics return input2FrameStartEma + qpc.DeltaSignedMilliSeconds(simStartTime, screenTime); } else { - return std::nullopt; + return MissingFrameMetricValue(); } } diff --git a/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorInternal.h b/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorInternal.h index 26bc910a..2829b1a5 100644 --- a/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorInternal.h +++ b/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorInternal.h @@ -1,4 +1,4 @@ -// Copyright (C) 2025 Intel Corporation +// Copyright (C) 2025 Intel Corporation // SPDX-License-Identifier: MIT #pragma once @@ -47,7 +47,7 @@ namespace pmon::util::metrics FrameMetrics& metrics, ComputedMetrics::StateDeltas& stateDeltas); - std::optional CalculatePcLatency( + double CalculatePcLatency( const QpcConverter& qpc, const SwapChainCoreState& chain, const FrameData& present, diff --git a/IntelPresentMon/CommonUtilities/mc/MetricsTypes.h b/IntelPresentMon/CommonUtilities/mc/MetricsTypes.h index 64efdea7..4766eaca 100644 --- a/IntelPresentMon/CommonUtilities/mc/MetricsTypes.h +++ b/IntelPresentMon/CommonUtilities/mc/MetricsTypes.h @@ -1,7 +1,9 @@ -// Copyright (C) 2025 Intel Corporation +// Copyright (C) 2025 Intel Corporation // SPDX-License-Identifier: MIT #pragma once +#include #include +#include #include #include #include "../cnr/FixedVector.h" @@ -12,6 +14,16 @@ struct PresentEvent; // From PresentMonTraceConsumer namespace pmon::util::metrics { + inline double MissingFrameMetricValue() + { + return std::numeric_limits::quiet_NaN(); + } + + inline bool IsMissingFrameMetricValue(double value) + { + return std::isnan(value); + } + // Metrics pipeline mode enum class MetricsVersion { V1, @@ -113,7 +125,7 @@ namespace pmon::util::metrics { double msUntilDisplayed = 0; double msBetweenDisplayChange = 0; uint64_t screenTimeQpc = 0; - std::optional msReadyTimeToDisplayLatency; + double msReadyTimeToDisplayLatency = MissingFrameMetricValue(); bool isDroppedFrame = false; // CPU Metrics (app frames only) @@ -129,24 +141,24 @@ namespace pmon::util::metrics { double msGPUTime = 0; // Input Latency (optional, app+displayed only) - std::optional msClickToPhotonLatency = {}; - std::optional msAllInputPhotonLatency = {}; - std::optional msInstrumentedInputTime; + double msClickToPhotonLatency = MissingFrameMetricValue(); + double msAllInputPhotonLatency = MissingFrameMetricValue(); + double msInstrumentedInputTime = MissingFrameMetricValue(); // Animation (optional, app+displayed only) - std::optional msAnimationError = {}; - std::optional msAnimationTime = {}; + double msAnimationError = MissingFrameMetricValue(); + double msAnimationTime = MissingFrameMetricValue(); // Instrumented Metrics (optional) - std::optional msInstrumentedLatency = {}; - std::optional msInstrumentedRenderLatency = {}; - std::optional msInstrumentedSleep = {}; - std::optional msInstrumentedGpuLatency = {}; - std::optional msPcLatency = {}; - std::optional msBetweenSimStarts = {}; + double msInstrumentedLatency = MissingFrameMetricValue(); + double msInstrumentedRenderLatency = MissingFrameMetricValue(); + double msInstrumentedSleep = MissingFrameMetricValue(); + double msInstrumentedGpuLatency = MissingFrameMetricValue(); + double msPcLatency = MissingFrameMetricValue(); + double msBetweenSimStarts = MissingFrameMetricValue(); // PCLatency (optional) - std::optional msFlipDelay = {}; // NVIDIA + double msFlipDelay = MissingFrameMetricValue(); // NVIDIA // Frame Classification FrameType frameType = {}; diff --git a/IntelPresentMon/CommonUtilities/mc/SwapChainState.cpp b/IntelPresentMon/CommonUtilities/mc/SwapChainState.cpp index 964fd282..8e2be523 100644 --- a/IntelPresentMon/CommonUtilities/mc/SwapChainState.cpp +++ b/IntelPresentMon/CommonUtilities/mc/SwapChainState.cpp @@ -21,38 +21,41 @@ namespace pmon::util::metrics { const uint64_t lastScreenTime = present.displayed[lastIdx].second; if (animationErrorSource == AnimationErrorSource::AppProvider) { - lastDisplayedSimStartTime = present.appSimStartTime; - if (firstAppSimStartTime == 0) { - firstAppSimStartTime = present.appSimStartTime; - } - lastDisplayedAppScreenTime = lastScreenTime; - } - else if (animationErrorSource == AnimationErrorSource::PCLatency) { - // In the case of PCLatency only set values if PCL sim start time is non-zero - if (present.pclSimStartTime != 0) { - lastDisplayedSimStartTime = present.pclSimStartTime; + if (present.appSimStartTime != 0) { + lastDisplayedSimStartTime = present.appSimStartTime; if (firstAppSimStartTime == 0) { - firstAppSimStartTime = present.pclSimStartTime; + firstAppSimStartTime = present.appSimStartTime; } lastDisplayedAppScreenTime = lastScreenTime; } } - else { // AnimationErrorSource::CpuStart - // Check for PCL or App sim start and possibly change source. - if (present.pclSimStartTime != 0) { - animationErrorSource = AnimationErrorSource::PCLatency; + else if (animationErrorSource == AnimationErrorSource::PCLatency) { + if (present.appSimStartTime != 0) { + animationErrorSource = AnimationErrorSource::AppProvider; + firstAppSimStartTime = present.appSimStartTime; + lastDisplayedSimStartTime = present.appSimStartTime; + lastDisplayedAppScreenTime = lastScreenTime; + } + else if (present.pclSimStartTime != 0) { lastDisplayedSimStartTime = present.pclSimStartTime; if (firstAppSimStartTime == 0) { firstAppSimStartTime = present.pclSimStartTime; } lastDisplayedAppScreenTime = lastScreenTime; } - else if (present.appSimStartTime != 0) { + } + else { // AnimationErrorSource::CpuStart + // Check for provider sim start and possibly change source. + if (present.appSimStartTime != 0) { animationErrorSource = AnimationErrorSource::AppProvider; + firstAppSimStartTime = present.appSimStartTime; lastDisplayedSimStartTime = present.appSimStartTime; - if (firstAppSimStartTime == 0) { - firstAppSimStartTime = present.appSimStartTime; - } + lastDisplayedAppScreenTime = lastScreenTime; + } + else if (present.pclSimStartTime != 0) { + animationErrorSource = AnimationErrorSource::PCLatency; + firstAppSimStartTime = present.pclSimStartTime; + lastDisplayedSimStartTime = present.pclSimStartTime; lastDisplayedAppScreenTime = lastScreenTime; } else { diff --git a/IntelPresentMon/PresentMonAPI2Tests/CsvHelper.h b/IntelPresentMon/PresentMonAPI2Tests/CsvHelper.h index 176a0c8d..96da9d9d 100644 --- a/IntelPresentMon/PresentMonAPI2Tests/CsvHelper.h +++ b/IntelPresentMon/PresentMonAPI2Tests/CsvHelper.h @@ -1,4 +1,4 @@ -// Copyright (C) 2022-2023 Intel Corporation +// Copyright (C) 2022-2023 Intel Corporation // SPDX-License-Identifier: MIT #pragma once @@ -11,7 +11,10 @@ #include #include #include +#include +#include #include +#include using namespace Microsoft::VisualStudio::CppUnitTestFramework; @@ -42,6 +45,7 @@ enum Header { Header_MsVideoBusy, Header_MsAnimationError, Header_AnimationTime, + Header_MsFlipDelay, Header_MsClickToPhotonLatency, Header_MsAllInputToPhotonLatency, @@ -72,6 +76,8 @@ enum Header { UnknownHeader, }; +constexpr double kMissingMetricValue = std::numeric_limits::quiet_NaN(); + struct v2Metrics { std::string appName; uint32_t processId = 0; @@ -95,20 +101,21 @@ struct v2Metrics { double msGpuBusy = 0.; double msGpuWait = 0.; double msVideoBusy = 0.; - std::optional msBetweenSimStart; - std::optional msUntilDisplayed; - std::optional msBetweenDisplayChange; - std::optional msPcLatency; - std::optional msAnimationError; - std::optional animationTime; - std::optional msClickToPhotonLatency; - std::optional msAllInputToPhotonLatency; - std::optional msInstrumentedLatency; - std::optional msInstrumentedRenderLatency; - std::optional msInstrumentedSleep; - std::optional msInstrumentedGPULatency; - std::optional msReadyTimeToDisplayLatency; - std::optional msReprojectedLatency; + double msBetweenSimStart = kMissingMetricValue; + double msUntilDisplayed = kMissingMetricValue; + double msBetweenDisplayChange = kMissingMetricValue; + double msPcLatency = kMissingMetricValue; + double msAnimationError = kMissingMetricValue; + double animationTime = kMissingMetricValue; + double msFlipDelay = kMissingMetricValue; + double msClickToPhotonLatency = kMissingMetricValue; + double msAllInputToPhotonLatency = kMissingMetricValue; + double msInstrumentedLatency = kMissingMetricValue; + double msInstrumentedRenderLatency = kMissingMetricValue; + double msInstrumentedSleep = kMissingMetricValue; + double msInstrumentedGPULatency = kMissingMetricValue; + double msReadyTimeToDisplayLatency = kMissingMetricValue; + double msReprojectedLatency = kMissingMetricValue; }; constexpr char const* GetHeaderString(Header h) @@ -136,6 +143,7 @@ constexpr char const* GetHeaderString(Header h) case Header_MsGPULatency: return "MsGPULatency"; case Header_MsGPUTime: return "MsGPUTime"; case Header_MsGPUBusy: return "MsGPUBusy"; + case Header_MsFlipDelay: return "MsFlipDelay"; case Header_MsVideoBusy: return "MsVideoBusy"; case Header_MsGPUWait: return "MsGPUWait"; case Header_MsAnimationError: return "MsAnimationError"; @@ -305,6 +313,52 @@ void CharConvert::Convert(const std::string data, T& convertedData, Header co } } +bool IsMissingMetricToken(const char* data) +{ + if (data == nullptr) { + return true; + } + + const char* tokenBegin = data; + while (*tokenBegin != '\0' && std::isspace(static_cast(*tokenBegin))) { + ++tokenBegin; + } + + const char* tokenEnd = tokenBegin; + while (*tokenEnd != '\0') { + ++tokenEnd; + } + + while (tokenEnd > tokenBegin && std::isspace(static_cast(*(tokenEnd - 1)))) { + --tokenEnd; + } + + if (tokenBegin == tokenEnd) { + return true; + } + + std::string token(tokenBegin, tokenEnd); + return _stricmp(token.c_str(), "NA") == 0 || + _stricmp(token.c_str(), "N/A") == 0 || + _stricmp(token.c_str(), "NaN") == 0; +} + +double ParseMetricValue(const char* data, Header columnId, size_t line) +{ + if (IsMissingMetricToken(data)) { + return kMissingMetricValue; + } + + double convertedData = 0.; + CharConvert converter; + converter.Convert(data, convertedData, columnId, line); + if (std::isnan(convertedData)) { + return kMissingMetricValue; + } + + return convertedData; +} + size_t countDecimalPlaces(double value) { std::string str = std::to_string(value); auto dotPos = str.find('.'); @@ -360,6 +414,27 @@ bool Validate(const T& param1, const T& param2) { } + + +bool ValidateMetricValue(double expected, double actual) +{ + const auto expectedMissing = std::isnan(expected); + const auto actualMissing = std::isnan(actual); + if (actualMissing) { + // If actual is missing, it's only valid if expected is exactly 0.0, + // otherwise it would be a mismatch with the gold file. The intent is to eventually + // migrate all gold files to use NA or NAN for missing metrics, but in the meantime + // we want to allow 0.0 as a valid expected value for metrics yet to be transitioned. + return expectedMissing || expected == 0.0; + } + + if (expectedMissing) { + return false; + } + + return Validate(expected, actual); +} + std::optional CreateCsvFile(std::string& output_dir, std::string& processName) { // Setup csv file @@ -373,7 +448,7 @@ std::optional CreateCsvFile(std::string& output_dir, std::string& csvFile << "Application,ProcessID,SwapChainAddress,PresentRuntime" ",SyncInterval,PresentFlags,AllowsTearing,PresentMode" - ",FrameType,TimeInSec,MsBetweenSimulationStart,MsBetweenPresents" + ",FrameType,TimeInQPC,MsBetweenSimulationStart,MsBetweenPresents" ",MsBetweenDisplayChange,MsInPresent,MsRenderPresentLatency" ",MsUntilDisplayed,MsPCLatency,CPUStartQPC,MsBetweenAppStart" ",MsCPUBusy,MsCPUWait,MsGPULatency,MsGPUTime,MsGPUBusy,MsGPUWait" @@ -642,13 +717,13 @@ bool CsvParser::VerifyBlobAgainstCsv(const std::string& processName, const unsig columnsMatch = Validate(v2MetricRow_.cpuFrameQpc, cpuStartQpc); break; case Header_MsBetweenAppStart: - columnsMatch = Validate(v2MetricRow_.msBetweenAppStart, msBetweenAppStart); + columnsMatch = ValidateMetricValue(v2MetricRow_.msBetweenAppStart, msBetweenAppStart); break; case Header_MsCPUBusy: - columnsMatch = Validate(v2MetricRow_.msCpuBusy, msCpuBusy); + columnsMatch = ValidateMetricValue(v2MetricRow_.msCpuBusy, msCpuBusy); break; case Header_MsCPUWait: - columnsMatch = Validate(v2MetricRow_.msCpuWait, msCpuWait); + columnsMatch = ValidateMetricValue(v2MetricRow_.msCpuWait, msCpuWait); break; case Header_MsGPULatency: columnsMatch = Validate(v2MetricRow_.msGpuLatency, msGpuLatency); @@ -663,139 +738,34 @@ bool CsvParser::VerifyBlobAgainstCsv(const std::string& processName, const unsig columnsMatch = Validate(v2MetricRow_.msGpuWait, msGpuWait); break; case Header_MsBetweenSimulationStart: - if (v2MetricRow_.msBetweenSimStart.has_value()) { - columnsMatch = Validate(v2MetricRow_.msBetweenSimStart.value(), msBetweenSimStartTime); - } - else - { - if (std::isnan(msBetweenSimStartTime)) { - columnsMatch = true; - } - else - { - columnsMatch = false; - } - } + columnsMatch = ValidateMetricValue(v2MetricRow_.msBetweenSimStart, msBetweenSimStartTime); break; case Header_MsUntilDisplayed: - if (v2MetricRow_.msUntilDisplayed.has_value()) { - columnsMatch = Validate(v2MetricRow_.msUntilDisplayed.value(), msUntilDisplayed); - } - else - { - if (std::isnan(msUntilDisplayed)) { - columnsMatch = true; - } - else - { - columnsMatch = false; - } - } + columnsMatch = ValidateMetricValue(v2MetricRow_.msUntilDisplayed, msUntilDisplayed); break; case Header_MsBetweenDisplayChange: - if (v2MetricRow_.msBetweenDisplayChange.has_value()) { - columnsMatch = Validate(v2MetricRow_.msBetweenDisplayChange.value(), msBetweenDisplayChange); - } - else - { - if (std::isnan(msBetweenDisplayChange)) { - columnsMatch = true; - } - else - { - columnsMatch = false; - } - } + columnsMatch = ValidateMetricValue(v2MetricRow_.msBetweenDisplayChange, msBetweenDisplayChange); break; case Header_MsPCLatency: - if (v2MetricRow_.msPcLatency.has_value()) { - columnsMatch = Validate(v2MetricRow_.msPcLatency.value(), msPcLatency); - } - else - { - if (std::isnan(msPcLatency)) { - columnsMatch = true; - } - else - { - columnsMatch = false; - } - } + columnsMatch = ValidateMetricValue(v2MetricRow_.msPcLatency, msPcLatency); break; case Header_MsAnimationError: - if (v2MetricRow_.msAnimationError.has_value()) { - columnsMatch = Validate(v2MetricRow_.msAnimationError.value(), msAnimationError); - } - else - { - if (std::isnan(msAnimationError)) { - columnsMatch = true; - } - else - { - columnsMatch = false; - } - } + columnsMatch = ValidateMetricValue(v2MetricRow_.msAnimationError, msAnimationError); break; case Header_AnimationTime: - if (v2MetricRow_.animationTime.has_value()) { - columnsMatch = Validate(v2MetricRow_.animationTime.value(), animationTime); - } - else - { - if (std::isnan(animationTime)) { - columnsMatch = true; - } - else - { - columnsMatch = false; - } - } + columnsMatch = ValidateMetricValue(v2MetricRow_.animationTime, animationTime); + break; + case Header_MsFlipDelay: + columnsMatch = ValidateMetricValue(v2MetricRow_.msFlipDelay, msFrameDelay); break; case Header_MsClickToPhotonLatency: - if (v2MetricRow_.msClickToPhotonLatency.has_value()) { - columnsMatch = Validate(v2MetricRow_.msClickToPhotonLatency.value(), msClickToPhotonLatency); - } - else - { - if (std::isnan(msClickToPhotonLatency)) { - columnsMatch = true; - } - else - { - columnsMatch = false; - } - } + columnsMatch = ValidateMetricValue(v2MetricRow_.msClickToPhotonLatency, msClickToPhotonLatency); break; case Header_MsAllInputToPhotonLatency: - if (v2MetricRow_.msAllInputToPhotonLatency.has_value()) { - columnsMatch = Validate(v2MetricRow_.msAllInputToPhotonLatency.value(), msAllInputToPhotonLatency); - } - else - { - if (std::isnan(msAllInputToPhotonLatency)) { - columnsMatch = true; - } - else - { - columnsMatch = false; - } - } + columnsMatch = ValidateMetricValue(v2MetricRow_.msAllInputToPhotonLatency, msAllInputToPhotonLatency); break; case Header_MsInstrumentedLatency: - if (v2MetricRow_.msInstrumentedLatency.has_value()) { - columnsMatch = Validate(v2MetricRow_.msInstrumentedLatency.value(), msInstrumentedLatency); - } - else - { - if (std::isnan(msInstrumentedLatency)) { - columnsMatch = true; - } - else - { - columnsMatch = false; - } - } + columnsMatch = ValidateMetricValue(v2MetricRow_.msInstrumentedLatency, msInstrumentedLatency); break; default: columnsMatch = true; @@ -918,6 +888,7 @@ bool CsvParser::Open(std::wstring const& path, uint32_t processId) { Header_MsBetweenDisplayChange, Header_MsAnimationError, Header_AnimationTime, + Header_MsFlipDelay, Header_MsClickToPhotonLatency, Header_MsAllInputToPhotonLatency, Header_MsBetweenSimulationStart, @@ -1076,16 +1047,7 @@ void CsvParser::ConvertToMetricDataType(const char* data, Header columnId) break; case Header_MsBetweenSimulationStart: { - if (strncmp(data, "NA", 2) != 0) { - double convertedData = 0.; - CharConvert converter; - converter.Convert(data, convertedData, columnId, line_); - v2MetricRow_.msBetweenSimStart = convertedData; - } - else - { - v2MetricRow_.msBetweenSimStart.reset(); - } + v2MetricRow_.msBetweenSimStart = ParseMetricValue(data, columnId, line_); } break; case Header_MsBetweenPresents: @@ -1096,15 +1058,7 @@ void CsvParser::ConvertToMetricDataType(const char* data, Header columnId) break; case Header_MsBetweenDisplayChange: { - if (strncmp(data, "NA", 2) != 0) { - double convertedData = 0.; - CharConvert converter; - converter.Convert(data, convertedData, columnId, line_); - v2MetricRow_.msBetweenDisplayChange = convertedData; - } - else { - v2MetricRow_.msBetweenDisplayChange.reset(); - } + v2MetricRow_.msBetweenDisplayChange = ParseMetricValue(data, columnId, line_); } break; case Header_MsInPresentAPI: { @@ -1120,95 +1074,42 @@ void CsvParser::ConvertToMetricDataType(const char* data, Header columnId) break; case Header_MsUntilDisplayed: { - if (strncmp(data, "NA", 2) != 0) { - double convertedData = 0.; - CharConvert converter; - converter.Convert(data, convertedData, columnId, line_); - v2MetricRow_.msUntilDisplayed = convertedData; - } - else - { - v2MetricRow_.msUntilDisplayed.reset(); - } + v2MetricRow_.msUntilDisplayed = ParseMetricValue(data, columnId, line_); } break; case Header_MsPCLatency: { - if (strncmp(data, "NA", 2) != 0) { - double convertedData = 0.; - CharConvert converter; - converter.Convert(data, convertedData, columnId, line_); - v2MetricRow_.msPcLatency = convertedData; - } - else { - v2MetricRow_.msPcLatency.reset(); - } + v2MetricRow_.msPcLatency = ParseMetricValue(data, columnId, line_); } break; case Header_MsAnimationError: { - if (strncmp(data, "NA", 2) != 0) { - double convertedData = 0.; - CharConvert converter; - converter.Convert(data, convertedData, columnId, line_); - v2MetricRow_.msAnimationError = convertedData; - } - else { - v2MetricRow_.msAnimationError.reset(); - } + v2MetricRow_.msAnimationError = ParseMetricValue(data, columnId, line_); } break; case Header_AnimationTime: { - if (strncmp(data, "NA", 2) != 0) { - double convertedData = 0.; - CharConvert converter; - converter.Convert(data, convertedData, columnId, line_); - v2MetricRow_.animationTime = convertedData; - } - else { - v2MetricRow_.animationTime.reset(); - } + v2MetricRow_.animationTime = ParseMetricValue(data, columnId, line_); + } + break; + case Header_MsFlipDelay: + { + v2MetricRow_.msFlipDelay = ParseMetricValue(data, columnId, line_); } break; case Header_MsClickToPhotonLatency: { - if (strncmp(data, "NA", 2) != 0) { - double convertedData = 0.; - CharConvert converter; - converter.Convert(data, convertedData, columnId, line_); - v2MetricRow_.msClickToPhotonLatency = convertedData; - } - else { - v2MetricRow_.msClickToPhotonLatency.reset(); - } + v2MetricRow_.msClickToPhotonLatency = ParseMetricValue(data, columnId, line_); } break; case Header_MsAllInputToPhotonLatency: { - if (strncmp(data, "NA", 2) != 0) { - double convertedData = 0.; - CharConvert converter; - converter.Convert(data, convertedData, columnId, line_); - v2MetricRow_.msAllInputToPhotonLatency = convertedData; - } - else { - v2MetricRow_.msAllInputToPhotonLatency.reset(); - } + v2MetricRow_.msAllInputToPhotonLatency = ParseMetricValue(data, columnId, line_); } break; case Header_MsInstrumentedLatency: { - if (strncmp(data, "NA", 2) != 0) { - double convertedData = 0.; - CharConvert converter; - converter.Convert(data, convertedData, columnId, line_); - v2MetricRow_.msInstrumentedLatency = convertedData; - } - else - { - v2MetricRow_.msInstrumentedLatency.reset(); - } + v2MetricRow_.msInstrumentedLatency = ParseMetricValue(data, columnId, line_); } break; default: diff --git a/IntelPresentMon/PresentMonAPI2Tests/PacedFrameTests.cpp b/IntelPresentMon/PresentMonAPI2Tests/PacedFrameTests.cpp index 2b13de63..61469ba0 100644 --- a/IntelPresentMon/PresentMonAPI2Tests/PacedFrameTests.cpp +++ b/IntelPresentMon/PresentMonAPI2Tests/PacedFrameTests.cpp @@ -7,9 +7,11 @@ #include "Folders.h" #include #include +#include #include #include #include +#include #include #include #include @@ -106,6 +108,8 @@ namespace PacedFrame "MsInstrumentedLatency", }; + constexpr double kMissingMetricValue = std::numeric_limits::quiet_NaN(); + struct FrameCsvRow { std::string application; @@ -117,14 +121,14 @@ namespace PacedFrame uint32_t allowsTearing = 0; std::string presentMode; std::string frameType; - std::optional cpuStartTime; - std::optional msBetweenSimulationStart; + double cpuStartTime = kMissingMetricValue; + double msBetweenSimulationStart = kMissingMetricValue; double msBetweenPresents = 0.0; - std::optional msBetweenDisplayChange; + double msBetweenDisplayChange = kMissingMetricValue; double msInPresentApi = 0.0; double msRenderPresentLatency = 0.0; - std::optional msUntilDisplayed; - std::optional msPcLatency; + double msUntilDisplayed = kMissingMetricValue; + double msPcLatency = kMissingMetricValue; double msBetweenAppStart = 0.0; double msCpuBusy = 0.0; double msCpuWait = 0.0; @@ -133,12 +137,12 @@ namespace PacedFrame double msGpuBusy = 0.0; double msGpuWait = 0.0; double msVideoBusy = 0.0; - std::optional msAnimationError; - std::optional animationTime; - std::optional msFlipDelay; - std::optional msAllInputToPhotonLatency; - std::optional msClickToPhotonLatency; - std::optional msInstrumentedLatency; + double msAnimationError = kMissingMetricValue; + double animationTime = kMissingMetricValue; + double msFlipDelay = kMissingMetricValue; + double msAllInputToPhotonLatency = kMissingMetricValue; + double msClickToPhotonLatency = kMissingMetricValue; + double msInstrumentedLatency = kMissingMetricValue; }; std::wstring MakeFailMessage(size_t row, const char* column, const std::string& expected, @@ -166,6 +170,11 @@ namespace PacedFrame return value == "NA" || value == "NaN" || value == "nan"; } + bool IsMissingValue(double value) + { + return std::isnan(value); + } + uint64_t ParseUint64(const std::string& value, size_t row, const char* column) { try { @@ -210,10 +219,10 @@ namespace PacedFrame return 0.0; } - std::optional ParseOptionalDouble(const std::string& value, size_t row, const char* column) + double ParseMetricDouble(const std::string& value, size_t row, const char* column) { if (IsMissingToken(value)) { - return std::nullopt; + return kMissingMetricValue; } return ParseDouble(value, row, column); } @@ -239,21 +248,21 @@ namespace PacedFrame parsed.allowsTearing = ParseUint32(row[static_cast(ColumnIndex::AllowsTearing)], rowIndex, "AllowsTearing"); parsed.presentMode = row[static_cast(ColumnIndex::PresentMode)]; parsed.frameType = row[static_cast(ColumnIndex::FrameType)]; - parsed.cpuStartTime = ParseOptionalDouble( + parsed.cpuStartTime = ParseMetricDouble( row[static_cast(ColumnIndex::CPUStartTime)], rowIndex, "CPUStartTime"); - parsed.msBetweenSimulationStart = ParseOptionalDouble( + parsed.msBetweenSimulationStart = ParseMetricDouble( row[static_cast(ColumnIndex::MsBetweenSimulationStart)], rowIndex, "MsBetweenSimulationStart"); parsed.msBetweenPresents = ParseDouble( row[static_cast(ColumnIndex::MsBetweenPresents)], rowIndex, "MsBetweenPresents"); - parsed.msBetweenDisplayChange = ParseOptionalDouble( + parsed.msBetweenDisplayChange = ParseMetricDouble( row[static_cast(ColumnIndex::MsBetweenDisplayChange)], rowIndex, "MsBetweenDisplayChange"); parsed.msInPresentApi = ParseDouble( row[static_cast(ColumnIndex::MsInPresentAPI)], rowIndex, "MsInPresentAPI"); parsed.msRenderPresentLatency = ParseDouble( row[static_cast(ColumnIndex::MsRenderPresentLatency)], rowIndex, "MsRenderPresentLatency"); - parsed.msUntilDisplayed = ParseOptionalDouble( + parsed.msUntilDisplayed = ParseMetricDouble( row[static_cast(ColumnIndex::MsUntilDisplayed)], rowIndex, "MsUntilDisplayed"); - parsed.msPcLatency = ParseOptionalDouble( + parsed.msPcLatency = ParseMetricDouble( row[static_cast(ColumnIndex::MsPCLatency)], rowIndex, "MsPCLatency"); parsed.msBetweenAppStart = ParseDouble( row[static_cast(ColumnIndex::MsBetweenAppStart)], rowIndex, "MsBetweenAppStart"); @@ -271,17 +280,17 @@ namespace PacedFrame row[static_cast(ColumnIndex::MsGPUWait)], rowIndex, "MsGPUWait"); parsed.msVideoBusy = ParseDouble( row[static_cast(ColumnIndex::MsVideoBusy)], rowIndex, "MsVideoBusy"); - parsed.msAnimationError = ParseOptionalDouble( + parsed.msAnimationError = ParseMetricDouble( row[static_cast(ColumnIndex::MsAnimationError)], rowIndex, "MsAnimationError"); - parsed.animationTime = ParseOptionalDouble( + parsed.animationTime = ParseMetricDouble( row[static_cast(ColumnIndex::AnimationTime)], rowIndex, "AnimationTime"); - parsed.msFlipDelay = ParseOptionalDouble( + parsed.msFlipDelay = ParseMetricDouble( row[static_cast(ColumnIndex::MsFlipDelay)], rowIndex, "MsFlipDelay"); - parsed.msAllInputToPhotonLatency = ParseOptionalDouble( + parsed.msAllInputToPhotonLatency = ParseMetricDouble( row[static_cast(ColumnIndex::MsAllInputToPhotonLatency)], rowIndex, "MsAllInputToPhotonLatency"); - parsed.msClickToPhotonLatency = ParseOptionalDouble( + parsed.msClickToPhotonLatency = ParseMetricDouble( row[static_cast(ColumnIndex::MsClickToPhotonLatency)], rowIndex, "MsClickToPhotonLatency"); - parsed.msInstrumentedLatency = ParseOptionalDouble( + parsed.msInstrumentedLatency = ParseMetricDouble( row[static_cast(ColumnIndex::MsInstrumentedLatency)], rowIndex, "MsInstrumentedLatency"); return parsed; } @@ -359,13 +368,15 @@ namespace PacedFrame return std::nullopt; } - void CompareOptionalDouble(const std::optional& expected, const std::optional& actual, + void CompareMetricDouble(double expected, double actual, size_t rowIndex, const char* column) { - if (expected.has_value() != actual.has_value()) { + const auto expectedMissing = IsMissingValue(expected); + const auto actualMissing = IsMissingValue(actual); + if (expectedMissing != actualMissing) { Assert::Fail(MakeFailMessage(rowIndex, column).c_str()); } - if (expected && actual && *expected != *actual) { + if (!expectedMissing && expected != actual) { Assert::Fail(MakeFailMessage(rowIndex, column).c_str()); } } @@ -402,14 +413,14 @@ namespace PacedFrame Assert::Fail(MakeFailMessage(rowIndex, "FrameType", expected.frameType, actual.frameType).c_str()); } - CompareOptionalDouble(expected.cpuStartTime, actual.cpuStartTime, + CompareMetricDouble(expected.cpuStartTime, actual.cpuStartTime, rowIndex, "CPUStartTime"); - CompareOptionalDouble(expected.msBetweenSimulationStart, actual.msBetweenSimulationStart, + CompareMetricDouble(expected.msBetweenSimulationStart, actual.msBetweenSimulationStart, rowIndex, "MsBetweenSimulationStart"); if (expected.msBetweenPresents != actual.msBetweenPresents) { Assert::Fail(MakeFailMessage(rowIndex, "MsBetweenPresents").c_str()); } - CompareOptionalDouble(expected.msBetweenDisplayChange, actual.msBetweenDisplayChange, + CompareMetricDouble(expected.msBetweenDisplayChange, actual.msBetweenDisplayChange, rowIndex, "MsBetweenDisplayChange"); if (expected.msInPresentApi != actual.msInPresentApi) { Assert::Fail(MakeFailMessage(rowIndex, "MsInPresentAPI").c_str()); @@ -417,9 +428,9 @@ namespace PacedFrame if (expected.msRenderPresentLatency != actual.msRenderPresentLatency) { Assert::Fail(MakeFailMessage(rowIndex, "MsRenderPresentLatency").c_str()); } - CompareOptionalDouble(expected.msUntilDisplayed, actual.msUntilDisplayed, + CompareMetricDouble(expected.msUntilDisplayed, actual.msUntilDisplayed, rowIndex, "MsUntilDisplayed"); - CompareOptionalDouble(expected.msPcLatency, actual.msPcLatency, + CompareMetricDouble(expected.msPcLatency, actual.msPcLatency, rowIndex, "MsPCLatency"); if (expected.msBetweenAppStart != actual.msBetweenAppStart) { Assert::Fail(MakeFailMessage(rowIndex, "MsBetweenAppStart").c_str()); @@ -445,17 +456,17 @@ namespace PacedFrame if (expected.msVideoBusy != actual.msVideoBusy) { Assert::Fail(MakeFailMessage(rowIndex, "MsVideoBusy").c_str()); } - CompareOptionalDouble(expected.msAnimationError, actual.msAnimationError, + CompareMetricDouble(expected.msAnimationError, actual.msAnimationError, rowIndex, "MsAnimationError"); - CompareOptionalDouble(expected.animationTime, actual.animationTime, + CompareMetricDouble(expected.animationTime, actual.animationTime, rowIndex, "AnimationTime"); - CompareOptionalDouble(expected.msFlipDelay, actual.msFlipDelay, + CompareMetricDouble(expected.msFlipDelay, actual.msFlipDelay, rowIndex, "MsFlipDelay"); - CompareOptionalDouble(expected.msAllInputToPhotonLatency, actual.msAllInputToPhotonLatency, + CompareMetricDouble(expected.msAllInputToPhotonLatency, actual.msAllInputToPhotonLatency, rowIndex, "MsAllInputToPhotonLatency"); - CompareOptionalDouble(expected.msClickToPhotonLatency, actual.msClickToPhotonLatency, + CompareMetricDouble(expected.msClickToPhotonLatency, actual.msClickToPhotonLatency, rowIndex, "MsClickToPhotonLatency"); - CompareOptionalDouble(expected.msInstrumentedLatency, actual.msInstrumentedLatency, + CompareMetricDouble(expected.msInstrumentedLatency, actual.msInstrumentedLatency, rowIndex, "MsInstrumentedLatency"); } diff --git a/IntelPresentMon/PresentMonMiddleware/DynamicMetric.h b/IntelPresentMon/PresentMonMiddleware/DynamicMetric.h index b254ddb6..35b3e0fd 100644 --- a/IntelPresentMon/PresentMonMiddleware/DynamicMetric.h +++ b/IntelPresentMon/PresentMonMiddleware/DynamicMetric.h @@ -70,7 +70,7 @@ namespace pmon::mid { const auto value = AdjustSample_(sample.*MemberPtr); // if samples has reserved size, it is needed - if (samples_.capacity()) { + if (samples_.capacity() && detail::DynamicStatSampleAdapter::HasValue(value)) { samples_.push_back(value); } for (auto* stat : needsUpdatePtrs_) { diff --git a/IntelPresentMon/PresentMonMiddleware/DynamicStat.cpp b/IntelPresentMon/PresentMonMiddleware/DynamicStat.cpp index 288df9aa..e019e0d3 100644 --- a/IntelPresentMon/PresentMonMiddleware/DynamicStat.cpp +++ b/IntelPresentMon/PresentMonMiddleware/DynamicStat.cpp @@ -15,55 +15,12 @@ namespace pmon::mid { namespace detail { - // TODO: consider ways of obviating this adapter construct - template - struct SampleAdapter_ - { - static bool HasValue(const T&) - { - return true; - } - static bool IsZero(const T& val) - { - return val == (T)0; - } - static double ToDouble(const T& val) - { - return (double)val; - } - static uint64_t ToUint64(const T& val) - { - return (uint64_t)val; - } - }; - - template - struct SampleAdapter_> - { - static bool HasValue(const std::optional& val) - { - return val.has_value(); - } - static bool IsZero(const std::optional& val) - { - return !val.has_value() || *val == (U)0; - } - static double ToDouble(const std::optional& val) - { - return val.has_value() ? (double)*val : 0.0; - } - static uint64_t ToUint64(const std::optional& val) - { - return val.has_value() ? (uint64_t)*val : 0; - } - }; - template void WriteOptionalValueToBlob_(uint8_t* pBase, size_t offsetBytes, PM_DATA_TYPE outType, const std::optional& value) { auto* pTarget = pBase + offsetBytes; - const double doubleVal = value ? SampleAdapter_::ToDouble(*value) : 0.0; - const uint64_t uint64Val = value ? SampleAdapter_::ToUint64(*value) : 0; + const double doubleVal = value ? DynamicStatSampleAdapter::ToDouble(*value) : 0.0; + const uint64_t uint64Val = value ? DynamicStatSampleAdapter::ToUint64(*value) : 0; switch (outType) { case PM_DATA_TYPE_DOUBLE: *reinterpret_cast(pTarget) = doubleVal; @@ -129,7 +86,7 @@ namespace pmon::mid WriteOptionalValueToBlob_(pBase, offsetBytes_, outType_, std::optional{}); return; } - const double rawValue = SampleAdapter_::ToDouble(*value); + const double rawValue = DynamicStatSampleAdapter::ToDouble(*value); // if value is present but zero, cannot recip zero so write nullopt if (rawValue == 0.0) { WriteOptionalValueToBlob_(pBase, offsetBytes_, outType_, std::optional{}); @@ -161,13 +118,13 @@ namespace pmon::mid bool NeedsSortedWindow() const override { return false; } void AddSample(T val) override { - if (!SampleAdapter_::HasValue(val)) { + if (!DynamicStatSampleAdapter::HasValue(val)) { return; } - if (skipZero_ && SampleAdapter_::IsZero(val)) { + if (skipZero_ && DynamicStatSampleAdapter::IsZero(val)) { return; } - sum_ += SampleAdapter_::ToDouble(val); + sum_ += DynamicStatSampleAdapter::ToDouble(val); ++count_; } void GatherToBlob(uint8_t* pBase) const override @@ -228,7 +185,7 @@ namespace pmon::mid // Step 0: locate the first valid value (ignore empties/invalids at the front). size_t firstValid = 0; while (firstValid < sortedSamples.size() && - !SampleAdapter_::HasValue(sortedSamples[firstValid])) { + !DynamicStatSampleAdapter::HasValue(sortedSamples[firstValid])) { ++firstValid; } const size_t validCount = sortedSamples.size() - firstValid; @@ -250,8 +207,8 @@ namespace pmon::mid // (but if at the end of container, use i for both sides of interpolation) const size_t i1 = (i + 1 < validCount) ? (i + 1) : i; // retrieve both samples - const double x0 = SampleAdapter_::ToDouble(sortedSamples[firstValid + i]); - const double x1 = SampleAdapter_::ToDouble(sortedSamples[firstValid + i1]); + const double x0 = DynamicStatSampleAdapter::ToDouble(sortedSamples[firstValid + i]); + const double x1 = DynamicStatSampleAdapter::ToDouble(sortedSamples[firstValid + i1]); // Step 4: perform linear interpolation value_ = x0 + g * (x1 - x0); @@ -282,10 +239,10 @@ namespace pmon::mid bool NeedsSortedWindow() const override { return false; } void AddSample(T val) override { - if (!SampleAdapter_::HasValue(val)) { + if (!DynamicStatSampleAdapter::HasValue(val)) { return; } - const double doubleVal = SampleAdapter_::ToDouble(val); + const double doubleVal = DynamicStatSampleAdapter::ToDouble(val); if (!value_) { value_ = doubleVal; return; @@ -343,6 +300,10 @@ namespace pmon::mid } void SetSampledValue(T val) override { + if (!DynamicStatSampleAdapter::HasValue(val)) { + value_.reset(); + return; + } value_ = val; } void GatherToBlob(uint8_t* pBase) const override @@ -413,7 +374,6 @@ namespace pmon::mid template std::unique_ptr> MakeDynamicStat(PM_STAT stat, PM_DATA_TYPE inType, PM_DATA_TYPE outType, size_t offsetBytes, std::optional reciprocationFactor); template std::unique_ptr> MakeDynamicStat(PM_STAT stat, PM_DATA_TYPE inType, PM_DATA_TYPE outType, size_t offsetBytes, std::optional reciprocationFactor); template std::unique_ptr> MakeDynamicStat(PM_STAT stat, PM_DATA_TYPE inType, PM_DATA_TYPE outType, size_t offsetBytes, std::optional reciprocationFactor); - template std::unique_ptr>> MakeDynamicStat>(PM_STAT stat, PM_DATA_TYPE inType, PM_DATA_TYPE outType, size_t offsetBytes, std::optional reciprocationFactor); template std::unique_ptr> MakeDynamicStat<::PresentMode>(PM_STAT stat, PM_DATA_TYPE inType, PM_DATA_TYPE outType, size_t offsetBytes, std::optional reciprocationFactor); template std::unique_ptr> MakeDynamicStat<::Runtime>(PM_STAT stat, PM_DATA_TYPE inType, PM_DATA_TYPE outType, size_t offsetBytes, std::optional reciprocationFactor); template std::unique_ptr> MakeDynamicStat<::FrameType>(PM_STAT stat, PM_DATA_TYPE inType, PM_DATA_TYPE outType, size_t offsetBytes, std::optional reciprocationFactor); diff --git a/IntelPresentMon/PresentMonMiddleware/DynamicStat.h b/IntelPresentMon/PresentMonMiddleware/DynamicStat.h index a0e2cc9b..b1fa865e 100644 --- a/IntelPresentMon/PresentMonMiddleware/DynamicStat.h +++ b/IntelPresentMon/PresentMonMiddleware/DynamicStat.h @@ -1,15 +1,50 @@ #pragma once +#include #include #include #include #include #include +#include #include "../PresentMonAPI2/PresentMonAPI.h" namespace pmon::mid { struct DynamicQueryWindow; + namespace detail + { + template + struct DynamicStatSampleAdapter + { + static bool HasValue(const T& val) + { + if constexpr (std::is_floating_point_v) { + return !std::isnan(val); + } + else { + return true; + } + } + + static bool IsZero(const T& val) + { + return val == (T)0; + } + + static double ToDouble(const T& val) + { + return (double)val; + } + + static uint64_t ToUint64(const T& val) + { + return (uint64_t)val; + } + }; + + } + template class DynamicStat { diff --git a/IntelPresentMon/PresentMonMiddleware/FrameEventQuery.cpp b/IntelPresentMon/PresentMonMiddleware/FrameEventQuery.cpp index 2a6e4cf7..c8d1edde 100644 --- a/IntelPresentMon/PresentMonMiddleware/FrameEventQuery.cpp +++ b/IntelPresentMon/PresentMonMiddleware/FrameEventQuery.cpp @@ -116,10 +116,8 @@ PM_FRAME_QUERY::GatherCommand_ PM_FRAME_QUERY::MapQueryElementToFrameGatherComma [&]() -> bool { if constexpr (util::metrics::HasFrameMetricMember) { constexpr auto memberPtr = util::metrics::FrameMetricMember::member; - using MemberType = typename util::MemberPointerInfo::MemberType; static const uint32_t memberOffset = uint32_t(util::MemberPointerOffset(memberPtr)); cmd.frameMetricsOffset = memberOffset; - cmd.isOptional = util::IsStdOptional; return true; } return false; @@ -149,26 +147,10 @@ void PM_FRAME_QUERY::GatherFromFrameMetrics_(const GatherCommand_& cmd, uint8_t* } // Write frame metric into the blob, preserving optional<...> semantics. - // For optional, nullopt maps to NaN for downstream compatibility. + // Missing migrated frame metrics are already encoded as NaN in FrameMetrics. const auto WriteValue = [&]() { auto& blobValue = *reinterpret_cast(pBlobBytes + cmd.blobOffset); - if (!cmd.isOptional) { - blobValue = *reinterpret_cast(pFrameMemberBytes); - } - else { - const auto& optValue = *reinterpret_cast*>(pFrameMemberBytes); - if (optValue) { - blobValue = *optValue; - } - else { - if constexpr (std::is_same_v) { - blobValue = std::numeric_limits::quiet_NaN(); - } - else { - blobValue = T{}; - } - } - } + blobValue = *reinterpret_cast(pFrameMemberBytes); // Additional display-metric specific logic if the value is zero and not-dropped if constexpr (std::is_same_v) { diff --git a/IntelPresentMon/PresentMonMiddleware/FrameEventQuery.h b/IntelPresentMon/PresentMonMiddleware/FrameEventQuery.h index 99bf042d..42fcb60f 100644 --- a/IntelPresentMon/PresentMonMiddleware/FrameEventQuery.h +++ b/IntelPresentMon/PresentMonMiddleware/FrameEventQuery.h @@ -49,8 +49,6 @@ struct PM_FRAME_QUERY uint32_t frameMetricsOffset = 0; uint32_t deviceId = 0; uint32_t arrayIdx = 0; - // indicates whether the source data is gatherType or optional - bool isOptional = false; bool isStatic = false; }; // functions diff --git a/IntelPresentMon/UnitTests/MetricsCore.cpp b/IntelPresentMon/UnitTests/MetricsCore.cpp index 6d67a0be..895f4c66 100644 --- a/IntelPresentMon/UnitTests/MetricsCore.cpp +++ b/IntelPresentMon/UnitTests/MetricsCore.cpp @@ -15,6 +15,14 @@ using namespace pmon::util; namespace MetricsCoreTests { + namespace + { + bool HasMetricValue(double value) + { + return !IsMissingFrameMetricValue(value); + } + } + // ============================================================================ // SECTION 1: Core Types & Foundation // ============================================================================ @@ -1095,6 +1103,67 @@ TEST_CLASS(ComputeMetricsForPresentTests) Assert::AreEqual(uint64_t(1'500), chain.lastDisplayedAppScreenTime); } + TEST_METHOD(UpdateAfterPresent_AnimationSource_AppProvider_BothPresent_RemainsAppProvider) + { + SwapChainCoreState chain{}; + chain.animationErrorSource = AnimationErrorSource::AppProvider; + chain.firstAppSimStartTime = 40'000; + chain.lastDisplayedSimStartTime = 41'000; + chain.lastDisplayedAppScreenTime = 8'800; + + auto frame = MakeFrame(PresentResult::Presented, 1'100, 55, 1'300, + { { FrameType::Application, 1'650 } }, + 10'000 /* appSimStartTime */, 12'000 /* pclSimStart */); + + chain.UpdateAfterPresent(frame); + + Assert::IsTrue(chain.animationErrorSource == AnimationErrorSource::AppProvider); + Assert::AreEqual(uint64_t(40'000), chain.firstAppSimStartTime); + Assert::AreEqual(uint64_t(10'000), chain.lastDisplayedSimStartTime); + Assert::IsTrue(chain.lastDisplayedSimStartTime != uint64_t(12'000)); + Assert::AreEqual(uint64_t(1'650), chain.lastDisplayedAppScreenTime); + } + + TEST_METHOD(UpdateAfterPresent_AnimationSource_AppProvider_MissingApp_NoPcl_LeavesAnchorsUnchanged) + { + SwapChainCoreState chain{}; + chain.animationErrorSource = AnimationErrorSource::AppProvider; + chain.firstAppSimStartTime = 40'000; + chain.lastDisplayedSimStartTime = 41'000; + chain.lastDisplayedAppScreenTime = 8'800; + + auto frame = MakeFrame(PresentResult::Presented, 2'000, 40, 2'300, + { { FrameType::Application, 9'950 } }, + 0 /* appSimStartTime */, 0 /* pclSimStart */); + + chain.UpdateAfterPresent(frame); + + Assert::IsTrue(chain.animationErrorSource == AnimationErrorSource::AppProvider); + Assert::AreEqual(uint64_t(40'000), chain.firstAppSimStartTime); + Assert::AreEqual(uint64_t(41'000), chain.lastDisplayedSimStartTime); + Assert::AreEqual(uint64_t(8'800), chain.lastDisplayedAppScreenTime); + } + + TEST_METHOD(UpdateAfterPresent_AnimationSource_AppProvider_MissingApp_WithPcl_LeavesAnchorsUnchanged) + { + SwapChainCoreState chain{}; + chain.animationErrorSource = AnimationErrorSource::AppProvider; + chain.firstAppSimStartTime = 40'000; + chain.lastDisplayedSimStartTime = 41'000; + chain.lastDisplayedAppScreenTime = 8'800; + + auto frame = MakeFrame(PresentResult::Presented, 2'100, 40, 2'400, + { { FrameType::Application, 9'960 } }, + 0 /* appSimStartTime */, 52'000 /* pclSimStart */); + + chain.UpdateAfterPresent(frame); + + Assert::IsTrue(chain.animationErrorSource == AnimationErrorSource::AppProvider); + Assert::AreEqual(uint64_t(40'000), chain.firstAppSimStartTime); + Assert::AreEqual(uint64_t(41'000), chain.lastDisplayedSimStartTime); + Assert::AreEqual(uint64_t(8'800), chain.lastDisplayedAppScreenTime); + } + TEST_METHOD(UpdateAfterPresent_AnimationSource_PCLatency_UpdatesSimStartAndFirstAppSim) { SwapChainCoreState chain{}; @@ -1111,6 +1180,30 @@ TEST_CLASS(ComputeMetricsForPresentTests) Assert::AreEqual(uint64_t(2'700), chain.lastDisplayedAppScreenTime); } + TEST_METHOD(UpdateAfterPresent_AnimationSource_PCLatency_MissingPcl_NoApp_LeavesAnchorsUnchanged) + { + SwapChainCoreState chain{}; + chain.animationErrorSource = AnimationErrorSource::PCLatency; + chain.firstAppSimStartTime = 30'000; + chain.lastDisplayedSimStartTime = 31'000; + chain.lastDisplayedAppScreenTime = 8'800; + + FrameData previousApp = MakeFrame(PresentResult::Presented, 5'000, 80, 5'300, + { { FrameType::Application, 5'800 } }); + chain.lastAppPresent = previousApp; + + auto frame = MakeFrame(PresentResult::Presented, 9'000, 90, 9'400, + { { FrameType::Application, 9'950 } }, + 0 /* appSimStartTime */, 0 /* pclSimStart */); + + chain.UpdateAfterPresent(frame); + + Assert::IsTrue(chain.animationErrorSource == AnimationErrorSource::PCLatency); + Assert::AreEqual(uint64_t(30'000), chain.firstAppSimStartTime); + Assert::AreEqual(uint64_t(31'000), chain.lastDisplayedSimStartTime); + Assert::AreEqual(uint64_t(8'800), chain.lastDisplayedAppScreenTime); + } + TEST_METHOD(UpdateAfterPresent_AnimationSource_CpuStart_FallbackToPreviousAppPresent) { SwapChainCoreState chain{}; @@ -1149,6 +1242,24 @@ TEST_CLASS(ComputeMetricsForPresentTests) Assert::AreEqual(uint64_t(20'000), chain.firstAppSimStartTime); } + TEST_METHOD(UpdateAfterPresent_AnimationSource_CpuStart_BothPresent_TransitionsDirectlyToAppProvider) + { + SwapChainCoreState chain{}; + chain.animationErrorSource = AnimationErrorSource::CpuStart; + chain.firstAppSimStartTime = 15'000; + chain.lastDisplayedSimStartTime = 15'000; + + auto frame = MakeFrame(PresentResult::Presented, 7'500, 75, 7'900, + { { FrameType::Application, 8'300 } }, + 20'000 /* appSimStartTime */, 30'000 /* pclSimStart */); + + chain.UpdateAfterPresent(frame); + + Assert::IsTrue(chain.animationErrorSource == AnimationErrorSource::AppProvider); + Assert::AreEqual(uint64_t(20'000), chain.lastDisplayedSimStartTime); + Assert::AreEqual(uint64_t(20'000), chain.firstAppSimStartTime); + } + TEST_METHOD(UpdateAfterPresent_AnimationSource_CpuStart_TransitionsToPCLatency) { SwapChainCoreState chain{}; @@ -1164,6 +1275,46 @@ TEST_CLASS(ComputeMetricsForPresentTests) Assert::AreEqual(uint64_t(30'000), chain.lastDisplayedSimStartTime); Assert::AreEqual(uint64_t(30'000), chain.firstAppSimStartTime); } + + TEST_METHOD(UpdateAfterPresent_AnimationSource_PCLatency_TransitionsToAppProvider_WhenAppOnly) + { + SwapChainCoreState chain{}; + chain.animationErrorSource = AnimationErrorSource::PCLatency; + chain.firstAppSimStartTime = 30'000; + chain.lastDisplayedSimStartTime = 31'000; + chain.lastDisplayedAppScreenTime = 8'800; + + auto frame = MakeFrame(PresentResult::Presented, 9'000, 90, 9'400, + { { FrameType::Application, 9'950 } }, + 40'000 /* appSimStartTime */, 0 /* pclSimStart */); + + chain.UpdateAfterPresent(frame); + + Assert::IsTrue(chain.animationErrorSource == AnimationErrorSource::AppProvider); + Assert::AreEqual(uint64_t(40'000), chain.firstAppSimStartTime); + Assert::AreEqual(uint64_t(40'000), chain.lastDisplayedSimStartTime); + Assert::AreEqual(uint64_t(9'950), chain.lastDisplayedAppScreenTime); + } + + TEST_METHOD(UpdateAfterPresent_AnimationSource_PCLatency_TransitionsToAppProvider_WhenAppAndPclBothPresent) + { + SwapChainCoreState chain{}; + chain.animationErrorSource = AnimationErrorSource::PCLatency; + chain.firstAppSimStartTime = 30'000; + chain.lastDisplayedSimStartTime = 31'000; + chain.lastDisplayedAppScreenTime = 8'800; + + auto frame = MakeFrame(PresentResult::Presented, 10'000, 100, 10'400, + { { FrameType::Application, 10'950 } }, + 40'000 /* appSimStartTime */, 50'000 /* pclSimStart */); + + chain.UpdateAfterPresent(frame); + + Assert::IsTrue(chain.animationErrorSource == AnimationErrorSource::AppProvider); + Assert::AreEqual(uint64_t(40'000), chain.firstAppSimStartTime); + Assert::AreEqual(uint64_t(40'000), chain.lastDisplayedSimStartTime); + Assert::AreEqual(uint64_t(10'950), chain.lastDisplayedAppScreenTime); + } }; TEST_CLASS(UpdateAfterPresentFlipDelayTests) @@ -1986,9 +2137,8 @@ TEST_CLASS(ComputeMetricsForPresentTests) Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; - if (m.msFlipDelay.has_value()) { - Assert::AreEqual(0.0, m.msFlipDelay.value(), 0.0001); - } + Assert::IsFalse(HasMetricValue(m.msFlipDelay), + L"msFlipDelay should be missing for a non-displayed frame."); } TEST_METHOD(Displayed_WithFlipDelay_ReturnsFlipDelayInMs) @@ -2012,9 +2162,9 @@ TEST_CLASS(ComputeMetricsForPresentTests) Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; - if (m.msFlipDelay.has_value()) { + if (HasMetricValue(m.msFlipDelay)) { double expected = qpc.DurationMilliSeconds(100'000); - Assert::AreEqual(expected, m.msFlipDelay.value(), 0.0001); + Assert::AreEqual(expected, m.msFlipDelay, 0.0001); } } @@ -2039,9 +2189,8 @@ TEST_CLASS(ComputeMetricsForPresentTests) Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; - if (m.msFlipDelay.has_value()) { - Assert::AreEqual(0.0, m.msFlipDelay.value(), 0.0001); - } + Assert::IsFalse(HasMetricValue(m.msFlipDelay), + L"msFlipDelay should be missing when flipDelay is zero."); } TEST_METHOD(DisplayedWithGeneratedFrame_AlsoIncludesFlipDelay) @@ -2065,9 +2214,9 @@ TEST_CLASS(ComputeMetricsForPresentTests) Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; - if (m.msFlipDelay.has_value()) { + if (HasMetricValue(m.msFlipDelay)) { double expected = qpc.DurationMilliSeconds(50'000); - Assert::AreEqual(expected, m.msFlipDelay.value(), 0.0001); + Assert::AreEqual(expected, m.msFlipDelay, 0.0001); } } }; @@ -2224,10 +2373,10 @@ TEST_CLASS(ComputeMetricsForPresentTests) uint64_t expectedEffectiveFlipDelaySecond = 100'000 + (5'500'000 - 5'000'000); double expectedMsFlipDelaySecond = qpc.DurationMilliSeconds(expectedEffectiveFlipDelaySecond); - Assert::IsTrue(secondMetrics.msFlipDelay.has_value(), + Assert::IsTrue(HasMetricValue(secondMetrics.msFlipDelay), L"msFlipDelay should be set for displayed frame"); - if (secondMetrics.msFlipDelay.has_value()) { - Assert::AreEqual(expectedMsFlipDelaySecond, secondMetrics.msFlipDelay.value(), 0.0001, + if (HasMetricValue(secondMetrics.msFlipDelay)) { + Assert::AreEqual(expectedMsFlipDelaySecond, secondMetrics.msFlipDelay, 0.0001, L"NV2 should adjust second's flipDelay to account for screenTime catch-up"); } } @@ -2269,10 +2418,10 @@ TEST_CLASS(ComputeMetricsForPresentTests) // No adjustment to flipDelay: should use original 75'000 double expectedMsFlipDelay = qpc.DurationMilliSeconds(75'000); - Assert::IsTrue(metrics.msFlipDelay.has_value(), + Assert::IsTrue(HasMetricValue(metrics.msFlipDelay), L"msFlipDelay should be set for displayed frame"); - if (metrics.msFlipDelay.has_value()) { - Assert::AreEqual(expectedMsFlipDelay, metrics.msFlipDelay.value(), 0.0001, + if (HasMetricValue(metrics.msFlipDelay)) { + Assert::AreEqual(expectedMsFlipDelay, metrics.msFlipDelay, 0.0001, L"No collapse: flipDelay should remain at original value"); } } @@ -2324,10 +2473,10 @@ TEST_CLASS(ComputeMetricsForPresentTests) // flipDelay should remain at original 50'000 double expectedMsFlipDelay = qpc.DurationMilliSeconds(50'000); - Assert::IsTrue(secondMetrics.msFlipDelay.has_value(), + Assert::IsTrue(HasMetricValue(secondMetrics.msFlipDelay), L"msFlipDelay should be set for displayed frame"); - if (secondMetrics.msFlipDelay.has_value()) { - Assert::AreEqual(expectedMsFlipDelay, secondMetrics.msFlipDelay.value(), 0.0001, + if (HasMetricValue(secondMetrics.msFlipDelay)) { + Assert::AreEqual(expectedMsFlipDelay, secondMetrics.msFlipDelay, 0.0001, L"NV2: when no collapse, flipDelay should remain unchanged"); } } @@ -2361,9 +2510,9 @@ TEST_CLASS(ComputeMetricsForPresentTests) L"NV1 should adjust current screenTime to lastDisplayedScreenTime"); const uint64_t expectedFlipDelay = 100'000 + (5'500'000 - 5'000'000); - Assert::IsTrue(m.msFlipDelay.has_value(), L"msFlipDelay should be set for displayed frame"); - if (m.msFlipDelay.has_value()) { - Assert::AreEqual(qpc.DurationMilliSeconds(expectedFlipDelay), m.msFlipDelay.value(), 0.0001, + Assert::IsTrue(HasMetricValue(m.msFlipDelay), L"msFlipDelay should be set for displayed frame"); + if (HasMetricValue(m.msFlipDelay)) { + Assert::AreEqual(qpc.DurationMilliSeconds(expectedFlipDelay), m.msFlipDelay, 0.0001, L"NV1 should adjust current flipDelay to account for screenTime catch-up"); } @@ -2535,8 +2684,8 @@ TEST_CLASS(ComputeMetricsForPresentTests) // msReadyTimeToDisplayLatency = screenTime - readyTime = 2'000'000 - 1'500'000 = 500'000 ticks = 0.05 ms double expected = qpc.DeltaUnsignedMilliSeconds(1'500'000, 2'000'000); - Assert::IsTrue(m.msReadyTimeToDisplayLatency.has_value()); - Assert::AreEqual(expected, m.msReadyTimeToDisplayLatency.value(), 0.0001); + Assert::IsTrue(HasMetricValue(m.msReadyTimeToDisplayLatency)); + Assert::AreEqual(expected, m.msReadyTimeToDisplayLatency, 0.0001); } TEST_METHOD(ReadyTimeToDisplay_ReadyTimeEqualsScreenTime) @@ -2563,14 +2712,14 @@ TEST_CLASS(ComputeMetricsForPresentTests) Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; - Assert::IsTrue(m.msReadyTimeToDisplayLatency.has_value()); - Assert::AreEqual(0.0, m.msReadyTimeToDisplayLatency.value(), 0.0001); + Assert::IsTrue(HasMetricValue(m.msReadyTimeToDisplayLatency)); + Assert::AreEqual(0.0, m.msReadyTimeToDisplayLatency, 0.0001); } TEST_METHOD(ReadyTimeToDisplay_NotDisplayed_ReturnsZero) { // Scenario: Frame with no displayed entries. - // Expected: msReadyTimeToDisplayLatency = 0.0 ms + // Expected: msReadyTimeToDisplayLatency is missing. QpcConverter qpc(10'000'000, 0); SwapChainCoreState chain{}; @@ -2586,14 +2735,14 @@ TEST_CLASS(ComputeMetricsForPresentTests) Assert::AreEqual(size_t(1), results.size()); const auto& m = results[0].metrics; - Assert::IsFalse(m.msReadyTimeToDisplayLatency.has_value()); + Assert::IsFalse(HasMetricValue(m.msReadyTimeToDisplayLatency)); } TEST_METHOD(ReadyTimeToDisplay_ReadyTimeZero) { - // Scenario: Ready time not set (edge case, readyTime = 0). + // Scenario: Ready time is set to a non-zero value before screen time. // readyTime = 70'000, screenTime = 2'000'000 - // Expected: msReadyTimeToDisplayLatency ≈ 0.2 ms (2'000'000 ticks) + // Expected: msReadyTimeToDisplayLatency ≈ 0.193 ms QpcConverter qpc(10'000'000, 0); SwapChainCoreState chain{}; @@ -2615,8 +2764,8 @@ TEST_CLASS(ComputeMetricsForPresentTests) // msReadyTimeToDisplayLatency = 2'000'000 - 70'000 = 1'930'000 ticks = 0.193 ms double expected = qpc.DeltaUnsignedMilliSeconds(70'000, 2'000'000); - Assert::IsTrue(m.msReadyTimeToDisplayLatency.has_value()); - Assert::AreEqual(expected, m.msReadyTimeToDisplayLatency.value(), 0.0001); + Assert::IsTrue(HasMetricValue(m.msReadyTimeToDisplayLatency)); + Assert::AreEqual(expected, m.msReadyTimeToDisplayLatency, 0.0001); } }; @@ -2717,20 +2866,20 @@ TEST_CLASS(ComputeMetricsForPresentTests) // 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(results[0].metrics.msReadyTimeToDisplayLatency.has_value()); - Assert::AreEqual(expected0, results[0].metrics.msReadyTimeToDisplayLatency.value(), 0.0001); + Assert::IsTrue(HasMetricValue(results[0].metrics.msReadyTimeToDisplayLatency)); + Assert::AreEqual(expected0, results[0].metrics.msReadyTimeToDisplayLatency, 0.0001); // 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(results[1].metrics.msReadyTimeToDisplayLatency.has_value()); - Assert::AreEqual(expected1, results[1].metrics.msReadyTimeToDisplayLatency.value(), 0.0001); + Assert::IsTrue(HasMetricValue(results[1].metrics.msReadyTimeToDisplayLatency)); + Assert::AreEqual(expected1, results[1].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(results2[0].metrics.msReadyTimeToDisplayLatency.has_value()); - Assert::AreEqual(expected2, results2[0].metrics.msReadyTimeToDisplayLatency.value(), 0.0001); + Assert::IsTrue(HasMetricValue(results2[0].metrics.msReadyTimeToDisplayLatency)); + Assert::AreEqual(expected2, results2[0].metrics.msReadyTimeToDisplayLatency, 0.0001); } }; @@ -2788,8 +2937,8 @@ TEST_CLASS(ComputeMetricsForPresentTests) double expectedDisplayLatency = qpc.DeltaUnsignedMilliSeconds(1'000'000, 4'000'000); Assert::AreEqual(expectedDisplayLatency, results1[0].metrics.msDisplayLatency, 0.0001); double expectedFlipDelay = qpc.DurationMilliSeconds(frame.flipDelay); - Assert::IsTrue(results1[0].metrics.msFlipDelay.has_value()); - Assert::AreEqual(expectedFlipDelay, results1[0].metrics.msFlipDelay.value(), 0.0001); + Assert::IsTrue(HasMetricValue(results1[0].metrics.msFlipDelay)); + Assert::AreEqual(expectedFlipDelay, results1[0].metrics.msFlipDelay, 0.0001); auto results2 = ComputeMetricsForPresent(qpc, next1, &next2, chain); Assert::AreEqual(size_t(1), results1.size()); @@ -2800,8 +2949,8 @@ TEST_CLASS(ComputeMetricsForPresentTests) double expectedDisplayLatency2 = qpc.DeltaUnsignedMilliSeconds(1'050'000, 4'000'000); Assert::AreEqual(expectedDisplayLatency2, results2[0].metrics.msDisplayLatency, 0.0001); double expectedFlipDelay2 = qpc.DurationMilliSeconds(1'030'000); - Assert::IsTrue(results2[0].metrics.msFlipDelay.has_value()); - Assert::AreEqual(expectedFlipDelay2, results2[0].metrics.msFlipDelay.value(), 0.0001); + Assert::IsTrue(HasMetricValue(results2[0].metrics.msFlipDelay)); + Assert::AreEqual(expectedFlipDelay2, results2[0].metrics.msFlipDelay, 0.0001); } TEST_METHOD(ReadyTimeToDisplay_NvCollapsed_UsesAdjustedScreenTime) @@ -2850,16 +2999,16 @@ TEST_CLASS(ComputeMetricsForPresentTests) // No adjust of first frame ready time = 4'000'000 - 1'100'000 = 2'900'000 ticks = 0.29 ms double expectedReadyTimeLatency = qpc.DeltaUnsignedMilliSeconds(1'100'000, 4'000'000); - Assert::IsTrue(results1[0].metrics.msReadyTimeToDisplayLatency.has_value()); - Assert::AreEqual(expectedReadyTimeLatency, results1[0].metrics.msReadyTimeToDisplayLatency.value(), 0.0001); + Assert::IsTrue(HasMetricValue(results1[0].metrics.msReadyTimeToDisplayLatency)); + Assert::AreEqual(expectedReadyTimeLatency, results1[0].metrics.msReadyTimeToDisplayLatency, 0.0001); auto results2 = ComputeMetricsForPresent(qpc, next1, &next2, chain); Assert::AreEqual(size_t(1), results1.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); - Assert::IsTrue(results2[0].metrics.msReadyTimeToDisplayLatency.has_value()); - Assert::AreEqual(expectedReadyTimeLatency2, results2[0].metrics.msReadyTimeToDisplayLatency.value(), 0.0001); + Assert::IsTrue(HasMetricValue(results2[0].metrics.msReadyTimeToDisplayLatency)); + Assert::AreEqual(expectedReadyTimeLatency2, results2[0].metrics.msReadyTimeToDisplayLatency, 0.0001); } }; @@ -4469,19 +4618,21 @@ TEST_CLASS(ComputeMetricsForPresentTests) { public: // ======================================================================== - // A1: AnimationTime_AppProvider_FirstFrame_ZeroWithoutAppSimStartTime + // A1: AnimationTime_CpuStart_FirstFrame_ZeroWithoutAppSimStartTime // ======================================================================== - TEST_METHOD(AnimationTime_AppProvider_FirstFrame_ZeroWithoutAppSimStartTime) + TEST_METHOD(AnimationTime_CpuStart_FirstFrame_ZeroWithoutAppSimStartTime) { // Scenario: // - SwapChainCoreState starts with CpuStart (default) - // - Current frame: displayed, displayIndex == appIndex, but appSimStartTime == 0 - // - No app data means source stays CpuStart + // - Current frame: displayed with appSimStartTime == 0 and pclSimStartTime == 0 + // - The existing body keeps CpuStart active when appSimStartTime is missing // // Expected Outcome: - // - msAnimationTime = std::nullopt (no valid sim start time, no history) + // - 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(10'000'000, 0); SwapChainCoreState state{}; @@ -4522,9 +4673,9 @@ TEST_CLASS(ComputeMetricsForPresentTests) const ComputedMetrics& result = metricsVector[0]; // Assert: msAnimationTime should have value a value of zero - Assert::IsTrue(result.metrics.msAnimationTime.has_value(), + Assert::IsTrue(HasMetricValue(result.metrics.msAnimationTime), L"msAnimationTime should have a value of zero"); - Assert::AreEqual(double(0.0), result.metrics.msAnimationTime.value()); + Assert::AreEqual(double(0.0), result.metrics.msAnimationTime, 0.0001); // Assert: firstAppSimStartTime in state should remain 0 Assert::AreEqual(uint64_t(0), state.firstAppSimStartTime, @@ -4579,9 +4730,9 @@ TEST_CLASS(ComputeMetricsForPresentTests) Assert::AreEqual(size_t(1), metricsVector.size()); const ComputedMetrics& result = metricsVector[0]; - Assert::IsTrue(result.metrics.msAnimationTime.has_value(), + Assert::IsTrue(HasMetricValue(result.metrics.msAnimationTime), L"msAnimationTime should have a value"); - Assert::AreEqual(double(0.0), result.metrics.msAnimationTime.value(), + Assert::AreEqual(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 @@ -4652,9 +4803,9 @@ TEST_CLASS(ComputeMetricsForPresentTests) const ComputedMetrics& result = metricsVector[0]; // Assert: msAnimationTime should be 0.005 ms - Assert::IsTrue(result.metrics.msAnimationTime.has_value()); + Assert::IsTrue(HasMetricValue(result.metrics.msAnimationTime)); double expectedMs = qpc.DeltaUnsignedMilliSeconds(100, 150); - Assert::AreEqual(expectedMs, result.metrics.msAnimationTime.value(), 0.0001, + Assert::AreEqual(expectedMs, result.metrics.msAnimationTime, 0.0001, L"msAnimationTime should reflect elapsed time from first app sim start"); // Assert: firstAppSimStartTime unchanged @@ -4714,9 +4865,9 @@ TEST_CLASS(ComputeMetricsForPresentTests) Assert::AreEqual(size_t(1), metrics1.size()); Assert::IsTrue( - metrics1[0].metrics.msAnimationTime.has_value(), + HasMetricValue(metrics1[0].metrics.msAnimationTime), L"First AppProvider frame should seed firstAppSimStartTime and animation time should be zero"); - Assert::AreEqual(double(0.0), metrics1[0].metrics.msAnimationTime.value(), + Assert::AreEqual(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 @@ -4749,13 +4900,13 @@ TEST_CLASS(ComputeMetricsForPresentTests) auto metrics2 = ComputeMetricsForPresent(qpc, frame2, &next2, state); Assert::AreEqual(size_t(1), metrics2.size()); Assert::IsTrue( - metrics2[0].metrics.msAnimationTime.has_value(), + HasMetricValue(metrics2[0].metrics.msAnimationTime), L"Second displayed app frame should report msAnimationTime."); double expected2 = qpc.DeltaUnsignedMilliSeconds(100, 150); Assert::AreEqual( expected2, - metrics2[0].metrics.msAnimationTime.value(), + metrics2[0].metrics.msAnimationTime, 0.0001, L"Frame 2's msAnimationTime should be relative to firstAppSimStartTime (100 → 150)."); @@ -4786,13 +4937,13 @@ TEST_CLASS(ComputeMetricsForPresentTests) auto metrics3 = ComputeMetricsForPresent(qpc, frame3, &next3, state); Assert::AreEqual(size_t(1), metrics3.size()); Assert::IsTrue( - metrics3[0].metrics.msAnimationTime.has_value(), + HasMetricValue(metrics3[0].metrics.msAnimationTime), L"Third displayed app frame should report msAnimationTime."); double expected3 = qpc.DeltaUnsignedMilliSeconds(100, 250); Assert::AreEqual( expected3, - metrics3[0].metrics.msAnimationTime.value(), + metrics3[0].metrics.msAnimationTime, 0.0001, L"Frame 3's msAnimationTime should be relative to original firstAppSimStartTime (100 → 250)."); @@ -4840,9 +4991,9 @@ TEST_CLASS(ComputeMetricsForPresentTests) const auto& droppedMetrics = droppedResults[0].metrics; // Discarded / not-displayed frame must NOT produce animation metrics. - Assert::IsFalse(droppedMetrics.msAnimationTime.has_value(), + Assert::IsFalse(HasMetricValue(droppedMetrics.msAnimationTime), L"Discarded frame should not have msAnimationTime"); - Assert::IsFalse(droppedMetrics.msAnimationError.has_value(), + Assert::IsFalse(HasMetricValue(droppedMetrics.msAnimationError), L"Discarded frame should not have msAnimationError"); // And it must NOT disturb the animation anchors from prior displayed frames. @@ -4878,13 +5029,13 @@ TEST_CLASS(ComputeMetricsForPresentTests) Assert::AreEqual(size_t(1), displayedResults.size()); const auto& displayedMetrics = displayedResults[0].metrics; - Assert::IsTrue(displayedMetrics.msAnimationTime.has_value(), + 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); - Assert::AreEqual(expected, displayedMetrics.msAnimationTime.value(), 0.0001); + Assert::AreEqual(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. @@ -4896,19 +5047,143 @@ TEST_CLASS(ComputeMetricsForPresentTests) L"lastDisplayedAppScreenTime should track the most recent displayed screen time"); } // ======================================================================== - // B1: AnimationTime_PCLatency_FirstFrame_ZeroWithoutPclSimStartTime + // A6: AnimationTime_AppProvider_MissingApp_NoPcl_IsMissingAndStateUnchanged // ======================================================================== - TEST_METHOD(AnimationTime_PCLatency_FirstFrame_ZeroWithoutPclSimStartTime) + 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. + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + state.animationErrorSource = AnimationErrorSource::AppProvider; + state.firstAppSimStartTime = 40'000; + state.lastDisplayedSimStartTime = 41'000; + state.lastDisplayedAppScreenTime = 8'800; + + 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 }); + + 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 metricsVector = ComputeMetricsForPresent(qpc, frame, &next, state); + + Assert::AreEqual(size_t(1), metricsVector.size()); + + const ComputedMetrics& result = metricsVector[0]; + + 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)"); + + 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. + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + state.animationErrorSource = AnimationErrorSource::AppProvider; + state.firstAppSimStartTime = 40'000; + state.lastDisplayedSimStartTime = 41'000; + state.lastDisplayedAppScreenTime = 8'800; + + 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 }); + + 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 metricsVector = ComputeMetricsForPresent(qpc, frame, &next, state); + + Assert::AreEqual(size_t(1), metricsVector.size()); + + const ComputedMetrics& result = metricsVector[0]; + + 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."); + + 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."); + } + // ======================================================================== + // B1: AnimationTime_CpuStart_FirstFrame_ZeroWithoutPclSimStartTime + // ======================================================================== + TEST_METHOD(AnimationTime_CpuStart_FirstFrame_ZeroWithoutPclSimStartTime) { // Scenario: // - SwapChainCoreState starts with CpuStart (default) - // - Current frame: displayed, displayIndex == appIndex, but pclSimStartTime == 0 - // - No PCL data means source stays CpuStart + // - Current frame: displayed with pclSimStartTime == 0 and appSimStartTime == 0 + // - The existing body keeps CpuStart active when pclSimStartTime is missing // // Expected Outcome: - // - msAnimationTime = std::nullopt (no valid sim start time, no history) + // - 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(10'000'000, 0); SwapChainCoreState state{}; @@ -4949,9 +5224,9 @@ TEST_CLASS(ComputeMetricsForPresentTests) const ComputedMetrics& result = metricsVector[0]; // Assert: msAnimationTime should be zero - Assert::IsTrue(result.metrics.msAnimationTime.has_value(), + Assert::IsTrue(HasMetricValue(result.metrics.msAnimationTime), L"msAnimationTime should be 0 whentransitioning"); - Assert::AreEqual(double(0.0), result.metrics.msAnimationTime.value()); + Assert::AreEqual(double(0.0), result.metrics.msAnimationTime, 0.0001); // Assert: firstAppSimStartTime in state should remain 0 Assert::AreEqual(uint64_t(0), state.firstAppSimStartTime, @@ -5007,7 +5282,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) const ComputedMetrics& result = metricsVector[0]; // Assert: msAnimationTime should have a value of zero - Assert::IsTrue(result.metrics.msAnimationTime.has_value(), + Assert::IsTrue(HasMetricValue(result.metrics.msAnimationTime), L"msAnimationTime should have a value"); // Assert: State should be updated @@ -5077,9 +5352,9 @@ TEST_CLASS(ComputeMetricsForPresentTests) const ComputedMetrics& result = metricsVector[0]; // Assert: msAnimationTime should be 0.01 ms - Assert::IsTrue(result.metrics.msAnimationTime.has_value()); + Assert::IsTrue(HasMetricValue(result.metrics.msAnimationTime)); double expectedMs = qpc.DeltaUnsignedMilliSeconds(100, 200); - Assert::AreEqual(expectedMs, result.metrics.msAnimationTime.value(), 0.0001, + Assert::AreEqual(expectedMs, result.metrics.msAnimationTime, 0.0001, L"msAnimationTime should reflect elapsed time from first pcl sim start"); // Assert: firstAppSimStartTime unchanged @@ -5141,7 +5416,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) // First displayed app frame seeds state; animation time is reported as zero. Assert::IsTrue( - metrics1[0].metrics.msAnimationTime.has_value(), + 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 @@ -5174,13 +5449,13 @@ TEST_CLASS(ComputeMetricsForPresentTests) auto metrics2 = ComputeMetricsForPresent(qpc, frame2, &next2, state); Assert::AreEqual(size_t(1), metrics2.size()); Assert::IsTrue( - metrics2[0].metrics.msAnimationTime.has_value(), + HasMetricValue(metrics1[0].metrics.msAnimationTime), L"Second displayed app frame should report msAnimationTime."); - + Assert::AreEqual(double(0.0), metrics1[0].metrics.msAnimationTime, 0.0001); double expected2 = qpc.DeltaUnsignedMilliSeconds(100, 150); Assert::AreEqual( expected2, - metrics2[0].metrics.msAnimationTime.value(), + metrics2[0].metrics.msAnimationTime, 0.0001, L"Frame 2's msAnimationTime should be relative to firstAppSimStartTime (100 → 150)."); @@ -5211,13 +5486,13 @@ TEST_CLASS(ComputeMetricsForPresentTests) auto metrics3 = ComputeMetricsForPresent(qpc, frame3, &next3, state); Assert::AreEqual(size_t(1), metrics3.size()); Assert::IsTrue( - metrics3[0].metrics.msAnimationTime.has_value(), + HasMetricValue(metrics3[0].metrics.msAnimationTime), L"Third displayed app frame should report msAnimationTime."); double expected3 = qpc.DeltaUnsignedMilliSeconds(100, 250); Assert::AreEqual( expected3, - metrics3[0].metrics.msAnimationTime.value(), + metrics3[0].metrics.msAnimationTime, 0.0001, L"Frame 3's msAnimationTime should be relative to original firstAppSimStartTime (100 → 250)."); @@ -5272,9 +5547,9 @@ TEST_CLASS(ComputeMetricsForPresentTests) // First displayed frame seeds animation state; animation time will be reported // still as we are transitioning to PCLatency. Assert::IsTrue( - metrics1[0].metrics.msAnimationTime.has_value(), + HasMetricValue(metrics1[0].metrics.msAnimationTime), L"Animation Time will be reported"); - Assert::AreEqual(double(0.0), metrics1[0].metrics.msAnimationTime.value()); + Assert::AreEqual(double(0.0), metrics1[0].metrics.msAnimationTime, 0.0001); Assert::AreEqual(uint64_t(100), chain.firstAppSimStartTime); Assert::AreEqual(uint64_t(100), chain.lastDisplayedSimStartTime); @@ -5295,7 +5570,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) // Not displayed → no animation time, and animation state should not advance Assert::IsFalse( - metrics2[0].metrics.msAnimationTime.has_value(), + HasMetricValue(metrics2[0].metrics.msAnimationTime), L"Non-displayed frame should not report animation time."); Assert::AreEqual(uint64_t(100), chain.firstAppSimStartTime, @@ -5324,13 +5599,13 @@ TEST_CLASS(ComputeMetricsForPresentTests) auto metrics3 = ComputeMetricsForPresent(qpc, frame3, &next3, chain); Assert::AreEqual(size_t(1), metrics3.size()); Assert::IsTrue( - metrics3[0].metrics.msAnimationTime.has_value(), + HasMetricValue(metrics3[0].metrics.msAnimationTime), L"Displayed frame with valid PCL sim start should report animation time."); double expected3 = qpc.DeltaUnsignedMilliSeconds(100, 300); Assert::AreEqual( expected3, - metrics3[0].metrics.msAnimationTime.value(), + metrics3[0].metrics.msAnimationTime, 0.0001, L"Frame 3's msAnimationTime should be measured from Frame 1's PCL sim start, skipping Frame 2."); @@ -5340,19 +5615,21 @@ TEST_CLASS(ComputeMetricsForPresentTests) L"lastDisplayedSimStartTime should advance to Frame 3's PCL sim start."); } // ======================================================================== - // B6: AnimationTime_PCLatency_FallsBackToCpuStart_WhenPclSimStartTimeZero + // B6: AnimationTime_PCLatency_MissingPclAndAppSimStart_NoDemotionOrFallback // ======================================================================== - TEST_METHOD(AnimationTime_PCLatency_FallsBackToCpuStart_WhenPclSimStartTimeZero) + TEST_METHOD(AnimationTime_PCLatency_MissingPclAndAppSimStart_NoDemotionOrFallback) { // Scenario: - // - Start with CpuStart source - // - Frame 1: PCL data establishes PCLatency source - // - Frame 2: pclSimStartTime = 0 (PCL data disappears) - // - Source should stay PCLatency (no fallback), msAnimationTime = nullopt + // - 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 = nullopt (PCL data missing) - // - animationErrorSource remains PCLatency (no fallback) + // - msAnimationTime is missing (NaN), not 0.0. + // - animationErrorSource remains PCLatency. + // - firstAppSimStartTime, lastDisplayedSimStartTime, and + // lastDisplayedAppScreenTime remain unchanged. QpcConverter qpc(10'000'000, 0); SwapChainCoreState state{}; @@ -5373,15 +5650,17 @@ TEST_CLASS(ComputeMetricsForPresentTests) 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: PCL data missing (pclSimStartTime = 0) + // 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; // PCL data disappeared + frame.pclSimStartTime = 0; frame.finalState = PresentResult::Presented; frame.displayed.PushBack({ FrameType::Application, 1'400'000 }); @@ -5401,19 +5680,145 @@ TEST_CLASS(ComputeMetricsForPresentTests) const ComputedMetrics& result = metricsVector[0]; - // Assert: msAnimationTime should be zero - Assert::IsTrue(result.metrics.msAnimationTime.has_value(), - L"msAnimationTime should be 0 whentransitioning"); - Assert::AreEqual(double(0.0), result.metrics.msAnimationTime.value()); + // 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 + // Assert: State should remain PCLatency with prior anchors intact. Assert::IsTrue(state.animationErrorSource == AnimationErrorSource::PCLatency, - L"animationErrorSource should remain PCLatency (no fallback)"); + L"animationErrorSource should remain PCLatency when no demotion is allowed."); - // Assert: firstAppSimStartTime unchanged Assert::AreEqual(uint64_t(100), state.firstAppSimStartTime, - L"firstAppSimStartTime should remain unchanged"); + 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."); + Assert::AreEqual(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."); + Assert::AreEqual(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 // ======================================================================== @@ -5467,9 +5872,9 @@ TEST_CLASS(ComputeMetricsForPresentTests) const ComputedMetrics& result = metricsVector[0]; - Assert::IsTrue(result.metrics.msAnimationTime.has_value(), + Assert::IsTrue(HasMetricValue(result.metrics.msAnimationTime), L"msAnimationTime should have a value"); - Assert::AreEqual(double(0.0), result.metrics.msAnimationTime.value(), + Assert::AreEqual(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, @@ -5536,9 +5941,9 @@ TEST_CLASS(ComputeMetricsForPresentTests) const ComputedMetrics& result = metricsVector[0]; // Assert: msAnimationTime should be 0 (first transition frame) - Assert::IsTrue(result.metrics.msAnimationTime.has_value(), + Assert::IsTrue(HasMetricValue(result.metrics.msAnimationTime), L"msAnimationTime should have a value on first valid CPU start"); - Assert::AreEqual(0.0, result.metrics.msAnimationTime.value(), 0.0001, + Assert::AreEqual(0.0, result.metrics.msAnimationTime, 0.0001, L"msAnimationTime should be 0 on first transition frame"); // Assert: State should be updated with CPU start @@ -5604,9 +6009,9 @@ TEST_CLASS(ComputeMetricsForPresentTests) Assert::AreEqual(size_t(1), metrics1.size()); const auto& m1 = metrics1[0].metrics; - Assert::IsTrue(m1.msAnimationTime.has_value(), + Assert::IsTrue(HasMetricValue(m1.msAnimationTime), L"CpuStart animation should report msAnimationTime even without App/PCL provider."); - const double anim1 = m1.msAnimationTime.value(); + 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."); @@ -5638,9 +6043,9 @@ TEST_CLASS(ComputeMetricsForPresentTests) Assert::AreEqual(size_t(1), metrics2.size()); const auto& m2 = metrics2[0].metrics; - Assert::IsTrue(m2.msAnimationTime.has_value(), + Assert::IsTrue(HasMetricValue(m2.msAnimationTime), L"Second CpuStart-driven frame should also report msAnimationTime."); - const double anim2 = m2.msAnimationTime.value(); + const double anim2 = m2.msAnimationTime; Assert::IsTrue(anim2 > anim1, L"CpuStart-based animation time should increase across frames as CpuStart advances."); @@ -5683,7 +6088,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) auto results = ComputeMetricsForPresent(qpc, present, &nextPresent, state); Assert::AreEqual(size_t(1), results.size()); - Assert::IsFalse(results[0].metrics.msAnimationError.has_value(), + Assert::IsFalse(HasMetricValue(results[0].metrics.msAnimationError), L"msAnimationError should be nullopt without prior displayed frame"); } @@ -5723,8 +6128,8 @@ TEST_CLASS(ComputeMetricsForPresentTests) auto results2 = ComputeMetricsForPresent(qpc, frame2, &frame3, state); Assert::AreEqual(size_t(1), results2.size()); - Assert::IsTrue(results2[0].metrics.msAnimationError.has_value()); - Assert::AreEqual(0.0, results2[0].metrics.msAnimationError.value(), 0.0001, + Assert::IsTrue(HasMetricValue(results2[0].metrics.msAnimationError)); + Assert::AreEqual(0.0, results2[0].metrics.msAnimationError, 0.0001, L"msAnimationError should be 0 when sim and display cadences match"); } @@ -5760,11 +6165,11 @@ TEST_CLASS(ComputeMetricsForPresentTests) frame3.displayed.PushBack({ FrameType::Application, 3000 }); auto results = ComputeMetricsForPresent(qpc, frame2, &frame3, state); - Assert::IsTrue(results[0].metrics.msAnimationError.has_value()); + 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 - Assert::AreEqual(expected, results[0].metrics.msAnimationError.value(), 0.0001); + Assert::AreEqual(expected, results[0].metrics.msAnimationError, 0.0001); } TEST_METHOD(AnimationError_AppProvider_TwoFrames_SimFasterThanDisplay) @@ -5799,11 +6204,11 @@ TEST_CLASS(ComputeMetricsForPresentTests) frame3.displayed.PushBack({ FrameType::Application, 3000 }); auto results = ComputeMetricsForPresent(qpc, frame2, &frame3, state); - Assert::IsTrue(results[0].metrics.msAnimationError.has_value()); + 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 - Assert::AreEqual(expected, results[0].metrics.msAnimationError.value(), 0.0001); + Assert::AreEqual(expected, results[0].metrics.msAnimationError, 0.0001); } TEST_METHOD(AnimationError_AppProvider_BackwardsSimStartTime_Nullopt) @@ -5838,7 +6243,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) frame3.displayed.PushBack({ FrameType::Application, 3000 }); auto results = ComputeMetricsForPresent(qpc, frame2, &frame3, state); - Assert::IsFalse(results[0].metrics.msAnimationError.has_value(), + Assert::IsFalse(HasMetricValue(results[0].metrics.msAnimationError), L"msAnimationError should be nullopt when sim start goes backward"); } @@ -5869,13 +6274,86 @@ TEST_CLASS(ComputeMetricsForPresentTests) 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(results[0].metrics.msAnimationError.has_value(), + 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); + + Assert::AreEqual(-0.001, appExpected, 0.0001, + L"Test setup should produce a distinct app-based animation error."); + Assert::AreEqual(0.003, pclExpected, 0.0001, + L"Test setup should produce a different hypothetical pcl-based animation error."); + Assert::AreEqual(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) @@ -5910,7 +6388,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) frame3.displayed.PushBack({ FrameType::Application, 3000 }); auto results = ComputeMetricsForPresent(qpc, frame2, &frame3, state); - Assert::IsFalse(results[0].metrics.msAnimationError.has_value()); + Assert::IsFalse(HasMetricValue(results[0].metrics.msAnimationError)); } // Section C: Animation Error – PCLatency Source @@ -5947,17 +6425,19 @@ TEST_CLASS(ComputeMetricsForPresentTests) frame3.displayed.PushBack({ FrameType::Application, 3000 }); auto results = ComputeMetricsForPresent(qpc, frame2, &frame3, state); - Assert::IsTrue(results[0].metrics.msAnimationError.has_value()); + 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 - Assert::AreEqual(expected, results[0].metrics.msAnimationError.value(), 0.0001); + Assert::AreEqual(expected, results[0].metrics.msAnimationError, 0.0001); } TEST_METHOD(AnimationError_PCLatency_CurrentPclSimStartZero_Nullopt) { // Scenario: PCL source, but current frame has pclSimStartTime = 0 - // Expected: msAnimationError = std::nullopt (no fallback to app in PCL mode) + // 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; @@ -5973,22 +6453,41 @@ TEST_CLASS(ComputeMetricsForPresentTests) frame2.presentStartTime = 2000; frame2.timeInPresent = 100; frame2.pclSimStartTime = 0; // PCL unavailable - frame2.appSimStartTime = 150; // app available, but should not be used + 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 }); - ComputeMetricsForPresent(qpc, frame1, &dummyNext, state); + 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::IsFalse(results[0].metrics.msAnimationError.has_value(), + 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) @@ -6012,104 +6511,100 @@ TEST_CLASS(ComputeMetricsForPresentTests) auto results = ComputeMetricsForPresent(qpc, present, &nextPresent, state); - Assert::IsFalse(results[0].metrics.msAnimationError.has_value(), + Assert::IsFalse(HasMetricValue(results[0].metrics.msAnimationError), L"msAnimationError should be nullopt on first valid PCL frame"); } - TEST_METHOD(AnimationError_PCLatency_TransitionFromAppToPcl_SourceSwitches) + TEST_METHOD(AnimationError_PCLatency_TransitionToAppProvider_AppOnly_Nullopt) { - // Scenario: Switching from CpuStart source to PCL - // Expected: Source auto-switches when PCL data arrives + // 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::CpuStart; - - // Frame 1: Establish baseline - FrameData frame1{}; - frame1.presentStartTime = 800; - frame1.timeInPresent = 100; // CPU end = 900 - frame1.finalState = PresentResult::Presented; - frame1.displayed.PushBack({ FrameType::Application, 1800 }); - - // Frame 2: Continue with CPU source - FrameData frame2{}; - frame2.presentStartTime = 1000; - frame2.timeInPresent = 100; // CPU end = 1100 - frame2.finalState = PresentResult::Presented; - frame2.displayed.PushBack({ FrameType::Application, 2000 }); - - // Frame 3: PCL becomes available, triggers source switch - FrameData frame3{}; - frame3.presentStartTime = 1200; - frame3.timeInPresent = 100; - frame3.pclSimStartTime = 150; // PCL data present - frame3.appSimStartTime = 150; - frame3.finalState = PresentResult::Presented; - frame3.displayed.PushBack({ FrameType::Application, 2100 }); + state.animationErrorSource = AnimationErrorSource::PCLatency; + state.firstAppSimStartTime = 300; + state.lastDisplayedSimStartTime = 320; + state.lastDisplayedAppScreenTime = 900'000; - // Frame 4: Next frame for completion - FrameData frame4{}; - frame4.presentStartTime = 1400; - frame4.timeInPresent = 100; - frame4.finalState = PresentResult::Presented; - frame4.displayed.PushBack({ FrameType::Application, 2200 }); + 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 }); - ComputeMetricsForPresent(qpc, frame1, &frame2, state); - ComputeMetricsForPresent(qpc, frame2, &frame3, state); + 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 }); - // Frame 3 processes with CpuStart source, then UpdateChain switches to PCLatency - auto results = ComputeMetricsForPresent(qpc, frame3, &frame4, state); + auto results = ComputeMetricsForPresent(qpc, frame, &next, state); - // Animation error computed using CPU start (source still CpuStart during calculation) - Assert::IsTrue(results[0].metrics.msAnimationError.has_value(), - L"Animation error should be computed with CPU start before source switch"); - - // After UpdateChain, source should have switched to PCLatency - Assert::IsTrue(state.animationErrorSource == AnimationErrorSource::PCLatency, - L"Source should auto-switch to PCLatency after UpdateChain"); + 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_SourcePriority_PclWinsOverApp) + TEST_METHOD(AnimationError_PCLatency_TransitionToAppProvider_BothPresent_Nullopt) { - // Scenario: Both PCL and App sim start present - // Expected: Use PCL (source priority) + // 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 frame1{}; - frame1.presentStartTime = 1000; - frame1.timeInPresent = 100; - frame1.pclSimStartTime = 100; - frame1.appSimStartTime = 200; // different value - frame1.finalState = PresentResult::Presented; - frame1.displayed.PushBack({ FrameType::Application, 1000 }); - - FrameData frame2{}; - frame2.presentStartTime = 2000; - frame2.timeInPresent = 100; - frame2.pclSimStartTime = 150; // PCL elapsed = 50 - frame2.appSimStartTime = 300; // app elapsed = 100 (should be ignored) - frame2.finalState = PresentResult::Presented; - frame2.displayed.PushBack({ FrameType::Application, 1050 }); // display elapsed = 50 + 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 dummyNext{}; - dummyNext.finalState = PresentResult::Presented; - dummyNext.displayed.PushBack({ FrameType::Application, 2000 }); - ComputeMetricsForPresent(qpc, frame1, &dummyNext, state); + 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 frame3{}; - frame3.finalState = PresentResult::Presented; - frame3.displayed.PushBack({ FrameType::Application, 3000 }); - auto results = ComputeMetricsForPresent(qpc, frame2, &frame3, state); + auto results = ComputeMetricsForPresent(qpc, frame, &next, state); - Assert::IsTrue(results[0].metrics.msAnimationError.has_value()); - // Should use PCL: sim=50, display=50, error=0 - double simElapsed = qpc.DeltaUnsignedMilliSeconds(100, 150); // PCL: 0.005 ms - double displayElapsed = qpc.DeltaUnsignedMilliSeconds(1000, 1050); - double expected = simElapsed - displayElapsed; // 0.0 ms - Assert::AreEqual(expected, results[0].metrics.msAnimationError.value(), 0.0001, - L"Should use PCL source, not app source"); + 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 @@ -6157,11 +6652,11 @@ TEST_CLASS(ComputeMetricsForPresentTests) frame4.displayed.PushBack({ FrameType::Application, 4000 }); auto results = ComputeMetricsForPresent(qpc, frame3, &frame4, state); - Assert::IsTrue(results[0].metrics.msAnimationError.has_value()); + 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 - Assert::AreEqual(expected, results[0].metrics.msAnimationError.value(), 0.0001); + Assert::AreEqual(expected, results[0].metrics.msAnimationError, 0.0001); } TEST_METHOD(AnimationError_CpuStart_Frame2DisplayIsGreaterThanFrame1Display) @@ -6197,7 +6692,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) ComputeMetricsForPresent(qpc, frame1, nullptr, state); auto results = ComputeMetricsForPresent(qpc, frame1, &frame2, state); - Assert::IsFalse(results[0].metrics.msAnimationError.has_value()); + Assert::IsFalse(HasMetricValue(results[0].metrics.msAnimationError)); } TEST_METHOD(AnimationError_CpuStart_TransitionToAppProvider_Nullopt) @@ -6231,12 +6726,63 @@ TEST_CLASS(ComputeMetricsForPresentTests) frame3.displayed.PushBack({ FrameType::Application, 4000 }); auto results = ComputeMetricsForPresent(qpc, frame2, &frame3, state); - Assert::IsFalse(results[0].metrics.msAnimationError.has_value(), + 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 }); + + 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) @@ -6263,16 +6809,16 @@ TEST_CLASS(ComputeMetricsForPresentTests) auto results = ComputeMetricsForPresent(qpc, present, &nextPresent, state); Assert::AreEqual(size_t(1), results.size()); - Assert::IsFalse(results[0].metrics.msAnimationError.has_value(), + Assert::IsFalse(HasMetricValue(results[0].metrics.msAnimationError), L"msAnimationError should be nullopt for non-app frames"); - Assert::IsFalse(results[0].metrics.msAnimationTime.has_value(), + Assert::IsFalse(HasMetricValue(results[0].metrics.msAnimationTime), L"msAnimationTime should be nullopt for non-app frames"); } - TEST_METHOD(AnimationError_FirstFrameEver_BothNullopt) + TEST_METHOD(AnimationError_FirstFrameEver_BothMissingMetric) { // Scenario: Very first frame, no prior state - // Expected: Both animation metrics = std::nullopt + // Expected: Both animation metrics = std::QuietNaN QpcConverter qpc(10'000'000, 0); SwapChainCoreState state{}; // all zeros state.animationErrorSource = AnimationErrorSource::AppProvider; @@ -6291,8 +6837,8 @@ TEST_CLASS(ComputeMetricsForPresentTests) auto results = ComputeMetricsForPresent(qpc, present, &nextPresent, state); - Assert::IsFalse(results[0].metrics.msAnimationError.has_value()); - Assert::IsTrue(results[0].metrics.msAnimationTime.has_value()); + Assert::IsFalse(HasMetricValue(results[0].metrics.msAnimationError)); + Assert::IsFalse(HasMetricValue(results[0].metrics.msAnimationTime)); } TEST_METHOD(AnimationError_BackwardsScreenTime_ErrorStillComputed) @@ -6327,7 +6873,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) frame3.displayed.PushBack({ FrameType::Application, 3000 }); auto results = ComputeMetricsForPresent(qpc, frame2, &frame3, state); - Assert::IsFalse(results[0].metrics.msAnimationError.has_value(), + Assert::IsFalse(HasMetricValue(results[0].metrics.msAnimationError), L"Error should be nullopt with backwards screen time"); } @@ -6363,11 +6909,11 @@ TEST_CLASS(ComputeMetricsForPresentTests) frame3.displayed.PushBack({ FrameType::Application, 3000 }); auto results = ComputeMetricsForPresent(qpc, frame2, &frame3, state); - Assert::IsTrue(results[0].metrics.msAnimationError.has_value()); + 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 - Assert::AreEqual(expected, results[0].metrics.msAnimationError.value(), 0.0001, + Assert::AreEqual(expected, results[0].metrics.msAnimationError, 0.0001, L"Large cadence mismatch should produce large positive error"); } @@ -6394,9 +6940,9 @@ TEST_CLASS(ComputeMetricsForPresentTests) auto results = ComputeMetricsForPresent(qpc, present, &nextPresent, state); - Assert::IsFalse(results[0].metrics.msAnimationError.has_value(), + Assert::IsFalse(HasMetricValue(results[0].metrics.msAnimationError), L"msAnimationError should be nullopt for Repeated frame type"); - Assert::IsFalse(results[0].metrics.msAnimationTime.has_value(), + Assert::IsFalse(HasMetricValue(results[0].metrics.msAnimationTime), L"msAnimationTime should be nullopt for Repeated frame type"); } @@ -6436,17 +6982,17 @@ TEST_CLASS(ComputeMetricsForPresentTests) Assert::AreEqual(size_t(2), resultsPartial.size()); // First display instance (Repeated) - no animation metrics - Assert::IsFalse(resultsPartial[0].metrics.msAnimationError.has_value(), + 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(resultsPartial[1].metrics.msAnimationError.has_value(), + 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; - Assert::AreEqual(expected, resultsPartial[1].metrics.msAnimationError.value(), 0.0001); + Assert::AreEqual(expected, resultsPartial[1].metrics.msAnimationError, 0.0001); } TEST_METHOD(Animation_AppProvider_PendingSequence_P1P2P3) { @@ -6525,11 +7071,11 @@ TEST_CLASS(ComputeMetricsForPresentTests) // P1 is the FIRST provider-driven frame, so it should only seed the state: // no animation error or time yet. - Assert::IsFalse(p1_metrics.msAnimationError.has_value(), + Assert::IsFalse(HasMetricValue(p1_metrics.msAnimationError), L"P1 should not report animation error; it seeds the animation state."); - Assert::IsTrue(p1_metrics.msAnimationTime.has_value(), + Assert::IsTrue(HasMetricValue(p1_metrics.msAnimationTime), L"P1 should report back 0.0."); - Assert::AreEqual(double(0.0), p1_metrics.msAnimationTime.value()); + Assert::AreEqual(double(0.0), p1_metrics.msAnimationTime, 0.0001); // UpdateAfterPresent should have run for P1 and switched to AppProvider: Assert::IsTrue(state.animationErrorSource == AnimationErrorSource::AppProvider, @@ -6574,17 +7120,17 @@ TEST_CLASS(ComputeMetricsForPresentTests) // => animationError = 0.0 ms // => animationTime = (200 - 100) ticks from firstAppSimStartTime -> 0.01 ms - Assert::IsTrue(p2_metrics.msAnimationError.has_value(), + Assert::IsTrue(HasMetricValue(p2_metrics.msAnimationError), L"P2 should report animation error."); - Assert::IsTrue(p2_metrics.msAnimationTime.has_value(), + Assert::IsTrue(HasMetricValue(p2_metrics.msAnimationTime), L"P2 should report animation time."); double expectedError = 0.0; - Assert::AreEqual(expectedError, p2_metrics.msAnimationError.value(), 0.0001, + Assert::AreEqual(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); - Assert::AreEqual(expectedAnim, p2_metrics.msAnimationTime.value(), 0.0001, + Assert::AreEqual(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" @@ -6687,9 +7233,9 @@ TEST_CLASS(ComputeMetricsForPresentTests) const auto& p2_metrics = p2_results[0].metrics; // Discarded / not-displayed frame must NOT produce animation metrics. - Assert::IsFalse(p2_metrics.msAnimationTime.has_value(), + Assert::IsFalse(HasMetricValue(p2_metrics.msAnimationTime), L"P2 (discarded) should not have msAnimationTime."); - Assert::IsFalse(p2_metrics.msAnimationError.has_value(), + 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. @@ -6723,11 +7269,11 @@ TEST_CLASS(ComputeMetricsForPresentTests) // P1 is the FIRST displayed frame with AppProvider sim start. // It should only seed animation state; no error/time yet. - Assert::IsFalse(p1_metrics.msAnimationError.has_value(), + Assert::IsFalse(HasMetricValue(p1_metrics.msAnimationError), L"P1 should not report animation error; it seeds the animation state."); - Assert::IsTrue(p1_metrics.msAnimationTime.has_value(), + Assert::IsTrue(HasMetricValue(p1_metrics.msAnimationTime), L"P1 should have an animation time of 0.0."); - Assert::AreEqual(double(0.0), p1_metrics.msAnimationTime.value()); + Assert::AreEqual(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, @@ -6804,11 +7350,11 @@ TEST_CLASS(ComputeMetricsForPresentTests) // Assertions for P1 Assert::AreEqual(size_t(1), p1_final.size()); - Assert::IsTrue(p1_final[0].metrics.msClickToPhotonLatency.has_value(), + Assert::IsTrue(HasMetricValue(p1_final[0].metrics.msClickToPhotonLatency), L"P1 should have msClickToPhotonLatency"); double expected = qpc.DeltaUnsignedMilliSeconds(400'000, 1'000'000); - Assert::AreEqual(expected, p1_final[0].metrics.msClickToPhotonLatency.value(), 0.0001, + Assert::AreEqual(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 @@ -6828,7 +7374,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) // - P2 (displayed, no own click) uses the stored click from P1 // // Expected: - // - P1: msClickToPhotonLatency == nullopt + // - 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) @@ -6866,8 +7412,8 @@ TEST_CLASS(ComputeMetricsForPresentTests) // Assertions for P1 Assert::AreEqual(size_t(1), p1_results.size()); - Assert::IsFalse(p1_results[0].metrics.msClickToPhotonLatency.has_value(), - L"P1 (dropped) should not have msClickToPhotonLatency"); + 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"); @@ -6880,11 +7426,11 @@ TEST_CLASS(ComputeMetricsForPresentTests) // Assertions for P2 Assert::AreEqual(size_t(1), p2_final.size()); - Assert::IsTrue(p2_final[0].metrics.msClickToPhotonLatency.has_value(), + Assert::IsTrue(HasMetricValue(p2_final[0].metrics.msClickToPhotonLatency), L"P2 should have msClickToPhotonLatency using P1's stored click"); double expected = qpc.DeltaUnsignedMilliSeconds(400'000, 1'000'000); - Assert::AreEqual(expected, p2_final[0].metrics.msClickToPhotonLatency.value(), 0.0001, + Assert::AreEqual(expected, p2_final[0].metrics.msClickToPhotonLatency, 0.0001, L"P2's click-to-photon should use P1's stored click"); // Optional: verify pending click is consumed @@ -6903,8 +7449,8 @@ TEST_CLASS(ComputeMetricsForPresentTests) // - P3 (displayed, no own input) uses the last stored input (450'000) // // Expected: - // - P1: msAllInputPhotonLatency == nullopt, state stores 300'000 - // - P2: msAllInputPhotonLatency == nullopt, state updates to 450'000 + // - 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); @@ -6947,16 +7493,16 @@ TEST_CLASS(ComputeMetricsForPresentTests) // P1 arrives (dropped) auto p1_results = ComputeMetricsForPresent(qpc, p1, nullptr, state); Assert::AreEqual(size_t(1), p1_results.size()); - Assert::IsFalse(p1_results[0].metrics.msAllInputPhotonLatency.has_value(), - L"P1 (dropped) should not have msAllInputPhotonLatency"); + 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"); // P2 arrives (dropped, overrides P1) auto p2_results = ComputeMetricsForPresent(qpc, p2, nullptr, state); Assert::AreEqual(size_t(1), p2_results.size()); - Assert::IsFalse(p2_results[0].metrics.msAllInputPhotonLatency.has_value(), - L"P2 (dropped) should not have msAllInputPhotonLatency"); + 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)"); @@ -6969,11 +7515,11 @@ TEST_CLASS(ComputeMetricsForPresentTests) // Assertions for P3 Assert::AreEqual(size_t(1), p3_final.size()); - Assert::IsTrue(p3_final[0].metrics.msAllInputPhotonLatency.has_value(), + Assert::IsTrue(HasMetricValue(p3_final[0].metrics.msAllInputPhotonLatency), L"P3 should have msAllInputPhotonLatency using last stored input"); double expected = qpc.DeltaUnsignedMilliSeconds(450'000, 1'000'000); - Assert::AreEqual(expected, p3_final[0].metrics.msAllInputPhotonLatency.value(), 0.0001, + Assert::AreEqual(expected, p3_final[0].metrics.msAllInputPhotonLatency, 0.0001, L"P3's all-input-to-photon should use P2's input (last wins)"); } @@ -7033,11 +7579,11 @@ TEST_CLASS(ComputeMetricsForPresentTests) // Assertions for P1 Assert::AreEqual(size_t(1), p1_final.size()); - Assert::IsTrue(p1_final[0].metrics.msAllInputPhotonLatency.has_value(), + Assert::IsTrue(HasMetricValue(p1_final[0].metrics.msAllInputPhotonLatency), L"P1 should have msAllInputPhotonLatency using its own input"); double expected = qpc.DeltaUnsignedMilliSeconds(500'000, 1'000'000); - Assert::AreEqual(expected, p1_final[0].metrics.msAllInputPhotonLatency.value(), 0.0001, + Assert::AreEqual(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)"); } @@ -7109,16 +7655,16 @@ TEST_CLASS(ComputeMetricsForPresentTests) Assert::AreEqual(size_t(1), p2_final.size()); // Verify P2 has animation time (sanity check we're in AppProvider mode) - Assert::IsTrue(p2_final[0].metrics.msAnimationTime.has_value(), + Assert::IsTrue(HasMetricValue(p2_final[0].metrics.msAnimationTime), L"P2 should have msAnimationTime (AppProvider mode)"); // Verify msInstrumentedInputTime is present - Assert::IsTrue(p2_final[0].metrics.msInstrumentedInputTime.has_value(), + Assert::IsTrue(HasMetricValue(p2_final[0].metrics.msInstrumentedInputTime), L"P2 should have msInstrumentedInputTime"); // Calculate expected: app input -> p2 screen double expectedInstr = qpc.DeltaUnsignedMilliSeconds(500'000, 1'100'000); - Assert::AreEqual(expectedInstr, p2_final[0].metrics.msInstrumentedInputTime.value(), 0.0001, + Assert::AreEqual(expectedInstr, p2_final[0].metrics.msInstrumentedInputTime, 0.0001, L"msInstrumentedInputTime should be P2 app input time to P2 screen time"); } }; @@ -7228,7 +7774,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) const auto& p0_metrics = p0_metrics_list[0].metrics; // Dropped frames never report PC latency directly. - Assert::IsFalse(p0_metrics.msPcLatency.has_value(), + Assert::IsFalse(HasMetricValue(p0_metrics.msPcLatency), L"P0: dropped frame should not report msPcLatency."); // Accumulator should have been initialized from Ping0 -> Sim0. @@ -7262,7 +7808,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) const auto& p1_metrics = p1_metrics_list[0].metrics; - Assert::IsFalse(p1_metrics.msPcLatency.has_value(), + Assert::IsFalse(HasMetricValue(p1_metrics.msPcLatency), L"P1: dropped frame should not report msPcLatency."); // Accumulator should have grown: now includes SIM0->SIM1 as well. @@ -7345,9 +7891,9 @@ TEST_CLASS(ComputeMetricsForPresentTests) // 1) PC Latency should be populated and positive for P2 when it finally // reaches the screen after the dropped chain. - Assert::IsTrue(p2_metrics.msPcLatency.has_value(), + 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.value() > 0.0, + 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 @@ -7393,7 +7939,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) 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(p0_results[0].metrics.msPcLatency.has_value(), + Assert::IsFalse(HasMetricValue(p0_results[0].metrics.msPcLatency), L"P0 should not report msPcLatency without PCL data."); Assert::AreEqual(0.0, state.accumulatedInput2FrameStartTime, 0.0001, L"P0 should not modify accumulatedInput2FrameStartTime when there is no PCL data."); @@ -7429,7 +7975,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) 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(p1_final[0].metrics.msPcLatency.has_value(), + Assert::IsFalse(HasMetricValue(p1_final[0].metrics.msPcLatency), L"P1 final metrics should not report msPcLatency without PCL data."); Assert::AreEqual(0.0, state.accumulatedInput2FrameStartTime, 0.0001, L"Accumulated input-to-frame-start time must remain 0 after finalizing P1."); @@ -7452,7 +7998,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) 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(p2_final[0].metrics.msPcLatency.has_value(), + Assert::IsFalse(HasMetricValue(p2_final[0].metrics.msPcLatency), L"P2 final metrics should not report msPcLatency without PCL data."); Assert::AreEqual(0.0, state.accumulatedInput2FrameStartTime, 0.0001, L"Accumulated input-to-frame-start time must still be 0 after P2."); @@ -7496,9 +8042,9 @@ TEST_CLASS(ComputeMetricsForPresentTests) L"P0 should emit metrics immediately when nextDisplayed == nullptr and two display samples exist."); const auto& p0_metrics = p0_results[0].metrics; - Assert::IsTrue(p0_metrics.msPcLatency.has_value(), + Assert::IsTrue(HasMetricValue(p0_metrics.msPcLatency), L"P0 should report msPcLatency for a direct PCL sample."); - Assert::IsTrue(p0_metrics.msPcLatency.value() > 0.0, + Assert::IsTrue(p0_metrics.msPcLatency > 0.0, L"P0 msPcLatency should be positive."); Assert::AreEqual(0.0, state.accumulatedInput2FrameStartTime, 0.0001, L"Direct PCL sample should not touch accumulatedInput2FrameStartTime."); @@ -7511,7 +8057,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) L"Input2FrameStartTimeEma should be seeded from the first Δ(PING,SIM)."); double expectedLatency = expectedEma + qpc.DeltaSignedMilliSeconds(20'000, 50'000); - Assert::AreEqual(expectedLatency, p0_metrics.msPcLatency.value(), 0.0001, + Assert::AreEqual(expectedLatency, p0_metrics.msPcLatency, 0.0001, L"msPcLatency should use pclSimStartTime (not lastSimStartTime) plus the seeded EMA."); } @@ -7557,7 +8103,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) 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(p0_final[0].metrics.msPcLatency.has_value(), + Assert::IsTrue(HasMetricValue(p0_final[0].metrics.msPcLatency), L"P0 should report msPcLatency when finalized."); double emaAfterP0 = state.Input2FrameStartTimeEma; Assert::IsTrue(emaAfterP0 > 0.0, @@ -7579,7 +8125,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) 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(p1_final[0].metrics.msPcLatency.has_value(), + Assert::IsTrue(HasMetricValue(p1_final[0].metrics.msPcLatency), L"P1 should report msPcLatency when finalized."); double emaAfterP1 = state.Input2FrameStartTimeEma; Assert::IsTrue(emaAfterP1 > 0.0, @@ -7616,7 +8162,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) 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(p0_results[0].metrics.msPcLatency.has_value(), + Assert::IsFalse(HasMetricValue(p0_results[0].metrics.msPcLatency), L"Dropped frames must not report msPcLatency."); double expectedAccum = qpc.DeltaUnsignedMilliSeconds(10'000, 20'000); @@ -7649,7 +8195,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) auto p0_results = ComputeMetricsForPresent(qpc, p0, nullptr, state); Assert::AreEqual(size_t(1), p0_results.size()); - Assert::IsFalse(p0_results[0].metrics.msPcLatency.has_value()); + Assert::IsFalse(HasMetricValue(p0_results[0].metrics.msPcLatency)); double accumAfterP0 = state.accumulatedInput2FrameStartTime; FrameData p1{}; @@ -7660,7 +8206,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) 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(p1_results[0].metrics.msPcLatency.has_value(), + 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."); @@ -7688,7 +8234,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) auto p0_results = ComputeMetricsForPresent(qpc, p0, nullptr, state); Assert::AreEqual(size_t(1), p0_results.size()); - Assert::IsFalse(p0_results[0].metrics.msPcLatency.has_value()); + Assert::IsFalse(HasMetricValue(p0_results[0].metrics.msPcLatency)); Assert::AreEqual(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, @@ -7728,7 +8274,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) auto p0_final = ComputeMetricsForPresent(qpc, p0, &p1, state); Assert::AreEqual(size_t(1), p0_final.size()); - Assert::IsTrue(p0_final[0].metrics.msPcLatency.has_value()); + Assert::IsTrue(HasMetricValue(p0_final[0].metrics.msPcLatency)); double emaAfterP0 = state.Input2FrameStartTimeEma; Assert::IsTrue(emaAfterP0 > 0.0); @@ -7742,9 +8288,9 @@ TEST_CLASS(ComputeMetricsForPresentTests) 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(p1_metrics.msPcLatency.has_value(), + Assert::IsTrue(HasMetricValue(p1_metrics.msPcLatency), L"P1 should report msPcLatency despite missing pclInputPingTime."); - Assert::IsTrue(p1_metrics.msPcLatency.value() > 0.0, + Assert::IsTrue(p1_metrics.msPcLatency > 0.0, L"P1 msPcLatency should stay positive."); Assert::AreEqual(0.0, state.accumulatedInput2FrameStartTime, 0.0001, L"No dropped chain means the accumulator must stay zero."); @@ -7788,7 +8334,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) auto p0_final = ComputeMetricsForPresent(qpc, p0, &p1, state); Assert::AreEqual(size_t(1), p0_final.size()); - Assert::IsTrue(p0_final[0].metrics.msPcLatency.has_value()); + Assert::IsTrue(HasMetricValue(p0_final[0].metrics.msPcLatency)); double emaAfterP0 = state.Input2FrameStartTimeEma; uint64_t fallbackSimStart = state.lastSimStartTime; Assert::IsTrue(emaAfterP0 > 0.0, @@ -7806,12 +8352,12 @@ TEST_CLASS(ComputeMetricsForPresentTests) 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(p1_metrics.msPcLatency.has_value(), + Assert::IsTrue(HasMetricValue(p1_metrics.msPcLatency), L"P1 should still report msPcLatency using the fallback lastSimStartTime."); Assert::AreEqual(emaAfterP0, state.Input2FrameStartTimeEma, 0.0001, L"EMA should remain unchanged when no new PCL sample exists."); double expectedLatency = emaAfterP0 + qpc.DeltaSignedMilliSeconds(fallbackSimStart, 90'000); - Assert::AreEqual(expectedLatency, p1_metrics.msPcLatency.value(), 0.0001, + Assert::AreEqual(expectedLatency, p1_metrics.msPcLatency, 0.0001, L"msPcLatency should use the stored EMA plus the delta from lastSimStartTime to screen time."); Assert::AreEqual(0.0, state.accumulatedInput2FrameStartTime, 0.0001, L"Accumulator should remain zero in this scenario."); @@ -7857,7 +8403,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) auto p2_results = ComputeMetricsForPresent(qpc, p2, nullptr, state); Assert::AreEqual(size_t(1), p2_results.size()); - Assert::IsFalse(p2_results[0].metrics.msPcLatency.has_value()); + Assert::IsFalse(HasMetricValue(p2_results[0].metrics.msPcLatency)); double expectedAccum = qpc.DeltaUnsignedMilliSeconds(100'000, 120'000); Assert::AreEqual(expectedAccum, state.accumulatedInput2FrameStartTime, 0.0001, @@ -7889,7 +8435,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) d0.finalState = PresentResult::Discarded; auto d0_results = ComputeMetricsForPresent(qpc, d0, nullptr, state); Assert::AreEqual(size_t(1), d0_results.size()); - Assert::IsFalse(d0_results[0].metrics.msPcLatency.has_value()); + Assert::IsFalse(HasMetricValue(d0_results[0].metrics.msPcLatency)); FrameData d1{}; d1.pclInputPingTime = 0; @@ -7897,7 +8443,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) d1.finalState = PresentResult::Discarded; auto d1_results = ComputeMetricsForPresent(qpc, d1, nullptr, state); Assert::AreEqual(size_t(1), d1_results.size()); - Assert::IsFalse(d1_results[0].metrics.msPcLatency.has_value()); + 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."); @@ -7919,9 +8465,9 @@ TEST_CLASS(ComputeMetricsForPresentTests) 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(p0_metrics.msPcLatency.has_value(), + Assert::IsTrue(HasMetricValue(p0_metrics.msPcLatency), L"Displayed frame with direct PCL data must report msPcLatency."); - Assert::IsTrue(p0_metrics.msPcLatency.value() > 0.0, + Assert::IsTrue(p0_metrics.msPcLatency > 0.0, L"msPcLatency should be positive for P0."); double expectedFirstEma = pmon::util::CalculateEma(0.0, @@ -8058,21 +8604,21 @@ TEST_CLASS(ComputeMetricsForPresentTests) double expectedBetween = qpc.DeltaUnsignedMilliSeconds(10'000, 20'000); // 10'000 ticks // 1) Instrumented sleep - Assert::IsTrue(m0.msInstrumentedSleep.has_value(), + Assert::IsTrue(HasMetricValue(m0.msInstrumentedSleep), L"P0: msInstrumentedSleep should have a value for valid AppSleepStart/End."); - Assert::AreEqual(expectedSleepMs, m0.msInstrumentedSleep.value(), 1e-6, + Assert::AreEqual(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(m0.msInstrumentedGpuLatency.has_value(), + Assert::IsTrue(HasMetricValue(m0.msInstrumentedGpuLatency), L"P0: msInstrumentedGpuLatency should have a value when InstrumentedStartTime and gpuStartTime are valid."); - Assert::AreEqual(expectedGpuMs, m0.msInstrumentedGpuLatency.value(), 1e-6, + Assert::AreEqual(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(m0.msBetweenSimStarts.has_value(), + Assert::IsTrue(HasMetricValue(m0.msBetweenSimStarts), L"P0: msBetweenSimStarts should have a value when lastSimStartTime and PclSimStartTime are non-zero."); - Assert::AreEqual(expectedBetween, m0.msBetweenSimStarts.value(), 1e-6, + Assert::AreEqual(expectedBetween, m0.msBetweenSimStarts, 1e-6, L"P0: msBetweenSimStarts should be based on PCL sim start, not App sim start."); } TEST_METHOD(InstrumentedDisplay_AppFrame_FullData_ComputesAll) @@ -8190,21 +8736,21 @@ TEST_CLASS(ComputeMetricsForPresentTests) double expectedTotalMs = qpc.DeltaUnsignedMilliSeconds(5'000, 30'000); // 25'000 ticks // Render latency - Assert::IsTrue(m0.msInstrumentedRenderLatency.has_value(), + Assert::IsTrue(HasMetricValue(m0.msInstrumentedRenderLatency), L"P0: msInstrumentedRenderLatency should have a value for a displayed app frame with AppRenderSubmitStartTime."); - Assert::AreEqual(expectedRenderMs, m0.msInstrumentedRenderLatency.value(), 1e-6, + Assert::AreEqual(expectedRenderMs, m0.msInstrumentedRenderLatency, 1e-6, L"P0: msInstrumentedRenderLatency did not match expected Δ(AppRenderSubmitStartTime, screenTime)."); // Ready-to-display latency - Assert::IsTrue(m0.msReadyTimeToDisplayLatency.has_value(), + Assert::IsTrue(HasMetricValue(m0.msReadyTimeToDisplayLatency), L"P0: msReadyTimeToDisplayLatency should have a value when ReadyTime and screenTime are valid."); - Assert::AreEqual(expectedReadyMs, m0.msReadyTimeToDisplayLatency.value(), 1e-6, + Assert::AreEqual(expectedReadyMs, m0.msReadyTimeToDisplayLatency, 1e-6, L"P0: msReadyTimeToDisplayLatency did not match expected Δ(ReadyTime, screenTime)."); // Total instrumented latency: from appSleepEndTime to screenTime - Assert::IsTrue(m0.msInstrumentedLatency.has_value(), + Assert::IsTrue(HasMetricValue(m0.msInstrumentedLatency), L"P0: msInstrumentedLatency should have a value when there is a valid instrumented start time."); - Assert::AreEqual(expectedTotalMs, m0.msInstrumentedLatency.value(), 1e-6, + Assert::AreEqual(expectedTotalMs, m0.msInstrumentedLatency, 1e-6, L"P0: msInstrumentedLatency did not match expected Δ(AppSleepEndTime, screenTime)."); } @@ -8274,19 +8820,19 @@ TEST_CLASS(ComputeMetricsForPresentTests) L"P0 (final) should emit exactly one metrics record once nextDisplayed is provided."); const auto& m0 = p0_final[0].metrics; - Assert::IsFalse(m0.msInstrumentedSleep.has_value(), + Assert::IsFalse(HasMetricValue(m0.msInstrumentedSleep), L"P0: Instrumented sleep must be absent when the app never emitted sleep markers."); - Assert::IsTrue(m0.msInstrumentedGpuLatency.has_value(), + Assert::IsTrue(HasMetricValue(m0.msInstrumentedGpuLatency), L"P0: GPU latency should fall back to AppSimStart when no sleep end exists."); - Assert::IsTrue(m0.msBetweenSimStarts.has_value(), + Assert::IsTrue(HasMetricValue(m0.msBetweenSimStarts), L"P0: Between-sim-starts should use the stored lastSimStartTime when AppSimStart is valid."); double expectedGpuMs = qpc.DeltaUnsignedMilliSeconds(70'000, 90'000); double expectedBetweenMs = qpc.DeltaUnsignedMilliSeconds(40'000, 70'000); - Assert::AreEqual(expectedGpuMs, m0.msInstrumentedGpuLatency.value(), 1e-6, + Assert::AreEqual(expectedGpuMs, m0.msInstrumentedGpuLatency, 1e-6, L"P0: msInstrumentedGpuLatency should measure Δ(AppSimStartTime, gpuStartTime)."); - Assert::AreEqual(expectedBetweenMs, m0.msBetweenSimStarts.value(), 1e-6, + Assert::AreEqual(expectedBetweenMs, m0.msBetweenSimStarts, 1e-6, L"P0: msBetweenSimStarts should use AppSimStart when no PCL sim exists."); auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, chain); @@ -8340,11 +8886,11 @@ TEST_CLASS(ComputeMetricsForPresentTests) Assert::AreEqual(size_t(1), p0_final.size()); const auto& m0 = p0_final[0].metrics; - Assert::IsFalse(m0.msInstrumentedSleep.has_value(), + Assert::IsFalse(HasMetricValue(m0.msInstrumentedSleep), L"P0: sleep metrics require both start and end markers."); - Assert::IsFalse(m0.msInstrumentedGpuLatency.has_value(), + Assert::IsFalse(HasMetricValue(m0.msInstrumentedGpuLatency), L"P0: GPU latency must remain off without an instrumented start time."); - Assert::IsFalse(m0.msBetweenSimStarts.has_value(), + Assert::IsFalse(HasMetricValue(m0.msBetweenSimStarts), L"P0: between-sim-starts cannot be computed without a new sim start."); auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, chain); @@ -8391,19 +8937,19 @@ TEST_CLASS(ComputeMetricsForPresentTests) double expectedGpuMs = qpc.DeltaUnsignedMilliSeconds(25'000, 45'000); double expectedBetweenMs = qpc.DeltaUnsignedMilliSeconds(5'000, 30'000); - Assert::IsTrue(m0.msInstrumentedSleep.has_value()); - Assert::AreEqual(expectedSleepMs, m0.msInstrumentedSleep.value(), 1e-6); + Assert::IsTrue(HasMetricValue(m0.msInstrumentedSleep)); + Assert::AreEqual(expectedSleepMs, m0.msInstrumentedSleep, 1e-6); - Assert::IsTrue(m0.msInstrumentedGpuLatency.has_value()); - Assert::AreEqual(expectedGpuMs, m0.msInstrumentedGpuLatency.value(), 1e-6); + Assert::IsTrue(HasMetricValue(m0.msInstrumentedGpuLatency)); + Assert::AreEqual(expectedGpuMs, m0.msInstrumentedGpuLatency, 1e-6); - Assert::IsTrue(m0.msBetweenSimStarts.has_value()); - Assert::AreEqual(expectedBetweenMs, m0.msBetweenSimStarts.value(), 1e-6); + Assert::IsTrue(HasMetricValue(m0.msBetweenSimStarts)); + Assert::AreEqual(expectedBetweenMs, m0.msBetweenSimStarts, 1e-6); - Assert::IsFalse(m0.msInstrumentedRenderLatency.has_value(), + Assert::IsFalse(HasMetricValue(m0.msInstrumentedRenderLatency), L"Display-dependent metrics must stay off for non-displayed frames."); - Assert::IsFalse(m0.msReadyTimeToDisplayLatency.has_value()); - Assert::IsFalse(m0.msInstrumentedLatency.has_value()); + Assert::IsFalse(HasMetricValue(m0.msReadyTimeToDisplayLatency)); + Assert::IsFalse(HasMetricValue(m0.msInstrumentedLatency)); } TEST_METHOD(InstrumentedCpuGpu_NonAppFrame_Ignored) @@ -8457,10 +9003,10 @@ TEST_CLASS(ComputeMetricsForPresentTests) Assert::AreEqual(size_t(1), p0_final.size()); const auto& m0 = p0_final[0].metrics; - Assert::IsFalse(m0.msInstrumentedSleep.has_value(), + Assert::IsFalse(HasMetricValue(m0.msInstrumentedSleep), L"Non-app displays must not emit instrumented CPU metrics."); - Assert::IsFalse(m0.msInstrumentedGpuLatency.has_value()); - Assert::IsFalse(m0.msBetweenSimStarts.has_value()); + 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()); @@ -8503,13 +9049,13 @@ TEST_CLASS(ComputeMetricsForPresentTests) double expectedReadyMs = qpc.DeltaUnsignedMilliSeconds(80'000, 100'000); double expectedTotalMs = qpc.DeltaUnsignedMilliSeconds(50'000, 100'000); - Assert::IsFalse(m0.msInstrumentedRenderLatency.has_value(), + Assert::IsFalse(HasMetricValue(m0.msInstrumentedRenderLatency), L"Render latency must remain off without appRenderSubmitStartTime."); - Assert::IsTrue(m0.msReadyTimeToDisplayLatency.has_value()); - Assert::AreEqual(expectedReadyMs, m0.msReadyTimeToDisplayLatency.value(), 1e-6); + Assert::IsTrue(HasMetricValue(m0.msReadyTimeToDisplayLatency)); + Assert::AreEqual(expectedReadyMs, m0.msReadyTimeToDisplayLatency, 1e-6); - Assert::IsTrue(m0.msInstrumentedLatency.has_value()); - Assert::AreEqual(expectedTotalMs, m0.msInstrumentedLatency.value(), 1e-6); + Assert::IsTrue(HasMetricValue(m0.msInstrumentedLatency)); + Assert::AreEqual(expectedTotalMs, m0.msInstrumentedLatency, 1e-6); auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, chain); Assert::AreEqual(size_t(0), p1_phase1.size()); @@ -8561,12 +9107,12 @@ TEST_CLASS(ComputeMetricsForPresentTests) double expectedReadyMs = qpc.DeltaUnsignedMilliSeconds(30'000, 60'000); double expectedTotalMs = qpc.DeltaUnsignedMilliSeconds(5'000, 60'000); - Assert::IsTrue(m0.msInstrumentedRenderLatency.has_value()); - Assert::AreEqual(expectedRenderMs, m0.msInstrumentedRenderLatency.value(), 1e-6); - Assert::IsTrue(m0.msReadyTimeToDisplayLatency.has_value()); - Assert::AreEqual(expectedReadyMs, m0.msReadyTimeToDisplayLatency.value(), 1e-6); - Assert::IsTrue(m0.msInstrumentedLatency.has_value()); - Assert::AreEqual(expectedTotalMs, m0.msInstrumentedLatency.value(), 1e-6, + 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, L"Total latency should fall back to AppSimStartTime when sleep end is missing."); auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, chain); @@ -8611,11 +9157,11 @@ TEST_CLASS(ComputeMetricsForPresentTests) double expectedRenderMs = qpc.DeltaUnsignedMilliSeconds(12'000, 70'000); double expectedReadyMs = qpc.DeltaUnsignedMilliSeconds(32'000, 70'000); - Assert::IsTrue(m0.msInstrumentedRenderLatency.has_value()); - Assert::AreEqual(expectedRenderMs, m0.msInstrumentedRenderLatency.value(), 1e-6); - Assert::IsTrue(m0.msReadyTimeToDisplayLatency.has_value()); - Assert::AreEqual(expectedReadyMs, m0.msReadyTimeToDisplayLatency.value(), 1e-6); - Assert::IsFalse(m0.msInstrumentedLatency.has_value(), + 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::IsFalse(HasMetricValue(m0.msInstrumentedLatency), L"Total instrumented latency must stay off without an instrumented start."); auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, chain); @@ -8656,8 +9202,8 @@ TEST_CLASS(ComputeMetricsForPresentTests) Assert::AreEqual(size_t(1), p0_final.size()); const auto& m0 = p0_final[0].metrics; - Assert::IsFalse(m0.msInstrumentedRenderLatency.has_value()); - Assert::IsFalse(m0.msInstrumentedLatency.has_value()); + 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()); @@ -8690,9 +9236,9 @@ TEST_CLASS(ComputeMetricsForPresentTests) Assert::AreEqual(size_t(1), p0_results.size()); const auto& m0 = p0_results[0].metrics; - Assert::IsFalse(m0.msInstrumentedRenderLatency.has_value()); - Assert::IsFalse(m0.msReadyTimeToDisplayLatency.has_value()); - Assert::IsFalse(m0.msInstrumentedLatency.has_value()); + Assert::IsFalse(HasMetricValue(m0.msInstrumentedRenderLatency)); + Assert::IsFalse(HasMetricValue(m0.msReadyTimeToDisplayLatency)); + Assert::IsFalse(HasMetricValue(m0.msInstrumentedLatency)); } TEST_METHOD(InstrumentedInput_DroppedAppFrame_PendingProviderInput_ConsumedOnDisplay) @@ -8725,6 +9271,8 @@ TEST_CLASS(ComputeMetricsForPresentTests) auto p0_results = ComputeMetricsForPresent(qpc, p0, nullptr, 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."); @@ -8745,10 +9293,10 @@ TEST_CLASS(ComputeMetricsForPresentTests) Assert::AreEqual(size_t(1), p1_final.size()); const auto& m1 = p1_final[0].metrics; - Assert::IsTrue(m1.msInstrumentedInputTime.has_value(), + Assert::IsTrue(HasMetricValue(m1.msInstrumentedInputTime), L"P1 should consume the cached provider input time once it is displayed."); double expectedInputMs = qpc.DeltaUnsignedMilliSeconds(pendingInputTime, 70'000); - Assert::AreEqual(expectedInputMs, m1.msInstrumentedInputTime.value(), 1e-6); + Assert::AreEqual(expectedInputMs, m1.msInstrumentedInputTime, 1e-6); Assert::AreEqual(uint64_t(0), chain.lastReceivedNotDisplayedAppProviderInputTime, L"Pending provider input cache must be cleared after consumption."); @@ -8806,8 +9354,8 @@ TEST_CLASS(ComputeMetricsForPresentTests) const auto& m1 = p1_final[0].metrics; double expectedInputMs = qpc.DeltaUnsignedMilliSeconds(directInputTime, 60'000); - Assert::IsTrue(m1.msInstrumentedInputTime.has_value()); - Assert::AreEqual(expectedInputMs, m1.msInstrumentedInputTime.value(), 1e-6, + Assert::IsTrue(HasMetricValue(m1.msInstrumentedInputTime)); + Assert::AreEqual(expectedInputMs, m1.msInstrumentedInputTime, 1e-6, L"P1 must prefer its own input marker over pending values."); Assert::AreEqual(uint64_t(0), chain.lastReceivedNotDisplayedAppProviderInputTime); @@ -8825,7 +9373,7 @@ TEST_CLASS(ComputeMetricsForPresentTests) // - 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 nullopt. + // 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). // @@ -8865,8 +9413,8 @@ TEST_CLASS(ComputeMetricsForPresentTests) auto p1_final = ComputeMetricsForPresent(qpc, p1, &p2, chain); Assert::AreEqual(size_t(1), p1_final.size()); const auto& m1 = p1_final[0].metrics; - Assert::IsFalse(m1.msInstrumentedInputTime.has_value(), - L"P1 should not report instrumented input latency because no app-frame pending sample existed."); + 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/PresentMon/CsvOutput.cpp b/PresentMon/CsvOutput.cpp index ccbbc383..a1d537e9 100644 --- a/PresentMon/CsvOutput.cpp +++ b/PresentMon/CsvOutput.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 @@ -7,6 +7,16 @@ static FILE* gGlobalOutputCsv = nullptr; static uint32_t gRecordingCount = 1; +static bool HasFrameMetricValue(double value) +{ + return !pmon::util::metrics::IsMissingFrameMetricValue(value); +} + +static void WriteMetricOrZero(FILE* fp, double value, int precision = 4) +{ + fwprintf(fp, L",%.*lf", precision, HasFrameMetricValue(value) ? value : 0); +} + void IncrementRecordingCount() { gRecordingCount += 1; @@ -771,9 +781,9 @@ void WriteCsvRow( } // MsBetweenAppStart, MsCPUBusy, MsCPUWait - fwprintf(fp, L",%.4lf,%.4lf,%.4lf", metrics.mMsCPUBusy + metrics.mMsCPUWait, - metrics.mMsCPUBusy, - metrics.mMsCPUWait); + WriteMetricOrZero(fp, metrics.mMsCPUBusy + metrics.mMsCPUWait); + WriteMetricOrZero(fp, metrics.mMsCPUBusy); + WriteMetricOrZero(fp, metrics.mMsCPUWait); if (args.mTrackGPU) { fwprintf(fp, L",%.4lf,%.4lf,%.4lf,%.4lf", metrics.mMsGPULatency, @@ -920,8 +930,8 @@ void WriteCsvRow( } // MsBetweenSimulationStart - if (metrics.msBetweenSimStarts.has_value()) { - fwprintf(fp, L",%.4lf", metrics.msBetweenSimStarts.value()); + if (HasFrameMetricValue(metrics.msBetweenSimStarts)) { + fwprintf(fp, L",%.4lf", metrics.msBetweenSimStarts); } else { fwprintf(fp, L",NA"); @@ -956,8 +966,8 @@ void WriteCsvRow( } } if (args.mTrackPcLatency) { - if (metrics.msPcLatency.has_value()) { - fwprintf(fp, L",%.4lf", metrics.msPcLatency.value()); + if (HasFrameMetricValue(metrics.msPcLatency)) { + fwprintf(fp, L",%.4lf", metrics.msPcLatency); } else { fwprintf(fp, L",NA"); @@ -994,9 +1004,9 @@ void WriteCsvRow( } // MsBetweenAppStart, MsCPUBusy, MsCPUWait - fwprintf(fp, L",%.4lf,%.4lf,%.4lf", metrics.msCPUBusy + metrics.msCPUWait, - metrics.msCPUBusy, - metrics.msCPUWait); + WriteMetricOrZero(fp, metrics.msCPUBusy + metrics.msCPUWait); + WriteMetricOrZero(fp, metrics.msCPUBusy); + WriteMetricOrZero(fp, metrics.msCPUWait); if (args.mTrackGPU) { fwprintf(fp, L",%.4lf,%.4lf,%.4lf,%.4lf", metrics.msGPULatency, @@ -1017,42 +1027,42 @@ void WriteCsvRow( metrics.msDisplayedTime); } } - if (metrics.msAnimationError.has_value()) { - fwprintf(fp, L",%.4lf", metrics.msAnimationError.value()); + if (HasFrameMetricValue(metrics.msAnimationError)) { + fwprintf(fp, L",%.4lf", metrics.msAnimationError); } else { fwprintf(fp, L",NA"); } - if (metrics.msAnimationTime.has_value()) { - fwprintf(fp, L",%.4lf", metrics.msAnimationTime.value()); + if (HasFrameMetricValue(metrics.msAnimationTime)) { + fwprintf(fp, L",%.4lf", metrics.msAnimationTime); } else { fwprintf(fp, L",NA"); } - if (metrics.msFlipDelay.has_value()) { - fwprintf(fp, L",%.4lf", metrics.msFlipDelay.value()); + if (HasFrameMetricValue(metrics.msFlipDelay)) { + fwprintf(fp, L",%.4lf", metrics.msFlipDelay); } else { fwprintf(fp, L",NA"); } } if (args.mTrackInput) { - if (metrics.msAllInputPhotonLatency.has_value()) { - fwprintf(fp, L",%.4lf", metrics.msAllInputPhotonLatency.value()); + if (HasFrameMetricValue(metrics.msAllInputPhotonLatency)) { + fwprintf(fp, L",%.4lf", metrics.msAllInputPhotonLatency); } else { fwprintf(fp, L",NA"); } - if (metrics.msClickToPhotonLatency.has_value()) { - fwprintf(fp, L",%.4lf", metrics.msClickToPhotonLatency.value()); + if (HasFrameMetricValue(metrics.msClickToPhotonLatency)) { + fwprintf(fp, L",%.4lf", metrics.msClickToPhotonLatency); } else { fwprintf(fp, L",NA"); } } if (args.mTrackAppTiming) { - if (metrics.msInstrumentedLatency.has_value()) { - fwprintf(fp, L",%.4lf", metrics.msInstrumentedLatency.value()); + if (HasFrameMetricValue(metrics.msInstrumentedLatency)) { + fwprintf(fp, L",%.4lf", metrics.msInstrumentedLatency); } else { fwprintf(fp, L",NA"); @@ -1137,8 +1147,8 @@ void WriteCsvRow( } // MsBetweenSimulationStart - if (metrics.msBetweenSimStarts.has_value()) { - fwprintf(fp, L",%.4lf", metrics.msBetweenSimStarts.value()); + if (HasFrameMetricValue(metrics.msBetweenSimStarts)) { + fwprintf(fp, L",%.4lf", metrics.msBetweenSimStarts); } else { fwprintf(fp, L",NA"); @@ -1173,8 +1183,8 @@ void WriteCsvRow( } } if (args.mTrackPcLatency) { - if (metrics.msPcLatency.has_value()) { - fwprintf(fp, L",%.4lf", metrics.msPcLatency.value()); + if (HasFrameMetricValue(metrics.msPcLatency)) { + fwprintf(fp, L",%.4lf", metrics.msPcLatency); } else { fwprintf(fp, L",NA"); @@ -1211,9 +1221,9 @@ void WriteCsvRow( } // MsBetweenAppStart, MsCPUBusy, MsCPUWait - fwprintf(fp, L",%.4lf,%.4lf,%.4lf", metrics.msCPUBusy + metrics.msCPUWait, - metrics.msCPUBusy, - metrics.msCPUWait); + WriteMetricOrZero(fp, metrics.msCPUBusy + metrics.msCPUWait); + WriteMetricOrZero(fp, metrics.msCPUBusy); + WriteMetricOrZero(fp, metrics.msCPUWait); if (args.mTrackGPU) { fwprintf(fp, L",%.4lf,%.4lf,%.4lf,%.4lf", metrics.msGPULatency, @@ -1234,14 +1244,14 @@ void WriteCsvRow( metrics.msDisplayedTime); } } - if (metrics.msAnimationError.has_value()) { - fwprintf(fp, L",%.4lf", metrics.msAnimationError.value()); + if (HasFrameMetricValue(metrics.msAnimationError)) { + fwprintf(fp, L",%.4lf", metrics.msAnimationError); } else { fwprintf(fp, L",NA"); } - if (metrics.msAnimationTime.has_value()) { - fwprintf(fp, L",%.4lf", metrics.msAnimationTime.value()); + if (HasFrameMetricValue(metrics.msAnimationTime)) { + fwprintf(fp, L",%.4lf", metrics.msAnimationTime); } else { if (metrics.msDisplayedTime == 0.0 || (metrics.frameType != FrameType::Application && metrics.frameType != FrameType::NotSet)) { @@ -1251,30 +1261,30 @@ void WriteCsvRow( fwprintf(fp, L",%.4lf", 0.0); } } - if (metrics.msFlipDelay.has_value()) { - fwprintf(fp, L",%.4lf", metrics.msFlipDelay.value()); + if (HasFrameMetricValue(metrics.msFlipDelay)) { + fwprintf(fp, L",%.4lf", metrics.msFlipDelay); } else { fwprintf(fp, L",NA"); } } if (args.mTrackInput) { - if (metrics.msAllInputPhotonLatency.has_value()) { - fwprintf(fp, L",%.4lf", metrics.msAllInputPhotonLatency.value()); + if (HasFrameMetricValue(metrics.msAllInputPhotonLatency)) { + fwprintf(fp, L",%.4lf", metrics.msAllInputPhotonLatency); } else { fwprintf(fp, L",NA"); } - if (metrics.msClickToPhotonLatency.has_value()) { - fwprintf(fp, L",%.4lf", metrics.msClickToPhotonLatency.value()); + if (HasFrameMetricValue(metrics.msClickToPhotonLatency)) { + fwprintf(fp, L",%.4lf", metrics.msClickToPhotonLatency); } else { fwprintf(fp, L",NA"); } } if (args.mTrackAppTiming) { - if (metrics.msInstrumentedLatency.has_value()) { - fwprintf(fp, L",%.4lf", metrics.msInstrumentedLatency.value()); + if (HasFrameMetricValue(metrics.msInstrumentedLatency)) { + fwprintf(fp, L",%.4lf", metrics.msInstrumentedLatency); } else { fwprintf(fp, L",NA"); diff --git a/PresentMon/OutputThread.cpp b/PresentMon/OutputThread.cpp index a82997b2..75e864a1 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 @@ -394,7 +394,7 @@ static FrameMetrics1 ToFrameMetrics1(pmon::util::metrics::FrameMetrics const& m) out.msVideoDuration = m.msVideoDuration; out.msSinceInput = m.msSinceInput; out.qpcScreenTime = m.screenTimeQpc; - out.msFlipDelay = m.msFlipDelay.has_value() ? m.msFlipDelay.value() : 0.0; + out.msFlipDelay = pmon::util::metrics::IsMissingFrameMetricValue(m.msFlipDelay) ? 0.0 : m.msFlipDelay; return out; }