// @lint-ignore-every LICENSELINT // Copyright Epic Games, Inc. All Rights Reserved. #include "OculusXRHMD.h" #include "OculusXRHMDPrivateRHI.h" #include "EngineAnalytics.h" #include "Interfaces/IAnalyticsProvider.h" #include "AnalyticsEventAttribute.h" #include "Slate/SceneViewport.h" #include "PostProcess/PostProcessHMD.h" #include "PostProcess/SceneRenderTargets.h" #include "HardwareInfo.h" #include "ScreenRendering.h" #include "GameFramework/PlayerController.h" #include "Math/UnrealMathUtility.h" #include "Math/TranslationMatrix.h" #include "Widgets/SViewport.h" #include "Layout/WidgetPath.h" #include "Framework/Application/SlateApplication.h" #include "Engine/Canvas.h" #include "Engine/GameEngine.h" #include "Engine/RendererSettings.h" #include "Misc/CoreDelegates.h" #include "GameFramework/WorldSettings.h" #include "Engine/StaticMesh.h" #include "Engine/StaticMeshActor.h" #include "Components/InstancedStaticMeshComponent.h" #include "Misc/EngineVersion.h" #include "ClearQuad.h" #include "DynamicResolutionState.h" #include "DynamicResolutionProxy.h" #include "OculusXRHMDRuntimeSettings.h" #include "OculusXRDelegates.h" #include "DataDrivenShaderPlatformInfo.h" #include "GenericPlatform/GenericPlatformMath.h" #include "LegacyScreenPercentageDriver.h" #if PLATFORM_ANDROID #include "Android/AndroidJNI.h" #include "Android/AndroidApplication.h" #include "HAL/IConsoleManager.h" #include "AndroidPermissionFunctionLibrary.h" #include "AndroidPermissionCallbackProxy.h" #endif #include "OculusShaders.h" #include "PipelineStateCache.h" #include "IOculusXRMRModule.h" #if WITH_EDITOR #include "Editor/UnrealEd/Classes/Editor/EditorEngine.h" #include "Settings/LevelEditorPlaySettings.h" #endif #if !UE_BUILD_SHIPPING #include "Debug/DebugDrawService.h" #endif #if OCULUS_HMD_SUPPORTED_PLATFORMS static TAutoConsoleVariable CVarOculusEnableSubsampledLayout( TEXT("r.Mobile.Oculus.EnableSubsampled"), 0, TEXT("0: Disable subsampled layout (Default)\n") TEXT("1: Enable subsampled layout on supported platforms\n"), ECVF_Scalability | ECVF_RenderThreadSafe); static TAutoConsoleVariable CVarOculusEnableLowLatencyVRS( TEXT("r.Mobile.Oculus.EnableLowLatencyVRS"), 0, TEXT("0: Disable late update of VRS textures (Default)\n") TEXT("1: Enable late update of VRS textures\n"), ECVF_Scalability | ECVF_RenderThreadSafe); static TAutoConsoleVariable CVarOculusForceSymmetric( TEXT("r.Mobile.Oculus.ForceSymmetric"), 0, TEXT("0: Use standard runtime-provided projection matrices (Default)\n") TEXT("1: Render both eyes with a symmetric projection, union of both FOVs (and corresponding higher rendertarget size to maintain PD)\n"), ECVF_Scalability | ECVF_RenderThreadSafe); // AppSpaceWarp static TAutoConsoleVariable CVarOculusEnableSpaceWarpUser( TEXT("r.Mobile.Oculus.SpaceWarp.Enable"), 0, TEXT("0 Disable spacewarp at runtime.\n") TEXT("1 Enable spacewarp at runtime.\n"), ECVF_Scalability | ECVF_RenderThreadSafe); static TAutoConsoleVariable CVarOculusEnableSpaceWarpInternal( TEXT("r.Mobile.Oculus.SpaceWarp.EnableInternal"), 0, TEXT("0 Disable spacewarp, for internal engine checking, don't modify.\n") TEXT("1 Enable spacewarp, for internal enegine checking, don't modify.\n"), ECVF_Scalability | ECVF_RenderThreadSafe); // Foveated Rendering static TAutoConsoleVariable CVarOculusFoveatedRenderingMethod( TEXT("r.Mobile.Oculus.FoveatedRendering.Method"), -1, TEXT("0 Fixed Foveated Rendering.\n") TEXT("1 Eye-Tracked Foveated Rendering.\n"), ECVF_Scalability | ECVF_RenderThreadSafe); static TAutoConsoleVariable CVarOculusFoveatedRenderingLevel( TEXT("r.Mobile.Oculus.FoveatedRendering.Level"), -1, TEXT("0 Off.\n") TEXT("1 Low.\n") TEXT("2 Medium.\n") TEXT("3 High.\n") TEXT("4 High Top.\n"), ECVF_Scalability | ECVF_RenderThreadSafe); static TAutoConsoleVariable CVarOculusDynamicFoveatedRendering( TEXT("r.Mobile.Oculus.FoveatedRendering.Dynamic"), -1, TEXT("0 Disable Dynamic Foveated Rendering at runtime.\n") TEXT("1 Enable Dynamic Foveated Rendering at runtime.\n"), ECVF_Scalability | ECVF_RenderThreadSafe); static TAutoConsoleVariable CVarOculusDynamicResolutionPixelDensity( TEXT("r.Oculus.DynamicResolution.PixelDensity"), 0, TEXT("0 Static Pixel Density corresponding to Pixel Density 1.0 (default)\n") TEXT(">0 Manual Pixel Density Override\n"), ECVF_Scalability | ECVF_RenderThreadSafe); #define OCULUS_PAUSED_IDLE_FPS 10 static const FString USE_SCENE_PERMISSION_NAME("com.oculus.permission.USE_SCENE"); namespace OculusXRHMD { #if !UE_BUILD_SHIPPING static void __cdecl OvrpLogCallback(ovrpLogLevel level, const char* message) { FString tbuf = ANSI_TO_TCHAR(message); const TCHAR* levelStr = TEXT(""); switch (level) { case ovrpLogLevel_Debug: levelStr = TEXT(" Debug:"); break; case ovrpLogLevel_Info: levelStr = TEXT(" Info:"); break; case ovrpLogLevel_Error: levelStr = TEXT(" Error:"); break; } GLog->Logf(TEXT("OCULUS:%s %s"), levelStr, *tbuf); } #endif // !UE_BUILD_SHIPPING //------------------------------------------------------------------------------------------------- // FOculusXRHMD //------------------------------------------------------------------------------------------------- const FName FOculusXRHMD::OculusSystemName(TEXT("OculusXRHMD")); FName FOculusXRHMD::GetSystemName() const { return OculusSystemName; } int32 FOculusXRHMD::GetXRSystemFlags() const { return EXRSystemFlags::IsHeadMounted; } FString FOculusXRHMD::GetVersionString() const { const char* Version; if (OVRP_FAILURE(FOculusXRHMDModule::GetPluginWrapper().GetVersion2(&Version))) { Version = "Unknown"; } return FString::Printf(TEXT("OVRPlugin: %s"), UTF8_TO_TCHAR(Version)); } bool FOculusXRHMD::DoesSupportPositionalTracking() const { ovrpBool trackingPositionSupported; return OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetTrackingPositionSupported2(&trackingPositionSupported)) && trackingPositionSupported; } bool FOculusXRHMD::HasValidTrackingPosition() { ovrpBool nodePositionTracked; return OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetNodePositionTracked2(ovrpNode_Head, &nodePositionTracked)) && nodePositionTracked; } struct TrackedDevice { ovrpNode Node; EXRTrackedDeviceType Type; }; static TrackedDevice TrackedDevices[] = { { ovrpNode_Head, EXRTrackedDeviceType::HeadMountedDisplay }, { ovrpNode_HandLeft, EXRTrackedDeviceType::Controller }, { ovrpNode_HandRight, EXRTrackedDeviceType::Controller }, { ovrpNode_TrackerZero, EXRTrackedDeviceType::TrackingReference }, { ovrpNode_TrackerOne, EXRTrackedDeviceType::TrackingReference }, { ovrpNode_TrackerTwo, EXRTrackedDeviceType::TrackingReference }, { ovrpNode_TrackerThree, EXRTrackedDeviceType::TrackingReference }, { ovrpNode_DeviceObjectZero, EXRTrackedDeviceType::Other }, }; static uint32 TrackedDeviceCount = sizeof(TrackedDevices) / sizeof(TrackedDevices[0]); bool FOculusXRHMD::EnumerateTrackedDevices(TArray& OutDevices, EXRTrackedDeviceType Type) { CheckInGameThread(); for (uint32 TrackedDeviceId = 0; TrackedDeviceId < TrackedDeviceCount; TrackedDeviceId++) { if (Type == EXRTrackedDeviceType::Any || Type == TrackedDevices[TrackedDeviceId].Type) { ovrpBool nodePresent; ovrpNode Node = TrackedDevices[TrackedDeviceId].Node; if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetNodePresent2(Node, &nodePresent)) && nodePresent) { const int32 ExternalDeviceId = OculusXRHMD::ToExternalDeviceId(Node); OutDevices.Add(ExternalDeviceId); } } } return true; } void FOculusXRHMD::UpdateRTPoses() { CheckInRenderThread(); FGameFrame* CurrentFrame = GetFrame_RenderThread(); if (CurrentFrame) { if (!CurrentFrame->Flags.bRTLateUpdateDone) { FOculusXRHMDModule::GetPluginWrapper().Update3(ovrpStep_Render, CurrentFrame->FrameNumber, 0.0); CurrentFrame->Flags.bRTLateUpdateDone = true; } } // else, Frame_RenderThread has already been reset/rendered (or not created yet). // This can happen when DoEnableStereo() is called, as SetViewportSize (which it calls) enques a render // immediately - meaning two render frames were enqueued in the span of one game tick. } bool FOculusXRHMD::GetCurrentPose(int32 InDeviceId, FQuat& OutOrientation, FVector& OutPosition) { OutOrientation = FQuat::Identity; OutPosition = FVector::ZeroVector; if ((size_t)InDeviceId >= TrackedDeviceCount) { return false; } ovrpNode Node = OculusXRHMD::ToOvrpNode(InDeviceId); const FSettings* CurrentSettings; FGameFrame* CurrentFrame; if (InRenderThread()) { CurrentSettings = GetSettings_RenderThread(); CurrentFrame = GetFrame_RenderThread(); UpdateRTPoses(); } else if (InGameThread()) { CurrentSettings = GetSettings(); CurrentFrame = NextFrameToRender.Get(); } else { return false; } if (!CurrentSettings || !CurrentFrame) { return false; } ovrpPoseStatef PoseState; FPose Pose; if (OVRP_FAILURE(FOculusXRHMDModule::GetPluginWrapper().GetNodePoseState3(ovrpStep_Render, CurrentFrame->FrameNumber, Node, &PoseState)) || !ConvertPose_Internal(PoseState.Pose, Pose, CurrentSettings, CurrentFrame->WorldToMetersScale)) { return false; } OutPosition = Pose.Position; OutOrientation = Pose.Orientation; return true; } bool FOculusXRHMD::GetRelativeEyePose(int32 InDeviceId, int32 ViewIndex, FQuat& OutOrientation, FVector& OutPosition) { OutOrientation = FQuat::Identity; OutPosition = FVector::ZeroVector; if (InDeviceId != HMDDeviceId) { return false; } ovrpNode Node; switch (ViewIndex) { case EStereoscopicEye::eSSE_LEFT_EYE: Node = ovrpNode_EyeLeft; break; case EStereoscopicEye::eSSE_RIGHT_EYE: Node = ovrpNode_EyeRight; break; case EStereoscopicEye::eSSE_MONOSCOPIC: Node = ovrpNode_EyeCenter; break; default: return false; } const FSettings* CurrentSettings; FGameFrame* CurrentFrame; if (InRenderThread()) { CurrentSettings = GetSettings_RenderThread(); CurrentFrame = GetFrame_RenderThread(); UpdateRTPoses(); } else if (InGameThread()) { CurrentSettings = GetSettings(); CurrentFrame = NextFrameToRender.Get(); } else { return false; } if (!CurrentSettings || !CurrentFrame) { return false; } ovrpPoseStatef HmdPoseState, EyePoseState; if (OVRP_FAILURE(FOculusXRHMDModule::GetPluginWrapper().GetNodePoseState3(ovrpStep_Render, CurrentFrame->FrameNumber, ovrpNode_Head, &HmdPoseState)) || OVRP_FAILURE(FOculusXRHMDModule::GetPluginWrapper().GetNodePoseState3(ovrpStep_Render, CurrentFrame->FrameNumber, Node, &EyePoseState))) { return false; } FPose HmdPose, EyePose; HmdPose.Orientation = ToFQuat(HmdPoseState.Pose.Orientation); HmdPose.Position = ToFVector(HmdPoseState.Pose.Position) * CurrentFrame->WorldToMetersScale; EyePose.Orientation = ToFQuat(EyePoseState.Pose.Orientation); EyePose.Position = ToFVector(EyePoseState.Pose.Position) * CurrentFrame->WorldToMetersScale; FQuat HmdOrientationInv = HmdPose.Orientation.Inverse(); OutOrientation = HmdOrientationInv * EyePose.Orientation; OutOrientation.Normalize(); OutPosition = HmdOrientationInv.RotateVector(EyePose.Position - HmdPose.Position); return true; } bool FOculusXRHMD::GetTrackingSensorProperties(int32 InDeviceId, FQuat& OutOrientation, FVector& OutPosition, FXRSensorProperties& OutSensorProperties) { CheckInGameThread(); if ((size_t)InDeviceId >= TrackedDeviceCount) { return false; } ovrpNode Node = OculusXRHMD::ToOvrpNode(InDeviceId); ovrpPoseStatef PoseState; FPose Pose; ovrpFrustum2f Frustum; if (OVRP_FAILURE(FOculusXRHMDModule::GetPluginWrapper().GetNodePoseState3(ovrpStep_Render, OVRP_CURRENT_FRAMEINDEX, Node, &PoseState)) || !ConvertPose(PoseState.Pose, Pose) || OVRP_FAILURE(FOculusXRHMDModule::GetPluginWrapper().GetNodeFrustum2(Node, &Frustum))) { return false; } OutPosition = Pose.Position; OutOrientation = Pose.Orientation; OutSensorProperties.LeftFOV = FMath::RadiansToDegrees(FMath::Atan(Frustum.Fov.LeftTan)); OutSensorProperties.RightFOV = FMath::RadiansToDegrees(FMath::Atan(Frustum.Fov.RightTan)); OutSensorProperties.TopFOV = FMath::RadiansToDegrees(FMath::Atan(Frustum.Fov.UpTan)); OutSensorProperties.BottomFOV = FMath::RadiansToDegrees(FMath::Atan(Frustum.Fov.DownTan)); OutSensorProperties.NearPlane = Frustum.zNear * Frame->WorldToMetersScale; OutSensorProperties.FarPlane = Frustum.zFar * Frame->WorldToMetersScale; OutSensorProperties.CameraDistance = 1.0f * Frame->WorldToMetersScale; return true; } void FOculusXRHMD::SetTrackingOrigin(EHMDTrackingOrigin::Type InOrigin) { TrackingOrigin = InOrigin; ovrpTrackingOrigin ovrpOrigin = ovrpTrackingOrigin_EyeLevel; if (InOrigin == EHMDTrackingOrigin::Floor) ovrpOrigin = ovrpTrackingOrigin_FloorLevel; if (InOrigin == EHMDTrackingOrigin::Stage) ovrpOrigin = ovrpTrackingOrigin_Stage; if (FOculusXRHMDModule::GetPluginWrapper().GetInitialized()) { EHMDTrackingOrigin::Type lastOrigin = GetTrackingOrigin(); FOculusXRHMDModule::GetPluginWrapper().SetTrackingOriginType2(ovrpOrigin); OCFlags.NeedSetTrackingOrigin = false; if (lastOrigin != InOrigin) Settings->BaseOffset = FVector::ZeroVector; } OnTrackingOriginChanged(); } EHMDTrackingOrigin::Type FOculusXRHMD::GetTrackingOrigin() const { EHMDTrackingOrigin::Type rv = EHMDTrackingOrigin::Eye; ovrpTrackingOrigin ovrpOrigin = ovrpTrackingOrigin_EyeLevel; if (FOculusXRHMDModule::GetPluginWrapper().GetInitialized() && OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetTrackingOriginType2(&ovrpOrigin))) { switch (ovrpOrigin) { case ovrpTrackingOrigin_EyeLevel: rv = EHMDTrackingOrigin::Eye; break; case ovrpTrackingOrigin_FloorLevel: rv = EHMDTrackingOrigin::Floor; break; case ovrpTrackingOrigin_Stage: rv = EHMDTrackingOrigin::Stage; break; default: UE_LOG(LogHMD, Error, TEXT("Unsupported ovr tracking origin type %d"), int(ovrpOrigin)); break; } } return rv; } bool FOculusXRHMD::GetFloorToEyeTrackingTransform(FTransform& OutFloorToEye) const { float EyeHeight = 0.f; const bool bSuccess = FOculusXRHMDModule::GetPluginWrapper().GetInitialized() && OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetUserEyeHeight2(&EyeHeight)); OutFloorToEye = FTransform(FVector(0.f, 0.f, -ConvertFloat_M2U(EyeHeight))); return bSuccess; } void FOculusXRHMD::ResetOrientationAndPosition(float yaw) { Recenter(RecenterOrientationAndPosition, yaw); } void FOculusXRHMD::ResetOrientation(float yaw) { Recenter(RecenterOrientation, yaw); } void FOculusXRHMD::ResetPosition() { Recenter(RecenterPosition, 0); } void FOculusXRHMD::Recenter(FRecenterTypes RecenterType, float Yaw) { CheckInGameThread(); if (NextFrameToRender) { const bool floorLevel = GetTrackingOrigin() != EHMDTrackingOrigin::Eye; ovrpPoseStatef poseState; FOculusXRHMDModule::GetPluginWrapper().Update3(ovrpStep_Render, NextFrameToRender->FrameNumber, 0.0); FOculusXRHMDModule::GetPluginWrapper().GetNodePoseState3(ovrpStep_Render, NextFrameToRender->FrameNumber, ovrpNode_Head, &poseState); if (RecenterType & RecenterPosition) { Settings->BaseOffset = ToFVector(poseState.Pose.Position); if (floorLevel) Settings->BaseOffset.Z = 0; } if (RecenterType & RecenterOrientation) { Settings->BaseOrientation = FRotator(0, FRotator(ToFQuat(poseState.Pose.Orientation)).Yaw - Yaw, 0).Quaternion(); } } } void FOculusXRHMD::SetBaseRotation(const FRotator& BaseRot) { SetBaseOrientation(BaseRot.Quaternion()); } FRotator FOculusXRHMD::GetBaseRotation() const { return GetBaseOrientation().Rotator(); } void FOculusXRHMD::SetBaseOrientation(const FQuat& BaseOrient) { CheckInGameThread(); Settings->BaseOrientation = BaseOrient; } FQuat FOculusXRHMD::GetBaseOrientation() const { CheckInGameThread(); return Settings->BaseOrientation; } bool FOculusXRHMD::IsHeadTrackingEnforced() const { if (IsInGameThread()) { return Settings.IsValid() && Settings->Flags.bHeadTrackingEnforced; } else { CheckInRenderThread(); return Settings_RenderThread.IsValid() && Settings_RenderThread->Flags.bHeadTrackingEnforced; } } void FOculusXRHMD::SetHeadTrackingEnforced(bool bEnabled) { CheckInGameThread(); check(Settings.IsValid()); const bool bOldValue = Settings->Flags.bHeadTrackingEnforced; Settings->Flags.bHeadTrackingEnforced = bEnabled; if (!bEnabled) { ResetControlRotation(); } else if (!bOldValue) { InitDevice(); } } bool FOculusXRHMD::IsHeadTrackingAllowed() const { bool bNeedEnableStereo = false; CheckInGameThread(); if (!FOculusXRHMDModule::GetPluginWrapper().GetInitialized()) { return false; } #if PLATFORM_WINDOWS // TODO: This is a temp fix of the case that callers wants to use IsHeadTrackingAllowed() to do something in UGameEngine::Start(). // Settings->Flags.bStereoEnabled won't be true until Window.IsValid() and UGameEngine::Tick() starts which is very late. // We might need a better mechanism to decouple Window.IsValid() and Settings->Flags.bStereoEnabled. bNeedEnableStereo = !GIsEditor && Flags.bNeedEnableStereo; #endif return (FHeadMountedDisplayBase::IsHeadTrackingAllowed() || bNeedEnableStereo); } void FOculusXRHMD::OnBeginPlay(FWorldContext& InWorldContext) { CheckInGameThread(); CachedViewportWidget.Reset(); CachedWindow.Reset(); bHardOcclusionsEnabled = false; #if WITH_EDITOR // @TODO: add more values here. // This call make sense when 'Play' is used from the Editor; if (GIsEditor && !GEnableVREditorHacks) { Settings->BaseOrientation = FQuat::Identity; Settings->BaseOffset = FVector::ZeroVector; Settings->ColorScale = ovrpVector4f{ 1, 1, 1, 1 }; Settings->ColorOffset = ovrpVector4f{ 0, 0, 0, 0 }; //Settings->WorldToMetersScale = InWorldContext.World()->GetWorldSettings()->WorldToMeters; //Settings->Flags.bWorldToMetersOverride = false; InitDevice(); FApp::SetUseVRFocus(true); FApp::SetHasVRFocus(true); OnStartGameFrame(InWorldContext); } #endif } void FOculusXRHMD::OnEndPlay(FWorldContext& InWorldContext) { CheckInGameThread(); #if WITH_EDITOR if (GIsEditor && !GEnableVREditorHacks) { // @todo vreditor: If we add support for starting PIE while in VR Editor, we don't want to kill stereo mode when exiting PIE if (Splash->IsShown()) { Splash->HideLoadingScreen(); // This will only request hiding the screen Splash->UpdateLoadingScreen_GameThread(); // Update is needed to complete removing the loading screen } EnableStereo(false); ReleaseDevice(); FApp::SetUseVRFocus(false); FApp::SetHasVRFocus(false); } #endif } DECLARE_STATS_GROUP(TEXT("Oculus System Metrics"), STATGROUP_OculusSystemMetrics, STATCAT_Advanced); DECLARE_FLOAT_COUNTER_STAT_EXTERN(TEXT("App CPU Time (ms)"), STAT_OculusSystem_AppCpuTime, STATGROUP_OculusSystemMetrics, ); DECLARE_FLOAT_COUNTER_STAT_EXTERN(TEXT("App GPU Time (ms)"), STAT_OculusSystem_AppGpuTime, STATGROUP_OculusSystemMetrics, ); DECLARE_FLOAT_COUNTER_STAT_EXTERN(TEXT("Compositor CPU Time (ms)"), STAT_OculusSystem_ComCpuTime, STATGROUP_OculusSystemMetrics, ); DECLARE_FLOAT_COUNTER_STAT_EXTERN(TEXT("Compositor GPU Time (ms)"), STAT_OculusSystem_ComGpuTime, STATGROUP_OculusSystemMetrics, ); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Compositor Dropped Frames"), STAT_OculusSystem_DroppedFrames, STATGROUP_OculusSystemMetrics, ); DECLARE_FLOAT_COUNTER_STAT_EXTERN(TEXT("System GPU Util %"), STAT_OculusSystem_GpuUtil, STATGROUP_OculusSystemMetrics, ); DECLARE_FLOAT_COUNTER_STAT_EXTERN(TEXT("System CPU Util Avg %"), STAT_OculusSystem_CpuUtilAvg, STATGROUP_OculusSystemMetrics, ); DECLARE_FLOAT_COUNTER_STAT_EXTERN(TEXT("System CPU Util Worst %"), STAT_OculusSystem_CpuUtilWorst, STATGROUP_OculusSystemMetrics, ); DECLARE_FLOAT_COUNTER_STAT_EXTERN(TEXT("CPU Clock Freq (MHz)"), STAT_OculusSystem_CpuFreq, STATGROUP_OculusSystemMetrics, ); DECLARE_FLOAT_COUNTER_STAT_EXTERN(TEXT("GPU Clock Freq (MHz)"), STAT_OculusSystem_GpuFreq, STATGROUP_OculusSystemMetrics, ); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("CPU Clock Level"), STAT_OculusSystem_CpuClockLvl, STATGROUP_OculusSystemMetrics, ); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("GPU Clock Level"), STAT_OculusSystem_GpuClockLvl, STATGROUP_OculusSystemMetrics, ); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("SpaceWarp Mode"), STAT_OculusSystem_ComSpaceWarpMode, STATGROUP_OculusSystemMetrics, ); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("CPU Core0 Util %"), STAT_OculusSystem_CpuCore0Util, STATGROUP_OculusSystemMetrics, ); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("CPU Core1 Util %"), STAT_OculusSystem_CpuCore1Util, STATGROUP_OculusSystemMetrics, ); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("CPU Core2 Util %"), STAT_OculusSystem_CpuCore2Util, STATGROUP_OculusSystemMetrics, ); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("CPU Core3 Util %"), STAT_OculusSystem_CpuCore3Util, STATGROUP_OculusSystemMetrics, ); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("CPU Core4 Util %"), STAT_OculusSystem_CpuCore4Util, STATGROUP_OculusSystemMetrics, ); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("CPU Core5 Util %"), STAT_OculusSystem_CpuCore5Util, STATGROUP_OculusSystemMetrics, ); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("CPU Core6 Util %"), STAT_OculusSystem_CpuCore6Util, STATGROUP_OculusSystemMetrics, ); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("CPU Core7 Util %"), STAT_OculusSystem_CpuCore7Util, STATGROUP_OculusSystemMetrics, ); DEFINE_STAT(STAT_OculusSystem_AppCpuTime); DEFINE_STAT(STAT_OculusSystem_AppGpuTime); DEFINE_STAT(STAT_OculusSystem_ComCpuTime); DEFINE_STAT(STAT_OculusSystem_ComGpuTime); DEFINE_STAT(STAT_OculusSystem_DroppedFrames); DEFINE_STAT(STAT_OculusSystem_GpuUtil); DEFINE_STAT(STAT_OculusSystem_CpuUtilAvg); DEFINE_STAT(STAT_OculusSystem_CpuUtilWorst); DEFINE_STAT(STAT_OculusSystem_CpuFreq); DEFINE_STAT(STAT_OculusSystem_GpuFreq); DEFINE_STAT(STAT_OculusSystem_CpuClockLvl); DEFINE_STAT(STAT_OculusSystem_GpuClockLvl); DEFINE_STAT(STAT_OculusSystem_ComSpaceWarpMode); DEFINE_STAT(STAT_OculusSystem_CpuCore0Util); DEFINE_STAT(STAT_OculusSystem_CpuCore1Util); DEFINE_STAT(STAT_OculusSystem_CpuCore2Util); DEFINE_STAT(STAT_OculusSystem_CpuCore3Util); DEFINE_STAT(STAT_OculusSystem_CpuCore4Util); DEFINE_STAT(STAT_OculusSystem_CpuCore5Util); DEFINE_STAT(STAT_OculusSystem_CpuCore6Util); DEFINE_STAT(STAT_OculusSystem_CpuCore7Util); void UpdateOculusSystemMetricsStats() { if (FOculusXRHMDModule::GetPluginWrapper().GetInitialized() == ovrpBool_False) { return; } ovrpBool bIsSupported; float valueFloat = 0; int valueInt = 0; if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().IsPerfMetricsSupported(ovrpPerfMetrics_App_CpuTime_Float, &bIsSupported)) && bIsSupported == ovrpBool_True) { if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetPerfMetricsFloat(ovrpPerfMetrics_App_CpuTime_Float, &valueFloat))) { SET_FLOAT_STAT(STAT_OculusSystem_AppCpuTime, valueFloat * 1000); } } if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().IsPerfMetricsSupported(ovrpPerfMetrics_App_GpuTime_Float, &bIsSupported)) && bIsSupported == ovrpBool_True) { if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetPerfMetricsFloat(ovrpPerfMetrics_App_GpuTime_Float, &valueFloat))) { SET_FLOAT_STAT(STAT_OculusSystem_AppGpuTime, valueFloat * 1000); } } if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().IsPerfMetricsSupported(ovrpPerfMetrics_Compositor_CpuTime_Float, &bIsSupported)) && bIsSupported == ovrpBool_True) { if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetPerfMetricsFloat(ovrpPerfMetrics_Compositor_CpuTime_Float, &valueFloat))) { SET_FLOAT_STAT(STAT_OculusSystem_ComCpuTime, valueFloat * 1000); } } if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().IsPerfMetricsSupported(ovrpPerfMetrics_Compositor_GpuTime_Float, &bIsSupported)) && bIsSupported == ovrpBool_True) { if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetPerfMetricsFloat(ovrpPerfMetrics_Compositor_GpuTime_Float, &valueFloat))) { SET_FLOAT_STAT(STAT_OculusSystem_ComGpuTime, valueFloat * 1000); } } if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().IsPerfMetricsSupported(ovrpPerfMetrics_Compositor_DroppedFrameCount_Int, &bIsSupported)) && bIsSupported == ovrpBool_True) { if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetPerfMetricsInt(ovrpPerfMetrics_Compositor_DroppedFrameCount_Int, &valueInt))) { SET_DWORD_STAT(STAT_OculusSystem_DroppedFrames, valueInt); } } if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().IsPerfMetricsSupported(ovrpPerfMetrics_System_GpuUtilPercentage_Float, &bIsSupported)) && bIsSupported == ovrpBool_True) { if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetPerfMetricsFloat(ovrpPerfMetrics_System_GpuUtilPercentage_Float, &valueFloat))) { SET_FLOAT_STAT(STAT_OculusSystem_GpuUtil, valueFloat * 100); } } if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().IsPerfMetricsSupported(ovrpPerfMetrics_System_CpuUtilAveragePercentage_Float, &bIsSupported)) && bIsSupported == ovrpBool_True) { if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetPerfMetricsFloat(ovrpPerfMetrics_System_CpuUtilAveragePercentage_Float, &valueFloat))) { SET_FLOAT_STAT(STAT_OculusSystem_CpuUtilAvg, valueFloat * 100); } } if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().IsPerfMetricsSupported(ovrpPerfMetrics_System_CpuUtilWorstPercentage_Float, &bIsSupported)) && bIsSupported == ovrpBool_True) { if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetPerfMetricsFloat(ovrpPerfMetrics_System_CpuUtilWorstPercentage_Float, &valueFloat))) { SET_FLOAT_STAT(STAT_OculusSystem_CpuUtilWorst, valueFloat * 100); } } if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().IsPerfMetricsSupported(ovrpPerfMetrics_Device_CpuClockFrequencyInMHz_Float, &bIsSupported)) && bIsSupported == ovrpBool_True) { if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetPerfMetricsFloat(ovrpPerfMetrics_Device_CpuClockFrequencyInMHz_Float, &valueFloat))) { SET_FLOAT_STAT(STAT_OculusSystem_CpuFreq, valueFloat); } } if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().IsPerfMetricsSupported(ovrpPerfMetrics_Device_GpuClockFrequencyInMHz_Float, &bIsSupported)) && bIsSupported == ovrpBool_True) { if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetPerfMetricsFloat(ovrpPerfMetrics_Device_GpuClockFrequencyInMHz_Float, &valueFloat))) { SET_FLOAT_STAT(STAT_OculusSystem_GpuFreq, valueFloat); } } if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().IsPerfMetricsSupported(ovrpPerfMetrics_Device_CpuClockLevel_Int, &bIsSupported)) && bIsSupported == ovrpBool_True) { if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetPerfMetricsInt(ovrpPerfMetrics_Device_CpuClockLevel_Int, &valueInt))) { SET_DWORD_STAT(STAT_OculusSystem_CpuClockLvl, valueInt); } } if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().IsPerfMetricsSupported(ovrpPerfMetrics_Device_GpuClockLevel_Int, &bIsSupported)) && bIsSupported == ovrpBool_True) { if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetPerfMetricsInt(ovrpPerfMetrics_Device_GpuClockLevel_Int, &valueInt))) { SET_DWORD_STAT(STAT_OculusSystem_GpuClockLvl, valueInt); } } if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().IsPerfMetricsSupported(ovrpPerfMetrics_Compositor_SpaceWarp_Mode_Int, &bIsSupported)) && bIsSupported == ovrpBool_True) { if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetPerfMetricsInt(ovrpPerfMetrics_Compositor_SpaceWarp_Mode_Int, &valueInt))) { SET_DWORD_STAT(STAT_OculusSystem_ComSpaceWarpMode, valueInt); } } if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().IsPerfMetricsSupported(ovrpPerfMetrics_Device_CpuCore0UtilPercentage_Float, &bIsSupported)) && bIsSupported == ovrpBool_True) { if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetPerfMetricsFloat(ovrpPerfMetrics_Device_CpuCore0UtilPercentage_Float, &valueFloat))) { SET_FLOAT_STAT(STAT_OculusSystem_CpuCore0Util, valueFloat); } } if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().IsPerfMetricsSupported(ovrpPerfMetrics_Device_CpuCore1UtilPercentage_Float, &bIsSupported)) && bIsSupported == ovrpBool_True) { if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetPerfMetricsFloat(ovrpPerfMetrics_Device_CpuCore1UtilPercentage_Float, &valueFloat))) { SET_FLOAT_STAT(STAT_OculusSystem_CpuCore1Util, valueFloat); } } if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().IsPerfMetricsSupported(ovrpPerfMetrics_Device_CpuCore2UtilPercentage_Float, &bIsSupported)) && bIsSupported == ovrpBool_True) { if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetPerfMetricsFloat(ovrpPerfMetrics_Device_CpuCore2UtilPercentage_Float, &valueFloat))) { SET_FLOAT_STAT(STAT_OculusSystem_CpuCore2Util, valueFloat); } } if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().IsPerfMetricsSupported(ovrpPerfMetrics_Device_CpuCore3UtilPercentage_Float, &bIsSupported)) && bIsSupported == ovrpBool_True) { if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetPerfMetricsFloat(ovrpPerfMetrics_Device_CpuCore3UtilPercentage_Float, &valueFloat))) { SET_FLOAT_STAT(STAT_OculusSystem_CpuCore3Util, valueFloat); } } if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().IsPerfMetricsSupported(ovrpPerfMetrics_Device_CpuCore4UtilPercentage_Float, &bIsSupported)) && bIsSupported == ovrpBool_True) { if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetPerfMetricsFloat(ovrpPerfMetrics_Device_CpuCore4UtilPercentage_Float, &valueFloat))) { SET_FLOAT_STAT(STAT_OculusSystem_CpuCore4Util, valueFloat); } } if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().IsPerfMetricsSupported(ovrpPerfMetrics_Device_CpuCore5UtilPercentage_Float, &bIsSupported)) && bIsSupported == ovrpBool_True) { if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetPerfMetricsFloat(ovrpPerfMetrics_Device_CpuCore5UtilPercentage_Float, &valueFloat))) { SET_FLOAT_STAT(STAT_OculusSystem_CpuCore5Util, valueFloat); } } if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().IsPerfMetricsSupported(ovrpPerfMetrics_Device_CpuCore6UtilPercentage_Float, &bIsSupported)) && bIsSupported == ovrpBool_True) { if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetPerfMetricsFloat(ovrpPerfMetrics_Device_CpuCore6UtilPercentage_Float, &valueFloat))) { SET_FLOAT_STAT(STAT_OculusSystem_CpuCore6Util, valueFloat); } } if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().IsPerfMetricsSupported(ovrpPerfMetrics_Device_CpuCore7UtilPercentage_Float, &bIsSupported)) && bIsSupported == ovrpBool_True) { if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetPerfMetricsFloat(ovrpPerfMetrics_Device_CpuCore7UtilPercentage_Float, &valueFloat))) { SET_FLOAT_STAT(STAT_OculusSystem_CpuCore7Util, valueFloat); } } } void FOculusXRHMD::OnBeginRendering_GameThread() { CheckInGameThread(); // We need to make sure we keep the Wait/Begin/End triplet in sync, so here we signal that we // can wait for the next frame in the next tick. Without this signal it's possible that two ticks // happen before the next frame is actually rendered. bShouldWait_GameThread = true; } bool FOculusXRHMD::OnStartGameFrame(FWorldContext& InWorldContext) { #if WITH_EDITOR // In the editor there can be multiple worlds. An editor world, pie worlds, other viewport worlds for editor pages. // XR hardware can only be running with one of them. if (GIsEditor && GEditor && GEditor->GetPIEWorldContext() != nullptr) { if (!InWorldContext.bIsPrimaryPIEInstance) { return false; } } #endif // WITH_EDITOR CheckInGameThread(); if (IsEngineExitRequested()) { return false; } UpdateOculusSystemMetricsStats(); RefreshTrackingToWorldTransform(InWorldContext); // check if HMD is marked as invalid and needs to be killed. ovrpBool appShouldRecreateDistortionWindow; if (FOculusXRHMDModule::GetPluginWrapper().GetInitialized() && OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetAppShouldRecreateDistortionWindow2(&appShouldRecreateDistortionWindow)) && appShouldRecreateDistortionWindow) { DoEnableStereo(false); ReleaseDevice(); if (!OCFlags.DisplayLostDetected) { FCoreDelegates::VRHeadsetLost.Broadcast(); OCFlags.DisplayLostDetected = true; } Flags.bNeedEnableStereo = true; } #if PLATFORM_ANDROID Flags.bNeedEnableStereo = true; // !!! #endif check(Settings.IsValid()); if (!Settings->IsStereoEnabled()) { FApp::SetUseVRFocus(false); FApp::SetHasVRFocus(false); } #if OCULUS_STRESS_TESTS_ENABLED FStressTester::TickCPU_GameThread(this); #endif if (bShutdownRequestQueued) { bShutdownRequestQueued = false; DoSessionShutdown(); } if (!InWorldContext.World() || (!(GEnableVREditorHacks && InWorldContext.WorldType == EWorldType::Editor) && !InWorldContext.World()->IsGameWorld())) // @todo vreditor: (Also see OnEndGameFrame()) Kind of a hack here so we can use VR in editor viewports. We need to consider when running GameWorld viewports inside the editor with VR. { // ignore all non-game worlds return false; } bool bStereoEnabled = Settings->Flags.bStereoEnabled; bool bStereoDesired = bStereoEnabled; if (Flags.bNeedEnableStereo) { bStereoDesired = true; } if (bStereoDesired && (Flags.bNeedDisableStereo || !Settings->Flags.bHMDEnabled)) { bStereoDesired = false; } bool bStereoDesiredAndIsConnected = bStereoDesired; if (bStereoDesired && !(bStereoEnabled ? IsHMDActive() : IsHMDEnabled())) { bStereoDesiredAndIsConnected = false; } Flags.bNeedEnableStereo = false; Flags.bNeedDisableStereo = false; if (bStereoEnabled != bStereoDesiredAndIsConnected) { bStereoEnabled = DoEnableStereo(bStereoDesiredAndIsConnected); } // Keep trying to enable stereo until we succeed Flags.bNeedEnableStereo = bStereoDesired && !bStereoEnabled; if (!Settings->IsStereoEnabled() && !Settings->Flags.bHeadTrackingEnforced) { return false; } if (Flags.bApplySystemOverridesOnStereo) { ApplySystemOverridesOnStereo(); Flags.bApplySystemOverridesOnStereo = false; } CachedWorldToMetersScale = InWorldContext.World()->GetWorldSettings()->WorldToMeters; // this should have already happened in FOculusXRInput, so this is usually a no-op. StartGameFrame_GameThread(); bool retval = true; UpdateHMDEvents(); if (FOculusXRHMDModule::GetPluginWrapper().GetInitialized()) { if (OCFlags.DisplayLostDetected) { FCoreDelegates::VRHeadsetReconnected.Broadcast(); OCFlags.DisplayLostDetected = false; } if (OCFlags.NeedSetTrackingOrigin) { SetTrackingOrigin(TrackingOrigin); } ovrpBool bAppHasVRFocus = ovrpBool_False; FOculusXRHMDModule::GetPluginWrapper().GetAppHasVrFocus2(&bAppHasVRFocus); FApp::SetUseVRFocus(true); FApp::SetHasVRFocus(bAppHasVRFocus != ovrpBool_False); // Do not pause if Editor is running (otherwise it will become very laggy) if (!GIsEditor) { if (!bAppHasVRFocus) { // not visible, if (!Settings->Flags.bPauseRendering) { UE_LOG(LogHMD, Log, TEXT("The app went out of VR focus, seizing rendering...")); } } else if (Settings->Flags.bPauseRendering) { UE_LOG(LogHMD, Log, TEXT("The app got VR focus, restoring rendering...")); } if (OCFlags.NeedSetFocusToGameViewport) { if (bAppHasVRFocus) { UE_LOG(LogHMD, Log, TEXT("Setting user focus to game viewport since session status is visible...")); FSlateApplication::Get().SetAllUserFocusToGameViewport(); OCFlags.NeedSetFocusToGameViewport = false; } } bool bPrevPause = Settings->Flags.bPauseRendering; Settings->Flags.bPauseRendering = !bAppHasVRFocus; if (Settings->Flags.bPauseRendering && (GEngine->GetMaxFPS() != OCULUS_PAUSED_IDLE_FPS)) { GEngine->SetMaxFPS(OCULUS_PAUSED_IDLE_FPS); } if (bPrevPause != Settings->Flags.bPauseRendering) { APlayerController* const PC = GEngine->GetFirstLocalPlayerController(InWorldContext.World()); if (Settings->Flags.bPauseRendering) { // focus is lost GEngine->SetMaxFPS(OCULUS_PAUSED_IDLE_FPS); if (!FCoreDelegates::ApplicationWillEnterBackgroundDelegate.IsBound()) { OCFlags.AppIsPaused = false; // default action: set pause if not already paused if (PC && !PC->IsPaused()) { PC->SetPause(true); OCFlags.AppIsPaused = true; } } else { FCoreDelegates::ApplicationWillEnterBackgroundDelegate.Broadcast(); } } else { // focus is gained GEngine->SetMaxFPS(0); if (!FCoreDelegates::ApplicationHasEnteredForegroundDelegate.IsBound()) { // default action: unpause if was paused by the plugin if (PC && OCFlags.AppIsPaused) { PC->SetPause(false); } OCFlags.AppIsPaused = false; } else { FCoreDelegates::ApplicationHasEnteredForegroundDelegate.Broadcast(); } } } } ovrpBool AppShouldQuit; ovrpBool AppShouldRecenter; if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetAppShouldQuit2(&AppShouldQuit)) && AppShouldQuit || OCFlags.EnforceExit) { FPlatformMisc::LowLevelOutputDebugString(TEXT("OculusXRHMD plugin requested exit (ShouldQuit == 1)\n")); #if WITH_EDITOR if (GIsEditor) { FSceneViewport* SceneVP = FindSceneViewport(); if (SceneVP && SceneVP->IsStereoRenderingAllowed()) { TSharedPtr Window = SceneVP->FindWindow(); Window->RequestDestroyWindow(); } } else #endif //WITH_EDITOR { // ApplicationWillTerminateDelegate will fire from inside of the RequestExit FPlatformMisc::RequestExit(false); } OCFlags.EnforceExit = false; retval = false; } else if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetAppShouldRecenter2(&AppShouldRecenter)) && AppShouldRecenter) { FPlatformMisc::LowLevelOutputDebugString(TEXT("OculusXRHMD plugin was requested to recenter\n")); if (FCoreDelegates::VRHeadsetRecenter.IsBound()) { FCoreDelegates::VRHeadsetRecenter.Broadcast(); } else { ResetOrientationAndPosition(); } // Call FOculusXRHMDModule::GetPluginWrapper().RecenterTrackingOrigin2 to clear AppShouldRecenter flag FOculusXRHMDModule::GetPluginWrapper().RecenterTrackingOrigin2(ovrpRecenterFlag_IgnoreAll); } UpdateHMDWornState(); } #if OCULUS_MR_SUPPORTED_PLATFORMS if (FOculusXRHMDModule::GetPluginWrapper().GetMixedRealityInitialized()) { FOculusXRHMDModule::GetPluginWrapper().UpdateExternalCamera(); } #endif if (IsEngineExitRequested()) { PreShutdown(); } return retval; } void FOculusXRHMD::DoSessionShutdown() { // Release resources ExecuteOnRenderThread([this]() { ExecuteOnRHIThread([this]() { for (int32 LayerIndex = 0; LayerIndex < Layers_RenderThread.Num(); LayerIndex++) { Layers_RenderThread[LayerIndex]->ReleaseResources_RHIThread(); } for (int32 LayerIndex = 0; LayerIndex < Layers_RHIThread.Num(); LayerIndex++) { Layers_RHIThread[LayerIndex]->ReleaseResources_RHIThread(); } if (Splash.IsValid()) { Splash->ReleaseResources_RHIThread(); } if (CustomPresent) { CustomPresent->ReleaseResources_RHIThread(); } Settings_RHIThread.Reset(); Frame_RHIThread.Reset(); Layers_RHIThread.Reset(); }); Settings_RenderThread.Reset(); Frame_RenderThread.Reset(); Layers_RenderThread.Reset(); EyeLayer_RenderThread.Reset(); DeferredDeletion.HandleLayerDeferredDeletionQueue_RenderThread(true); EnableInsightPassthrough_RenderThread(false); }); Frame.Reset(); NextFrameToRender.Reset(); LastFrameToRender.Reset(); #if !UE_BUILD_SHIPPING UDebugDrawService::Unregister(DrawDebugDelegateHandle); #endif // The Editor may release VR focus in OnEndPlay if (!GIsEditor) { FApp::SetUseVRFocus(false); FApp::SetHasVRFocus(false); } ShutdownSession(); } bool FOculusXRHMD::OnEndGameFrame(FWorldContext& InWorldContext) { CheckInGameThread(); FGameFrame* const CurrentGameFrame = Frame.Get(); if (CurrentGameFrame) { // don't use the cached value, as it could be affected by the player's position, so we update it here at the latest point in the game frame CurrentGameFrame->TrackingToWorld = ComputeTrackingToWorldTransform(InWorldContext); CurrentGameFrame->LastTrackingToWorld = LastTrackingToWorld; LastTrackingToWorld = CurrentGameFrame->TrackingToWorld; } else { return false; } if (!InWorldContext.World() || (!(GEnableVREditorHacks && InWorldContext.WorldType == EWorldType::Editor) && !InWorldContext.World()->IsGameWorld())) { // ignore all non-game worlds return false; } FinishGameFrame_GameThread(); return true; } FVector2D FOculusXRHMD::GetPlayAreaBounds(EHMDTrackingOrigin::Type Origin) const { ovrpVector3f Dimensions; if (Origin == EHMDTrackingOrigin::Stage && OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetBoundaryDimensions2(ovrpBoundary_PlayArea, &Dimensions))) { Dimensions.z *= -1.0; FVector Bounds = ConvertVector_M2U(Dimensions); return FVector2D(Bounds.X, Bounds.Z); } return FVector2D::ZeroVector; } bool FOculusXRHMD::IsHMDEnabled() const { CheckInGameThread(); return (Settings->Flags.bHMDEnabled); } EHMDWornState::Type FOculusXRHMD::GetHMDWornState() { ovrpBool userPresent; if (FOculusXRHMDModule::GetPluginWrapper().GetInitialized() && OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetUserPresent2(&userPresent)) && userPresent) { return EHMDWornState::Worn; } else { return EHMDWornState::NotWorn; } } void FOculusXRHMD::EnableHMD(bool enable) { CheckInGameThread(); Settings->Flags.bHMDEnabled = enable; if (!Settings->Flags.bHMDEnabled) { EnableStereo(false); } } bool FOculusXRHMD::GetHMDMonitorInfo(MonitorInfo& MonitorDesc) { CheckInGameThread(); MonitorDesc.MonitorName = FString("Oculus Window"); MonitorDesc.MonitorId = 0; MonitorDesc.DesktopX = MonitorDesc.DesktopY = 0; MonitorDesc.ResolutionX = MonitorDesc.ResolutionY = 0; MonitorDesc.WindowSizeX = MonitorDesc.WindowSizeY = 0; if (Settings.IsValid()) { MonitorDesc.ResolutionX = MonitorDesc.WindowSizeX = Settings->RenderTargetSize.X; MonitorDesc.ResolutionY = MonitorDesc.WindowSizeY = Settings->RenderTargetSize.Y; } return true; } void FOculusXRHMD::GetFieldOfView(float& InOutHFOVInDegrees, float& InOutVFOVInDegrees) const { ovrpFrustum2f Frustum; if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetNodeFrustum2(ovrpNode_EyeCenter, &Frustum))) { InOutVFOVInDegrees = FMath::RadiansToDegrees(FMath::Atan(Frustum.Fov.UpTan) + FMath::Atan(Frustum.Fov.DownTan)); InOutHFOVInDegrees = FMath::RadiansToDegrees(FMath::Atan(Frustum.Fov.LeftTan) + FMath::Atan(Frustum.Fov.RightTan)); } } void FOculusXRHMD::SetInterpupillaryDistance(float NewInterpupillaryDistance) { CheckInGameThread(); if (FOculusXRHMDModule::GetPluginWrapper().GetInitialized()) { FOculusXRHMDModule::GetPluginWrapper().SetUserIPD2(NewInterpupillaryDistance); } } float FOculusXRHMD::GetInterpupillaryDistance() const { CheckInGameThread(); float UserIPD; if (!FOculusXRHMDModule::GetPluginWrapper().GetInitialized() || OVRP_FAILURE(FOculusXRHMDModule::GetPluginWrapper().GetUserIPD2(&UserIPD))) { return 0.0f; } return UserIPD; } bool FOculusXRHMD::GetHMDDistortionEnabled(EShadingPath /* ShadingPath */) const { return false; } bool FOculusXRHMD::IsChromaAbCorrectionEnabled() const { CheckInGameThread(); return true; } bool FOculusXRHMD::HasHiddenAreaMesh() const { if (IsInParallelRenderingThread()) { if (ShouldDisableHiddenAndVisibileAreaMeshForSpectatorScreen_RenderThread()) { return false; } } return HiddenAreaMeshes[0].IsValid() && HiddenAreaMeshes[1].IsValid(); } bool FOculusXRHMD::HasVisibleAreaMesh() const { if (IsInParallelRenderingThread()) { if (ShouldDisableHiddenAndVisibileAreaMeshForSpectatorScreen_RenderThread()) { return false; } } return VisibleAreaMeshes[0].IsValid() && VisibleAreaMeshes[1].IsValid(); } static void DrawOcclusionMesh(FRHICommandList& RHICmdList, int32 ViewIndex, const FHMDViewMesh MeshAssets[]) { check(ViewIndex != INDEX_NONE); const uint32 MeshIndex = (ViewIndex == EStereoscopicEye::eSSE_LEFT_EYE) ? 0 : 1; const FHMDViewMesh& Mesh = MeshAssets[MeshIndex]; check(Mesh.IsValid()); RHICmdList.SetStreamSource(0, Mesh.VertexBufferRHI, 0); RHICmdList.DrawIndexedPrimitive(Mesh.IndexBufferRHI, 0, 0, Mesh.NumVertices, 0, Mesh.NumTriangles, 1); } void FOculusXRHMD::DrawHiddenAreaMesh(FRHICommandList& RHICmdList, int32 ViewIndex) const { DrawOcclusionMesh(RHICmdList, ViewIndex, HiddenAreaMeshes); } void FOculusXRHMD::DrawVisibleAreaMesh(FRHICommandList& RHICmdList, int32 ViewIndex) const { DrawOcclusionMesh(RHICmdList, ViewIndex, VisibleAreaMeshes); } float FOculusXRHMD::GetPixelDenity() const { if (IsInGameThread()) { return Settings.IsValid() ? Settings->PixelDensity : 1.0f; } else { return Settings_RenderThread.IsValid() ? Settings_RenderThread->PixelDensity : 1.0f; } } void FOculusXRHMD::SetPixelDensity(const float NewPixelDensity) { CheckInGameThread(); Settings->SetPixelDensity(NewPixelDensity); } FIntPoint FOculusXRHMD::GetIdealRenderTargetSize() const { if (IsInGameThread()) { return Settings.IsValid() ? Settings->RenderTargetSize : 1.0f; } else { return Settings_RenderThread.IsValid() ? Settings_RenderThread->RenderTargetSize : 1.0f; } } void FOculusXRHMD::GetMotionControllerData(UObject* WorldContext, const EControllerHand Hand, FXRMotionControllerData& MotionControllerData) { MotionControllerData.DeviceName = OculusSystemName; MotionControllerData.ApplicationInstanceID = FApp::GetInstanceId(); MotionControllerData.DeviceVisualType = EXRVisualType::Controller; MotionControllerData.TrackingStatus = ETrackingStatus::NotTracked; MotionControllerData.HandIndex = Hand; MotionControllerData.bValid = false; if ((Hand == EControllerHand::Left) || (Hand == EControllerHand::Right)) { const FName MotionControllerName("OculusXRInputDevice"); TArray MotionControllers = IModularFeatures::Get().GetModularFeatureImplementations(IMotionController::GetModularFeatureName()); const IMotionController* MotionController = nullptr; for (const IMotionController* Itr : MotionControllers) { if (Itr->GetMotionControllerDeviceTypeName() == MotionControllerName) { MotionController = Itr; break; } } const float WorldToMeters = GetWorldToMetersScale(); if (MotionController) { bool bSuccess = false; FVector Position = FVector::ZeroVector; FRotator Rotation = FRotator::ZeroRotator; const FTransform TrackingToWorld = GetTrackingToWorldTransform(); const FName AimSource = Hand == EControllerHand::Left ? FName("LeftAim") : FName("RightAim"); bSuccess = MotionController->GetControllerOrientationAndPosition(0, AimSource, Rotation, Position, WorldToMeters); if (bSuccess) { MotionControllerData.AimPosition = TrackingToWorld.TransformPosition(Position); MotionControllerData.AimRotation = TrackingToWorld.TransformRotation(FQuat(Rotation)); } MotionControllerData.bValid |= bSuccess; FName GripSource = Hand == EControllerHand::Left ? FName("LeftGrip") : FName("RightGrip"); bSuccess = MotionController->GetControllerOrientationAndPosition(0, GripSource, Rotation, Position, WorldToMeters); if (bSuccess) { MotionControllerData.GripPosition = TrackingToWorld.TransformPosition(Position); MotionControllerData.GripRotation = TrackingToWorld.TransformRotation(FQuat(Rotation)); } MotionControllerData.bValid |= bSuccess; MotionControllerData.TrackingStatus = MotionController->GetControllerTrackingStatus(0, GripSource); } } } bool FOculusXRHMD::IsStereoEnabled() const { if (IsInGameThread()) { return Settings.IsValid() && Settings->IsStereoEnabled(); } else { return Settings_RenderThread.IsValid() && Settings_RenderThread->IsStereoEnabled(); } } bool FOculusXRHMD::IsStereoEnabledOnNextFrame() const { // !!! return Settings.IsValid() && Settings->IsStereoEnabled(); } bool FOculusXRHMD::EnableStereo(bool bStereo) { CheckInGameThread(); if (bStereo) { LoadFromSettings(); CheckMultiPlayer(); } return DoEnableStereo(bStereo); } void FOculusXRHMD::AdjustViewRect(int32 ViewIndex, int32& X, int32& Y, uint32& SizeX, uint32& SizeY) const { if (Settings.IsValid()) { X = Settings->EyeUnscaledRenderViewport[ViewIndex].Min.X; Y = Settings->EyeUnscaledRenderViewport[ViewIndex].Min.Y; SizeX = Settings->EyeUnscaledRenderViewport[ViewIndex].Size().X; SizeY = Settings->EyeUnscaledRenderViewport[ViewIndex].Size().Y; } else { SizeX = SizeX / 2; X += SizeX * ViewIndex; } } FIntRect FOculusXRHMD::GetAsymmetricViewRect(const int32 ViewIndex, const FIntRect& ViewRect) { FIntRect AsymmetricViewRect = ViewRect; if (Settings_RenderThread.IsValid() && Frame_RenderThread.IsValid()) { const ovrpFovf& EyeBufferFov = Frame_RenderThread->Fov[ViewIndex]; const ovrpFovf& FrameFov = Frame_RenderThread->SymmetricFov[ViewIndex]; const int32 ViewPixelSize = AsymmetricViewRect.Size().X; // if using symmetric rendering, only send UVs of the asymmetrical subrect (the rest isn't useful) to the VR runtime const float symTanSize = FrameFov.LeftTan + FrameFov.RightTan; AsymmetricViewRect.Min.X += (FrameFov.LeftTan - EyeBufferFov.LeftTan) * (ViewPixelSize / symTanSize); AsymmetricViewRect.Max.X -= (FrameFov.RightTan - EyeBufferFov.RightTan) * (ViewPixelSize / symTanSize); } return AsymmetricViewRect; } void FOculusXRHMD::SetFinalViewRect(FRHICommandListImmediate& RHICmdList, const int32 ViewIndex, const FIntRect& FinalViewRect) { CheckInRenderThread(); if (ViewIndex == INDEX_NONE || ViewIndex < 0 || ViewIndex >= ovrpEye_Count) { return; } FIntRect AsymmetricViewRect = GetAsymmetricViewRect(ViewIndex, FinalViewRect); if (Settings_RenderThread.IsValid()) { Settings_RenderThread->EyeRenderViewport[ViewIndex] = AsymmetricViewRect; } // Called after RHIThread has already started. Need to update Settings_RHIThread as well. ExecuteOnRHIThread_DoNotWait([this, ViewIndex, AsymmetricViewRect]() { CheckInRHIThread(); if (Settings_RHIThread.IsValid()) { Settings_RHIThread->EyeRenderViewport[ViewIndex] = AsymmetricViewRect; } }); } #ifdef WITH_OCULUS_BRANCH void FOculusXRHMD::CalculateScissorRect(const int32 ViewIndex, const FIntRect& ViewRect, FIntRect& OutRect) { CheckInRenderThread(); if (ViewIndex == INDEX_NONE || ViewIndex < 0 || ViewIndex >= ovrpEye_Count) { return; } OutRect = GetAsymmetricViewRect(ViewIndex, ViewRect); } #endif // WITH_OCULUS_BRANCH void FOculusXRHMD::CalculateStereoViewOffset(const int32 ViewIndex, FRotator& ViewRotation, const float WorldToMeters, FVector& ViewLocation) { // This method is called from GetProjectionData on a game thread. if (InGameThread() && ViewIndex == EStereoscopicEye::eSSE_LEFT_EYE && NextFrameToRender.IsValid()) { // Inverse out GameHeadPose.Rotation since PlayerOrientation already contains head rotation. FQuat HeadOrientation = FQuat::Identity; FVector HeadPosition; GetCurrentPose(HMDDeviceId, HeadOrientation, HeadPosition); NextFrameToRender->HeadOrientation = HeadOrientation; NextFrameToRender->PlayerOrientation = LastPlayerOrientation = ViewRotation.Quaternion() * HeadOrientation.Inverse(); NextFrameToRender->PlayerLocation = LastPlayerLocation = ViewLocation; } FHeadMountedDisplayBase::CalculateStereoViewOffset(ViewIndex, ViewRotation, WorldToMeters, ViewLocation); } FMatrix FOculusXRHMD::GetStereoProjectionMatrix(int32 ViewIndex) const { CheckInGameThread(); check(IsStereoEnabled()); FMatrix proj = (ViewIndex == EStereoscopicEye::eSSE_MONOSCOPIC) ? ToFMatrix(Settings->MonoProjectionMatrix) : ToFMatrix(Settings->EyeProjectionMatrices[ViewIndex]); // correct far and near planes for reversed-Z projection matrix const float WorldScale = GetWorldToMetersScale() * (1.0 / 100.0f); // physical scale is 100 UUs/meter float InNearZ = GNearClippingPlane * WorldScale; proj.M[3][3] = 0.0f; proj.M[2][3] = 1.0f; proj.M[2][2] = 0.0f; proj.M[3][2] = InNearZ; return proj; } void FOculusXRHMD::InitCanvasFromView(FSceneView* InView, UCanvas* Canvas) { // This is used for placing small HUDs (with names) // over other players (for example, in Capture Flag). // HmdOrientation should be initialized by GetCurrentOrientation (or // user's own value). } void FOculusXRHMD::RenderTexture_RenderThread(class FRHICommandListImmediate& RHICmdList, class FRHITexture* BackBuffer, class FRHITexture* SrcTexture, FVector2D WindowSize) const { CheckInRenderThread(); check(CustomPresent); #if PLATFORM_ANDROID return; #endif if (SpectatorScreenController) { SpectatorScreenController->RenderSpectatorScreen_RenderThread(RHICmdList, BackBuffer, SrcTexture, WindowSize); } } FVector2D FOculusXRHMD::GetEyeCenterPoint_RenderThread(int32 ViewIndex) const { CheckInRenderThread(); check(IsStereoEnabled() || IsHeadTrackingEnforced()); // Don't use GetStereoProjectionMatrix because it is game thread only on oculus, we also don't need the zplane adjustments for this. const FMatrix StereoProjectionMatrix = ToFMatrix(Settings_RenderThread->EyeProjectionMatrices[ViewIndex]); //0,0,1 is the straight ahead point, wherever it maps to is the center of the projection plane in -1..1 coordinates. -1,-1 is bottom left. const FVector4 ScreenCenter = StereoProjectionMatrix.TransformPosition(FVector(0.0f, 0.0f, 1.0f)); //transform into 0-1 screen coordinates 0,0 is top left. const FVector2D CenterPoint(0.5f + (ScreenCenter.X / 2.0f), 0.5f - (ScreenCenter.Y / 2.0f)); return CenterPoint; } FIntRect FOculusXRHMD::GetFullFlatEyeRect_RenderThread(FTexture2DRHIRef EyeTexture) const { CheckInRenderThread(); // Rift does this differently than other platforms, it already has an idea of what rectangle it wants to use stored. FIntRect& EyeRect = Settings_RenderThread->EyeRenderViewport[0]; // But the rectangle rift specifies has corners cut off, so we will crop a little more. if (ShouldDisableHiddenAndVisibileAreaMeshForSpectatorScreen_RenderThread()) { return EyeRect; } else { static FVector2D SrcNormRectMin(0.05f, 0.0f); static FVector2D SrcNormRectMax(0.95f, 1.0f); const int32 SizeX = EyeRect.Max.X - EyeRect.Min.X; const int32 SizeY = EyeRect.Max.Y - EyeRect.Min.Y; return FIntRect(EyeRect.Min.X + SizeX * SrcNormRectMin.X, EyeRect.Min.Y + SizeY * SrcNormRectMin.Y, EyeRect.Min.X + SizeX * SrcNormRectMax.X, EyeRect.Min.Y + SizeY * SrcNormRectMax.Y); } } void FOculusXRHMD::CopyTexture_RenderThread(FRHICommandListImmediate& RHICmdList, FRHITexture2D* SrcTexture, FIntRect SrcRect, FRHITexture2D* DstTexture, FIntRect DstRect, bool bClearBlack, bool bNoAlpha) const { if (bClearBlack) { FRHIRenderPassInfo RPInfo(DstTexture, ERenderTargetActions::DontLoad_Store); RHICmdList.BeginRenderPass(RPInfo, TEXT("ClearToBlack")); { const FIntRect ClearRect(0, 0, DstTexture->GetSizeX(), DstTexture->GetSizeY()); RHICmdList.SetViewport(ClearRect.Min.X, ClearRect.Min.Y, 0, ClearRect.Max.X, ClearRect.Max.Y, 1.0f); DrawClearQuad(RHICmdList, FLinearColor::Black); } RHICmdList.EndRenderPass(); } check(CustomPresent); CustomPresent->CopyTexture_RenderThread(RHICmdList, DstTexture, SrcTexture, DstRect, SrcRect, false, bNoAlpha, true, true); } bool FOculusXRHMD::PopulateAnalyticsAttributes(TArray& EventAttributes) { if (!FHeadMountedDisplayBase::PopulateAnalyticsAttributes(EventAttributes)) { return false; } EventAttributes.Add(FAnalyticsEventAttribute(TEXT("HQBuffer"), (bool)Settings->Flags.bHQBuffer)); EventAttributes.Add(FAnalyticsEventAttribute(TEXT("HQDistortion"), (bool)Settings->Flags.bHQDistortion)); EventAttributes.Add(FAnalyticsEventAttribute(TEXT("UpdateOnRT"), (bool)Settings->Flags.bUpdateOnRT)); return true; } bool FOculusXRHMD::ShouldUseSeparateRenderTarget() const { return IsStereoEnabled(); } void FOculusXRHMD::CalculateRenderTargetSize(const FViewport& Viewport, uint32& InOutSizeX, uint32& InOutSizeY) { // TODO this should use Settings_RenderThread if !CheckInGameThread() // This is called before StartRenderFrame_GameThread() on startup if (!Settings->IsStereoEnabled()) { return; } InOutSizeX = Settings->RenderTargetSize.X; InOutSizeY = Settings->RenderTargetSize.Y; check(InOutSizeX != 0 && InOutSizeY != 0); } void FOculusXRHMD::AllocateEyeBuffer() { CheckInGameThread(); ExecuteOnRenderThread([&]() { InitializeEyeLayer_RenderThread(GetImmediateCommandList_ForRenderCommand()); const FXRSwapChainPtr& SwapChain = EyeLayer_RenderThread->GetSwapChain(); if (SwapChain.IsValid()) { const FRHITexture2D* const SwapChainTexture = SwapChain->GetTexture2DArray() ? SwapChain->GetTexture2DArray() : SwapChain->GetTexture2D(); UE_LOG(LogHMD, Log, TEXT("Allocating Oculus %d x %d rendertarget swapchain"), SwapChainTexture->GetSizeX(), SwapChainTexture->GetSizeY()); } }); bNeedReAllocateViewportRenderTarget = true; } bool FOculusXRHMD::NeedReAllocateViewportRenderTarget(const FViewport& Viewport) { CheckInGameThread(); return ensureMsgf(Settings.IsValid(), TEXT("Unexpected issue with Oculus settings on the GameThread. This should be valid when this is called in EnqueueBeginRenderFrame() - has the callsite changed?")) && Settings->IsStereoEnabled() && bNeedReAllocateViewportRenderTarget; } bool FOculusXRHMD::NeedReAllocateDepthTexture(const TRefCountPtr& DepthTarget) { CheckInRenderThread(); return ensureMsgf(Settings_RenderThread.IsValid(), TEXT("Unexpected issue with Oculus settings on the RenderThread. This should be valid when this is called in AllocateCommonDepthTargets() - has the callsite changed?")) && Settings_RenderThread->IsStereoEnabled() && bNeedReAllocateDepthTexture_RenderThread; } bool FOculusXRHMD::NeedReAllocateShadingRateTexture(const TRefCountPtr& FoveationTarget) { CheckInRenderThread(); return ensureMsgf(Settings_RenderThread.IsValid(), TEXT("Unexpected issue with Oculus settings on the RenderThread. This should be valid when this is called in AllocateFoveationTexture() - has the callsite changed?")) && Settings_RenderThread->IsStereoEnabled() && bNeedReAllocateFoveationTexture_RenderThread; } #ifdef WITH_OCULUS_BRANCH bool FOculusXRHMD::NeedReAllocateMotionVectorTexture(const TRefCountPtr& MotionVectorTarget, const TRefCountPtr& MotionVectorDepthTarget) { CheckInRenderThread(); return ensureMsgf(Settings_RenderThread.IsValid(), TEXT("Unexpected issue with Oculus settings on the RenderThread. This should be valid when this is called in AllocateMotionVectorTexture() - has the callsite changed?")) && Settings_RenderThread->IsStereoEnabled() && bNeedReAllocateMotionVectorTexture_RenderThread; } #endif // WITH_OCULUS_BRANCH bool FOculusXRHMD::AllocateRenderTargetTexture(uint32 Index, uint32 SizeX, uint32 SizeY, uint8 Format, uint32 NumMips, ETextureCreateFlags InTexFlags, ETextureCreateFlags InTargetableTextureFlags, FTexture2DRHIRef& OutTargetableTexture, FTexture2DRHIRef& OutShaderResourceTexture, uint32 NumSamples) { CheckInRenderThread(); check(Index == 0); if (LayerMap[0].IsValid()) { const FXRSwapChainPtr& SwapChain = EyeLayer_RenderThread->GetSwapChain(); if (SwapChain.IsValid()) { OutTargetableTexture = OutShaderResourceTexture = SwapChain->GetTexture2DArray() ? SwapChain->GetTexture2DArray() : SwapChain->GetTexture2D(); bNeedReAllocateViewportRenderTarget = false; return true; } } OutTargetableTexture = OutShaderResourceTexture = nullptr; return false; } bool FOculusXRHMD::AllocateDepthTexture(uint32 Index, uint32 SizeX, uint32 SizeY, uint8 Format, uint32 NumMips, ETextureCreateFlags FlagsIn, ETextureCreateFlags TargetableTextureFlags, FTexture2DRHIRef& OutTargetableTexture, FTexture2DRHIRef& OutShaderResourceTexture, uint32 NumSamples) { CheckInRenderThread(); check(Index == 0); if (EyeLayer_RenderThread.IsValid()) { const FXRSwapChainPtr& SwapChain = EyeLayer_RenderThread->GetDepthSwapChain(); if (SwapChain.IsValid()) { FTexture2DRHIRef Texture = SwapChain->GetTexture2DArray() ? SwapChain->GetTexture2DArray() : SwapChain->GetTexture2D(); FIntPoint TexSize = Texture->GetSizeXY(); // Ensure the texture size matches the eye layer. We may get other depth allocations unrelated to the main scene render. if (FIntPoint(SizeX, SizeY) == TexSize) { if (bNeedReAllocateDepthTexture_RenderThread) { UE_LOG(LogHMD, Log, TEXT("Allocating Oculus %d x %d depth rendertarget swapchain"), SizeX, SizeY); bNeedReAllocateDepthTexture_RenderThread = false; } OutTargetableTexture = OutShaderResourceTexture = Texture; return true; } } } OutTargetableTexture = OutShaderResourceTexture = nullptr; return false; } bool FOculusXRHMD::AllocateShadingRateTexture(uint32 Index, uint32 RenderSizeX, uint32 RenderSizeY, uint8 Format, uint32 NumMips, ETextureCreateFlags InTexFlags, ETextureCreateFlags InTargetableTextureFlags, FTexture2DRHIRef& OutTexture, FIntPoint& OutTextureSize) { CheckInRenderThread(); check(Index == 0); if (EyeLayer_RenderThread.IsValid()) { const FXRSwapChainPtr& SwapChain = EyeLayer_RenderThread->GetFoveationSwapChain(); if (SwapChain.IsValid()) { FTexture2DRHIRef Texture = SwapChain->GetTexture2DArray() ? SwapChain->GetTexture2DArray() : SwapChain->GetTexture2D(); FIntPoint TexSize = Texture->GetSizeXY(); // Only set texture and return true if we have a valid texture of compatible size if (Texture->IsValid() && TexSize.X > 0 && TexSize.Y > 0) { if (bNeedReAllocateFoveationTexture_RenderThread) { UE_LOG(LogHMD, Log, TEXT("Allocating Oculus %d x %d variable resolution swapchain"), TexSize.X, TexSize.Y, Index); bNeedReAllocateFoveationTexture_RenderThread = false; } // This is a hack to turn force the runtime to use FDM over FSR when we allocate our FDM to avoid a crash on Quest 3 // TODO: Remove this for UE 5.3 after there's an engine-side fix ExecuteOnRHIThread_DoNotWait([this]() { // Set this in AllocateShadingRateTexture because it guarantees that this runs after VulkanExtensions has initially // selected the shading rate type, before the FDM is actually going to be used, and only when we actually have an FDM CustomPresent->UseFragmentDensityMapOverShadingRate_RHIThread(); }); OutTexture = Texture; OutTextureSize = TexSize; return true; } } } OutTexture = nullptr; return false; } #ifdef WITH_OCULUS_BRANCH bool FOculusXRHMD::AllocateMotionVectorTexture(uint32 Index, uint8 Format, uint32 NumMips, ETextureCreateFlags InTexFlags, ETextureCreateFlags InTargetableTextureFlags, FTexture2DRHIRef& OutTexture, FIntPoint& OutTextureSize, FTexture2DRHIRef& OutDepthTexture, FIntPoint& OutDepthTextureSize) { CheckInRenderThread(); check(Index == 0); if (EyeLayer_RenderThread.IsValid()) { const FXRSwapChainPtr& SwapChain = EyeLayer_RenderThread->GetMotionVectorSwapChain(); if (SwapChain.IsValid()) { FTexture2DRHIRef Texture = SwapChain->GetTexture2DArray() ? SwapChain->GetTexture2DArray() : SwapChain->GetTexture2D(); FIntPoint TexSize = Texture->GetSizeXY(); const FXRSwapChainPtr& DepthSwapChain = EyeLayer_RenderThread->GetMotionVectorDepthSwapChain(); if (DepthSwapChain.IsValid()) { FTexture2DRHIRef DepthTexture = DepthSwapChain->GetTexture2DArray() ? DepthSwapChain->GetTexture2DArray() : DepthSwapChain->GetTexture2D(); FIntPoint DepthTexSize = DepthTexture->GetSizeXY(); if (DepthTexture->IsValid() && DepthTexSize.X > 0 && DepthTexSize.Y > 0) { OutDepthTextureSize = DepthTexSize; OutDepthTexture = DepthTexture; } else { return false; } } // Only set texture and return true if we have a valid texture of compatible size if (Texture->IsValid() && TexSize.X > 0 && TexSize.Y > 0) { if (bNeedReAllocateMotionVectorTexture_RenderThread) { UE_LOG(LogHMD, Log, TEXT("[Mobile SpaceWarp] Allocating Oculus %d x %d motion vector swapchain"), TexSize.X, TexSize.Y, Index); bNeedReAllocateMotionVectorTexture_RenderThread = false; } OutTexture = Texture; OutTextureSize = TexSize; return true; } } } OutTexture = nullptr; return false; } #endif // WITH_OCULUS_BRANCH #if defined(WITH_OCULUS_BRANCH) bool FOculusXRHMD::FindEnvironmentDepthTexture_RenderThread(FTextureRHIRef& OutTexture, FVector2f& OutDepthFactors, FMatrix44f OutScreenToDepthMatrices[2], FMatrix44f OutDepthViewProjMatrices[2]) { CheckInRenderThread(); if (Frame_RenderThread.IsValid()) { int SwapchainIndex; if (ComputeEnvironmentDepthParameters_RenderThread(OutDepthFactors, OutScreenToDepthMatrices, OutDepthViewProjMatrices, SwapchainIndex)) { if (SwapchainIndex >= EnvironmentDepthSwapchain.Num()) { return false; } OutTexture = EnvironmentDepthSwapchain[SwapchainIndex]; return true; } } return false; } #endif // defined(WITH_OCULUS_BRANCH) EPixelFormat FOculusXRHMD::GetActualColorSwapchainFormat() const { if (!CustomPresent.IsValid()) { UE_LOG(LogHMD, Log, TEXT("Invalid CustomPresent! PF_R8G8B8A8 will be used as the default swapchain format!")); return PF_R8G8B8A8; } return CustomPresent->GetDefaultPixelFormat(); } void FOculusXRHMD::UpdateViewportWidget(bool bUseSeparateRenderTarget, const class FViewport& Viewport, class SViewport* ViewportWidget) { CheckInGameThread(); check(ViewportWidget); TSharedPtr Window = CachedWindow.Pin(); TSharedPtr CurrentlyCachedWidget = CachedViewportWidget.Pin(); TSharedRef Widget = ViewportWidget->AsShared(); if (!Window.IsValid() || Widget != CurrentlyCachedWidget) { Window = FSlateApplication::Get().FindWidgetWindow(Widget); CachedViewportWidget = Widget; CachedWindow = Window; } if (!Settings->IsStereoEnabled()) { // Restore AutoResizeViewport mode for the window if (Window.IsValid()) { Window->SetMirrorWindow(false); Window->SetViewportSizeDrivenByWindow(true); } return; } if (bUseSeparateRenderTarget && Frame.IsValid()) { if (Window.IsValid()) { const auto SlateWindowSize = Window->GetSizeInScreen(); CachedWindowSize = FIntPoint(static_cast(SlateWindowSize.X), static_cast(SlateWindowSize.Y)); } else { CachedWindowSize = Viewport.GetSizeXY(); } } } FXRRenderBridge* FOculusXRHMD::GetActiveRenderBridge_GameThread(bool bUseSeparateRenderTarget) { CheckInGameThread(); if (bUseSeparateRenderTarget && NextFrameToRender.IsValid()) { return CustomPresent; } else { return nullptr; } } void FOculusXRHMD::UpdateHMDWornState() { const EHMDWornState::Type NewHMDWornState = GetHMDWornState(); if (NewHMDWornState != HMDWornState) { HMDWornState = NewHMDWornState; if (HMDWornState == EHMDWornState::Worn) { FCoreDelegates::VRHeadsetPutOnHead.Broadcast(); } else if (HMDWornState == EHMDWornState::NotWorn) { FCoreDelegates::VRHeadsetRemovedFromHead.Broadcast(); } } } void FOculusXRHMD::UpdateHMDEvents() { ovrpEventDataBuffer buf; while (FOculusXRHMDModule::GetPluginWrapper().PollEvent(&buf) == ovrpSuccess) { if (buf.EventType == ovrpEventType_None) { break; } else if (buf.EventType == ovrpEventType_DisplayRefreshRateChange) { ovrpEventDisplayRefreshRateChange* rateChangedEvent = (ovrpEventDisplayRefreshRateChange*)&buf; FOculusEventDelegates::OculusDisplayRefreshRateChanged.Broadcast(rateChangedEvent->FromRefreshRate, rateChangedEvent->ToRefreshRate); } else { for (auto& it : EventPollingDelegates) { bool HandledEvent = false; it.ExecuteIfBound(&buf, HandledEvent); } } } } uint32 FOculusXRHMD::CreateLayer(const IStereoLayers::FLayerDesc& InLayerDesc) { CheckInGameThread(); uint32 LayerId = NextLayerId++; FLayerPtr Layer = MakeShareable(new FLayer(LayerId)); LayerMap.Add(LayerId, Layer); Layer->SetDesc(Settings.Get(), InLayerDesc); return LayerId; } void FOculusXRHMD::DestroyLayer(uint32 LayerId) { CheckInGameThread(); FLayerPtr* LayerFound = LayerMap.Find(LayerId); if (LayerFound) { (*LayerFound)->DestroyLayer(); } LayerMap.Remove(LayerId); } void FOculusXRHMD::SetLayerDesc(uint32 LayerId, const IStereoLayers::FLayerDesc& InLayerDesc) { CheckInGameThread(); FLayerPtr* LayerFound = LayerMap.Find(LayerId); if (LayerFound) { FLayer* Layer = new FLayer(**LayerFound); Layer->SetDesc(Settings.Get(), InLayerDesc); *LayerFound = MakeShareable(Layer); } } bool FOculusXRHMD::GetLayerDesc(uint32 LayerId, IStereoLayers::FLayerDesc& OutLayerDesc) { CheckInGameThread(); FLayerPtr* LayerFound = LayerMap.Find(LayerId); if (LayerFound) { OutLayerDesc = (*LayerFound)->GetDesc(); return true; } return false; } void FOculusXRHMD::MarkTextureForUpdate(uint32 LayerId) { CheckInGameThread(); FLayerPtr* LayerFound = LayerMap.Find(LayerId); if (LayerFound) { (*LayerFound)->MarkTextureForUpdate(); } } void FOculusXRHMD::SetSplashRotationToForward() { //if update splash screen is shown, update the head orientation default to recenter splash screens FQuat HeadOrientation = FQuat::Identity; FVector HeadPosition; GetCurrentPose(HMDDeviceId, HeadOrientation, HeadPosition); SplashRotation = FRotator(HeadOrientation); SplashRotation.Pitch = 0; SplashRotation.Roll = 0; } FOculusXRSplashDesc FOculusXRHMD::GetUESplashScreenDesc() { FOculusXRSplashDesc Desc; Desc.LoadedTexture = bSplashShowMovie ? SplashMovie : SplashTexture; Desc.TransformInMeters = Desc.TransformInMeters * FTransform(SplashOffset / GetWorldToMetersScale()); Desc.bNoAlphaChannel = true; Desc.bIsDynamic = bSplashShowMovie; Desc.QuadSizeInMeters *= SplashScale; return Desc; } void FOculusXRHMD::EyeTrackedFoveatedRenderingFallback() { FoveatedRenderingMethod = EOculusXRFoveatedRenderingMethod::FixedFoveatedRendering; FoveatedRenderingLevel = EOculusXRFoveatedRenderingLevel::High; bDynamicFoveatedRendering = true; } void FOculusXRHMD::GetAllocatedTexture(uint32 LayerId, FTextureRHIRef& Texture, FTextureRHIRef& LeftTexture) { Texture = LeftTexture = nullptr; FLayerPtr* LayerFound = nullptr; if (IsInGameThread()) { LayerFound = LayerMap.Find(LayerId); } else if (IsInParallelRenderingThread()) { for (int32 LayerIndex = 0; LayerIndex < Layers_RenderThread.Num(); LayerIndex++) { if (Layers_RenderThread[LayerIndex]->GetId() == LayerId) { LayerFound = &Layers_RenderThread[LayerIndex]; } } } else if (IsInRHIThread()) { for (int32 LayerIndex = 0; LayerIndex < Layers_RHIThread.Num(); LayerIndex++) { if (Layers_RHIThread[LayerIndex]->GetId() == LayerId) { LayerFound = &Layers_RHIThread[LayerIndex]; } } } else { return; } if (LayerFound && (*LayerFound)->GetSwapChain().IsValid()) { bool bRightTexture = (*LayerFound)->GetRightSwapChain().IsValid(); const IStereoLayers::FLayerDesc& Desc = (*LayerFound)->GetDesc(); if (Desc.HasShape()) { if (bRightTexture) { Texture = (*LayerFound)->GetRightSwapChain()->GetTextureCube(); LeftTexture = (*LayerFound)->GetSwapChain()->GetTextureCube(); } else { Texture = LeftTexture = (*LayerFound)->GetSwapChain()->GetTextureCube(); } } else if (Desc.HasShape() || Desc.HasShape()) { if (bRightTexture) { Texture = (*LayerFound)->GetRightSwapChain()->GetTexture2D(); LeftTexture = (*LayerFound)->GetSwapChain()->GetTexture2D(); } else { Texture = LeftTexture = (*LayerFound)->GetSwapChain()->GetTexture2D(); } } } } IStereoLayers::FLayerDesc FOculusXRHMD::GetDebugCanvasLayerDesc(FTextureRHIRef Texture) { IStereoLayers::FLayerDesc StereoLayerDesc; ovrpBool cylinderSupported = ovrpBool_False; ovrpResult result = FOculusXRHMDModule::GetPluginWrapper().IsLayerShapeSupported(ovrpShape_Cylinder, &cylinderSupported); if (OVRP_SUCCESS(result) && cylinderSupported) { StereoLayerDesc = IStereoLayers::FLayerDesc(FCylinderLayer(100.f, 488.f / 4, 180.f)); StereoLayerDesc.Transform = FTransform(FVector(0.f, 0, 0)); // 100/0/0 for quads } else { StereoLayerDesc.Transform = FTransform(FVector(100.f, 0, 0)); } StereoLayerDesc.QuadSize = FVector2D(180.f, 180.f); StereoLayerDesc.PositionType = IStereoLayers::ELayerType::FaceLocked; StereoLayerDesc.LayerSize = Texture->GetTexture2D()->GetSizeXY(); StereoLayerDesc.Flags = IStereoLayers::ELayerFlags::LAYER_FLAG_TEX_CONTINUOUS_UPDATE; StereoLayerDesc.Flags |= IStereoLayers::ELayerFlags::LAYER_FLAG_QUAD_PRESERVE_TEX_RATIO; return StereoLayerDesc; } void FOculusXRHMD::SetupViewFamily(FSceneViewFamily& InViewFamily) { InViewFamily.EngineShowFlags.StereoRendering = IsStereoEnabled(); } void FOculusXRHMD::SetupView(FSceneViewFamily& InViewFamily, FSceneView& InView) { CheckInGameThread(); } void FOculusXRHMD::BeginRenderViewFamily(FSceneViewFamily& InViewFamily) { CheckInGameThread(); if (Settings.IsValid() && Settings->IsStereoEnabled()) { // This should already have been set by UpdateStereoRenderingParams(). // It must still match the value used there. check(Settings->CurrentShaderPlatform == InViewFamily.Scene->GetShaderPlatform()); Settings->Flags.bsRGBEyeBuffer = IsMobilePlatform(Settings->CurrentShaderPlatform) && IsMobileColorsRGB(); if (NextFrameToRender.IsValid()) { NextFrameToRender->ShowFlags = InViewFamily.EngineShowFlags; } if (SpectatorScreenController != nullptr) { SpectatorScreenController->BeginRenderViewFamily(); } } StartRenderFrame_GameThread(); } void FOculusXRHMD::EnableInsightPassthrough_RenderThread(bool bEnablePassthrough) { const bool bShouldEnable = (InsightInitStatus == FInsightInitStatus::NotInitialized) && bEnablePassthrough; if (bShouldEnable) { if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().InitializeInsightPassthrough())) { UE_LOG(LogHMD, Log, TEXT("Passthrough Initialized")); InsightInitStatus = FInsightInitStatus::Initialized; } else { InsightInitStatus = FInsightInitStatus::Failed; UE_LOG(LogHMD, Log, TEXT("Passthrough initialization failed")); } } else { const bool bShouldShutdown = (InsightInitStatus == FInsightInitStatus::Initialized) && !bEnablePassthrough; if (bShouldShutdown) { // it may already be deinitialized. if (!FOculusXRHMDModule::GetPluginWrapper().GetInsightPassthroughInitialized() || OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().ShutdownInsightPassthrough())) { UE_LOG(LogHMD, Log, TEXT("Passthrough shutdown")); InsightInitStatus = FInsightInitStatus::NotInitialized; } else { UE_LOG(LogHMD, Log, TEXT("Failed to shut down passthrough. It may be still in use.")); } } } } void FOculusXRHMD::PreRenderViewFamily_RenderThread(FRDGBuilder& GraphBuilder, FSceneViewFamily& ViewFamily) { CheckInRenderThread(); } void FOculusXRHMD::OnBeginRendering_RenderThread(FRHICommandListImmediate& RHICmdList, FSceneViewFamily& ViewFamily) { CheckInRenderThread(); if (!Frame_RenderThread.IsValid()) { return; } if (!Settings_RenderThread.IsValid() || !Settings_RenderThread->IsStereoEnabled()) { return; } // If using OVRPlugin OpenXR, only update spectator screen mode with VR focus, since we are running the frameloop // and cycling through the swapchain even without VR focus with OVRPlugin OpenXR ovrpXrApi NativeXrApi; FOculusXRHMDModule::GetPluginWrapper().GetNativeXrApiType(&NativeXrApi); if (SpectatorScreenController && (NativeXrApi != ovrpXrApi_OpenXR || FApp::HasVRFocus())) { SpectatorScreenController->UpdateSpectatorScreenMode_RenderThread(); Frame_RenderThread->Flags.bSpectatorScreenActive = SpectatorScreenController->GetSpectatorScreenMode() != ESpectatorScreenMode::Disabled; } // Update mirror texture CustomPresent->UpdateMirrorTexture_RenderThread(); #if !PLATFORM_ANDROID #if 0 // The entire target should be cleared by the tonemapper and pp material \ // Clear the padding between two eyes const int32 GapMinX = ViewFamily.Views[0]->UnscaledViewRect.Max.X; const int32 GapMaxX = ViewFamily.Views[1]->UnscaledViewRect.Min.X; if (GapMinX < GapMaxX) { SCOPED_DRAW_EVENT(RHICmdList, OculusClearQuad) const int32 GapMinY = ViewFamily.Views[0]->UnscaledViewRect.Min.Y; const int32 GapMaxY = ViewFamily.Views[1]->UnscaledViewRect.Max.Y; FRHIRenderPassInfo RPInfo(ViewFamily.RenderTarget->GetRenderTargetTexture(), ERenderTargetActions::DontLoad_Store); RHICmdList.BeginRenderPass(RPInfo, TEXT("Clear")); { RHICmdList.SetViewport(GapMinX, GapMinY, 0, GapMaxX, GapMaxY, 1.0f); DrawClearQuad(RHICmdList, FLinearColor::Black); } RHICmdList.EndRenderPass(); } #endif #else // ensure we have attached JNI to this thread - this has to happen persistently as the JNI could detach if the app loses focus FAndroidApplication::GetJavaEnv(); #endif EnableInsightPassthrough_RenderThread(Settings_RenderThread->Flags.bInsightPassthroughEnabled); // Start RHI frame StartRHIFrame_RenderThread(); // Update performance stats PerformanceStats.Frames++; PerformanceStats.Seconds = FPlatformTime::Seconds(); } void FOculusXRHMD::PreRenderView_RenderThread(FRDGBuilder& GraphBuilder, FSceneView& InView) { } void FOculusXRHMD::PostRenderViewFamily_RenderThread(FRDGBuilder& GraphBuilder, FSceneViewFamily& InViewFamily) { CheckInRenderThread(); if (InViewFamily.Views[0]->StereoPass != EStereoscopicPass::eSSP_FULL) { FinishRenderFrame_RenderThread(GraphBuilder); } } #if UE_VERSION_OLDER_THAN(5, 3, 0) void FOculusXRHMD::PostRenderBasePassMobile_RenderThread(FRHICommandListImmediate& RHICmdList, FSceneView& InView) #else void FOculusXRHMD::PostRenderBasePassMobile_RenderThread(FRHICommandList& RHICmdList, FSceneView& InView) #endif { if (bHardOcclusionsEnabled) { RenderHardOcclusions_RenderThread(RHICmdList, InView); } #ifndef WITH_OCULUS_BRANCH UpdateFoveationOffsets_RenderThread(); #endif } BEGIN_SHADER_PARAMETER_STRUCT(FPostBasePassViewExtensionParameters, ) SHADER_PARAMETER_RDG_UNIFORM_BUFFER(FSceneTextureUniformParameters, SceneTextures) RENDER_TARGET_BINDING_SLOTS() END_SHADER_PARAMETER_STRUCT() void FOculusXRHMD::PostRenderBasePassDeferred_RenderThread(FRDGBuilder& GraphBuilder, FSceneView& InView, const FRenderTargetBindingSlots& RenderTargets, TRDGUniformBufferRef SceneTextures) { if (bHardOcclusionsEnabled) { auto* PassParameters = GraphBuilder.AllocParameters(); PassParameters->RenderTargets = RenderTargets; PassParameters->SceneTextures = SceneTextures; GraphBuilder.AddPass(RDG_EVENT_NAME("RenderHardOcclusions_RenderThread"), PassParameters, ERDGPassFlags::Raster, [this, &InView](FRHICommandListImmediate& RHICmdList) { RenderHardOcclusions_RenderThread(RHICmdList, InView); }); } } #ifdef WITH_OCULUS_BRANCH #if UE_VERSION_OLDER_THAN(5, 3, 0) void FOculusXRHMD::PostSceneColorRenderingMobile_RenderThread(FRHICommandListImmediate& RHICmdList, FSceneView& InView) #else void FOculusXRHMD::PostSceneColorRenderingMobile_RenderThread(FRHICommandList& RHICmdList, FSceneView& InView) #endif { UpdateFoveationOffsets_RenderThread(); } #endif int32 FOculusXRHMD::GetPriority() const { // We want to run after the FDefaultXRCamera's view extension return -1; } #ifdef WITH_OCULUS_BRANCH bool FOculusXRHMD::LateLatchingEnabled() const { #if OCULUS_HMD_SUPPORTED_PLATFORMS_VULKAN && PLATFORM_ANDROID // No LateLatching supported when occlusion culling is enabled due to mid frame submission // No LateLatching supported for non Multi view ATM due to viewUniformBuffer reusing. // The setting can be disabled in FOculusXRHMD::UpdateStereoRenderingParams return Settings->bLateLatching; #else return false; #endif } void FOculusXRHMD::PreLateLatchingViewFamily_RenderThread(FRHICommandListImmediate& RHICmdList, FSceneViewFamily& InViewFamily) { CheckInRenderThread(); FGameFrame* CurrentFrame = GetFrame_RenderThread(); if (CurrentFrame) { CurrentFrame->Flags.bRTLateUpdateDone = false; // Allow LateLatching to update poses again } } #endif bool FOculusXRHMD::SupportsSpaceWarp() const { #if PLATFORM_ANDROID // Use All static value here since those can't be change at runtime ensureMsgf(CustomPresent.IsValid(), TEXT("SupportsSpaceWarp can only be called post CustomPresent created")); const bool bOvrPlugin_OpenXR = Settings->XrApi == EOculusXRXrApi::OVRPluginOpenXR; static const auto CVarMobileMultiView = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("vr.MobileMultiView")); static const auto CVarSupportMobileSpaceWarp = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("vr.SupportMobileSpaceWarp")); bool bIsMobileMultiViewEnabled = (CVarMobileMultiView && CVarMobileMultiView->GetValueOnAnyThread() != 0); bool bIsUsingMobileMultiView = GSupportsMobileMultiView && bIsMobileMultiViewEnabled; bool bIsVulkan = CustomPresent->GetRenderAPI() == ovrpRenderAPI_Vulkan; bool spaceWarpSupported = bOvrPlugin_OpenXR && bIsVulkan && bIsUsingMobileMultiView && CVarSupportMobileSpaceWarp && (CVarSupportMobileSpaceWarp->GetValueOnAnyThread() != 0); return spaceWarpSupported; #else return false; #endif } FOculusXRHMD::FOculusXRHMD(const FAutoRegister& AutoRegister) : FHeadMountedDisplayBase(nullptr) , FHMDSceneViewExtension(AutoRegister) , ConsoleCommands(this) , InsightInitStatus(FInsightInitStatus::NotInitialized) , bShutdownRequestQueued(false) , bShouldWait_GameThread(true) , bIsRendering_RenderThread(false) { Flags.Raw = 0; OCFlags.Raw = 0; TrackingOrigin = EHMDTrackingOrigin::Type::Eye; DeltaControlRotation = FRotator::ZeroRotator; // used from ApplyHmdRotation LastPlayerOrientation = FQuat::Identity; LastPlayerLocation = FVector::ZeroVector; CachedWindowSize = FIntPoint::ZeroValue; CachedWorldToMetersScale = 100.0f; LastTrackingToWorld = FTransform::Identity; NextFrameNumber = 0; WaitFrameNumber = (uint32)-1; NextLayerId = 0; Settings = CreateNewSettings(); RendererModule = nullptr; SplashLayerHandle = -1; SplashRotation = FRotator(); bIsStandaloneStereoOnlyDevice = IHeadMountedDisplayModule::IsAvailable() && IHeadMountedDisplayModule::Get().IsStandaloneStereoOnlyDevice(); bMultiPlayer = false; } FOculusXRHMD::~FOculusXRHMD() { Shutdown(); } bool FOculusXRHMD::Startup() { if (GIsEditor) { Settings->Flags.bHeadTrackingEnforced = true; } check(!CustomPresent.IsValid()); FString RHIString; { FString HardwareDetails = FHardwareInfo::GetHardwareDetailsString(); FString RHILookup = NAME_RHI.ToString() + TEXT("="); if (!FParse::Value(*HardwareDetails, *RHILookup, RHIString)) { return false; } } #if OCULUS_HMD_SUPPORTED_PLATFORMS_D3D11 if (RHIString == TEXT("D3D11")) { CustomPresent = CreateCustomPresent_D3D11(this); } else #endif #if OCULUS_HMD_SUPPORTED_PLATFORMS_D3D12 if (RHIString == TEXT("D3D12")) { CustomPresent = CreateCustomPresent_D3D12(this); } else #endif #if OCULUS_HMD_SUPPORTED_PLATFORMS_VULKAN if (RHIString == TEXT("Vulkan")) { CustomPresent = CreateCustomPresent_Vulkan(this); } else #endif { UE_LOG(LogHMD, Warning, TEXT("%s is not currently supported by OculusXRHMD plugin"), *RHIString); return false; } // grab a pointer to the renderer module for displaying our mirror window static const FName RendererModuleName("Renderer"); RendererModule = FModuleManager::GetModulePtr(RendererModuleName); #if PLATFORM_ANDROID // register our application lifetime delegates FCoreDelegates::ApplicationWillEnterBackgroundDelegate.AddRaw(this, &FOculusXRHMD::ApplicationPauseDelegate); FCoreDelegates::ApplicationHasEnteredForegroundDelegate.AddRaw(this, &FOculusXRHMD::ApplicationResumeDelegate); #endif // Create eye layer IStereoLayers::FLayerDesc EyeLayerDesc; EyeLayerDesc.Priority = INT_MIN; EyeLayerDesc.Flags = LAYER_FLAG_TEX_CONTINUOUS_UPDATE; uint32 EyeLayerId = CreateLayer(EyeLayerDesc); check(EyeLayerId == 0); Splash = MakeShareable(new FSplash(this)); Splash->Startup(); #if !PLATFORM_ANDROID SpectatorScreenController = MakeUnique(this); #endif UE_LOG(LogHMD, Log, TEXT("Oculus plugin initialized. Version: %s"), *GetVersionString()); return true; } void FOculusXRHMD::PreShutdown() { if (Splash.IsValid()) { Splash->PreShutdown(); } } void FOculusXRHMD::Shutdown() { CheckInGameThread(); if (Splash.IsValid()) { Splash->Shutdown(); Splash = nullptr; // The base implementation stores a raw pointer to the Splash object and tries to deallocate it in its destructor LoadingScreen = nullptr; } if (CustomPresent.IsValid()) { CustomPresent->Shutdown(); CustomPresent = nullptr; } ReleaseDevice(); Settings.Reset(); LayerMap.Reset(); } void FOculusXRHMD::ApplicationPauseDelegate() { ExecuteOnRenderThread([this]() { ExecuteOnRHIThread([this]() { FOculusXRHMDModule::GetPluginWrapper().DestroyDistortionWindow2(); }); }); OCFlags.AppIsPaused = true; } void FOculusXRHMD::ApplicationResumeDelegate() { if (OCFlags.AppIsPaused && !InitializeSession()) { UE_LOG(LogHMD, Log, TEXT("HMD initialization failed")); } OCFlags.AppIsPaused = false; } static const FString EYE_TRACKING_PERMISSION_NAME("com.oculus.permission.EYE_TRACKING"); bool FOculusXRHMD::CheckEyeTrackingPermission(EOculusXRFoveatedRenderingMethod InFoveatedRenderingMethod) { #if PLATFORM_ANDROID // Check and request eye tracking permissions, bind delegate for handling permission request result if (!UAndroidPermissionFunctionLibrary::CheckPermission(EYE_TRACKING_PERMISSION_NAME)) { TArray Permissions; Permissions.Add(EYE_TRACKING_PERMISSION_NAME); UAndroidPermissionCallbackProxy* Proxy = UAndroidPermissionFunctionLibrary::AcquirePermissions(Permissions); Proxy->OnPermissionsGrantedDelegate.AddLambda([this, InFoveatedRenderingMethod](const TArray& Permissions, const TArray& GrantResults) { int PermIndex = Permissions.Find(EYE_TRACKING_PERMISSION_NAME); if (PermIndex != INDEX_NONE && GrantResults[PermIndex]) { UE_LOG(LogHMD, Verbose, TEXT("com.oculus.permission.EYE_TRACKING permission granted")); FoveatedRenderingMethod = InFoveatedRenderingMethod; FOculusEventDelegates::OculusEyeTrackingStateChanged.Broadcast(true); } else { UE_LOG(LogHMD, Log, TEXT("com.oculus.permission.EYE_TRACKING permission denied")); if (InFoveatedRenderingMethod == EOculusXRFoveatedRenderingMethod::EyeTrackedFoveatedRendering) { EyeTrackedFoveatedRenderingFallback(); } FOculusEventDelegates::OculusEyeTrackingStateChanged.Broadcast(false); } }); return false; } #endif // PLATFORM_ANDROID return true; } bool FOculusXRHMD::InitializeSession() { UE_LOG(LogHMD, Log, TEXT("Initializing OVRPlugin session")); if (!FOculusXRHMDModule::GetPluginWrapper().GetInitialized()) { #if !UE_BUILD_SHIPPING ovrpLogCallback logCallback = OvrpLogCallback; #else ovrpLogCallback logCallback = nullptr; #endif #if PLATFORM_ANDROID void* activity = (void*)FAndroidApplication::GetGameActivityThis(); #else void* activity = nullptr; #endif int initializeFlags = GIsEditor ? ovrpInitializeFlag_SupportsVRToggle : 0; initializeFlags |= CustomPresent->SupportsSRGB() ? ovrpInitializeFlag_SupportSRGBFrameBuffer : 0; if (Settings->Flags.bSupportsDash) { initializeFlags |= ovrpInitializeFlag_FocusAware; } if (SupportsSpaceWarp()) // Configure for space warp { initializeFlags |= ovrpInitializeFlag_SupportAppSpaceWarp; UE_LOG(LogHMD, Log, TEXT("[Mobile SpaceWarp] Application is configured to support mobile spacewarp")); } bNeedReAllocateMotionVectorTexture_RenderThread = false; #if WITH_EDITOR && PLATFORM_WINDOWS // Attempt Late Initialization in-editor // FOculusXRHMDModule::PreInit always returns true in this case, // so we need to check the flag directly. if (GIsEditor && FOculusXRHMDModule::Get().PreInit() && !FOculusXRHMDModule::Get().bPreInit) { return false; } #endif if (OVRP_FAILURE(FOculusXRHMDModule::GetPluginWrapper().Initialize7( CustomPresent->GetRenderAPI(), logCallback, activity, CustomPresent->GetOvrpInstance(), CustomPresent->GetOvrpPhysicalDevice(), CustomPresent->GetOvrpDevice(), CustomPresent->GetOvrpCommandQueue(), nullptr /*vkGetInstanceProcAddr*/, 0 /*vkQueueFamilyIndex*/, nullptr /*d3dDevice*/, initializeFlags, { OVRP_VERSION }))) { return false; } ovrpBool Supported = ovrpBool_False; if (Settings->bSupportEyeTrackedFoveatedRendering) { FOculusXRHMDModule::GetPluginWrapper().GetFoveationEyeTrackedSupported(&Supported); } bEyeTrackedFoveatedRenderingSupported = Supported == ovrpBool_True; SetFoveatedRenderingMethod(Settings->FoveatedRenderingMethod); SetFoveatedRenderingLevel(Settings->FoveatedRenderingLevel, Settings->bDynamicFoveatedRendering); NextFrameNumber = 0; WaitFrameNumber = (uint32)-1; } FOculusXRHMDModule::GetPluginWrapper().SetAppEngineInfo2( "Unreal Engine", TCHAR_TO_ANSI(*FEngineVersion::Current().ToString()), GIsEditor ? ovrpBool_True : ovrpBool_False); int flag = ovrpDistortionWindowFlag_None; FOculusXRHMDModule::GetPluginWrapper().SetupDistortionWindow3(flag); FOculusXRHMDModule::GetPluginWrapper().SetSuggestedCpuPerformanceLevel((ovrpProcessorPerformanceLevel)Settings->SuggestedCpuPerfLevel); FOculusXRHMDModule::GetPluginWrapper().SetSuggestedGpuPerformanceLevel((ovrpProcessorPerformanceLevel)Settings->SuggestedGpuPerfLevel); FOculusXRHMDModule::GetPluginWrapper().SetFoveationEyeTracked(FoveatedRenderingMethod == EOculusXRFoveatedRenderingMethod::EyeTrackedFoveatedRendering); FOculusXRHMDModule::GetPluginWrapper().SetTiledMultiResLevel((ovrpTiledMultiResLevel)FoveatedRenderingLevel.load()); FOculusXRHMDModule::GetPluginWrapper().SetTiledMultiResDynamic(bDynamicFoveatedRendering.load()); FOculusXRHMDModule::GetPluginWrapper().SetAppCPUPriority2(ovrpBool_True); FOculusXRHMDModule::GetPluginWrapper().SetLocalDimming(ovrpBool_True); OCFlags.NeedSetTrackingOrigin = true; FOculusXRHMDModule::GetPluginWrapper().SetClientColorDesc((ovrpColorSpace)Settings->ColorSpace); return true; } void FOculusXRHMD::ShutdownSession() { ExecuteOnRenderThread([this]() { ExecuteOnRHIThread([this]() { FOculusXRHMDModule::GetPluginWrapper().DestroyDistortionWindow2(); }); }); FOculusXRHMDModule::GetPluginWrapper().Shutdown2(); bIsRendering_RenderThread = false; } bool FOculusXRHMD::InitDevice() { CheckInGameThread(); if (FOculusXRHMDModule::GetPluginWrapper().GetInitialized()) { // Already created and present return true; } if (!IsHMDEnabled()) { // Don't bother if HMD is not connected return false; } LoadFromSettings(); if (!InitializeSession()) { UE_LOG(LogHMD, Log, TEXT("HMD initialization failed")); return false; } // Don't need to reset these flags on application resume, so put them in InitDevice instead of InitializeSession bNeedReAllocateViewportRenderTarget = true; bNeedReAllocateDepthTexture_RenderThread = false; bNeedReAllocateFoveationTexture_RenderThread = false; Flags.bNeedDisableStereo = false; OCFlags.NeedSetFocusToGameViewport = true; if (!CustomPresent->IsUsingCorrectDisplayAdapter()) { UE_LOG(LogHMD, Error, TEXT("Using incorrect display adapter for HMD.")); ShutdownSession(); return false; } if (OVRP_FAILURE(FOculusXRHMDModule::GetPluginWrapper().GetSystemHeadsetType2(&Settings->SystemHeadset))) { Settings->SystemHeadset = ovrpSystemHeadset_None; } FOculusXRHMDModule::GetPluginWrapper().Update3(ovrpStep_Render, 0, 0.0); if (Settings->Flags.bPixelDensityAdaptive) { static const auto DynamicResOperationCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("r.DynamicRes.OperationMode")); if (DynamicResOperationCVar) { DynamicResOperationCVar->Set(2); } GEngine->ChangeDynamicResolutionStateAtNextFrame(MakeShareable(new FDynamicResolutionState(Settings))); } UpdateHmdRenderInfo(); UpdateStereoRenderingParams(); const bool bEnablePassthrough = Settings->Flags.bInsightPassthroughEnabled; ExecuteOnRenderThread([this, bEnablePassthrough](FRHICommandListImmediate& RHICmdList) { InitializeEyeLayer_RenderThread(RHICmdList); EnableInsightPassthrough_RenderThread(bEnablePassthrough); }); if (!EyeLayer_RenderThread.IsValid() || !EyeLayer_RenderThread->GetSwapChain().IsValid()) { UE_LOG(LogHMD, Error, TEXT("Failed to create eye layer swap chain.")); ShutdownSession(); return false; } if (!HiddenAreaMeshes[0].IsValid() || !HiddenAreaMeshes[1].IsValid()) { SetupOcclusionMeshes(); } #if !UE_BUILD_SHIPPING DrawDebugDelegateHandle = UDebugDrawService::Register(TEXT("Game"), FDebugDrawDelegate::CreateRaw(this, &FOculusXRHMD::DrawDebug)); #endif // Do not set VR focus in Editor by just creating a device; Editor may have it created w/o requiring focus. // Instead, set VR focus in OnBeginPlay (VR Preview will run there first). if (!GIsEditor) { FApp::SetUseVRFocus(true); FApp::SetHasVRFocus(true); } FOculusXRHMDModule::GetPluginWrapper().SetClientColorDesc((ovrpColorSpace)Settings->ColorSpace); return true; } void FOculusXRHMD::ReleaseDevice() { CheckInGameThread(); if (FOculusXRHMDModule::GetPluginWrapper().GetInitialized()) { // Wait until the next frame before ending the session (workaround for DX12/Vulkan resources being ripped out from under us before we're done with them). bShutdownRequestQueued = true; } } void BuildOcclusionMesh(FRHICommandList& RHICmdList, FHMDViewMesh& Mesh, ovrpEye Eye, ovrpViewportStencilType MeshType) { int VertexCount = 0; int IndexCount = 0; ovrpResult Result = ovrpResult::ovrpFailure; if (OVRP_FAILURE(Result = FOculusXRHMDModule::GetPluginWrapper().GetViewportStencil(Eye, MeshType, nullptr, &VertexCount, nullptr, &IndexCount))) { return; } FRHIResourceCreateInfo CreateInfo(TEXT("FOculusXRHMD")); #if UE_VERSION_OLDER_THAN(5, 3, 0) Mesh.VertexBufferRHI = RHICreateVertexBuffer(sizeof(FFilterVertex) * VertexCount, BUF_Static, CreateInfo); void* VoidPtr = RHILockBuffer(Mesh.VertexBufferRHI, 0, sizeof(FFilterVertex) * VertexCount, RLM_WriteOnly); #else Mesh.VertexBufferRHI = RHICmdList.CreateVertexBuffer(sizeof(FFilterVertex) * VertexCount, BUF_Static, CreateInfo); void* VoidPtr = RHICmdList.LockBuffer(Mesh.VertexBufferRHI, 0, sizeof(FFilterVertex) * VertexCount, RLM_WriteOnly); #endif FFilterVertex* pVertices = reinterpret_cast(VoidPtr); #if UE_VERSION_OLDER_THAN(5, 3, 0) Mesh.IndexBufferRHI = RHICreateIndexBuffer(sizeof(uint16), sizeof(uint16) * IndexCount, BUF_Static, CreateInfo); void* VoidPtr2 = RHILockBuffer(Mesh.IndexBufferRHI, 0, sizeof(uint16) * IndexCount, RLM_WriteOnly); #else Mesh.IndexBufferRHI = RHICmdList.CreateIndexBuffer(sizeof(uint16), sizeof(uint16) * IndexCount, BUF_Static, CreateInfo); void* VoidPtr2 = RHICmdList.LockBuffer(Mesh.IndexBufferRHI, 0, sizeof(uint16) * IndexCount, RLM_WriteOnly); #endif uint16* pIndices = reinterpret_cast(VoidPtr2); ovrpVector2f* const ovrpVertices = new ovrpVector2f[VertexCount]; FOculusXRHMDModule::GetPluginWrapper().GetViewportStencil(Eye, MeshType, ovrpVertices, &VertexCount, pIndices, &IndexCount); for (int i = 0; i < VertexCount; ++i) { FFilterVertex& Vertex = pVertices[i]; CA_SUPPRESS(6385); // warning C6385: Reading invalid data from 'ovrpVertices': the readable size is 'VertexCount*8' bytes, but '16' bytes may be read const ovrpVector2f& Position = ovrpVertices[i]; if (MeshType == ovrpViewportStencilType_HiddenArea) { Vertex.Position.X = (Position.x * 2.0f) - 1.0f; Vertex.Position.Y = (Position.y * 2.0f) - 1.0f; Vertex.Position.Z = 1.0f; Vertex.Position.W = 1.0f; Vertex.UV.X = 0.0f; Vertex.UV.Y = 0.0f; } else if (MeshType == ovrpViewportStencilType_VisibleArea) { Vertex.Position.X = Position.x; Vertex.Position.Y = 1.0f - Position.y; Vertex.Position.Z = 0.0f; Vertex.Position.W = 1.0f; Vertex.UV.X = Position.x; Vertex.UV.Y = 1.0f - Position.y; } else { check(0); } } Mesh.NumIndices = IndexCount; Mesh.NumVertices = VertexCount; Mesh.NumTriangles = IndexCount / 3; delete[] ovrpVertices; #if UE_VERSION_OLDER_THAN(5, 3, 0) RHIUnlockBuffer(Mesh.VertexBufferRHI); RHIUnlockBuffer(Mesh.IndexBufferRHI); #else RHICmdList.UnlockBuffer(Mesh.VertexBufferRHI); RHICmdList.UnlockBuffer(Mesh.IndexBufferRHI); #endif } void FOculusXRHMD::SetupOcclusionMeshes() { CheckInGameThread(); FOculusXRHMD* const Self = this; ENQUEUE_RENDER_COMMAND(SetupOcclusionMeshesCmd) ([Self](FRHICommandList& RHICmdList) { BuildOcclusionMesh(RHICmdList, Self->HiddenAreaMeshes[0], ovrpEye_Left, ovrpViewportStencilType_HiddenArea); BuildOcclusionMesh(RHICmdList, Self->HiddenAreaMeshes[1], ovrpEye_Right, ovrpViewportStencilType_HiddenArea); BuildOcclusionMesh(RHICmdList, Self->VisibleAreaMeshes[0], ovrpEye_Left, ovrpViewportStencilType_VisibleArea); BuildOcclusionMesh(RHICmdList, Self->VisibleAreaMeshes[1], ovrpEye_Right, ovrpViewportStencilType_VisibleArea); }); } static ovrpMatrix4f ovrpMatrix4f_Projection(const ovrpFrustum2f& frustum, bool leftHanded) { float handednessScale = leftHanded ? 1.0f : -1.0f; // A projection matrix is very like a scaling from NDC, so we can start with that. float projXScale = 2.0f / (frustum.Fov.LeftTan + frustum.Fov.RightTan); float projXOffset = (frustum.Fov.LeftTan - frustum.Fov.RightTan) * projXScale * 0.5f; float projYScale = 2.0f / (frustum.Fov.UpTan + frustum.Fov.DownTan); float projYOffset = (frustum.Fov.UpTan - frustum.Fov.DownTan) * projYScale * 0.5f; ovrpMatrix4f projection; // Produces X result, mapping clip edges to [-w,+w] projection.M[0][0] = projXScale; projection.M[0][1] = 0.0f; projection.M[0][2] = handednessScale * projXOffset; projection.M[0][3] = 0.0f; // Produces Y result, mapping clip edges to [-w,+w] // Hey - why is that YOffset negated? // It's because a projection matrix transforms from world coords with Y=up, // whereas this is derived from an NDC scaling, which is Y=down. projection.M[1][0] = 0.0f; projection.M[1][1] = projYScale; projection.M[1][2] = handednessScale * -projYOffset; projection.M[1][3] = 0.0f; // Produces Z-buffer result projection.M[2][0] = 0.0f; projection.M[2][1] = 0.0f; if (FGenericPlatformMath::IsFinite(frustum.zFar)) { projection.M[2][2] = -handednessScale * frustum.zFar / (frustum.zNear - frustum.zFar); projection.M[2][3] = (frustum.zFar * frustum.zNear) / (frustum.zNear - frustum.zFar); } else { projection.M[2][2] = handednessScale; projection.M[2][3] = -frustum.zNear; } // Produces W result (= Z in) projection.M[3][0] = 0.0f; projection.M[3][1] = 0.0f; projection.M[3][2] = handednessScale; projection.M[3][3] = 0.0f; return projection; } void FOculusXRHMD::UpdateStereoRenderingParams() { CheckInGameThread(); // Update PixelDensity bool bSupportsDepth = true; if (Settings->Flags.bPixelDensityAdaptive) { FLayer* EyeLayer = EyeLayer_RenderThread.Get(); float NewPixelDensity = 1.0; if (EyeLayer && EyeLayer->GetOvrpId()) { ovrpSizei RecommendedResolution = { 0, 0 }; FOculusXRHMDModule::GetPluginWrapper().GetLayerRecommendedResolution(EyeLayer->GetOvrpId(), &RecommendedResolution); if (RecommendedResolution.h > 0) { NewPixelDensity = RecommendedResolution.h * (float)Settings->PixelDensityMax / Settings->RenderTargetSize.Y; } } const float PixelDensityCVarOverride = CVarOculusDynamicResolutionPixelDensity.GetValueOnAnyThread(); if (PixelDensityCVarOverride > 0) { NewPixelDensity = PixelDensityCVarOverride; } Settings->SetPixelDensitySmooth(NewPixelDensity); } else { static const auto PixelDensityCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("vr.PixelDensity")); Settings->SetPixelDensity(PixelDensityCVar ? PixelDensityCVar->GetFloat() : 1.0f); // Due to hijacking the depth target directly from the scene context, we can't support depth compositing if it's being scaled by screen percentage since it wont match our color render target dimensions. static const auto ScreenPercentageCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("r.ScreenPercentage")); float ScreenPercentage = (!ScreenPercentageCVar) ? 100.0f : ScreenPercentageCVar->GetFloat(); #if !UE_VERSION_OLDER_THAN(5, 3, 0) // 5.3 changes the default screen percentage to 0 and uses r.ScreenPercentage.Default values to determine screen percentage if (ScreenPercentageCVar->GetFloat() <= 0.0f) { static const auto VRScreenPercentageModeCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("r.ScreenPercentage.Default.VR.Mode")); // With default VR screen percentage modes, only support depth with manual screen percentage set to 100 if (VRScreenPercentageModeCVar && VRScreenPercentageModeCVar->GetInt() == static_cast(EScreenPercentageMode::Manual)) { static const auto ManualScreenPercentageCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("r.ScreenPercentage.Default")); ScreenPercentage = ManualScreenPercentageCVar->GetFloat(); } } #endif bSupportsDepth = ScreenPercentage == 100.0f; } // Update EyeLayer FLayerPtr* EyeLayerFound = LayerMap.Find(0); FLayer* EyeLayer = new FLayer(**EyeLayerFound); *EyeLayerFound = MakeShareable(EyeLayer); ovrpLayout Layout = ovrpLayout_DoubleWide; static const auto CVarMobileMultiView = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("vr.MobileMultiView")); const bool bIsMobileMultiViewEnabled = (CVarMobileMultiView && CVarMobileMultiView->GetValueOnAnyThread() != 0); const bool bIsUsingMobileMultiView = (GSupportsMobileMultiView || GRHISupportsArrayIndexFromAnyShader) && bIsMobileMultiViewEnabled; Settings->CurrentFeatureLevel = GEngine ? GEngine->GetDefaultWorldFeatureLevel() : GMaxRHIFeatureLevel; Settings->CurrentShaderPlatform = GShaderPlatformForFeatureLevel[Settings->CurrentFeatureLevel]; // for now only mobile rendering codepaths use the array rendering system, so PC-native should stay in doublewide if (bIsUsingMobileMultiView && IsMobilePlatform(Settings->CurrentShaderPlatform)) { Layout = ovrpLayout_Array; } #if PLATFORM_ANDROID if (!bIsUsingMobileMultiView && Settings->bLateLatching) { UE_CLOG(true, LogHMD, Error, TEXT("LateLatching can't be used when Multiview is off, force disabling.")); Settings->bLateLatching = false; } if (GetMutableDefault()->bOcclusionCulling && Settings->bLateLatching) { UE_CLOG(true, LogHMD, Error, TEXT("LateLatching can't used when Occlusion culling is on due to mid frame vkQueueSubmit, force disabling")); Settings->bLateLatching = false; } #endif const bool bForceSymmetric = CVarOculusForceSymmetric.GetValueOnAnyThread() == 1 && (Layout == ovrpLayout_Array); ovrpLayerDesc_EyeFov EyeLayerDesc; const bool requestsSubsampled = CVarOculusEnableSubsampledLayout.GetValueOnAnyThread() == 1 && CustomPresent->SupportsSubsampled(); int eyeLayerFlags = requestsSubsampled ? ovrpLayerFlag_Subsampled : 0; ovrpTextureFormat MvPixelFormat = ovrpTextureFormat_R16G16B16A16_FP; ovrpTextureFormat MvDepthFormat = ovrpTextureFormat_D24_S8; int SpaceWarpAllocateFlag = 0; if (SupportsSpaceWarp()) { SpaceWarpAllocateFlag = ovrpLayerFlag_SpaceWarpDataAllocation | ovrpLayerFlag_SpaceWarpDedicatedDepth; bool spaceWarpEnabledByUser = CVarOculusEnableSpaceWarpUser.GetValueOnAnyThread() != 0; bool spaceWarpEnabledInternal = CVarOculusEnableSpaceWarpInternal.GetValueOnAnyThread() != 0; if (spaceWarpEnabledByUser != spaceWarpEnabledInternal) { CVarOculusEnableSpaceWarpInternal->Set(spaceWarpEnabledByUser); } } const bool bCompositeDepth = Settings->Flags.bCompositeDepth; if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().CalculateEyeLayerDesc3( Layout, Settings->Flags.bPixelDensityAdaptive ? Settings->PixelDensityMax : Settings->PixelDensity, Settings->Flags.bHQDistortion ? 0 : 1, 1, // UNDONE CustomPresent->GetOvrpTextureFormat(CustomPresent->GetDefaultPixelFormat(), Settings->Flags.bsRGBEyeBuffer), (bCompositeDepth && bSupportsDepth) ? CustomPresent->GetDefaultDepthOvrpTextureFormat() : ovrpTextureFormat_None, MvPixelFormat, MvDepthFormat, 1.0f, CustomPresent->GetLayerFlags() | eyeLayerFlags | SpaceWarpAllocateFlag, &EyeLayerDesc))) { ovrpFovf FrameFov[ovrpEye_Count] = { EyeLayerDesc.Fov[0], EyeLayerDesc.Fov[1] }; if (bForceSymmetric) { // calculate symmetric FOV from runtime-provided asym in EyeLayerDesc FrameFov[0].RightTan = FrameFov[1].RightTan = FMath::Max(EyeLayerDesc.Fov[0].RightTan, EyeLayerDesc.Fov[1].RightTan); FrameFov[0].LeftTan = FrameFov[1].LeftTan = FMath::Max(EyeLayerDesc.Fov[0].LeftTan, EyeLayerDesc.Fov[1].LeftTan); const float asymTanSize = EyeLayerDesc.Fov[0].RightTan + EyeLayerDesc.Fov[0].LeftTan; const float symTanSize = FrameFov[0].RightTan + FrameFov[0].LeftTan; // compute new resolution from a number of tile multiple, and the increase in width from symmetric FOV const int numberTiles = (int)floor(EyeLayerDesc.TextureSize.w * symTanSize / (96.0 * asymTanSize)); EyeLayerDesc.TextureSize.w = EyeLayerDesc.MaxViewportSize.w = numberTiles * 96; } // Scaling for DynamicResolution will happen later - see FSceneRenderer::PrepareViewRectsForRendering. // If scaling does occur, EyeRenderViewport will be updated in FOculusXRHMD::SetFinalViewRect. FIntPoint UnscaledViewportSize = FIntPoint(EyeLayerDesc.MaxViewportSize.w, EyeLayerDesc.MaxViewportSize.h); if (Settings->Flags.bPixelDensityAdaptive) { FIntPoint ViewRect = FIntPoint( FMath::CeilToInt(EyeLayerDesc.MaxViewportSize.w / Settings->PixelDensityMax), FMath::CeilToInt(EyeLayerDesc.MaxViewportSize.h / Settings->PixelDensityMax)); UnscaledViewportSize = ViewRect; FIntPoint UpperViewRect = FIntPoint( FMath::CeilToInt(ViewRect.X * Settings->PixelDensityMax), FMath::CeilToInt(ViewRect.Y * Settings->PixelDensityMax)); FIntPoint TextureSize; QuantizeSceneBufferSize(UpperViewRect, TextureSize); EyeLayerDesc.MaxViewportSize.w = TextureSize.X; EyeLayerDesc.MaxViewportSize.h = TextureSize.Y; } // Unreal assumes no gutter between eyes EyeLayerDesc.TextureSize.w = EyeLayerDesc.MaxViewportSize.w; EyeLayerDesc.TextureSize.h = EyeLayerDesc.MaxViewportSize.h; if (Layout == ovrpLayout_DoubleWide) { EyeLayerDesc.TextureSize.w *= 2; } EyeLayer->SetEyeLayerDesc(EyeLayerDesc); EyeLayer->bNeedsTexSrgbCreate = Settings->Flags.bsRGBEyeBuffer; Settings->RenderTargetSize = FIntPoint(EyeLayerDesc.TextureSize.w, EyeLayerDesc.TextureSize.h); Settings->EyeRenderViewport[0].Min = FIntPoint::ZeroValue; Settings->EyeRenderViewport[0].Max = UnscaledViewportSize; Settings->EyeRenderViewport[1].Min = FIntPoint(Layout == ovrpLayout_DoubleWide ? UnscaledViewportSize.X : 0, 0); Settings->EyeRenderViewport[1].Max = Settings->EyeRenderViewport[1].Min + UnscaledViewportSize; Settings->EyeUnscaledRenderViewport[0] = Settings->EyeRenderViewport[0]; Settings->EyeUnscaledRenderViewport[1] = Settings->EyeRenderViewport[1]; // Update projection matrices ovrpFrustum2f frustumLeft = { 0.001f, 1000.0f, FrameFov[0] }; ovrpFrustum2f frustumRight = { 0.001f, 1000.0f, FrameFov[1] }; ovrpFrustum2f frustumCenter = { 0.001f, 1000.0f, { FrameFov[0].UpTan, FrameFov[0].DownTan, FrameFov[0].LeftTan, FrameFov[1].RightTan } }; Settings->EyeProjectionMatrices[0] = ovrpMatrix4f_Projection(frustumLeft, true); Settings->EyeProjectionMatrices[1] = ovrpMatrix4f_Projection(frustumRight, true); Settings->MonoProjectionMatrix = ovrpMatrix4f_Projection(frustumCenter, true); // given that we send a subrect in vpRectSubmit, the FOV is the default asym one in EyeLayerDesc, not FrameFov if (Frame.IsValid()) { Frame->Fov[0] = EyeLayerDesc.Fov[0]; Frame->Fov[1] = EyeLayerDesc.Fov[1]; Frame->SymmetricFov[0] = FrameFov[0]; Frame->SymmetricFov[1] = FrameFov[1]; } // Flag if need to recreate render targets if (!EyeLayer->CanReuseResources(EyeLayer_RenderThread.Get())) { AllocateEyeBuffer(); } } } void FOculusXRHMD::UpdateHmdRenderInfo() { CheckInGameThread(); FOculusXRHMDModule::GetPluginWrapper().GetSystemDisplayFrequency2(&Settings->VsyncToNextVsync); } void FOculusXRHMD::InitializeEyeLayer_RenderThread(FRHICommandListImmediate& RHICmdList) { check(!InGameThread()); CheckInRenderThread(); if (LayerMap[0].IsValid()) { FLayerPtr EyeLayer = LayerMap[0]->Clone(); EyeLayer->Initialize_RenderThread(Settings_RenderThread.Get(), CustomPresent, &DeferredDeletion, RHICmdList, EyeLayer_RenderThread.Get()); if (Layers_RenderThread.Num() > 0) { Layers_RenderThread[0] = EyeLayer; } else { Layers_RenderThread.Add(EyeLayer); } if (EyeLayer->GetDepthSwapChain().IsValid()) { if (!EyeLayer_RenderThread.IsValid() || EyeLayer->GetDepthSwapChain() != EyeLayer_RenderThread->GetDepthSwapChain()) { bNeedReAllocateDepthTexture_RenderThread = true; } } if (EyeLayer->GetFoveationSwapChain().IsValid()) { if (!EyeLayer_RenderThread.IsValid() || EyeLayer->GetFoveationSwapChain() != EyeLayer_RenderThread->GetFoveationSwapChain()) { bNeedReAllocateFoveationTexture_RenderThread = true; } #if !UE_VERSION_OLDER_THAN(5, 3, 0) FoveationImageGenerator = MakeShared(EyeLayer->GetFoveationSwapChain()); #endif // !UE_VERSION_OLDER_THAN(5, 3, 0) } if (EyeLayer->GetMotionVectorSwapChain().IsValid()) { if (!EyeLayer_RenderThread.IsValid() || EyeLayer->GetMotionVectorSwapChain() != EyeLayer_RenderThread->GetMotionVectorSwapChain() || EyeLayer->GetMotionVectorDepthSwapChain() != EyeLayer_RenderThread->GetMotionVectorDepthSwapChain()) { bNeedReAllocateMotionVectorTexture_RenderThread = true; UE_LOG(LogHMD, VeryVerbose, TEXT("[Mobile SpaceWarp] request to re-allocate motionVector textures")); } } if (EyeLayer_RenderThread.IsValid()) { DeferredDeletion.AddLayerToDeferredDeletionQueue(EyeLayer_RenderThread); } EyeLayer_RenderThread = EyeLayer; } } void FOculusXRHMD::ApplySystemOverridesOnStereo(bool force) { CheckInGameThread(); // ALWAYS SET r.FinishCurrentFrame to 0! Otherwise the perf might be poor. // @TODO: revise the FD3D11DynamicRHI::RHIEndDrawingViewport code (and other renderers) // to ignore this var completely. static const auto CFinishFrameVar = IConsoleManager::Get().FindConsoleVariable(TEXT("r.FinishCurrentFrame")); CFinishFrameVar->Set(0); } bool FOculusXRHMD::OnOculusStateChange(bool bIsEnabledNow) { if (!bIsEnabledNow) { // Switching from stereo ReleaseDevice(); ResetControlRotation(); return true; } else { // Switching to stereo if (InitDevice()) { Flags.bApplySystemOverridesOnStereo = true; return true; } DeltaControlRotation = FRotator::ZeroRotator; } return false; } class FSceneViewport* FOculusXRHMD::FindSceneViewport() { if (!GIsEditor) { UGameEngine* GameEngine = Cast(GEngine); return GameEngine->SceneViewport.Get(); } #if WITH_EDITOR else { UEditorEngine* EditorEngine = CastChecked(GEngine); FSceneViewport* PIEViewport = (FSceneViewport*)EditorEngine->GetPIEViewport(); if (PIEViewport != nullptr && PIEViewport->IsStereoRenderingAllowed()) { // PIE is setup for stereo rendering return PIEViewport; } else { // Check to see if the active editor viewport is drawing in stereo mode // @todo vreditor: Should work with even non-active viewport! FSceneViewport* EditorViewport = (FSceneViewport*)EditorEngine->GetActiveViewport(); if (EditorViewport != nullptr && EditorViewport->IsStereoRenderingAllowed()) { return EditorViewport; } } } #endif return nullptr; } bool FOculusXRHMD::ShouldDisableHiddenAndVisibileAreaMeshForSpectatorScreen_RenderThread() const { CheckInRenderThread(); // If you really need the eye corners to look nice, and can't just crop more, // and are willing to suffer a frametime hit... you could do this: #if 0 switch(GetSpectatorScreenMode_RenderThread()) { case ESpectatorScreenMode::SingleEyeLetterboxed: case ESpectatorScreenMode::SingleEyeCroppedToFill: case ESpectatorScreenMode::TexturePlusEye: return true; } #endif return false; } ESpectatorScreenMode FOculusXRHMD::GetSpectatorScreenMode_RenderThread() const { CheckInRenderThread(); return SpectatorScreenController ? SpectatorScreenController->GetSpectatorScreenMode() : ESpectatorScreenMode::Disabled; } #if !UE_BUILD_SHIPPING static const char* FormatLatencyReading(char* buff, size_t size, float val) { if (val < 0.000001f) { FCStringAnsi::Strcpy(buff, size, "N/A "); } else { FCStringAnsi::Snprintf(buff, size, "%4.2fms", val * 1000.0f); } return buff; } void FOculusXRHMD::DrawDebug(UCanvas* InCanvas, APlayerController* InPlayerController) { CheckInGameThread(); if (InCanvas && IsStereoEnabled() && Settings->Flags.bShowStats) { static const FColor TextColor(0, 255, 0); // Pick a larger font on console. UFont* const Font = FPlatformProperties::SupportsWindowedMode() ? GEngine->GetSmallFont() : GEngine->GetMediumFont(); const int32 RowHeight = FMath::TruncToInt(Font->GetMaxCharHeight() * 1.1f); float ClipX = InCanvas->ClipX; float ClipY = InCanvas->ClipY; float LeftPos = 0; ClipX -= 100; LeftPos = ClipX * 0.3f; float TopPos = ClipY * 0.4f; int32 X = (int32)LeftPos; int32 Y = (int32)TopPos; FString Str; if (!Settings->Flags.bPixelDensityAdaptive) { Str = FString::Printf(TEXT("PD: %.2f"), Settings->PixelDensity); } else { Str = FString::Printf(TEXT("PD: %.2f [%0.2f, %0.2f]"), Settings->PixelDensity, Settings->PixelDensityMin, Settings->PixelDensityMax); } InCanvas->Canvas->DrawShadowedString(X, Y, *Str, Font, TextColor); Y += RowHeight; Str = FString::Printf(TEXT("W-to-m scale: %.2f uu/m"), GetWorldToMetersScale()); InCanvas->Canvas->DrawShadowedString(X, Y, *Str, Font, TextColor); ovrpAppLatencyTimings AppLatencyTimings; if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetAppLatencyTimings2(&AppLatencyTimings))) { Y += RowHeight; char buf[5][20]; char destStr[100]; FCStringAnsi::Snprintf(destStr, sizeof(destStr), "Latency, ren: %s tw: %s pp: %s err: %s %s", FormatLatencyReading(buf[0], sizeof(buf[0]), AppLatencyTimings.LatencyRender), FormatLatencyReading(buf[1], sizeof(buf[1]), AppLatencyTimings.LatencyTimewarp), FormatLatencyReading(buf[2], sizeof(buf[2]), AppLatencyTimings.LatencyPostPresent), FormatLatencyReading(buf[3], sizeof(buf[3]), AppLatencyTimings.ErrorRender), FormatLatencyReading(buf[4], sizeof(buf[4]), AppLatencyTimings.ErrorTimewarp)); Str = ANSI_TO_TCHAR(destStr); InCanvas->Canvas->DrawShadowedString(X, Y, *Str, Font, TextColor); } // Second row X = (int32)LeftPos + 200; Y = (int32)TopPos; Str = FString::Printf(TEXT("HQ dist: %s"), (Settings->Flags.bHQDistortion) ? TEXT("ON") : TEXT("OFF")); InCanvas->Canvas->DrawShadowedString(X, Y, *Str, Font, TextColor); Y += RowHeight; float UserIPD; if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetUserIPD2(&UserIPD))) { Str = FString::Printf(TEXT("IPD: %.2f mm"), UserIPD * 1000.f); InCanvas->Canvas->DrawShadowedString(X, Y, *Str, Font, TextColor); Y += RowHeight; } } } #endif // #if !UE_BUILD_SHIPPING FOculusXRHMD* FOculusXRHMD::GetOculusXRHMD() { #if OCULUS_HMD_SUPPORTED_PLATFORMS if (GEngine && GEngine->XRSystem.IsValid()) { if (GEngine->XRSystem->GetSystemName() == OculusXRHMD::FOculusXRHMD::OculusSystemName) { return static_cast(GEngine->XRSystem.Get()); } } #endif return nullptr; } bool FOculusXRHMD::IsHMDActive() const { return FOculusXRHMDModule::GetPluginWrapper().GetInitialized() != ovrpBool_False; } float FOculusXRHMD::GetWorldToMetersScale() const { CheckInGameThread(); if (NextFrameToRender.IsValid()) { return NextFrameToRender->WorldToMetersScale; } if (GWorld != nullptr) { #if WITH_EDITOR // Workaround to allow WorldToMeters scaling to work correctly for controllers while running inside PIE. // The main world will most likely not be pointing at the PIE world while polling input, so if we find a world context // of that type, use that world's WorldToMeters instead. if (GIsEditor) { for (const FWorldContext& Context : GEngine->GetWorldContexts()) { if (Context.WorldType == EWorldType::PIE) { return Context.World()->GetWorldSettings()->WorldToMeters; } } } #endif //WITH_EDITOR // We're not currently rendering a frame, so just use whatever world to meters the main world is using. // This can happen when we're polling input in the main engine loop, before ticking any worlds. return GWorld->GetWorldSettings()->WorldToMeters; } return 100.0f; } FVector FOculusXRHMD::GetNeckPosition(const FQuat& HeadOrientation, const FVector& HeadPosition) { CheckInGameThread(); FVector NeckPosition = HeadOrientation.Inverse().RotateVector(HeadPosition); ovrpVector2f NeckEyeDistance; if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetUserNeckEyeDistance2(&NeckEyeDistance))) { const float WorldToMetersScale = GetWorldToMetersScale(); NeckPosition.X -= NeckEyeDistance.x * WorldToMetersScale; NeckPosition.Z -= NeckEyeDistance.y * WorldToMetersScale; } return NeckPosition; } void FOculusXRHMD::SetBaseOffsetInMeters(const FVector& BaseOffset) { CheckInGameThread(); Settings->BaseOffset = BaseOffset; } FVector FOculusXRHMD::GetBaseOffsetInMeters() const { CheckInGameThread(); return Settings->BaseOffset; } bool FOculusXRHMD::ConvertPose(const ovrpPosef& InPose, FPose& OutPose) const { CheckInGameThread(); if (!NextFrameToRender.IsValid()) { return false; } return ConvertPose_Internal(InPose, OutPose, Settings.Get(), NextFrameToRender->WorldToMetersScale); } bool FOculusXRHMD::ConvertPose(const FPose& InPose, ovrpPosef& OutPose) const { CheckInGameThread(); if (!NextFrameToRender.IsValid()) { return false; } return ConvertPose_Internal(InPose, OutPose, Settings.Get(), NextFrameToRender->WorldToMetersScale); } bool FOculusXRHMD::ConvertPose_RenderThread(const ovrpPosef& InPose, FPose& OutPose) const { CheckInRenderThread(); if (!Frame_RenderThread.IsValid()) { return false; } return ConvertPose_Internal(InPose, OutPose, Settings_RenderThread.Get(), Frame_RenderThread->WorldToMetersScale); } bool FOculusXRHMD::ConvertPose_Internal(const ovrpPosef& InPose, FPose& OutPose, const FSettings* Settings, float WorldToMetersScale) { return OculusXRHMD::ConvertPose_Internal(InPose, OutPose, Settings->BaseOrientation, Settings->BaseOffset, WorldToMetersScale); } bool FOculusXRHMD::ConvertPose_Internal(const FPose& InPose, ovrpPosef& OutPose, const FSettings* Settings, float WorldToMetersScale) { return OculusXRHMD::ConvertPose_Internal(InPose, OutPose, Settings->BaseOrientation, Settings->BaseOffset, WorldToMetersScale); } FVector FOculusXRHMD::ScaleAndMovePointWithPlayer(ovrpVector3f& OculusXRHMDPoint) { CheckInGameThread(); FMatrix TranslationMatrix; TranslationMatrix.SetIdentity(); TranslationMatrix = TranslationMatrix.ConcatTranslation(LastPlayerLocation); FVector ConvertedPoint = ToFVector(OculusXRHMDPoint) * GetWorldToMetersScale(); FRotator RotateWithPlayer = LastPlayerOrientation.Rotator(); FVector TransformWithPlayer = RotateWithPlayer.RotateVector(ConvertedPoint); TransformWithPlayer = FVector(TranslationMatrix.TransformPosition(TransformWithPlayer)); if (GetXRCamera(HMDDeviceId)->GetUseImplicitHMDPosition()) { FQuat HeadOrientation = FQuat::Identity; FVector HeadPosition; GetCurrentPose(HMDDeviceId, HeadOrientation, HeadPosition); TransformWithPlayer -= RotateWithPlayer.RotateVector(HeadPosition); } return TransformWithPlayer; } ovrpVector3f FOculusXRHMD::WorldLocationToOculusPoint(const FVector& InUnrealPosition) { CheckInGameThread(); FQuat AdjustedPlayerOrientation = GetBaseOrientation().Inverse() * LastPlayerOrientation; AdjustedPlayerOrientation.Normalize(); FVector AdjustedPlayerLocation = LastPlayerLocation; if (GetXRCamera(HMDDeviceId)->GetUseImplicitHMDPosition()) { FQuat HeadOrientation = FQuat::Identity; // Unused FVector HeadPosition; GetCurrentPose(HMDDeviceId, HeadOrientation, HeadPosition); AdjustedPlayerLocation -= LastPlayerOrientation.Inverse().RotateVector(HeadPosition); } const FTransform InvWorldTransform = FTransform(AdjustedPlayerOrientation, AdjustedPlayerLocation).Inverse(); const FVector ConvertedPosition = InvWorldTransform.TransformPosition(InUnrealPosition) / GetWorldToMetersScale(); return ToOvrpVector3f(ConvertedPosition); } float FOculusXRHMD::ConvertFloat_M2U(float OculusFloat) const { CheckInGameThread(); return OculusFloat * GetWorldToMetersScale(); } FVector FOculusXRHMD::ConvertVector_M2U(ovrpVector3f OculusXRHMDPoint) const { CheckInGameThread(); return ToFVector(OculusXRHMDPoint) * GetWorldToMetersScale(); } bool FOculusXRHMD::GetUserProfile(UserProfile& OutProfile) { float UserIPD; ovrpVector2f UserNeckEyeDistance; float UserEyeHeight; if (FOculusXRHMDModule::GetPluginWrapper().GetInitialized() && OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetUserIPD2(&UserIPD)) && OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetUserNeckEyeDistance2(&UserNeckEyeDistance)) && OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetUserEyeHeight2(&UserEyeHeight))) { OutProfile.IPD = UserIPD; OutProfile.EyeDepth = UserNeckEyeDistance.x; OutProfile.EyeHeight = UserEyeHeight; return true; } return false; } float FOculusXRHMD::GetVsyncToNextVsync() const { CheckInGameThread(); return Settings->VsyncToNextVsync; } FPerformanceStats FOculusXRHMD::GetPerformanceStats() const { return PerformanceStats; } void FOculusXRHMD::GetSuggestedCpuAndGpuPerformanceLevels(EOculusXRProcessorPerformanceLevel& CpuPerfLevel, EOculusXRProcessorPerformanceLevel& GpuPerfLevel) { CheckInGameThread(); CpuPerfLevel = Settings->SuggestedCpuPerfLevel; GpuPerfLevel = Settings->SuggestedGpuPerfLevel; } void FOculusXRHMD::SetSuggestedCpuAndGpuPerformanceLevels(EOculusXRProcessorPerformanceLevel CpuPerfLevel, EOculusXRProcessorPerformanceLevel GpuPerfLevel) { CheckInGameThread(); FOculusXRHMDModule::GetPluginWrapper().SetSuggestedCpuPerformanceLevel((ovrpProcessorPerformanceLevel)CpuPerfLevel); FOculusXRHMDModule::GetPluginWrapper().SetSuggestedGpuPerformanceLevel((ovrpProcessorPerformanceLevel)GpuPerfLevel); Settings->SuggestedCpuPerfLevel = CpuPerfLevel; Settings->SuggestedGpuPerfLevel = GpuPerfLevel; } void FOculusXRHMD::SetFoveatedRenderingMethod(EOculusXRFoveatedRenderingMethod InFoveatedRenderingMethod) { #ifdef WITH_OCULUS_BRANCH Settings->FoveatedRenderingMethod = InFoveatedRenderingMethod; // Don't switch to eye tracked foveated rendering when it's not supported or permissions are denied if (InFoveatedRenderingMethod == EOculusXRFoveatedRenderingMethod::EyeTrackedFoveatedRendering && !(bEyeTrackedFoveatedRenderingSupported && CheckEyeTrackingPermission(InFoveatedRenderingMethod))) { return; } #else Settings->FoveatedRenderingMethod = EOculusXRFoveatedRenderingMethod::FixedFoveatedRendering; if (InFoveatedRenderingMethod == EOculusXRFoveatedRenderingMethod::EyeTrackedFoveatedRendering) { UE_LOG(LogHMD, Warning, TEXT("Eye Tracked Foveated Rendering is not supported on this engine version, using Fixed Foveated Rendering instead")); } #endif // WITH_OCULUS_BRANCH FoveatedRenderingMethod = Settings->FoveatedRenderingMethod; } void FOculusXRHMD::SetFoveatedRenderingLevel(EOculusXRFoveatedRenderingLevel InFoveationLevel, bool isDynamic) { FoveatedRenderingLevel = Settings->FoveatedRenderingLevel = InFoveationLevel; bDynamicFoveatedRendering = Settings->bDynamicFoveatedRendering = isDynamic; } void FOculusXRHMD::SetColorScaleAndOffset(FLinearColor ColorScale, FLinearColor ColorOffset, bool bApplyToAllLayers) { CheckInGameThread(); Settings->bApplyColorScaleAndOffsetToAllLayers = bApplyToAllLayers; Settings->ColorScale = LinearColorToOvrpVector4f(ColorScale); Settings->ColorOffset = LinearColorToOvrpVector4f(ColorOffset); } void FOculusXRHMD::SetEnvironmentDepthHandRemoval(bool RemoveHands) { FOculusXRHMDModule::GetPluginWrapper().SetEnvironmentDepthHandRemoval(RemoveHands); bEnvironmentDepthHandRemovalEnabled = RemoveHands; } void FOculusXRHMD::StartEnvironmentDepth(int CreateFlags) { #if PLATFORM_ANDROID // Check and request scene permissions (this is needed for environment depth to work) // bind delegate for handling permission request result if (!UAndroidPermissionFunctionLibrary::CheckPermission(USE_SCENE_PERMISSION_NAME)) { TArray Permissions; Permissions.Add(USE_SCENE_PERMISSION_NAME); UAndroidPermissionCallbackProxy* Proxy = UAndroidPermissionFunctionLibrary::AcquirePermissions(Permissions); static FDelegateHandle DelegateHandle; DelegateHandle = Proxy->OnPermissionsGrantedDelegate.AddLambda([this, Proxy, CreateFlags](const TArray& Permissions, const TArray& GrantResults) { int PermIndex = Permissions.Find(USE_SCENE_PERMISSION_NAME); if (PermIndex != INDEX_NONE && GrantResults[PermIndex]) { UE_LOG(LogHMD, Verbose, TEXT("%s permission granted"), *USE_SCENE_PERMISSION_NAME); StartEnvironmentDepth(CreateFlags); } else { UE_LOG(LogHMD, Log, TEXT("%s permission denied"), *USE_SCENE_PERMISSION_NAME); } Proxy->OnPermissionsGrantedDelegate.Remove(DelegateHandle); }); return; } #endif // PLATFORM_ANDROID ExecuteOnRenderThread_DoNotWait([this, CreateFlags]() { ovrpEnvironmentDepthTextureDesc DepthTextureDesc; if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().InitializeEnvironmentDepth(CreateFlags)) && OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetEnvironmentDepthTextureDesc(&DepthTextureDesc))) { TArray DepthTextures; int32 TextureCount; if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetEnvironmentDepthTextureStageCount(&TextureCount))) { // We don't really do different depth texture formats right now and it's always a // single multiview texture, so no need for a separate right eye texture for now. // We may need a separate Left/RightDepthTextures in the future. DepthTextures.SetNum(TextureCount); for (int32 TextureIndex = 0; TextureIndex < TextureCount; TextureIndex++) { if (OVRP_FAILURE(FOculusXRHMDModule::GetPluginWrapper().GetEnvironmentDepthTexture(TextureIndex, ovrpEye_Left, &DepthTextures[TextureIndex]))) { UE_LOG(LogHMD, Error, TEXT("Failed to create insight depth texture. NOTE: This causes a leak of %d other texture(s), which will go unused."), TextureIndex); return; } } uint32 SizeX = DepthTextureDesc.TextureSize.w; uint32 SizeY = DepthTextureDesc.TextureSize.h; EPixelFormat DepthFormat = CustomPresent->GetPixelFormat(DepthTextureDesc.Format); uint32 NumMips = DepthTextureDesc.MipLevels; uint32 NumSamples = DepthTextureDesc.SampleCount; uint32 NumSamplesTileMem = 1; ETextureCreateFlags DepthTexCreateFlags = TexCreate_ShaderResource | TexCreate_InputAttachmentRead; FClearValueBinding DepthTextureBinding = FClearValueBinding::DepthFar; ERHIResourceType ResourceType; if (DepthTextureDesc.Layout == ovrpLayout_Array) { ResourceType = RRT_Texture2DArray; } else { ResourceType = RRT_Texture2D; } if (CustomPresent) { if (!EnvironmentDepthSwapchain.IsEmpty()) { EnvironmentDepthSwapchain.Empty(); } EnvironmentDepthSwapchain = CustomPresent->CreateSwapChainTextures_RenderThread(SizeX, SizeY, DepthFormat, DepthTextureBinding, NumMips, NumSamples, NumSamplesTileMem, ResourceType, DepthTextures, DepthTexCreateFlags, *FString::Printf(TEXT("Oculus Environment Depth Swapchain"))); } FOculusXRHMDModule::GetPluginWrapper().SetEnvironmentDepthHandRemoval(bEnvironmentDepthHandRemovalEnabled); FOculusXRHMDModule::GetPluginWrapper().StartEnvironmentDepth(); } } }); } void FOculusXRHMD::StopEnvironmentDepth() { ExecuteOnRenderThread_DoNotWait([this]() { ExecuteOnRHIThread_DoNotWait([this]() { if (!EnvironmentDepthSwapchain.IsEmpty()) { EnvironmentDepthSwapchain.Empty(); } if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().StopEnvironmentDepth())) { FOculusXRHMDModule::GetPluginWrapper().DestroyEnvironmentDepth(); } }); }); } bool FOculusXRHMD::IsEnvironmentDepthStarted() { return !EnvironmentDepthSwapchain.IsEmpty(); } void FOculusXRHMD::EnableHardOcclusions(bool bEnable) { bHardOcclusionsEnabled = bEnable; } bool FOculusXRHMD::DoEnableStereo(bool bStereo) { CheckInGameThread(); FSceneViewport* SceneVP = FindSceneViewport(); if (!Settings->Flags.bHMDEnabled || (SceneVP && !SceneVP->IsStereoRenderingAllowed())) { bStereo = false; } if (Settings->Flags.bStereoEnabled && bStereo || !Settings->Flags.bStereoEnabled && !bStereo) { // already in the desired mode return Settings->Flags.bStereoEnabled; } TSharedPtr Window; if (SceneVP) { Window = SceneVP->FindWindow(); } if (!Window.IsValid() || !SceneVP || !SceneVP->GetViewportWidget().IsValid()) { // try again next frame if (bStereo) { Flags.bNeedEnableStereo = true; // a special case when stereo is enabled while window is not available yet: // most likely this is happening from BeginPlay. In this case, if frame exists (created in OnBeginPlay) // then we need init device and populate the initial tracking for head/hand poses. if (Frame.IsValid()) { InitDevice(); } } else { Flags.bNeedDisableStereo = true; } return Settings->Flags.bStereoEnabled; } if (OnOculusStateChange(bStereo)) { Settings->Flags.bStereoEnabled = bStereo; // Uncap fps to enable FPS higher than 62 GEngine->bForceDisableFrameRateSmoothing = bStereo; // Set MirrorWindow state on the Window Window->SetMirrorWindow(bStereo); if (bStereo) { // Start frame StartGameFrame_GameThread(); StartRenderFrame_GameThread(); // Set viewport size to Rift resolution // NOTE: this can enqueue a render frame right away as a result (calling into FOculusXRHMD::BeginRenderViewFamily) SceneVP->SetViewportSize(Settings->RenderTargetSize.X, Settings->RenderTargetSize.Y); if (Settings->Flags.bPauseRendering) { GEngine->SetMaxFPS(10); } } else { // Work around an error log that can happen when enabling stereo rendering again if (NextFrameNumber == WaitFrameNumber) { NextFrameNumber++; } if (Settings->Flags.bPauseRendering) { GEngine->SetMaxFPS(0); } // Restore viewport size to window size FVector2D size = Window->GetSizeInScreen(); SceneVP->SetViewportSize(size.X, size.Y); Window->SetViewportSizeDrivenByWindow(true); } } return Settings->Flags.bStereoEnabled; } void FOculusXRHMD::ResetControlRotation() const { // Switching back to non-stereo mode: reset player rotation and aim. // Should we go through all playercontrollers here? APlayerController* pc = GEngine->GetFirstLocalPlayerController(GWorld); if (pc) { // Reset Aim? @todo FRotator r = pc->GetControlRotation(); r.Normalize(); // Reset roll and pitch of the player r.Roll = 0; r.Pitch = 0; pc->SetControlRotation(r); } } void FOculusXRHMD::UpdateFoveationOffsets_RenderThread() { #ifdef WITH_OCULUS_BRANCH CheckInRenderThread(); SCOPED_NAMED_EVENT(UpdateFoveationOffsets_RenderThread, FColor::Red); // Don't execute anything if we're not using Eye Tracked Foveated Rendering (this already takes into account if it's supported or not) if (!Frame_RenderThread.IsValid() || Frame_RenderThread->FoveatedRenderingMethod != EOculusXRFoveatedRenderingMethod::EyeTrackedFoveatedRendering) { return; } const FXRSwapChainPtr& SwapChain = EyeLayer_RenderThread->GetSwapChain(); if (!SwapChain.IsValid()) { return; } const FRHITexture2D* const SwapChainTexture = SwapChain->GetTexture2DArray() ? SwapChain->GetTexture2DArray() : SwapChain->GetTexture2D(); if (!SwapChainTexture) { return; } const FIntPoint SwapChainDimensions = SwapChainTexture->GetSizeXY(); // Enqueue the actual update on the RHI thread, which should execute right before the EndRenderPass call ExecuteOnRHIThread_DoNotWait([this, SwapChainDimensions]() { SCOPED_NAMED_EVENT(UpdateFoveationEyeTracked_RHIThread, FColor::Red); bool bUseOffsets = false; FIntPoint Offsets[2]; // Make sure the the Foveated Rendering Method is still eye tracked at RHI thread time before getting offsets. // If the base setting was changed to fixed, even if the frame's setting is still eye tracked, we should switch to fixed. This // usually indicates that eye tracking failed on the previous frame, so we don't need to try it again. if (Frame_RHIThread.IsValid() && Frame_RHIThread->FoveatedRenderingMethod == EOculusXRFoveatedRenderingMethod::EyeTrackedFoveatedRendering && FoveatedRenderingMethod == EOculusXRFoveatedRenderingMethod::EyeTrackedFoveatedRendering) { if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().SetFoveationEyeTracked(ovrpBool_True))) { ovrpVector2f fovCenter[2]; ovrpResult Result = FOculusXRHMDModule::GetPluginWrapper().GetFoveationEyeTrackedCenter(fovCenter); if (OVRP_SUCCESS(Result)) { Offsets[0].X = fovCenter[0].x * SwapChainDimensions.X / 2; Offsets[0].Y = fovCenter[0].y * SwapChainDimensions.Y / 2; Offsets[1].X = fovCenter[1].x * SwapChainDimensions.X / 2; Offsets[1].Y = fovCenter[1].y * SwapChainDimensions.Y / 2; bUseOffsets = true; } else if (Result != ovrpFailure_DataIsInvalid) { // Fall back to dynamic FFR High if OVRPlugin call actually fails, since we're not expecting GFR to work again. // Additional rendering changes can be made by binding the changes to OculusEyeTrackingStateChanged EyeTrackedFoveatedRenderingFallback(); FOculusEventDelegates::OculusEyeTrackingStateChanged.Broadcast(false); } } } if (CustomPresent) { CustomPresent->UpdateFoveationOffsets_RHIThread(bUseOffsets, Offsets); } }); #endif // WITH_OCULUS_BRANCH } class FHardOcclusionsPS : public FGlobalShader { DECLARE_SHADER_TYPE(FHardOcclusionsPS, Global); static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment) { FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment); } static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters) { // This file is already guarded with OCULUS_HMD_SUPPORTED_PLATFORMS return true; } /** Default constructor. */ FHardOcclusionsPS() {} /** Initialization constructor. */ FHardOcclusionsPS(const ShaderMetaType::CompiledShaderInitializerType& Initializer) : FGlobalShader(Initializer) { EnvironmentDepthTexture.Bind(Initializer.ParameterMap, TEXT("EnvironmentDepthTexture")); EnvironmentDepthSampler.Bind(Initializer.ParameterMap, TEXT("EnvironmentDepthSampler")); DepthFactors.Bind(Initializer.ParameterMap, TEXT("DepthFactors")); ScreenToDepthMatrices.Bind(Initializer.ParameterMap, TEXT("ScreenToDepthMatrices")); DepthViewId.Bind(Initializer.ParameterMap, TEXT("DepthViewId")); } #if UE_VERSION_OLDER_THAN(5, 3, 0) template void SetParameters( FRHICommandListImmediate& RHICmdList, const TShaderRHIParamRef ShaderRHI, FRHISamplerState* Sampler, FRHITexture* Texture, const FVector2f& Factors, const FMatrix44f ScreenToDepth[ovrpEye_Count], const int ViewId) { SetTextureParameter(RHICmdList, ShaderRHI, EnvironmentDepthTexture, EnvironmentDepthSampler, Sampler, Texture); SetShaderValue(RHICmdList, ShaderRHI, DepthFactors, Factors); SetShaderValueArray(RHICmdList, ShaderRHI, ScreenToDepthMatrices, ScreenToDepth, ovrpEye_Count); SetShaderValue(RHICmdList, ShaderRHI, DepthViewId, ViewId); } #else void SetParameters( FRHIBatchedShaderParameters& BatchedParameters, FRHISamplerState* Sampler, FRHITexture* Texture, const FVector2f& Factors, const FMatrix44f ScreenToDepth[ovrpEye_Count], const int ViewId) { SetTextureParameter(BatchedParameters, EnvironmentDepthTexture, EnvironmentDepthSampler, Sampler, Texture); SetShaderValue(BatchedParameters, DepthFactors, Factors); SetShaderValueArray(BatchedParameters, ScreenToDepthMatrices, ScreenToDepth, ovrpEye_Count); SetShaderValue(BatchedParameters, DepthViewId, ViewId); } #endif private: LAYOUT_FIELD(FShaderResourceParameter, EnvironmentDepthTexture); LAYOUT_FIELD(FShaderResourceParameter, EnvironmentDepthSampler); LAYOUT_FIELD(FShaderParameter, DepthFactors); LAYOUT_FIELD(FShaderParameter, ScreenToDepthMatrices); LAYOUT_FIELD(FShaderParameter, DepthViewId); }; IMPLEMENT_SHADER_TYPE(, FHardOcclusionsPS, TEXT("/Plugin/OculusXR/Private/HardOcclusions.usf"), TEXT("HardOcclusionsPS"), SF_Pixel); FMatrix44f MakeProjectionMatrix(ovrpFovf cameraFovAngles) { const float tanAngleWidth = cameraFovAngles.RightTan + cameraFovAngles.LeftTan; const float tanAngleHeight = cameraFovAngles.UpTan + cameraFovAngles.DownTan; FMatrix44f Matrix = FMatrix44f::Identity; // Scale Matrix.M[0][0] = 1.0f / tanAngleWidth; Matrix.M[1][1] = 1.0f / tanAngleHeight; // Offset Matrix.M[0][3] = cameraFovAngles.LeftTan / tanAngleWidth; Matrix.M[1][3] = cameraFovAngles.DownTan / tanAngleHeight; Matrix.M[2][3] = -1.0f; return Matrix; } FMatrix44f MakeUnprojectionMatrix(ovrpFovf cameraFovAngles) { FMatrix44f Matrix = FMatrix44f::Identity; // Scale Matrix.M[0][0] = cameraFovAngles.RightTan + cameraFovAngles.LeftTan; Matrix.M[1][1] = cameraFovAngles.UpTan + cameraFovAngles.DownTan; // Offset Matrix.M[0][3] = -cameraFovAngles.LeftTan; Matrix.M[1][3] = -cameraFovAngles.DownTan; Matrix.M[2][3] = 1.0; return Matrix; } bool FOculusXRHMD::ComputeEnvironmentDepthParameters_RenderThread(FVector2f& DepthFactors, FMatrix44f ScreenToDepth[ovrpEye_Count], FMatrix44f DepthViewProj[ovrpEye_Count], int& SwapchainIndex) { float ScreenNearZ = GNearClippingPlane / Frame_RenderThread->WorldToMetersScale; ovrpFovf* ScreenFov = Frame_RenderThread->SymmetricFov; ovrpEnvironmentDepthFrameDesc DepthFrameDesc[ovrpEye_Count]; if (FOculusXRHMDModule::GetPluginWrapper().GetEnvironmentDepthFrameDesc(ovrpEye_Left, &DepthFrameDesc[0]) != ovrpSuccess || !DepthFrameDesc[0].IsValid) { return false; } if (FOculusXRHMDModule::GetPluginWrapper().GetEnvironmentDepthFrameDesc(ovrpEye_Right, &DepthFrameDesc[1]) != ovrpSuccess || !DepthFrameDesc[1].IsValid) { return false; } SwapchainIndex = DepthFrameDesc[0].SwapchainIndex; const float WorldToMetersScale = Frame_RenderThread->WorldToMetersScale; if (DepthViewProj != nullptr) { for (int i = 0; i < ovrpEye_Count; ++i) { ovrpFrustum2f DepthFrustum; DepthFrustum.Fov = DepthFrameDesc[i].Fov; DepthFrustum.zNear = DepthFrameDesc[i].NearZ * WorldToMetersScale; DepthFrustum.zFar = DepthFrameDesc[i].FarZ * WorldToMetersScale; FMatrix DepthProjectionMatrix = ToFMatrix(ovrpMatrix4f_Projection(DepthFrustum, true)); auto DepthOrientation = ToFQuat(DepthFrameDesc[i].CreatePose.Orientation); // NOTE: This matrix is the same as applied in SetupViewFrustum in SceneView.cpp auto ViewMatrix = DepthOrientation.Inverse().ToMatrix() * FMatrix(FPlane(0, 0, 1, 0), FPlane(1, 0, 0, 0), FPlane(0, 1, 0, 0), FPlane(0, 0, 0, 1)); ovrpPoseStatef EyePoseState; if (OVRP_SUCCESS(FOculusXRHMDModule::GetPluginWrapper().GetNodePoseState3(ovrpStep_Render, Frame_RenderThread->FrameNumber, (ovrpNode)i, &EyePoseState))) { auto DepthTranslation = ToFVector(DepthFrameDesc[i].CreatePose.Position) * WorldToMetersScale; auto EyePos = ToFVector(EyePoseState.Pose.Position) * WorldToMetersScale; auto Delta = EyePos - DepthTranslation; // NOTE: The view matrix here is relative to the VR camera, this is necessary to support // Large Worlds and avoid rounding errors when getting very far away from the origin ViewMatrix = ViewMatrix.ConcatTranslation(ViewMatrix.TransformPosition(Delta)); } DepthViewProj[i] = (FMatrix44f)(ViewMatrix * DepthProjectionMatrix); } } // Assume NearZ and FarZ are the same for left and right eyes float DepthNearZ = DepthFrameDesc[ovrpEye_Left].NearZ; float DepthFarZ = DepthFrameDesc[ovrpEye_Left].FarZ; float Scale; float Offset; if (DepthFarZ < DepthNearZ || (!FGenericPlatformMath::IsFinite(DepthFarZ))) { // Inf far plane: Scale = DepthNearZ; Offset = 0.0f; } else { // Finite far plane: Scale = (DepthFarZ * DepthNearZ) / (DepthFarZ - DepthNearZ); Offset = DepthNearZ / (DepthFarZ - DepthNearZ); } DepthFactors.X = -ScreenNearZ / Scale; DepthFactors.Y = Offset * ScreenNearZ / Scale + 1.0f; // The pose extrapolated to the predicted display time of the current frame FQuat ScreenOrientation = Frame_RenderThread->HeadOrientation; for (int i = 0; i < ovrpEye_Count; ++i) { // Screen To Depth represents the transformation matrix used to map normalised screen UV coordinates to // normalised environment depth texture UV coordinates. This needs to account for 2 things: // 1. The field of view of the two textures may be different, Unreal typically renders using a symmetric fov. // That is to say the FOV of the left and right eyes is the same. The environment depth on the other hand // has a different FOV for the left and right eyes. So we need to scale and offset accordingly to account // for this difference. auto T_ScreenCamera_ScreenNormCoord = MakeUnprojectionMatrix(ScreenFov[i]); auto T_DepthNormCoord_DepthCamera = MakeProjectionMatrix(DepthFrameDesc[i].Fov); // 2. The headset may have moved in between capturing the environment depth and rendering the frame. We // can only account for rotation of the headset, not translation. auto DepthOrientation = ToFQuat(DepthFrameDesc[i].CreatePose.Orientation); if (!DepthOrientation.IsNormalized()) { UE_LOG(LogHMD, Error, TEXT("DepthOrientation is not normalized %f %f %f %f"), DepthOrientation.X, DepthOrientation.Y, DepthOrientation.Z, DepthOrientation.W); DepthOrientation.Normalize(); } auto ScreenToDepthQuat = ScreenOrientation.Inverse() * DepthOrientation; FMatrix44f R_DepthCamera_ScreenCamera = FQuat4f(ScreenToDepthQuat.Y, ScreenToDepthQuat.Z, ScreenToDepthQuat.X, ScreenToDepthQuat.W).GetNormalized().ToMatrix(); ScreenToDepth[i] = T_DepthNormCoord_DepthCamera * R_DepthCamera_ScreenCamera * T_ScreenCamera_ScreenNormCoord; } return true; } #if !UE_VERSION_OLDER_THAN(5, 3, 0) BEGIN_SHADER_PARAMETER_STRUCT(FDrawRectangleParameters, ) SHADER_PARAMETER(FVector4f, PosScaleBias) SHADER_PARAMETER(FVector4f, UVScaleBias) SHADER_PARAMETER(FVector4f, InvTargetSizeAndTextureSize) END_SHADER_PARAMETER_STRUCT() #endif void FOculusXRHMD::DrawHmdViewMesh( FRHICommandList& RHICmdList, float X, float Y, float SizeX, float SizeY, float U, float V, float SizeU, float SizeV, FIntPoint TargetSize, FIntPoint TextureSize, int32 StereoView, const TShaderRef& VertexShader) { FDrawRectangleParameters Parameters; Parameters.PosScaleBias = FVector4f(SizeX, SizeY, X, Y); Parameters.UVScaleBias = FVector4f(SizeU, SizeV, U, V); Parameters.InvTargetSizeAndTextureSize = FVector4f( 1.0f / TargetSize.X, 1.0f / TargetSize.Y, 1.0f / TextureSize.X, 1.0f / TextureSize.Y); #if UE_VERSION_OLDER_THAN(5, 3, 0) SetUniformBufferParameterImmediate(RHICmdList, VertexShader.GetVertexShader(), VertexShader->GetUniformBufferParameter(), Parameters); #else FRHIBatchedShaderParameters& BatchedParameters = RHICmdList.GetScratchShaderParameters(); SetUniformBufferParameterImmediate(BatchedParameters, VertexShader->GetUniformBufferParameter(), Parameters); RHICmdList.SetBatchedShaderParameters(VertexShader.GetVertexShader(), BatchedParameters); #endif RendererModule->DrawRectangle( RHICmdList, X, Y, SizeX, SizeY, 0, 0, TextureSize.X, TextureSize.Y, TargetSize, TextureSize, VertexShader); } #if UE_VERSION_OLDER_THAN(5, 3, 0) void FOculusXRHMD::RenderHardOcclusions_RenderThread(FRHICommandListImmediate& RHICmdList, const FSceneView& InView) #else void FOculusXRHMD::RenderHardOcclusions_RenderThread(FRHICommandList& RHICmdList, const FSceneView& InView) #endif { checkSlow(RHICmdList.IsInsideRenderPass()); FVector2f DepthFactors; FMatrix44f ScreenToDepthMatrices[ovrpEye_Count]; int SwapchainIndex; if (!Frame_RenderThread.IsValid() || InView.bIsSceneCapture || InView.bIsReflectionCapture || InView.bIsPlanarReflection || !ComputeEnvironmentDepthParameters_RenderThread(DepthFactors, ScreenToDepthMatrices, nullptr, SwapchainIndex)) { return; } if (SwapchainIndex >= EnvironmentDepthSwapchain.Num()) { return; } FGraphicsPipelineStateInitializer GraphicsPSOInit; RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit); GraphicsPSOInit.BlendState = TStaticBlendState<>::GetRHI(); GraphicsPSOInit.RasterizerState = TStaticRasterizerState<>::GetRHI(); GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState<>::GetRHI(); FGlobalShaderMap* GlobalShaderMap = GetGlobalShaderMap(InView.FeatureLevel); TShaderMapRef VertexShader(GlobalShaderMap); TShaderMapRef PixelShader(GlobalShaderMap); GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GFilterVertexDeclaration.VertexDeclarationRHI; GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader(); GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader.GetPixelShader(); GraphicsPSOInit.PrimitiveType = PT_TriangleList; SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit, 0); FRHITexture* DepthTexture = EnvironmentDepthSwapchain[SwapchainIndex]; FRHISamplerState* DepthSampler = TStaticSamplerState<>::GetRHI(); FIntPoint TextureSize = DepthTexture->GetDesc().Extent; FIntRect ScreenRect = InView.UnscaledViewRect; #if UE_VERSION_OLDER_THAN(5, 3, 0) PixelShader->SetParameters(RHICmdList, PixelShader.GetPixelShader(), DepthSampler, DepthTexture, DepthFactors, ScreenToDepthMatrices, InView.StereoViewIndex); #else FRHIBatchedShaderParameters& BatchedParameters = RHICmdList.GetScratchShaderParameters(); PixelShader->SetParameters(BatchedParameters, DepthSampler, DepthTexture, DepthFactors, ScreenToDepthMatrices, InView.StereoViewIndex); RHICmdList.SetBatchedShaderParameters(PixelShader.GetPixelShader(), BatchedParameters); #endif check(Settings->CurrentShaderPlatform == InView.Family->Scene->GetShaderPlatform()); if (!IsMobilePlatform(Settings->CurrentShaderPlatform) && InView.StereoViewIndex != INDEX_NONE) { SCOPED_DRAW_EVENTF(RHICmdList, RenderHardOcclusions_RenderThread, TEXT("View %d"), InView.StereoViewIndex); int32 width = ScreenRect.Width() / 2; int32 height = ScreenRect.Height(); int32 x = InView.StereoViewIndex == EStereoscopicEye::eSSE_LEFT_EYE ? 0 : width; int32 y = 0; DrawHmdViewMesh( RHICmdList, x, y, width, height, 0, 0, TextureSize.X, TextureSize.Y, FIntPoint(ScreenRect.Width(), ScreenRect.Height()), TextureSize, InView.StereoViewIndex, VertexShader); } else { SCOPED_DRAW_EVENT(RHICmdList, RenderHardOcclusions_RenderThread); RendererModule->DrawRectangle( RHICmdList, 0, 0, ScreenRect.Width(), ScreenRect.Height(), 0, 0, TextureSize.X, TextureSize.Y, FIntPoint(ScreenRect.Width(), ScreenRect.Height()), TextureSize, VertexShader); } } FSettingsPtr FOculusXRHMD::CreateNewSettings() const { FSettingsPtr Result(MakeShareable(new FSettings())); return Result; } FGameFramePtr FOculusXRHMD::CreateNewGameFrame() const { FGameFramePtr Result(MakeShareable(new FGameFrame())); Result->FrameNumber = NextFrameNumber; Result->WindowSize = CachedWindowSize; Result->WorldToMetersScale = CachedWorldToMetersScale; Result->NearClippingPlane = GNearClippingPlane; // Allow CVars to override the app's foveated rendering settings (set -1 to restore app's setting) Result->FoveatedRenderingMethod = CVarOculusFoveatedRenderingMethod.GetValueOnAnyThread() >= 0 ? (EOculusXRFoveatedRenderingMethod)CVarOculusFoveatedRenderingMethod.GetValueOnAnyThread() : FoveatedRenderingMethod.load(); Result->FoveatedRenderingLevel = CVarOculusFoveatedRenderingLevel.GetValueOnAnyThread() >= 0 ? (EOculusXRFoveatedRenderingLevel)CVarOculusFoveatedRenderingLevel.GetValueOnAnyThread() : FoveatedRenderingLevel.load(); Result->bDynamicFoveatedRendering = CVarOculusDynamicFoveatedRendering.GetValueOnAnyThread() >= 0 ? (bool)CVarOculusDynamicFoveatedRendering.GetValueOnAnyThread() : bDynamicFoveatedRendering.load(); Result->Flags.bSplashIsShown = Splash->IsShown(); return Result; } void FOculusXRHMD::StartGameFrame_GameThread() { CheckInGameThread(); check(Settings.IsValid()); // TODO: Below check should NOT be limited to bMultiPlayer specificially even though we haven't found a non-MultiPlayer case falling into this case yet. // Remove bMultiPlayer. if (bMultiPlayer && !bShouldWait_GameThread) { return; } if (!Frame.IsValid()) { Splash->UpdateLoadingScreen_GameThread(); //the result of this is used in CreateGameFrame to know if Frame is a "real" one or a "splash" one. if (Settings->Flags.bHMDEnabled) { Frame = CreateNewGameFrame(); NextFrameToRender = Frame; UE_LOG(LogHMD, VeryVerbose, TEXT("StartGameFrame %u"), Frame->FrameNumber); if (!Splash->IsShown()) { FThreadIdleStats::FScopeIdle Scope; if (FOculusXRHMDModule::GetPluginWrapper().GetInitialized() && WaitFrameNumber != Frame->FrameNumber) { SCOPED_NAMED_EVENT(WaitFrame, FColor::Red); UE_LOG(LogHMD, Verbose, TEXT("FOculusXRHMDModule::GetPluginWrapper().WaitToBeginFrame %u"), Frame->FrameNumber); ovrpResult Result; if (OVRP_FAILURE(Result = FOculusXRHMDModule::GetPluginWrapper().WaitToBeginFrame(Frame->FrameNumber))) { UE_LOG(LogHMD, Error, TEXT("FOculusXRHMDModule::GetPluginWrapper().WaitToBeginFrame %u failed (%d)"), Frame->FrameNumber, Result); } else { WaitFrameNumber = Frame->FrameNumber; bShouldWait_GameThread = false; } } FOculusXRHMDModule::GetPluginWrapper().Update3(ovrpStep_Render, Frame->FrameNumber, 0.0); } } UpdateStereoRenderingParams(); } } void FOculusXRHMD::FinishGameFrame_GameThread() { CheckInGameThread(); if (Frame.IsValid()) { UE_LOG(LogHMD, VeryVerbose, TEXT("FinishGameFrame %u"), Frame->FrameNumber); } Frame.Reset(); } void FOculusXRHMD::StartRenderFrame_GameThread() { CheckInGameThread(); if (NextFrameToRender.IsValid() && NextFrameToRender != LastFrameToRender) { UE_LOG(LogHMD, VeryVerbose, TEXT("StartRenderFrame %u"), NextFrameToRender->FrameNumber); LastFrameToRender = NextFrameToRender; NextFrameToRender->Flags.bSplashIsShown = Splash->IsShown(); ovrpXrApi NativeXrApi; FOculusXRHMDModule::GetPluginWrapper().GetNativeXrApiType(&NativeXrApi); if ((NextFrameToRender->ShowFlags.Rendering || NativeXrApi == ovrpXrApi_OpenXR) && !NextFrameToRender->Flags.bSplashIsShown) { NextFrameNumber++; } FSettingsPtr XSettings = Settings->Clone(); FGameFramePtr XFrame = NextFrameToRender->Clone(); TArray XLayers; XLayers.Empty(LayerMap.Num()); for (auto Pair : LayerMap) { XLayers.Emplace(Pair.Value->Clone()); } XLayers.Sort(FLayerPtr_CompareId()); ExecuteOnRenderThread_DoNotWait([this, XSettings, XFrame, XLayers](FRHICommandListImmediate& RHICmdList) { if (XFrame.IsValid()) { Settings_RenderThread = XSettings; Frame_RenderThread = XFrame; int32 XLayerIndex = 0; int32 LayerIndex_RenderThread = 0; TArray ValidXLayers; while (XLayerIndex < XLayers.Num() && LayerIndex_RenderThread < Layers_RenderThread.Num()) { uint32 LayerIdA = XLayers[XLayerIndex]->GetId(); uint32 LayerIdB = Layers_RenderThread[LayerIndex_RenderThread]->GetId(); if (LayerIdA < LayerIdB) { if (XLayers[XLayerIndex]->Initialize_RenderThread(Settings_RenderThread.Get(), CustomPresent, &DeferredDeletion, RHICmdList)) { ValidXLayers.Add(XLayers[XLayerIndex]); } XLayerIndex++; } else if (LayerIdA > LayerIdB) { DeferredDeletion.AddLayerToDeferredDeletionQueue(Layers_RenderThread[LayerIndex_RenderThread++]); } else { if (XLayers[XLayerIndex]->Initialize_RenderThread(Settings_RenderThread.Get(), CustomPresent, &DeferredDeletion, RHICmdList, Layers_RenderThread[LayerIndex_RenderThread].Get())) { LayerIndex_RenderThread++; ValidXLayers.Add(XLayers[XLayerIndex]); } XLayerIndex++; } } while (XLayerIndex < XLayers.Num()) { if (XLayers[XLayerIndex]->Initialize_RenderThread(Settings_RenderThread.Get(), CustomPresent, &DeferredDeletion, RHICmdList)) { ValidXLayers.Add(XLayers[XLayerIndex]); } XLayerIndex++; } while (LayerIndex_RenderThread < Layers_RenderThread.Num()) { DeferredDeletion.AddLayerToDeferredDeletionQueue(Layers_RenderThread[LayerIndex_RenderThread++]); } Layers_RenderThread = ValidXLayers; DeferredDeletion.HandleLayerDeferredDeletionQueue_RenderThread(); } }); } } void FOculusXRHMD::FinishRenderFrame_RenderThread(FRDGBuilder& GraphBuilder) { CheckInRenderThread(); // TODO: Below check should NOT be limited to bMultiPlayer specificially even though we haven't found a non-MultiPlayer case falling into this case yet. // Remove bMultiPlayer. if (bMultiPlayer && !bIsRendering_RenderThread) { //we must keep Frame_RenderThread alive if we haven't started to use it to render yet! return; } if (Frame_RenderThread.IsValid()) { UE_LOG(LogHMD, VeryVerbose, TEXT("FinishRenderFrame %u"), Frame_RenderThread->FrameNumber); AddPass(GraphBuilder, RDG_EVENT_NAME("FinishRenderFrame"), [this](FRHICommandListImmediate& RHICmdList) { if (Frame_RenderThread->ShowFlags.Rendering) { for (int32 LayerIndex = 0; LayerIndex < Layers_RenderThread.Num(); LayerIndex++) { Layers_RenderThread[LayerIndex]->UpdateTexture_RenderThread(Settings_RenderThread.Get(), CustomPresent, RHICmdList); Layers_RenderThread[LayerIndex]->UpdatePassthrough_RenderThread(CustomPresent, RHICmdList, Frame_RenderThread.Get()); } } Frame_RenderThread.Reset(); }); } bIsRendering_RenderThread = false; } void FOculusXRHMD::StartRHIFrame_RenderThread() { CheckInRenderThread(); if (Frame_RenderThread.IsValid()) { UE_LOG(LogHMD, VeryVerbose, TEXT("StartRHIFrame %u"), Frame_RenderThread->FrameNumber); FSettingsPtr XSettings = Settings_RenderThread->Clone(); FGameFramePtr XFrame = Frame_RenderThread->Clone(); TArray XLayers = Layers_RenderThread; for (int32 XLayerIndex = 0; XLayerIndex < XLayers.Num(); XLayerIndex++) { XLayers[XLayerIndex] = XLayers[XLayerIndex]->Clone(); } ExecuteOnRHIThread_DoNotWait([this, XSettings, XFrame, XLayers]() { if (XFrame.IsValid()) { Settings_RHIThread = XSettings; Frame_RHIThread = XFrame; Layers_RHIThread = XLayers; ovrpXrApi NativeXrApi; FOculusXRHMDModule::GetPluginWrapper().GetNativeXrApiType(&NativeXrApi); if ((Frame_RHIThread->ShowFlags.Rendering || NativeXrApi == ovrpXrApi_OpenXR) && !Frame_RHIThread->Flags.bSplashIsShown) { SCOPED_NAMED_EVENT(BeginFrame, FColor::Red); UE_LOG(LogHMD, Verbose, TEXT("FOculusXRHMDModule::GetPluginWrapper().BeginFrame4 %u"), Frame_RHIThread->FrameNumber); ovrpResult Result; if (OVRP_FAILURE(Result = FOculusXRHMDModule::GetPluginWrapper().BeginFrame4(Frame_RHIThread->FrameNumber, CustomPresent->GetOvrpCommandQueue()))) { UE_LOG(LogHMD, Error, TEXT("FOculusXRHMDModule::GetPluginWrapper().BeginFrame4 %u failed (%d)"), Frame_RHIThread->FrameNumber, Result); Frame_RHIThread->ShowFlags.Rendering = false; } else { #if PLATFORM_ANDROID FOculusXRHMDModule::GetPluginWrapper().SetTiledMultiResLevel((ovrpTiledMultiResLevel)Frame_RHIThread->FoveatedRenderingLevel); FOculusXRHMDModule::GetPluginWrapper().SetTiledMultiResDynamic(Frame_RHIThread->bDynamicFoveatedRendering ? ovrpBool_True : ovrpBool_False); #ifdef WITH_OCULUS_BRANCH // If we're using eye tracked foveated rendering, set that at the end of the render pass instead (through UpdateFoveationOffsets_RenderThread) if (Frame_RHIThread->FoveatedRenderingMethod != EOculusXRFoveatedRenderingMethod::EyeTrackedFoveatedRendering) { FOculusXRHMDModule::GetPluginWrapper().SetFoveationEyeTracked(ovrpBool_False); // Need to also not use offsets when turning off eye tracked foveated rendering if (CustomPresent) { CustomPresent->UpdateFoveationOffsets_RHIThread(false, nullptr); } } #endif // WITH_OCULUS_BRANCH #endif // PLATFORM_ANDROID } } } }); // TODO: Add a hook to resolve discarded frames before we start a new frame. // TODO: Below check should NOT be limited to bMultiPlayer specificially even though we haven't found a non-MultiPlayer case falling into this case yet. // Remove bMultiPlayer. UE_CLOG(bMultiPlayer && bIsRendering_RenderThread, LogHMD, Verbose, TEXT("Discarded previous frame and started rendering a new frame.")); bIsRendering_RenderThread = true; } } void FOculusXRHMD::FinishRHIFrame_RHIThread() { CheckInRHIThread(); if (Frame_RHIThread.IsValid()) { UE_LOG(LogHMD, VeryVerbose, TEXT("FinishRHIFrame %u"), Frame_RHIThread->FrameNumber); ovrpXrApi NativeXrApi; FOculusXRHMDModule::GetPluginWrapper().GetNativeXrApiType(&NativeXrApi); if ((Frame_RHIThread->ShowFlags.Rendering || NativeXrApi == ovrpXrApi_OpenXR) && !Frame_RHIThread->Flags.bSplashIsShown) { SCOPED_NAMED_EVENT(EndFrame, FColor::Red); TArray Layers = Layers_RHIThread; Layers.Sort(FLayerPtr_CompareTotal()); TArray LayerSubmitPtr; int32 LayerNum = Layers.Num(); LayerSubmitPtr.SetNum(LayerNum); int32 FinalLayerNumber = 0; for (int32 LayerIndex = 0; LayerIndex < LayerNum; LayerIndex++) { if (Layers[LayerIndex]->IsVisible()) { LayerSubmitPtr[FinalLayerNumber++] = Layers[LayerIndex]->UpdateLayer_RHIThread(Settings_RHIThread.Get(), Frame_RHIThread.Get(), LayerIndex); } } UE_LOG(LogHMD, Verbose, TEXT("FOculusXRHMDModule::GetPluginWrapper().EndFrame4 %u"), Frame_RHIThread->FrameNumber); FOculusXRHMDModule::GetPluginWrapper().SetEyeFovPremultipliedAlphaMode(false); ovrpResult Result; if (OVRP_FAILURE(Result = FOculusXRHMDModule::GetPluginWrapper().EndFrame4(Frame_RHIThread->FrameNumber, LayerSubmitPtr.GetData(), FinalLayerNumber, CustomPresent->GetOvrpCommandQueue()))) { UE_LOG(LogHMD, Error, TEXT("FOculusXRHMDModule::GetPluginWrapper().EndFrame4 %u failed (%d)"), Frame_RHIThread->FrameNumber, Result); } else { for (int32 LayerIndex = 0; LayerIndex < Layers.Num(); LayerIndex++) { Layers[LayerIndex]->IncrementSwapChainIndex_RHIThread(CustomPresent); } } } } Frame_RHIThread.Reset(); } void FOculusXRHMD::AddEventPollingDelegate(const FOculusXRHMDEventPollingDelegate& NewDelegate) { EventPollingDelegates.Add(NewDelegate); } /// @cond DOXYGEN_WARNINGS #define BOOLEAN_COMMAND_HANDLER_BODY(ConsoleName, FieldExpr) \ do \ { \ if (Args.Num()) \ { \ if (Args[0].Equals(TEXT("toggle"), ESearchCase::IgnoreCase)) \ { \ (FieldExpr) = !(FieldExpr); \ } \ else \ { \ (FieldExpr) = FCString::ToBool(*Args[0]); \ } \ } \ Ar.Logf(ConsoleName TEXT(" = %s"), (FieldExpr) ? TEXT("On") : TEXT("Off")); \ } \ while (false) void FOculusXRHMD::UpdateOnRenderThreadCommandHandler(const TArray& Args, UWorld* World, FOutputDevice& Ar) { CheckInGameThread(); BOOLEAN_COMMAND_HANDLER_BODY(TEXT("vr.oculus.bUpdateOnRenderThread"), Settings->Flags.bUpdateOnRT); } void FOculusXRHMD::PixelDensityMinCommandHandler(const TArray& Args, UWorld*, FOutputDevice& Ar) { CheckInGameThread(); if (Args.Num()) { Settings->SetPixelDensityMin(FCString::Atof(*Args[0])); } Ar.Logf(TEXT("vr.oculus.PixelDensity.min = \"%1.2f\""), Settings->PixelDensityMin); } void FOculusXRHMD::PixelDensityMaxCommandHandler(const TArray& Args, UWorld*, FOutputDevice& Ar) { CheckInGameThread(); if (Args.Num()) { Settings->SetPixelDensityMax(FCString::Atof(*Args[0])); } Ar.Logf(TEXT("vr.oculus.PixelDensity.max = \"%1.2f\""), Settings->PixelDensityMax); } void FOculusXRHMD::HQBufferCommandHandler(const TArray& Args, UWorld*, FOutputDevice& Ar) { CheckInGameThread(); BOOLEAN_COMMAND_HANDLER_BODY(TEXT("vr.oculus.bHQBuffer"), Settings->Flags.bHQBuffer); } void FOculusXRHMD::HQDistortionCommandHandler(const TArray& Args, UWorld*, FOutputDevice& Ar) { CheckInGameThread(); BOOLEAN_COMMAND_HANDLER_BODY(TEXT("vr.oculus.bHQDistortion"), Settings->Flags.bHQDistortion); } void FOculusXRHMD::ShowGlobalMenuCommandHandler(const TArray& Args, UWorld* World, FOutputDevice& Ar) { CheckInGameThread(); if (OVRP_FAILURE(FOculusXRHMDModule::GetPluginWrapper().ShowSystemUI2(ovrpUI::ovrpUI_GlobalMenu))) { Ar.Logf(TEXT("Could not show platform menu")); } } void FOculusXRHMD::ShowQuitMenuCommandHandler(const TArray& Args, UWorld* World, FOutputDevice& Ar) { CheckInGameThread(); if (OVRP_FAILURE(FOculusXRHMDModule::GetPluginWrapper().ShowSystemUI2(ovrpUI::ovrpUI_ConfirmQuit))) { Ar.Logf(TEXT("Could not show platform menu")); } } #if !UE_BUILD_SHIPPING void FOculusXRHMD::StatsCommandHandler(const TArray& Args, UWorld*, FOutputDevice& Ar) { CheckInGameThread(); BOOLEAN_COMMAND_HANDLER_BODY(TEXT("vr.oculus.Debug.bShowStats"), Settings->Flags.bShowStats); } void FOculusXRHMD::ShowSettingsCommandHandler(const TArray& Args, UWorld* World, FOutputDevice& Ar) { Ar.Logf(TEXT("stereo ipd=%.4f\n nearPlane=%.4f"), GetInterpupillaryDistance(), GNearClippingPlane); } void FOculusXRHMD::IPDCommandHandler(const TArray& Args, UWorld* World, FOutputDevice& Ar) { if (Args.Num() > 0) { SetInterpupillaryDistance(FCString::Atof(*Args[0])); } Ar.Logf(TEXT("vr.oculus.Debug.IPD = %f"), GetInterpupillaryDistance()); } #endif // !UE_BUILD_SHIPPING void FOculusXRHMD::LoadFromSettings() { UOculusXRHMDRuntimeSettings* HMDSettings = GetMutableDefault(); check(HMDSettings); Settings->Flags.bSupportsDash = HMDSettings->bSupportsDash; #if PLATFORM_ANDROID Settings->Flags.bCompositeDepth = HMDSettings->bCompositeDepthMobile; #else Settings->Flags.bCompositeDepth = HMDSettings->bCompositesDepth; #endif Settings->Flags.bHQDistortion = HMDSettings->bHQDistortion; Settings->Flags.bInsightPassthroughEnabled = HMDSettings->bInsightPassthroughEnabled; #ifdef WITH_OCULUS_BRANCH Settings->Flags.bPixelDensityAdaptive = HMDSettings->bDynamicResolution; #endif Settings->SuggestedCpuPerfLevel = HMDSettings->SuggestedCpuPerfLevel; Settings->SuggestedGpuPerfLevel = HMDSettings->SuggestedGpuPerfLevel; Settings->FoveatedRenderingMethod = HMDSettings->FoveatedRenderingMethod; Settings->FoveatedRenderingLevel = HMDSettings->FoveatedRenderingLevel; Settings->bDynamicFoveatedRendering = HMDSettings->bDynamicFoveatedRendering; Settings->PixelDensityMin = HMDSettings->PixelDensityMin; Settings->PixelDensityMax = HMDSettings->PixelDensityMax; Settings->ColorSpace = HMDSettings->ColorSpace; Settings->ControllerPoseAlignment = HMDSettings->ControllerPoseAlignment; Settings->bLateLatching = HMDSettings->bLateLatching; Settings->XrApi = HMDSettings->XrApi; Settings->bSupportExperimentalFeatures = HMDSettings->bSupportExperimentalFeatures; Settings->bSupportEyeTrackedFoveatedRendering = HMDSettings->bSupportEyeTrackedFoveatedRendering; Settings->SystemSplashBackground = HMDSettings->SystemSplashBackground; Settings->FaceTrackingDataSource.Empty(ovrpFaceConstants_FaceTrackingDataSourcesCount); Settings->FaceTrackingDataSource.Append(HMDSettings->FaceTrackingDataSource); } void FOculusXRHMD::CheckMultiPlayer() { #if WITH_EDITOR && PLATFORM_WINDOWS ULevelEditorPlaySettings* PlayInSettings = GetMutableDefault(); check(PlayInSettings); int PlayNumberOfClients = 0; PlayInSettings->GetPlayNumberOfClients(PlayNumberOfClients); bMultiPlayer = PlayNumberOfClients > 1; #endif } /// @endcond } // namespace OculusXRHMD #endif //OCULUS_HMD_SUPPORTED_PLATFORMS