Giant_Beast_2025/Plugins/MetaXR/Source/OculusXRHMD/Private/OculusXRMultiPlayerTestingSubsystem.cpp

380 lines
12 KiB
C++

// Copyright (c) Meta Platforms, Inc. and affiliates.
#include "OculusXRMultiPlayerTestingSubsystem.h"
#include "IHeadMountedDisplay.h"
#include "IXRTrackingSystem.h"
#include "Widgets/SViewport.h"
#include "Slate/SceneViewport.h"
#include "OculusXRHMDModule.h"
#include "OculusXRHMDRuntimeSettings.h"
#include "OculusXRHMD.h"
#if WITH_EDITOR
#include "Editor/UnrealEd/Classes/Editor/EditorEngine.h"
#include "Settings/LevelEditorPlaySettings.h"
#endif
#if PLATFORM_WINDOWS
#include "Windows/WindowsPlatformProcess.h"
#include <TlHelp32.h>
#endif
/**
* Initialize the subsystem. USubsystem override
*/
void UOculusXRMultiPlayerTestingSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
}
/**
* De-initializes the subsystem. USubsystem override
*/
void UOculusXRMultiPlayerTestingSubsystem::Deinitialize()
{
Super::Deinitialize();
}
ETickableTickType UOculusXRMultiPlayerTestingSubsystem::GetTickableTickType() const
{
return IsMultiPlayerTestingSupported() ? ETickableTickType::Always : ETickableTickType::Never;
}
bool UOculusXRMultiPlayerTestingSubsystem::IsAllowedToTick() const
{
return IsMultiPlayerTestingSupported();
}
void UOculusXRMultiPlayerTestingSubsystem::Tick(float)
{
#if PLATFORM_WINDOWS && WITH_EDITOR
bool bSingleProcessMode = true;
if (!IsMultiPlayerTestingEnabled(bSingleProcessMode))
{
return;
}
if (GEngine)
{
const ULevelEditorPlaySettings* PlayInSettings = GetDefault<ULevelEditorPlaySettings>();
bool RunUnderOneProcess = true;
PlayInSettings->GetRunUnderOneProcess(RunUnderOneProcess);
if (GetMutableDefault<UOculusXRHMDRuntimeSettings>()->bSetActivePIEToPrimary && RunUnderOneProcess)
{
SwitchPrimaryPIE_SingleProcess();
}
}
#endif
}
bool UOculusXRMultiPlayerTestingSubsystem::IsMultiPlayerTestingSupported() const
{
#if PLATFORM_WINDOWS && WITH_EDITOR
return true;
#else
return false;
#endif
}
#if PLATFORM_WINDOWS
bool UOculusXRMultiPlayerTestingSubsystem::IsMultiPlayerTestingEnabled(bool& bSingleProcessMode)
{
bSingleProcessMode = false;
NumClients = 1;
// ULevelEditorPlaySettings of 2nd+ player of MultipleProcess mode does NOT have correct GetRunUnderOneProcess() data.
// Let's use InInstanceNum to figure out how many clients are enabled already.
int InInstanceNum = 0;
FParse::Value(FCommandLine::Get(), TEXT("InInstanceNum="), InInstanceNum);
if (InInstanceNum)
{
bSingleProcessMode = false;
NumClients = InInstanceNum + 1;
}
#if WITH_EDITOR
else
{
// SingleProcess mode OR 1st player of MultipleProcess mode are kicked off from editor
const ULevelEditorPlaySettings* PlayInSettings = GetDefault<ULevelEditorPlaySettings>();
if (PlayInSettings)
{
PlayInSettings->GetPlayNumberOfClients(NumClients);
PlayInSettings->GetRunUnderOneProcess(bSingleProcessMode);
}
}
#endif // WITH_EDITOR
#ifdef WITH_OCULUS_BRANCH
return (NumClients > 1
&& GEngine
&& GEngine->XRSystem.IsValid()
&& GEngine->XRSystem->GetStereoRenderingDevice().IsValid()
&& GEngine->XRSystem->GetStereoRenderingDevice()->IsStereoEnabled());
#else
return (NumClients > 1 && OculusXRHMD::FOculusXRHMD::GetOculusXRHMD() && OculusXRHMD::FOculusXRHMD::GetOculusXRHMD()->IsStereoEnabled());
#endif
}
static void SwitchPrimaryPIE(const TArray<FString>& Args, UWorld*, FOutputDevice& Ar)
{
if (!GEngine || !GEngine->XRSystem.IsValid())
{
Ar.Logf(TEXT("XRSystem must be valid to switch PrimaryPIE!"));
return;
}
if (!GetMutableDefault<UOculusXRHMDRuntimeSettings>()->bSetCVarPIEToPrimary)
{
Ar.Logf(TEXT("Please enable \"Set CVar PIE To Primary\" in MetaXRPlugin settings!"));
return;
}
if (Args.Num())
{
if (UOculusXRMultiPlayerTestingSubsystem* MPTSSubsystem = GEngine ? GEngine->GetEngineSubsystem<UOculusXRMultiPlayerTestingSubsystem>() : nullptr)
{
MPTSSubsystem->SwitchPrimaryPIE(nullptr, FCString::Atoi(*Args[0]));
}
}
else
{
Ar.Logf(TEXT("Invalid PrimaryPIEIndex!"));
}
}
const int DefaultNextProcIndex = -1;
static FAutoConsoleCommand CSwitchHMDCmd(
TEXT("vr.PrimaryPIEIndex"),
TEXT("Set primary PIE index on the fly. Setting it to -1 is to set the index to next PIE window. If RunUnderOneProcess is enabled, this index can be set to any PIE index."),
FConsoleCommandWithWorldArgsAndOutputDeviceDelegate::CreateStatic(SwitchPrimaryPIE));
void UOculusXRMultiPlayerTestingSubsystem::SwitchPrimaryPIE(UGameViewportClient* InViewport, TOptional<int> PrimaryPIEIndex)
{
bool bSingleProcessMode = true;
if (!IsMultiPlayerTestingEnabled(bSingleProcessMode))
{
return;
}
if (!bSingleProcessMode)
{
check(PrimaryPIEIndex.IsSet());
SwitchPrimaryPIE_MultiProcess(PrimaryPIEIndex.GetValue());
}
#if WITH_EDITOR
else
{
SwitchPrimaryPIE_SingleProcess(InViewport, PrimaryPIEIndex);
}
#endif
}
void UOculusXRMultiPlayerTestingSubsystem::GetUnrealEditorProcs(TArray<int32>& ProcIds)
{
FString ProcNameWithExtension = "UnrealEditor.exe";
HANDLE SnapShot = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (SnapShot != INVALID_HANDLE_VALUE)
{
PROCESSENTRY32 Entry;
Entry.dwSize = sizeof(PROCESSENTRY32);
const int ProjectNameMaxLength = 512;
if (::Process32First(SnapShot, &Entry))
{
do
{
// find unreal editor processes
if (FCString::Stricmp(*ProcNameWithExtension, Entry.szExeFile) == 0)
{
int32 ProcID = Entry.th32ProcessID;
HWND HWnd = FWindowsPlatformMisc::GetTopLevelWindowHandle(ProcID);
if (HWnd)
{
const TCHAR* ProjectName = FApp::GetProjectName();
WCHAR Buffer[ProjectNameMaxLength];
GetWindowText(HWnd, Buffer, ProjectNameMaxLength);
size_t ProjectNameLen = _tcslen(ProjectName);
if (_tcsnccmp(ProjectName, Buffer, ProjectNameLen) == 0)
{
ProcIds.Add(ProcID);
}
}
}
}
while (::Process32Next(SnapShot, &Entry));
}
::CloseHandle(SnapShot);
}
ProcIds.Sort(TLess<int32>());
}
// Activate PIE window across processes.
void UOculusXRMultiPlayerTestingSubsystem::SwitchPrimaryPIE_MultiProcess(int PrimaryPIEIndex)
{
uint32 CurProcId = FPlatformProcess::GetCurrentProcessId();
uint32 DestProcId = -1;
TArray<int32> ProcIds;
GetUnrealEditorProcs(ProcIds);
// For now, PrimaryPIEIndex must be -1 which means we always switch to next player.
// The reason is users don't know each process' index and cannot easily set it via vr.PrimaryPIEIndex even if we sort processIDs first.
if (PrimaryPIEIndex == DefaultNextProcIndex)
{ // find next process's Id
int32 CurProcIdx = ProcIds.Find(CurProcId);
check(CurProcIdx != INDEX_NONE);
int32 NextProcIdx = (CurProcIdx + 1) % ProcIds.Num();
DestProcId = ProcIds[NextProcIdx];
}
else
{
UE_LOG(LogHMD, Warning, TEXT("vr.PrimaryPIEIndex must be -1 if RunUnderOneProcess is not set."));
return;
}
if (DestProcId == -1)
{
UE_LOG(LogHMD, Warning, TEXT("Cannot find vr.PrimaryPIEIndex: %d"), PrimaryPIEIndex);
return;
}
else
{
UE_LOG(LogHMD, Log, TEXT("SwitchPrimaryPIE from ProcID: %d to: %d"), CurProcId, DestProcId);
HWND MainWindowHandle = FWindowsPlatformMisc::GetTopLevelWindowHandle(DestProcId);
::SwitchToThisWindow(MainWindowHandle, true);
}
}
#if WITH_EDITOR
void UOculusXRMultiPlayerTestingSubsystem::SwitchPrimaryPIE_SingleProcess(UGameViewportClient* InViewport, TOptional<int> PrimaryPIEIndex)
{
if (InViewport && PrimaryPIEIndex.IsSet())
{
UE_LOG(LogHMD, Warning, TEXT("InViewport and PrimaryPIEIndex should not be set at the same time."));
return;
}
if (!GEditor)
{
UE_LOG(LogHMD, Warning, TEXT("SwitchPrimaryPIE_SingleProcess is only supported in editor."));
return;
}
if (PrimaryPIEIndex == DefaultNextProcIndex)
{ // Activate the world next to the current primary one
for (const FWorldContext& WorldContext : GEditor->GetWorldContexts())
{
if (WorldContext.bIsPrimaryPIEInstance)
{
PrimaryPIEIndex = (WorldContext.PIEInstance + 1) % NumClients;
}
}
}
FWorldContext* OldPrimaryWorld = nullptr;
FWorldContext* NewPrimaryWorld = nullptr;
for (const FWorldContext& WorldContext : GEditor->GetWorldContexts())
{
if (WorldContext.bIsPrimaryPIEInstance)
{
OldPrimaryWorld = GEditor->GetWorldContextFromWorld(WorldContext.World());
}
else if (WorldContext.GameViewport == InViewport || (PrimaryPIEIndex.IsSet() && WorldContext.PIEInstance == PrimaryPIEIndex.GetValue()))
{
NewPrimaryWorld = GEditor->GetWorldContextFromWorld(WorldContext.World());
}
}
if (OldPrimaryWorld && NewPrimaryWorld)
{
SwitchPrimaryPIE(OldPrimaryWorld, NewPrimaryWorld);
}
else
{
UE_LOG(LogHMD, Warning, TEXT("Failed to switch primary PIE."));
}
}
void UOculusXRMultiPlayerTestingSubsystem::SwitchPrimaryPIE_SingleProcess()
{
NumClients = 0;
const ULevelEditorPlaySettings* PlayInSettings = GetDefault<ULevelEditorPlaySettings>();
if (PlayInSettings)
{
PlayInSettings->GetPlayNumberOfClients(NumClients);
}
if (!(GEditor && GEditor->GetPlayInEditorSessionInfo().IsSet() && GEditor->GetPlayInEditorSessionInfo()->NumClientInstancesCreated == NumClients))
{
return;
}
FWorldContext* OldPrimaryWorld = nullptr;
FWorldContext* NewPrimaryWorld = nullptr;
for (const FWorldContext& WorldContext : GEditor->GetWorldContexts())
{
if (!WorldContext.GameViewport || !WorldContext.GameViewport->GetGameViewport())
{
UE_LOG(LogHMD, Warning, TEXT("No GameViewport or SceneViewPort for the current world %d."), WorldContext.PIEInstance);
continue;
}
const FSceneViewport* SceneViewPort = WorldContext.GameViewport->GetGameViewport();
if (WorldContext.bIsPrimaryPIEInstance && !SceneViewPort->HasFocus())
{
OldPrimaryWorld = GEditor->GetWorldContextFromWorld(WorldContext.World());
}
else if (!WorldContext.bIsPrimaryPIEInstance && SceneViewPort->HasFocus())
{
NewPrimaryWorld = GEditor->GetWorldContextFromWorld(WorldContext.World());
}
}
if (OldPrimaryWorld && NewPrimaryWorld)
{
UE_LOG(LogHMD, Log, TEXT("SwitchPrimaryPIE from %d to %d."), OldPrimaryWorld->PIEInstance, NewPrimaryWorld->PIEInstance);
SwitchPrimaryPIE(OldPrimaryWorld, NewPrimaryWorld);
}
}
void UOculusXRMultiPlayerTestingSubsystem::SwitchPrimaryPIE(FWorldContext* OldPrimaryWorld, FWorldContext* NewPrimaryWorld)
{
auto TogglePrimaryWorld = [](FWorldContext* PrimaryWorld, bool bEnable) {
check(PrimaryWorld != nullptr);
FSceneViewport* SceneViewPort = PrimaryWorld->GameViewport->GetGameViewport();
PrimaryWorld->bIsPrimaryPIEInstance = bEnable;
SceneViewPort->GetViewportWidget().Pin()->EnableStereoRendering(bEnable);
SceneViewPort->GetViewportWidget().Pin()->SetRenderDirectlyToWindow(bEnable);
SceneViewPort->SetPlayInEditorGetsMouseControl(bEnable);
SceneViewPort->SetViewportSize(SceneViewPort->GetSizeXY().X, SceneViewPort->GetSizeXY().Y);
if (bEnable)
{
SceneViewPort->FindWindow()->GetNativeWindow()->SetWindowFocus();
}
else
{
SceneViewPort->FindWindow()->SetViewportSizeDrivenByWindow(true);
}
};
if (NewPrimaryWorld->GameViewport && NewPrimaryWorld->GameViewport->GetGameViewport() && OldPrimaryWorld->GameViewport && OldPrimaryWorld->GameViewport->GetGameViewport())
{
TogglePrimaryWorld(NewPrimaryWorld, true);
TogglePrimaryWorld(OldPrimaryWorld, false);
// ATM, in Vanilla/Stock UE, no MultiPlayer support in OpenXR plugin.
// TODO: Remove below branch checking and refactor FOculusXRHMD to be the same with FOpenXRHMD once IOpenXRExtensionPlugin::ReCalcPose(...) is upstreamed.
#ifdef WITH_OCULUS_BRANCH
OculusXR::FMultiPlayerStateExtensionPlugin& MPPlugin = FOculusXRHMDModule::Get().GetExtensionPluginManager().GetMultiPlayerStateExtensionPlugin();
MPPlugin.SwitchPrimaryPIE(NewPrimaryWorld->PIEInstance);
#endif // WITH_OCULUS_BRANCH
if (OculusXRHMD::FOculusXRHMD::GetOculusXRHMD())
{
OculusXRHMD::FOculusXRHMD::GetOculusXRHMD()->SwitchPrimaryPIE(NewPrimaryWorld->PIEInstance);
}
}
}
#endif // WITH_EDITOR
#endif // PLATFORM_WINDOWS