Files
UnrealEngineUWP/Engine/Source/Runtime/HeadMountedDisplay/Private/MotionDelayBuffer.cpp
Jeff Fisher 25ff24a7b1 UE-181824 Move HeadMountedDisplay files to XRBase plugin to reduce UE overhead.
-Moving much of engine/source/runtime/headmounteddisplay to a plugin mainly to reduce minimum UE executable size and memory use.  But this is also nice for organization.  The ideal would be to move everything except interfaces used by core engine. VREditor pulling in UMotionControllerComponent is the main blocker at this time.  That dependency is expected to go away eventually at which point we can do the rest of this transfer.
-Added dependencies to XRBase plugin or module as necessary, refactored to avoid some dependencies, removed a few unnecessary dependencies.
-Stripped vestigial VR references from a few project templates.  Mostly just unused HeadMountedDisplay module refs, but a tiny bit of code from TP_Puzzle.
-Fixed dependency cycle ignores related to MaterialShaderQualitySettings PIEPreviewDeviceProfileSelector and UnrealEd.  For reasons I do not understand the HeadMountedDisplay dependency on AugmentedReality was preventing the detection of those, long existent, cycles.
#jira UE-181824
#review
#rb Robert.Srinivasiah
#preflight 642f3cbf4c3ccbbdf1990a1f  6434291c28551807175e1142

[CL 24984065 by Jeff Fisher in ue5-main branch]
2023-04-10 17:12:32 -04: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;
}