Files
Jeremy Bowers 70d096f8d7 Fix issue with intermittent slowdown.
An error in the way frames were submitted to XR resulted in intermittent
slowdown that fooled me for a while as being shader compilation stutter,
and had me down the wrong track of investigating that.

However this was actually the root cause. This should bring Baiji up to
performance parity with the current Dolphin release, as near as I can tell.

Resolves Issue #13.
2026-10-16 01:00:07 -04:00

1954 lines
74 KiB
C++

// Copyright 2024 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#ifdef ENABLE_VR
// VulkanLoader.h must come first — it defines VK_NO_PROTOTYPES before vulkan.h.
#include "VideoBackends/Vulkan/VulkanLoader.h"
#define XR_USE_GRAPHICS_API_VULKAN
#include "VideoBackends/Vulkan/VulkanOpenXR.h"
#include <algorithm>
#include <array>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "Common/Assert.h"
#include "Common/Logging/Log.h"
#include "Common/ScopeGuard.h"
#include "Common/Timer.h"
#include "Core/Config/GraphicsSettings.h"
#include "VideoBackends/Vulkan/CommandBufferManager.h"
#include "VideoBackends/Vulkan/StateTracker.h"
#include "VideoBackends/Vulkan/VKTexture.h"
#include "VideoBackends/Vulkan/VulkanContext.h"
#include "VideoCommon/AbstractGfx.h"
#include "VideoCommon/TextureConfig.h"
#include "VideoCommon/VideoConfig.h"
#include "VideoCommon/OnScreenDisplay.h"
#include "VideoCommon/VR/OpenXRManager.h"
#ifdef _WIN32
#include <windows.h> // for SEH __try/__except
#endif
#if defined(ANDROID)
#include <android/log.h>
#endif
namespace Vulkan
{
std::unique_ptr<VulkanOpenXR> g_openxr_vk;
namespace
{
uint64_t ElapsedUs(uint64_t start_us, uint64_t end_us)
{
if (start_us == 0 || end_us <= start_us)
return 0;
return end_us - start_us;
}
template <typename T>
void DestroySwapchainVulkanObjects(T& swapchain)
{
const VkDevice device = g_vulkan_context->GetDevice();
// VKFramebuffer/VKTexture normally defer destruction until a command buffer is recycled.
// OpenXR owns the VkImages, however, and xrDestroySwapchain may destroy them immediately.
// Destroy our dependent Vulkan objects synchronously before returning ownership to OpenXR.
for (auto& framebuffer : swapchain.framebuffers)
{
if (framebuffer)
{
const VkFramebuffer handle = framebuffer->ReleaseHandle();
if (handle != VK_NULL_HANDLE)
vkDestroyFramebuffer(device, handle, nullptr);
}
}
swapchain.framebuffers.clear();
for (auto& texture : swapchain.textures)
{
if (texture)
{
const VkImageView view = texture->ReleaseView();
if (view != VK_NULL_HANDLE)
vkDestroyImageView(device, view, nullptr);
}
}
swapchain.textures.clear();
for (VkImageView view : swapchain.fdm_views)
vkDestroyImageView(device, view, nullptr);
swapchain.fdm_views.clear();
}
static void AppendOptionalOpenXRExtensions(std::vector<const char*>* extensions)
{
if (VR::OpenXRManager::IsRuntimeExtensionSupported(XR_FB_DISPLAY_REFRESH_RATE_EXTENSION_NAME))
{
extensions->push_back(XR_FB_DISPLAY_REFRESH_RATE_EXTENSION_NAME);
INFO_LOG_FMT(VIDEO, "OpenXR: Enabling XR_FB_display_refresh_rate.");
}
// Fixed foveated rendering: on Vulkan the runtime hands us fragment density map images
// (XR_FB_foveation_vulkan) that our swapchain render passes read.
const auto foveation_exts = VR::OpenXRManager::GetAvailableFoveationExtensions(true);
if (!foveation_exts.empty())
{
extensions->insert(extensions->end(), foveation_exts.begin(), foveation_exts.end());
INFO_LOG_FMT(VIDEO, "OpenXR: Enabling XR_FB_foveation (+configuration, vulkan, "
"swapchain_update_state).");
}
}
} // namespace
#if defined(ANDROID)
static void AppendOptionalAndroidOpenXRExtensions(std::vector<const char*>* extensions)
{
if (VR::OpenXRManager::IsRuntimeExtensionSupported(
XR_KHR_ANDROID_THREAD_SETTINGS_EXTENSION_NAME))
{
extensions->push_back(XR_KHR_ANDROID_THREAD_SETTINGS_EXTENSION_NAME);
INFO_LOG_FMT(VIDEO, "OpenXR: Enabling XR_KHR_android_thread_settings.");
}
if (VR::OpenXRManager::IsRuntimeExtensionSupported(XR_EXT_PERFORMANCE_SETTINGS_EXTENSION_NAME))
{
extensions->push_back(XR_EXT_PERFORMANCE_SETTINGS_EXTENSION_NAME);
INFO_LOG_FMT(VIDEO, "OpenXR: Enabling XR_EXT_performance_settings.");
}
}
#endif
XRVkEyeSwapchain::XRVkEyeSwapchain() = default;
XRVkEyeSwapchain::~XRVkEyeSwapchain() = default;
XRVkEyeSwapchain::XRVkEyeSwapchain(XRVkEyeSwapchain&&) noexcept = default;
XRVkEyeSwapchain& XRVkEyeSwapchain::operator=(XRVkEyeSwapchain&&) noexcept = default;
XRVkLayeredSwapchain::XRVkLayeredSwapchain() = default;
XRVkLayeredSwapchain::~XRVkLayeredSwapchain() = default;
XRVkLayeredSwapchain::XRVkLayeredSwapchain(XRVkLayeredSwapchain&&) noexcept = default;
XRVkLayeredSwapchain& XRVkLayeredSwapchain::operator=(XRVkLayeredSwapchain&&) noexcept = default;
static const char* VkFormatToString(int64_t format)
{
switch (static_cast<VkFormat>(format))
{
case VK_FORMAT_R8G8B8A8_UNORM:
return "VK_FORMAT_R8G8B8A8_UNORM";
case VK_FORMAT_R8G8B8A8_SRGB:
return "VK_FORMAT_R8G8B8A8_SRGB";
case VK_FORMAT_B8G8R8A8_UNORM:
return "VK_FORMAT_B8G8R8A8_UNORM";
case VK_FORMAT_B8G8R8A8_SRGB:
return "VK_FORMAT_B8G8R8A8_SRGB";
case VK_FORMAT_A2B10G10R10_UNORM_PACK32:
return "VK_FORMAT_A2B10G10R10_UNORM_PACK32";
case VK_FORMAT_R16G16B16A16_SFLOAT:
return "VK_FORMAT_R16G16B16A16_SFLOAT";
default:
return "UNKNOWN_VK_FORMAT";
}
}
static AbstractTextureFormat VkFormatToAbstractFormat(VkFormat format)
{
switch (format)
{
case VK_FORMAT_R8G8B8A8_UNORM:
case VK_FORMAT_R8G8B8A8_SRGB:
return AbstractTextureFormat::RGBA8;
case VK_FORMAT_B8G8R8A8_UNORM:
case VK_FORMAT_B8G8R8A8_SRGB:
return AbstractTextureFormat::BGRA8;
case VK_FORMAT_A2B10G10R10_UNORM_PACK32:
return AbstractTextureFormat::RGB10_A2;
case VK_FORMAT_R16G16B16A16_SFLOAT:
return AbstractTextureFormat::RGBA16F;
default:
return AbstractTextureFormat::RGBA8;
}
}
static bool SelectSwapchainFormat(XrSession session, int64_t* out_format)
{
uint32_t format_count = 0;
XrResult result = xrEnumerateSwapchainFormats(session, 0, &format_count, nullptr);
if (XR_FAILED(result) || format_count == 0)
{
ERROR_LOG_FMT(VIDEO, "OpenXR: xrEnumerateSwapchainFormats (count) failed ({}).",
static_cast<int>(result));
return false;
}
std::vector<int64_t> runtime_formats(format_count);
result = xrEnumerateSwapchainFormats(session, format_count, &format_count, runtime_formats.data());
if (XR_FAILED(result))
{
ERROR_LOG_FMT(VIDEO, "OpenXR: xrEnumerateSwapchainFormats failed ({}).",
static_cast<int>(result));
return false;
}
for (const int64_t format : runtime_formats)
{
INFO_LOG_FMT(VIDEO, "OpenXR: Runtime swapchain format {} ({})", static_cast<long long>(format),
VkFormatToString(format));
}
#if defined(ANDROID)
// Quest's compositor expects sRGB swapchains for Dolphin's gamma-encoded XFB. We render through
// a UNORM view alias below to avoid a double gamma encode.
static constexpr std::array<VkFormat, 6> preferred_formats = {
VK_FORMAT_R8G8B8A8_SRGB, VK_FORMAT_B8G8R8A8_SRGB,
VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_B8G8R8A8_UNORM,
VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_FORMAT_R16G16B16A16_SFLOAT};
#else
// Prefer sRGB on PC so the OpenXR compositor decodes Dolphin's gamma-encoded XFB correctly.
static constexpr std::array<VkFormat, 6> preferred_formats = {
VK_FORMAT_R8G8B8A8_SRGB, VK_FORMAT_B8G8R8A8_SRGB,
VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_B8G8R8A8_UNORM,
VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_FORMAT_R16G16B16A16_SFLOAT};
#endif
for (const VkFormat preferred : preferred_formats)
{
const int64_t wanted = static_cast<int64_t>(preferred);
if (std::find(runtime_formats.begin(), runtime_formats.end(), wanted) != runtime_formats.end())
{
*out_format = wanted;
INFO_LOG_FMT(VIDEO, "OpenXR: Selected swapchain format {} ({}).",
static_cast<long long>(*out_format), VkFormatToString(*out_format));
return true;
}
}
*out_format = runtime_formats.front();
WARN_LOG_FMT(VIDEO,
"OpenXR: No preferred Vulkan swapchain format found; falling back to runtime "
"format {} ({}).",
static_cast<long long>(*out_format), VkFormatToString(*out_format));
return true;
}
// Separate function for SEH protection — __try cannot be used in functions
// that have C++ objects requiring unwinding.
#ifdef _WIN32
static XrResult SafeCreateSession(XrInstance instance, const XrSessionCreateInfo* info,
XrSession* session)
{
__try
{
return xrCreateSession(instance, info, session);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
ERROR_LOG_FMT(VIDEO,
"OpenXR: xrCreateSession CRASHED (exception {:#010x}). "
"The OpenXR runtime may require Vulkan extensions that BaijiSteamVR did not enable. "
"Check the 'Required Vulkan instance/device extensions' log lines above.",
static_cast<unsigned>(GetExceptionCode()));
return XR_ERROR_RUNTIME_FAILURE;
}
}
#else
static XrResult SafeCreateSession(XrInstance instance, const XrSessionCreateInfo* info,
XrSession* session)
{
return xrCreateSession(instance, info, session);
}
#endif
VulkanOpenXR::VulkanOpenXR() = default;
VulkanOpenXR::~VulkanOpenXR()
{
Shutdown();
}
std::unique_lock<std::mutex> VulkanOpenXR::AcquireGraphicsQueueLock()
{
if (g_command_buffer_mgr)
return g_command_buffer_mgr->AcquireQueueLock();
return {};
}
bool VulkanOpenXR::WaitForPendingFrameFinalization(std::string_view reason)
{
#if defined(ANDROID)
if (m_async_frame_finalization_in_flight.load(std::memory_order_acquire) ||
m_stereo_finalization_in_flight.load(std::memory_order_acquire))
{
static uint32_t s_async_wait_log_count = 0;
if (!g_command_buffer_mgr)
{
ERROR_LOG_FMT(VIDEO,
"OpenXR Vulkan: pending async final XR submit cannot be waited because the "
"command buffer manager is gone.");
return false;
}
const uint64_t wait_start_us = Common::Timer::NowUs();
g_command_buffer_mgr->WaitForWorkerThreadIdle();
const uint64_t wait_us = ElapsedUs(wait_start_us, Common::Timer::NowUs());
if (s_async_wait_log_count < 20 || wait_us >= 500)
{
INFO_LOG_FMT(VIDEO,
"OpenXR Vulkan: waited {} us for pending async final XR submit ({}).",
wait_us, reason.empty() ? "frame loop" : reason);
__android_log_print(ANDROID_LOG_INFO, "DolphinXR",
"OpenXR Vulkan: waited %llu us for pending async final XR submit (%.*s)",
static_cast<unsigned long long>(wait_us),
static_cast<int>(reason.empty() ? std::string_view{"frame loop"}.size() :
reason.size()),
reason.empty() ? "frame loop" : reason.data());
s_async_wait_log_count++;
}
}
if (m_async_frame_finalization_failed.exchange(false, std::memory_order_acq_rel))
{
ERROR_LOG_FMT(VIDEO, "OpenXR Vulkan: previous async final XR submit failed.");
return false;
}
#endif
return true;
}
void VulkanOpenXR::FinalizePendingXRFrame(PendingXRFrame frame, VkResult submit_result)
{
const uint64_t finalize_start_us = Common::Timer::NowUs();
uint64_t release_total_us = 0;
uint64_t end_frame_us = 0;
bool success = true;
XrSwapchainImageReleaseInfo release_info{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
const auto release_swapchain = [&](XrSwapchain swapchain, std::string_view name) {
if (swapchain == XR_NULL_HANDLE)
return;
const uint64_t release_start_us = Common::Timer::NowUs();
XrResult result = XR_SUCCESS;
{
auto queue_lock = AcquireGraphicsQueueLock();
result = xrReleaseSwapchainImage(swapchain, &release_info);
}
const uint64_t release_us = ElapsedUs(release_start_us, Common::Timer::NowUs());
release_total_us += release_us;
if (XR_FAILED(result))
{
WARN_LOG_FMT(VIDEO, "OpenXR: async xrReleaseSwapchainImage failed for {} ({}).", name,
static_cast<int>(result));
success = false;
}
};
if (frame.layered_acquired)
release_swapchain(frame.layered_swapchain, "layered swapchain");
for (uint32_t eye = 0; eye < 2; ++eye)
{
if (frame.eye_acquired[eye])
release_swapchain(frame.eye_swapchains[eye], eye == 0 ? "eye 0" : "eye 1");
}
if (submit_result != VK_SUCCESS)
{
m_async_frame_finalization_failed.store(true, std::memory_order_release);
success = false;
}
if (!success)
{
m_async_frame_finalization_in_flight.store(false, std::memory_order_release);
return;
}
const uint64_t end_frame_start_us = Common::Timer::NowUs();
if (!VR::g_openxr)
{
success = false;
}
else if (VR::g_openxr->IsFrameThreadActive())
{
// The pacing thread owns xrEndFrame — publish the released frame's views to it.
// Publishing after the releases guarantees the compositor picks up the new image.
VR::g_openxr->PublishFrame(frame.projection_views, frame.layer_flags);
}
else
{
XrCompositionLayerProjection projection_layer{XR_TYPE_COMPOSITION_LAYER_PROJECTION};
projection_layer.layerFlags = frame.layer_flags;
projection_layer.space = frame.space;
projection_layer.viewCount = static_cast<uint32_t>(frame.projection_views.size());
projection_layer.views = frame.projection_views.data();
const std::vector<XrCompositionLayerBaseHeader*> layers = {
reinterpret_cast<XrCompositionLayerBaseHeader*>(&projection_layer)};
if (!VR::g_openxr->EndFrameDetached(frame.display_time, frame.environment_blend_mode,
frame.should_render, layers))
{
success = false;
}
}
end_frame_us = ElapsedUs(end_frame_start_us, Common::Timer::NowUs());
if (!success)
m_async_frame_finalization_failed.store(true, std::memory_order_release);
const uint64_t finalize_end_us = Common::Timer::NowUs();
const uint64_t queue_delay_us = ElapsedUs(frame.queued_time_us, finalize_start_us);
const uint64_t finalize_us = ElapsedUs(finalize_start_us, finalize_end_us);
if (frame.debug_frame_id <= 20 || frame.debug_frame_id % 300 == 0 || queue_delay_us >= 5000 ||
finalize_us >= 5000)
{
INFO_LOG_FMT(VIDEO,
"OpenXR Vulkan: async final XR submit #{} timing queue_delay={}us "
"release={}us end_frame={}us finalize={}us success={}.",
frame.debug_frame_id, queue_delay_us, release_total_us, end_frame_us, finalize_us,
success);
#if defined(ANDROID)
__android_log_print(ANDROID_LOG_INFO, "DolphinXR",
"OpenXR Vulkan: async final XR submit #%llu timing queue_delay=%lluus "
"release=%lluus end_frame=%lluus finalize=%lluus success=%d",
static_cast<unsigned long long>(frame.debug_frame_id),
static_cast<unsigned long long>(queue_delay_us),
static_cast<unsigned long long>(release_total_us),
static_cast<unsigned long long>(end_frame_us),
static_cast<unsigned long long>(finalize_us), static_cast<int>(success));
#endif
}
m_async_frame_finalization_in_flight.store(false, std::memory_order_release);
}
// static
bool VulkanOpenXR::PreQueryVulkanExtensions(VulkanExtensionRequirements& out)
{
INFO_LOG_FMT(VIDEO, "OpenXR Vulkan: Pre-querying required Vulkan extensions...");
// Dolphin owns VkInstance/VkDevice creation and the runtime only binds to them at
// xrCreateSession. The runtime-assisted XR_KHR_vulkan_enable2 path
// (xrCreateVulkanInstanceKHR/xrCreateVulkanDeviceKHR) is deliberately not used: creating a
// second device against the same XrInstance crashes inside SteamVR's Vulkan interop.
INFO_LOG_FMT(VIDEO, "OpenXR Vulkan: Using {}.", XR_KHR_VULKAN_ENABLE_EXTENSION_NAME);
auto mgr = std::make_unique<VR::OpenXRManager>();
std::vector<const char*> extensions = {XR_KHR_VULKAN_ENABLE_EXTENSION_NAME};
AppendOptionalOpenXRExtensions(&extensions);
#if defined(ANDROID)
extensions.push_back(XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME);
AppendOptionalAndroidOpenXRExtensions(&extensions);
#endif
// Required by the OpenXR spec when XR_SWAPCHAIN_USAGE_MUTABLE_FORMAT_BIT is used
// on Vulkan. The sRGB swapchain path uses a UNORM view alias to avoid double gamma.
if (VR::OpenXRManager::IsRuntimeExtensionSupported(
XR_KHR_VULKAN_SWAPCHAIN_FORMAT_LIST_EXTENSION_NAME))
{
extensions.push_back(XR_KHR_VULKAN_SWAPCHAIN_FORMAT_LIST_EXTENSION_NAME);
}
const auto controller_exts = VR::OpenXRManager::GetAvailableControllerExtensions();
extensions.insert(extensions.end(), controller_exts.begin(), controller_exts.end());
if (!mgr->CreateInstance(extensions))
return false;
if (!mgr->InitializeSystem())
return false;
if (!mgr->EnumerateViewConfigurations())
return false;
const XrInstance xr_instance = mgr->GetInstance();
const XrSystemId xr_system = mgr->GetSystemId();
PFN_xrGetVulkanGraphicsRequirementsKHR pfnGetVulkanRequirements = nullptr;
xrGetInstanceProcAddr(xr_instance, "xrGetVulkanGraphicsRequirementsKHR",
reinterpret_cast<PFN_xrVoidFunction*>(&pfnGetVulkanRequirements));
if (pfnGetVulkanRequirements)
{
XrGraphicsRequirementsVulkanKHR requirements{XR_TYPE_GRAPHICS_REQUIREMENTS_VULKAN_KHR};
const XrResult result = pfnGetVulkanRequirements(xr_instance, xr_system, &requirements);
if (XR_SUCCEEDED(result))
{
const u32 reported_max =
VK_MAKE_API_VERSION(0, XR_VERSION_MAJOR(requirements.maxApiVersionSupported),
XR_VERSION_MINOR(requirements.maxApiVersionSupported),
XR_VERSION_PATCH(requirements.maxApiVersionSupported));
INFO_LOG_FMT(VIDEO, "OpenXR: Pre-query Vulkan API range min {}.{}.{}, max {}.{}.{}",
XR_VERSION_MAJOR(requirements.minApiVersionSupported),
XR_VERSION_MINOR(requirements.minApiVersionSupported),
XR_VERSION_PATCH(requirements.minApiVersionSupported),
XR_VERSION_MAJOR(requirements.maxApiVersionSupported),
XR_VERSION_MINOR(requirements.maxApiVersionSupported),
XR_VERSION_PATCH(requirements.maxApiVersionSupported));
// XR_KHR_vulkan_enable v1 always reports max=1.0.0 (Meta's Oculus runtime quirk),
// even though the runtime actually supports newer Vulkan. Clamping the instance to
// 1.0 there breaks BaijiSteamVR's multiview path (a 1.1 feature) and crashes vkCreateDevice.
// Treat 1.0.0 as "unknown" so BaijiSteamVR keeps its negotiated 1.1/1.2 instance.
if (reported_max <= VK_API_VERSION_1_0)
{
WARN_LOG_FMT(VIDEO,
"OpenXR: Runtime reported max Vulkan 1.0.0 (v1 extension quirk); "
"ignoring and using BaijiSteamVR's instance version.");
out.max_api_version = 0;
}
else
{
out.max_api_version = reported_max;
}
}
}
// Query required Vulkan instance extensions.
PFN_xrGetVulkanInstanceExtensionsKHR pfnGetInstanceExts = nullptr;
xrGetInstanceProcAddr(xr_instance, "xrGetVulkanInstanceExtensionsKHR",
reinterpret_cast<PFN_xrVoidFunction*>(&pfnGetInstanceExts));
if (pfnGetInstanceExts)
{
uint32_t ext_len = 0;
pfnGetInstanceExts(xr_instance, xr_system, 0, &ext_len, nullptr);
if (ext_len > 0)
{
std::string ext_str(ext_len, '\0');
pfnGetInstanceExts(xr_instance, xr_system, ext_len, &ext_len, ext_str.data());
INFO_LOG_FMT(VIDEO, "OpenXR: Required Vulkan instance extensions: {}", ext_str);
// Parse space-separated extension list.
std::istringstream iss(ext_str);
std::string ext;
while (iss >> ext)
out.instance_extensions.push_back(ext);
}
}
// Query required Vulkan device extensions.
PFN_xrGetVulkanDeviceExtensionsKHR pfnGetDeviceExts = nullptr;
xrGetInstanceProcAddr(xr_instance, "xrGetVulkanDeviceExtensionsKHR",
reinterpret_cast<PFN_xrVoidFunction*>(&pfnGetDeviceExts));
if (pfnGetDeviceExts)
{
uint32_t ext_len = 0;
pfnGetDeviceExts(xr_instance, xr_system, 0, &ext_len, nullptr);
if (ext_len > 0)
{
std::string ext_str(ext_len, '\0');
pfnGetDeviceExts(xr_instance, xr_system, ext_len, &ext_len, ext_str.data());
INFO_LOG_FMT(VIDEO, "OpenXR: Required Vulkan device extensions: {}", ext_str);
std::istringstream iss(ext_str);
std::string ext;
while (iss >> ext)
out.device_extensions.push_back(ext);
}
}
// Keep the OpenXRManager alive — Initialize() will reuse it.
VR::g_openxr = std::move(mgr);
INFO_LOG_FMT(VIDEO, "OpenXR Vulkan: Pre-query complete ({} instance, {} device extensions).",
out.instance_extensions.size(), out.device_extensions.size());
return true;
}
bool VulkanOpenXR::Initialize()
{
INFO_LOG_FMT(VIDEO, "OpenXR Vulkan: Starting initialization...");
// If PreQueryVulkanExtensions() was called, VR::g_openxr already exists.
if (!VR::g_openxr)
{
auto mgr = std::make_unique<VR::OpenXRManager>();
std::vector<const char*> extensions = {XR_KHR_VULKAN_ENABLE_EXTENSION_NAME};
AppendOptionalOpenXRExtensions(&extensions);
#if defined(ANDROID)
extensions.push_back(XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME);
AppendOptionalAndroidOpenXRExtensions(&extensions);
#endif
if (VR::OpenXRManager::IsRuntimeExtensionSupported(
XR_KHR_VULKAN_SWAPCHAIN_FORMAT_LIST_EXTENSION_NAME))
{
extensions.push_back(XR_KHR_VULKAN_SWAPCHAIN_FORMAT_LIST_EXTENSION_NAME);
}
const auto controller_exts = VR::OpenXRManager::GetAvailableControllerExtensions();
extensions.insert(extensions.end(), controller_exts.begin(), controller_exts.end());
if (!mgr->CreateInstance(extensions))
return false;
if (!mgr->InitializeSystem())
return false;
if (!mgr->EnumerateViewConfigurations())
return false;
VR::g_openxr = std::move(mgr);
}
#if defined(_WIN32) || defined(ANDROID)
// Windows desktop and Quest standalone: keep coverage only when the OpenXR runtime
// can actually composite it (XR_FB_passthrough or an ALPHA_BLEND environment).
g_backend_info.bSupportsVRPassthroughCoverage &=
VR::g_openxr->SupportsPassthrough();
if (g_ActiveConfig.vr_passthrough && !g_ActiveConfig.vr_stereo_screen &&
!g_backend_info.bSupportsVRPassthroughCoverage)
{
OSD::AddMessage("Passthrough disabled: this OpenXR runtime does not expose a compatible "
"passthrough mode.",
OSD::Duration::VERY_LONG);
Config::SetBaseOrCurrent(Config::GFX_VR_PASSTHROUGH, false);
g_ActiveConfig.vr_passthrough = false;
}
#else
g_backend_info.bSupportsVRPassthroughCoverage = false;
#endif
INFO_LOG_FMT(VIDEO, "OpenXR Vulkan: Creating session...");
if (!CreateSessionVulkan())
{
ERROR_LOG_FMT(VIDEO, "OpenXR Vulkan: Session creation failed — disabling VR.");
VR::g_openxr.reset();
return false;
}
INFO_LOG_FMT(VIDEO, "OpenXR Vulkan: Creating reference space...");
if (!VR::g_openxr->CreateReferenceSpace())
{
ERROR_LOG_FMT(VIDEO, "OpenXR Vulkan: Reference space creation failed — disabling VR.");
VR::g_openxr.reset();
return false;
}
INFO_LOG_FMT(VIDEO, "OpenXR Vulkan: Creating swapchains...");
if (!CreateSwapchains())
{
ERROR_LOG_FMT(VIDEO, "OpenXR Vulkan: Swapchain creation failed — disabling VR.");
VR::g_openxr.reset();
return false;
}
// Register this object as the swapchain provider so Presenter can acquire eye images.
VR::g_openxr->SetSwapchain(this);
INFO_LOG_FMT(VIDEO, "OpenXR Vulkan: Initialization complete.");
return true;
}
void VulkanOpenXR::Shutdown()
{
WaitForPendingFrameFinalization("during shutdown");
// Clear swapchain pointer before destroying swapchains so no dangling use occurs.
if (VR::g_openxr)
VR::g_openxr->SetSwapchain(nullptr);
DestroySwapchains();
// Fully tear down the runtime between games, mirroring the D3D11 backend. ~OpenXRManager() runs
// DestroySession() (ordered session exit) before destroying the XrInstance, so the next game
// starts from a clean runtime state instead of a retained instance bound to a dead device.
if (VR::g_openxr)
VR::g_openxr.reset();
// Wait for all GPU work to finish before the Vulkan device is destroyed. Once the device has
// already been lost, another wait only re-enters the failing driver path.
if (g_vulkan_context && (!g_command_buffer_mgr || !g_command_buffer_mgr->IsDeviceLost()))
vkDeviceWaitIdle(g_vulkan_context->GetDevice());
INFO_LOG_FMT(VIDEO, "OpenXR Vulkan: Shut down.");
}
bool VulkanOpenXR::CreateSessionVulkan()
{
ASSERT(g_vulkan_context != nullptr);
ASSERT(VR::g_openxr != nullptr);
const XrInstance xr_instance = VR::g_openxr->GetInstance();
const XrSystemId xr_system = VR::g_openxr->GetSystemId();
// --- Query Vulkan graphics requirements (mandatory before session creation) ---
INFO_LOG_FMT(VIDEO, "OpenXR Vulkan: Querying graphics requirements...");
PFN_xrGetVulkanGraphicsRequirementsKHR pfnGetVulkanRequirements = nullptr;
const char* const requirements_function = "xrGetVulkanGraphicsRequirementsKHR";
XrResult result = xrGetInstanceProcAddr(
xr_instance, requirements_function,
reinterpret_cast<PFN_xrVoidFunction*>(&pfnGetVulkanRequirements));
if (XR_FAILED(result) || pfnGetVulkanRequirements == nullptr)
{
ERROR_LOG_FMT(VIDEO, "OpenXR: Could not load {}.", requirements_function);
return false;
}
XrGraphicsRequirementsVulkanKHR requirements{XR_TYPE_GRAPHICS_REQUIREMENTS_VULKAN_KHR};
result = pfnGetVulkanRequirements(xr_instance, xr_system, &requirements);
if (XR_FAILED(result))
{
ERROR_LOG_FMT(VIDEO, "OpenXR: xrGetVulkanGraphicsRequirementsKHR failed ({}).",
static_cast<int>(result));
return false;
}
INFO_LOG_FMT(VIDEO, "OpenXR: Vulkan requirements — min API {}.{}.{}, max API {}.{}.{}",
XR_VERSION_MAJOR(requirements.minApiVersionSupported),
XR_VERSION_MINOR(requirements.minApiVersionSupported),
XR_VERSION_PATCH(requirements.minApiVersionSupported),
XR_VERSION_MAJOR(requirements.maxApiVersionSupported),
XR_VERSION_MINOR(requirements.maxApiVersionSupported),
XR_VERSION_PATCH(requirements.maxApiVersionSupported));
// Log BaijiSteamVR's Vulkan API version for comparison.
const u32 dolphin_api = g_vulkan_context->GetDeviceInfo().apiVersion;
INFO_LOG_FMT(VIDEO, "OpenXR: BaijiSteamVR VkPhysicalDevice API version {}.{}.{}",
VK_VERSION_MAJOR(dolphin_api), VK_VERSION_MINOR(dolphin_api),
VK_VERSION_PATCH(dolphin_api));
// --- Query required Vulkan instance extensions ---
INFO_LOG_FMT(VIDEO, "OpenXR Vulkan: Querying required Vulkan instance extensions...");
PFN_xrGetVulkanInstanceExtensionsKHR pfnGetInstanceExts = nullptr;
result = xrGetInstanceProcAddr(xr_instance, "xrGetVulkanInstanceExtensionsKHR",
reinterpret_cast<PFN_xrVoidFunction*>(&pfnGetInstanceExts));
if (XR_SUCCEEDED(result) && pfnGetInstanceExts != nullptr)
{
uint32_t ext_len = 0;
pfnGetInstanceExts(xr_instance, xr_system, 0, &ext_len, nullptr);
if (ext_len > 0)
{
std::string ext_str(ext_len, '\0');
pfnGetInstanceExts(xr_instance, xr_system, ext_len, &ext_len, ext_str.data());
INFO_LOG_FMT(VIDEO, "OpenXR: Required Vulkan instance extensions: {}", ext_str);
}
}
// --- Query required Vulkan device extensions ---
INFO_LOG_FMT(VIDEO, "OpenXR Vulkan: Querying required Vulkan device extensions...");
PFN_xrGetVulkanDeviceExtensionsKHR pfnGetDeviceExts = nullptr;
result = xrGetInstanceProcAddr(xr_instance, "xrGetVulkanDeviceExtensionsKHR",
reinterpret_cast<PFN_xrVoidFunction*>(&pfnGetDeviceExts));
if (XR_SUCCEEDED(result) && pfnGetDeviceExts != nullptr)
{
uint32_t ext_len = 0;
pfnGetDeviceExts(xr_instance, xr_system, 0, &ext_len, nullptr);
if (ext_len > 0)
{
std::string ext_str(ext_len, '\0');
pfnGetDeviceExts(xr_instance, xr_system, ext_len, &ext_len, ext_str.data());
INFO_LOG_FMT(VIDEO, "OpenXR: Required Vulkan device extensions: {}", ext_str);
}
}
// --- Verify the physical device matches what the runtime expects ---
INFO_LOG_FMT(VIDEO, "OpenXR Vulkan: Checking physical device...");
VkPhysicalDevice xr_physical_device = VK_NULL_HANDLE;
PFN_xrGetVulkanGraphicsDeviceKHR pfnGetVulkanDevice = nullptr;
result = xrGetInstanceProcAddr(xr_instance, "xrGetVulkanGraphicsDeviceKHR",
reinterpret_cast<PFN_xrVoidFunction*>(&pfnGetVulkanDevice));
if (XR_SUCCEEDED(result) && pfnGetVulkanDevice != nullptr)
{
pfnGetVulkanDevice(xr_instance, xr_system, g_vulkan_context->GetVulkanInstance(),
&xr_physical_device);
}
if (xr_physical_device != VK_NULL_HANDLE &&
xr_physical_device != g_vulkan_context->GetPhysicalDevice())
{
ERROR_LOG_FMT(VIDEO, "OpenXR: Active VkPhysicalDevice differs from the runtime selection.");
return false;
}
INFO_LOG_FMT(VIDEO, "OpenXR: VkPhysicalDevice matches runtime expectation.");
// --- Create XrSession bound to the active Vulkan device ---
INFO_LOG_FMT(VIDEO, "OpenXR Vulkan: Creating XrSession with Vulkan binding...");
INFO_LOG_FMT(VIDEO, " VkInstance={}, VkPhysicalDevice={}, VkDevice={}, queueFamily={}, "
"queueIndex=0",
reinterpret_cast<void*>(g_vulkan_context->GetVulkanInstance()),
reinterpret_cast<void*>(g_vulkan_context->GetPhysicalDevice()),
reinterpret_cast<void*>(g_vulkan_context->GetDevice()),
g_vulkan_context->GetGraphicsQueueFamilyIndex());
XrGraphicsBindingVulkanKHR vk_binding{XR_TYPE_GRAPHICS_BINDING_VULKAN_KHR};
vk_binding.instance = g_vulkan_context->GetVulkanInstance();
vk_binding.physicalDevice = g_vulkan_context->GetPhysicalDevice();
vk_binding.device = g_vulkan_context->GetDevice();
vk_binding.queueFamilyIndex = g_vulkan_context->GetGraphicsQueueFamilyIndex();
vk_binding.queueIndex = 0;
XrSessionCreateInfo session_info{XR_TYPE_SESSION_CREATE_INFO};
session_info.next = &vk_binding;
session_info.systemId = xr_system;
XrSession session = XR_NULL_HANDLE;
result = SafeCreateSession(xr_instance, &session_info, &session);
if (XR_FAILED(result))
{
ERROR_LOG_FMT(VIDEO, "OpenXR: xrCreateSession failed ({}).", static_cast<int>(result));
return false;
}
VR::g_openxr->SetSession(session);
INFO_LOG_FMT(VIDEO, "OpenXR Vulkan: Session created successfully.");
return true;
}
// static
bool VulkanOpenXR::ShouldUseFoveation()
{
// Foveate only the stereo projection path: the flat panel reuses eye swapchain #0,
// and foveating a world-locked quad would just blur its edges for no gain.
return g_ActiveConfig.stereo_mode == StereoMode::OpenXR && VR::g_openxr &&
VR::g_openxr->IsFoveationUsable() && g_vulkan_context->SupportsFragmentDensityMap();
}
// static
bool VulkanOpenXR::PrepareFoveationImages(
const std::vector<XrSwapchainImageFoveationVulkanFB>& fdm_images,
std::vector<VkImageView>* out_views)
{
out_views->clear();
out_views->reserve(fdm_images.size());
const auto cleanup = [out_views]() {
for (VkImageView view : *out_views)
vkDestroyImageView(g_vulkan_context->GetDevice(), view, nullptr);
out_views->clear();
};
for (const auto& fdm : fdm_images)
{
if (fdm.image == VK_NULL_HANDLE)
{
WARN_LOG_FMT(VIDEO, "OpenXR: Runtime returned no fragment density map image.");
cleanup();
return false;
}
VkImageViewCreateInfo view_info = {VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
view_info.image = fdm.image;
// 2D_ARRAY covers both per-eye (1 layer) and multiview (2 layer) density maps.
view_info.viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY;
view_info.format = VK_FORMAT_R8G8_UNORM;
view_info.components = {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY,
VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY};
view_info.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, VK_REMAINING_ARRAY_LAYERS};
VkImageView view = VK_NULL_HANDLE;
const VkResult res =
vkCreateImageView(g_vulkan_context->GetDevice(), &view_info, nullptr, &view);
if (res != VK_SUCCESS)
{
LOG_VULKAN_ERROR(res, "vkCreateImageView (fragment density map) failed: ");
cleanup();
return false;
}
out_views->push_back(view);
// One-time transition into the fixed layout the render passes expect. Contents at
// this point are only pre-profile defaults; the runtime rewrites the density values
// once the foveation profile is applied via xrUpdateSwapchainFB.
VkImageMemoryBarrier barrier = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
barrier.srcAccessMask = 0;
barrier.dstAccessMask = VK_ACCESS_FRAGMENT_DENSITY_MAP_READ_BIT_EXT;
barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
barrier.newLayout = VK_IMAGE_LAYOUT_FRAGMENT_DENSITY_MAP_OPTIMAL_EXT;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = fdm.image;
barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, VK_REMAINING_ARRAY_LAYERS};
vkCmdPipelineBarrier(g_command_buffer_mgr->GetCurrentInitCommandBuffer(),
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
VK_PIPELINE_STAGE_FRAGMENT_DENSITY_PROCESS_BIT_EXT, 0, 0, nullptr, 0,
nullptr, 1, &barrier);
}
return true;
}
bool VulkanOpenXR::CreateSwapchains()
{
ASSERT(VR::g_openxr != nullptr);
int64_t swapchain_format = 0;
if (!SelectSwapchainFormat(VR::g_openxr->GetSession(), &swapchain_format))
return false;
m_use_layered_swapchain = false;
m_frame_uses_layered_swapchain = false;
m_foveated = false;
#if defined(ANDROID)
const auto& view_cfgs = VR::g_openxr->GetViewConfigViews();
const bool matching_eye_sizes =
view_cfgs[0].recommendedImageRectWidth == view_cfgs[1].recommendedImageRectWidth &&
view_cfgs[0].recommendedImageRectHeight == view_cfgs[1].recommendedImageRectHeight;
if (g_vulkan_context->SupportsMultiview() && g_ActiveConfig.vr_use_vulkan_multiview &&
g_ActiveConfig.stereo_mode == StereoMode::OpenXR)
{
if (matching_eye_sizes)
{
if (CreateLayeredSwapchain(swapchain_format))
{
m_use_layered_swapchain = true;
}
else
{
WARN_LOG_FMT(VIDEO,
"OpenXR: Layered Vulkan swapchain creation failed; falling back to "
"two per-eye swapchains.");
}
}
else
{
WARN_LOG_FMT(VIDEO,
"OpenXR: Layered Vulkan swapchain disabled because eye sizes differ "
"({}x{} vs {}x{}).",
view_cfgs[0].recommendedImageRectWidth,
view_cfgs[0].recommendedImageRectHeight,
view_cfgs[1].recommendedImageRectWidth,
view_cfgs[1].recommendedImageRectHeight);
}
}
#endif
if (!CreateEyeSwapchains(swapchain_format))
{
DestroySwapchains();
return false;
}
return true;
}
bool VulkanOpenXR::CreateLayeredSwapchain(int64_t swapchain_format)
{
ASSERT(VR::g_openxr != nullptr);
const auto& view_cfgs = VR::g_openxr->GetViewConfigViews();
const AbstractTextureFormat abstract_format =
VkFormatToAbstractFormat(static_cast<VkFormat>(swapchain_format));
auto& sc = m_layered_swapchain;
sc.width = view_cfgs[0].recommendedImageRectWidth;
sc.height = view_cfgs[0].recommendedImageRectHeight;
auto cleanup = [&sc]() {
DestroySwapchainVulkanObjects(sc);
if (sc.swapchain != XR_NULL_HANDLE)
{
xrDestroySwapchain(sc.swapchain);
sc.swapchain = XR_NULL_HANDLE;
}
sc.width = 0;
sc.height = 0;
};
XrSwapchainCreateInfo info{XR_TYPE_SWAPCHAIN_CREATE_INFO};
info.arraySize = 2;
info.format = swapchain_format;
info.width = sc.width;
info.height = sc.height;
info.mipCount = 1;
info.faceCount = 1;
info.sampleCount = 1;
info.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_TRANSFER_DST_BIT;
const VkFormat vk_sc_format = static_cast<VkFormat>(swapchain_format);
const VkFormat vk_view_format = VKTexture::GetLinearFormat(vk_sc_format);
const std::array<VkFormat, 2> view_formats = {vk_sc_format, vk_view_format};
XrVulkanSwapchainFormatListCreateInfoKHR format_list{
XR_TYPE_VULKAN_SWAPCHAIN_FORMAT_LIST_CREATE_INFO_KHR};
if (vk_view_format != vk_sc_format)
{
info.usageFlags |= XR_SWAPCHAIN_USAGE_MUTABLE_FORMAT_BIT;
if (VR::g_openxr->IsExtensionEnabled(XR_KHR_VULKAN_SWAPCHAIN_FORMAT_LIST_EXTENSION_NAME))
{
format_list.viewFormatCount = static_cast<uint32_t>(view_formats.size());
format_list.viewFormats = view_formats.data();
info.next = &format_list;
}
}
// Fixed foveated rendering: ask the runtime to allocate fragment density maps.
XrSwapchainCreateInfoFoveationFB foveation_info{XR_TYPE_SWAPCHAIN_CREATE_INFO_FOVEATION_FB};
bool foveated = ShouldUseFoveation();
if (foveated)
{
foveation_info.flags = XR_SWAPCHAIN_CREATE_FOVEATION_FRAGMENT_DENSITY_MAP_BIT_FB;
// XrSwapchainCreateInfoFoveationFB::next is (non-const) void*.
foveation_info.next = const_cast<void*>(info.next);
info.next = &foveation_info;
}
XrResult result = xrCreateSwapchain(VR::g_openxr->GetSession(), &info, &sc.swapchain);
if (XR_FAILED(result) && foveated)
{
WARN_LOG_FMT(VIDEO,
"OpenXR: Foveated layered swapchain creation failed ({}); retrying without "
"foveation.",
static_cast<int>(result));
info.next = foveation_info.next;
foveated = false;
result = xrCreateSwapchain(VR::g_openxr->GetSession(), &info, &sc.swapchain);
}
if (XR_FAILED(result))
{
WARN_LOG_FMT(VIDEO, "OpenXR: xrCreateSwapchain failed for layered Vulkan swapchain ({}).",
static_cast<int>(result));
cleanup();
return false;
}
uint32_t image_count = 0;
result = xrEnumerateSwapchainImages(sc.swapchain, 0, &image_count, nullptr);
if (XR_FAILED(result) || image_count == 0)
{
WARN_LOG_FMT(VIDEO,
"OpenXR: xrEnumerateSwapchainImages failed for layered Vulkan swapchain ({}).",
static_cast<int>(result));
cleanup();
return false;
}
std::vector<XrSwapchainImageVulkanKHR> images(image_count,
{XR_TYPE_SWAPCHAIN_IMAGE_VULKAN_KHR});
std::vector<XrSwapchainImageFoveationVulkanFB> fdm_images;
if (foveated)
{
fdm_images.resize(image_count, {XR_TYPE_SWAPCHAIN_IMAGE_FOVEATION_VULKAN_FB});
for (uint32_t i = 0; i < image_count; ++i)
images[i].next = &fdm_images[i];
}
result = xrEnumerateSwapchainImages(
sc.swapchain, image_count, &image_count,
reinterpret_cast<XrSwapchainImageBaseHeader*>(images.data()));
if (XR_FAILED(result))
{
WARN_LOG_FMT(VIDEO,
"OpenXR: xrEnumerateSwapchainImages data failed for layered Vulkan swapchain "
"({}).",
static_cast<int>(result));
cleanup();
return false;
}
if (foveated && !PrepareFoveationImages(fdm_images, &sc.fdm_views))
{
WARN_LOG_FMT(VIDEO, "OpenXR: Layered swapchain continues without foveation.");
foveated = false;
}
sc.textures.resize(image_count);
sc.framebuffers.resize(image_count);
for (uint32_t i = 0; i < image_count; ++i)
{
TextureConfig tex_config(sc.width, sc.height, /*levels=*/1, /*layers=*/2, /*samples=*/1,
abstract_format, AbstractTextureFlag_RenderTarget,
AbstractTextureType::Texture_2DArray);
sc.textures[i] = VKTexture::CreateAdopted(tex_config, images[i].image,
VK_IMAGE_VIEW_TYPE_2D_ARRAY,
VK_IMAGE_LAYOUT_UNDEFINED, vk_view_format);
if (!sc.textures[i])
{
WARN_LOG_FMT(VIDEO,
"OpenXR: VKTexture::CreateAdopted failed for layered Vulkan image {}.", i);
cleanup();
return false;
}
sc.framebuffers[i] = VKFramebuffer::CreateMultiview(
sc.textures[i].get(), nullptr, {}, foveated ? sc.fdm_views[i] : VK_NULL_HANDLE);
if (!sc.framebuffers[i])
{
WARN_LOG_FMT(VIDEO,
"OpenXR: VKFramebuffer::CreateMultiview failed for layered Vulkan image {}.",
i);
cleanup();
return false;
}
}
if (foveated)
{
VR::g_openxr->ApplyFoveationToSwapchain(sc.swapchain);
m_foveated = true;
}
INFO_LOG_FMT(VIDEO,
"OpenXR: Layered Vulkan swapchain ready: {}x{}, {} images, arraySize=2{}.",
sc.width, sc.height, image_count, foveated ? ", foveated" : "");
return true;
}
bool VulkanOpenXR::CreateEyeSwapchains(int64_t swapchain_format)
{
ASSERT(VR::g_openxr != nullptr);
const auto& view_cfgs = VR::g_openxr->GetViewConfigViews();
const AbstractTextureFormat abstract_format =
VkFormatToAbstractFormat(static_cast<VkFormat>(swapchain_format));
for (uint32_t eye = 0; eye < 2; ++eye)
{
auto& sc = m_eye_swapchains[eye];
sc.width = view_cfgs[eye].recommendedImageRectWidth;
sc.height = view_cfgs[eye].recommendedImageRectHeight;
// Format must come from xrEnumerateSwapchainFormats.
XrSwapchainCreateInfo info{XR_TYPE_SWAPCHAIN_CREATE_INFO};
info.arraySize = 1;
info.format = swapchain_format;
info.width = sc.width;
info.height = sc.height;
info.mipCount = 1;
info.faceCount = 1;
info.sampleCount = 1;
info.usageFlags =
XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_TRANSFER_DST_BIT;
const VkFormat vk_sc_format = static_cast<VkFormat>(swapchain_format);
const VkFormat vk_view_format = VKTexture::GetLinearFormat(vk_sc_format);
const std::array<VkFormat, 2> view_formats = {vk_sc_format, vk_view_format};
XrVulkanSwapchainFormatListCreateInfoKHR format_list{
XR_TYPE_VULKAN_SWAPCHAIN_FORMAT_LIST_CREATE_INFO_KHR};
if (vk_view_format != vk_sc_format)
{
info.usageFlags |= XR_SWAPCHAIN_USAGE_MUTABLE_FORMAT_BIT;
if (VR::g_openxr->IsExtensionEnabled(XR_KHR_VULKAN_SWAPCHAIN_FORMAT_LIST_EXTENSION_NAME))
{
format_list.viewFormatCount = static_cast<uint32_t>(view_formats.size());
format_list.viewFormats = view_formats.data();
info.next = &format_list;
}
}
// Fixed foveated rendering: ask the runtime to allocate fragment density maps.
XrSwapchainCreateInfoFoveationFB foveation_info{XR_TYPE_SWAPCHAIN_CREATE_INFO_FOVEATION_FB};
bool foveated = ShouldUseFoveation();
if (foveated)
{
foveation_info.flags = XR_SWAPCHAIN_CREATE_FOVEATION_FRAGMENT_DENSITY_MAP_BIT_FB;
// XrSwapchainCreateInfoFoveationFB::next is (non-const) void*.
foveation_info.next = const_cast<void*>(info.next);
info.next = &foveation_info;
}
XrResult result = xrCreateSwapchain(VR::g_openxr->GetSession(), &info, &sc.swapchain);
if (XR_FAILED(result) && foveated)
{
WARN_LOG_FMT(VIDEO,
"OpenXR: Foveated swapchain creation failed for eye {} ({}); retrying "
"without foveation.",
eye, static_cast<int>(result));
info.next = foveation_info.next;
foveated = false;
result = xrCreateSwapchain(VR::g_openxr->GetSession(), &info, &sc.swapchain);
}
if (XR_FAILED(result))
{
ERROR_LOG_FMT(VIDEO, "OpenXR: xrCreateSwapchain failed for eye {} ({}).", eye,
static_cast<int>(result));
return false;
}
// Enumerate the Vulkan images backing this swapchain.
uint32_t image_count = 0;
xrEnumerateSwapchainImages(sc.swapchain, 0, &image_count, nullptr);
std::vector<XrSwapchainImageVulkanKHR> images(image_count,
{XR_TYPE_SWAPCHAIN_IMAGE_VULKAN_KHR});
std::vector<XrSwapchainImageFoveationVulkanFB> fdm_images;
if (foveated)
{
fdm_images.resize(image_count, {XR_TYPE_SWAPCHAIN_IMAGE_FOVEATION_VULKAN_FB});
for (uint32_t i = 0; i < image_count; ++i)
images[i].next = &fdm_images[i];
}
xrEnumerateSwapchainImages(sc.swapchain, image_count, &image_count,
reinterpret_cast<XrSwapchainImageBaseHeader*>(images.data()));
if (foveated && !PrepareFoveationImages(fdm_images, &sc.fdm_views))
{
WARN_LOG_FMT(VIDEO, "OpenXR: Eye {} swapchain continues without foveation.", eye);
foveated = false;
}
sc.textures.resize(image_count);
sc.framebuffers.resize(image_count);
for (uint32_t i = 0; i < image_count; ++i)
{
// Build a TextureConfig matching the swapchain image properties.
TextureConfig tex_config(sc.width, sc.height, /*levels=*/1, /*layers=*/1, /*samples=*/1,
abstract_format, AbstractTextureFlag_RenderTarget,
AbstractTextureType::Texture_2D);
// Adopt the runtime-owned VkImage. For sRGB swapchains, use a UNORM view alias so
// BlitFromTexture writes raw sRGB-encoded bytes without a second sRGB encode.
sc.textures[i] =
VKTexture::CreateAdopted(tex_config, images[i].image, VK_IMAGE_VIEW_TYPE_2D,
VK_IMAGE_LAYOUT_UNDEFINED, vk_view_format);
if (!sc.textures[i])
{
ERROR_LOG_FMT(VIDEO, "OpenXR: VKTexture::CreateAdopted failed for eye {}, image {}.", eye,
i);
return false;
}
sc.framebuffers[i] = VKFramebuffer::Create(sc.textures[i].get(), nullptr, {},
foveated ? sc.fdm_views[i] : VK_NULL_HANDLE);
if (!sc.framebuffers[i])
{
ERROR_LOG_FMT(VIDEO, "OpenXR: VKFramebuffer::Create failed for eye {}, image {}.", eye, i);
return false;
}
}
if (foveated)
{
VR::g_openxr->ApplyFoveationToSwapchain(sc.swapchain);
m_foveated = true;
}
INFO_LOG_FMT(VIDEO, "OpenXR: Eye {} swapchain ready: {}x{}, {} images{}.", eye, sc.width,
sc.height, image_count, foveated ? ", foveated" : "");
}
return true;
}
void VulkanOpenXR::DestroySwapchains()
{
// Finish and submit the current command buffer as well as waiting for queued GPU work. A plain
// vkDeviceWaitIdle does not make it safe to destroy objects referenced by an unsubmitted buffer.
// Do not submit again after VK_ERROR_DEVICE_LOST; the command buffer manager has already queued
// a clean emulation stop and further driver calls can make the failure cascade.
if (g_gfx && (!g_command_buffer_mgr || !g_command_buffer_mgr->IsDeviceLost()))
g_gfx->WaitForGPUIdle();
else if (g_vulkan_context && (!g_command_buffer_mgr || !g_command_buffer_mgr->IsDeviceLost()))
vkDeviceWaitIdle(g_vulkan_context->GetDevice());
if (m_layered_image_acquired && m_layered_swapchain.swapchain != XR_NULL_HANDLE)
{
XrSwapchainImageReleaseInfo release_info{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
XrResult release_result = XR_SUCCESS;
{
auto queue_lock = AcquireGraphicsQueueLock();
release_result = xrReleaseSwapchainImage(m_layered_swapchain.swapchain, &release_info);
}
if (XR_FAILED(release_result))
{
WARN_LOG_FMT(VIDEO,
"OpenXR: xrReleaseSwapchainImage during shutdown failed for layered "
"swapchain ({}).",
static_cast<int>(release_result));
}
m_layered_image_acquired = false;
}
for (uint32_t eye = 0; eye < 2; ++eye)
{
auto& sc = m_eye_swapchains[eye];
if (m_image_acquired[eye] && sc.swapchain != XR_NULL_HANDLE)
{
XrSwapchainImageReleaseInfo release_info{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
XrResult release_result = XR_SUCCESS;
{
auto queue_lock = AcquireGraphicsQueueLock();
release_result = xrReleaseSwapchainImage(sc.swapchain, &release_info);
}
if (XR_FAILED(release_result))
{
WARN_LOG_FMT(VIDEO,
"OpenXR: xrReleaseSwapchainImage during shutdown failed for eye {} ({}).",
eye, static_cast<int>(release_result));
}
m_image_acquired[eye] = false;
}
}
// The compositor may still be consuming the last submitted layers while the session is
// FOCUSED. Complete the requested STOPPING -> xrEndSession -> EXITING transition before
// destroying the swapchains referenced by those layers.
if (VR::g_openxr)
VR::g_openxr->ShutdownSession();
DestroySwapchainVulkanObjects(m_layered_swapchain);
if (m_layered_swapchain.swapchain != XR_NULL_HANDLE)
{
const XrResult destroy_result = xrDestroySwapchain(m_layered_swapchain.swapchain);
if (XR_FAILED(destroy_result))
{
WARN_LOG_FMT(VIDEO, "OpenXR: xrDestroySwapchain failed for layered swapchain ({}).",
static_cast<int>(destroy_result));
}
m_layered_swapchain.swapchain = XR_NULL_HANDLE;
}
m_layered_swapchain.width = 0;
m_layered_swapchain.height = 0;
m_use_layered_swapchain = false;
m_frame_uses_layered_swapchain = false;
for (uint32_t eye = 0; eye < 2; ++eye)
{
auto& sc = m_eye_swapchains[eye];
// Release Dolphin framebuffers and views synchronously before the runtime destroys its images.
DestroySwapchainVulkanObjects(sc);
if (sc.swapchain != XR_NULL_HANDLE)
{
const XrResult destroy_result = xrDestroySwapchain(sc.swapchain);
if (XR_FAILED(destroy_result))
{
WARN_LOG_FMT(VIDEO, "OpenXR: xrDestroySwapchain failed for eye {} ({}).", eye,
static_cast<int>(destroy_result));
}
sc.swapchain = XR_NULL_HANDLE;
}
}
}
AbstractFramebuffer* VulkanOpenXR::AcquireEyeFramebuffer(uint32_t eye_index)
{
ASSERT(eye_index < 2);
#if defined(BAIJI_STEAMVR)
// Do not acquire a second XR image pair while the previous pair is still being written.
// The submit worker releases/publishes the pending pair after its fence signals; meanwhile
// the pacing thread keeps reprojecting the last completed pair.
if (m_stereo_finalization_in_flight.load(std::memory_order_acquire))
return nullptr;
#endif
static unsigned int s_openxr_vk_acquire_log_count = 0;
const uint64_t perf_start_us = Common::Timer::NowUs();
Common::ScopeGuard perf_guard([perf_start_us] {
g_vulkan_context->GetPerfCounters().xr_swapchain_us.fetch_add(
Common::Timer::NowUs() - perf_start_us, std::memory_order_relaxed);
});
auto& sc = m_eye_swapchains[eye_index];
m_frame_uses_layered_swapchain = false;
XrSwapchainImageAcquireInfo acquire_info{XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO};
XrResult acquire_result = XR_SUCCESS;
{
auto queue_lock = AcquireGraphicsQueueLock();
acquire_result =
xrAcquireSwapchainImage(sc.swapchain, &acquire_info, &m_acquired_image_index[eye_index]);
}
if (XR_FAILED(acquire_result))
{
ERROR_LOG_FMT(VIDEO, "OpenXR: xrAcquireSwapchainImage failed for eye {}.", eye_index);
return nullptr;
}
m_image_acquired[eye_index] = true;
// Block until the acquired image is safe to write.
XrSwapchainImageWaitInfo wait_info{XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO};
wait_info.timeout = XR_INFINITE_DURATION;
if (XR_FAILED(xrWaitSwapchainImage(sc.swapchain, &wait_info)))
{
ERROR_LOG_FMT(VIDEO, "OpenXR: xrWaitSwapchainImage failed for eye {}.", eye_index);
// Ensure we don't leak an acquired image if waiting fails.
XrSwapchainImageReleaseInfo release_info{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
XrResult release_result = XR_SUCCESS;
{
auto queue_lock = AcquireGraphicsQueueLock();
release_result = xrReleaseSwapchainImage(sc.swapchain, &release_info);
}
if (XR_FAILED(release_result))
{
WARN_LOG_FMT(VIDEO,
"OpenXR: xrReleaseSwapchainImage after wait failure failed for eye {} ({}).",
eye_index, static_cast<int>(release_result));
}
m_image_acquired[eye_index] = false;
return nullptr;
}
// Reset the image layout to UNDEFINED — the runtime may have changed it since last release,
// and we're about to clear the image anyway via SetAndClearFramebuffer.
const uint32_t idx = m_acquired_image_index[eye_index];
sc.textures[idx]->OverrideImageLayout(VK_IMAGE_LAYOUT_UNDEFINED);
if (s_openxr_vk_acquire_log_count < 12)
{
INFO_LOG_FMT(OPENXR,
"OpenXR VK acquire #{}: eye={} image={} format={} size={}x{} layout={} "
"gfx_queue_family={}",
s_openxr_vk_acquire_log_count + 1, eye_index, idx,
static_cast<int>(sc.textures[idx]->GetFormat()), sc.width, sc.height,
static_cast<int>(sc.textures[idx]->GetLayout()),
g_vulkan_context->GetGraphicsQueueFamilyIndex());
s_openxr_vk_acquire_log_count++;
}
return sc.framebuffers[idx].get();
}
AbstractFramebuffer* VulkanOpenXR::AcquireLayeredFramebuffer()
{
static unsigned int s_openxr_vk_layered_acquire_log_count = 0;
auto& sc = m_layered_swapchain;
if (!m_use_layered_swapchain || sc.swapchain == XR_NULL_HANDLE)
return nullptr;
XrSwapchainImageAcquireInfo acquire_info{XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO};
XrResult acquire_result = XR_SUCCESS;
{
auto queue_lock = AcquireGraphicsQueueLock();
acquire_result =
xrAcquireSwapchainImage(sc.swapchain, &acquire_info, &m_acquired_layered_image_index);
}
if (XR_FAILED(acquire_result))
{
ERROR_LOG_FMT(VIDEO, "OpenXR: xrAcquireSwapchainImage failed for layered swapchain.");
m_frame_uses_layered_swapchain = false;
return nullptr;
}
m_layered_image_acquired = true;
XrSwapchainImageWaitInfo wait_info{XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO};
wait_info.timeout = XR_INFINITE_DURATION;
if (XR_FAILED(xrWaitSwapchainImage(sc.swapchain, &wait_info)))
{
ERROR_LOG_FMT(VIDEO, "OpenXR: xrWaitSwapchainImage failed for layered swapchain.");
XrSwapchainImageReleaseInfo release_info{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
XrResult release_result = XR_SUCCESS;
{
auto queue_lock = AcquireGraphicsQueueLock();
release_result = xrReleaseSwapchainImage(sc.swapchain, &release_info);
}
if (XR_FAILED(release_result))
{
WARN_LOG_FMT(VIDEO,
"OpenXR: xrReleaseSwapchainImage after layered wait failure failed ({}).",
static_cast<int>(release_result));
}
m_layered_image_acquired = false;
m_frame_uses_layered_swapchain = false;
return nullptr;
}
const uint32_t idx = m_acquired_layered_image_index;
sc.textures[idx]->OverrideImageLayout(VK_IMAGE_LAYOUT_UNDEFINED);
m_frame_uses_layered_swapchain = true;
if (s_openxr_vk_layered_acquire_log_count < 12)
{
INFO_LOG_FMT(OPENXR,
"OpenXR VK layered acquire #{}: image={} format={} size={}x{} layers={} "
"layout={} gfx_queue_family={}",
s_openxr_vk_layered_acquire_log_count + 1, idx,
static_cast<int>(sc.textures[idx]->GetFormat()), sc.width, sc.height,
sc.textures[idx]->GetLayers(), static_cast<int>(sc.textures[idx]->GetLayout()),
g_vulkan_context->GetGraphicsQueueFamilyIndex());
s_openxr_vk_layered_acquire_log_count++;
}
return sc.framebuffers[idx].get();
}
void VulkanOpenXR::ReleaseEyeTexture(uint32_t eye_index)
{
ASSERT(eye_index < 2);
if (!m_image_acquired[eye_index])
return;
#ifdef BAIJI_STEAMVR
if (g_ActiveConfig.vr_stereo_screen && g_ActiveConfig.stereo_mode == StereoMode::SBS)
{
// Frame StereoScreen: render both eyes before giving either image back.
// SubmitStereoFlatFrame finishes their shared GPU work and releases the pair.
StateTracker::GetInstance()->EndRenderPass();
return;
}
#endif
#if defined(ANDROID)
// End any active render pass so the eye image is no longer bound for rendering.
// We defer the Vulkan submit/xrReleaseSwapchainImage pairing until SubmitFrame()
// so both eyes stay within one command-buffer lifetime.
StateTracker::GetInstance()->EndRenderPass();
#else
// PC OpenXR runtimes are sensitive to holding runtime-owned Vulkan swapchain images
// across Dolphin's later frame-submit work. Preserve the original PC path: submit the
// blit for this eye, then release the image back to the runtime.
//
// The spec only requires the image writes to be *submitted* before
// xrReleaseSwapchainImage; GPU-side ordering is the runtime's job. Virtual Desktop
// does that with a timeline semaphore it creates on our device at session creation —
// which silently did nothing until the timelineSemaphore feature was enabled at
// device creation, so this path used to drain the GPU per eye as a workaround
// (serializing CPU/GPU/compositor every frame). Keep the drain only as a fallback for
// VD/Quest-class runtimes when the feature could not be enabled — it is a property of
// the runtime's strictness, not of which projection path is active. Frame's
// completed-pair StereoScreen handoff is handled above instead.
StateTracker::GetInstance()->EndRenderPass();
const bool wait_for_completion =
!g_vulkan_context->SupportsTimelineSemaphores() && VR::g_openxr &&
VR::g_openxr->IsQuestOrVirtualDesktopRuntime();
g_command_buffer_mgr->SubmitCommandBuffer(false, wait_for_completion);
StateTracker::GetInstance()->InvalidateCachedState();
XrSwapchainImageReleaseInfo release_info{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
XrResult result = XR_SUCCESS;
const uint64_t perf_release_start_us = Common::Timer::NowUs();
{
auto queue_lock = AcquireGraphicsQueueLock();
result = xrReleaseSwapchainImage(m_eye_swapchains[eye_index].swapchain, &release_info);
}
g_vulkan_context->GetPerfCounters().xr_swapchain_us.fetch_add(
Common::Timer::NowUs() - perf_release_start_us, std::memory_order_relaxed);
if (XR_FAILED(result))
{
WARN_LOG_FMT(VIDEO, "OpenXR: xrReleaseSwapchainImage failed for eye {} ({}).", eye_index,
static_cast<int>(result));
}
m_image_acquired[eye_index] = false;
#endif
}
void VulkanOpenXR::ReleaseLayeredTexture()
{
if (!m_layered_image_acquired)
return;
#if defined(ANDROID)
StateTracker::GetInstance()->EndRenderPass();
#else
// Same contract as ReleaseEyeTexture: submit before release; only drain the GPU as a
// fallback for VD/Quest-class runtimes when timeline semaphores could not be enabled.
StateTracker::GetInstance()->EndRenderPass();
const bool wait_for_completion =
!g_vulkan_context->SupportsTimelineSemaphores() && VR::g_openxr &&
VR::g_openxr->IsQuestOrVirtualDesktopRuntime();
g_command_buffer_mgr->SubmitCommandBuffer(false, wait_for_completion);
StateTracker::GetInstance()->InvalidateCachedState();
XrSwapchainImageReleaseInfo release_info{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
XrResult result = XR_SUCCESS;
{
auto queue_lock = AcquireGraphicsQueueLock();
result = xrReleaseSwapchainImage(m_layered_swapchain.swapchain, &release_info);
}
if (XR_FAILED(result))
{
WARN_LOG_FMT(VIDEO, "OpenXR: xrReleaseSwapchainImage failed for layered swapchain ({}).",
static_cast<int>(result));
}
m_layered_image_acquired = false;
#endif
}
bool VulkanOpenXR::SubmitFrame()
{
ASSERT(VR::g_openxr != nullptr);
#if defined(ANDROID)
static unsigned int s_openxr_vk_submit_frame_log_count = 0;
static uint64_t s_openxr_vk_async_frame_id = 0;
bool has_acquired_images = false;
has_acquired_images |= m_layered_image_acquired;
for (bool acquired : m_image_acquired)
{
has_acquired_images |= acquired;
}
if (has_acquired_images)
{
if (!WaitForPendingFrameFinalization("before queuing next async XR submit"))
return false;
PendingXRFrame pending_frame;
pending_frame.debug_frame_id = ++s_openxr_vk_async_frame_id;
pending_frame.queued_time_us = Common::Timer::NowUs();
pending_frame.display_time = VR::g_openxr->GetPredictedDisplayTime();
pending_frame.environment_blend_mode = VR::g_openxr->GetActiveBlendMode();
pending_frame.should_render = VR::g_openxr->ShouldRender();
pending_frame.space = VR::g_openxr->GetReferenceSpace();
pending_frame.layer_flags = VR::g_openxr->GetProjectionLayerExtraFlags();
// Pose the presented XFB was rendered with (stamped at its XFB copy, selected in
// FetchXFB) — not the live snapshot, which can already belong to the next frame
// when presentation is deferred to VI time.
const auto& eye_views = VR::g_openxr->GetPresentEyeViews();
const bool submit_layered =
m_frame_uses_layered_swapchain && m_use_layered_swapchain &&
m_layered_swapchain.swapchain != XR_NULL_HANDLE;
for (uint32_t eye = 0; eye < 2; ++eye)
{
auto& pv = pending_frame.projection_views[eye];
pv = {XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW};
pv.pose = eye_views[eye].pose;
pv.fov = eye_views[eye].fov;
if (submit_layered)
{
pv.subImage.swapchain = m_layered_swapchain.swapchain;
pv.subImage.imageArrayIndex = eye;
pv.subImage.imageRect = {{0, 0},
{static_cast<int32_t>(m_layered_swapchain.width),
static_cast<int32_t>(m_layered_swapchain.height)}};
}
else
{
pv.subImage.swapchain = m_eye_swapchains[eye].swapchain;
pv.subImage.imageArrayIndex = 0;
pv.subImage.imageRect = {{0, 0},
{static_cast<int32_t>(m_eye_swapchains[eye].width),
static_cast<int32_t>(m_eye_swapchains[eye].height)}};
}
}
pending_frame.layered_acquired = m_layered_image_acquired;
pending_frame.layered_swapchain = m_layered_swapchain.swapchain;
m_layered_image_acquired = false;
for (uint32_t eye = 0; eye < 2; ++eye)
{
pending_frame.eye_acquired[eye] = m_image_acquired[eye];
pending_frame.eye_swapchains[eye] = m_eye_swapchains[eye].swapchain;
m_image_acquired[eye] = false;
}
m_frame_uses_layered_swapchain = false;
if (s_openxr_vk_submit_frame_log_count < 60)
{
INFO_LOG_FMT(VIDEO,
"OpenXR Vulkan: queued async final XR submit #{} "
"(layered_acquired={} eye0_acquired={} eye1_acquired={} "
"submit_layered={} layered_swapchain_enabled={}).",
pending_frame.debug_frame_id, pending_frame.layered_acquired,
pending_frame.eye_acquired[0], pending_frame.eye_acquired[1], submit_layered,
m_use_layered_swapchain);
#if defined(ANDROID)
__android_log_print(
ANDROID_LOG_INFO, "DolphinXR",
"OpenXR Vulkan: queued async final XR submit #%llu "
"(layered_acquired=%d eye0_acquired=%d eye1_acquired=%d submit_layered=%d "
"layered_swapchain_enabled=%d)",
static_cast<unsigned long long>(pending_frame.debug_frame_id),
static_cast<int>(pending_frame.layered_acquired),
static_cast<int>(pending_frame.eye_acquired[0]),
static_cast<int>(pending_frame.eye_acquired[1]), static_cast<int>(submit_layered),
static_cast<int>(m_use_layered_swapchain));
#endif
s_openxr_vk_submit_frame_log_count++;
}
// Submit the eye rendering work once per frame before releasing the swapchain
// images back to the runtime. Do not wait for GPU completion here; waiting
// serializes the emulator, GPU, and Quest compositor every frame. We still
// advance the Vulkan frame resources because the Quest direct-to-HMD path
// skips PresentBackbuffer(), which normally resets descriptor pools.
StateTracker::GetInstance()->EndRenderPass();
m_async_frame_finalization_in_flight.store(true, std::memory_order_release);
g_command_buffer_mgr->SubmitCommandBuffer(
true, false, true, VK_NULL_HANDLE, 0xFFFFFFFF,
[this, frame = std::move(pending_frame)](VkResult submit_result) mutable {
FinalizePendingXRFrame(std::move(frame), submit_result);
});
StateTracker::GetInstance()->InvalidateCachedState();
return true;
}
#endif
// See above: stamped present pose, not the live snapshot.
const auto& eye_views = VR::g_openxr->GetPresentEyeViews();
const bool submit_layered =
m_frame_uses_layered_swapchain && m_use_layered_swapchain &&
m_layered_swapchain.swapchain != XR_NULL_HANDLE;
for (uint32_t eye = 0; eye < 2; ++eye)
{
auto& pv = m_projection_views[eye];
pv = {XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW};
pv.pose = eye_views[eye].pose;
pv.fov = eye_views[eye].fov;
if (submit_layered)
{
pv.subImage.swapchain = m_layered_swapchain.swapchain;
pv.subImage.imageArrayIndex = eye;
pv.subImage.imageRect = {{0, 0},
{static_cast<int32_t>(m_layered_swapchain.width),
static_cast<int32_t>(m_layered_swapchain.height)}};
}
else
{
pv.subImage.swapchain = m_eye_swapchains[eye].swapchain;
pv.subImage.imageArrayIndex = 0;
pv.subImage.imageRect = {
{0, 0},
{static_cast<int32_t>(m_eye_swapchains[eye].width),
static_cast<int32_t>(m_eye_swapchains[eye].height)}};
}
}
if (VR::g_openxr->IsFrameThreadActive())
{
// The pacing thread owns xrEndFrame; hand it the rendered views (poses included).
VR::g_openxr->PublishFrame(m_projection_views,
VR::g_openxr->GetProjectionLayerExtraFlags());
m_frame_uses_layered_swapchain = false;
return true;
}
m_projection_layer = {XR_TYPE_COMPOSITION_LAYER_PROJECTION};
m_projection_layer.space = VR::g_openxr->GetReferenceSpace();
m_projection_layer.viewCount = 2;
m_projection_layer.views = m_projection_views.data();
m_projection_layer.layerFlags = VR::g_openxr->GetProjectionLayerExtraFlags();
const std::vector<XrCompositionLayerBaseHeader*> layers = {
reinterpret_cast<XrCompositionLayerBaseHeader*>(&m_projection_layer)};
const bool result = VR::g_openxr->EndFrame(layers);
m_frame_uses_layered_swapchain = false;
return result;
}
bool VulkanOpenXR::SubmitFlatFrame()
{
ASSERT(VR::g_openxr != nullptr);
#if defined(ANDROID)
// On Android the per-eye ReleaseEyeTexture only ends the render pass; the command-buffer
// submit and xrReleaseSwapchainImage are deferred to submit time. Flat mode has a single
// image, so do that here synchronously. advance_to_next_frame recycles descriptor pools
// because the Quest direct-to-HMD path skips PresentBackbuffer(). The spec only requires the
// writes to be submitted (not completed) before release; the shared graphics queue preserves
// ordering, so we do not wait for the GPU.
if (m_image_acquired[0])
{
StateTracker::GetInstance()->EndRenderPass();
g_command_buffer_mgr->SubmitCommandBuffer(false, false, true);
StateTracker::GetInstance()->InvalidateCachedState();
XrSwapchainImageReleaseInfo release_info{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
XrResult result = XR_SUCCESS;
{
auto queue_lock = AcquireGraphicsQueueLock();
result = xrReleaseSwapchainImage(m_eye_swapchains[0].swapchain, &release_info);
}
if (XR_FAILED(result))
{
WARN_LOG_FMT(VIDEO, "OpenXR: xrReleaseSwapchainImage failed for flat panel ({}).",
static_cast<int>(result));
}
m_image_acquired[0] = false;
}
#endif
return VR::g_openxr->SubmitFlatQuadFrame(m_eye_swapchains[0].swapchain,
m_eye_swapchains[0].width, m_eye_swapchains[0].height);
}
void VulkanOpenXR::FinalizeStereoFlatFrameAfterGPU(VkFence fence, bool have_pair,
VkResult submit_result)
{
Common::ScopeGuard completion_guard([this] {
m_stereo_finalization_in_flight.store(false, std::memory_order_release);
});
if (submit_result == VK_SUCCESS)
{
const VkResult wait_result =
vkWaitForFences(g_vulkan_context->GetDevice(), 1, &fence, VK_TRUE, UINT64_MAX);
if (wait_result != VK_SUCCESS)
{
LOG_VULKAN_ERROR(wait_result, "StereoScreen async completion fence wait failed: ");
m_async_frame_finalization_failed.store(true, std::memory_order_release);
return;
}
}
else
{
LOG_VULKAN_ERROR(submit_result, "StereoScreen async queue submission failed: ");
m_async_frame_finalization_failed.store(true, std::memory_order_release);
}
if (!VR::g_openxr)
{
ERROR_LOG_FMT(VIDEO, "OpenXR Vulkan: XR manager disappeared before stereo pair finalization.");
return;
}
bool released = true;
{
// The image writes are now complete. Keep the existing handoff -> graphics queue -> publish
// order for the short release/publish transaction, but do not hold either lock during the
// GPU fence wait above.
VR::OpenXRManager::ScopedVideoFrameHandoff handoff(VR::g_openxr.get());
{
auto queue_lock = AcquireGraphicsQueueLock();
XrSwapchainImageReleaseInfo release_info{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
for (uint32_t eye = 0; eye < 2; ++eye)
{
if (!m_image_acquired[eye])
continue;
const XrResult result =
xrReleaseSwapchainImage(m_eye_swapchains[eye].swapchain, &release_info);
if (XR_FAILED(result))
{
WARN_LOG_FMT(VIDEO, "OpenXR: async completed stereo eye {} release failed ({}).", eye,
static_cast<int>(result));
released = false;
}
else
{
m_image_acquired[eye] = false;
}
}
}
if (!released || submit_result != VK_SUCCESS)
return;
if (have_pair && VR::g_openxr->IsFrameThreadActive())
{
if (!VR::g_openxr->SubmitStereoQuadFrame({m_eye_swapchains[0].swapchain,
m_eye_swapchains[1].swapchain},
m_eye_swapchains[0].width,
m_eye_swapchains[0].height))
{
m_async_frame_finalization_failed.store(true, std::memory_order_release);
}
}
}
}
bool VulkanOpenXR::SubmitStereoFlatFrame()
{
ASSERT(VR::g_openxr != nullptr);
#if defined(BAIJI_STEAMVR)
// Keep displaying the last completed pair while the submit worker finishes the prior pair.
if (m_stereo_finalization_in_flight.load(std::memory_order_acquire))
return true;
if (m_image_acquired[0] || m_image_acquired[1])
{
const bool have_pair = m_image_acquired[0] && m_image_acquired[1];
StateTracker::GetInstance()->EndRenderPass();
if (VR::g_openxr->IsFrameThreadActive() && g_command_buffer_mgr->UsesThreadedSubmission())
{
// Let the Vulkan worker wait for GPU completion. Until its fence signals, the XR pacing
// thread can safely reproject the last released pair and this video thread can continue
// emulation; AcquireEyeFramebuffer skips another XR pair while finalization is pending.
const VkFence completion_fence = g_command_buffer_mgr->GetCurrentFence();
m_stereo_finalization_in_flight.store(true, std::memory_order_release);
const bool queued = g_command_buffer_mgr->SubmitCommandBuffer(
true, false, false, VK_NULL_HANDLE, 0xFFFFFFFF,
[this, completion_fence, have_pair](VkResult submit_result) {
FinalizeStereoFlatFrameAfterGPU(completion_fence, have_pair, submit_result);
});
StateTracker::GetInstance()->InvalidateCachedState();
if (!queued)
{
m_stereo_finalization_in_flight.store(false, std::memory_order_release);
return false;
}
return have_pair;
}
// Submitted writes alone left visible doubled/stale images on Steam Frame.
// Finish BOTH eye blits before releasing either image to the runtime. Wait
// once for the pair, rather than draining the GPU separately for each eye.
// Keep this completion wait even when timeline semaphores are enabled.
// PresentBackbuffer already advances frame resources, so do not advance them
// a second time here.
const bool submitted = g_command_buffer_mgr->SubmitCommandBuffer(false, true);
StateTracker::GetInstance()->InvalidateCachedState();
if (!submitted)
return false;
bool released = true;
{
auto queue_lock = AcquireGraphicsQueueLock();
XrSwapchainImageReleaseInfo release_info{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
for (uint32_t eye = 0; eye < 2; ++eye)
{
if (!m_image_acquired[eye])
continue;
const XrResult result =
xrReleaseSwapchainImage(m_eye_swapchains[eye].swapchain, &release_info);
if (XR_FAILED(result))
{
WARN_LOG_FMT(VIDEO, "OpenXR: completed stereo eye {} release failed ({}).", eye,
static_cast<int>(result));
released = false;
}
m_image_acquired[eye] = false;
}
}
if (!have_pair || !released)
return false;
}
#elif defined(ANDROID)
// On Android the per-eye ReleaseEyeTexture only ends the render pass; the command-buffer
// submit and xrReleaseSwapchainImage are deferred to submit time. End the render pass once,
// submit the accumulated command buffer, then release both eye images synchronously.
bool any_acquired = false;
for (uint32_t eye = 0; eye < 2; ++eye)
{
if (m_image_acquired[eye])
{
any_acquired = true;
break;
}
}
if (any_acquired)
{
StateTracker::GetInstance()->EndRenderPass();
g_command_buffer_mgr->SubmitCommandBuffer(false, false, true);
StateTracker::GetInstance()->InvalidateCachedState();
for (uint32_t eye = 0; eye < 2; ++eye)
{
if (!m_image_acquired[eye])
continue;
XrSwapchainImageReleaseInfo release_info{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
XrResult result = XR_SUCCESS;
{
auto queue_lock = AcquireGraphicsQueueLock();
result = xrReleaseSwapchainImage(m_eye_swapchains[eye].swapchain, &release_info);
}
if (XR_FAILED(result))
{
WARN_LOG_FMT(VIDEO,
"OpenXR: xrReleaseSwapchainImage failed for stereo screen eye {} ({}).", eye,
static_cast<int>(result));
}
m_image_acquired[eye] = false;
}
}
#endif
return VR::g_openxr->SubmitStereoQuadFrame({m_eye_swapchains[0].swapchain, m_eye_swapchains[1].swapchain},
m_eye_swapchains[0].width, m_eye_swapchains[0].height);
}
} // namespace Vulkan
#endif // ENABLE_VR