OpenXR: keep controller input in Flat Vulkan mode

This commit is contained in:
Jeremy Bowers committed 2026-10-03 21:00:54 -04:00
1 parent 3ec01541fd
commit bec9d1beae
7 files changed
+59 -30

No files matched your search

+4 -4
View File
@@ -149,7 +149,7 @@ bool VideoBackend::Initialize(const WindowSystemInfo& wsi)
// both the instance and device; without them xrCreateSession will crash.
Vulkan::VulkanExtensionRequirements vr_ext_requirements;
bool vr_extensions_queried = false;
if (g_Config.VRSessionActive())
if (g_Config.OpenXRSessionActive())
{
vr_extensions_queried = Vulkan::VulkanOpenXR::PreQueryVulkanExtensions(vr_ext_requirements);
if (!vr_extensions_queried)
@@ -396,10 +396,10 @@ bool VideoBackend::Initialize(const WindowSystemInfo& wsi)
static_cast<int>(g_ActiveConfig.stereo_mode),
g_ActiveConfig.VRSessionActive() ? "YES" : "NO",
g_ActiveConfig.vr_flat_screen ? "YES" : "NO");
if (g_ActiveConfig.VRSessionActive())
if (g_ActiveConfig.OpenXRSessionActive())
{
auto openxr = std::make_unique<Vulkan::VulkanOpenXR>();
if (!openxr->Initialize())
if (!openxr->Initialize(!g_ActiveConfig.VRSessionActive()))
{
WARN_LOG_FMT(VIDEO, "OpenXR initialization failed; continuing without VR.");
}
@@ -424,7 +424,7 @@ void VideoBackend::Shutdown()
#ifdef ENABLE_VR
// Whether VR actually ran this session. Captured before the OpenXR objects are torn down so the
// persist decision below can tell a VR game stop from a plain non-VR shutdown.
const bool vr_active = Vulkan::g_openxr_vk != nullptr;
const bool vr_active = Vulkan::g_openxr_vk && !Vulkan::g_openxr_vk->IsInputOnly();
// The OpenXR pacing thread can be inside xrEndFrame(), where the runtime accesses our graphics
// queue. Stop it before waiting on the Vulkan device: vkDeviceWaitIdle() must not race an
@@ -547,9 +547,10 @@ bool VulkanOpenXR::PreQueryVulkanExtensions(VulkanExtensionRequirements& out)
return true;
}
bool VulkanOpenXR::Initialize()
bool VulkanOpenXR::Initialize(bool input_only)
{
INFO_LOG_FMT(VIDEO, "OpenXR Vulkan: Starting initialization...");
m_input_only = input_only;
// If PreQueryVulkanExtensions() was called, VR::g_openxr already exists.
if (!VR::g_openxr)
@@ -615,6 +616,13 @@ bool VulkanOpenXR::Initialize()
return false;
}
if (input_only)
{
INFO_LOG_FMT(VIDEO, "OpenXR Vulkan: Starting input-only session for Flat Vulkan controls.");
VR::g_openxr->StartInputOnlyFrameThread();
return true;
}
INFO_LOG_FMT(VIDEO, "OpenXR Vulkan: Creating swapchains...");
if (!CreateSwapchains())
{
@@ -104,10 +104,12 @@ public:
// OpenXR runtime, and stores the OpenXRManager in VR::g_openxr for later reuse.
static bool PreQueryVulkanExtensions(VulkanExtensionRequirements& out_requirements);
// Full initialization: creates Vulkan-bound XrSession, reference space, and
// per-eye swapchains. If PreQueryVulkanExtensions() was called, reuses the
// existing VR::g_openxr; otherwise creates a new one.
bool Initialize();
// Creates a Vulkan-bound XrSession and reference space. Normal VR also creates per-eye
// swapchains; input_only starts an empty-layer pacing loop for controller input while the
// game's Vulkan window surface remains the output. If PreQueryVulkanExtensions() was called,
// reuses the existing VR::g_openxr; otherwise creates a new one.
bool Initialize(bool input_only = false);
bool IsInputOnly() const { return m_input_only; }
// Tears down swapchains and resets g_openxr.
void Shutdown();
@@ -205,6 +207,7 @@ private:
// True when any swapchain framebuffer carries a fragment density map; PostProcessing
// compiles matching foveated pipelines when set.
bool m_foveated = false;
bool m_input_only = false;
// Reused per-frame composition data (avoids per-frame heap allocation).
std::array<XrCompositionLayerProjectionView, 2> m_projection_views{};
+18 -10
View File
@@ -1073,19 +1073,26 @@ void OpenXRManager::StartFrameThread()
INFO_LOG_FMT(OPENXR, "OpenXR: XR pacing thread started.");
}
void OpenXRManager::StartInputOnlyFrameThread()
{
m_input_only_frame_thread.store(true, std::memory_order_release);
StartFrameThread();
}
void OpenXRManager::StopFrameThread()
{
if (!m_frame_thread.joinable())
return;
m_frame_thread_should_exit.store(true, std::memory_order_release);
if (m_frame_thread.joinable())
{
std::lock_guard<std::mutex> lock(m_publish_mutex);
m_publish_cv.notify_all();
m_frame_thread_should_exit.store(true, std::memory_order_release);
{
std::lock_guard<std::mutex> lock(m_publish_mutex);
m_publish_cv.notify_all();
}
m_frame_thread.join();
m_frame_thread_running.store(false, std::memory_order_release);
INFO_LOG_FMT(OPENXR, "OpenXR: XR pacing thread stopped.");
}
m_frame_thread.join();
m_frame_thread_running.store(false, std::memory_order_release);
INFO_LOG_FMT(OPENXR, "OpenXR: XR pacing thread stopped.");
m_input_only_frame_thread.store(false, std::memory_order_release);
}
void OpenXRManager::ShutdownSession()
@@ -1291,7 +1298,8 @@ void OpenXRManager::FrameThreadLoop()
while (!m_frame_thread_should_exit.load(std::memory_order_acquire))
{
const bool eager_heartbeat = g_ActiveConfig.vr_eager_heartbeat;
const bool eager_heartbeat = m_input_only_frame_thread.load(std::memory_order_acquire) ||
g_ActiveConfig.vr_eager_heartbeat;
const u64 cycle_start_us = Common::Timer::NowUs();
if (!PollEvents())
break;
@@ -219,6 +219,9 @@ public:
// Started by SetSwapchain() when UseXRPacingThread is enabled; when inactive, the
// legacy synchronous flow (Presenter calling Wait/Begin/End inline) applies.
void StartFrameThread();
// Run the XR frame protocol without a graphics swapchain so OpenXR controller actions remain
// live while Flat Vulkan mode renders only to its desktop surface.
void StartInputOnlyFrameThread();
void StopFrameThread();
bool IsFrameThreadActive() const
{
@@ -637,6 +640,7 @@ private:
std::thread m_frame_thread;
std::atomic<bool> m_frame_thread_running{false};
std::atomic<bool> m_frame_thread_should_exit{false};
std::atomic<bool> m_input_only_frame_thread{false};
std::mutex m_publish_mutex;
std::condition_variable m_publish_cv;
PublishedXRFrame m_published_frame; // guarded by m_publish_mutex
+1 -2
View File
@@ -164,8 +164,7 @@ void VideoConfig::Refresh()
color_correction.fHDRPaperWhiteNits = Config::Get(Config::GFX_CC_HDR_PAPER_WHITE_NITS);
stereo_mode = Config::Get(Config::GFX_STEREO_MODE);
const OpenXRPresentationMode vr_presentation_mode =
Config::Get(Config::GFX_VR_PRESENTATION_MODE);
vr_presentation_mode = Config::Get(Config::GFX_VR_PRESENTATION_MODE);
bool vr_openxr_enabled = Config::Get(Config::GFX_VR_ENABLE_OPENXR);
vr_flat_screen = Config::Get(Config::GFX_VR_FLAT_SCREEN);
vr_stereo_screen = Config::Get(Config::GFX_VR_STEREO_SCREEN);
+16 -9
View File
@@ -388,12 +388,13 @@ struct VideoConfig final
// them natively over each eye (like a Fullscreen override) instead of capturing them onto the
// virtual screen. Downscaled copies always qualify; full-res copies only when blended.
bool vr_auto_native_efb_effects = true;
// Render the whole game as a flat mono panel in the VR scene (StereoMode::Off), while the
// OpenXR session still runs. Used by the "Launch games in VR = off" cinema path on Quest.
bool vr_flat_screen = false;
// Render the game using classic SBS stereoscopy and submit it as a stereoscopic virtual screen
// (two OpenXR quad layers). Bypasses the immersive per-eye reprojection path.
bool vr_stereo_screen = false;
// Render the whole game as a flat mono panel in the VR scene (StereoMode::Off), while the
// OpenXR session still runs. Used by the "Launch games in VR = off" cinema path on Quest.
bool vr_flat_screen = false;
// Render the game using classic SBS stereoscopy and submit it as a stereoscopic virtual screen
// (two OpenXR quad layers). Bypasses the immersive per-eye reprojection path.
bool vr_stereo_screen = false;
OpenXRPresentationMode vr_presentation_mode = OpenXRPresentationMode::Legacy;
float vr_screen_distance = 1.5f;
float vr_screen_size = 1.5f;
float vr_hud_thickness = 0.0f; // World-space depth (m) spread across a 2D layer's ortho-Z (0 = flat)
@@ -489,13 +490,19 @@ float vr_screen_distance = 1.5f;
return bPreferVSForLinePointExpansion;
}
bool MultisamplingEnabled() const { return iMultisamples > 1; }
// True whenever an OpenXR session must run: per-eye stereo (StereoMode::OpenXR), the flat
// mono panel path, or the classic-SBS stereoscopic virtual-screen path. Use this for session
// lifecycle gates; keep immersive stereo-specific rendering on stereo_mode == OpenXR.
// True whenever game content is rendered/presented through OpenXR: immersive stereo, the flat
// mono panel path, or the classic-SBS stereoscopic virtual-screen path. Flat Vulkan output can
// separately run OpenXR for controller input without switching its renderer or presentation.
bool VRSessionActive() const
{
return stereo_mode == StereoMode::OpenXR || vr_flat_screen || vr_stereo_screen;
}
// Flat Vulkan output still keeps an OpenXR session alive for Frame controller actions.
// The session submits no composition layers, so rendering/presentation stays on the window.
bool OpenXRSessionActive() const
{
return VRSessionActive() || vr_presentation_mode == OpenXRPresentationMode::Vulkan;
}
// Hold one head pose for every draw of a game frame, refreshed only at the XFB-copy
// boundary. Required whenever ImmediateXFB is off: presentation then happens at VI
// time, interleaved with the NEXT frame's draw stream, so a per-draw pose refresh