#if UNITY_EDITOR && ENABLE_TEST_SUPPORT #define TEST_SUPPORT #endif using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; using System.Security.Cryptography; using System.Text; using UnityEngine.Scripting; using UnityEngine.XR.Management; using UnityEngine.XR.OpenXR.Input; using UnityEngine.XR.OpenXR.Features; #if UNITY_EDITOR using UnityEditor; using UnityEditor.XR.Management; using UnityEditor.XR.OpenXR; #endif [assembly: Preserve] [assembly: InternalsVisibleTo("Unity.XR.OpenXR.TestHelpers")] [assembly: InternalsVisibleTo("Unity.XR.OpenXR.Tests")] [assembly: InternalsVisibleTo("Unity.XR.OpenXR.Tests.Editor")] [assembly: InternalsVisibleTo("Unity.XR.OpenXR.Editor")] namespace UnityEngine.XR.OpenXR { /// /// Loader for the OpenXR Plug-in. Used by [XR Plug-in Management](https://docs.unity3d.com/Packages/com.unity.xr.management@latest) to manage OpenXR lifecycle. /// #if UNITY_EDITOR [XRSupportedBuildTarget(BuildTargetGroup.Standalone, new BuildTarget[] {BuildTarget.StandaloneWindows64})] [XRSupportedBuildTarget(BuildTargetGroup.Android)] [XRSupportedBuildTarget(BuildTargetGroup.WSA)] #endif public class OpenXRLoader : OpenXRLoaderBase #if UNITY_EDITOR , IXRLoaderPreInit #endif { #if UNITY_EDITOR public string GetPreInitLibraryName(BuildTarget buildTarget, BuildTargetGroup buildTargetGroup) { return "UnityOpenXR"; } #endif } /// /// Base abstract class to hold common loader code. /// #if UNITY_EDITOR // Hide this by default from the UI by setting to Unknown. [XRSupportedBuildTarget(BuildTargetGroup.Unknown)] #endif public partial class OpenXRLoaderBase : XRLoaderHelper { private class FeatureLoggingInfo { public FeatureLoggingInfo(string nameUi, string version, string company, string extensionStrings) { m_nameUi = nameUi; m_version = version; m_company = company; m_openxrExtensionStrings = extensionStrings; } public string m_nameUi; public string m_version; public string m_company; public string m_openxrExtensionStrings; } private List featureLoggingInfo; const double k_IdlePollingWaitTimeInSeconds = 0.1; private static List s_DisplaySubsystemDescriptors = new List(); private static List s_InputSubsystemDescriptors = new List(); /// /// Represents the running OpenXRLoader instance. This value should be non null after calling /// Initialize until a subsequent call to DeInitialize is made. /// internal static OpenXRLoaderBase Instance { get; private set; } internal enum LoaderState { Uninitialized, InitializeAttempted, Initialized, StartAttempted, Started, StopAttempted, Stopped, DeinitializeAttempted } internal LoaderState currentLoaderState { get; private set; } = LoaderState.Uninitialized; List validLoaderInitStates = new List { LoaderState.Uninitialized, LoaderState.InitializeAttempted }; List validLoaderStartStates = new List { LoaderState.Initialized, LoaderState.StartAttempted, LoaderState.Stopped }; List validLoaderStopStates = new List { LoaderState.StartAttempted, LoaderState.Started, LoaderState.StopAttempted }; List validLoaderDeinitStates = new List { LoaderState.InitializeAttempted, LoaderState.Initialized, LoaderState.Stopped, LoaderState.DeinitializeAttempted }; List runningStates = new List() { LoaderState.Initialized, LoaderState.StartAttempted, LoaderState.Started }; #if TEST_SUPPORT [NonSerialized] internal LoaderState targetLoaderState; bool ShouldExitEarly() { return (currentLoaderState == targetLoaderState); } #endif OpenXRFeature.NativeEvent currentOpenXRState; private bool actionSetsAttached; /// /// Reference to the current display subsystem if the loader is initialized, or null if the loader is not initialized. /// internal XRDisplaySubsystem displaySubsystem => GetLoadedSubsystem(); /// /// Reference to the current input subsystem if the loader is initialized, or null if the loader is not initialized. /// internal XRInputSubsystem inputSubsystem => Instance?.GetLoadedSubsystem(); /// /// True if the loader has been initialized, false otherwise. /// private bool isInitialized => currentLoaderState != LoaderState.Uninitialized && currentLoaderState != LoaderState.DeinitializeAttempted; /// /// True if the loader has been started, false otherwise. /// private bool isStarted => runningStates.Contains(currentLoaderState); private UnhandledExceptionEventHandler unhandledExceptionHandler = null; internal bool DisableValidationChecksOnEnteringPlaymode = false; static void ExceptionHandler(object sender, UnhandledExceptionEventArgs args) { var section = DiagnosticReport.GetSection("Unhandled Exception Report"); DiagnosticReport.AddSectionEntry(section, "Is Terminating", $"{args.IsTerminating}"); var e = (Exception)args.ExceptionObject; DiagnosticReport.AddSectionEntry(section, "Message", $"{e.Message}"); DiagnosticReport.AddSectionEntry(section, "Source", $"{e.Source}"); DiagnosticReport.AddSectionEntry(section, "Stack Trace", $"\n{e.StackTrace}"); DiagnosticReport.DumpReport("Uncaught Exception"); } /// /// See [XRLoader.Initialize](xref:UnityEngine.XR.Management.XRLoader.Initialize) /// /// True if initialized, false otherwise. public override bool Initialize() { if (currentLoaderState == LoaderState.Initialized) return true; if (!validLoaderInitStates.Contains(currentLoaderState)) return false; if (Instance != null) { Debug.LogError("Only one OpenXRLoader can be initialized at any given time"); return false; } #if UNITY_EDITOR if (!DisableValidationChecksOnEnteringPlaymode) { if (OpenXRProjectValidation.LogPlaymodeValidationIssues()) return false; } #endif DiagnosticReport.StartReport(); // Wrap the initialization in a try catch block to ensure if any exceptions are thrown that // we cleanup, otherwise the user will not be able to run again until they restart the editor. try { if (InitializeInternal()) return true; } catch (Exception e) { Debug.LogException(e); } Deinitialize(); Instance = null; OpenXRAnalytics.SendInitializeEvent(false); return false; } private bool InitializeInternal() { Instance = this; currentLoaderState = LoaderState.InitializeAttempted; #if TEST_SUPPORT if (ShouldExitEarly()) return false; #endif OpenXRLoaderBase.Internal_SetSuccessfullyInitialized(false); OpenXRInput.RegisterLayouts(); OpenXRFeature.Initialize(); if (!LoadOpenXRSymbols()) { Debug.LogError("Failed to load openxr runtime loader."); return false; } // Sort the features array by priority in descending order (highest priority first) OpenXRSettings.Instance.features = OpenXRSettings.Instance.features .Where(f => f != null) .OrderByDescending(f => f.priority) .ThenBy(f => f.nameUi) .ToArray(); OpenXRFeature.HookGetInstanceProcAddr(); if (!Internal_InitializeSession()) return false; RequestOpenXRFeatures(); RegisterOpenXRCallbacks(); if (null != OpenXRSettings.Instance) OpenXRSettings.Instance.ApplySettings(); if (!CreateSubsystems()) return false; if (OpenXRFeature.requiredFeatureFailed) return false; SetApplicationInfo(); OpenXRAnalytics.SendInitializeEvent(true); OpenXRFeature.ReceiveLoaderEvent(this, OpenXRFeature.LoaderEvent.SubsystemCreate); DebugLogEnabledSpecExtensions(); Application.onBeforeRender += ProcessOpenXRMessageLoop; currentLoaderState = LoaderState.Initialized; return true; } private bool CreateSubsystems() { // NOTE: This function is only necessary to handle subsystems being lost after domain reload. If that issue is fixed // at the management level the code below can be folded back into Initialize // NOTE: Below we check to see if a subsystem is already created before creating it. This is done because we currently // re-create the subsystems after a domain reload to fix a deficiency in XR Managements handling of domain reload. To // ensure we properly handle a fix to that deficiency we first check to make sure the subsystems are not already created. if (displaySubsystem == null) { CreateSubsystem(s_DisplaySubsystemDescriptors, "OpenXR Display"); if (displaySubsystem == null) return false; } if (inputSubsystem == null) { CreateSubsystem(s_InputSubsystemDescriptors, "OpenXR Input"); if (inputSubsystem == null) return false; } return true; } private double lastPollCheckTime = 0; internal void ProcessOpenXRMessageLoop() { if (currentOpenXRState == OpenXRFeature.NativeEvent.XrIdle || currentOpenXRState == OpenXRFeature.NativeEvent.XrStopping || currentOpenXRState == OpenXRFeature.NativeEvent.XrExiting || currentOpenXRState == OpenXRFeature.NativeEvent.XrLossPending || currentOpenXRState == OpenXRFeature.NativeEvent.XrInstanceLossPending) { var time = Time.realtimeSinceStartup; if ((time - lastPollCheckTime) < k_IdlePollingWaitTimeInSeconds) return; lastPollCheckTime = time; } Internal_PumpMessageLoop(); } /// /// See [XRLoader.Start](xref:UnityEngine.XR.Management.XRLoader.Start) /// /// True if started, false otherwise. public override bool Start() { if (currentLoaderState == LoaderState.Started) return true; if (!validLoaderStartStates.Contains(currentLoaderState)) return false; currentLoaderState = LoaderState.StartAttempted; #if TEST_SUPPORT if (ShouldExitEarly()) return false; #endif if (!StartInternal()) { Stop(); return false; } currentLoaderState = LoaderState.Started; return true; } private bool StartInternal() { // In order to get XrReady, we have to at least attempt to create // the session if it isn't already there. if (!Internal_CreateSessionIfNeeded()) return false; if (currentOpenXRState != OpenXRFeature.NativeEvent.XrReady || (currentLoaderState != LoaderState.StartAttempted && currentLoaderState != LoaderState.Started)) { return true; } // Note: Display has to be started before Input so that Input can have access to the Session object StartSubsystem(); if (!displaySubsystem?.running ?? false) return false; // calls xrBeginSession Internal_BeginSession(); if (!actionSetsAttached) { OpenXRInput.AttachActionSets(); actionSetsAttached = true; } if (!displaySubsystem?.running ?? false) StartSubsystem(); if (!inputSubsystem?.running ?? false) StartSubsystem(); var inputRunning = inputSubsystem?.running ?? false; var displayRunning = displaySubsystem?.running ?? false; if (inputRunning && displayRunning) { OpenXRFeature.ReceiveLoaderEvent(this, OpenXRFeature.LoaderEvent.SubsystemStart); return true; } return false; } /// /// See [XRLoader.Stop](xref:UnityEngine.XR.Management.XRLoader.Stop) /// /// True if stopped, false otherwise. public override bool Stop() { if (currentLoaderState == LoaderState.Stopped) return true; if (!validLoaderStopStates.Contains(currentLoaderState)) return false; currentLoaderState = LoaderState.StopAttempted; #if TEST_SUPPORT if (ShouldExitEarly()) return false; #endif var inputRunning = inputSubsystem?.running ?? false; var displayRunning = displaySubsystem?.running ?? false; if (inputRunning || displayRunning) OpenXRFeature.ReceiveLoaderEvent(this, OpenXRFeature.LoaderEvent.SubsystemStop); if (inputRunning) StopSubsystem(); if (displayRunning) StopSubsystem(); StopInternal(); currentLoaderState = LoaderState.Stopped; return true; } private void StopInternal() { Internal_EndSession(); ProcessOpenXRMessageLoop(); } /// /// See [XRLoader.DeInitialize](xref:UnityEngine.XR.Management.XRLoader.Stop) /// /// True if deinitialized, false otherwise. public override bool Deinitialize() { if (currentLoaderState == LoaderState.Uninitialized) return true; if (!validLoaderDeinitStates.Contains(currentLoaderState)) return false; currentLoaderState = LoaderState.DeinitializeAttempted; try { #if TEST_SUPPORT if (ShouldExitEarly()) return false; // The test hook above will leave the loader in a half initialized state. To work // around this we reset the instance pointer if it is missing. if (Instance == null) Instance = this; #endif Internal_RequestExitSession(); Application.onBeforeRender -= ProcessOpenXRMessageLoop; ProcessOpenXRMessageLoop(); // Drain any remaining events. OpenXRFeature.ReceiveLoaderEvent(this, OpenXRFeature.LoaderEvent.SubsystemDestroy); DestroySubsystem(); DestroySubsystem(); DiagnosticReport.DumpReport("System Shutdown"); Internal_DestroySession(); ProcessOpenXRMessageLoop(); Internal_UnloadOpenXRLibrary(); currentLoaderState = LoaderState.Uninitialized; actionSetsAttached = false; if (unhandledExceptionHandler != null) { AppDomain currentDomain = AppDomain.CurrentDomain; currentDomain.UnhandledException -= unhandledExceptionHandler; unhandledExceptionHandler = null; } return base.Deinitialize(); } finally { // Ensure we always clear the instance reference even if some part of Deinitialize threw an exception Instance = null; } } internal new void CreateSubsystem(List descriptors, string id) where TDescriptor : ISubsystemDescriptor where TSubsystem : ISubsystem { base.CreateSubsystem(descriptors, id); } internal new void StartSubsystem() where T : class, ISubsystem => base.StartSubsystem(); internal new void StopSubsystem() where T : class, ISubsystem => base.StopSubsystem(); internal new void DestroySubsystem() where T : class, ISubsystem => base.DestroySubsystem(); private void SetApplicationInfo() { var md5 = MD5.Create(); byte[] data = md5.ComputeHash(Encoding.UTF8.GetBytes(Application.version)); if (BitConverter.IsLittleEndian) Array.Reverse(data); uint applicationVersionHash = BitConverter.ToUInt32(data, 0); Internal_SetApplicationInfo(Application.productName, Application.version, applicationVersionHash, Application.unityVersion); } internal static byte[] StringToWCHAR_T(string s) { var encoding = Environment.OSVersion.Platform == PlatformID.Unix ? Encoding.UTF32 : Encoding.Unicode; return encoding.GetBytes(s + '\0'); } private bool LoadOpenXRSymbols() { string loaderPath = "openxr_loader"; #if UNITY_EDITOR_WIN loaderPath = "..\\..\\..\\RuntimeLoaders\\windows\\x64\\openxr_loader"; #elif UNITY_EDITOR_OSX // no loader for osx, use the mock by default loaderPath = $"../../MockRuntime/osx/openxr_loader"; #endif #if UNITY_EDITOR // Pass down active loader path to plugin EditorBuildSettings.TryGetConfigObject(Constants.k_SettingsKey, out var obj); if (obj != null && (obj is IPackageSettings packageSettings)) { var extensionLoaderPath = packageSettings.GetActiveLoaderLibraryPath(); if (!String.IsNullOrEmpty(extensionLoaderPath)) loaderPath = extensionLoaderPath; } #endif if (!Internal_LoadOpenXRLibrary(StringToWCHAR_T(loaderPath))) return false; return true; } private void RequestOpenXRFeatures() { var instance = OpenXRSettings.Instance; if (instance == null || instance.features == null) return; featureLoggingInfo = new List(instance.featureCount); foreach (var feature in instance.features) { if (feature == null || !feature.enabled) continue; // Store feature logging info to be logged later. // We need to log this after we've determined the version of the OpenXR Runtime featureLoggingInfo.Add(new FeatureLoggingInfo(feature.nameUi, feature.version, feature.company, feature.openxrExtensionStrings)); if (!string.IsNullOrEmpty(feature.openxrExtensionStrings)) { // Check to see if any of the required extensions are not supported by the runtime foreach (var extensionString in feature.openxrExtensionStrings.Split(' ')) { // Request each extension. if (string.IsNullOrWhiteSpace(extensionString)) continue; Internal_RequestEnableExtensionString(extensionString); } } } } private void LogRequestedOpenXRFeatures() { var instance = OpenXRSettings.Instance; if (instance == null || instance.features == null) return; StringBuilder requestedLog = new StringBuilder(""); StringBuilder failedLog = new StringBuilder(""); uint count = 0; uint failedCount = 0; foreach (var feature in featureLoggingInfo) { requestedLog.Append($" {feature.m_nameUi}: Version={feature.m_version}, Company=\"{feature.m_company}\""); if (!string.IsNullOrEmpty(feature.m_openxrExtensionStrings)) { requestedLog.Append($", Extensions=\"{feature.m_openxrExtensionStrings}\""); // Check to see if any of the required extensions are not supported by the runtime foreach (var extensionString in feature.m_openxrExtensionStrings.Split(' ')) { if (string.IsNullOrWhiteSpace(extensionString)) continue; if (!Internal_IsExtensionEnabled(extensionString)) { ++failedCount; failedLog.Append($" {extensionString}: Feature=\"{feature.m_nameUi}\": Version={feature.m_version}, Company=\"{feature.m_company}\"\n"); } } } requestedLog.Append("\n"); } var section = DiagnosticReport.GetSection("OpenXR Runtime Info"); DiagnosticReport.AddSectionBreak(section); DiagnosticReport.AddSectionEntry(section, "Features requested to be enabled", $"({count})\n{requestedLog.ToString()}"); DiagnosticReport.AddSectionBreak(section); DiagnosticReport.AddSectionEntry(section, "Requested feature extensions not supported by runtime", $"({failedCount})\n{failedLog.ToString()}"); } private static void DebugLogEnabledSpecExtensions() { var section = DiagnosticReport.GetSection("OpenXR Runtime Info"); DiagnosticReport.AddSectionBreak(section); var extensions = OpenXRRuntime.GetEnabledExtensions(); var log = new StringBuilder($"({extensions.Length})\n"); foreach (var extension in extensions) log.Append($" {extension}: Version={OpenXRRuntime.GetExtensionVersion(extension)}\n"); DiagnosticReport.AddSectionEntry(section, "Runtime extensions enabled", log.ToString()); } [AOT.MonoPInvokeCallback(typeof(ReceiveNativeEventDelegate))] private static void ReceiveNativeEvent(OpenXRFeature.NativeEvent e, ulong payload) { var loader = Instance; if (loader != null) loader.currentOpenXRState = e; switch (e) { case OpenXRFeature.NativeEvent.XrRestartRequested: OpenXRRestarter.Instance.ShutdownAndRestart(); break; case OpenXRFeature.NativeEvent.XrReady: loader.StartInternal(); break; case OpenXRFeature.NativeEvent.XrBeginSession: loader.LogRequestedOpenXRFeatures(); break; case OpenXRFeature.NativeEvent.XrFocused: DiagnosticReport.DumpReport("System Startup Completed"); break; case OpenXRFeature.NativeEvent.XrRequestRestartLoop: Debug.Log("XR Initialization failed, will try to restart xr periodically."); OpenXRRestarter.Instance.PauseAndShutdownAndRestart(); break; case OpenXRFeature.NativeEvent.XrRequestGetSystemLoop: OpenXRRestarter.Instance.PauseAndRetryInitialization(); break; case OpenXRFeature.NativeEvent.XrStopping: loader.StopInternal(); break; default: break; } OpenXRFeature.ReceiveNativeEvent(e, payload); if ((loader == null || !loader.isStarted) && e != OpenXRFeature.NativeEvent.XrInstanceChanged) return; switch (e) { case OpenXRFeature.NativeEvent.XrExiting: OpenXRRestarter.Instance.Shutdown(); break; case OpenXRFeature.NativeEvent.XrLossPending: OpenXRRestarter.Instance.ShutdownAndRestart(); break; case OpenXRFeature.NativeEvent.XrInstanceLossPending: OpenXRRestarter.Instance.Shutdown(); break; default: break; } } internal delegate void ReceiveNativeEventDelegate(OpenXRFeature.NativeEvent e, ulong payload); internal static void RegisterOpenXRCallbacks() { Internal_SetCallbacks(ReceiveNativeEvent); } #if UNITY_EDITOR private void OnAfterAssemblyReload() { AssemblyReloadEvents.afterAssemblyReload -= OnAfterAssemblyReload; // Recreate the subsystems. Note that post domain reload this is more about // repopulating the subsystem instance map than it is about actually creating subsystems. At this // point the domain reload is finished and the SubsystemManager has already patched // all of the subsystem interop pointers so we just need to go out and get them again. CreateSubsystems(); } private void OnEnable() { AppDomain currentDomain = AppDomain.CurrentDomain; if (unhandledExceptionHandler != null) { currentDomain.UnhandledException -= unhandledExceptionHandler; unhandledExceptionHandler = null; } unhandledExceptionHandler = new UnhandledExceptionEventHandler(ExceptionHandler); currentDomain.UnhandledException += unhandledExceptionHandler; // If the loader is already initialized then this is likely due to a domain // reload so we need patch u the running instance reference. if (isInitialized && Instance == null) { Instance = this; // Recreate subsystems after all assemblies are finished loading. This cannot be done here // because the SubsystemManager is handling domain reload itself, but is called after // this call to OnEnable but before the afterAssemblyReload callback. The SubsystemManager will // reset the subsystem instance list on domain reload and thus if we create the subsystems now they // will be invalidated immediately after by the domain reload and the list will be out of sync. By waiting // until the afterAssemblyReload we can ensure the list has been created first. AssemblyReloadEvents.afterAssemblyReload += OnAfterAssemblyReload; // Re-register the callbacks with the plugin to reflect the new class instance RegisterOpenXRCallbacks(); // Hook ourself back into onBeforeRender. While the onBeforeRender should no longer contain our // message loop hook we will remove it first just to be extra safe. Application.onBeforeRender -= ProcessOpenXRMessageLoop; Application.onBeforeRender += ProcessOpenXRMessageLoop; } } #endif } }