/** * @file * * @author Mamadou Babaei * * @section LICENSE * * (The MIT License) * * Copyright (c) 2020 - 2025 SenseGlove * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. * * @section DESCRIPTION * * */ #include "SGTracking/SGHMDTracker.h" #if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 5 #include "Async/Async.h" #include "Async/Future.h" #include "Async/TaskGraphInterfaces.h" #endif /* ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 5 */ #include "Engine/Engine.h" #include "IHeadMountedDisplay.h" #include "IXRTrackingSystem.h" #if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 5 #include "Misc/Optional.h" #include "Misc/Timespan.h" #endif /* ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 5 */ #include "SGSettings/SGSettings.h" struct FSGHMDTracker::FStaticImpl { /************************ * Static methods ************************/ static void GetHMDMonitorInfo( IHeadMountedDisplay* Device, IHeadMountedDisplay::MonitorInfo& OutMonitorInfo); /************************ * Constructor / Destructor ************************/ explicit FStaticImpl() = default; virtual ~FStaticImpl() = delete; }; void FSGHMDTracker::FStaticImpl::GetHMDMonitorInfo( IHeadMountedDisplay* Device, IHeadMountedDisplay::MonitorInfo& OutMonitorInfo) { #if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION <= 5 Device->GetHMDMonitorInfo(OutMonitorInfo); return; #endif /* ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION <= 5 */ #if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 6 TPromise> Promise; const TFuture> Future{Promise.GetFuture()}; AsyncTask(ENamedThreads::ActualRenderingThread, [ Device, Promise = MoveTemp(Promise)]() mutable { IHeadMountedDisplay::MonitorInfo MonitorInfo; Device->GetHMDMonitorInfo(MonitorInfo); Promise.SetValue(MoveTemp(MonitorInfo)); }); if (Future.WaitFor(FTimespan::FromMilliseconds(100))) { TOptional Result{Future.Get()}; if (Result.IsSet()) { OutMonitorInfo = Result.GetValue(); } } #endif /* ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 6 */ } FName FSGHMDTracker::GetDeviceName() { if (GEngine && GEngine->XRSystem.IsValid() && GEngine->XRSystem->IsHeadTrackingAllowed()) { const IHeadMountedDisplay* Device{GEngine->XRSystem->GetHMDDevice()}; if (Device) { const FName DeviceName{Device->GetHMDName()}; return DeviceName; } } return NAME_None; } bool FSGHMDTracker::IsMetaQuest2() { static const FName MetaQuest2DeviceIdentifier{TEXT("Meta Quest 2")}; static const FName OculusQuest2DeviceIdentifier{TEXT("Oculus Quest2")}; static const FString Quest2DeviceIdentifier1{TEXT("Quest 2")}; static const FString Quest2DeviceIdentifier2{TEXT("Quest2")}; const FName DeviceName{GetDeviceName()}; bool bMatched = DeviceName == MetaQuest2DeviceIdentifier || DeviceName == OculusQuest2DeviceIdentifier; if (!bMatched) { const FString DeviceNameString{GetDeviceName().ToString()}; bMatched = DeviceNameString.Contains(Quest2DeviceIdentifier1) || DeviceNameString.Contains(Quest2DeviceIdentifier2); } return bMatched; } bool FSGHMDTracker::IsMetaQuestPro() { static const FName MetaQuestProDeviceIdentifier{TEXT("Meta Quest Pro")}; static const FName OculusQuestProDeviceIdentifier{TEXT("Oculus QuestPro")}; static const FString QuestProDeviceIdentifier1{TEXT("Quest Pro")}; static const FString QuestProDeviceIdentifier2{TEXT("QuestPro")}; const FName DeviceName{GetDeviceName()}; bool bMatched = DeviceName == MetaQuestProDeviceIdentifier || DeviceName == OculusQuestProDeviceIdentifier; if (!bMatched) { const FString DeviceNameString{GetDeviceName().ToString()}; bMatched = DeviceNameString.Contains(QuestProDeviceIdentifier1) || DeviceNameString.Contains(QuestProDeviceIdentifier2); } return bMatched; } bool FSGHMDTracker::IsMetaQuest3() { static const FName MetaQuest3DeviceIdentifier{TEXT("Meta Quest 3")}; static const FName OculusQuest3DeviceIdentifier{TEXT("Oculus Quest3")}; static const FString Quest3DeviceIdentifier1{TEXT("Quest 3")}; static const FString Quest3DeviceIdentifier2{TEXT("Quest3")}; const FName DeviceName{GetDeviceName()}; bool bMatched = DeviceName == MetaQuest3DeviceIdentifier || DeviceName == OculusQuest3DeviceIdentifier; if (!bMatched) { const FString DeviceNameString{GetDeviceName().ToString()}; bMatched = DeviceNameString.Contains(Quest3DeviceIdentifier1) || DeviceNameString.Contains(Quest3DeviceIdentifier2); } return bMatched; } bool FSGHMDTracker::IsHtcVivePro() { #if PLATFORM_ANDROID return false; #else /* PLATFORM_ANDROID */ static const FName DeviceIdentifier{TEXT("Vive OpenXR: Vive SRanipal")}; static const FString ViveDeviceIdentifier{TEXT("Vive")}; static const FString SRanipalDeviceIdentifier{TEXT("SRanipal")}; static const FName DeviceIdentifier2{TEXT("SteamVR/OpenXR : lighthouse")}; static const FString LighthouseIdentifier{TEXT("lighthouse")}; const FName DeviceName{GetDeviceName()}; bool bNameMatches = DeviceName == DeviceIdentifier || DeviceName == DeviceIdentifier2; if (!bNameMatches) { const FString DeviceNameString{GetDeviceName().ToString()}; bNameMatches = DeviceNameString.Contains(ViveDeviceIdentifier) || DeviceNameString.Contains(SRanipalDeviceIdentifier) || DeviceNameString.Contains(LighthouseIdentifier); } bool bResolutionMatches = false; if (bNameMatches) { if (GEngine && GEngine->XRSystem.IsValid() && GEngine->XRSystem->IsHeadTrackingAllowed()) { IHeadMountedDisplay* Device{GEngine->XRSystem->GetHMDDevice()}; if (Device) { IHeadMountedDisplay::MonitorInfo HmdMonitorInfo; FStaticImpl::GetHMDMonitorInfo(Device, HmdMonitorInfo); /// NOTE // VIVE Pro: // - Official Spec: 1440 x 1600 pixels per eye (2880 x 1600 pixels combined) // - Unreal OpenXR Resolution (PCVR): 4936 x 2740 // VIVE Focus 3: // - Official Spec: 2448 x 2448 pixels per eye (4896 x 2448 pixels combined) // - Unreal OpenXR Resolution (PCVR): 5016 x 2508 // - Unreal OpenXR Resolution (Standalone): ? // VIVE XR Elite // - Official Spec: 1920 x 1920 pixels per eye (3840 x 1920 pixels combined) // - Unreal OpenXR Resolution (PCVR): 5016 x 2508 // - Unreal OpenXR Resolution (Standalone): ? // VIVE Focus Vision // - Official Spec: 2448 x 2448 pixels per eye (4896 x 2448 pixels combined) // - Unreal OpenXR Resolution (PCVR): 5016 x 2508 // - Unreal OpenXR Resolution (Standalone): 1720 x 1720 if (HmdMonitorInfo.ResolutionX < 5000 && HmdMonitorInfo.ResolutionY > 2700) { bResolutionMatches = true; } } } } const bool bMatched = bNameMatches && bResolutionMatches; return bMatched; #endif /* PLATFORM_ANDROID */ } bool FSGHMDTracker::IsHtcViveFocus3() { #if PLATFORM_ANDROID static const FName DeviceIdentifier{TEXT("WAVE:SUE")}; const FName DeviceName{GetDeviceName()}; const bool bNameMatches = DeviceName == DeviceIdentifier; #else /* PLATFORM_ANDROID */ static const FName DeviceIdentifier{TEXT("Vive OpenXR: Vive SRanipal")}; static const FString ViveDeviceIdentifier{TEXT("Vive")}; static const FString SRanipalDeviceIdentifier{TEXT("SRanipal")}; const FName DeviceName{GetDeviceName()}; bool bNameMatches = DeviceName == DeviceIdentifier; if (!bNameMatches) { const FString DeviceNameString{GetDeviceName().ToString()}; bNameMatches = DeviceNameString.Contains(ViveDeviceIdentifier) || DeviceNameString.Contains(SRanipalDeviceIdentifier); } #endif /* PLATFORM_ANDROID */ /// NOTE /// For the time being since we cannot distinguish between VIVE Focus Vision, VIVE XR Elite, and VIVE Focus 3 using /// resolution checks, we skip the resolution checks altogether and assume it's not a VIVE Pro, as VIVE Pro won't /// come with Android support. #if PLATFORM_ANDROID bool bResolutionMatches = true; #else /* PLATFORM_ANDROID */ bool bResolutionMatches = false; if (bNameMatches) { if (GEngine && GEngine->XRSystem.IsValid() && GEngine->XRSystem->IsHeadTrackingAllowed()) { IHeadMountedDisplay* Device{GEngine->XRSystem->GetHMDDevice()}; if (Device) { IHeadMountedDisplay::MonitorInfo HmdMonitorInfo; FStaticImpl::GetHMDMonitorInfo(Device, HmdMonitorInfo); /// NOTE // VIVE Pro: // - Official Spec: 1440 x 1600 pixels per eye (2880 x 1600 pixels combined) // - Unreal OpenXR Resolution (PCVR): 4936 x 2740 // VIVE Focus 3: // - Official Spec: 2448 x 2448 pixels per eye (4896 x 2448 pixels combined) // - Unreal OpenXR Resolution (PCVR): 5016 x 2508 // - Unreal OpenXR Resolution (Standalone): ? // VIVE XR Elite // - Official Spec: 1920 x 1920 pixels per eye (3840 x 1920 pixels combined) // - Unreal OpenXR Resolution (PCVR): 5016 x 2508 // - Unreal OpenXR Resolution (Standalone): ? // VIVE Focus Vision // - Official Spec: 2448 x 2448 pixels per eye (4896 x 2448 pixels combined) // - Unreal OpenXR Resolution (PCVR): 5016 x 2508 // - Unreal OpenXR Resolution (Standalone): 1720 x 1720 if (HmdMonitorInfo.ResolutionX > 5000 && (HmdMonitorInfo.ResolutionY > 2500 && HmdMonitorInfo.ResolutionY < 2700)) { bResolutionMatches = true; } } } } #endif /* PLATFORM_ANDROID */ const bool bMatched = bNameMatches && bResolutionMatches; return bMatched; } bool FSGHMDTracker::IsHtcViveXRElite() { #if PLATFORM_ANDROID static const FName DeviceIdentifier{TEXT("WAVE:SUE")}; const FName DeviceName{GetDeviceName()}; const bool bNameMatches = DeviceName == DeviceIdentifier; #else /* PLATFORM_ANDROID */ static const FName DeviceIdentifier{TEXT("Vive OpenXR: Vive SRanipal")}; static const FString ViveDeviceIdentifier{TEXT("Vive")}; static const FString SRanipalDeviceIdentifier{TEXT("SRanipal")}; const FName DeviceName{GetDeviceName()}; bool bNameMatches = DeviceName == DeviceIdentifier; if (!bNameMatches) { const FString DeviceNameString{GetDeviceName().ToString()}; bNameMatches = DeviceNameString.Contains(ViveDeviceIdentifier) || DeviceNameString.Contains(SRanipalDeviceIdentifier); } #endif /* PLATFORM_ANDROID */ /// NOTE /// For the time being since we cannot distinguish between VIVE Focus Vision, VIVE XR Elite, and VIVE Focus 3 using /// resolution checks, we skip the resolution checks altogether and assume it's not a VIVE Pro, as VIVE Pro won't /// come with Android support. #if PLATFORM_ANDROID bool bResolutionMatches = true; #else /* PLATFORM_ANDROID */ bool bResolutionMatches = false; if (bNameMatches) { if (GEngine && GEngine->XRSystem.IsValid() && GEngine->XRSystem->IsHeadTrackingAllowed()) { IHeadMountedDisplay* Device{GEngine->XRSystem->GetHMDDevice()}; if (Device) { IHeadMountedDisplay::MonitorInfo HmdMonitorInfo; FStaticImpl::GetHMDMonitorInfo(Device, HmdMonitorInfo); /// NOTE // VIVE Pro: // - Official Spec: 1440 x 1600 pixels per eye (2880 x 1600 pixels combined) // - Unreal OpenXR Resolution (PCVR): 4936 x 2740 // VIVE Focus 3: // - Official Spec: 2448 x 2448 pixels per eye (4896 x 2448 pixels combined) // - Unreal OpenXR Resolution (PCVR): 5016 x 2508 // - Unreal OpenXR Resolution (Standalone): ? // VIVE XR Elite // - Official Spec: 1920 x 1920 pixels per eye (3840 x 1920 pixels combined) // - Unreal OpenXR Resolution (PCVR): 5016 x 2508 // - Unreal OpenXR Resolution (Standalone): ? // VIVE Focus Vision // - Official Spec: 2448 x 2448 pixels per eye (4896 x 2448 pixels combined) // - Unreal OpenXR Resolution (PCVR): 5016 x 2508 // - Unreal OpenXR Resolution (Standalone): 1720 x 1720 if (HmdMonitorInfo.ResolutionX > 5000 && (HmdMonitorInfo.ResolutionY > 2500 && HmdMonitorInfo.ResolutionY < 2700)) { bResolutionMatches = true; } } } } #endif /* PLATFORM_ANDROID */ const bool bMatched = bNameMatches && bResolutionMatches; return bMatched; } bool FSGHMDTracker::IsHtcViveFocusVision() { #if PLATFORM_ANDROID static const FName DeviceIdentifier{TEXT("WAVE:SUE")}; const FName DeviceName{GetDeviceName()}; const bool bNameMatches = DeviceName == DeviceIdentifier; #else /* PLATFORM_ANDROID */ static const FName DeviceIdentifier{TEXT("Vive OpenXR: Vive SRanipal")}; static const FString ViveDeviceIdentifier{TEXT("Vive")}; static const FString SRanipalDeviceIdentifier{TEXT("SRanipal")}; const FName DeviceName{GetDeviceName()}; bool bNameMatches = DeviceName == DeviceIdentifier; if (!bNameMatches) { const FString DeviceNameString{GetDeviceName().ToString()}; bNameMatches = DeviceNameString.Contains(ViveDeviceIdentifier) || DeviceNameString.Contains(SRanipalDeviceIdentifier); } #endif /* PLATFORM_ANDROID */ /// NOTE /// For the time being since we cannot distinguish between VIVE Focus Vision, VIVE XR Elite, and VIVE Focus 3 using /// resolution checks, we skip the resolution checks altogether and assume it's not a VIVE Pro, as VIVE Pro won't /// come with Android support. #if PLATFORM_ANDROID bool bResolutionMatches = true; #else /* PLATFORM_ANDROID */ bool bResolutionMatches = false; if (bNameMatches) { if (GEngine && GEngine->XRSystem.IsValid() && GEngine->XRSystem->IsHeadTrackingAllowed()) { IHeadMountedDisplay* Device{GEngine->XRSystem->GetHMDDevice()}; if (Device) { IHeadMountedDisplay::MonitorInfo HmdMonitorInfo; FStaticImpl::GetHMDMonitorInfo(Device, HmdMonitorInfo); /// NOTE // VIVE Pro: // - Official Spec: 1440 x 1600 pixels per eye (2880 x 1600 pixels combined) // - Unreal OpenXR Resolution (PCVR): 4936 x 2740 // VIVE Focus 3: // - Official Spec: 2448 x 2448 pixels per eye (4896 x 2448 pixels combined) // - Unreal OpenXR Resolution (PCVR): 5016 x 2508 // - Unreal OpenXR Resolution (Standalone): ? // VIVE XR Elite // - Official Spec: 1920 x 1920 pixels per eye (3840 x 1920 pixels combined) // - Unreal OpenXR Resolution (PCVR): 5016 x 2508 // - Unreal OpenXR Resolution (Standalone): ? // VIVE Focus Vision // - Official Spec: 2448 x 2448 pixels per eye (4896 x 2448 pixels combined) // - Unreal OpenXR Resolution (PCVR): 5016 x 2508 // - Unreal OpenXR Resolution (Standalone): 1720 x 1720 if (HmdMonitorInfo.ResolutionX > 5000 && (HmdMonitorInfo.ResolutionY > 2500 && HmdMonitorInfo.ResolutionY < 2700)) { bResolutionMatches = true; } } } } #endif /* PLATFORM_ANDROID */ const bool bMatched = bNameMatches && bResolutionMatches; return bMatched; } ESGHeadMountedDisplayDevice FSGHMDTracker::GetDeviceType() { if (IsMetaQuest2()) { return ESGHeadMountedDisplayDevice::MetaQuest2; } if (IsMetaQuestPro()) { return ESGHeadMountedDisplayDevice::MetaQuestPro; } if (IsMetaQuest3()) { return ESGHeadMountedDisplayDevice::MetaQuest3; } if (IsHtcVivePro()) { return ESGHeadMountedDisplayDevice::HtcVivePro; } const USGSettings* Settings{USGSettings::GetInstance()}; ensureAlwaysMsgf(Settings, TEXT("The settings subsystem has not been initialized!")); if (Settings->GetTrackingSettings().HMDTrackingSettings.ViveHMDDetectionPriority == ESGViveHMDDetectionPriority::FocusVision_XRElite_Focus3) { if (IsHtcViveFocusVision()) { return ESGHeadMountedDisplayDevice::HtcViveFocusVision; } if (IsHtcViveXRElite()) { return ESGHeadMountedDisplayDevice::HtcViveXRElite; } if (IsHtcViveFocus3()) { return ESGHeadMountedDisplayDevice::HtcViveFocus3; } } if (Settings->GetTrackingSettings().HMDTrackingSettings.ViveHMDDetectionPriority == ESGViveHMDDetectionPriority::FocusVision_Focus3_XRElite) { if (IsHtcViveFocusVision()) { return ESGHeadMountedDisplayDevice::HtcViveFocusVision; } if (IsHtcViveFocus3()) { return ESGHeadMountedDisplayDevice::HtcViveFocus3; } if (IsHtcViveXRElite()) { return ESGHeadMountedDisplayDevice::HtcViveXRElite; } } if (Settings->GetTrackingSettings().HMDTrackingSettings.ViveHMDDetectionPriority == ESGViveHMDDetectionPriority::XRElite_FocusVision_Focus3) { if (IsHtcViveXRElite()) { return ESGHeadMountedDisplayDevice::HtcViveXRElite; } if (IsHtcViveFocusVision()) { return ESGHeadMountedDisplayDevice::HtcViveFocusVision; } if (IsHtcViveFocus3()) { return ESGHeadMountedDisplayDevice::HtcViveFocus3; } } if (Settings->GetTrackingSettings().HMDTrackingSettings.ViveHMDDetectionPriority == ESGViveHMDDetectionPriority::XRElite_Focus3_FocusVision) { if (IsHtcViveXRElite()) { return ESGHeadMountedDisplayDevice::HtcViveXRElite; } if (IsHtcViveFocus3()) { return ESGHeadMountedDisplayDevice::HtcViveFocus3; } if (IsHtcViveFocusVision()) { return ESGHeadMountedDisplayDevice::HtcViveFocusVision; } } if (Settings->GetTrackingSettings().HMDTrackingSettings.ViveHMDDetectionPriority == ESGViveHMDDetectionPriority::Focus3_FocusVision_XRElite) { if (IsHtcViveFocus3()) { return ESGHeadMountedDisplayDevice::HtcViveFocus3; } if (IsHtcViveFocusVision()) { return ESGHeadMountedDisplayDevice::HtcViveFocusVision; } if (IsHtcViveXRElite()) { return ESGHeadMountedDisplayDevice::HtcViveXRElite; } } if (Settings->GetTrackingSettings().HMDTrackingSettings.ViveHMDDetectionPriority == ESGViveHMDDetectionPriority::Focus3_XRElite_FocusVision) { if (IsHtcViveFocus3()) { return ESGHeadMountedDisplayDevice::HtcViveFocus3; } if (IsHtcViveXRElite()) { return ESGHeadMountedDisplayDevice::HtcViveXRElite; } if (IsHtcViveFocusVision()) { return ESGHeadMountedDisplayDevice::HtcViveFocusVision; } } return ESGHeadMountedDisplayDevice::None; }