Files
UnrealEngine/Engine/Source/Runtime/HeadMountedDisplay/Private/MotionDelayBuffer.cpp
2025-05-18 13:04:45 +08:00

553 lines
17 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "MotionDelayBuffer.h"
#include "HAL/PlatformTime.h" // for FPlatformTime::Seconds()
#include "Components/SceneComponent.h"
#include "RenderingThread.h" // for ENQUEUE_RENDER_COMMAND
#include "SceneView.h" // for FSceneViewFamily
#include "Logging/LogMacros.h" // for DEFINE_LOG_CATEGORY_STATIC
#include "IXRTrackingSystem.h"
#include "IMotionController.h"
#include "Templates/TypeHash.h"
#include "Features/IModularFeatures.h"
#include "GameFramework/WorldSettings.h"
#include "Engine/Engine.h" // for GEngine->XRSystem
DEFINE_LOG_CATEGORY_STATIC(LogMotionDelayBuffer, Log, All);
/* FMotionDelayTarget
*****************************************************************************/
class FMotionDelayTarget : public TSharedFromThis<FMotionDelayTarget, ESPMode::ThreadSafe>
{
public:
struct FMotionSource
{
int32 PlayerIndex;
FName SourceId;
bool operator==(const FMotionSource& Rhs) const;
bool operator!=(const FMotionSource& Rhs) const;
};
FMotionDelayTarget(FMotionSource MotionSource);
public:
FLateUpdateManager LateUpdate;
FMotionSource MotionSource;
TCircularHistoryBuffer<FVector> ScaleHistoryBuffer;
};
FMotionDelayTarget::FMotionDelayTarget(FMotionDelayTarget::FMotionSource InMotionSource)
: MotionSource(InMotionSource)
{}
static uint32 GetTypeHash(const FMotionDelayTarget::FMotionSource& Lhs)
{
return HashCombine( GetTypeHash(Lhs.PlayerIndex), GetTypeHash(Lhs.SourceId) );
}
bool FMotionDelayTarget::FMotionSource::operator==(const FMotionSource& Rhs) const
{
return (PlayerIndex == Rhs.PlayerIndex) && (SourceId == Rhs.SourceId);
}
bool FMotionDelayTarget::FMotionSource::operator!=(const FMotionSource& Rhs) const
{
return !operator==(Rhs);
}
/* MotionDelayService_Impl
*****************************************************************************/
namespace MotionDelayService_Impl
{
struct FPoseSample
{
FPoseSample() : Position(FVector::ZeroVector), Orientation(FRotator::ZeroRotator), TimeStamp(0.0) {}
FVector Position;
FRotator Orientation;
double TimeStamp;
};
static TMap< FMotionDelayTarget::FMotionSource, TCircularHistoryBuffer<FPoseSample> > SourceSamples;
// shared ref since the render thread needs reliable access to the late update buffer
typedef TSharedRef<FMotionDelayTarget, ESPMode::ThreadSafe> FSharedDelayTarget;
static TMap<TWeakObjectPtr<USceneComponent>, FSharedDelayTarget> DelayTargets;
static TArray<TWeakPtr<FMotionDelayClient, ESPMode::ThreadSafe>> DelayClients;
static bool bIsEnabled = false;
static uint32 SharedBufferSizes = 0;
static float DefaultWorldToMetersScale = 100.f;
// frame ids to make sure we 'sync' and 'clean up' once per frame (triggered via, potentionally multiple, FMotionDelayClients)
static uint32 LastSyncFrameId = UINT_MAX;
static uint32 PostRenderCleanupId_RenderThread = UINT_MAX;
static double FrameSyncTime = 0.0;
template <typename F>
static FORCEINLINE void ForEachClient(F Action);
template <typename F>
static FORCEINLINE void ForEachTarget(F Action);
static int32 EstimateDelayIndex(uint32 MillisecDelay);
static uint32 FindNeededBufferSize();
static void RefreshDelayBufferSizes();
static void RefreshMotionSources();
static void SyncDelayBuffers(uint32 FrameId);
static bool SampleDevicePose(const FMotionDelayTarget::FMotionSource& TargetSource, FPoseSample& PoseOut);
static void PostRender_RenderThread(uint32 FrameId);
}
//------------------------------------------------------------------------------
template <typename F>
static FORCEINLINE void MotionDelayService_Impl::ForEachClient(F Action)
{
checkSlow(IsInGameThread());
for (int32 ClientIndex = DelayClients.Num() - 1; ClientIndex >= 0; --ClientIndex)
{
if (DelayClients[ClientIndex].IsValid())
{
Action(DelayClients[ClientIndex].Pin().Get());
}
else
{
DelayClients.RemoveAtSwap(ClientIndex);
}
}
}
//------------------------------------------------------------------------------
template <typename F>
static FORCEINLINE void MotionDelayService_Impl::ForEachTarget(F Action)
{
checkSlow(IsInGameThread());
TArray<TWeakObjectPtr<USceneComponent>> StaleTargets;
for (auto& Target : DelayTargets)
{
if (Target.Key.IsValid())
{
Action(Target.Key.Get(), Target.Value.Get());
}
else
{
StaleTargets.Add(Target.Key);
}
}
for (TWeakObjectPtr<USceneComponent>& StalePtr : StaleTargets)
{
DelayTargets.Remove(StalePtr);
}
}
//------------------------------------------------------------------------------
static int32 MotionDelayService_Impl::EstimateDelayIndex(uint32 MillisecDelay)
{
const float FixedFrameRateMs = 1000.f * 1.f / 90.f;
return FMath::CeilToInt(MillisecDelay / FixedFrameRateMs);
}
//------------------------------------------------------------------------------
static uint32 MotionDelayService_Impl::FindNeededBufferSize()
{
uint32 SharedSize = 0;
auto FindMaxSize = [&SharedSize](FMotionDelayClient* Client)
{
const uint32 NeededSize = EstimateDelayIndex(Client->GetDesiredDelay()) + 1;
SharedSize = FMath::Max(SharedSize, NeededSize);
};
ForEachClient(FindMaxSize);
return SharedSize;
}
//------------------------------------------------------------------------------
static void MotionDelayService_Impl::RefreshDelayBufferSizes()
{
const uint32 RequiredBufferSize = FindNeededBufferSize();
if (SharedBufferSizes != RequiredBufferSize)
{
for (auto& Sampler : SourceSamples)
{
Sampler.Value.Resize(RequiredBufferSize);
}
auto ResizeTarget = [RequiredBufferSize](USceneComponent* /*Component*/, FMotionDelayTarget& Target)
{
Target.ScaleHistoryBuffer.Resize(RequiredBufferSize);
};
ForEachTarget(ResizeTarget);
SharedBufferSizes = RequiredBufferSize;
}
}
//------------------------------------------------------------------------------
static void MotionDelayService_Impl::RefreshMotionSources()
{
TArray<FMotionDelayTarget::FMotionSource> UneededSources;
UneededSources.Reserve(SourceSamples.Num());
for (auto& Sampler : SourceSamples)
{
UneededSources.Add(Sampler.Key);
}
auto RemovedUsedSource = [&UneededSources](USceneComponent* /*Component*/, FMotionDelayTarget& Target)
{
UneededSources.Remove(Target.MotionSource);
};
ForEachTarget(RemovedUsedSource);
for (const FMotionDelayTarget::FMotionSource& Source : UneededSources)
{
SourceSamples.Remove(Source);
}
}
//------------------------------------------------------------------------------
static void MotionDelayService_Impl::SyncDelayBuffers(uint32 FrameId)
{
checkSlow(IsInGameThread());
if (FrameId != LastSyncFrameId)
{
RefreshDelayBufferSizes();
for (auto& Sampler : SourceSamples)
{
FPoseSample NewSample;
if (!SampleDevicePose(Sampler.Key, NewSample))
{
if (Sampler.Value.Num() > 0)
{
const FPoseSample& LastSample = Sampler.Value[0];
NewSample.Position = LastSample.Position;
NewSample.Orientation = LastSample.Orientation;
}
}
Sampler.Value.Add(NewSample);
}
auto RemovedUsedSource = [](USceneComponent* Component, FMotionDelayTarget& Target)
{
UWorld* World = Component->GetWorld();
const float WorldToMeteresScale = World ? World->GetWorldSettings()->WorldToMeters : DefaultWorldToMetersScale;
FVector FrameScale(WorldToMeteresScale / DefaultWorldToMetersScale);
FrameScale *= Component->GetComponentScale();
Target.ScaleHistoryBuffer.Add(FrameScale);
const USceneComponent* AttachParent = Component->GetAttachParent();
FTransform ParentTransform = AttachParent ? AttachParent->GetComponentTransform() : FTransform::Identity;
Target.LateUpdate.Setup(ParentTransform, Component, false);
};
ForEachTarget(RemovedUsedSource);
LastSyncFrameId = FrameId;
FrameSyncTime = FPlatformTime::Seconds();
}
}
//------------------------------------------------------------------------------
static bool MotionDelayService_Impl::SampleDevicePose(const FMotionDelayTarget::FMotionSource& TargetSource, FPoseSample& PoseOut)
{
PoseOut = FPoseSample();
PoseOut.TimeStamp = FPlatformTime::Seconds();
bool bSuccess = false;
TArray<IMotionController*> MotionControllers = IModularFeatures::Get().GetModularFeatureImplementations<IMotionController>(IMotionController::GetModularFeatureName());
for (IMotionController* MotionController : MotionControllers)
{
if (MotionController != nullptr)
{
if (MotionController->GetControllerOrientationAndPosition(TargetSource.PlayerIndex, TargetSource.SourceId, PoseOut.Orientation, PoseOut.Position, DefaultWorldToMetersScale))
{
bSuccess = true;
break;
}
}
}
if (TargetSource.SourceId == IMotionController::HMDSourceId)
{
IXRTrackingSystem* TrackingSys = GEngine->XRSystem.Get();
if (TrackingSys)
{
FQuat OrientationQuat;
if (TrackingSys->GetCurrentPose(IXRTrackingSystem::HMDDeviceId, OrientationQuat, PoseOut.Position))
{
PoseOut.Orientation = OrientationQuat.Rotator();
bSuccess = true;
}
}
}
return bSuccess;
}
/* FMotionDelayService
*****************************************************************************/
//------------------------------------------------------------------------------
void FMotionDelayService::SetEnabled(bool bEnable)
{
MotionDelayService_Impl::bIsEnabled = bEnable;
}
//------------------------------------------------------------------------------
bool FMotionDelayService::RegisterDelayTarget(USceneComponent* MotionControlledComponent, const int32 PlayerIndex, const FName SourceId)
{
using namespace MotionDelayService_Impl;
if (bIsEnabled)
{
FMotionDelayTarget::FMotionSource TargetSource;
TargetSource.PlayerIndex = PlayerIndex;
TargetSource.SourceId = SourceId;
if (FSharedDelayTarget* DelayTarget = DelayTargets.Find(MotionControlledComponent))
{
if (DelayTarget->Get().MotionSource != TargetSource)
{
DelayTarget->Get().MotionSource = TargetSource;
RefreshMotionSources();
}
}
else
{
FSharedDelayTarget NewDelayTarget = DelayTargets.Add(MotionControlledComponent, MakeShareable(new FMotionDelayTarget(TargetSource)));
if (SharedBufferSizes > 0)
{
NewDelayTarget->ScaleHistoryBuffer.Resize(SharedBufferSizes);
}
}
if (!SourceSamples.Contains(TargetSource))
{
TCircularHistoryBuffer<FPoseSample>& SampleBuffer = SourceSamples.Add(TargetSource);
if (SharedBufferSizes > 0)
{
SampleBuffer.Resize(SharedBufferSizes);
}
}
}
return bIsEnabled;
}
//------------------------------------------------------------------------------
void FMotionDelayService::RegisterDelayClient(TSharedRef<FMotionDelayClient, ESPMode::ThreadSafe> DelayClient)
{
using namespace MotionDelayService_Impl;
DelayClients.Add(DelayClient);
RefreshDelayBufferSizes();
}
/* FMotionDelayClient
*****************************************************************************/
namespace MotionDelayClient_Impl
{
using namespace MotionDelayService_Impl;
static void CalulateDelayTransform(uint32 DesiredDelay, const TCircularHistoryBuffer<FPoseSample>& SampleBuffer, const TCircularHistoryBuffer<FVector> ScaleBuffer, FTransform& TransformOut);
}
//------------------------------------------------------------------------------
static void MotionDelayClient_Impl::CalulateDelayTransform(uint32 DesiredDelay, const TCircularHistoryBuffer<MotionDelayService_Impl::FPoseSample>& SampleBuffer, const TCircularHistoryBuffer<FVector> ScaleBuffer, FTransform& TransformOut)
{
checkSlow(IsInGameThread());
int32 SampleAIndex = SampleBuffer.Num() - 1, SampleBIndex = INDEX_NONE;
const int32 EstimatedIndex = FMath::Min(SampleAIndex, EstimateDelayIndex(DesiredDelay));
const double DelaySeconds = DesiredDelay / 1000.f;
// need to use the same current time for all delay transforms in a single frame, so that all
// the calculated (interpolated) transforms match up relative to each other
// NOTE: this is not what the current time will be by the time this renders, so you may have to fold in some extra delay
const double CurrentTime = MotionDelayService_Impl::FrameSyncTime;
int8 IncrementDir = 0;
for (int32 SampleIndex = EstimatedIndex; SampleIndex < SampleBuffer.Num() && SampleIndex >= 0; SampleIndex += IncrementDir)
{
const FPoseSample& Sample = SampleBuffer[SampleIndex];
const double TimeSince = CurrentTime - Sample.TimeStamp;
if (IncrementDir == 0)
{
IncrementDir = (TimeSince >= DelaySeconds) ? -1 : +1;
}
else if ((-IncrementDir + 1) / 2 != (TimeSince >= DelaySeconds))
{
SampleAIndex = SampleIndex;
SampleBIndex = SampleIndex - IncrementDir;
break;
}
}
auto MakeTransform = [&SampleBuffer, &ScaleBuffer](int32 Index)->FTransform
{
const FPoseSample& Sample = SampleBuffer[Index];
const FVector& ComponentScale = ScaleBuffer[Index];
return FTransform(Sample.Orientation, Sample.Position, ComponentScale);
};
if (SampleBIndex == INDEX_NONE)
{
UE_CLOG(SampleBuffer.IsFull(), LogMotionDelayBuffer, Warning, TEXT("Not enough space in this motion delay buffer to accomedate the desired delay."));
if (IncrementDir > 0)
{
TransformOut = MakeTransform(SampleAIndex);
}
else if (IncrementDir < 0)
{
TransformOut = MakeTransform(0);
}
}
else
{
const FPoseSample& SampleA = SampleBuffer[SampleAIndex];
const FPoseSample& SampleB = SampleBuffer[SampleBIndex];
FTransform TransformA = MakeTransform(SampleAIndex);
FTransform TransformB = MakeTransform(SampleBIndex);
const float BlendAlpha = (float)((CurrentTime - SampleA.TimeStamp - DelaySeconds) / (SampleB.TimeStamp - SampleA.TimeStamp));
TransformOut.Blend(TransformA, TransformB, BlendAlpha);
}
}
//------------------------------------------------------------------------------
FMotionDelayClient::FMotionDelayClient(const FAutoRegister& AutoRegister)
: FSceneViewExtensionBase(AutoRegister)
{}
//------------------------------------------------------------------------------
void FMotionDelayClient::BeginRenderViewFamily(FSceneViewFamily& ViewFamily)
{
using namespace MotionDelayService_Impl;
// ensure that we take a sample for this frame
SyncDelayBuffers(ViewFamily.FrameNumber);
TArray<FTargetTransform> RenderTransforms;
RenderTransforms.Reserve(DelayTargets.Num());
TArray<USceneComponent*> ExemptTargets;
GetExemptTargets(ExemptTargets);
auto IsTargetExempt = [&ExemptTargets](const TWeakObjectPtr<USceneComponent>& DelayTarget)->bool
{
bool bExempt = !DelayTarget.IsValid();
if (!bExempt)
{
USceneComponent* Target = DelayTarget.Get();
bExempt = ExemptTargets.Contains(Target);
}
return bExempt;
};
const uint32 DesiredLatency = GetDesiredDelay();
for (auto& Target : DelayTargets)
{
if (IsTargetExempt(Target.Key))
{
continue;
}
TCircularHistoryBuffer<FPoseSample>* SampleBuffer = SourceSamples.Find(Target.Value->MotionSource);
if (ensure(SampleBuffer))
{
FTargetTransform RenderTransform;
RenderTransform.DelayTarget = Target.Value;
RenderTransform.DelayTransform = RenderTransform.RestoreTransform = Target.Key->GetRelativeTransform();
MotionDelayClient_Impl::CalulateDelayTransform(DesiredLatency, *SampleBuffer, Target.Value->ScaleHistoryBuffer, RenderTransform.DelayTransform);
RenderTransforms.Add(RenderTransform);
}
}
TWeakPtr<FMotionDelayClient, ESPMode::ThreadSafe> ThisPtr = SharedThis(this);
ENQUEUE_RENDER_COMMAND(MotionDelayClientSetup)(
[ThisPtr, RenderTransforms](FRHICommandListImmediate& /*RHICmdList*/)
{
if (ThisPtr.IsValid())
{
ThisPtr.Pin()->TargetTransforms_RenderThread = RenderTransforms;
}
}
);
}
//------------------------------------------------------------------------------
void FMotionDelayClient::PostRenderViewFamily_RenderThread(FRDGBuilder& GraphBuilder, FSceneViewFamily& ViewFamily)
{
if (MotionDelayService_Impl::PostRenderCleanupId_RenderThread != ViewFamily.FrameNumber)
{
MotionDelayService_Impl::PostRenderCleanupId_RenderThread = ViewFamily.FrameNumber;
}
}
//------------------------------------------------------------------------------
int32 FMotionDelayClient::GetPriority() const
{
return -5;
}
//------------------------------------------------------------------------------
bool FMotionDelayClient::IsActiveThisFrame_Internal(const FSceneViewExtensionContext&) const
{
using namespace MotionDelayService_Impl;
const uint32 DesiredLatency = GetDesiredDelay();
return DesiredLatency > 0 && DelayTargets.Num() > 0;
}
//------------------------------------------------------------------------------
void FMotionDelayClient::Apply_RenderThread(FSceneInterface* Scene)
{
for (const FTargetTransform& Transform : TargetTransforms_RenderThread)
{
Transform.DelayTarget->LateUpdate.Apply_RenderThread(Scene, Transform.RestoreTransform, Transform.DelayTransform);
}
}
//------------------------------------------------------------------------------
void FMotionDelayClient::Restore_RenderThread(FSceneInterface* Scene)
{
for (const FTargetTransform& Transform : TargetTransforms_RenderThread)
{
Transform.DelayTarget->LateUpdate.Apply_RenderThread(Scene, Transform.DelayTransform, Transform.RestoreTransform);
}
}
//------------------------------------------------------------------------------
bool FMotionDelayClient::FindDelayTransform(USceneComponent* Target, uint32 Delay, FTransform& TransformOut)
{
using namespace MotionDelayService_Impl;
bool bFoundTransform = false;
if (FSharedDelayTarget* TargetData = DelayTargets.Find(Target))
{
TCircularHistoryBuffer<FPoseSample>* SampleBuffer = SourceSamples.Find((*TargetData)->MotionSource);
if (ensure(SampleBuffer))
{
MotionDelayClient_Impl::CalulateDelayTransform(Delay, *SampleBuffer, (*TargetData)->ScaleHistoryBuffer, TransformOut);
bFoundTransform = true;
}
}
return bFoundTransform;
}