mirror of
https://github.com/thejerf/baijiSteamVR.git
synced 2026-10-19 19:03:06 +02:00
Trying to stop the emulation in VR or S3D modes would result in a QT dialog box popping up about whether you were sure you wanted to end the emulation, but that caused no visible effect on the screen. Now there's a dialog visible for it when emulating in those modes, and it should correctly work for flat emulation and when pressing the Stop button in the QT UI. This fixes #11.
792 lines
38 KiB
C++
792 lines
38 KiB
C++
// Copyright 2024 Dolphin Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
#pragma once
|
|
|
|
#ifdef ENABLE_VR
|
|
|
|
#include <array>
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <condition_variable>
|
|
#include <cstdint>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
#include <openxr/openxr.h>
|
|
|
|
#if defined(ANDROID)
|
|
#include <jni.h>
|
|
#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<std::mutex> 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<const char*> 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<const char*>& 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<XrCompositionLayerBaseHeader*>& layers);
|
|
bool EndFrameDetached(XrTime display_time, XrEnvironmentBlendMode environment_blend_mode,
|
|
bool should_render,
|
|
const std::vector<XrCompositionLayerBaseHeader*>& 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<XrCompositionLayerProjectionView, 2>& 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<XrCompositionLayerQuad, 2>& 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<std::mutex>(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<std::mutex> 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<XrSwapchain, 2> 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<double>(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<XREyeView, 2>& GetEyeViews() const { return m_eye_views; }
|
|
// The pose actually used by the GS cache to render the current game frame.
|
|
const std::array<XREyeView, 2>& 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<XREyeView, 2>& 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<XREyeView, 2>& GetPresentEyeViews() const
|
|
{
|
|
return m_present_eye_views_valid ? m_present_eye_views : m_submitted_eye_views;
|
|
}
|
|
const std::array<XrViewConfigurationView, 2>& 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<std::array<float, 4>, 4>& out_proj_rows,
|
|
std::array<std::array<float, 4>, 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<std::array<float, 4>, 4>& out_proj_rows,
|
|
std::array<std::array<float, 4>, 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<float, 9>& 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<float, 3>* out_position,
|
|
std::array<float, 9>* 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<std::array<float, 4>, 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<const char*> 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<std::array<float, 4>, 4>& out_proj_rows,
|
|
std::array<std::array<float, 4>, 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<Common::VR::OpenXRControllerState, 2>& controllers);
|
|
void CacheSubmittedFrame(const std::vector<XrCompositionLayerBaseHeader*>& 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<float, 3>* out_position,
|
|
std::array<float, 9>* 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<XREyeView, 2> 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<std::string> 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<bool> 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<float, 3> m_camera_anchor_pending{};
|
|
bool m_camera_anchor_has_target = false;
|
|
std::array<float, 3> m_camera_anchor_target{};
|
|
int m_camera_anchor_missing_frames = 0;
|
|
std::array<float, 3> 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<float, 9> 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<float, 9> 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<float, 9> 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<bool, 2> m_controller_anchor_cache_valid{false, false};
|
|
std::array<std::array<float, 3>, 2> m_controller_anchor_cache{};
|
|
std::array<std::array<float, 9>, 2> m_controller_anchor_cache_rot{};
|
|
std::array<float, 2> 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<PendingAndroidThreadRegistration> m_pending_thread_registrations;
|
|
|
|
void FlushPendingAndroidThreadRegistrations();
|
|
#endif
|
|
|
|
XrSessionState m_session_state = XR_SESSION_STATE_UNKNOWN;
|
|
std::atomic<bool> m_session_running{false};
|
|
std::atomic<bool> 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<XrTime> m_predicted_display_time_snapshot{0};
|
|
std::atomic<int64_t> m_predicted_display_period_snapshot{0};
|
|
std::atomic<bool> 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<XrCompositionLayerProjectionView, 2> views{};
|
|
XrCompositionLayerFlags layer_flags = 0;
|
|
XrCompositionLayerQuad quad{XR_TYPE_COMPOSITION_LAYER_QUAD};
|
|
std::array<XrCompositionLayerQuad, 2> stereo_quads{};
|
|
uint32_t stereo_quad_count = 0;
|
|
};
|
|
|
|
std::thread m_frame_thread;
|
|
std::atomic<bool> m_frame_thread_running{false};
|
|
std::atomic<bool> 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<int> 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<bool> m_frame_begun{false};
|
|
std::atomic<bool> m_stop_confirmation_active{false};
|
|
std::atomic<OpenXRStopConfirmationResult> m_stop_confirmation_result{
|
|
OpenXRStopConfirmationResult::Idle};
|
|
std::mutex m_stop_confirmation_input_mutex;
|
|
std::optional<std::chrono::steady_clock::time_point> 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<XrPath, 2> 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<XrSpace, 2> m_aim_spaces{XR_NULL_HANDLE, XR_NULL_HANDLE};
|
|
std::array<XrSpace, 2> m_grip_spaces{XR_NULL_HANDLE, XR_NULL_HANDLE};
|
|
std::array<bool, 2> m_haptics_active{false, false};
|
|
std::array<XrPath, 2> m_logged_interaction_profiles{XR_NULL_PATH, XR_NULL_PATH};
|
|
|
|
std::array<XrViewConfigurationView, 2> m_view_config_views{};
|
|
std::array<XrView, 2> m_views{};
|
|
std::vector<XrEnvironmentBlendMode> m_supported_blend_modes;
|
|
std::array<XREyeView, 2> m_eye_views{};
|
|
std::array<XREyeView, 2> m_rendered_eye_views{};
|
|
std::array<XREyeView, 2> 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<XREyeView, 2> views{};
|
|
};
|
|
std::array<XFBPoseStamp, 8> m_xfb_pose_stamps{};
|
|
size_t m_xfb_pose_stamp_next = 0;
|
|
uint64_t m_xfb_pose_stamp_serial = 0;
|
|
std::array<XREyeView, 2> m_present_eye_views{};
|
|
bool m_present_eye_views_valid = false;
|
|
XrTime m_last_predicted_display_time = 0;
|
|
std::atomic<double> 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<bool> 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<float> m_flat_screen_aspect{16.0f / 9.0f};
|
|
std::atomic<bool> 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<OpenXRManager> g_openxr;
|
|
|
|
} // namespace VR
|
|
|
|
#endif // ENABLE_VR
|