// 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 #include #include #include #include #include "Common/Assert.h" #include "Common/Logging/Log.h" #include "VideoBackends/Vulkan/CommandBufferManager.h" #include "VideoBackends/Vulkan/StateTracker.h" #include "VideoBackends/Vulkan/VKTexture.h" #include "VideoBackends/Vulkan/VulkanContext.h" #include "VideoCommon/TextureConfig.h" #include "VideoCommon/VR/OpenXRManager.h" #ifdef _WIN32 #include // for SEH __try/__except #endif namespace Vulkan { std::unique_ptr g_openxr_vk; static const char* VkFormatToString(int64_t format) { switch (static_cast(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_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_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(result)); return false; } std::vector 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(result)); return false; } for (const int64_t format : runtime_formats) { INFO_LOG_FMT(VIDEO, "OpenXR: Runtime swapchain format {} ({})", static_cast(format), VkFormatToString(format)); } // Prefer linear (non-sRGB) formats — Dolphin handles gamma in shaders. static constexpr std::array preferred_formats = { VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_R8G8B8A8_SRGB, VK_FORMAT_B8G8R8A8_SRGB, VK_FORMAT_R16G16B16A16_SFLOAT}; for (const VkFormat preferred : preferred_formats) { const int64_t wanted = static_cast(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(*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(*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(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(); } // static bool VulkanOpenXR::PreQueryVulkanExtensions(VulkanExtensionRequirements& out) { INFO_LOG_FMT(VIDEO, "OpenXR Vulkan: Pre-querying required Vulkan extensions..."); auto mgr = std::make_unique(); const std::vector extensions = {XR_KHR_VULKAN_ENABLE_EXTENSION_NAME}; 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(); // Query required Vulkan instance extensions. PFN_xrGetVulkanInstanceExtensionsKHR pfnGetInstanceExts = nullptr; xrGetInstanceProcAddr(xr_instance, "xrGetVulkanInstanceExtensionsKHR", reinterpret_cast(&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(&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(); const std::vector extensions = {XR_KHR_VULKAN_ENABLE_EXTENSION_NAME}; if (!mgr->CreateInstance(extensions)) return false; if (!mgr->InitializeSystem()) return false; if (!mgr->EnumerateViewConfigurations()) return false; VR::g_openxr = std::move(mgr); } 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() { // Clear swapchain pointer before destroying swapchains so no dangling use occurs. if (VR::g_openxr) VR::g_openxr->SetSwapchain(nullptr); DestroySwapchains(); VR::g_openxr.reset(); // Wait for all GPU work to finish before the Vulkan device is destroyed. if (g_vulkan_context) 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; XrResult result = xrGetInstanceProcAddr( xr_instance, "xrGetVulkanGraphicsRequirementsKHR", reinterpret_cast(&pfnGetVulkanRequirements)); if (XR_FAILED(result) || pfnGetVulkanRequirements == nullptr) { ERROR_LOG_FMT(VIDEO, "OpenXR: Could not load xrGetVulkanGraphicsRequirementsKHR."); 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(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(&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); } else { INFO_LOG_FMT(VIDEO, "OpenXR: No additional Vulkan instance extensions required."); } } // --- 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(&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); } else { INFO_LOG_FMT(VIDEO, "OpenXR: No additional Vulkan device extensions required."); } } // --- Verify the physical device matches what the runtime expects --- INFO_LOG_FMT(VIDEO, "OpenXR Vulkan: Checking physical device..."); PFN_xrGetVulkanGraphicsDeviceKHR pfnGetVulkanDevice = nullptr; result = xrGetInstanceProcAddr(xr_instance, "xrGetVulkanGraphicsDeviceKHR", reinterpret_cast(&pfnGetVulkanDevice)); if (XR_SUCCEEDED(result) && pfnGetVulkanDevice != nullptr) { VkPhysicalDevice xr_physical_device = VK_NULL_HANDLE; result = pfnGetVulkanDevice(xr_instance, xr_system, g_vulkan_context->GetVulkanInstance(), &xr_physical_device); if (XR_SUCCEEDED(result)) { if (xr_physical_device != g_vulkan_context->GetPhysicalDevice()) { WARN_LOG_FMT(VIDEO, "OpenXR: Runtime wants a different VkPhysicalDevice than Dolphin selected. " "VR may not work correctly."); } else { 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(g_vulkan_context->GetVulkanInstance()), reinterpret_cast(g_vulkan_context->GetPhysicalDevice()), reinterpret_cast(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(result)); return false; } VR::g_openxr->SetSession(session); INFO_LOG_FMT(VIDEO, "OpenXR Vulkan: Session created successfully."); return true; } bool VulkanOpenXR::CreateSwapchains() { ASSERT(VR::g_openxr != nullptr); const auto& view_cfgs = VR::g_openxr->GetViewConfigViews(); int64_t swapchain_format = 0; if (!SelectSwapchainFormat(VR::g_openxr->GetSession(), &swapchain_format)) return false; const AbstractTextureFormat abstract_format = VkFormatToAbstractFormat(static_cast(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; XrResult 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(result)); return false; } // Enumerate the Vulkan images backing this swapchain. uint32_t image_count = 0; xrEnumerateSwapchainImages(sc.swapchain, 0, &image_count, nullptr); std::vector images(image_count, {XR_TYPE_SWAPCHAIN_IMAGE_VULKAN_KHR}); xrEnumerateSwapchainImages(sc.swapchain, image_count, &image_count, reinterpret_cast(images.data())); 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. VKTexture::CreateAdopted wraps the image // without allocating device memory (the runtime owns it). sc.textures[i] = VKTexture::CreateAdopted(tex_config, images[i].image, VK_IMAGE_VIEW_TYPE_2D, VK_IMAGE_LAYOUT_UNDEFINED); 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, {}); if (!sc.framebuffers[i]) { ERROR_LOG_FMT(VIDEO, "OpenXR: VKFramebuffer::Create failed for eye {}, image {}.", eye, i); return false; } } INFO_LOG_FMT(VIDEO, "OpenXR: Eye {} swapchain ready: {}x{}, {} images.", eye, sc.width, sc.height, image_count); } return true; } void VulkanOpenXR::DestroySwapchains() { // Wait for the GPU to finish all pending work before destroying resources. if (g_vulkan_context) vkDeviceWaitIdle(g_vulkan_context->GetDevice()); 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}; const XrResult 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(release_result)); } m_image_acquired[eye] = false; } // Release Dolphin wrappers before destroying the swapchain so the // runtime's VkImages are only freed after our views are gone. sc.framebuffers.clear(); sc.textures.clear(); 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(destroy_result)); } sc.swapchain = XR_NULL_HANDLE; } } } AbstractFramebuffer* VulkanOpenXR::AcquireEyeFramebuffer(uint32_t eye_index) { ASSERT(eye_index < 2); auto& sc = m_eye_swapchains[eye_index]; XrSwapchainImageAcquireInfo acquire_info{XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO}; if (XR_FAILED( xrAcquireSwapchainImage(sc.swapchain, &acquire_info, &m_acquired_image_index[eye_index]))) { 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}; const XrResult 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(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); return sc.framebuffers[idx].get(); } void VulkanOpenXR::ReleaseEyeTexture(uint32_t eye_index) { ASSERT(eye_index < 2); if (!m_image_acquired[eye_index]) return; // End any active render pass so the image layout transitions are recorded // and the image is no longer being written to. StateTracker::GetInstance()->EndRenderPass(); XrSwapchainImageReleaseInfo release_info{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO}; const XrResult result = xrReleaseSwapchainImage(m_eye_swapchains[eye_index].swapchain, &release_info); if (XR_FAILED(result)) { WARN_LOG_FMT(VIDEO, "OpenXR: xrReleaseSwapchainImage failed for eye {} ({}).", eye_index, static_cast(result)); } m_image_acquired[eye_index] = false; } bool VulkanOpenXR::SubmitFrame() { ASSERT(VR::g_openxr != nullptr); const auto& eye_views = VR::g_openxr->GetEyeViews(); 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; pv.subImage.swapchain = m_eye_swapchains[eye].swapchain; pv.subImage.imageArrayIndex = 0; pv.subImage.imageRect = { {0, 0}, {static_cast(m_eye_swapchains[eye].width), static_cast(m_eye_swapchains[eye].height)}}; } 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(); const std::vector layers = { reinterpret_cast(&m_projection_layer)}; return VR::g_openxr->EndFrame(layers); } } // namespace Vulkan #endif // ENABLE_VR