Files
thejerf--baijiSteamVR/Source/Core/VideoBackends/Vulkan/CommandBufferManager.cpp
T
iChris4andClaude Opus 5 05d91da8bf Fixed Vulkan VR crash when opening a second game in one process
Opening a game, closing it, and opening another crashed or device-lost on
the Vulkan backend with VR enabled. D3D11 and D3D12 both restart cleanly,
so this was Vulkan-specific rather than an OpenXR runtime limitation.

Root cause, proven with VK_EXT_device_fault plus
VK_EXT_device_address_binding_report: the fault is a write-invalid at a
page-aligned address that the binding tracer reports as not owned by the
second game device. The OpenXR compositor still references resources that
lived on the first game VkDevice; destroying that device frees the memory
and the compositor later writes into the freed page. The D3D backends are
immune because their shared compositor resources are COM ref-counted,
while vkDestroyDevice frees unconditionally.

Fix: persist and reuse the VulkanContext (VkInstance and VkDevice) across
VR games, so vkCreateDevice/vkDestroyDevice run once per process and those
pages are never unmapped. Per-game objects are still fully recreated,
including the surface, swapchain, object cache, command buffers, and the
OpenXR session and XrInstance. A device-lost context is never persisted. A
non-VR launch, or a runtime requiring a device extension the persisted
device lacks, discards it and initializes fresh.

Two problems that persisting exposed:

- InitBackendInfo() creates and destroys a temporary VkInstance on every
  game boot and then calls UnloadVulkanLibrary(), which nulls every global
  Vulkan function pointer. Reuse now re-resolves the instance and device
  dispatch for the persisted handles.

- That same UnloadVulkanLibrary() unmaps vulkan-1.dll on runtimes that do
  not otherwise pin it, invalidating the persisted handles. This is why
  VDXR still failed after SteamVR worked. Added KeepVulkanLibraryLoaded()
  so unloading is suppressed while a device is persisted.

Also removes the XR_KHR_vulkan_enable2 path. Creating a second device
against a retained XrInstance crashes inside the runtime in a way SEH
cannot catch. The backend now always uses legacy XR_KHR_vulkan_enable,
where Dolphin owns instance and device creation and the runtime binds at
xrCreateSession.

Supporting changes: ordered OpenXR session teardown (ShutdownSession and
DestroySession), a device-lost guard in CommandBufferManager that stops
emulation instead of resubmitting to a dead device, synchronous release of
Dolphin views and framebuffers before xrDestroySwapchain, and the two GPU
fault diagnostics. Note that vkGetDeviceFaultInfoEXT must be called with
vendorBinarySize zeroed on the second call, or the driver writes the
vendor blob to a null pointer.

Verified on SteamVR/PSVR2, VDXR/Quest 3, and Meta Link over repeated
open/close cycles.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 15:43:14 +02:00

802 lines
30 KiB
C++

// Copyright 2016 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "VideoBackends/Vulkan/CommandBufferManager.h"
#include <algorithm>
#include <atomic>
#include <array>
#include <cstdint>
#include <utility>
#include "Common/Assert.h"
#include "Common/Config/Config.h"
#include "Common/Logging/Log.h"
#include "Common/MsgHandler.h"
#include "Common/Thread.h"
#include "Common/Timer.h"
#include "Core/Core.h"
#include "Core/Config/GraphicsSettings.h"
#include "VideoBackends/Vulkan/VulkanContext.h"
#include "VideoCommon/Constants.h"
#if defined(ANDROID) && defined(ENABLE_VR)
#include "VideoCommon/VR/OpenXRManager.h"
#endif
#include "vulkan/vulkan_core.h"
#if defined(ANDROID)
#include <android/log.h>
#endif
namespace Vulkan
{
namespace
{
constexpr u32 MAX_SUBMIT_DECISION_LOGS = 96;
constexpr u32 MAX_WORKER_EXECUTE_LOGS = 64;
constexpr u32 MAX_WORKER_DRAIN_LOGS = 32;
std::atomic<u32> s_submit_decision_log_count{0};
std::atomic<u32> s_worker_execute_log_count{0};
std::atomic<u32> s_worker_drain_log_count{0};
bool ShouldLogSubmitDebug(std::atomic<u32>& counter, u32 limit)
{
return counter.fetch_add(1, std::memory_order_relaxed) < limit;
}
const char* BoolString(bool value)
{
return value ? "true" : "false";
}
} // namespace
CommandBufferManager::CommandBufferManager(bool use_threaded_submission)
: m_use_threaded_submission(use_threaded_submission)
{
}
CommandBufferManager::~CommandBufferManager()
{
// If the worker thread is enabled, stop and block until it exits.
if (m_use_threaded_submission)
{
WaitForWorkerThreadIdle();
m_submit_thread.Shutdown();
}
DestroyCommandBuffers();
}
bool CommandBufferManager::Initialize(size_t swapchain_image_count)
{
if (!CreateCommandBuffers(swapchain_image_count))
return false;
if (m_use_threaded_submission && !CreateSubmitThread())
return false;
return true;
}
bool CommandBufferManager::CreateCommandBuffers(size_t swapchain_image_count)
{
static constexpr VkSemaphoreCreateInfo semaphore_create_info = {
VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, nullptr, 0};
VkDevice device = g_vulkan_context->GetDevice();
VkResult res;
for (CmdBufferResources& resources : m_command_buffers)
{
resources.init_command_buffer_used = false;
resources.semaphore_used = false;
VkCommandPoolCreateInfo pool_info = {VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, nullptr, 0,
g_vulkan_context->GetGraphicsQueueFamilyIndex()};
res = vkCreateCommandPool(g_vulkan_context->GetDevice(), &pool_info, nullptr,
&resources.command_pool);
if (res != VK_SUCCESS)
{
LOG_VULKAN_ERROR(res, "vkCreateCommandPool failed: ");
return false;
}
VkCommandBufferAllocateInfo buffer_info = {
VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, nullptr, resources.command_pool,
VK_COMMAND_BUFFER_LEVEL_PRIMARY, static_cast<uint32_t>(resources.command_buffers.size())};
res = vkAllocateCommandBuffers(device, &buffer_info, resources.command_buffers.data());
if (res != VK_SUCCESS)
{
LOG_VULKAN_ERROR(res, "vkAllocateCommandBuffers failed: ");
return false;
}
VkFenceCreateInfo fence_info = {VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, nullptr,
VK_FENCE_CREATE_SIGNALED_BIT};
res = vkCreateFence(device, &fence_info, nullptr, &resources.fence);
if (res != VK_SUCCESS)
{
LOG_VULKAN_ERROR(res, "vkCreateFence failed: ");
return false;
}
res = vkCreateSemaphore(device, &semaphore_create_info, nullptr, &resources.semaphore);
if (res != VK_SUCCESS)
{
LOG_VULKAN_ERROR(res, "vkCreateSemaphore failed: ");
return false;
}
}
m_present_semaphores.reserve(swapchain_image_count);
for (uint32_t i = 0; i < swapchain_image_count; i++)
{
VkSemaphore present_semaphore;
res = vkCreateSemaphore(device, &semaphore_create_info, nullptr, &present_semaphore);
if (res != VK_SUCCESS)
{
LOG_VULKAN_ERROR(res, "vkCreateSemaphore failed: ");
return false;
}
m_present_semaphores.push_back(present_semaphore);
}
// Activate the first command buffer. BeginCommandBuffer moves forward, so start with the last
m_current_cmd_buffer = static_cast<u32>(m_command_buffers.size()) - 1;
BeginCommandBuffer();
return true;
}
void CommandBufferManager::DestroyCommandBuffers()
{
VkDevice device = g_vulkan_context->GetDevice();
for (CmdBufferResources& resources : m_command_buffers)
{
// The Vulkan spec section 5.2 says: "When a pool is destroyed, all command buffers allocated
// from the pool are freed.". So we don't need to free the command buffers, just the pools.
// We destroy the command pool first, to avoid any warnings from the validation layers about
// objects which are pending destruction being in-use.
if (resources.command_pool != VK_NULL_HANDLE)
vkDestroyCommandPool(device, resources.command_pool, nullptr);
// Destroy any pending objects.
for (auto& it : resources.cleanup_resources)
it();
if (resources.semaphore != VK_NULL_HANDLE)
vkDestroySemaphore(device, resources.semaphore, nullptr);
if (resources.fence != VK_NULL_HANDLE)
vkDestroyFence(device, resources.fence, nullptr);
}
for (FrameResources& resources : m_frame_resources)
{
for (VkDescriptorPool descriptor_pool : resources.descriptor_pools)
{
vkDestroyDescriptorPool(device, descriptor_pool, nullptr);
}
}
for (VkSemaphore present_semaphore : m_present_semaphores)
{
vkDestroySemaphore(device, present_semaphore, nullptr);
}
}
VkDescriptorPool CommandBufferManager::CreateDescriptorPool(u32 max_descriptor_sets)
{
/*
* Worst case descriptor counts according to the descriptor layout created in ObjectCache.cpp:
* UNIFORM_BUFFER_DYNAMIC: 3
* COMBINED_IMAGE_SAMPLER: NUM_UTILITY_PIXEL_SAMPLERS + NUM_COMPUTE_SHADER_SAMPLERS +
* VideoCommon::MAX_PIXEL_SHADER_SAMPLERS
* STORAGE_BUFFER: 2
* UNIFORM_TEXEL_BUFFER: 3
* STORAGE_IMAGE: 1
*/
const std::array<VkDescriptorPoolSize, 5> pool_sizes{{
{VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, max_descriptor_sets * 3},
{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
max_descriptor_sets *
(VideoCommon::MAX_PIXEL_SHADER_SAMPLERS + VideoCommon::MAX_COMPUTE_SHADER_SAMPLERS +
NUM_UTILITY_PIXEL_SAMPLERS)},
{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, max_descriptor_sets * 2},
{VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, max_descriptor_sets * 3},
{VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, max_descriptor_sets * 1},
}};
const VkDescriptorPoolCreateInfo pool_create_info = {
VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO, nullptr, 0, max_descriptor_sets,
static_cast<u32>(pool_sizes.size()), pool_sizes.data(),
};
VkDevice device = g_vulkan_context->GetDevice();
VkDescriptorPool descriptor_pool = VK_NULL_HANDLE;
VkResult res = vkCreateDescriptorPool(device, &pool_create_info, nullptr, &descriptor_pool);
if (res != VK_SUCCESS)
{
LOG_VULKAN_ERROR(res, "vkCreateDescriptorPool failed: ");
return VK_NULL_HANDLE;
}
return descriptor_pool;
}
VkDescriptorSet CommandBufferManager::AllocateDescriptorSet(VkDescriptorSetLayout set_layout)
{
VkDescriptorSet descriptor_set = VK_NULL_HANDLE;
FrameResources& resources = GetCurrentFrameResources();
if (!resources.descriptor_pools.empty()) [[likely]]
{
VkDescriptorSetAllocateInfo allocate_info = {
VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, nullptr,
resources.descriptor_pools[resources.current_descriptor_pool_index], 1, &set_layout};
VkResult res =
vkAllocateDescriptorSets(g_vulkan_context->GetDevice(), &allocate_info, &descriptor_set);
if (res != VK_SUCCESS &&
resources.descriptor_pools.size() > resources.current_descriptor_pool_index + 1)
{
// Mark the next descriptor set as active and try again.
resources.current_descriptor_pool_index++;
descriptor_set = AllocateDescriptorSet(set_layout);
}
}
if (descriptor_set == VK_NULL_HANDLE) [[unlikely]]
{
VkDescriptorPool descriptor_pool = CreateDescriptorPool(DESCRIPTOR_SETS_PER_POOL);
m_descriptor_set_count += DESCRIPTOR_SETS_PER_POOL;
if (descriptor_pool == VK_NULL_HANDLE) [[unlikely]]
return VK_NULL_HANDLE;
resources.descriptor_pools.push_back(descriptor_pool);
resources.current_descriptor_pool_index =
static_cast<u32>(resources.descriptor_pools.size()) - 1;
descriptor_set = AllocateDescriptorSet(set_layout);
}
return descriptor_set;
}
bool CommandBufferManager::CreateSubmitThread()
{
m_submit_thread.Reset("VK submission thread", [this](PendingCommandBufferSubmit submit) {
#if defined(ANDROID) && defined(ENABLE_VR)
static thread_local bool registered_with_openxr = false;
if (!registered_with_openxr && VR::g_openxr)
{
const bool registered = VR::g_openxr->RegisterCurrentAndroidThread(
VR::OpenXRManager::AndroidThreadType::RendererWorker, "VK submission thread");
INFO_LOG_FMT(VIDEO,
"Vulkan submit worker: OpenXR RendererWorker registration result={} for "
"'VK submission thread'.",
registered);
#if defined(ANDROID)
__android_log_print(ANDROID_LOG_INFO, "DolphinXR",
"Vulkan submit worker: OpenXR RendererWorker registration result=%d "
"for 'VK submission thread'",
static_cast<int>(registered));
#endif
// Fast core, off the CPU/Video cores (see Core.cpp thread pins).
if (Config::Get(Config::GFX_VR_PIN_EMULATION_CORES))
{
const int core =
Common::PinCurrentThreadToPerformanceCore(Common::ThreadCoreRole::VRSubmit);
if (core >= 0)
INFO_LOG_FMT(VIDEO, "Vulkan submit worker pinned to performance core cpu{}.", core);
}
registered_with_openxr = true;
}
#endif
if (ShouldLogSubmitDebug(s_worker_execute_log_count, MAX_WORKER_EXECUTE_LOGS))
{
const bool has_present = submit.present_swap_chain != VK_NULL_HANDLE;
INFO_LOG_FMT(VIDEO,
"Vulkan submit worker executing: cmd={} present={} image={} "
"(first {} worker submits logged).",
submit.command_buffer_index, has_present, submit.present_image_index,
MAX_WORKER_EXECUTE_LOGS);
#if defined(ANDROID)
__android_log_print(ANDROID_LOG_INFO, "DolphinXR",
"Vulkan submit worker executing: cmd=%u present=%d image=%u",
submit.command_buffer_index, static_cast<int>(has_present),
submit.present_image_index);
#endif
}
SubmitCommandBuffer(submit.command_buffer_index, submit.present_swap_chain,
submit.present_image_index);
if (submit.post_submit_callback)
submit.post_submit_callback();
CmdBufferResources& resources = m_command_buffers[submit.command_buffer_index];
resources.waiting_for_submit.store(false, std::memory_order_release);
});
return true;
}
void CommandBufferManager::WaitForWorkerThreadIdle()
{
if (!m_use_threaded_submission)
return;
if (ShouldLogSubmitDebug(s_worker_drain_log_count, MAX_WORKER_DRAIN_LOGS))
{
INFO_LOG_FMT(VIDEO,
"Vulkan submit worker drain requested via WaitForWorkerThreadIdle() "
"(first {} drains logged).",
MAX_WORKER_DRAIN_LOGS);
#if defined(ANDROID)
__android_log_print(ANDROID_LOG_INFO, "DolphinXR",
"Vulkan submit worker drain requested via WaitForWorkerThreadIdle()");
#endif
}
m_submit_thread.WaitForCompletion();
}
void CommandBufferManager::WaitForFenceCounter(u64 fence_counter)
{
if (m_completed_fence_counter >= fence_counter)
return;
// Find the first command buffer which covers this counter value.
u32 index = (m_current_cmd_buffer + 1) % NUM_COMMAND_BUFFERS;
while (index != m_current_cmd_buffer)
{
if (m_command_buffers[index].fence_counter >= fence_counter)
break;
index = (index + 1) % NUM_COMMAND_BUFFERS;
}
ASSERT(index != m_current_cmd_buffer);
WaitForCommandBufferCompletion(index);
}
void CommandBufferManager::WaitForCommandBufferCompletion(u32 index)
{
if (IsDeviceLost())
return;
const u64 perf_start_us = Common::Timer::NowUs();
CmdBufferResources& resources = m_command_buffers[index];
// Ensure this command buffer has been submitted.
if (resources.waiting_for_submit.load(std::memory_order_acquire))
{
WaitForWorkerThreadIdle();
ASSERT_MSG(VIDEO, !resources.waiting_for_submit.load(std::memory_order_relaxed),
"Submit thread is idle but command buffer is still waiting for submission!");
}
// Wait for this command buffer to be completed.
VkResult res =
vkWaitForFences(g_vulkan_context->GetDevice(), 1, &resources.fence, VK_TRUE, UINT64_MAX);
if (res != VK_SUCCESS)
{
LOG_VULKAN_ERROR(res, "vkWaitForFences failed: ");
if (res == VK_ERROR_DEVICE_LOST)
HandleDeviceLost("vkWaitForFences");
return;
}
// Clean up any resources for command buffers between the last known completed buffer and this
// now-completed command buffer. If we use >2 buffers, this may be more than one buffer.
const u64 now_completed_counter = resources.fence_counter;
u32 cleanup_index = (m_current_cmd_buffer + 1) % NUM_COMMAND_BUFFERS;
while (cleanup_index != m_current_cmd_buffer)
{
CmdBufferResources& cleanup_resources = m_command_buffers[cleanup_index];
if (cleanup_resources.fence_counter > now_completed_counter)
break;
if (cleanup_resources.fence_counter > m_completed_fence_counter)
{
for (auto& it : cleanup_resources.cleanup_resources)
it();
cleanup_resources.cleanup_resources.clear();
}
cleanup_index = (cleanup_index + 1) % NUM_COMMAND_BUFFERS;
}
m_completed_fence_counter = now_completed_counter;
g_vulkan_context->GetPerfCounters().fence_wait_us.fetch_add(
Common::Timer::NowUs() - perf_start_us, std::memory_order_relaxed);
}
bool CommandBufferManager::SubmitCommandBuffer(bool submit_on_worker_thread,
bool wait_for_completion, bool advance_to_next_frame,
VkSwapchainKHR present_swap_chain,
uint32_t present_image_index,
std::function<void()> post_submit_callback)
{
if (IsDeviceLost())
return false;
// End the current command buffer.
CmdBufferResources& resources = GetCurrentCmdBufferResources();
for (VkCommandBuffer command_buffer : resources.command_buffers)
{
VkResult res = vkEndCommandBuffer(command_buffer);
if (res != VK_SUCCESS)
{
LOG_VULKAN_ERROR(res, "vkEndCommandBuffer failed: ");
PanicAlertFmt("Failed to end command buffer: {} ({})", VkResultToString(res),
static_cast<int>(res));
}
}
const bool submit_to_worker =
m_use_threaded_submission && submit_on_worker_thread && !wait_for_completion;
const bool has_present = present_swap_chain != VK_NULL_HANDLE;
const char* submit_path = submit_to_worker ? "worker" : "sync";
const char* sync_reason = "worker";
if (!submit_to_worker)
{
if (!m_use_threaded_submission)
sync_reason = "threading_disabled";
else if (!submit_on_worker_thread)
sync_reason = "caller_requested_sync";
else
sync_reason = "wait_for_completion";
}
if (ShouldLogSubmitDebug(s_submit_decision_log_count, MAX_SUBMIT_DECISION_LOGS))
{
INFO_LOG_FMT(VIDEO,
"Vulkan submit decision: path={} reason={} threaded={} request_worker={} wait={} "
"advance={} present={} cmd={} frame={} fence={} init={} semaphore={} "
"(first {} decisions logged).",
submit_path, sync_reason, BoolString(m_use_threaded_submission),
BoolString(submit_on_worker_thread), BoolString(wait_for_completion),
BoolString(advance_to_next_frame), BoolString(has_present), m_current_cmd_buffer,
m_current_frame, resources.fence_counter, BoolString(resources.init_command_buffer_used),
BoolString(resources.semaphore_used), MAX_SUBMIT_DECISION_LOGS);
#if defined(ANDROID)
__android_log_print(
ANDROID_LOG_INFO, "DolphinXR",
"Vulkan submit decision: path=%s reason=%s threaded=%d request_worker=%d wait=%d "
"advance=%d present=%d cmd=%u frame=%u fence=%llu init=%d semaphore=%d",
submit_path, sync_reason, static_cast<int>(m_use_threaded_submission),
static_cast<int>(submit_on_worker_thread), static_cast<int>(wait_for_completion),
static_cast<int>(advance_to_next_frame), static_cast<int>(has_present), m_current_cmd_buffer,
m_current_frame, static_cast<unsigned long long>(resources.fence_counter),
static_cast<int>(resources.init_command_buffer_used), static_cast<int>(resources.semaphore_used));
#endif
}
// Submitting off-thread?
if (submit_to_worker)
{
resources.waiting_for_submit.store(true, std::memory_order_relaxed);
// Push to the pending submit queue.
m_submit_thread.Push(
{present_swap_chain, present_image_index, m_current_cmd_buffer,
std::move(post_submit_callback)});
}
else
{
WaitForWorkerThreadIdle();
// Pass through to normal submission path.
const VkResult submit_result =
SubmitCommandBuffer(m_current_cmd_buffer, present_swap_chain, present_image_index);
if (post_submit_callback)
post_submit_callback();
if (submit_result != VK_SUCCESS)
return false;
if (wait_for_completion)
WaitForCommandBufferCompletion(m_current_cmd_buffer);
if (IsDeviceLost())
return false;
}
// VR perf diagnostics: once per ~5 s of presented frames, dump where the GPU thread's
// CPU time went (this runs on the GPU thread only — the worker thread uses the inner
// SubmitCommandBuffer overload directly).
if (has_present)
{
static u64 s_perf_window_start_us = 0;
static u32 s_perf_window_frames = 0;
s_perf_window_frames++;
const u64 now_us = Common::Timer::NowUs();
if (s_perf_window_start_us == 0)
s_perf_window_start_us = now_us;
const u64 elapsed_us = now_us - s_perf_window_start_us;
if (elapsed_us >= 5'000'000)
{
auto& perf = g_vulkan_context->GetPerfCounters();
const double frames = static_cast<double>(std::max<u32>(s_perf_window_frames, 1));
WARN_LOG_FMT(VIDEO,
"VKPERF: frames={} wall={:.2f}ms/f submit={:.2f}ms/f (n={:.1f}/f) "
"fence_wait={:.2f}ms/f xr_swapchain={:.2f}ms/f uniforms={:.2f}ms/f "
"vtx_commit={:.2f}ms/f draw_bind={:.2f}ms/f draws={:.0f}/f "
"pipelines_created={}",
s_perf_window_frames, elapsed_us / 1000.0 / frames,
perf.submit_us.exchange(0, std::memory_order_relaxed) / 1000.0 / frames,
perf.submit_count.exchange(0, std::memory_order_relaxed) / frames,
perf.fence_wait_us.exchange(0, std::memory_order_relaxed) / 1000.0 / frames,
perf.xr_swapchain_us.exchange(0, std::memory_order_relaxed) / 1000.0 / frames,
perf.uniform_us.exchange(0, std::memory_order_relaxed) / 1000.0 / frames,
perf.vertex_commit_us.exchange(0, std::memory_order_relaxed) / 1000.0 / frames,
perf.draw_us.exchange(0, std::memory_order_relaxed) / 1000.0 / frames,
perf.draw_count.exchange(0, std::memory_order_relaxed) / frames,
perf.pipelines_created.exchange(0, std::memory_order_relaxed));
s_perf_window_frames = 0;
s_perf_window_start_us = now_us;
}
}
if (advance_to_next_frame)
{
m_current_frame = (m_current_frame + 1) % NUM_FRAMES_IN_FLIGHT;
// Wait for all command buffers that used the descriptor pool to finish
u32 cmd_buffer_index = (m_current_cmd_buffer + 1) % NUM_COMMAND_BUFFERS;
while (cmd_buffer_index != m_current_cmd_buffer)
{
CmdBufferResources& cmd_buffer = m_command_buffers[cmd_buffer_index];
if (cmd_buffer.frame_index == m_current_frame && cmd_buffer.fence_counter != 0 &&
cmd_buffer.fence_counter > m_completed_fence_counter)
{
WaitForCommandBufferCompletion(cmd_buffer_index);
}
cmd_buffer_index = (cmd_buffer_index + 1) % NUM_COMMAND_BUFFERS;
}
if (IsDeviceLost())
return false;
// Reset the descriptor pools
FrameResources& frame_resources = GetCurrentFrameResources();
if (frame_resources.descriptor_pools.size() == 1) [[likely]]
{
VkResult res = vkResetDescriptorPool(g_vulkan_context->GetDevice(),
frame_resources.descriptor_pools[0], 0);
if (res != VK_SUCCESS)
LOG_VULKAN_ERROR(res, "vkResetDescriptorPool failed: ");
}
else [[unlikely]]
{
for (VkDescriptorPool descriptor_pool : frame_resources.descriptor_pools)
{
vkDestroyDescriptorPool(g_vulkan_context->GetDevice(), descriptor_pool, nullptr);
}
frame_resources.descriptor_pools.clear();
VkDescriptorPool descriptor_pool = CreateDescriptorPool(m_descriptor_set_count);
if (descriptor_pool != VK_NULL_HANDLE) [[likely]]
frame_resources.descriptor_pools.push_back(descriptor_pool);
}
frame_resources.current_descriptor_pool_index = 0;
}
// Switch to next cmdbuffer.
BeginCommandBuffer();
return !IsDeviceLost();
}
VkResult CommandBufferManager::SubmitCommandBuffer(u32 command_buffer_index,
VkSwapchainKHR present_swap_chain,
u32 present_image_index)
{
if (IsDeviceLost())
return VK_ERROR_DEVICE_LOST;
const u64 perf_start_us = Common::Timer::NowUs();
CmdBufferResources& resources = m_command_buffers[command_buffer_index];
// This may be executed on the worker thread, so don't modify any state of the manager class.
uint32_t wait_bits = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
VkSubmitInfo submit_info = {VK_STRUCTURE_TYPE_SUBMIT_INFO,
nullptr,
0,
nullptr,
&wait_bits,
static_cast<u32>(resources.command_buffers.size()),
resources.command_buffers.data(),
0,
nullptr};
// If the init command buffer did not have any commands recorded, don't submit it.
if (!resources.init_command_buffer_used)
{
submit_info.commandBufferCount = 1;
submit_info.pCommandBuffers = &resources.command_buffers[1];
}
if (resources.semaphore_used)
{
submit_info.pWaitSemaphores = &resources.semaphore;
submit_info.waitSemaphoreCount = 1;
}
if (present_swap_chain != VK_NULL_HANDLE)
{
submit_info.signalSemaphoreCount = 1;
submit_info.pSignalSemaphores = &m_present_semaphores[present_image_index];
}
{
auto queue_lock = AcquireQueueLock();
VkResult res =
vkQueueSubmit(g_vulkan_context->GetGraphicsQueue(), 1, &submit_info, resources.fence);
if (res != VK_SUCCESS)
{
LOG_VULKAN_ERROR(res, "vkQueueSubmit failed: ");
if (res == VK_ERROR_DEVICE_LOST)
{
HandleDeviceLost("vkQueueSubmit");
}
else
{
PanicAlertFmt("Failed to submit command buffer: {} ({})", VkResultToString(res),
static_cast<int>(res));
}
return res;
}
// Do we have a swap chain to present?
if (present_swap_chain != VK_NULL_HANDLE)
{
// Should have a signal semaphore.
VkPresentInfoKHR present_info = {VK_STRUCTURE_TYPE_PRESENT_INFO_KHR,
nullptr,
1,
&m_present_semaphores[present_image_index],
1,
&present_swap_chain,
&present_image_index,
nullptr};
m_last_present_result = vkQueuePresentKHR(g_vulkan_context->GetPresentQueue(), &present_info);
if (m_last_present_result != VK_SUCCESS)
{
// VK_ERROR_OUT_OF_DATE_KHR is not fatal, just means we need to recreate our swap chain.
if (m_last_present_result != VK_ERROR_OUT_OF_DATE_KHR &&
m_last_present_result != VK_SUBOPTIMAL_KHR &&
m_last_present_result != VK_ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT)
{
LOG_VULKAN_ERROR(m_last_present_result, "vkQueuePresentKHR failed: ");
if (m_last_present_result == VK_ERROR_DEVICE_LOST)
HandleDeviceLost("vkQueuePresentKHR");
}
// Don't treat VK_SUBOPTIMAL_KHR as fatal on Android. Android 10+ requires prerotation.
// See https://twitter.com/Themaister/status/1207062674011574273
#ifdef VK_USE_PLATFORM_ANDROID_KHR
if (m_last_present_result != VK_SUBOPTIMAL_KHR)
m_last_present_failed.Set();
#else
m_last_present_failed.Set();
#endif
}
}
}
auto& perf = g_vulkan_context->GetPerfCounters();
perf.submit_us.fetch_add(Common::Timer::NowUs() - perf_start_us, std::memory_order_relaxed);
perf.submit_count.fetch_add(1, std::memory_order_relaxed);
return IsDeviceLost() ? VK_ERROR_DEVICE_LOST : VK_SUCCESS;
}
void CommandBufferManager::BeginCommandBuffer()
{
if (IsDeviceLost())
return;
// Move to the next command buffer.
const u32 next_buffer_index = (m_current_cmd_buffer + 1) % NUM_COMMAND_BUFFERS;
CmdBufferResources& resources = m_command_buffers[next_buffer_index];
// Wait for the GPU to finish with all resources for this command buffer.
if (resources.fence_counter > m_completed_fence_counter)
WaitForCommandBufferCompletion(next_buffer_index);
if (IsDeviceLost())
return;
// Reset fence to unsignaled before starting.
VkResult res = vkResetFences(g_vulkan_context->GetDevice(), 1, &resources.fence);
if (res != VK_SUCCESS)
LOG_VULKAN_ERROR(res, "vkResetFences failed: ");
// Reset command pools to beginning since we can re-use the memory now
res = vkResetCommandPool(g_vulkan_context->GetDevice(), resources.command_pool, 0);
if (res != VK_SUCCESS)
LOG_VULKAN_ERROR(res, "vkResetCommandPool failed: ");
// Enable commands to be recorded to the two buffers again.
VkCommandBufferBeginInfo begin_info = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, nullptr,
VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, nullptr};
for (VkCommandBuffer command_buffer : resources.command_buffers)
{
res = vkBeginCommandBuffer(command_buffer, &begin_info);
if (res != VK_SUCCESS)
LOG_VULKAN_ERROR(res, "vkBeginCommandBuffer failed: ");
}
// Reset upload command buffer state
resources.init_command_buffer_used = false;
resources.semaphore_used = false;
resources.fence_counter = m_next_fence_counter++;
resources.frame_index = m_current_frame;
m_current_cmd_buffer = next_buffer_index;
}
void CommandBufferManager::HandleDeviceLost(std::string_view operation)
{
if (m_device_lost.exchange(true, std::memory_order_acq_rel))
return;
ERROR_LOG_FMT(VIDEO,
"Vulkan device lost in {}. Stopping emulation and suppressing further GPU "
"submissions to protect the graphics driver.",
operation);
ERROR_LOG_FMT(VIDEO,
"Vulkan device loss state: current_cmd={} current_frame={} next_fence={} "
"completed_fence={} last_present={}.",
m_current_cmd_buffer, m_current_frame, m_next_fence_counter,
m_completed_fence_counter, static_cast<int>(m_last_present_result));
if (g_vulkan_context)
g_vulkan_context->LogDeviceFaultInfo();
Core::QueueHostJob([](Core::System& system) { Core::Stop(system); }, true);
}
void CommandBufferManager::DeferBufferViewDestruction(VkBufferView object)
{
CmdBufferResources& cmd_buffer_resources = GetCurrentCmdBufferResources();
cmd_buffer_resources.cleanup_resources.push_back(
[object] { vkDestroyBufferView(g_vulkan_context->GetDevice(), object, nullptr); });
}
void CommandBufferManager::DeferBufferDestruction(VkBuffer buffer, VmaAllocation alloc)
{
CmdBufferResources& cmd_buffer_resources = GetCurrentCmdBufferResources();
cmd_buffer_resources.cleanup_resources.push_back(
[buffer, alloc] { vmaDestroyBuffer(g_vulkan_context->GetMemoryAllocator(), buffer, alloc); });
}
void CommandBufferManager::DeferFramebufferDestruction(VkFramebuffer object)
{
CmdBufferResources& cmd_buffer_resources = GetCurrentCmdBufferResources();
cmd_buffer_resources.cleanup_resources.push_back(
[object] { vkDestroyFramebuffer(g_vulkan_context->GetDevice(), object, nullptr); });
}
void CommandBufferManager::DeferImageDestruction(VkImage image, VmaAllocation alloc)
{
CmdBufferResources& cmd_buffer_resources = GetCurrentCmdBufferResources();
cmd_buffer_resources.cleanup_resources.push_back(
[image, alloc] { vmaDestroyImage(g_vulkan_context->GetMemoryAllocator(), image, alloc); });
}
void CommandBufferManager::DeferPipelineDestruction(VkPipeline object)
{
CmdBufferResources& cmd_buffer_resources = GetCurrentCmdBufferResources();
cmd_buffer_resources.cleanup_resources.push_back(
[object] { vkDestroyPipeline(g_vulkan_context->GetDevice(), object, nullptr); });
}
void CommandBufferManager::DeferImageViewDestruction(VkImageView object)
{
CmdBufferResources& cmd_buffer_resources = GetCurrentCmdBufferResources();
cmd_buffer_resources.cleanup_resources.push_back(
[object] { vkDestroyImageView(g_vulkan_context->GetDevice(), object, nullptr); });
}
std::unique_ptr<CommandBufferManager> g_command_buffer_mgr;
} // namespace Vulkan