mirror of
https://github.com/thejerf/baijiSteamVR.git
synced 2026-10-23 10:02:02 +02:00
1793 lines
68 KiB
C++
1793 lines
68 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 Dolphin 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))
|
|
{
|
|
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)
|
|
{
|
|
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");
|
|
}
|
|
|
|
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 Dolphin's multiview path (a 1.1 feature) and crashes vkCreateDevice.
|
|
// Treat 1.0.0 as "unknown" so Dolphin 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 Dolphin'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(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)
|
|
{
|
|
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;
|
|
}
|
|
|
|
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())
|
|
{
|
|
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 Dolphin's Vulkan API version for comparison.
|
|
const u32 dolphin_api = g_vulkan_context->GetDeviceInfo().apiVersion;
|
|
INFO_LOG_FMT(VIDEO, "OpenXR: Dolphin 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);
|
|
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;
|
|
|
|
#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 (SteamVR never
|
|
// needed it).
|
|
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)]() mutable {
|
|
FinalizePendingXRFrame(std::move(frame));
|
|
});
|
|
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);
|
|
}
|
|
|
|
bool VulkanOpenXR::SubmitStereoFlatFrame()
|
|
{
|
|
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. 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
|