Files
Brandyn / Techy fcc1b09210 init
2026-04-04 15:40:51 -05:00

180 lines
9.3 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Estimation/TimecodeEstimator.h"
#include "Engine/EngineCustomTimeStep.h"
#include "Engine/TimecodeProvider.h"
#include "Estimation/IClockedTimeStep.h"
#include "HAL/IConsoleManager.h"
#include "ITimeManagementModule.h"
namespace UE::TimeManagement::TimecodeEstimation
{
static TAutoConsoleVariable<bool> CVarLogSampling(
TEXT("Timecode.LogTimecodeSampling"), false, TEXT("When estimating timecode, whether to log sampled time and the current time. For this to take effect, you must use UTimecodeRegressionProvider as custom engine timestep.")
);
static TAutoConsoleVariable<bool> CVarLogEstimation(
TEXT("Timecode.LogTimecodeEstimation"), false, TEXT("When estimating timecode, whether to log estimated time and the current time. For this to take effect, you must use UTimecodeRegressionProvider as custom engine timestep.")
);
static TAutoConsoleVariable<bool> CVarLogTimecodeDifference(
TEXT("Timecode.LogTimecodeDifference"), false, TEXT("Logs the timecode difference between the timecode of the current underlying clock and what is estimated. This is useful for debugging.")
);
static TAutoConsoleVariable<float> CVarUnclearEstimationSubframeThreshold(
TEXT("Timecode.UnclearEstimationSubframeThreshold"), 0.1f,
TEXT("If Abs(timecode's subframe - 0.5f) <= this value, a warning is logged. That is because an estimated value like 14:11:43:16.49 is not a "
"clear estimate... it is very close to both 14:11:43:16.00, or 14:11:43:17.00")
);
static void LogEstimatedTime(
double InRelativeTime, IClockedTimeStep& Clock, UTimecodeProvider& TimecodeProvider,
const FFrameTime& InEstimatedTime, const FFrameTime& InUnroundedEstimatedTime, const FFrameRate& InFrameRate
)
{
const auto ToString = [](const FTimecode& InTime)
{
constexpr bool bForceSignDisplay = false, bDisplaySubframe = true;
return InTime.ToString(bForceSignDisplay, bDisplaySubframe);
};
const TOptional<double> ClockTime = Clock.GetUnderlyingClockTime_AnyThread();
const double AppTime = FApp::GetCurrentTime();
UE_CLOG(CVarLogEstimation.GetValueOnAnyThread(), LogTimeManagement, Log,
TEXT("Estimate %f at %s \t\t(Unrounded: %s \tClock %s, \tApp: %f)"),
InRelativeTime,
*ToString(FQualifiedFrameTime(InEstimatedTime, InFrameRate).ToTimecode()),
*ToString(FQualifiedFrameTime(InUnroundedEstimatedTime, InFrameRate).ToTimecode()),
ClockTime ? *FString::SanitizeFloat(*ClockTime) : TEXT("unset"), AppTime
);
if (CVarLogTimecodeDifference.GetValueOnAnyThread())
{
TimecodeProvider.FetchAndUpdate();
const FQualifiedFrameTime ActualFrameTime = TimecodeProvider.GetQualifiedFrameTime();
FTimecode ActualTC = ActualFrameTime.ToTimecode();
FTimecode EstimatedTC = FQualifiedFrameTime(InEstimatedTime, InFrameRate).ToTimecode();
FTimecode EstimatedRoundedTC = FQualifiedFrameTime(InUnroundedEstimatedTime, InFrameRate).ToTimecode();
const bool bIsTimeEqual = EstimatedTC == ActualTC;
if (bIsTimeEqual)
{
return;
}
if (InEstimatedTime > ActualFrameTime.Time)
{
const FTimecode AbsDeltaTC = FQualifiedFrameTime(InEstimatedTime - ActualFrameTime.Time, InFrameRate).ToTimecode();
UE_CLOG(!bIsTimeEqual && InEstimatedTime > ActualFrameTime.Time, LogTimeManagement, Warning,
TEXT("Leading timecode \tDelta: +%s \tActual: %s \tEstimate: %s \tEstimate (unrounded): %s"),
*ToString(AbsDeltaTC), *ToString(ActualTC), *ToString(EstimatedTC), *ToString(EstimatedRoundedTC)
);
}
else
{
const FTimecode AbsDeltaTC = FQualifiedFrameTime(ActualFrameTime.Time - InEstimatedTime, InFrameRate).ToTimecode();
UE_CLOG(!bIsTimeEqual && InEstimatedTime < ActualFrameTime.Time, LogTimeManagement, Warning,
TEXT("Trailing timecode \tDelta: -%s \tActual: %s \tEstimate: %s \tEstimate (unrounded): %s"),
*ToString(AbsDeltaTC), *ToString(ActualTC), *ToString(EstimatedTC), *ToString(EstimatedRoundedTC)
);
}
}
}
FTimecodeEstimator::FTimecodeEstimator(
SIZE_T InNumSamples,
UTimecodeProvider& InTimecode, IClockedTimeStep& InEngineCustomTimeStep
)
// Counter-intuitively, we should NOT initialize the start times because it's too early... defer until the data actually starts being sampled.
// For example, if the custom time step was just changed, then FApp::CurrentTime may not contain the correct value, yet.
// Or the API user might construct now and only use FTimecodeEstimator much later.
: StartClockTime()
, TimecodeProvider(InTimecode)
, EngineCustomTimeStep(InEngineCustomTimeStep)
, ClockToTimecodeSamples(InNumSamples)
, LastFrameRate(InTimecode.GetFrameRate())
{
// There's no point in constructing FTimecodeEstimator if the number of linear regression samples is 0; it'll just use the latest value.
ensure(InNumSamples > 0);
}
TOptional<FFetchAndUpdateStats> FTimecodeEstimator::FetchAndUpdate()
{
const TOptional<double> ClockTime = EngineCustomTimeStep.GetUnderlyingClockTime_AnyThread();
if (!ClockTime)
{
return {};
}
if (!StartClockTime)
{
StartClockTime = ClockTime;
}
TimecodeProvider.FetchAndUpdate(); // FetchAndUpdate fetches the latest timecode value so below GetQualifiedFrameTime returns the lastest value.
const FQualifiedFrameTime CurrentFrameTime = TimecodeProvider.GetQualifiedFrameTime();
const FFrameRate CurrentFrameRate = CurrentFrameTime.Rate;
// In a true production environment, the frame rate of the timecode device should not really change on the fly, but we should handle it anyway.
if (CurrentFrameRate != LastFrameRate)
{
ClockToTimecodeSamples = FCachedLinearRegressionSums(ClockToTimecodeSamples.Samples.Capacity());
LastFrameRate = CurrentFrameRate;
}
// We regress based on relative time for numerical stability. See StartClockTime docstring.
// Clock values can be very big but double precision is best near 0.
const FFrameTime& FrameTime = CurrentFrameTime.Time;
const double FrameTimeAsSeconds = LastFrameRate.AsSeconds(FrameTime);
const double RelativeTime = *ClockTime - *StartClockTime;
AddSampleAndUpdateSums(FVector2d{ RelativeTime, FrameTimeAsSeconds }, ClockToTimecodeSamples);
ComputeLinearRegressionSlopeAndOffset(ClockToTimecodeSamples.CachedSums, LinearRegressionFunction);
constexpr bool bForceSignDisplay = false, bDisplaySubframe = true;
UE_CLOG(CVarLogSampling.GetValueOnAnyThread(), LogTimeManagement, Log,
TEXT("Sampling %f at %s\t\t(Clock: %f, \tApp: %f)"),
RelativeTime, *FQualifiedFrameTime(FrameTime, LastFrameRate).ToTimecode().ToString(bForceSignDisplay, bDisplaySubframe),
*ClockTime, FApp::GetCurrentTime()
);
return FFetchAndUpdateStats{ CurrentFrameTime };
}
FQualifiedFrameTime FTimecodeEstimator::EstimateFrameTime() const
{
if (!ClockToTimecodeSamples.IsEmpty()
&& ensureMsgf(StartClockTime, TEXT("Invariant: StartClockTime was supposed to have been set when the data was sampled!")))
{
const double RelativeTime = FApp::GetCurrentTime() - *StartClockTime;
const double EstimatedSeconds = LinearRegressionFunction.Evaluate(RelativeTime);
const FFrameTime UnroundedEstimatedTime = EstimatedSeconds * LastFrameRate;
// Subframe close to 0.5f? It may round to the wrong frame. Flag it so UE developer can investigate it, e.g. 0.47f is quite close 0.5f.
// We'd expect it to very close to the full frame, e.g. 14:11:43:16.04, or 14:11:43:16.87.
// A value like 14:11:43:16.42 is much closer to 0.5 than we'd expect... it may indicate that the time step is not linearly correlated
// to the timecode provider used (e.g. not really a fixed frame rate or some kind of noise).
const bool bUnclearEstimation = FMath::Abs(UnroundedEstimatedTime.GetSubFrame() - 0.5f) <= CVarUnclearEstimationSubframeThreshold.GetValueOnAnyThread();
constexpr bool bForceSignDisplay = false, bDisplaySubframe = true;
UE_CLOG(bUnclearEstimation, LogTimeManagement, Warning,
TEXT("Time %f estimate of %s is very close to 0.5 subframe border... the resulting timecode's frame may be off by 1."),
RelativeTime, *FQualifiedFrameTime(UnroundedEstimatedTime, LastFrameRate).ToTimecode().ToString(bForceSignDisplay, bDisplaySubframe)
);
// FApp::GetCurrentTime() is usually slightly behind, e.g. relative underlying clock may be 2.028365, but FApp::GetCurrentTime() is 2.028364.
// That can cause slight trailing (provider 14:11:43:17.00, est. 14:11:43:16.99), or leading (provider: 14:11:43:19.00, est. 14:11:43:19.01).
// We only care about full frames... we drop subframes. That's why slight leading is no problem as the frame number stays the same.
// Trailing *is* a problem though because the frame is one less.
// Note: this comment was written when UCatchupFixedRateCustomTimeStep was the only timestep that UTimecodeRegressionProvider was set up with:
// so this observation is untested with other time steps that may have been added later (5.7+). If you find that behaviour is different
// between time steps, then we need to adjust this strategy.
const FFrameTime RoundedEstimatedTime = UnroundedEstimatedTime.RoundToFrame();
LogEstimatedTime(RelativeTime, EngineCustomTimeStep, TimecodeProvider, RoundedEstimatedTime, UnroundedEstimatedTime, LastFrameRate);
return FQualifiedFrameTime(RoundedEstimatedTime, LastFrameRate);
}
// This may cause jumps at the beginning, but it can be circumvented by warming up the engine, i.e. just let it run for a few frames
UE_LOG(LogTimeManagement, Log, TEXT("No data sampled, yet. This frame will fall back to actual timecode without estimation."));
return TimecodeProvider.GetQualifiedFrameTime();
}
}