// %BANNER_BEGIN%
// ---------------------------------------------------------------------
// %COPYRIGHT_BEGIN%
// Copyright (c) (2024) Magic Leap, Inc. All Rights Reserved.
// Use of this file is governed by the Software License Agreement, located here: https://www.magicleap.com/software-license-agreement-ml2
// Terms and conditions applicable to third-party materials accompanying this distribution may also be found in the top-level NOTICE file appearing herein.
// %COPYRIGHT_END%
// ---------------------------------------------------------------------
// %BANNER_END%
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
using MagicLeap.OpenXR.Constants;
using MagicLeap.OpenXR.SystemInfo;
using MagicLeap.OpenXR.NativeDelegates;
using MagicLeap.OpenXR.Time;
using MagicLeap.OpenXR.ViewConfiguration;
using UnityEngine;
using UnityEngine.LowLevel;
using UnityEngine.XR.ARSubsystems;
using UnityEngine.XR.MagicLeap;
using UnityEngine.XR.MagicLeap.Native;
using UnityEngine.XR.OpenXR.NativeTypes;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.XR.OpenXR.Features;
#endif
namespace MagicLeap.OpenXR.Features
{
///
/// Enables the Magic Leap OpenXR Loader for Android, and modifies the AndroidManifest to be compatible with ML2.
///
#if UNITY_EDITOR
[OpenXRFeature(UiName = "Magic Leap 2 Support",
Desc="Necessary to deploy a Magic Leap 2 compatible application.",
Company = "Magic Leap",
Version = "1.0.0",
BuildTargetGroups = new []{ BuildTargetGroup.Android, BuildTargetGroup.Standalone },
FeatureId = FeatureId,
OpenxrExtensionStrings = "XR_ML_compat XR_KHR_convert_timespec_time XR_EXT_view_configuration_depth_range XR_ML_view_configuration_depth_range_change"
)]
#endif
public partial class MagicLeapFeature : MagicLeapOpenXRFeatureWithInterception
{
private struct MLPerceptionSnapshotUpdate { }
public enum FarClipMode : byte
{
///
/// Do not restrict the Camera's far clip plane distance.
///
None,
///
/// Restrict the Camera's far clip plane distance to no more than the maximum allowed by the device.
///
Maximum,
///
/// Restrict the Camera's far clip plane to no more than the distance recommended by Magic Leap.
///
Recommended,
}
public enum NearClipMode : byte
{
///
/// Restrict the Camera's near clip plane to no less than the absolute minimum allowed (25cm).
///
Minimum,
///
/// Restrict the Camera's near clip plane to no less than the distance configured in the system's settings.
///
Recommended,
#if DISABLE_MAGICLEAP_CLIP_ENFORCEMENT
///
/// Do not restrict the Camera's near clip plane distance.
///
None,
#endif
}
///
/// The feature id string. This is used to give the feature a well known id for reference.
///
public const string FeatureId = "com.magicleap.openxr.feature.ml2";
[SerializeField]
[Tooltip("Should MLPerception snapshots be performed? This is used to support certain legacy ML APIs.")]
private bool perceptionSnapshots;
[SerializeField]
[Tooltip("Determines if the far clipping plane should be clamped, and to what maximum value.")]
private FarClipMode farClipPolicy = FarClipMode.Recommended;
[SerializeField]
[Tooltip("Determines the minimum value the near clipping plane will be clamped to.")]
private NearClipMode nearClipPolicy = NearClipMode.Recommended;
public FarClipMode FarClipPolicy => farClipPolicy;
public NearClipMode NearClipPolicy => nearClipPolicy;
private IntPtr mlSnapshot = IntPtr.Zero;
public bool EnablePerceptionSnapshots
{
get => perceptionSnapshots;
set
{
perceptionSnapshots = value;
if (perceptionSnapshots)
{
RegisterSnapshotPlayerLoop();
}
else
{
UnregisterSnapshotPlayerLoop();
}
}
}
public float MinNearZ => viewConfigurationDepthRange.MinNearZ;
public float RecommendedNearZ => viewConfigurationDepthRange.RecommendedNearZ;
public float MaxFarZ => viewConfigurationDepthRange.MaxFarZ;
public float RecommendedFarZ => viewConfigurationDepthRange.RecommendedFarZ;
//This is used when DISABLE_MAGICLEAP_CLIP_ENFORCEMENT flag is toggled, for use with NearClipMode.None
// Unity doesn't like its camera nearClip going below 0.01 and will lock up if it does.
private const float MinimumNearClip = 0.01f;
private static readonly List SessionSubsystemDesc = new();
private XrSessionState sessionState = XrSessionState.Unknown;
private XrGetInstanceProcAddr getInstanceProcAddr;
private TimeSpecNativeFunctions timeSpecNativeFunctions;
private SystemInfoNativeFunctions systemInfoNativeFunctions;
private ViewConfigNativeFunctions magicLeapViewConfigurationNativeFunctions;
private XrViewConfigurationDepthRange viewConfigurationDepthRange;
private PlayerLoopSystem snapshotUpdatePlayerLoop;
private FeatureLifecycleNativeListener lifecycleNativeListener;
private bool playerLoopRegistered;
protected override IntPtr HookGetInstanceProcAddr(IntPtr func)
{
lifecycleNativeListener = NativeBindings.MLOpenXRGetLifecycleListener(); ;
return base.HookGetInstanceProcAddr(func);
}
protected override bool OnInstanceCreate(ulong xrInstance)
{
var result = base.OnInstanceCreate(xrInstance);
if (!result)
{
return false;
}
getInstanceProcAddr = Marshal.GetDelegateForFunctionPointer(xrGetInstanceProcAddr);
timeSpecNativeFunctions = NativeFunctionsBase.Create(getInstanceProcAddr, xrInstance);
magicLeapViewConfigurationNativeFunctions = NativeFunctionsBase.Create(getInstanceProcAddr, xrInstance);
systemInfoNativeFunctions = NativeFunctionsBase.Create(getInstanceProcAddr, xrInstance);
EnumerateViewConfigurationViews();
lifecycleNativeListener.InstanceCreated(xrInstance, xrGetInstanceProcAddr);
return true;
}
internal override unsafe XrResult OnWaitFrame(ulong session, XrFrameWaitInfo* frameWaitInfo, XrFrameState* frameState, XrWaitFrame origWaitFrame)
{
var result = base.OnWaitFrame(session, frameWaitInfo, frameState, origWaitFrame);
if (result != XrResult.Success)
{
return result;
}
if (frameState->PredictedDisplayPeriod != Values.InfiniteDuration)
{
Interlocked.Exchange(ref PredictedDisplayTime, (frameState->PredictedDisplayTime + (long)frameState->PredictedDisplayPeriod));
}
else
{
Interlocked.Exchange(ref PredictedDisplayTime, frameState->PredictedDisplayTime);
}
lifecycleNativeListener.PredictedDisplayTimeChanged(PredictedDisplayTime);
MLXrSecondaryViewState.IsActive = false;
if (frameState->Next != IntPtr.Zero)
{
var secondaryFrameState = Marshal.PtrToStructure(frameState->Next);
for (int i = 0; i < secondaryFrameState.ViewConfigurationCount; i++)
{
var viewState = secondaryFrameState.ViewConfigurationStates[i];
if (viewState.Active)
{
MLXrSecondaryViewState.IsActive = true;
break;
}
}
}
return result;
}
private void EnumerateViewConfigurationViews()
{
var xrResult = systemInfoNativeFunctions.GetSystemId(out var systemId);
if (!Utils.DidXrCallSucceed(xrResult, nameof(systemInfoNativeFunctions.GetSystemId)))
{
return;
}
xrResult = magicLeapViewConfigurationNativeFunctions.EnumerateConfigurationViews(AppInstance, systemId, out viewConfigurationDepthRange);
Utils.DidXrCallSucceed(xrResult, nameof(magicLeapViewConfigurationNativeFunctions.EnumerateConfigurationViews));
}
protected override void OnInstanceDestroy(ulong xrInstance)
{
base.OnInstanceDestroy(xrInstance);
lifecycleNativeListener.InstanceDestroyed(xrInstance);
}
protected override void MarkFunctionsToIntercept()
{
InterceptWaitFrame = true;
}
protected override void OnAppSpaceChange(ulong xrSpace)
{
base.OnAppSpaceChange(xrSpace);
lifecycleNativeListener.AppSpaceChanged(xrSpace);
}
protected override void OnSessionCreate(ulong xrSession)
{
base.OnSessionCreate(xrSession);
lifecycleNativeListener.SessionCreated(xrSession);
if (Application.isEditor)
{
return;
}
if (!perceptionSnapshots)
{
return;
}
RegisterSnapshotPlayerLoop();
}
private void RegisterSnapshotPlayerLoop()
{
if (playerLoopRegistered)
{
return;
}
snapshotUpdatePlayerLoop = new PlayerLoopSystem()
{
subSystemList = Array.Empty(),
updateDelegate = PerformMLPerceptionSnapshot,
type = typeof(MLPerceptionSnapshotUpdate)
};
var playerLoop = PlayerLoop.GetCurrentPlayerLoop();
if (!PlayerLoopUtil.InstallIntoPlayerLoop(ref playerLoop, snapshotUpdatePlayerLoop, PlayerLoopUtil.InstallPath))
Debug.LogError("Unable to install snapshotting Update delegate into player loop!");
else
PlayerLoop.SetPlayerLoop(playerLoop);
playerLoopRegistered = true;
}
private void UnregisterSnapshotPlayerLoop()
{
if (!playerLoopRegistered)
{
return;
}
var playerLoop = PlayerLoop.GetCurrentPlayerLoop();
PlayerLoopUtil.RemoveFromPlayerLoop(ref playerLoop, snapshotUpdatePlayerLoop, PlayerLoopUtil.InstallPath);
playerLoopRegistered = false;
}
protected override void OnSessionBegin(ulong xrSession)
{
base.OnSessionBegin(xrSession);
if (!Application.isEditor)
{
Application.onBeforeRender += EnforceClippingPlanes;
}
}
protected override void OnSessionEnd(ulong xrSession)
{
base.OnSessionEnd(xrSession);
if (!Application.isEditor)
{
Application.onBeforeRender -= EnforceClippingPlanes;
}
}
protected override void OnSessionDestroy(ulong xrSession)
{
base.OnSessionDestroy(xrSession);
lifecycleNativeListener.SessionDestroyed(xrSession);
if (Application.isEditor)
{
return;
}
if (!perceptionSnapshots)
{
return;
}
UnregisterSnapshotPlayerLoop();
}
protected override void OnSessionStateChange(int oldState, int newState)
{
base.OnSessionStateChange(oldState, newState);
sessionState = (XrSessionState)newState;
}
protected override void OnSubsystemCreate()
{
base.OnSubsystemCreate();
CreateSubsystem(SessionSubsystemDesc, "MagicLeapXr-Session");
}
protected override void OnSubsystemDestroy()
{
base.OnSubsystemDestroy();
DestroySubsystem();
}
private void PerformMLPerceptionSnapshot()
{
if (!perceptionSnapshots || Application.isEditor || sessionState != XrSessionState.Focused)
return;
var result = MLResult.Code.Ok;
if (mlSnapshot != IntPtr.Zero)
{
result = MagicLeapNativeBindings.MLPerceptionReleaseSnapshot(mlSnapshot);
if(!MLResult.DidNativeCallSucceed(result, nameof(MagicLeapNativeBindings.MLPerceptionReleaseSnapshot)))
{
mlSnapshot = IntPtr.Zero;
return;
}
}
result = MagicLeapNativeBindings.MLPerceptionGetSnapshot(ref mlSnapshot);
MLResult.DidNativeCallSucceed(result, nameof(MagicLeapNativeBindings.MLPerceptionGetSnapshot));
}
private void EnforceClippingPlanes() => ApplyToCamera(Camera.main);
public void SetNearClipPolicy(NearClipMode mode)
{
nearClipPolicy = mode;
EnumerateViewConfigurationViews();
EnforceClippingPlanes();
}
private void ApplyFarClip(ref float zFar)
{
switch (farClipPolicy)
{
case FarClipMode.Maximum:
zFar = Mathf.Min(zFar, Mathf.Min(zFar, MaxFarZ));
break;
case FarClipMode.Recommended:
zFar = Mathf.Min(zFar, RecommendedFarZ);
break;
case FarClipMode.None:
default:
break;
}
}
public void ApplyNearClip(ref float zNear)
{
switch (nearClipPolicy)
{
// Whatever is set in the system settings menu is our new minimum, even if it is
// above the system recommendation.
case NearClipMode.Minimum:
zNear = Mathf.Max(zNear, MinNearZ);
break;
case NearClipMode.Recommended:
zNear = Mathf.Max(zNear, RecommendedNearZ);
break;
#if DISABLE_MAGICLEAP_CLIP_ENFORCEMENT
case NearClipMode.None:
zNear = MinimumNearClip;
break;
#endif
default:
break;
}
}
public void ApplyToCamera(Camera camera, bool warnIfNearClipChanged = true)
{
if (!camera)
return;
var zFar = camera.farClipPlane;
var zNear = camera.nearClipPlane;
ApplyFarClip(ref zFar);
ApplyNearClip(ref zNear);
if (warnIfNearClipChanged && zNear > camera.nearClipPlane)
Debug.LogWarning($"Main Camera's nearClipPlane value is less than the minimum value for this device. Increasing to {zNear}");
camera.farClipPlane = zFar;
camera.nearClipPlane = zNear;
}
public long ConvertSystemTimeToXrTime(long systemTime)
{
var timeSpec = new TimeSpec
{
Seconds = systemTime / 1000000000,
NanoSeconds = systemTime % 1000000000
};
unsafe
{
var xrResult = timeSpecNativeFunctions.XrConvertTimeSpecTimeToTime(AppInstance, in timeSpec, out var xrTime);
Utils.DidXrCallSucceed(xrResult, nameof(timeSpecNativeFunctions.XrConvertTimeSpecTimeToTime));
return xrTime;
}
}
public long ConvertXrTimeToSystemTime(long xrTime)
{
unsafe
{
var xrResult = timeSpecNativeFunctions.XrConvertTimeToTimeSpecTime(AppInstance, xrTime, out var xrTimeSpec);
if (!Utils.DidXrCallSucceed(xrResult, nameof(timeSpecNativeFunctions.XrConvertTimeToTimeSpecTime)))
{
return 0;
}
return xrTimeSpec.Seconds * 1000000000 + xrTimeSpec.NanoSeconds;
}
}
}
}