using Facepunch.XR; using NativeEngine; using System.Diagnostics; using System.Numerics; using System.Runtime.InteropServices; using System.Text; namespace Sandbox.VR; /// /// Native helpers for VR /// internal static unsafe partial class VRNative { /// /// Private store for distance between user's pupils, in inches. /// Accessed and converted through and /// private static float IPD { get; set; } = 0f; /// /// Distance between user's pupils, in millimetres /// public static float IPDMillimetres => IPD.InchToMillimeter(); /// /// Distance between user's pupils, in inches /// public static float IPDInches => IPD; /// /// Headset refresh rate, in Hz /// public static float RefreshRate { get; private set; } // // Data // public static List TrackedDevices { get; private set; } // // Common properties // /// /// Is the SteamVR dashboard currently visible? /// public static bool IsDashboardVisible => SessionState != SessionState.Focused; /// /// Has the user selected that they're left hand dominant in SteamVR? /// public static bool IsLeftHandDominant => false; /// /// Spans both eyes - equivalent to (eye width * 2, eye height) /// internal static Vector2 FullRenderTargetSize { get; private set; } /// /// Spans one eye /// internal static Vector2 EyeRenderTargetSize { get; private set; } /// /// Scales the relative position between the two eyes /// public static float WorldScale { get; set; } = 1.0f; // // Performance timings // internal static uint _totalFrames; internal static uint _totalDroppedFrames; internal static uint _totalReprojectedFrames; private static void CopyStringToBuffer( string input, byte* buffer, uint maxLength ) { byte[] inputBytes = Encoding.ASCII.GetBytes( input ); for ( int i = 0; i < maxLength; i++ ) { if ( i < inputBytes.Length ) buffer[i] = inputBytes[i]; else buffer[i] = 0; } } private static string BufferToString( byte* buffer, uint maxLength ) { var str = new byte[maxLength]; for ( int i = 0; i < maxLength; i++ ) { str[i] = buffer[i]; } return Encoding.ASCII.GetString( str ).TrimEnd( '\0' ); } public delegate void DebugUtilsMessengerCallback( string message, DebugCallbackType type ); public delegate void DebugUtilsErrorCallback( string message ); private static Logger Log = new( "OpenXR" ); private static void XrErrorCallback( string message ) { Log.Error( $"{message}" ); Application.Exit(); // For now } private static void XrDebugCallback( string message, DebugCallbackType type ) { switch ( type ) { case DebugCallbackType.Verbose: Log.Trace( $"{message}" ); break; case DebugCallbackType.Warning: Log.Warning( $"{message}" ); break; case DebugCallbackType.Error: Log.Error( $"{message}" ); break; case DebugCallbackType.Info: default: Log.Info( $"{message}" ); break; } } private static Instance Instance = new(); private static EventManager EventManager = new(); private static Facepunch.XR.Input Input = new(); private static Compositor Compositor = new(); private static void FpxrCheck( XRResult result ) { if ( result < XRResult.Success ) { Log.Warning( $"Facepunch.XR: {result}" ); } } private static InstanceProperties InstanceProperties; internal static void CreateInstance() { if ( !VRSystem.HasHeadset ) return; // Initialize app config callbacks { if ( VRSystem.WantsDebug ) { // Set up a debug callback for logging Facepunch.XR messages var pDebugCallback = Marshal.GetFunctionPointerForDelegate( XrDebugCallback ); ApplicationConfig.SetDebugCallback( pDebugCallback ); } var pErrorCallback = Marshal.GetFunctionPointerForDelegate( XrErrorCallback ); ApplicationConfig.SetErrorCallback( pErrorCallback ); } // Create the OpenXR instance var instanceInfo = new InstanceInfo() { graphicsApi = GraphicsAPI.Vulkan, useDebugMessenger = true }; CopyStringToBuffer( "s&box", instanceInfo.appName, Constants.MaxAppNameSize ); var manifestPath = "core/cfg/fpxr/actions.json"; // this isn't great but EngineFileSystem.CoreContent isn't initialised when we call this CopyStringToBuffer( manifestPath, instanceInfo.actionManifestPath, Constants.MaxPathSize ); Instance = Instance.Create( instanceInfo ); InstanceProperties = Instance.GetProperties(); } internal static unsafe void CreateCompositor() { // Give FPXR all the info it needs about our vulkan device, instance, etc var vulkanInfo = new VulkanInfo() { vkDevice = g_pRenderDevice.GetDeviceSpecificInfo( DeviceSpecificInfo_t.DSI_VULKAN_DEVICE ), vkPhysicalDevice = g_pRenderDevice.GetDeviceSpecificInfo( DeviceSpecificInfo_t.DSI_VULKAN_PHYSICAL_DEVICE ), vkInstance = g_pRenderDevice.GetDeviceSpecificInfo( DeviceSpecificInfo_t.DSI_VULKAN_INSTANCE ), vkQueueIndex = 0, vkQueueFamilyIndex = ReadUInt32( g_pRenderDevice.GetDeviceSpecificInfo( DeviceSpecificInfo_t.DSI_VULKAN_QUEUE_FAMILY_INDEX ) ), }; Compositor = Instance.Compositor( vulkanInfo ); Input = Instance.Input(); EventManager = Compositor.EventManager(); // Save off display info so we can use it for rendering RefreshRate = Compositor.GetDisplayRefreshRate(); EyeRenderTargetSize = new Vector2( Compositor.GetEyeWidth(), Compositor.GetEyeHeight() ); FullRenderTargetSize = new Vector2( Compositor.GetRenderTargetWidth(), Compositor.GetRenderTargetHeight() ); Log.Trace( $"Full render target dims: {FullRenderTargetSize}" ); Log.Trace( $"Eye render target dims: {EyeRenderTargetSize}" ); Log.Trace( $"Display refresh rate: {RefreshRate}Hz" ); } public static SessionState SessionState { get; private set; } = SessionState.Unknown; internal static ViewInfo LeftEyeInfo = ViewInfo.Zero; internal static ViewInfo RightEyeInfo = ViewInfo.Zero; internal static void Update() { if ( VRSystem.IsRendering ) { FpxrCheck( Compositor.GetViewInfo( 0, out LeftEyeInfo ) ); FpxrCheck( Compositor.GetViewInfo( 1, out RightEyeInfo ) ); UpdateIPD(); } // // Poll the event loop // while ( EventManager.PumpEvent( out var e ) != XRResult.NoEventsPending ) { if ( e.type == EventType.SessionStateChanged ) { var sessionStateChangedEventData = e.GetData(); Log.Trace( $"Session state changed to: {sessionStateChangedEventData.state}" ); SessionState = sessionStateChangedEventData.state; } } } internal static void TriggerHapticVibration( float duration, float frequency, float amplitude, InputSource source ) { FpxrCheck( Input.TriggerHapticVibration( duration, frequency, amplitude, source ) ); } internal static void Reset() { WorldScale = 1.0f; } internal static bool IsHMDInStandby() { return SessionState != SessionState.Focused; } internal static Matrix CreateProjection( float tanL, float tanR, float tanU, float tanD, float near, float far ) { var result = new Matrix4x4( 2f / (tanR - tanL), 0f, 0f, 0f, 0f, 2f / (tanU - tanD), 0f, 0f, (tanR + tanL) / (tanR - tanL), (tanU + tanD) / (tanU - tanD), -far / (far - near), -(far * near) / (far - near), 0f, 0f, -1f, 0f ); return result; } internal static Matrix GetProjectionMatrix( float znear, float zfar, VREye eye ) { var viewInfo = eye == VREye.Left ? LeftEyeInfo : RightEyeInfo; float left = MathF.Tan( viewInfo.fovLeft ); float right = MathF.Tan( viewInfo.fovRight ); float up = MathF.Tan( viewInfo.fovUp ); float down = MathF.Tan( viewInfo.fovDown ); return CreateProjection( left, right, up, down, znear, zfar ).Transpose(); } internal static Vector4 GetClipForEye( VREye eye ) { var eyeInfo = eye == VREye.Left ? LeftEyeInfo : RightEyeInfo; return new Vector4( MathF.Tan( eyeInfo.fovLeft ), MathF.Tan( eyeInfo.fovDown ), MathF.Tan( eyeInfo.fovRight ), MathF.Tan( eyeInfo.fovUp ) ); } internal static Transform GetTransformForEye( Vector3 cameraPosition, Rotation cameraRotation, VREye eye ) { var transform = new Transform(); // // Calculate transform based on user IPD // var positionOffset = (eye == VREye.Left ? cameraRotation.Left : cameraRotation.Right) * IPD; transform.Position = cameraPosition + positionOffset; transform.Rotation = cameraRotation; transform.Scale = 1.0f; // // Save off poses for later submit // if ( eye == VREye.Left ) LeftEyeRenderPose = LeftEyeInfo.pose; else RightEyeRenderPose = RightEyeInfo.pose; return transform; } internal static Transform GetHeadTransform() { // Take the center of the two eye poses var headPos = (LeftEyeInfo.pose.GetTransform().Position + RightEyeInfo.pose.GetTransform().Position) / 2.0f; var headRot = Rotation.Slerp( LeftEyeInfo.pose.GetTransform().Rotation, RightEyeInfo.pose.GetTransform().Rotation, 0.5f ); return new Transform( headPos, headRot ); } internal static void UpdateIPD() { // IPD is distance between two views. // We can calculate it by taking the distance between the two eye poses. IPD = (LeftEyeInfo.pose.GetTransform().Position - RightEyeInfo.pose.GetTransform().Position).Length / WorldScale; } internal static string GetSystemName() { var instanceProperties = Instance.GetProperties(); return BufferToString( instanceProperties.systemName, Constants.MaxSystemNameSize ); } internal static InputPoseHandState GetHandPoseState( InputSource source, MotionRange motionRange ) { FpxrCheck( Input.GetHandPoseState( source, motionRange, out var state ) ); return state; } internal static float GetFingerCurl( InputSource source, FingerValue finger ) { return Input.GetFingerCurl( source, finger ); } internal static bool HandTrackingSupported() { return InstanceProperties.supportsHandTracking; } internal static Transform GetOffsetForDeviceRole( TrackedDeviceRole role ) { // // alex: Offset controllers to match original SteamVR pose // We do this because /input/grip/pose is the palm, but we want the controller // to match the SteamVR pose (which is the controller itself) // // This should be uniform across all controllers because the palm is the same // for all of them. // return role switch { TrackedDeviceRole.LeftHand => new Transform( new Vector3( 5f, -2f, -3f ), Rotation.From( 10, -10, 90 ) ), TrackedDeviceRole.RightHand => new Transform( new Vector3( 5f, 2f, -3f ), Rotation.From( 10, 10, -90 ) ), _ => Transform.Zero }; } }