using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using UnityEngine.XR.OpenXR.NativeTypes;
using UnityEngine.XR.OpenXR.Features.Extensions;
using UnityEngine.XR.OpenXR.Features.Extensions.PerformanceSettings;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.XR.OpenXR.Features;
using UnityEditor.Build;
#endif
[assembly: InternalsVisibleTo("Unity.XR.OpenXR.Tests")]
[assembly: InternalsVisibleTo("Unity.XR.OpenXR.Tests.Editor")]
namespace UnityEngine.XR.OpenXR.Features.Mock
{
#if UNITY_EDITOR
[OpenXRFeature(UiName = "Mock Runtime",
BuildTargetGroups = new[] {UnityEditor.BuildTargetGroup.Standalone, UnityEditor.BuildTargetGroup.Android},
Company = "Unity",
Desc = "Mock runtime extension for automated testing.",
DocumentationLink = Constants.k_DocumentationURL,
#if !OPENXR_USE_KHRONOS_LOADER
CustomRuntimeLoaderBuildTargets = new[] { UnityEditor.BuildTarget.StandaloneWindows64, UnityEditor.BuildTarget.StandaloneOSX, UnityEditor.BuildTarget.Android },
#endif
OpenxrExtensionStrings = MockRuntime.XR_UNITY_null_gfx + " " + XR_UNITY_android_present,
Version = "0.0.2",
FeatureId = featureId)]
#endif
///
/// OpenXR Mock Runtime
///
public class MockRuntime : OpenXRFeature
{
///
/// Script events that are possible to subscribe to.
///
public enum ScriptEvent
{
///
/// Dummy script event.
///
Unknown,
///
/// EndFrame script event.
///
EndFrame,
///
/// HapticImpulse script event.
///
HapticImpulse,
///
/// HapticStop script event.
///
HapticStop
}
///
/// Delegate invoked on ScriptEvents
///
/// ScriptEvent invoked.
/// Parameters of the script event.
public delegate void ScriptEventDelegate(ScriptEvent evt, ulong param);
///
/// Delegate invoked before function calls.
///
/// The OpenXR function name.
/// The XrResult of the callback.
public delegate XrResult BeforeFunctionDelegate(string functionName);
///
/// Delegate invoked after function calls.
///
/// The OpenXR function name.
/// The XrResult of the function.
public delegate void AfterFunctionDelegate(string functionName, XrResult result);
private static Dictionary s_AfterFunctionCallbacks = null;
private static Dictionary s_BeforeFunctionCallbacks = null;
///
/// Subscribe delegates to ScriptEvents.
///
public static event ScriptEventDelegate onScriptEvent;
///
/// The feature id string. This is used to give the feature a well known id for reference.
///
public const string featureId = "com.unity.openxr.feature.mockruntime";
///
/// Don't fail to build if there are validation errors.
///
public bool ignoreValidationErrors = false;
///
/// Return the singleton instance of the Mock Runtime feature.
///
public static MockRuntime Instance => OpenXRSettings.Instance.GetFeature();
[AOT.MonoPInvokeCallback(typeof(ScriptEventDelegate))]
private static void ReceiveScriptEvent(ScriptEvent evt, ulong param) => onScriptEvent?.Invoke(evt, param);
[AOT.MonoPInvokeCallback(typeof(BeforeFunctionDelegate))]
private static XrResult BeforeFunctionCallback(string function)
{
var callback = GetBeforeFunctionCallback(function);
if (null == callback)
return XrResult.Success;
return callback(function);
}
[AOT.MonoPInvokeCallback(typeof(BeforeFunctionDelegate))]
private static void AfterFunctionCallback(string function, XrResult result)
{
var callback = GetAfterFunctionCallback(function);
if (null == callback)
return;
callback(function, result);
}
///
/// Set the callbacks to call before and after the given OpenXR function is called within the Mock Runtime
///
/// Note that since some OpenXR functions are called from within the graphics thread that care should
/// be taken to maintain thread safety from within the callbacks.
///
/// Note that function callbacks can be set prior to the MockRuntime being initialized but will be
/// reset when the mock runtime is shutdown.
///
/// OpenXR function name
/// Callback to call before the OpenXR function is called (null to clear)
/// Callback to call after the OpenXR function is called (null to clear)
public static void SetFunctionCallback(string function, BeforeFunctionDelegate beforeCallback, AfterFunctionDelegate afterCallback)
{
if (beforeCallback != null)
{
if (null == s_BeforeFunctionCallbacks)
s_BeforeFunctionCallbacks = new Dictionary();
s_BeforeFunctionCallbacks[function] = beforeCallback;
}
else if (s_BeforeFunctionCallbacks != null)
{
s_BeforeFunctionCallbacks.Remove(function);
if (s_BeforeFunctionCallbacks.Count == 0)
s_BeforeFunctionCallbacks = null;
}
if (afterCallback != null)
{
if (null == s_AfterFunctionCallbacks)
s_AfterFunctionCallbacks = new Dictionary();
s_AfterFunctionCallbacks[function] = afterCallback;
}
else if (s_AfterFunctionCallbacks != null)
{
s_AfterFunctionCallbacks.Remove(function);
if (s_AfterFunctionCallbacks.Count == 0)
s_AfterFunctionCallbacks = null;
}
MockRuntime_RegisterFunctionCallbacks(
s_BeforeFunctionCallbacks != null ? BeforeFunctionCallback : (BeforeFunctionDelegate)null,
s_AfterFunctionCallbacks != null ? AfterFunctionCallback : (AfterFunctionDelegate)null);
}
///
/// Set a callback to call before the given OpenXR function is called within the Mock Runtime
///
/// Note that since some OpenXR functions are called from within the graphics thread that care should
/// be taken to maintain thread safety from within the callbacks.
///
/// Note that function callbacks can be set prior to the MockRuntime being initialized but will be
/// reset when the mock runtime is shutdown.
///
/// OpenXR function name
/// Callback to call before the OpenXR function is called (null to clear)
public static void SetFunctionCallback(string function, BeforeFunctionDelegate beforeCallback) =>
SetFunctionCallback(function, beforeCallback, GetAfterFunctionCallback(function));
///
/// Set a callback to call before the given OpenXR function is called within the Mock Runtime
///
/// OpenXR function name
/// Callback to call after the OpenXR function is called (null to clear)
public static void SetFunctionCallback(string function, AfterFunctionDelegate afterCallback) =>
SetFunctionCallback(function, GetBeforeFunctionCallback(function), afterCallback);
///
/// Return the callback set to be called before the given OpenXR function is called
///
/// OpenXR function name
/// Callback or null if no callback is set
public static BeforeFunctionDelegate GetBeforeFunctionCallback(string function)
{
if (null == s_BeforeFunctionCallbacks)
return null;
if (!s_BeforeFunctionCallbacks.TryGetValue(function, out var callback))
return null;
return callback;
}
///
/// Return the callback set to be called after the given OpenXR function is called
///
/// OpenXR function name
/// Callback or null if no callback is set
public static AfterFunctionDelegate GetAfterFunctionCallback(string function)
{
if (null == s_AfterFunctionCallbacks)
return null;
if (!s_AfterFunctionCallbacks.TryGetValue(function, out var callback))
return null;
return callback;
}
///
/// Remove all OpenXR function callbacks
///
public static void ClearFunctionCallbacks()
{
s_BeforeFunctionCallbacks = null;
s_AfterFunctionCallbacks = null;
MockRuntime_RegisterFunctionCallbacks(null, null);
}
///
/// Reset the MockRuntime testing settings back to defaults
///
public static void ResetDefaults()
{
#if UNITY_INCLUDE_TESTS
var instance = Instance;
instance.TestCallback = (methodName, param) => true;
#endif
onScriptEvent = null;
ClearFunctionCallbacks();
}
///
protected internal override void OnInstanceDestroy(ulong instance)
{
#if UNITY_INCLUDE_TESTS
TestCallback(MethodBase.GetCurrentMethod().Name, instance);
XrInstance = 0ul;
if (!KeepFunctionCallbacks)
{
#endif
// When the mock runtime instance shuts down we remove any callbacks that
// were set up to ensure they do not linger around for the next usage of the mock runtime.
ClearFunctionCallbacks();
#if UNITY_INCLUDE_TESTS
}
#endif
}
#if UNITY_INCLUDE_TESTS
[NonSerialized] public Func TestCallback = (methodName, param) => true;
public const string XR_UNITY_mock_test = "XR_UNITY_mock_test";
public const string XR_UNITY_null_gfx = "XR_UNITY_null_gfx";
public const string XR_UNITY_android_present = "XR_UNITY_android_present";
public ulong XrInstance { get; private set; } = 0ul;
public ulong XrSession { get; private set; } = 0ul;
private static bool s_KeepFunctionCallbacks;
internal static bool KeepFunctionCallbacks
{
get
{
return s_KeepFunctionCallbacks;
}
set
{
s_KeepFunctionCallbacks = value;
SetKeepFunctionCallbacks(value);
}
}
///
/// Return the current session state of the MockRuntime
///
public static XrSessionState sessionState => Internal_GetSessionState();
protected internal override IntPtr HookGetInstanceProcAddr(IntPtr func)
{
var ret = TestCallback(MethodBase.GetCurrentMethod().Name, func);
if (!(ret is IntPtr))
return HookCreateInstance(func);
return HookCreateInstance((IntPtr)ret);
}
protected internal override void OnSystemChange(ulong xrSystem)
{
TestCallback(MethodBase.GetCurrentMethod().Name, xrSystem);
}
protected internal override bool OnInstanceCreate(ulong xrInstance)
{
var result = (bool)TestCallback(MethodBase.GetCurrentMethod().Name, xrInstance);
if (result)
XrInstance = xrInstance;
Internal_RegisterScriptEventCallback(ReceiveScriptEvent);
return result;
}
protected internal override void OnSessionCreate(ulong xrSession)
{
XrSession = xrSession;
TestCallback(MethodBase.GetCurrentMethod().Name, xrSession);
}
protected internal override void OnSessionBegin(ulong xrSession)
{
TestCallback(MethodBase.GetCurrentMethod().Name, xrSession);
}
protected internal override void OnAppSpaceChange(ulong xrSpace)
{
TestCallback(MethodBase.GetCurrentMethod().Name, xrSpace);
}
protected internal override void OnSessionEnd(ulong xrSession)
{
TestCallback(MethodBase.GetCurrentMethod().Name, xrSession);
}
protected internal override void OnSessionDestroy(ulong session)
{
TestCallback(MethodBase.GetCurrentMethod().Name, session);
XrSession = 0ul;
}
protected internal override void OnSessionLossPending(ulong xrSession)
{
TestCallback(MethodBase.GetCurrentMethod().Name, xrSession);
}
protected internal override void OnInstanceLossPending(ulong xrInstance)
{
TestCallback(MethodBase.GetCurrentMethod().Name, xrInstance);
}
protected internal override void OnSubsystemCreate()
{
TestCallback(MethodBase.GetCurrentMethod().Name, 0);
}
protected internal override void OnSubsystemStart()
{
TestCallback(MethodBase.GetCurrentMethod().Name, 0);
}
protected internal override void OnSessionExiting(ulong xrSession)
{
TestCallback(MethodBase.GetCurrentMethod().Name, xrSession);
}
protected internal override void OnSubsystemDestroy()
{
TestCallback(MethodBase.GetCurrentMethod().Name, 0);
}
protected internal override void OnSubsystemStop()
{
TestCallback(MethodBase.GetCurrentMethod().Name, 0);
}
protected internal override void OnFormFactorChange(int xrFormFactor)
{
TestCallback(MethodBase.GetCurrentMethod().Name, xrFormFactor);
}
protected internal override void OnEnvironmentBlendModeChange(XrEnvironmentBlendMode xrEnvironmentBlendMode)
{
TestCallback(MethodBase.GetCurrentMethod().Name, xrEnvironmentBlendMode);
}
protected internal override void OnViewConfigurationTypeChange(int xrViewConfigurationType)
{
TestCallback(MethodBase.GetCurrentMethod().Name, xrViewConfigurationType);
}
internal class XrSessionStateChangedParams
{
public int OldState;
public int NewState;
}
protected internal override void OnSessionStateChange(int oldState, int newState)
{
TestCallback(MethodBase.GetCurrentMethod().Name, new XrSessionStateChangedParams() {OldState = oldState, NewState = newState});
}
public static bool TransitionToState(XrSessionState state, bool forceTransition)
{
var instance = Instance;
if (instance.XrSession == 0)
return false;
return Internal_TransitionToState(state, forceTransition);
}
public static void ChooseEnvironmentBlendMode(XrEnvironmentBlendMode mode)
{
SetEnvironmentBlendMode(mode);
}
public static XrEnvironmentBlendMode GetXrEnvironmentBlendMode()
{
return GetEnvironmentBlendMode();
}
#if UNITY_EDITOR
protected internal override void GetValidationChecks(List results, BuildTargetGroup target)
{
foreach (var res in results)
{
var check = res.checkPredicate;
res.checkPredicate = () =>
{
if (enabled && ignoreValidationErrors)
return true;
return check();
};
}
TestCallback(MethodBase.GetCurrentMethod().Name, results);
}
#endif
#endif
const string extLib = "mock_api";
///
/// Called to hook xrGetInstanceProcAddr.
///
/// xrGetInstanceProcAddr native function pointer
/// Function pointer that Unity will use to look up XR native functions.
[DllImport(extLib, EntryPoint = "MockRuntime_HookCreateInstance")]
public static extern IntPtr HookCreateInstance(IntPtr func);
///
/// Keep function callbacks when resetting MockRuntime.
///
/// True to keep callbacks.
[DllImport(extLib, EntryPoint = "MockRuntime_SetKeepFunctionCallbacks")]
public static extern void SetKeepFunctionCallbacks([MarshalAs(UnmanagedType.I1)] bool value);
///
/// Set the runtime ViewPose.
///
/// The XrViewConfigurationType to use.
/// The indexed view being set.
/// Position of the view.
/// Orientation of the view.
/// Field of View.
[DllImport(extLib, EntryPoint = "MockRuntime_SetView")]
public static extern void SetViewPose(XrViewConfigurationType viewConfigurationType, int viewIndex, Vector3 position, Quaternion orientation, Vector4 fov);
///
/// Set the runtime ViewState.
///
/// The XrViewConfigurationType to use.
/// The XrViewStateFlags to set.
[DllImport(extLib, EntryPoint = "MockRuntime_SetViewState")]
public static extern void SetViewState(XrViewConfigurationType viewConfigurationType, XrViewStateFlags viewStateFlags);
///
/// Set the reference space to use at Runtime.
///
/// The type of reference space being set.
/// Position of the space.
/// Orientation of the space.
/// XrSpaceLocationFlags for the space.
[DllImport(extLib, EntryPoint = "MockRuntime_SetReferenceSpace")]
public static extern void SetSpace(XrReferenceSpaceType referenceSpace, Vector3 position, Quaternion orientation, XrSpaceLocationFlags locationFlags);
///
/// Set the reference space to use for input actions.
///
/// Handle to the input action.
/// Position of the space.
/// Orientation of the space.
/// XrSpaceLocationFlags for the space.
[DllImport(extLib, EntryPoint = "MockRuntime_SetActionSpace")]
public static extern void SetSpace(ulong actionHandle, Vector3 position, Quaternion orientation, XrSpaceLocationFlags locationFlags);
[DllImport(extLib, EntryPoint = "MockRuntime_RegisterScriptEventCallback")]
private static extern XrResult Internal_RegisterScriptEventCallback(ScriptEventDelegate callback);
[DllImport(extLib, EntryPoint = "MockRuntime_TransitionToState")]
[return: MarshalAs(UnmanagedType.U1)]
private static extern bool Internal_TransitionToState(XrSessionState state, [MarshalAs(UnmanagedType.I1)] bool forceTransition);
[DllImport(extLib, EntryPoint = "MockRuntime_GetSessionState")]
private static extern XrSessionState Internal_GetSessionState();
///
/// Request to exit the runtime session.
///
[DllImport(extLib, EntryPoint = "MockRuntime_RequestExitSession")]
public static extern void RequestExitSession();
///
/// Force MockRuntime instance loss.
///
[DllImport(extLib, EntryPoint = "MockRuntime_CauseInstanceLoss")]
public static extern void CauseInstanceLoss();
///
/// Force user presence change.
///
/// User present when true.
[DllImport(extLib, EntryPoint = "MockRuntime_CauseUserPresenceChange")]
public static extern void CauseUserPresenceChange([MarshalAs(UnmanagedType.U1)] bool hasUserPresent);
[DllImport(extLib, EntryPoint = "MockRuntime_SetReferenceSpaceBounds")]
internal static extern void SetReferenceSpaceBounds(XrReferenceSpaceType referenceSpace, Vector2 bounds);
[DllImport(extLib, EntryPoint = "MockRuntime_GetEndFrameStats")]
internal static extern void GetEndFrameStats(out int primaryLayerCount, out int secondaryLayerCount);
[DllImport(extLib, EntryPoint = "MockRuntime_ActivateSecondaryView")]
internal static extern void ActivateSecondaryView(XrViewConfigurationType viewConfigurationType, [MarshalAs(UnmanagedType.I1)] bool activate);
[DllImport(extLib, EntryPoint = "MockRuntime_RegisterFunctionCallbacks")]
private static extern void MockRuntime_RegisterFunctionCallbacks(BeforeFunctionDelegate hookBefore, AfterFunctionDelegate hookAfter);
[DllImport(extLib, EntryPoint = "MockRuntime_MetaPerformanceMetrics_SeedCounterOnce_Float")]
internal static extern void MetaPerformanceMetrics_SeedCounterOnce_Float(string xrPathString, float value, uint unit);
[DllImport(extLib, EntryPoint = "MockRuntime_PerformanceSettings_CauseNotification")]
internal static extern void PerformanceSettings_CauseNotification(PerformanceDomain domain, PerformanceSubDomain subDomain, PerformanceNotificationLevel level);
[DllImport(extLib, EntryPoint = "MockRuntime_PerformanceSettings_GetPerformanceLevelHint")]
internal static extern PerformanceLevelHint PerformanceSettings_GetPerformanceLevelHint(PerformanceDomain domain);
#if UNITY_EDITOR
static void UseGenericLoaderAndroid()
{
#if UNITY_2021_3_OR_NEWER
var defines = PlayerSettings.GetScriptingDefineSymbols(NamedBuildTarget.Android);
#else
var defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(BuildTargetGroup.Android);
#endif
defines += ";OPENXR_USE_KHRONOS_LOADER";
#if UNITY_2021_3_OR_NEWER
PlayerSettings.SetScriptingDefineSymbols(NamedBuildTarget.Android, defines);
#else
PlayerSettings.SetScriptingDefineSymbolsForGroup(BuildTargetGroup.Android, defines);
#endif
#if UNITY_2023_1_OR_NEWER
// Use GameActivity if possible so we have test coverage there.
// No JNI on main thread when GameActivity is selected.
PlayerSettings.Android.applicationEntry = AndroidApplicationEntry.GameActivity;
PlayerSettings.Android.targetSdkVersion = AndroidSdkVersions.AndroidApiLevel33;
#endif
}
#endif
#if UNITY_ANDROID
[DllImport(extLib, EntryPoint = "MockRuntime_IsAndroidThreadTypeRegistered")]
[return: MarshalAs(UnmanagedType.U1)]
private static extern bool Internal_IsAndroidThreadTypeRegistered(uint threadType);
[DllImport(extLib, EntryPoint = "MockRuntime_GetRegisteredAndroidThreadsCount")]
private static extern ulong Internal_GetRegisteredAndroidThreadsCount();
#endif
internal static bool IsAndroidThreadTypeRegistered(uint threadType)
{
#if UNITY_ANDROID
return Internal_IsAndroidThreadTypeRegistered(threadType);
#else
return false;
#endif
}
internal static ulong GetRegisteredAndroidThreadsCount()
{
#if UNITY_ANDROID
return Internal_GetRegisteredAndroidThreadsCount();
#else
return 0;
#endif
}
}
}