// Copyright 2024 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #ifdef ENABLE_VR #include #include #include #include #include #include "VideoCommon/AbstractFramebuffer.h" // OpenXRManager owns the XrInstance, XrSystemId, XrSession, and reference XrSpace. // It handles the OpenXR session state machine and per-frame timing. // // Backend-specific swapchain creation is handled by the respective backend class // (D3DOpenXR for D3D11, VulkanOpenXR for Vulkan), which creates the XrSession // with a graphics binding and then calls SetSession() + SetSwapchain() to // transfer ownership here. namespace VR { // Per-eye pose and field-of-view returned by xrLocateViews. struct XREyeView { XrPosef pose; XrFovf fov; }; // Abstract interface for backend-specific per-eye swapchain management. // Implemented by D3DOpenXR (D3D11) and VulkanOpenXR (Vulkan). // Used by Presenter::RenderXFBToScreen() to blit game frames into the HMD's // eye textures without depending on backend-specific types. class IOpenXRSwapchain { public: virtual ~IOpenXRSwapchain() = default; // Acquire the next swapchain image for the given eye (0 = left, 1 = right). // Returns an AbstractFramebuffer* to render into, or nullptr on error. // Must be paired with ReleaseEyeTexture() after rendering is complete. virtual AbstractFramebuffer* AcquireEyeFramebuffer(uint32_t eye) = 0; // Signal the runtime that rendering into the given eye's current image is done. virtual void ReleaseEyeTexture(uint32_t eye) = 0; // Build the XrCompositionLayerProjection from the current eye poses and submit // via xrEndFrame. Call after both eyes have been rendered and released. virtual bool SubmitFrame() = 0; // Pixel dimensions of the eye render targets (typically HMD recommended resolution). virtual uint32_t GetEyeWidth() const = 0; virtual uint32_t GetEyeHeight() const = 0; }; class OpenXRManager { public: OpenXRManager(); ~OpenXRManager(); OpenXRManager(const OpenXRManager&) = delete; OpenXRManager& operator=(const OpenXRManager&) = delete; // Step 1: Create XrInstance. // extra_extensions must include the graphics API extension (e.g. XR_KHR_D3D11_ENABLE_EXTENSION_NAME). bool CreateInstance(const std::vector& extra_extensions = {}); // Step 2: Locate the HMD system. bool InitializeSystem(); // Step 2b: Query recommended per-eye render resolution. // Must be called before the backend creates swapchains. bool EnumerateViewConfigurations(); // Step 3 (called by backend after creating session with graphics binding). void SetSession(XrSession session); // Step 3b: Register the backend swapchain implementation for use by the presenter. // Raw pointer — the backend object outlives the manager. void SetSwapchain(IOpenXRSwapchain* swapchain) { m_swapchain = swapchain; } IOpenXRSwapchain* GetSwapchain() const { return m_swapchain; } // Step 4: Create the local reference space used for head tracking. bool CreateReferenceSpace(); // ---- Per-frame interface ---- // Poll XrEvents and drive the session state machine. // Returns false if the render loop should stop. bool PollEvents(); // xrWaitFrame — blocks until the runtime wants a new frame rendered. // Call at the start of each rendered frame. bool WaitFrame(); // xrBeginFrame — signals the runtime that rendering has started. bool BeginFrame(); // xrEndFrame — submits the completed layer stack to the compositor. bool EndFrame(const std::vector& layers); // xrLocateViews — fills m_eye_views with the predicted head pose for each eye. // Call between BeginFrame and rendering. bool LocateViews(); // ---- Accessors ---- XrInstance GetInstance() const { return m_instance; } XrSystemId GetSystemId() const { return m_system_id; } XrSession GetSession() const { return m_session; } XrSpace GetReferenceSpace() const { return m_reference_space; } XrTime GetPredictedDisplayTime() const { return m_frame_state.predictedDisplayTime; } bool IsSessionRunning() const { return m_session_running; } bool ShouldRender() const { return m_frame_state.shouldRender == XR_TRUE; } const std::array& GetEyeViews() const { return m_eye_views; } const std::array& GetViewConfigViews() const { return m_view_config_views; } // Compute per-eye projection rows with head rotation and eye position baked in. // // out_proj_rows[0..1] = left-eye rows 0,1; [2..3] = right-eye rows 0,1. // x_clip = dot(row0, viewPos), y_clip = dot(row1, viewPos) // Head rotation and eye position (scaled by units_per_meter) are baked in. // // out_z_rows[0..1] = left/right eye z-axis row. // z_eye = dot(z_row, viewPos) gives eye-space depth for perspective divide // (f.pos.w = -z_eye) and depth buffer recomputation. void GetEyeProjectionRows( float units_per_meter, std::array, 4>& out_proj_rows, std::array, 2>& out_z_rows) const; // Compute per-eye projection rows WITHOUT head rotation (for head-locked content). // Same layout as GetEyeProjectionRows but only includes the raw asymmetric frustum // projection and per-eye IPD offset. Content rendered with these rows follows // the user's head movements. void GetRawEyeProjectionRows( float units_per_meter, std::array, 4>& out_proj_rows) const; // Request recentering of the VR home position. // Applied on the OpenXR render thread during LocateViews. void RequestRecenter(); private: bool InitializeInputActions(); void DestroyInputActions(); void UpdateInputActions(); void UpdateHaptics(); void ResetInputActionsState(); void HandleSessionStateChange(XrSessionState new_state); XrInstance m_instance = XR_NULL_HANDLE; XrSystemId m_system_id = XR_NULL_SYSTEM_ID; XrSession m_session = XR_NULL_HANDLE; XrSpace m_reference_space = XR_NULL_HANDLE; // Non-owning pointer; lifetime managed by the backend (D3DOpenXR). IOpenXRSwapchain* m_swapchain = nullptr; XrSessionState m_session_state = XR_SESSION_STATE_UNKNOWN; bool m_session_running = false; bool m_exit_render_loop = false; XrFrameState m_frame_state{XR_TYPE_FRAME_STATE}; // OpenXR input action set used to expose VR controller input to Dolphin's // regular controller mapping UI. XrActionSet m_input_action_set = XR_NULL_HANDLE; std::array m_input_hand_paths{XR_NULL_PATH, XR_NULL_PATH}; XrAction m_action_primary_click = XR_NULL_HANDLE; XrAction m_action_secondary_click = XR_NULL_HANDLE; XrAction m_action_menu_click = XR_NULL_HANDLE; XrAction m_action_thumbstick_click = XR_NULL_HANDLE; XrAction m_action_trigger_click = XR_NULL_HANDLE; XrAction m_action_squeeze_click = XR_NULL_HANDLE; XrAction m_action_trigger_value = XR_NULL_HANDLE; XrAction m_action_squeeze_value = XR_NULL_HANDLE; XrAction m_action_thumbstick_x = XR_NULL_HANDLE; XrAction m_action_thumbstick_y = XR_NULL_HANDLE; XrAction m_action_aim_pose = XR_NULL_HANDLE; XrAction m_action_grip_pose = XR_NULL_HANDLE; XrAction m_action_haptic = XR_NULL_HANDLE; std::array m_aim_spaces{XR_NULL_HANDLE, XR_NULL_HANDLE}; std::array m_grip_spaces{XR_NULL_HANDLE, XR_NULL_HANDLE}; std::array m_haptics_active{false, false}; std::array m_view_config_views{}; std::array m_views{}; std::array m_eye_views{}; // "Home" head-center position, recorded on first LocateViews. // All positional tracking is relative to this to avoid floor-level offsets. mutable bool m_home_set{false}; mutable XrVector3f m_home_position{0.f, 0.f, 0.f}; std::atomic m_recenter_requested{false}; }; // Global instance — created by the backend during VideoBackend::Initialize(). extern std::unique_ptr g_openxr; } // namespace VR #endif // ENABLE_VR