diff --git a/Source/Core/VideoBackends/Vulkan/VKMain.cpp b/Source/Core/VideoBackends/Vulkan/VKMain.cpp index 04107274ca..c37de4c3f3 100644 --- a/Source/Core/VideoBackends/Vulkan/VKMain.cpp +++ b/Source/Core/VideoBackends/Vulkan/VKMain.cpp @@ -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(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(); - 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 diff --git a/Source/Core/VideoBackends/Vulkan/VulkanOpenXR.cpp b/Source/Core/VideoBackends/Vulkan/VulkanOpenXR.cpp index be6349ebd3..b5c28deb1e 100644 --- a/Source/Core/VideoBackends/Vulkan/VulkanOpenXR.cpp +++ b/Source/Core/VideoBackends/Vulkan/VulkanOpenXR.cpp @@ -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()) { diff --git a/Source/Core/VideoBackends/Vulkan/VulkanOpenXR.h b/Source/Core/VideoBackends/Vulkan/VulkanOpenXR.h index ed1b11e52e..c0293ec021 100644 --- a/Source/Core/VideoBackends/Vulkan/VulkanOpenXR.h +++ b/Source/Core/VideoBackends/Vulkan/VulkanOpenXR.h @@ -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 m_projection_views{}; diff --git a/Source/Core/VideoCommon/VR/OpenXRManager.cpp b/Source/Core/VideoCommon/VR/OpenXRManager.cpp index 9d88acf300..5f7cade0b0 100644 --- a/Source/Core/VideoCommon/VR/OpenXRManager.cpp +++ b/Source/Core/VideoCommon/VR/OpenXRManager.cpp @@ -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 lock(m_publish_mutex); - m_publish_cv.notify_all(); + m_frame_thread_should_exit.store(true, std::memory_order_release); + { + std::lock_guard 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; diff --git a/Source/Core/VideoCommon/VR/OpenXRManager.h b/Source/Core/VideoCommon/VR/OpenXRManager.h index 20bde17b85..9c16c3c581 100644 --- a/Source/Core/VideoCommon/VR/OpenXRManager.h +++ b/Source/Core/VideoCommon/VR/OpenXRManager.h @@ -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 m_frame_thread_running{false}; std::atomic m_frame_thread_should_exit{false}; + std::atomic 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 diff --git a/Source/Core/VideoCommon/VideoConfig.cpp b/Source/Core/VideoCommon/VideoConfig.cpp index b4f49cb120..3a7f59dcb8 100644 --- a/Source/Core/VideoCommon/VideoConfig.cpp +++ b/Source/Core/VideoCommon/VideoConfig.cpp @@ -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); diff --git a/Source/Core/VideoCommon/VideoConfig.h b/Source/Core/VideoCommon/VideoConfig.h index 7ca637387d..66c5cb5a51 100644 --- a/Source/Core/VideoCommon/VideoConfig.h +++ b/Source/Core/VideoCommon/VideoConfig.h @@ -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