// Copyright 2024 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #ifdef ENABLE_VR #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(ANDROID) #include #endif #include "Common/Matrix.h" #include "Common/VR/OpenXRInputState.h" #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 (D3D11, etc.) is handled by the respective // backend class (e.g. D3DOpenXR), 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. Used by Presenter::RenderXFBToScreen() to blit game // frames into the HMD's eye textures without depending on D3D11-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; // Optional fast path for runtimes/backends that can expose both eyes as a // two-layer framebuffer. The default implementation preserves the per-eye path. virtual bool SupportsLayeredRendering() const { return false; } virtual AbstractFramebuffer* AcquireLayeredFramebuffer() { return nullptr; } virtual void ReleaseLayeredTexture() {} // True when the eye framebuffers carry a fragment density map (Vulkan VR foveation). // PostProcessing then compiles pipeline variants against foveated render passes. virtual bool HasFoveatedFramebuffers() const { return false; } // Whether xrEndFrame may be issued from the XR pacing thread. GL/GLES bindings expect // frame calls from the thread owning the context, so the GL backend opts out and // keeps the legacy inline frame flow. virtual bool SupportsDetachedFrameLoop() const { return true; } // ---- Flat mono panel path (StereoMode::Off + vr_flat_screen) ---- // The game is rendered mono and shown on a world-locked quad in the VR scene. The default // implementations reuse per-eye swapchain #0, so a backend only needs to expose that image's // raw XrSwapchain handle via GetFlatSwapchain() to opt in. virtual XrSwapchain GetFlatSwapchain() const { return XR_NULL_HANDLE; } virtual bool SupportsFlatScreen() const { return GetFlatSwapchain() != XR_NULL_HANDLE; } // Acquire the swapchain image the mono game frame is blit into. virtual AbstractFramebuffer* AcquireFlatFramebuffer() { return AcquireEyeFramebuffer(0); } virtual void ReleaseFlatTexture() { ReleaseEyeTexture(0); } // Build the XrCompositionLayerQuad from the flat swapchain and submit via xrEndFrame. // Defined in OpenXRManager.cpp; delegates the quad math to OpenXRManager::SubmitFlatQuadFrame. virtual bool SubmitFlatFrame(); // ---- Stereoscopic virtual screen path (StereoMode::SBS + vr_stereo_screen) ---- // The game is rendered with classic SBS stereoscopy; layer 0 is left eye and layer 1 is right // eye. The default implementations reuse the per-eye swapchains and submit two quad layers // with LEFT/RIGHT eye visibility at the virtual-screen pose. virtual XrSwapchain GetEyeSwapchainHandle(uint32_t eye) const { return XR_NULL_HANDLE; } virtual bool SupportsStereoFlatScreen() const { return GetEyeSwapchainHandle(0) != XR_NULL_HANDLE && GetEyeSwapchainHandle(1) != XR_NULL_HANDLE; } virtual bool SubmitStereoFlatFrame(); // Vulkan-backed OpenXR calls can touch the VkQueue bound to the XrSession. Backends that need // external queue synchronization return a held lock here; other backends return an empty lock. virtual std::unique_lock AcquireGraphicsQueueLock() { return {}; } // Vulkan backends may provide a stable, depth-independent quad used to confirm stopping // emulation. The image is prepared by the presenter and remains valid until hidden. virtual bool PrepareStopConfirmationLayer(std::string_view text) { return false; } virtual XrCompositionLayerQuad* GetStopConfirmationLayer() { return nullptr; } // Backends that finish xrEndFrame asynchronously must complete the previous frame before the // next xrBeginFrame call. Synchronous backends have no pending work. virtual bool WaitForPendingFrameFinalization(std::string_view reason = {}) { return true; } // 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; }; enum class OpenXRStopConfirmationResult : uint8_t { Idle, Pending, Stop, Continue, }; class OpenXRManager { public: OpenXRManager(); ~OpenXRManager(); OpenXRManager(const OpenXRManager&) = delete; OpenXRManager& operator=(const OpenXRManager&) = delete; // Query which optional controller-profile extensions are available on this system. // Returns string literal pointers (valid indefinitely). Call before CreateInstance(). static std::vector GetAvailableControllerExtensions(); // Returns true if the named OpenXR extension is advertised by the current runtime. // Call before CreateInstance(); does not require an active instance. static bool IsRuntimeExtensionSupported(std::string_view ext_name); // Returns true if the named extension was enabled on this manager's XrInstance. bool IsExtensionEnabled(std::string_view ext_name) const; #if defined(ANDROID) static void SetAndroidAppInfo(JavaVM* vm, JNIEnv* env, jobject activity); static void ClearAndroidAppInfo(JNIEnv* env); enum class AndroidThreadType { ApplicationMain, ApplicationWorker, RendererMain, RendererWorker, }; // Register the calling thread with runtimes that expose XR_KHR_android_thread_settings. // Returns false when the extension is unavailable or the runtime rejects the request. bool RegisterCurrentAndroidThread(AndroidThreadType type, std::string_view label = {}); // Ask Quest/OpenXR runtimes for the highest available CPU/GPU performance level. bool RequestAndroidHighPerformanceLevel(); #endif // 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. Starts the XR pacing thread // (when enabled); passing nullptr stops it before the backend tears swapchains down. void SetSwapchain(IOpenXRSwapchain* swapchain); IOpenXRSwapchain* GetSwapchain() const { return m_swapchain; } // Gracefully leaves a running session. Stops the frame loop, requests the runtime's // STOPPING transition, and pumps events until xrEndSession has been issued or a short // timeout expires. Safe to call repeatedly and from the destructor. void ShutdownSession(); // Destroys all objects owned by the current session while retaining the XrInstance and system. // This permits a new graphics session to be created without reconnecting the runtime. void DestroySession(); // 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); bool EndFrameDetached(XrTime display_time, XrEnvironmentBlendMode environment_blend_mode, bool should_render, const std::vector& layers, bool lock_graphics_queue = true); bool BeginStopConfirmation(std::string_view text); void EndStopConfirmation(); OpenXRStopConfirmationResult GetStopConfirmationResult() const { return m_stop_confirmation_result.load(std::memory_order_acquire); } // Ends the inline frame Presenter pre-began before temporarily transferring frame ownership // to the pacing thread for the stop-confirmation prompt. bool EndPreparedFrameForPacingThread(); // ---- XR pacing thread ---- // Owns the xrWaitFrame → xrBeginFrame → xrEndFrame loop (and the session event pump), // running at HMD cadence. Rendered frames are handed over via PublishFrame; when the // game runs slower than the display, the thread re-submits the last published layers // each cycle and the compositor reprojects them (ATW) with a fresh head pose. This // keeps the emulation/video thread free of blocking XR calls — on single-core games // every blocked ms was stolen straight from emulation. // // Started by SetSwapchain() when UseXRPacingThread is enabled; when inactive, the // legacy synchronous flow (Presenter calling Wait/Begin/End inline) applies. void StartFrameThread(); void StopFrameThread(); bool IsFrameThreadActive() const { return m_frame_thread_running.load(std::memory_order_acquire); } // Hand the pacing thread a rendered stereo frame. The views (incl. the poses the // content was rendered with) are copied; swapchain handles inside must stay valid // until StopFrameThread(). void PublishFrame(const std::array& views, XrCompositionLayerFlags layer_flags); // Flat mono panel variant: a fully built world-locked quad layer. void PublishQuadFrame(const XrCompositionLayerQuad& quad); // Stereoscopic virtual screen variant: two world-locked quad layers, one per eye. void PublishStereoQuadFrame(const std::array& quads); // Brackets the video thread's release-swapchain-images → publish-poses sequence. // Releasing an eye image flips the compositor's front image immediately, but the // matching pose only becomes visible to the pacing thread at PublishFrame — so an // eager heartbeat firing in between submits the NEW image with the OLD pose, which // ATW warps backwards (a previously shown frame flashes during head motion). The // pacing thread waits out this window before its own xrEndFrame. RAII helper below. void BeginVideoFrameHandoff() { m_video_handoff_active.fetch_add(1, std::memory_order_release); } void EndVideoFrameHandoff() { m_video_handoff_active.fetch_sub(1, std::memory_order_release); } class ScopedVideoFrameHandoff { public: explicit ScopedVideoFrameHandoff(OpenXRManager* mgr) : m_mgr(mgr) { if (m_mgr) { #ifdef BAIJI_STEAMVR m_handoff_lock = std::unique_lock(m_mgr->m_video_handoff_mutex); #endif m_mgr->BeginVideoFrameHandoff(); } } ~ScopedVideoFrameHandoff() { if (m_mgr) m_mgr->EndVideoFrameHandoff(); } ScopedVideoFrameHandoff(const ScopedVideoFrameHandoff&) = delete; ScopedVideoFrameHandoff& operator=(const ScopedVideoFrameHandoff&) = delete; private: OpenXRManager* m_mgr; #ifdef BAIJI_STEAMVR std::unique_lock m_handoff_lock; #endif }; // xrLocateViews — fills m_eye_views with the predicted head pose for each eye. // Call between BeginFrame and rendering. bool LocateViews(); // Request recentering of the VR home position. // Applied on the OpenXR render thread during LocateViews. void RequestRecenter(); // ---- Flat mono panel (vr_flat_screen) ---- // Aspect ratio (width / height) of the flat game panel. Set by the presenter each frame // before SubmitFlatFrame so the quad's world size matches the game's output. void SetFlatScreenAspect(float aspect) { m_flat_screen_aspect.store(aspect, std::memory_order_relaxed); } // Build a world-locked XrCompositionLayerQuad from an already-rendered mono swapchain image // and submit it via EndFrame(). Distance/size come from vr_screen_distance/vr_screen_size; // pose is captured on first use and recomputed on recenter. Backends call this from // SubmitFlatFrame(); passthrough layers are prepended centrally in EndFrameDetached. bool SubmitFlatQuadFrame(XrSwapchain swapchain, uint32_t width, uint32_t height); // Build two world-locked XrCompositionLayerQuad layers (one per eye) from already-rendered // eye swapchain images and submit them via EndFrame(). Backends call this from // SubmitStereoFlatFrame(); passthrough layers are prepended centrally in EndFrameDetached. bool SubmitStereoQuadFrame(std::array swapchains, uint32_t width, uint32_t height); // ---- 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; } // Frame-state snapshots. Written by whichever thread runs WaitFrame (the pacing // thread when active, the presenting thread otherwise) and readable from any thread. XrTime GetPredictedDisplayTime() const { return m_predicted_display_time_snapshot.load(std::memory_order_acquire); } double GetEstimatedDisplayPeriodMs() const { return m_estimated_display_period_ms.load(std::memory_order_acquire); } float GetStartupDisplayRefreshRateHz() const { return m_startup_display_refresh_rate_hz; } // HMD's native display period from XrFrameState (e.g. 11.11ms for 90Hz) double GetNativeDisplayPeriodMs() const { return static_cast(m_predicted_display_period_snapshot.load( std::memory_order_acquire)) / 1000000.0; } bool IsSessionRunning() const { return m_session_running.load(std::memory_order_acquire); } bool IsSessionFocused() const { return m_session_focused.load(std::memory_order_acquire); } bool ShouldRender() const { return m_should_render_snapshot.load(std::memory_order_acquire); } const std::array& GetEyeViews() const { return m_eye_views; } // The pose actually used by the GS cache to render the current game frame. const std::array& GetRenderedEyeViews() const { return m_rendered_eye_views; } // The pose submitted to OpenXR for the current game frame. Usually this matches // GetRenderedEyeViews(), but no-tracking mode submits the real HMD pose so the // fixed rendered view stays locked to the headset instead of the play space. const std::array& GetSubmittedEyeViews() const { return m_submitted_eye_views; } void RecordRenderedEyeViews(); // XFB pose stamping. With deferred (VI-time) presentation the XFB being presented // finished rendering earlier — by present time the next game frame's first draw may // already have overwritten m_submitted_eye_views with ITS pose, so submitting the // live snapshot pairs old content with a newer pose and the compositor's ATW warps // it to the wrong place (world jumps between "old" and "new" positions per publish). // StampXFBPose() runs at the XFB copy (video thread, FIFO-ordered — the completed // frame's pose is still current there); SelectPresentPoseForXFB() runs when the // presenter fetches an XFB for display; GetPresentEyeViews() is what SubmitFrame // must publish. All three are video-thread-only, like m_submitted_eye_views. void StampXFBPose(uint32_t xfb_addr); void SelectPresentPoseForXFB(uint32_t xfb_addr); const std::array& GetPresentEyeViews() const { return m_present_eye_views_valid ? m_present_eye_views : m_submitted_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 projection rows from the same tracked HMD pose without applying game-camera // adjustments (Free Look is applied by GeometryShaderManager, while Camera Forward/Height and // Camera Anchor are applied in GetEyeProjectionRows). World-fixed screen/HUD routes use these // rows so moving the game camera does not also move the virtual screen. void GetTrackedEyeProjectionRows( float units_per_meter, std::array, 4>& out_proj_rows, std::array, 2>& out_z_rows) const; // ---- Camera Anchor (Elements Group Override "CameraAnchor" handling) ---- // Anchors the VR camera to a game element's view-space origin (e.g. a character's // head for first-person view). Video-thread only, like m_submitted_eye_views. // SetPendingCameraAnchor is called at flush time when an anchor element draws // (first call per frame wins); CommitCameraAnchorFrame runs once per frame at // VertexManagerBase::OnEndFrame and latches + smooths the position, so the offset // GetEyeProjectionRows applies never changes mid-frame (all draws of a frame must // share one camera). Position is in game units, game view space — the same space // and units as the ex/ey/ez eye offsets in GetEyeProjectionRows. // // rotation is a row-major 3x3 whose columns are the camera rig's axes expressed in // view space (identity = camera stays aligned with the game camera). It may carry // the raw scale of the element's matrix — it is orthonormalized at commit time. // // units_per_meter is the anchor's own world scale (<= 0 = keep the global setting), so a // first-person anchor can have a different scale than the game's configured value. void SetPendingCameraAnchor(float x, float y, float z, const std::array& rotation, float units_per_meter); void CommitCameraAnchorFrame(); // World scale everything VR-side must use this frame: the active camera anchor's scale while // one is engaged (smoothed, so engaging/releasing glides), else the live global setting. float GetEffectiveUnitsPerMeter() const; // ---- Controller Anchor (Elements Group Override "ControllerAnchor" handling) ---- // Maps a VR controller's aim pose into game view space (game units) by replaying the // eye-position chain of GetEyeProjectionRows with the controller substituted for the // eye, so an element repositioned to the result appears at the physical controller. // out_rotation (optional) receives the controller's orientation in view space as a // row-major 3x3 (camera-anchor rig rotation included): directions map identically // between reference space and game view space — the same identification the position // path relies on. Video-thread only. While the head-pose lock is effective the result // is latched per game frame (same cadence as the GS eye-projection cache) so an // anchored element never moves mid-frame; InvalidateControllerAnchorCache() must be // called wherever GeometryShaderManager::InvalidateVRHeadPose() is. Returns false // when the controller pose is unavailable (the element then renders untouched). bool GetControllerAnchorViewPose(int hand, float units_per_meter, std::array* out_position, std::array* out_rotation); void InvalidateControllerAnchorCache(); // 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; // True for Virtual Desktop / Quest-class runtimes (strict about runtime-owned Vulkan // swapchain image synchronization). Used to scope the release-path GPU-drain fallback. bool IsQuestOrVirtualDesktopRuntime() const; // ---- XR_FB_foveation (fixed foveated rendering) ---- // Foveation extensions the runtime advertises, for the backend's CreateInstance list: // XR_FB_foveation, XR_FB_foveation_configuration, XR_FB_swapchain_update_state, and // (when for_vulkan) XR_FB_foveation_vulkan. Empty when the base extensions are missing. // Call before CreateInstance(). static std::vector GetAvailableFoveationExtensions(bool for_vulkan); // True when the foveation extensions are enabled on this instance and the user's // FoveationLevel setting is not Off. Backends check this before creating swapchains // with a foveation profile request. bool IsFoveationUsable() const; // Create a foveation profile from the FoveationLevel/DynamicFoveation settings and // apply it to the swapchain via xrUpdateSwapchainFB. Call after xrCreateSwapchain // (the swapchain must have been created with XrSwapchainCreateInfoFoveationFB). bool ApplyFoveationToSwapchain(XrSwapchain swapchain); // True when the runtime supports XR_ENVIRONMENT_BLEND_MODE_ALPHA_BLEND. bool SupportsAlphaBlend() const; // True when the headset can show its camera feed behind transparent projection-layer // pixels, either via XR_FB_passthrough (Quest family, standalone and Link) or via the // ALPHA_BLEND environment blend mode (Varjo, HoloLens, ...). bool SupportsPassthrough() const; // Blend mode for xrEndFrame. ALPHA_BLEND only when passthrough is on and the runtime // has no XR_FB_passthrough (the FB layer composites behind an OPAQUE projection layer). XrEnvironmentBlendMode GetActiveBlendMode() const; // Extra XrCompositionLayerFlags the backends must set on the projection layer so the // compositor reads its per-pixel alpha. 0 when passthrough is off or unsupported. XrCompositionLayerFlags GetProjectionLayerExtraFlags() const; private: struct PublishedXRFrame; void GetEyeProjectionRowsImpl( float units_per_meter, bool apply_game_camera, std::array, 4>& out_proj_rows, std::array, 2>& out_z_rows) const; // XR_FB_passthrough: usable when the extension loaded and the system reports support. bool IsFBPassthroughUsable() const; // Create/start or pause the FB passthrough feed to match the Passthrough setting. // Requires an XrSession; called once per submitted frame from EndFrameDetached. void UpdateFBPassthrough(bool enable); void DestroyFBPassthrough(); bool InitializeInputActions(); void DestroyInputActions(); void UpdateInputActions(); void UpdateStopConfirmationInput( const std::array& controllers); void CacheSubmittedFrame(const std::vector& layers); void UpdateHaptics(); // Core of GetControllerAnchorViewPose without the per-frame cache: maps an aim pose // (reference space) into game view space. Frame/video-thread only. bool MapAimPoseToGameView(const Common::VR::OpenXRPoseState& aim, float units_per_meter, std::array* out_position, std::array* out_rotation) const; // Computes the intersection of the aim ray with the virtual screen (flat panel quad in // flat-screen mode, ortho virtual screen otherwise), using the same transform chain the // renderer uses to place that screen. void ComputeVirtualScreenHit(const Common::VR::OpenXRPoseState& aim, Common::VR::OpenXRScreenHit* out_hit) const; // XrTime of "now" via the platform time-conversion extension; falls back to the // predicted display time when unavailable. Input wants measured poses — locating at the // predicted display time extrapolates fast controller motion several frames ahead. XrTime GetInputSampleTime(); void EnsureHomePositionFromCurrentViews() const; std::array GetTrackingAdjustedEyeViews() const; void ResetInputActionsState(); void SetSessionFocused(bool focused, bool pause_emulation_when_unfocused = true); void HandleSessionStateChange(XrSessionState new_state); // World-locked quad pose for the flat panel. Refresh from video-thread view data after a // reference-space rebase; serialize readers because input sampling also queries this pose. XrPosef GetFlatScreenPose() const; void RefreshFlatScreenPose(); void CaptureStartupDisplayRefreshRateFromExtension(); void SetStartupDisplayRefreshRate(float refresh_rate_hz, std::string_view source); // Request a specific display refresh rate from the OpenXR runtime. Only supported when // XR_FB_display_refresh_rate is available; silently ignored otherwise. Rate is clamped // to a positive value. Returns true if the request was issued (not necessarily honored). bool RequestDisplayRefreshRate(float refresh_rate_hz); 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; XrReferenceSpaceType m_reference_space_type = XR_REFERENCE_SPACE_TYPE_LOCAL; uint32_t m_max_layer_count = 0; // Non-owning pointer; lifetime managed by the backend (D3DOpenXR). IOpenXRSwapchain* m_swapchain = nullptr; std::vector m_enabled_extensions; std::string m_runtime_name; std::string m_system_name; // Cached IsQuestOrVirtualDesktopRuntime() result. Derived from the runtime/system names // (fixed after instance/system init) and queried per draw on the Vulkan path — the name // scans must not run per draw (profiled hot). mutable std::optional m_quest_or_vd_runtime; uint32_t m_system_vendor_id = 0; PFN_xrGetDisplayRefreshRateFB m_xrGetDisplayRefreshRateFB = nullptr; PFN_xrRequestDisplayRefreshRateFB m_xrRequestDisplayRefreshRateFB = nullptr; // XR_FB_foveation entry points (null when the extension is unavailable). PFN_xrCreateFoveationProfileFB m_xrCreateFoveationProfileFB = nullptr; PFN_xrDestroyFoveationProfileFB m_xrDestroyFoveationProfileFB = nullptr; PFN_xrUpdateSwapchainFB m_xrUpdateSwapchainFB = nullptr; // XR_FB_passthrough entry points (null when the extension is unavailable). PFN_xrCreatePassthroughFB m_xrCreatePassthroughFB = nullptr; PFN_xrDestroyPassthroughFB m_xrDestroyPassthroughFB = nullptr; PFN_xrPassthroughStartFB m_xrPassthroughStartFB = nullptr; PFN_xrPassthroughPauseFB m_xrPassthroughPauseFB = nullptr; PFN_xrCreatePassthroughLayerFB m_xrCreatePassthroughLayerFB = nullptr; PFN_xrDestroyPassthroughLayerFB m_xrDestroyPassthroughLayerFB = nullptr; PFN_xrPassthroughLayerPauseFB m_xrPassthroughLayerPauseFB = nullptr; PFN_xrPassthroughLayerResumeFB m_xrPassthroughLayerResumeFB = nullptr; // Camera anchor state (video thread only; game units, game view space). // pending: raw capture from this frame's anchor draw. target: latched value the // smoothed position converges to — held for a few frames when the anchor element // temporarily disappears (culling), then released so the camera glides back to // the default position ({0,0,0} = no offset). bool m_camera_anchor_pending_valid = false; std::array m_camera_anchor_pending{}; bool m_camera_anchor_has_target = false; std::array m_camera_anchor_target{}; int m_camera_anchor_missing_frames = 0; std::array m_camera_anchor_position{}; // Anchor rotation (row-major 3x3, columns = rig axes in view space). The smoothed // matrix is re-orthonormalized every commit; m_camera_anchor_rotation_active caches // whether it differs from identity so GetEyeProjectionRows can skip the multiplies. std::array m_camera_anchor_pending_rotation{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f}; std::array m_camera_anchor_target_rotation{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f}; std::array m_camera_anchor_rotation{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f}; bool m_camera_anchor_rotation_active = false; // Per-anchor world scale. *_target is the override's requested value (0 = none); the smoothed // current value is 0 while disengaged so GetEffectiveUnitsPerMeter passes the global setting // through untouched (dragging the global slider must not lag). float m_camera_anchor_pending_upm = 0.0f; float m_camera_anchor_target_upm = 0.0f; float m_camera_anchor_upm = 0.0f; // Cached xrGetInstanceProcAddr result for the platform time-conversion function // (xrConvertWin32PerformanceCounterToTimeKHR / xrConvertTimespecTimeToTimeKHR). PFN_xrVoidFunction m_pfn_convert_now_to_time = nullptr; // Controller Anchor per-frame cache (video-thread only). Under the head-pose lock a // valid entry is reused until InvalidateControllerAnchorCache() so all draws of a game // frame see one hand pose; the upm is stored because it scales the position. std::array m_controller_anchor_cache_valid{false, false}; std::array, 2> m_controller_anchor_cache{}; std::array, 2> m_controller_anchor_cache_rot{}; std::array m_controller_anchor_cache_upm{}; // XR_FB_passthrough state. The composition layer member gives the struct stable // storage across the xrEndFrame call that references it. bool m_system_supports_fb_passthrough = false; XrPassthroughFB m_fb_passthrough = XR_NULL_HANDLE; XrPassthroughLayerFB m_fb_passthrough_layer = XR_NULL_HANDLE; bool m_fb_passthrough_running = false; bool m_fb_passthrough_create_failed = false; XrCompositionLayerPassthroughFB m_fb_passthrough_composition_layer{ XR_TYPE_COMPOSITION_LAYER_PASSTHROUGH_FB}; #if defined(ANDROID) PFN_xrVoidFunction m_xrSetAndroidApplicationThreadKHR = nullptr; PFN_xrVoidFunction m_xrPerfSettingsSetPerformanceLevelEXT = nullptr; // Threads that called RegisterCurrentAndroidThread before the XrSession existed. // Replayed by FlushPendingAndroidThreadRegistrations() from SetSession(). // Reason: Meta's runtime needs threads tagged via XR_KHR_android_thread_settings to // apply big.LITTLE pinning + DVFS escalation; without tags it keeps clocks idle. struct PendingAndroidThreadRegistration { uint32_t thread_id; AndroidThreadType type; std::string label; }; std::mutex m_pending_thread_registrations_mutex; std::vector m_pending_thread_registrations; void FlushPendingAndroidThreadRegistrations(); #endif XrSessionState m_session_state = XR_SESSION_STATE_UNKNOWN; std::atomic m_session_running{false}; std::atomic m_session_focused{false}; std::mutex m_session_focus_mutex; bool m_focus_loss_pause_attempted = false; bool m_emulation_paused_for_focus_loss = false; bool m_focus_pause_pending = false; bool m_resume_after_pending_focus_pause = false; bool m_exit_render_loop = false; XrFrameState m_frame_state{XR_TYPE_FRAME_STATE}; XrViewStateFlags m_view_state_flags = 0; // Cross-thread snapshots of the latest xrWaitFrame result (see accessors above). std::atomic m_predicted_display_time_snapshot{0}; std::atomic m_predicted_display_period_snapshot{0}; std::atomic m_should_render_snapshot{false}; // ---- XR pacing thread state ---- void FrameThreadLoop(); struct PublishedXRFrame { bool valid = false; bool is_quad = false; bool is_stereo_quad = false; std::array views{}; XrCompositionLayerFlags layer_flags = 0; XrCompositionLayerQuad quad{XR_TYPE_COMPOSITION_LAYER_QUAD}; std::array stereo_quads{}; uint32_t stereo_quad_count = 0; }; std::thread m_frame_thread; std::atomic m_frame_thread_running{false}; std::atomic m_frame_thread_should_exit{false}; std::mutex m_publish_mutex; std::condition_variable m_publish_cv; PublishedXRFrame m_published_frame; // guarded by m_publish_mutex uint64_t m_publish_serial = 0; // guarded by m_publish_mutex // >0 while the video thread is mid release-images/publish-poses (see handoff helpers). std::atomic m_video_handoff_active{0}; // Frame: exclude xrEndFrame from the whole stereo image handoff. The atomic // flag alone has a check-to-submit race, allowing a new left image and old right // image to be selected together. Lock order: handoff -> publish or graphics queue; // neither publish nor graphics queue may be held while taking this lock. std::mutex m_video_handoff_mutex; std::atomic m_frame_begun{false}; std::atomic m_stop_confirmation_active{false}; std::atomic m_stop_confirmation_result{ OpenXRStopConfirmationResult::Idle}; std::mutex m_stop_confirmation_input_mutex; std::optional m_stop_confirmation_neutral_since; bool m_stop_confirmation_armed = false; bool m_stop_confirmation_previous_primary = false; bool m_stop_confirmation_previous_secondary = false; // 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_frame_x_click = XR_NULL_HANDLE; XrAction m_action_frame_y_click = XR_NULL_HANDLE; XrAction m_action_menu_click = XR_NULL_HANDLE; XrAction m_action_system_click = XR_NULL_HANDLE; XrAction m_action_view_click = XR_NULL_HANDLE; XrAction m_action_bumper_click = XR_NULL_HANDLE; XrAction m_action_dpad_up_click = XR_NULL_HANDLE; XrAction m_action_dpad_down_click = XR_NULL_HANDLE; XrAction m_action_dpad_left_click = XR_NULL_HANDLE; XrAction m_action_dpad_right_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_squeeze_force = 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_logged_interaction_profiles{XR_NULL_PATH, XR_NULL_PATH}; std::array m_view_config_views{}; std::array m_views{}; std::vector m_supported_blend_modes; std::array m_eye_views{}; std::array m_rendered_eye_views{}; std::array m_submitted_eye_views{}; // XFB pose stamps (see StampXFBPose). Small ring keyed by XFB address: games cycle // 2-3 XFB buffers, and VI duplicate presents must find the pose of an OLDER XFB // even after a newer copy landed. serial == 0 marks an empty slot. struct XFBPoseStamp { uint32_t xfb_addr = 0; uint64_t serial = 0; std::array views{}; }; std::array m_xfb_pose_stamps{}; size_t m_xfb_pose_stamp_next = 0; uint64_t m_xfb_pose_stamp_serial = 0; std::array m_present_eye_views{}; bool m_present_eye_views_valid = false; XrTime m_last_predicted_display_time = 0; std::atomic m_estimated_display_period_ms{0.0}; float m_startup_display_refresh_rate_hz = 0.0f; // "Home" head-center position, recorded from the first stable tracked pose. // 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}; // Flat panel state. Reference-space events request a video-thread pose refresh; cached pose // access is synchronized because OpenXR input sampling runs on the pacing thread. The // composition layer member gives stable storage across xrEndFrame calls that reference it. std::atomic m_flat_screen_aspect{16.0f / 9.0f}; std::atomic m_flat_screen_reanchor_pending{false}; mutable std::mutex m_flat_screen_pose_mutex; mutable bool m_flat_screen_pose_valid = false; mutable XrPosef m_flat_screen_pose{{0.f, 0.f, 0.f, 1.f}, {0.f, 0.f, 0.f}}; XrCompositionLayerQuad m_flat_quad_layer{XR_TYPE_COMPOSITION_LAYER_QUAD}; }; // Global instance — created by the backend during VideoBackend::Initialize(). extern std::unique_ptr g_openxr; } // namespace VR #endif // ENABLE_VR