version 2.5.0

This commit is contained in:
Sean Lu
2025-01-10 17:17:03 +08:00
parent ddc3c4c6d8
commit 2372c9429a
1086 changed files with 290974 additions and 77367 deletions

View File

@@ -0,0 +1,38 @@
// Copyright HTC Corporation All Rights Reserved.
using System;
using System.Runtime.InteropServices;
namespace VIVE.OpenXR.Feature
{
public interface IViveFeatureWrapper
{
public bool OnInstanceCreate(XrInstance xrInstance, IntPtr xrGetInstanceProcAddr);
public void OnInstanceDestroy();
}
public class ViveFeatureWrapperBase<T> where T : ViveFeatureWrapperBase<T>, new()
{
private static readonly Lazy<T> lazyInstance = new Lazy<T>(() => new T());
public static T Instance => lazyInstance.Value;
// Set true in yourfeature's OnInstanceCreate
public bool IsInited { get; protected set; } = false;
public OpenXRHelper.xrGetInstanceProcAddrDelegate xrGetInstanceProcAddr;
/// <summary>
/// Complete the xrGetInstanceProcAddr by set the pointer received in OnInstanceCreate
/// </summary>
/// <param name="intPtr"></param>
public void SetGetInstanceProcAddrPtr(IntPtr intPtr)
{
if (intPtr == null || intPtr == IntPtr.Zero)
throw new Exception("xrGetInstanceProcAddr is null");
xrGetInstanceProcAddr = Marshal.GetDelegateForFunctionPointer<OpenXRHelper.xrGetInstanceProcAddrDelegate>(intPtr);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a27dc5505cdb29347aeda46676cedaa8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,80 @@
using System;
using System.Runtime.InteropServices;
namespace VIVE.OpenXR
{
public static class MemoryTools
{
/// <summary>
/// Convert the enum array to IntPtr. Should call <see cref="ReleaseRawMemory(IntPtr)"/> after use.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="array"></param>
/// <returns></returns>
public static IntPtr ToIntPtr<T>(T[] array) where T : Enum
{
int size = Marshal.SizeOf(typeof(T)) * array.Length;
IntPtr ptr = Marshal.AllocHGlobal(size);
int[] intArray = new int[array.Length];
for (int i = 0; i < array.Length; i++)
intArray[i] = (int)(object)array[i];
Marshal.Copy(intArray, 0, ptr, array.Length);
return ptr;
}
/// <summary>
/// Make the same size raw buffer from input array.
/// </summary>
/// <typeparam name="T">Data type could be primitive type or struct. Should call <see cref="ReleaseRawMemory(IntPtr)"/> after use.</typeparam>
/// <param name="refArray">The data array</param>
/// <returns>The memory handle. Should release by <see cref="ReleaseRawMemory(IntPtr)"/></returns>
public static IntPtr MakeRawMemory<T>(T[] refArray)
{
int size = Marshal.SizeOf(typeof(T)) * refArray.Length;
return Marshal.AllocHGlobal(size);
}
/// <summary>
/// Copy the raw memory to the array. You should make sure the array has the same size as the raw memory.
/// </summary>
/// <typeparam name="T">Convert the memory to this type array.</typeparam>
/// <param name="array">The output array.</param>
/// <param name="raw">The data source in raw memory form.</param>
/// <param name="count">Specify the copy count. Count should be less than array length.</param>
public static void CopyFromRawMemory<T>(T[] array, IntPtr raw, int count = 0)
{
int N = array.Length;
if (count > 0 && count < array.Length)
N = count;
int step = Marshal.SizeOf(typeof(T));
for (int i = 0; i < N; i++)
{
array[i] = Marshal.PtrToStructure<T>(IntPtr.Add(raw, i * step));
}
}
/// <summary>
/// Make the same size raw buffer from input array. Make sure the raw has enough size.
/// </summary>
/// <typeparam name="T">Convert this type array to raw memory.</typeparam>
/// <param name="raw">The output data in raw memory form</param>
/// <param name="array">The data source</param>
public static void CopyToRawMemory<T>(IntPtr raw, T[] array)
{
int step = Marshal.SizeOf(typeof(T));
for (int i = 0; i < array.Length; i++)
{
Marshal.StructureToPtr<T>(array[i], IntPtr.Add(raw, i * step), false);
}
}
/// <summary>
/// Release the raw memory handle which is created by <see cref="MakeRawMemory{T}(T[])"/>
/// </summary>
/// <param name="ptr"></param>
public static void ReleaseRawMemory(IntPtr ptr)
{
Marshal.FreeHGlobal(ptr);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9a887cb158a37cf45b17458a4f27d7ee
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -3,6 +3,7 @@
using System;
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.Profiling;
namespace VIVE.OpenXR.Feature
{
@@ -10,23 +11,16 @@ namespace VIVE.OpenXR.Feature
/// To use this wrapper, you need to call CommonWrapper.Instance.OnInstanceCreate() in your feature's OnInstanceCreate(),
/// and call CommonWrapper.Instance.OnInstanceDestroy() in your feature's OnInstanceDestroy().
/// </summary>
public class CommonWrapper
public class CommonWrapper : ViveFeatureWrapperBase<CommonWrapper>, IViveFeatureWrapper
{
static CommonWrapper instance = null;
public static CommonWrapper Instance
{
get
{
if (instance == null)
instance = new CommonWrapper();
return instance;
}
}
bool isInited = false;
OpenXRHelper.xrGetInstanceProcAddrDelegate XrGetInstanceProcAddr;
OpenXRHelper.xrGetSystemPropertiesDelegate XrGetSystemProperties;
OpenXRHelper.xrCreateSwapchainDelegate XrCreateSwapchain;
OpenXRHelper.xrDestroySwapchainDelegate XrDestroySwapchain;
OpenXRHelper.xrEnumerateSwapchainFormatsDelegate XrEnumerateSwapchainFormats;
OpenXRHelper.xrEnumerateSwapchainImagesDelegate XrEnumerateSwapchainImages;
OpenXRHelper.xrWaitSwapchainImageDelegate XrWaitSwapchainImage;
OpenXRHelper.xrAcquireSwapchainImageDelegate XrAcquireSwapchainImage;
OpenXRHelper.xrReleaseSwapchainImageDelegate XrReleaseSwapchainImage;
/// <summary>
/// In feature's OnInstanceCreate(), call CommonWrapper.Instance.OnInstanceCreate() for init common APIs.
@@ -35,32 +29,32 @@ namespace VIVE.OpenXR.Feature
/// <param name="xrGetInstanceProcAddr">Pass OpenXRFeature.xrGetInstanceProcAddr in.</param>
/// <returns></returns>
/// <exception cref="Exception">If input data not valid.</exception>
public bool OnInstanceCreate(XrInstance xrInstance, IntPtr xrGetInstanceProcAddr)
public bool OnInstanceCreate(XrInstance xrInstance, IntPtr xrGetInstanceProcAddrPtr)
{
if (isInited) return true;
if (IsInited) return true;
if (xrInstance == 0)
throw new Exception("CommonWrapper: xrInstance is null");
Debug.Log("CommonWrapper: OnInstanceCreate()");
/// OpenXRFeature.xrGetInstanceProcAddr
if (xrGetInstanceProcAddr == null || xrGetInstanceProcAddr == IntPtr.Zero)
throw new Exception("CommonWrapper: xrGetInstanceProcAddr is null");
Debug.Log("CommonWrapper: Get function pointer of xrGetInstanceProcAddr.");
XrGetInstanceProcAddr = Marshal.GetDelegateForFunctionPointer(
xrGetInstanceProcAddr,
typeof(OpenXRHelper.xrGetInstanceProcAddrDelegate)) as OpenXRHelper.xrGetInstanceProcAddrDelegate;
SetGetInstanceProcAddrPtr(xrGetInstanceProcAddrPtr);
bool ret = true;
IntPtr funcPtr = IntPtr.Zero;
ret &= OpenXRHelper.GetXrFunctionDelegate(XrGetInstanceProcAddr, xrInstance, "xrGetSystemProperties", out XrGetSystemProperties);
ret &= OpenXRHelper.GetXrFunctionDelegate(xrGetInstanceProcAddr, xrInstance, "xrGetSystemProperties", out XrGetSystemProperties);
ret &= OpenXRHelper.GetXrFunctionDelegate(xrGetInstanceProcAddr, xrInstance, "xrCreateSwapchain", out XrCreateSwapchain);
ret &= OpenXRHelper.GetXrFunctionDelegate(xrGetInstanceProcAddr, xrInstance, "xrDestroySwapchain", out XrDestroySwapchain);
ret &= OpenXRHelper.GetXrFunctionDelegate(xrGetInstanceProcAddr, xrInstance, "xrEnumerateSwapchainFormats", out XrEnumerateSwapchainFormats);
ret &= OpenXRHelper.GetXrFunctionDelegate(xrGetInstanceProcAddr, xrInstance, "xrEnumerateSwapchainImages", out XrEnumerateSwapchainImages);
ret &= OpenXRHelper.GetXrFunctionDelegate(xrGetInstanceProcAddr, xrInstance, "xrWaitSwapchainImage", out XrWaitSwapchainImage);
ret &= OpenXRHelper.GetXrFunctionDelegate(xrGetInstanceProcAddr, xrInstance, "xrAcquireSwapchainImage", out XrAcquireSwapchainImage);
ret &= OpenXRHelper.GetXrFunctionDelegate(xrGetInstanceProcAddr, xrInstance, "xrReleaseSwapchainImage", out XrReleaseSwapchainImage);
if (!ret)
throw new Exception("CommonWrapper: Get function pointers failed.");
isInited = ret;
IsInited = ret;
return ret;
}
@@ -70,21 +64,20 @@ namespace VIVE.OpenXR.Feature
/// <returns></returns>
public void OnInstanceDestroy()
{
isInited = false;
XrGetInstanceProcAddr = null;
IsInited = false;
XrGetSystemProperties = null;
Debug.Log("CommonWrapper: OnInstanceDestroy()");
}
public XrResult GetInstanceProcAddr(XrInstance instance, string name, out IntPtr function)
{
if (isInited == false || XrGetInstanceProcAddr == null)
if (IsInited == false || xrGetInstanceProcAddr == null)
{
function = IntPtr.Zero;
return XrResult.XR_ERROR_HANDLE_INVALID;
}
return XrGetInstanceProcAddr(instance, name, out function);
return xrGetInstanceProcAddr(instance, name, out function);
}
/// <summary>
@@ -97,7 +90,7 @@ namespace VIVE.OpenXR.Feature
/// <returns></returns>
public XrResult GetSystemProperties(XrInstance instance, XrSystemId systemId, ref XrSystemProperties properties)
{
if (isInited == false || XrGetSystemProperties == null)
if (IsInited == false || XrGetSystemProperties == null)
{
return XrResult.XR_ERROR_HANDLE_INVALID;
}
@@ -136,5 +129,115 @@ namespace VIVE.OpenXR.Feature
Marshal.FreeHGlobal(systemProperties.next);
return ret;
}
public XrResult CreateSwapchain(XrSession session, ref XrSwapchainCreateInfo createInfo, out XrSwapchain swapchain)
{
if (IsInited == false || XrCreateSwapchain == null)
{
swapchain = default;
return XrResult.XR_ERROR_HANDLE_INVALID;
}
return XrCreateSwapchain(session, ref createInfo, out swapchain);
}
public XrResult DestroySwapchain(XrSwapchain swapchain)
{
if (IsInited == false || XrDestroySwapchain == null)
{
return XrResult.XR_ERROR_HANDLE_INVALID;
}
return XrDestroySwapchain(swapchain);
}
public XrResult EnumerateSwapchainFormats(XrSession session, uint formatCapacityInput, ref uint formatCountOutput, ref long[] formats)
{
if (IsInited == false || XrEnumerateSwapchainFormats == null)
{
formatCountOutput = 0;
return XrResult.XR_ERROR_HANDLE_INVALID;
}
if (formatCapacityInput != 0 && (formats == null || formats.Length < formatCapacityInput))
return XrResult.XR_ERROR_SIZE_INSUFFICIENT;
if (formatCapacityInput == 0)
{
Debug.Log("CommonWrapper: EnumerateSwapchainFormats(ci=" + formatCapacityInput + ")");
return XrEnumerateSwapchainFormats(session, 0, ref formatCountOutput, IntPtr.Zero);
}
else
{
Debug.Log("CommonWrapper: EnumerateSwapchainFormats(ci=" + formatCapacityInput + ", formats=long[" + formats.Length + "])");
IntPtr formatsPtr = MemoryTools.MakeRawMemory(formats);
var ret = XrEnumerateSwapchainFormats(session, formatCapacityInput, ref formatCountOutput, formatsPtr);
if (ret == XrResult.XR_SUCCESS)
MemoryTools.CopyFromRawMemory(formats, formatsPtr, (int)formatCountOutput);
MemoryTools.ReleaseRawMemory(formatsPtr);
return ret;
}
}
public XrResult EnumerateSwapchainImages(XrSwapchain swapchain, uint imageCapacityInput, ref uint imageCountOutput, IntPtr imagesPtr)
{
if (IsInited == false || XrEnumerateSwapchainImages == null)
{
imageCountOutput = 0;
return XrResult.XR_ERROR_HANDLE_INVALID;
}
return XrEnumerateSwapchainImages(swapchain, imageCapacityInput, ref imageCountOutput, imagesPtr);
}
[DllImport("viveopenxr", EntryPoint = "CwAcquireSwapchainImage")]
public static extern XrResult CwAcquireSwapchainImage(XrSwapchain swapchain, ref XrSwapchainImageAcquireInfo acquireInfo, out uint index);
public XrResult AcquireSwapchainImage(XrSwapchain swapchain, ref XrSwapchainImageAcquireInfo acquireInfo, out uint index)
{
if (IsInited == false || XrAcquireSwapchainImage == null)
{
index = 0;
return XrResult.XR_ERROR_HANDLE_INVALID;
}
Profiler.BeginSample("ASW: xrAcqScImg");
var res = XrAcquireSwapchainImage(swapchain, ref acquireInfo, out index);
Profiler.EndSample();
return res;
}
[DllImport("viveopenxr", EntryPoint = "CwWaitSwapchainImage")]
public static extern XrResult CwWaitSwapchainImage(XrSwapchain swapchain, ref XrSwapchainImageWaitInfo waitInfo);
public XrResult WaitSwapchainImage(XrSwapchain swapchain, ref XrSwapchainImageWaitInfo waitInfo)
{
if (IsInited == false || XrWaitSwapchainImage == null)
{
return XrResult.XR_ERROR_HANDLE_INVALID;
}
Profiler.BeginSample("ASW: xrWaitScImg");
var res = XrWaitSwapchainImage(swapchain, ref waitInfo);
Profiler.EndSample();
return res;
}
[DllImport("viveopenxr", EntryPoint = "CwReleaseSwapchainImage")]
public static extern XrResult CwReleaseSwapchainImage(XrSwapchain swapchain, ref XrSwapchainImageReleaseInfo releaseInfo);
public XrResult ReleaseSwapchainImage(XrSwapchain swapchain, ref XrSwapchainImageReleaseInfo releaseInfo)
{
if (IsInited == false || XrReleaseSwapchainImage == null)
{
return XrResult.XR_ERROR_HANDLE_INVALID;
}
// Add Profiler
Profiler.BeginSample("ASW: xrRelScImg");
var res = XrReleaseSwapchainImage(swapchain, ref releaseInfo);
Profiler.EndSample();
return res;
}
}
}

View File

@@ -0,0 +1,207 @@
// Copyright HTC Corporation All Rights Reserved.
using System;
using UnityEngine;
using UnityEngine.XR.OpenXR;
namespace VIVE.OpenXR.Feature
{
using XrFutureEXT = System.IntPtr;
/// <summary>
/// To use this wrapper,
/// 1. Add the "XR_EXT_Future" extension to the instance's enabled extensions list.
/// 2. Call FutureWrapper.Instance.OnInstanceCreate() in your feature's OnInstanceCreate().
/// 3. Call FutureWrapper.Instance.OnInstanceDestroy() in your feature's OnInstanceDestroy().
///
/// <see cref="VIVE.OpenXR.Toolkits.FutureTask.Poll"/> function helps make async Task.
/// </summary>
public class FutureWrapper : ViveFeatureWrapperBase<FutureWrapper>, IViveFeatureWrapper
{
public enum XrFutureStateEXT
{
None = 0, // Not defined in extension. A default value.
Pending = 1,
Ready = 2,
MAX = 0x7FFFFFFF
}
public struct XrFuturePollInfoEXT {
public XrStructureType type; // XR_TYPE_FUTURE_POLL_INFO_EXT
public IntPtr next;
public XrFutureEXT future;
}
public struct XrFuturePollResultEXT {
public XrStructureType type; // XR_TYPE_FUTURE_POLL_RESULT_EXT
public IntPtr next;
public XrFutureStateEXT state;
}
public struct XrFutureCancelInfoEXT
{
public XrStructureType type; // XR_TYPE_FUTURE_CANCEL_INFO_EXT
public IntPtr next;
public XrFutureEXT future;
}
public struct XrFutureCompletionBaseHeaderEXT
{
public XrStructureType type; // XR_TYPE_FUTURE_COMPLETION_EXT
public IntPtr next;
public XrResult futureResult;
}
public struct XrFutureCompletionEXT
{
public XrStructureType type; // XR_TYPE_FUTURE_COMPLETION_EXT
public IntPtr next;
public XrResult futureResult;
}
public delegate XrResult XrPollFutureEXTDelegate(XrInstance instance, ref XrFuturePollInfoEXT pollInfo, out XrFuturePollResultEXT pollResult);
public delegate XrResult XrCancelFutureEXTDelegate(XrInstance instance, ref XrFutureCancelInfoEXT cancelInfo);
XrPollFutureEXTDelegate XrPollFutureEXT;
XrCancelFutureEXTDelegate XrCancelFutureEXT;
XrInstance xrInstance;
/// <summary>
/// Features should call FutureWrapper.Instance.OnInstanceCreate() in their OnInstanceCreate().
/// </summary>
/// <param name="xrInstance"></param>
/// <param name="xrGetInstanceProcAddrPtr"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public bool OnInstanceCreate(XrInstance xrInstance, IntPtr xrGetInstanceProcAddrPtr)
{
if (IsInited) return true;
if (xrInstance == null)
throw new Exception("FutureWrapper: xrInstance is null");
this.xrInstance = xrInstance;
if (xrGetInstanceProcAddrPtr == null)
throw new Exception("FutureWrapper: xrGetInstanceProcAddr is null");
SetGetInstanceProcAddrPtr(xrGetInstanceProcAddrPtr);
Debug.Log("FutureWrapper: OnInstanceCreate()");
bool hasFuture = OpenXRRuntime.IsExtensionEnabled("XR_EXT_future");
if (!hasFuture)
{
Debug.LogError("FutureWrapper: XR_EXT_future is not enabled. Check your feature's kOpenxrExtensionString.");
return false;
}
bool ret = true;
IntPtr funcPtr = IntPtr.Zero;
ret &= OpenXRHelper.GetXrFunctionDelegate(xrGetInstanceProcAddr, xrInstance, "xrPollFutureEXT", out XrPollFutureEXT);
ret &= OpenXRHelper.GetXrFunctionDelegate(xrGetInstanceProcAddr, xrInstance, "xrCancelFutureEXT", out XrCancelFutureEXT);
if (!ret)
{
Debug.LogError("FutureWrapper: Failed to get function pointer.");
return false;
}
IsInited = ret;
return ret;
}
public void OnInstanceDestroy()
{
Debug.Log("FutureWrapper: OnInstanceDestroy()");
IsInited = false;
XrPollFutureEXT = null;
XrCancelFutureEXT = null;
xrInstance = 0;
}
/// <summary>
/// Used to get the state of a future. If Ready, Call complete functions to get the result.
/// </summary>
/// <param name="pollInfo"></param>
/// <param name="pollResult"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public XrResult PollFuture(ref XrFuturePollInfoEXT pollInfo, out XrFuturePollResultEXT pollResult)
{
pollResult= new XrFuturePollResultEXT()
{
type = XrStructureType.XR_TYPE_FUTURE_POLL_RESULT_EXT,
next = IntPtr.Zero,
state = XrFutureStateEXT.None
};
if (!IsInited)
return XrResult.XR_ERROR_HANDLE_INVALID;
return XrPollFutureEXT(xrInstance, ref pollInfo, out pollResult);
}
/// <summary>
/// Used to get the state of a future. If Ready, Call complete functions to get the result.
/// </summary>
/// <param name="future"></param>
/// <param name="pollResult"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public XrResult PollFuture(XrFutureEXT future, out XrFuturePollResultEXT pollResult)
{
pollResult = new XrFuturePollResultEXT()
{
type = XrStructureType.XR_TYPE_FUTURE_POLL_RESULT_EXT,
next = IntPtr.Zero,
state = XrFutureStateEXT.None
};
if (!IsInited)
return XrResult.XR_ERROR_HANDLE_INVALID;
XrFuturePollInfoEXT pollInfo = new XrFuturePollInfoEXT()
{
type = XrStructureType.XR_TYPE_FUTURE_POLL_INFO_EXT,
next = IntPtr.Zero,
future = future
};
return XrPollFutureEXT(xrInstance, ref pollInfo, out pollResult);
}
/// <summary>
/// This function cancels the future and signals that the async operation is not required.
/// After a future has been cancelled any functions using this future must return XR_ERROR_FUTURE_INVALID_EXT.
/// </summary>
/// <param name="cancelInfo"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public XrResult CancelFuture(ref XrFutureCancelInfoEXT cancelInfo)
{
if (!IsInited)
return XrResult.XR_ERROR_HANDLE_INVALID;
return XrCancelFutureEXT(xrInstance, ref cancelInfo);
}
/// <summary>
/// <see cref="CancelFuture(ref XrFutureCancelInfoEXT)"/>
/// </summary>
/// <param name="future"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public XrResult CancelFuture(XrFutureEXT future)
{
if (!IsInited)
return XrResult.XR_ERROR_HANDLE_INVALID;
XrFutureCancelInfoEXT cancelInfo = new XrFutureCancelInfoEXT()
{
type = XrStructureType.XR_TYPE_FUTURE_CANCEL_INFO_EXT,
next = IntPtr.Zero,
future = future
};
return XrCancelFutureEXT(xrInstance, ref cancelInfo);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e8522c7af0a4127409a8800e1ddd5985
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,10 +1,5 @@
// Copyright HTC Corporation All Rights Reserved.
// Remove FAKE_DATA if editor or windows is supported.
#if UNITY_EDITOR
#define FAKE_DATA
#endif
using System;
using UnityEngine;
@@ -15,21 +10,8 @@ namespace VIVE.OpenXR.Feature
/// To use this wrapper, you need to call CommonWrapper.Instance.OnInstanceCreate() in your feature's OnInstanceCreate(),
/// and call CommonWrapper.Instance.OnInstanceDestroy() in your feature's OnInstanceDestroy().
/// </summary>
public class SpaceWrapper
public class SpaceWrapper : ViveFeatureWrapperBase<SpaceWrapper>, IViveFeatureWrapper
{
static SpaceWrapper instance = null;
public static SpaceWrapper Instance
{
get
{
if (instance == null)
instance = new SpaceWrapper();
return instance;
}
}
bool isInited = false;
delegate XrResult DelegateXrLocateSpace(XrSpace space, XrSpace baseSpace, XrTime time, ref XrSpaceLocation location);
delegate XrResult DelegateXrDestroySpace(XrSpace space);
@@ -44,31 +26,30 @@ namespace VIVE.OpenXR.Feature
/// <param name="GetAddr"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public bool OnInstanceCreate(XrInstance xrInstance, OpenXRHelper.xrGetInstanceProcAddrDelegate GetAddr)
public bool OnInstanceCreate(XrInstance xrInstance, IntPtr GetAddr)
{
if (isInited) return true;
if (IsInited) return true;
if (xrInstance == null)
throw new Exception("ViveSpace: xrInstance is null");
if (GetAddr == null)
throw new Exception("ViveSpace: xrGetInstanceProcAddr is null");
SetGetInstanceProcAddrPtr(GetAddr);
Debug.Log("ViveSpace: OnInstanceCreate()");
bool ret = true;
IntPtr funcPtr = IntPtr.Zero;
ret &= OpenXRHelper.GetXrFunctionDelegate(GetAddr, xrInstance, "xrCreateReferenceSpace", out XrCreateReferenceSpace);
ret &= OpenXRHelper.GetXrFunctionDelegate(GetAddr, xrInstance, "xrLocateSpace", out XrLocateSpace);
ret &= OpenXRHelper.GetXrFunctionDelegate(GetAddr, xrInstance, "xrDestroySpace", out XrDestroySpace);
isInited = ret;
ret &= OpenXRHelper.GetXrFunctionDelegate(xrGetInstanceProcAddr, xrInstance, "xrCreateReferenceSpace", out XrCreateReferenceSpace);
ret &= OpenXRHelper.GetXrFunctionDelegate(xrGetInstanceProcAddr, xrInstance, "xrLocateSpace", out XrLocateSpace);
ret &= OpenXRHelper.GetXrFunctionDelegate(xrGetInstanceProcAddr, xrInstance, "xrDestroySpace", out XrDestroySpace);
IsInited = ret;
return ret;
}
public void OnInstanceDestroy()
{
isInited = false;
IsInited = false;
XrCreateReferenceSpace = null;
XrLocateSpace = null;
XrDestroySpace = null;
@@ -77,8 +58,8 @@ namespace VIVE.OpenXR.Feature
/// <summary>
/// Create a reference space without create info.
/// Example:
/// CreateReferenceSpace(session, XrReferenceSpaceType.XR_REFERENCE_SPACE_TYPE_LOCAL, XrPosef.identity, out space);
/// CreateReferenceSpace(session, XrReferenceSpaceType.XR_REFERENCE_SPACE_TYPE_STAGE, XrPosef.identity, out space);
/// CreateReferenceSpace(session, XrReferenceSpaceType.XR_REFERENCE_SPACE_TYPE_LOCAL, XrPosef.Identity, out space);
/// CreateReferenceSpace(session, XrReferenceSpaceType.XR_REFERENCE_SPACE_TYPE_STAGE, XrPosef.Identity, out space);
/// </summary>
/// <param name="session"></param>
/// <param name="referenceSpaceType"></param>
@@ -87,8 +68,9 @@ namespace VIVE.OpenXR.Feature
/// <returns></returns>
public XrResult CreateReferenceSpace(XrSession session, XrReferenceSpaceType referenceSpaceType, XrPosef pose, out XrSpace space)
{
if (!isInited)
throw new Exception("ViveSpace: not initialized");
space = 0;
if (!IsInited)
return XrResult.XR_ERROR_HANDLE_INVALID;
var createInfo = new XrReferenceSpaceCreateInfo();
createInfo.type = XrStructureType.XR_TYPE_REFERENCE_SPACE_CREATE_INFO;
@@ -107,24 +89,25 @@ namespace VIVE.OpenXR.Feature
/// <returns></returns>
public XrResult CreateReferenceSpace(XrSession session, XrReferenceSpaceCreateInfo createInfo, out XrSpace space)
{
if (!isInited)
throw new Exception("ViveSpace: not initialized");
space = 0;
if (!IsInited)
return XrResult.XR_ERROR_HANDLE_INVALID;
return XrCreateReferenceSpace(session, ref createInfo, out space);
}
public XrResult LocateSpace(XrSpace space, XrSpace baseSpace, XrTime time, ref XrSpaceLocation location)
{
if (!isInited)
throw new Exception("ViveSpace: not initialized");
Debug.Log($"LocateSpace(s={space}, bs={baseSpace}, t={time}");
if (!IsInited)
return XrResult.XR_ERROR_HANDLE_INVALID;
//Debug.Log($"LocateSpace(s={space}, bs={baseSpace}, t={time}");
return XrLocateSpace(space, baseSpace, time, ref location);
}
public XrResult DestroySpace(XrSpace space)
{
if (!isInited)
throw new Exception("ViveSpace: not initialized");
if (!IsInited)
return XrResult.XR_ERROR_HANDLE_INVALID;
Debug.Log($"DestroySpace({space})");
return XrDestroySpace(space);
}
@@ -157,19 +140,6 @@ namespace VIVE.OpenXR.Feature
public bool GetRelatedPose(XrSpace baseSpace, XrTime time, out UnityEngine.Pose pose)
{
#if FAKE_DATA
if (Application.isEditor)
{
// make a random Pose
//var pos = new Vector3(UnityEngine.Random.Range(-1f, 1f), UnityEngine.Random.Range(-1f, 1f), UnityEngine.Random.Range(-1f, 1f));
//var rot = new Quaternion(UnityEngine.Random.Range(-1f, 1f), UnityEngine.Random.Range(-1f, 1f), UnityEngine.Random.Range(-1f, 1f), UnityEngine.Random.Range(-1f, 1f));
var pos = Vector3.up;
var rot = Quaternion.identity;
rot.Normalize();
pose = new Pose(pos, rot);
return true;
}
#endif
// If the xrBaseSpace is changed, the pose will be updated.
pose = default;
XrSpaceLocation location = new XrSpaceLocation();
@@ -179,14 +149,14 @@ namespace VIVE.OpenXR.Feature
if (ret != XrResult.XR_SUCCESS)
{
Debug.Log("Space: LocateSpace ret=" + ret);
//Debug.Log("Space: LocateSpace ret=" + ret);
return false;
}
Debug.Log("Space: baseSpace=" + baseSpace + ", space=" + space + ", time=" + time + ", ret=" + ret);
Debug.Log("Space: location.locationFlags=" + location.locationFlags);
Debug.Log("Space: location.pose.position=" + location.pose.position.x + "," + location.pose.position.y + "," + location.pose.position.z);
Debug.Log("Space: location.pose.orientation=" + location.pose.orientation.x + "," + location.pose.orientation.y + "," + location.pose.orientation.z + "," + location.pose.orientation.w);
//Debug.Log("Space: baseSpace=" + baseSpace + ", space=" + space + ", time=" + time + ", ret=" + ret);
//Debug.Log("Space: location.locationFlags=" + location.locationFlags);
//Debug.Log("Space: location.pose.position=" + location.pose.position.x + "," + location.pose.position.y + "," + location.pose.position.z);
//Debug.Log("Space: location.pose.orientation=" + location.pose.orientation.x + "," + location.pose.orientation.y + "," + location.pose.orientation.z + "," + location.pose.orientation.w);
if ((location.locationFlags & XrSpaceLocationFlags.XR_SPACE_LOCATION_POSITION_VALID_BIT) > 0 &&
(location.locationFlags & XrSpaceLocationFlags.XR_SPACE_LOCATION_ORIENTATION_VALID_BIT) > 0)
{
@@ -211,7 +181,7 @@ namespace VIVE.OpenXR.Feature
// Managered resource
}
// Non managered resource
Debug.Log($"Space: DestroySpace({space})");
//Debug.Log($"Space: DestroySpace({space})");
SpaceWrapper.Instance.DestroySpace(space);
space = 0;
disposed = true;

View File

@@ -3,81 +3,180 @@ using System.Runtime.InteropServices;
using System;
using UnityEngine;
using AOT;
using System.Collections.Generic;
using System.Text;
namespace VIVE.OpenXR
{
/// <summary>
/// This class is made for all features that need to intercept OpenXR API calls.
/// Some APIs will be called by Unity internally, and we need to intercept them in c# to get some information.
/// Append more interceptable functions for this class by adding a new partial class.
/// The partial class can help the delegate name be nice to read and search.
/// Please create per function in one partial class.
///
/// For all features want to use this class, please call <see cref="HookGetInstanceProcAddr" /> in your feature class.
/// For example:
/// protected override IntPtr HookGetInstanceProcAddr(IntPtr func)
/// {
/// return HtcInterceptors.Instance.HookGetInstanceProcAddr(func);
/// }
/// </summary>
/// <summary>
/// This class is made for all features that need to intercept OpenXR API calls.
/// Some APIs will be called by Unity internally, and we need to intercept them in c# to get some information.
/// Append more interceptable functions for this class by adding a new partial class.
/// The partial class can help the delegate name be nice to read and search.
/// Please create per function in one partial class.
///
/// For all features want to use this class, please call <see cref="HookGetInstanceProcAddr" /> in your feature class.
/// For example:
/// protected override IntPtr HookGetInstanceProcAddr(IntPtr func)
/// {
/// return ViveInterceptors.Instance.HookGetInstanceProcAddr(func);
/// }
/// </summary>
partial class ViveInterceptors
{
public const string TAG = "Interceptors";
public static ViveInterceptors instance = null;
public static ViveInterceptors Instance
{
get
{
if (instance == null)
instance = new ViveInterceptors();
return instance;
public const string TAG = "VIVE.OpenXR.ViveInterceptors";
static StringBuilder m_sb = null;
static StringBuilder sb {
get {
if (m_sb == null) { m_sb = new StringBuilder(); }
return m_sb;
}
}
static void DEBUG(StringBuilder msg) { Debug.LogFormat("{0} {1}", TAG, msg); }
static void ERROR(StringBuilder msg) { Debug.LogErrorFormat("{0} {1}", TAG, msg); }
public ViveInterceptors()
{
Debug.Log("HtcInterceptors");
}
public static ViveInterceptors instance = null;
public static ViveInterceptors Instance
{
get
{
if (instance == null)
instance = new ViveInterceptors();
return instance;
}
}
bool isInited = false;
public ViveInterceptors()
{
Debug.Log("ViveInterceptors");
}
public delegate XrResult DelegateXrGetInstanceProcAddr(XrInstance instance, string name, out IntPtr function);
private static readonly DelegateXrGetInstanceProcAddr hookXrGetInstanceProcAddrHandle = new DelegateXrGetInstanceProcAddr(XrGetInstanceProcAddrInterceptor);
private static readonly IntPtr hookGetInstanceProcAddrHandlePtr = Marshal.GetFunctionPointerForDelegate(hookXrGetInstanceProcAddrHandle);
static DelegateXrGetInstanceProcAddr XrGetInstanceProcAddrOriginal = null;
public delegate XrResult DelegateXrGetInstanceProcAddr(XrInstance instance, string name, out IntPtr function);
private static readonly DelegateXrGetInstanceProcAddr hookXrGetInstanceProcAddrHandle = new DelegateXrGetInstanceProcAddr(XrGetInstanceProcAddrInterceptor);
private static readonly IntPtr hookGetInstanceProcAddrHandlePtr = Marshal.GetFunctionPointerForDelegate(hookXrGetInstanceProcAddrHandle);
static DelegateXrGetInstanceProcAddr XrGetInstanceProcAddrOriginal = null;
[MonoPInvokeCallback(typeof(DelegateXrGetInstanceProcAddr))]
private static XrResult XrGetInstanceProcAddrInterceptor(XrInstance instance, string name, out IntPtr function)
{
// Custom interceptors
if (name == "xrWaitFrame")
{
Debug.Log($"{TAG}: XrGetInstanceProcAddrInterceptor() {name} is intercepted.");
var ret = XrGetInstanceProcAddrOriginal(instance, name, out function);
if (ret == XrResult.XR_SUCCESS)
{
XrWaitFrameOriginal = Marshal.GetDelegateForFunctionPointer<DelegateXrWaitFrame>(function);
function = xrWaitFrameInterceptorPtr;
}
return ret;
}
return XrGetInstanceProcAddrOriginal(instance, name, out function);
}
[MonoPInvokeCallback(typeof(DelegateXrGetInstanceProcAddr))]
private static XrResult XrGetInstanceProcAddrInterceptor(XrInstance instance, string name, out IntPtr function)
{
// Used to check if the original function is already hooked.
if (instance == 0 && name == "ViveInterceptorHooked")
{
function = IntPtr.Zero;
return XrResult.XR_SUCCESS;
}
// Custom interceptors
if (name == "xrWaitFrame" && requiredFunctions.Contains(name))
{
Debug.Log($"{TAG}: XrGetInstanceProcAddrInterceptor() {name} is intercepted.");
var ret = XrGetInstanceProcAddrOriginal(instance, name, out function);
if (ret == XrResult.XR_SUCCESS)
{
XrWaitFrameOriginal = Marshal.GetDelegateForFunctionPointer<DelegateXrWaitFrame>(function);
function = xrWaitFrameInterceptorPtr;
}
return ret;
}
if (name == "xrEndFrame" && requiredFunctions.Contains(name))
{
Debug.Log($"{TAG}: XrGetInstanceProcAddrInterceptor() {name} is intercepted.");
var ret = XrGetInstanceProcAddrOriginal(instance, name, out function);
if (ret == XrResult.XR_SUCCESS)
{
XrEndFrameOriginal = Marshal.GetDelegateForFunctionPointer<DelegateXrEndFrame>(function);
function = xrEndFrameInterceptorPtr;
}
return ret;
}
#if PERFORMANCE_TEST
if (name == "xrLocateSpace" && requiredFunctions.Contains(name))
{
Debug.Log($"{TAG}: XrGetInstanceProcAddrInterceptor() {name} is intercepted.");
var ret = XrGetInstanceProcAddrOriginal(instance, name, out function);
if (ret == XrResult.XR_SUCCESS)
{
XrLocateSpaceOriginal = Marshal.GetDelegateForFunctionPointer<DelegateXrLocateSpace>(function);
function = xrLocateSpaceInterceptorPtr;
}
return ret;
}
#endif
if (name == "xrPollEvent" && requiredFunctions.Contains(name))
{
Debug.Log($"{TAG}: XrGetInstanceProcAddrInterceptor() {name} is intercepted.");
var ret = XrGetInstanceProcAddrOriginal(instance, name, out function);
if (ret == XrResult.XR_SUCCESS)
{
xrPollEventOrigin = Marshal.GetDelegateForFunctionPointer < xrPollEventDelegate > (function);
function = xrPollEventPtr;
}
return ret;
}
if (name == "xrBeginSession" && requiredFunctions.Contains(name))
{
Debug.Log($"{TAG}: XrGetInstanceProcAddrInterceptor() {name} is intercepted.");
var ret = XrGetInstanceProcAddrOriginal(instance, name, out function);
if (ret == XrResult.XR_SUCCESS)
{
xrBeginSessionOrigin = Marshal.GetDelegateForFunctionPointer<xrBeginSessionDelegate>(function);
function = xrBeginSessionPtr;
}
return ret;
}
return XrGetInstanceProcAddrOriginal(instance, name, out function);
}
public IntPtr HookGetInstanceProcAddr(IntPtr func)
{
Debug.Log($"{TAG}: HookGetInstanceProcAddr");
if (XrGetInstanceProcAddrOriginal == null)
{
Debug.Log($"{TAG}: registering our own xrGetInstanceProcAddr");
XrGetInstanceProcAddrOriginal = Marshal.GetDelegateForFunctionPointer<DelegateXrGetInstanceProcAddr>(func);
#if UNITY_EDITOR
if (Application.isEditor) {
// This is a trick to check if the original function is already hooked by this class. Sometimes, the static XrGetInstanceProcAddrOriginal didn't work as expected.
Debug.Log($"{TAG}: Check if duplicate hooked by this script with instance=0 and \"ViveInterceptorHooked\" name. If following a loader error, ignore it.");
// E OpenXR-Loader: Error [SPEC | xrGetInstanceProcAddr | VUID-xrGetInstanceProcAddr-instance-parameter] : XR_NULL_HANDLE for instance but query for ViveInterceptorHooked requires a valid instance
// Call XrGetInstanceProcAddrOriginal to check if the original function is already hooked by this class
if (XrGetInstanceProcAddrOriginal(0, "ViveInterceptorHooked", out IntPtr function) == XrResult.XR_SUCCESS)
{
// If it is called successfully, it means the original function is already hooked. So we should return the original function.
Debug.Log($"{TAG}: Already hooked");
return func;
}
}
#endif
return hookGetInstanceProcAddrHandlePtr;
}
else
{
// Dont return hookGetInstanceProcAddrHandlePtr again.
// If this hook function is called by multiple features, it should only work at the first time.
// If called by other features, it should return the original function.
return func;
}
}
static readonly List<string> requiredFunctions = new List<string>();
/// <summary>
/// Call before <see cref="HookGetInstanceProcAddr" /> to add required functions."/>
/// </summary>
/// <param name="name"></param>
public void AddRequiredFunction(string name)
{
if (requiredFunctions.Contains(name)) return;
Debug.Log($"{TAG}: AddRequiredFunction({name})");
requiredFunctions.Add(name);
}
}
}
public IntPtr HookGetInstanceProcAddr(IntPtr func)
{
Debug.Log($"{TAG}: registering our own xrGetInstanceProcAddr");
if (XrGetInstanceProcAddrOriginal == null)
{
XrGetInstanceProcAddrOriginal = Marshal.GetDelegateForFunctionPointer<DelegateXrGetInstanceProcAddr>(func);
isInited = true;
return hookGetInstanceProcAddrHandlePtr;
}
else
{
return func;
}
}
}
}

View File

@@ -0,0 +1,85 @@
// Copyright HTC Corporation All Rights Reserved.
#define DEBUG
using AOT;
using System;
using System.Runtime.InteropServices;
using UnityEngine.Profiling;
using VIVE.OpenXR.FrameSynchronization;
namespace VIVE.OpenXR
{
partial class ViveInterceptors
{
#region xrBeginSession
public delegate XrResult xrBeginSessionDelegate(XrSession session, ref XrSessionBeginInfo beginInfo);
private static xrBeginSessionDelegate xrBeginSessionOrigin = null;
[MonoPInvokeCallback(typeof(xrBeginSessionDelegate))]
private static XrResult xrBeginSessionInterceptor(XrSession session, ref XrSessionBeginInfo beginInfo)
{
Profiler.BeginSample("ViveInterceptors:BeginSession");
XrResult result = XrResult.XR_ERROR_FUNCTION_UNSUPPORTED;
if (xrBeginSessionOrigin != null)
{
if (m_EnableFrameSynchronization)
{
frameSynchronizationSessionBeginInfo.mode = m_FrameSynchronizationMode;
frameSynchronizationSessionBeginInfo.next = beginInfo.next;
beginInfo.next = Marshal.AllocHGlobal(Marshal.SizeOf(frameSynchronizationSessionBeginInfo));
long offset = 0;
if (IntPtr.Size == 4)
offset = beginInfo.next.ToInt32();
else
offset = beginInfo.next.ToInt64();
IntPtr frame_synchronization_session_begin_info_ptr = new IntPtr(offset);
Marshal.StructureToPtr(frameSynchronizationSessionBeginInfo, frame_synchronization_session_begin_info_ptr, false);
#if DEBUG
if (IntPtr.Size == 4)
offset = beginInfo.next.ToInt32();
else
offset = beginInfo.next.ToInt64();
IntPtr fs_begin_info_ptr = new IntPtr(offset);
XrFrameSynchronizationSessionBeginInfoHTC fsBeginInfo = (XrFrameSynchronizationSessionBeginInfoHTC)Marshal.PtrToStructure(fs_begin_info_ptr, typeof(XrFrameSynchronizationSessionBeginInfoHTC));
sb.Clear().Append("xrBeginSessionInterceptor() beginInfo.next = (").Append(fsBeginInfo.type).Append(", ").Append(fsBeginInfo.mode).Append(")"); DEBUG(sb);
#endif
}
result = xrBeginSessionOrigin(session, ref beginInfo);
}
else
{
sb.Clear().Append("xrBeginSessionInterceptor() Not assign xrBeginSession!"); ERROR(sb);
}
Profiler.EndSample();
return result;
}
private static readonly xrBeginSessionDelegate xrBeginSession = new xrBeginSessionDelegate(xrBeginSessionInterceptor);
private static readonly IntPtr xrBeginSessionPtr = Marshal.GetFunctionPointerForDelegate(xrBeginSession);
#endregion
private static XrFrameSynchronizationSessionBeginInfoHTC frameSynchronizationSessionBeginInfo = XrFrameSynchronizationSessionBeginInfoHTC.identity;
private static bool m_EnableFrameSynchronization = false;
private static XrFrameSynchronizationModeHTC m_FrameSynchronizationMode = XrFrameSynchronizationModeHTC.XR_FRAME_SYNCHRONIZATION_MODE_STABILIZED_HTC;
/// <summary>
/// Activate or deactivate the Frame Synchronization feature.
/// </summary>
/// <param name="active">True for activate</param>
/// <param name="mode">The <see cref="XrFrameSynchronizationModeHTC"/> used for Frame Synchronization.</param>
public void ActivateFrameSynchronization(bool active, XrFrameSynchronizationModeHTC mode)
{
m_EnableFrameSynchronization = active;
m_FrameSynchronizationMode = mode;
sb.Clear().Append("ActivateFrameSynchronization() ").Append(active ? "enable " : "disable ").Append(mode); DEBUG(sb);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8c222b96d7eb4ca4bb6390e07b1967bb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,129 @@
// Copyright HTC Corporation All Rights Reserved.
using AOT;
using System;
using System.Runtime.InteropServices;
using UnityEngine.Profiling;
using VIVE.OpenXR.DisplayRefreshRate;
using VIVE.OpenXR.Passthrough;
using VIVE.OpenXR.UserPresence;
namespace VIVE.OpenXR
{
partial class ViveInterceptors
{
#region xrPollEvent
public delegate XrResult xrPollEventDelegate(XrInstance instance, ref XrEventDataBuffer eventData);
private static xrPollEventDelegate xrPollEventOrigin = null;
[MonoPInvokeCallback(typeof(xrPollEventDelegate))]
private static XrResult xrPollEventInterceptor(XrInstance instance, ref XrEventDataBuffer eventData)
{
Profiler.BeginSample("ViveInterceptors:WaitFrame");
XrResult result = XrResult.XR_SUCCESS;
if (xrPollEventOrigin != null)
{
result = xrPollEventOrigin(instance, ref eventData);
if (result == XrResult.XR_SUCCESS)
{
sb.Clear().Append("xrPollEventInterceptor() xrPollEvent ").Append(eventData.type); DEBUG(sb);
switch(eventData.type)
{
case XrStructureType.XR_TYPE_EVENT_DATA_PASSTHROUGH_CONFIGURATION_IMAGE_RATE_CHANGED_HTC:
if (XrEventDataPassthroughConfigurationImageRateChangedHTC.Get(eventData, out XrEventDataPassthroughConfigurationImageRateChangedHTC eventDataPassthroughConfigurationImageRate))
{
fromImageRate = eventDataPassthroughConfigurationImageRate.fromImageRate;
toImageRate = eventDataPassthroughConfigurationImageRate.toImageRate;
sb.Clear().Append("xrPollEventInterceptor() XR_TYPE_EVENT_DATA_PASSTHROUGH_CONFIGURATION_IMAGE_RATE_CHANGED_HTC")
.Append(", fromImageRate.srcImageRate: ").Append(fromImageRate.srcImageRate)
.Append(", fromImageRatesrc.dstImageRate: ").Append(fromImageRate.dstImageRate)
.Append(", toImageRate.srcImageRate: ").Append(toImageRate.srcImageRate)
.Append(", toImageRate.dstImageRate: ").Append(toImageRate.dstImageRate);
DEBUG(sb);
VivePassthroughImageRateChanged.Send(fromImageRate.srcImageRate, fromImageRate.dstImageRate, toImageRate.srcImageRate, toImageRate.dstImageRate);
}
break;
case XrStructureType.XR_TYPE_EVENT_DATA_PASSTHROUGH_CONFIGURATION_IMAGE_QUALITY_CHANGED_HTC:
if (XrEventDataPassthroughConfigurationImageQualityChangedHTC.Get(eventData, out XrEventDataPassthroughConfigurationImageQualityChangedHTC eventDataPassthroughConfigurationImageQuality))
{
fromImageQuality = eventDataPassthroughConfigurationImageQuality.fromImageQuality;
toImageQuality = eventDataPassthroughConfigurationImageQuality.toImageQuality;
sb.Clear().Append("xrPollEventInterceptor() XR_TYPE_EVENT_DATA_PASSTHROUGH_CONFIGURATION_IMAGE_QUALITY_CHANGED_HTC")
.Append(", fromImageQuality: ").Append(fromImageQuality.scale)
.Append(", toImageQuality: ").Append(toImageQuality.scale);
DEBUG(sb);
VivePassthroughImageQualityChanged.Send(fromImageQuality.scale, toImageQuality.scale);
}
break;
case XrStructureType.XR_TYPE_EVENT_DATA_DISPLAY_REFRESH_RATE_CHANGED_FB:
if(XrEventDataDisplayRefreshRateChangedFB.Get(eventData, out XrEventDataDisplayRefreshRateChangedFB eventDataDisplayRefreshRate))
{
fromDisplayRefreshRate = eventDataDisplayRefreshRate.fromDisplayRefreshRate;
toDisplayRefreshRate = eventDataDisplayRefreshRate.toDisplayRefreshRate;
sb.Clear().Append("xrPollEventInterceptor() XR_TYPE_EVENT_DATA_DISPLAY_REFRESH_RATE_CHANGED_FB")
.Append(", fromDisplayRefreshRate: ").Append(fromDisplayRefreshRate)
.Append(", toDisplayRefreshRate: ").Append(toDisplayRefreshRate);
DEBUG(sb);
ViveDisplayRefreshRateChanged.Send(fromDisplayRefreshRate, toDisplayRefreshRate);
}
break;
case XrStructureType.XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED:
if (XrEventDataSessionStateChanged.Get(eventData, out XrEventDataSessionStateChanged eventDataSession))
{
switch(eventDataSession.state)
{
case XrSessionState.XR_SESSION_STATE_READY:
isUserPresent = true;
break;
case XrSessionState.XR_SESSION_STATE_STOPPING:
isUserPresent = false;
break;
default:
break;
}
sb.Clear().Append("xrPollEventInterceptor() XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED")
.Append(", session: ").Append(eventDataSession.session)
.Append(", state: ").Append(eventDataSession.state)
.Append(", isUserPresent: ").Append(isUserPresent);
DEBUG(sb);
}
break;
case XrStructureType.XR_TYPE_EVENT_DATA_USER_PRESENCE_CHANGED_EXT:
if (XrEventDataUserPresenceChangedEXT.Get(eventData, out XrEventDataUserPresenceChangedEXT eventDataUserPresence))
{
isUserPresent = eventDataUserPresence.isUserPresent;
sb.Clear().Append("xrPollEventInterceptor() XR_TYPE_EVENT_DATA_USER_PRESENCE_CHANGED_EXT")
.Append(", session: ").Append(eventDataUserPresence.session)
.Append(", isUserPresent: ").Append(isUserPresent);
DEBUG(sb);
}
break;
default:
break;
}
}
//sb.Clear().Append("xrPollEventInterceptor() xrPollEvent result: ").Append(result).Append(", isUserPresent: ").Append(isUserPresent); DEBUG(sb);
}
Profiler.EndSample();
return result;
}
private static readonly xrPollEventDelegate xrPollEvent = new xrPollEventDelegate(xrPollEventInterceptor);
private static readonly IntPtr xrPollEventPtr = Marshal.GetFunctionPointerForDelegate(xrPollEvent);
#endregion
private static bool isUserPresent = true;
public bool IsUserPresent() { return isUserPresent; }
private static float fromDisplayRefreshRate, toDisplayRefreshRate;
public float FromDisplayRefreshRate() { return fromDisplayRefreshRate; }
public float ToDisplayRefreshRate() { return toDisplayRefreshRate; }
private static XrPassthroughConfigurationImageRateHTC fromImageRate, toImageRate;
private static XrPassthroughConfigurationImageQualityHTC fromImageQuality, toImageQuality;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c2cc5716d3f563f49a47da6c1bd8ccbe
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,89 @@
// Copyright HTC Corporation All Rights Reserved.
using System.Runtime.InteropServices;
using System;
using AOT;
using UnityEngine.Profiling;
namespace VIVE.OpenXR
{
partial class ViveInterceptors
{
public struct XrCompositionLayerBaseHeader
{
public XrStructureType type; // This base structure itself has no associated XrStructureType value.
public System.IntPtr next;
public XrCompositionLayerFlags layerFlags;
public XrSpace space;
}
public struct XrFrameEndInfo
{
public XrStructureType type;
public System.IntPtr next;
public XrTime displayTime;
public XrEnvironmentBlendMode environmentBlendMode;
public uint layerCount;
public IntPtr layers; // XrCompositionLayerBaseHeader IntPtr array
}
public delegate XrResult DelegateXrEndFrame(XrSession session, ref XrFrameEndInfo frameEndInfo);
private static readonly DelegateXrEndFrame xrEndFrameInterceptorHandle = new DelegateXrEndFrame(XrEndFrameInterceptor);
private static readonly IntPtr xrEndFrameInterceptorPtr = Marshal.GetFunctionPointerForDelegate(xrEndFrameInterceptorHandle);
static DelegateXrEndFrame XrEndFrameOriginal = null;
[MonoPInvokeCallback(typeof(DelegateXrEndFrame))]
private static XrResult XrEndFrameInterceptor(XrSession session, ref XrFrameEndInfo frameEndInfo)
{
// instance must not null
//if (instance == null)
// return XrEndFrameOriginal(session, ref frameEndInfo);
Profiler.BeginSample("VI:EndFrame");
XrResult result = XrResult.XR_SUCCESS;
if (instance.BeforeOriginalEndFrame != null &&
!instance.BeforeOriginalEndFrame(session, ref frameEndInfo, ref result))
{
Profiler.EndSample();
return result;
}
result = XrEndFrameOriginal(session, ref frameEndInfo);
instance.AfterOriginalEndFrame?.Invoke(session, ref frameEndInfo, ref result);
Profiler.EndSample();
return result;
}
/// <summary>
/// If you return false, the original function will not be called.
/// </summary>
/// <param name="session"></param>
/// <param name="frameEndInfo"></param>
/// <param name="result"></param>
/// <returns></returns>
public delegate bool DelegateXrEndFrameInterceptor(XrSession session, ref XrFrameEndInfo frameEndInfo, ref XrResult result);
/// <summary>
/// Use this to intercept the original function. This will be called before the original function.
/// </summary>
public DelegateXrEndFrameInterceptor BeforeOriginalEndFrame;
/// <summary>
/// Use this to intercept the original function. This will be called after the original function.
/// </summary>
public DelegateXrEndFrameInterceptor AfterOriginalEndFrame;
#if PERFORMANCE_TEST
public delegate XrResult DelegateXrLocateSpace(XrSpace space, XrSpace baseSpace, XrTime time, ref XrSpaceLocation location);
private static readonly DelegateXrLocateSpace xrLocateSpaceInterceptorHandle = new DelegateXrLocateSpace(XrLocateSpaceInterceptor);
private static readonly IntPtr xrLocateSpaceInterceptorPtr = Marshal.GetFunctionPointerForDelegate(xrLocateSpaceInterceptorHandle);
static DelegateXrLocateSpace XrLocateSpaceOriginal = null;
[MonoPInvokeCallback(typeof(DelegateXrLocateSpace))]
public static XrResult XrLocateSpaceInterceptor(XrSpace space, XrSpace baseSpace, XrTime time, ref XrSpaceLocation location)
{
Profiler.BeginSample("VI:LocateSpace");
var ret = XrLocateSpaceOriginal(space, baseSpace, time, ref location);
Profiler.EndSample();
return ret;
}
#endif
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6bf7cf55d82ac6343b4eda92d1197a66
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -3,59 +3,106 @@ using System.Runtime.InteropServices;
using System;
using UnityEngine;
using AOT;
using UnityEngine.Profiling;
namespace VIVE.OpenXR
{
partial class ViveInterceptors
{
#region XRWaitFrame
public struct XrFrameWaitInfo
{
public XrStructureType type;
public IntPtr next;
}
partial class ViveInterceptors
{
#region XRWaitFrame
public struct XrFrameWaitInfo
{
public XrStructureType type;
public IntPtr next;
}
public struct XrFrameState
{
public XrStructureType type;
public IntPtr next;
public XrTime predictedDisplayTime;
public XrDuration predictedDisplayPeriod;
public XrBool32 shouldRender;
}
public struct XrFrameState
{
public XrStructureType type;
public IntPtr next;
public XrTime predictedDisplayTime;
public XrDuration predictedDisplayPeriod;
public XrBool32 shouldRender;
}
public delegate XrResult DelegateXrWaitFrame(XrSession session, ref XrFrameWaitInfo frameWaitInfo, ref XrFrameState frameState);
private static readonly DelegateXrWaitFrame xrWaitFrameInterceptorHandle = new DelegateXrWaitFrame(XrWaitFrameInterceptor);
private static readonly IntPtr xrWaitFrameInterceptorPtr = Marshal.GetFunctionPointerForDelegate(xrWaitFrameInterceptorHandle);
static DelegateXrWaitFrame XrWaitFrameOriginal = null;
bool isWaitFrameIntercepted = false;
[MonoPInvokeCallback(typeof(DelegateXrWaitFrame))]
private static XrResult XrWaitFrameInterceptor(XrSession session, ref XrFrameWaitInfo frameWaitInfo, ref XrFrameState frameState)
{
var ret = XrWaitFrameOriginal(session, ref frameWaitInfo, ref frameState);
currentFrameState = frameState;
return ret;
}
public delegate XrResult DelegateXrWaitFrame(XrSession session, ref XrFrameWaitInfo frameWaitInfo, ref XrFrameState frameState);
private static readonly DelegateXrWaitFrame xrWaitFrameInterceptorHandle = new DelegateXrWaitFrame(XrWaitFrameInterceptor);
private static readonly IntPtr xrWaitFrameInterceptorPtr = Marshal.GetFunctionPointerForDelegate(xrWaitFrameInterceptorHandle);
static DelegateXrWaitFrame XrWaitFrameOriginal = null;
static XrFrameState currentFrameState = new XrFrameState() { predictedDisplayTime = 0 };
[MonoPInvokeCallback(typeof(DelegateXrWaitFrame))]
private static XrResult XrWaitFrameInterceptor(XrSession session, ref XrFrameWaitInfo frameWaitInfo, ref XrFrameState frameState)
{
// instance must not null
//if (instance == null)
// return XrWaitFrameOriginal(session, ref frameWaitInfo, ref frameState);
Profiler.BeginSample("VI:WaitFrame");
instance.isWaitFrameIntercepted = true;
XrResult result = XrResult.XR_SUCCESS;
if (instance.BeforeOriginalWaitFrame != null &&
!instance.BeforeOriginalWaitFrame(session, ref frameWaitInfo, ref frameState, ref result))
{
Profiler.EndSample();
return result;
}
var ret = XrWaitFrameOriginal(session, ref frameWaitInfo, ref frameState);
instance.AfterOriginalWaitFrame?.Invoke(session, ref frameWaitInfo, ref frameState, ref result);
currentFrameState = frameState;
Profiler.EndSample();
return result;
}
public XrFrameState GetCurrentFrameState()
{
if (!isInited) throw new Exception("ViveInterceptors is not inited");
static XrFrameState currentFrameState = new XrFrameState() { predictedDisplayTime = 0 };
return currentFrameState;
}
/// <summary>
/// Get the waitframe's result: XrFrameState. This result used in update is not matching the current frame. Use it after onBeforeRender.
/// </summary>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public XrFrameState GetCurrentFrameState()
{
if (!isWaitFrameIntercepted) throw new Exception("ViveInterceptors is not intercepted");
public XrTime GetPredictTime()
{
if (!isInited) throw new Exception("ViveInterceptors is not inited");
return currentFrameState;
}
Debug.Log($"{TAG}: XrWaitFrameInterceptor(predictedDisplayTime={currentFrameState.predictedDisplayTime}");
if (currentFrameState.predictedDisplayTime == 0)
return new XrTime((long)(1000000L * (Time.unscaledTimeAsDouble + 0.011f)));
else
return currentFrameState.predictedDisplayTime;
}
#endregion XRWaitFrame
}
}
/// <summary>
/// Must request xrWaitFrame before calling this function. This result used in update is not matching the current frame. Use it after onBeforeRender.
/// </summary>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public XrTime GetPredictTime()
{
if (!isWaitFrameIntercepted) throw new Exception("ViveInterceptors is not intercepted");
//Debug.Log($"{TAG}: XrWaitFrameInterceptor(predictedDisplayTime={currentFrameState.predictedDisplayTime}");
if (currentFrameState.predictedDisplayTime == 0)
return new XrTime((long)(1000000L * (Time.unscaledTimeAsDouble + 0.011f)));
else
return currentFrameState.predictedDisplayTime;
}
/// <summary>
/// Register WaitFrame event
/// </summary>
/// <param name="session"></param>
/// <param name="frameWaitInfo"></param>
/// <param name="frameState"></param>
/// <param name="result"></param>
/// <returns></returns>
public delegate bool DelegateXrWaitFrameInterceptor(XrSession session, ref XrFrameWaitInfo frameWaitInfo, ref XrFrameState frameState, ref XrResult result);
/// <summary>
/// Use this to intercept the original function. This will be called before the original function.
/// </summary>
public DelegateXrWaitFrameInterceptor BeforeOriginalWaitFrame;
/// <summary>
/// Use this to intercept the original function. This will be called after the original function.
/// </summary>
public DelegateXrWaitFrameInterceptor AfterOriginalWaitFrame;
#endregion XRWaitFrame
}
}

View File

@@ -0,0 +1,329 @@
using AOT;
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
namespace VIVE.OpenXR.Common.RenderThread
{
#region syncObject
public class Message
{
public bool isFree = true;
}
/// <summary>
/// MessagePool class manages a pool of message objects for reuse. You can enter any kind of message object.
/// However when obtain, the message object will not able to cast to the type you want.
/// You should only use one kind of message. Not mix different kind of message.
/// </summary>
public class MessagePool
{
// pool member is used to store message objects in a list.
// Note that the size of this list will dynamically adjust as needed but will not automatically shrink.
private readonly List<Message> pool = new List<Message>(2) { };
private int index = 0;
public MessagePool() { }
// Next method calculates the next index value for cycling through message objects in the pool.
private int Next(int value)
{
if (++value >= pool.Count)
value = 0;
return value;
}
// Obtain method retrieves a message object from the pool.
// Ensure proper state setup for the message after retrieval and call Release() to the message after use.
public T Obtain<T>() where T : Message, new()
{
int c = pool.Count;
int i = index;
for (int j = 0; j < c; i++, j++)
{
if (i >= c)
i = 0;
if (pool[i].isFree)
{
//Debug.LogError("Obtain idx=" + i);
index = i;
return (T)pool[i];
}
}
index = Next(i);
var newItem = new T()
{
isFree = true
};
pool.Insert(index, newItem);
Debug.Log("RT.MessagePool.Obtain() pool count=" + pool.Count);
return newItem;
}
// Lock method marks a message as "in use" to prevent other code from reusing it.
// This is already called to the message obtained from the pool.
public static void Lock(Message msg)
{
msg.isFree = false;
}
/// <summary>
/// Release method marks a message as "free" so that other code can reuse it.
/// You can use it in RenderThread. It will not trigger the GC event.
/// </summary>
/// <param name="msg"></param>
public static void Release(Message msg)
{
msg.isFree = true;
}
}
/// <summary>
/// PreAllocatedQueue class is a message queue based on MessagePool for preallocating message objects.
/// Its main functionality is to add message objects to the queue and retrieve them from the queue.
/// Messages should be enqueued in GameThread and dequeued in RenderThread.
/// In render thread, dequeue will not trigger the GC event. Because the queue is preallocated.
/// The 'lock' expression is not used for list's size change. Because lock should be avoid used in RenderThread.
/// Set the queueSize as the double count of message you want to pass to render thread in one frame, and the
/// list will never change size during runtime. Therefore we don't need to use 'lock' to protect the list.
/// </summary>
public class PreAllocatedQueue : MessagePool
{
// list member is used to store preallocated message objects in a list.
// Note that the size of this list is set during initialization and does not dynamically adjust.
private List<Message> list = new List<Message>();
private int queueBegin = 0;
private int queueEnd = 0;
/// <summary>
/// The queueSize should be the double count of message you want to pass to render thread in one frame.
/// </summary>
/// <param name="queueSize"></param>
public PreAllocatedQueue(int queueSize = 2) : base() {
for (int i = 0; i < queueSize; i++)
{
list.Add(null);
}
}
private int Next(int value)
{
if (++value >= list.Count)
value = 0;
return value;
}
/// <summary>
/// Enqueue method adds a message object to the queue.
/// If the queue is full, the new message is added to the end of the list.
///
/// This function is designed to use the message object obtained from the MessagePool.
/// Ensure only one type of message object is used in the queue.
///
/// Enqueue will increase the queue size if the queue is full. This may trigger GC.Alloc.
/// This function should be used in GameThread.
/// </summary>
/// <param name="msg"></param>
public void Enqueue(Message msg)
{
Lock(msg);
queueEnd = Next(queueEnd);
// If the queue is full, add the message to the end of the list. Should not let it happen.
// Use larger queue size to avoid this issue.
// If you see the error log here, you should increase the queue size in your design.
if (queueEnd == queueBegin)
{
// Should let Insert and queueBegin be atomic. No lock protection here.
list.Insert(queueEnd, msg);
queueBegin++;
Debug.LogError("RT.MessagePool.Enqueue() list count=" + list.Count);
}
else
{
list[queueEnd] = msg;
}
}
/// <summary>
/// Dequeue method retrieves a message object from the queue.
/// This method returns the first message object in the queue and removes it from the queue.
/// This function will not trigger the GC event. Free to use in RenderThread.
/// After use the Message, call Release() to the message.
/// </summary>
/// <returns></returns>
public Message Dequeue()
{
// No lock protection here. If list is not change size, it is safe.
// However if list changed size, it is safe in most case.
queueBegin = Next(queueBegin);
return list[queueBegin];
}
}
/// <summary>
/// RenderThreadTask class is used to execute specified tasks on the rendering thread.
/// You don't need to develop a native function to run your task on the rendering thread.
/// And you don't need to design how to pass data to render thread.
/// This class can be run in Unity Editor since Unity 2021. Test your code in Unity Editor can save your time.
///
/// You should only create RenderThreadTask as static readonly. Do not create RenderThreadTask in dynamic.
///
/// You should not run Unity.Engine code in RenderThread. It will cause the Unity.Engine to hang.
/// Any exception will not be caught and shown in RenderThread.
/// You should print your error message out to clearify your issue.
///
/// The 'lock' expression is not used here. Because I believe the lock is not necessary in this case.
/// And the lock will cause the performance issue. All the design here help you not to use 'lock'.
/// </summary>
public class RenderThreadTask
{
private static IntPtr GetFunctionPointerForDelegate(Delegate del)
{
return System.Runtime.InteropServices.Marshal.GetFunctionPointerForDelegate(del);
}
public delegate void RenderEventDelegate(int e);
private static readonly RenderEventDelegate handle = new RenderEventDelegate(RunSyncObjectInRenderThread);
private static readonly IntPtr handlePtr = GetFunctionPointerForDelegate(handle);
public delegate void Receiver(PreAllocatedQueue dataQueue);
// CommandList is used to store all RenderThreadTask objects.
// Do not create RenderThreadTask object in dynamic. It will cause the CommandList to increase infinitly.
private static List<RenderThreadTask> CommandList = new List<RenderThreadTask>();
private PreAllocatedQueue queue;
public PreAllocatedQueue Queue { get { return queue; } }
private readonly Receiver receiver;
private readonly int id;
/// <summary>
/// Input the receiver as render thread callback. The receiver will be executed in render thread.
/// queueSize should be the double count of message you want to pass to render thread in one frame.
/// </summary>
/// <param name="render">The callback in render thread.</param>
/// <param name="queueSize">If issue this event once in a frame, set queueSize as 2.</param>
/// <exception cref="ArgumentNullException"></exception>
public RenderThreadTask(Receiver render, int queueSize = 2)
{
queue = new PreAllocatedQueue(queueSize);
receiver = render;
if (receiver == null)
throw new ArgumentNullException("receiver should not be null");
CommandList.Add(this);
id = CommandList.IndexOf(this);
}
~RenderThreadTask()
{
try { CommandList.RemoveAt(id); } finally { }
}
void IssuePluginEvent(IntPtr callback, int eventID)
{
// Older version will hang after run script in render thread.
GL.IssuePluginEvent(callback, eventID);
return;
}
void IssuePluginEvent(CommandBuffer cmdBuf, IntPtr callback, int eventID)
{
cmdBuf.IssuePluginEvent(callback, eventID);
return;
}
/// <summary>
/// IssueEvent method submits this task's receiver, which is set in constructor, to be executed on the rendering thread.
/// </summary>
public void IssueEvent()
{
// Let the render thread run the RunSyncObjectInRenderThread(id)
IssuePluginEvent(handlePtr, id);
}
public void IssueInCommandBuffer(CommandBuffer cmdBuf)
{
// Let the render thread run the RunSyncObjectInRenderThread(id)
IssuePluginEvent(cmdBuf, handlePtr, id);
}
// Called by RunSyncObjectInRenderThread()
private void Receive()
{
receiver(queue);
}
// RunSyncObjectInRenderThread method is a static method used to execute a specified task on the rendering thread.
// This method is invoked by Unity's rendering event mechanism and does not need to be called directly by developers.
[MonoPInvokeCallback(typeof(RenderEventDelegate))]
private static void RunSyncObjectInRenderThread(int id)
{
CommandList[id].Receive();
}
}
#endregion
#region sample
// Not to compile this sample into your application. Just for reference. You can run this sample in Unity Editor and it will work.
#if UNITY_EDITOR
public class ViveRenderThreadTaskSample : MonoBehaviour
{
// Create your own message class.
internal class SampleMessage : Message
{
public int dataPassedToRenderThread;
}
// Use static readonly to create RenderThreadTask. Keep internal to avoid miss use by other developers.
internal static readonly RenderThreadTask sampleRenderThreadTask1 = new RenderThreadTask(SampleReceiver1);
// Different task use different RenderThreadTask and different recevier.
internal static readonly RenderThreadTask sampleRenderThreadTask2 = new RenderThreadTask(SampleReceiver2);
private static void SampleReceiver1(PreAllocatedQueue dataQueue)
{
var msg = dataQueue.Dequeue() as SampleMessage;
// no need to check msg if it is null because your design should avoid it.
// Keep data before release. Use local variable to keep data and release msg early. Should not keep the msg instance itself.
var data = msg.dataPassedToRenderThread;
// Make sure release the msg if finished. Other wise the memory will keep increasing when Obtain.
MessagePool.Release(msg);
Debug.Log("Task1, the data passed to render thread: " + data);
}
private static void SampleReceiver2(PreAllocatedQueue dataQueue)
{
var msg = dataQueue.Dequeue() as SampleMessage;
var data = msg.dataPassedToRenderThread;
MessagePool.Release(msg);
Debug.Log("Task2, the data passed to render thread: " + data);
}
// Send a message to the render thread every frame.
private void Update()
{
// Make sure only one kind of message object is used in the queue.
var msg = sampleRenderThreadTask1.Queue.Obtain<SampleMessage>();
msg.dataPassedToRenderThread = 123;
sampleRenderThreadTask1.Queue.Enqueue(msg);
sampleRenderThreadTask1.IssueEvent();
}
// Send a message to render thread when something clicked. Make sure only one click in one frame because the queue size is only two.
public void OnClicked()
{
// Reuse the same message type is ok.
var msg = sampleRenderThreadTask2.Queue.Obtain<SampleMessage>();
msg.dataPassedToRenderThread = 234;
sampleRenderThreadTask2.Queue.Enqueue(msg);
sampleRenderThreadTask2.IssueEvent();
}
}
#endif
#endregion
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 251b4bedf6420fc4e84be778e501343f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: