mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-08 17:28:49 -10:00
gfx/vulkan: replace per-submit vkQueueWaitIdle with fences (v2)
Two of the vkQueueWaitIdle call sites in the vulkan driver follow a
"submit one-shot transfer, drain the queue, free the staging cmd"
pattern that has three avoidable costs in the threaded build:
1. The wait is scoped to the entire queue, so the calling thread
blocks on every unrelated submission that was already in flight
(frame draws, menu, HW core work, present), not just its own
transfer.
2. queue_lock is held across the wait, so any other thread that
needs the queue (notably vkQueuePresentKHR, which is also a
queue operation requiring external synchronisation) is blocked
for the full GPU-drain interval rather than the few microseconds
of the submit itself. This is the same hazard the comment in
gfx/common/vulkan_common.c around the swapchain_acquire_semaphore
replacement already calls out (Windows TDR 0x887A0006).
3. For the per-frame font atlas upload, the queue drain serialises
this transfer against every other queue operation in flight,
destroying CPU-GPU parallelism for any frame where the OSD,
menu, or FPS counter dirties glyphs.
Convert both sites to:
create or reset fence
lock queue_lock
vkQueueSubmit(.., fence)
unlock queue_lock
vkWaitForFences(this fence)
This preserves the existing correctness guarantee (the transfer is
complete before the caller touches the destination) while:
- waiting only on the specific submission instead of the whole queue
- holding queue_lock for the submit call only, not the wait
- not changing the externally-synchronised contract on VkQueue
Sites converted:
vulkan_create_texture (VULKAN_TEXTURE_STATIC path)
Hit on shader pass / overlay / set_texture_image loads. The
original /* Very crude ... only happen during init */ comment is
out of date in practice: this fires on content load, savestate
restore and shader reload, all of which are user-visible. Uses
a per-call fence since this is not on a per-frame path and the
create/destroy cost is negligible against the upload itself.
vulkan_raster_font_flush (atlas dirty-region upload)
Per-frame hot path whenever menu / OSD / FPS text changes.
Highest-impact site in the file. Uses a recycled fence stored
on the vulkan_raster_t struct (lazy-created on first use, reset
between uses, destroyed in vulkan_font_free) to avoid per-frame
vkCreateFence / vkDestroyFence churn. zero-initialised via the
existing calloc() in vulkan_font_init, so the VK_NULL_HANDLE
branch in the lazy-init triggers correctly on the first call.
Both sites also include a fallback path: if vkCreateFence ever fails
(which in practice only happens under genuine host/device memory
exhaustion), the code falls back to vkQueueWaitIdle so synchronisation
remains correct.
The remaining vkQueueWaitIdle calls are in teardown / swapchain
rebuild / synchronous-readback paths where either a fence does not
cleanly apply (no single submission to wait on) or the cost is not on
a performance-sensitive path. Those are intentionally left for
separate changes.
This commit is contained in:
1 parent
7e4bd504aa
commit
e050ecdb7d
1 file changed
+82
-30
+82
-30
@@ -406,6 +406,12 @@ typedef struct
|
||||
unsigned dirty_x_min, dirty_y_min;
|
||||
unsigned dirty_x_max, dirty_y_max;
|
||||
|
||||
/* Recycled fence used by vulkan_raster_font_flush to scope
|
||||
* the wait for the atlas upload submission. Lazy-created on
|
||||
* first use; reset on subsequent uses to avoid per-frame
|
||||
* vkCreateFence/vkDestroyFence churn. */
|
||||
VkFence upload_fence;
|
||||
|
||||
bool needs_update;
|
||||
} vulkan_raster_t;
|
||||
|
||||
@@ -1536,19 +1542,38 @@ static struct vk_texture vulkan_create_texture(vk_t *vk,
|
||||
submit_info.signalSemaphoreCount = 0;
|
||||
submit_info.pSignalSemaphores = NULL;
|
||||
|
||||
#ifdef HAVE_THREADS
|
||||
slock_lock(vk->context->queue_lock);
|
||||
#endif
|
||||
vkQueueSubmit(vk->context->queue,
|
||||
1, &submit_info, VK_NULL_HANDLE);
|
||||
{
|
||||
/* Wait only on this submission, not the entire
|
||||
* queue, and release queue_lock immediately after
|
||||
* submit so other threads can proceed in parallel
|
||||
* with the transfer. If fence creation fails, fall
|
||||
* back to a queue drain for correct synchronisation. */
|
||||
VkFence fence = VK_NULL_HANDLE;
|
||||
VkFenceCreateInfo fence_info;
|
||||
fence_info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
|
||||
fence_info.pNext = NULL;
|
||||
fence_info.flags = 0;
|
||||
vkCreateFence(vk->context->device,
|
||||
&fence_info, NULL, &fence);
|
||||
|
||||
/* TODO: Very crude, but texture uploads only happen
|
||||
* during init, so waiting for GPU to complete transfer
|
||||
* and blocking isn't a big deal. */
|
||||
vkQueueWaitIdle(vk->context->queue);
|
||||
#ifdef HAVE_THREADS
|
||||
slock_unlock(vk->context->queue_lock);
|
||||
slock_lock(vk->context->queue_lock);
|
||||
#endif
|
||||
vkQueueSubmit(vk->context->queue,
|
||||
1, &submit_info, fence);
|
||||
if (fence == VK_NULL_HANDLE)
|
||||
vkQueueWaitIdle(vk->context->queue);
|
||||
#ifdef HAVE_THREADS
|
||||
slock_unlock(vk->context->queue_lock);
|
||||
#endif
|
||||
|
||||
if (fence != VK_NULL_HANDLE)
|
||||
{
|
||||
vkWaitForFences(vk->context->device,
|
||||
1, &fence, VK_TRUE, UINT64_MAX);
|
||||
vkDestroyFence(vk->context->device, fence, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
vkFreeCommandBuffers(vk->context->device,
|
||||
vk->staging_pool, 1, &staging);
|
||||
@@ -2135,6 +2160,8 @@ static void vulkan_font_free(void *data, bool is_threaded)
|
||||
font->font_driver->free(font->font_data);
|
||||
|
||||
vkQueueWaitIdle(font->vk->context->queue);
|
||||
if (font->upload_fence != VK_NULL_HANDLE)
|
||||
vkDestroyFence(font->vk->context->device, font->upload_fence, NULL);
|
||||
vulkan_destroy_texture(
|
||||
font->vk->context->device, &font->texture);
|
||||
vulkan_destroy_texture(
|
||||
@@ -2514,9 +2541,11 @@ static void vulkan_font_render_msg(
|
||||
*/
|
||||
|
||||
/* Upload dirty atlas region to the GPU before the draw.
|
||||
* Use a dedicated staging command buffer with vkQueueWaitIdle
|
||||
* to guarantee the transfer is complete before the fragment
|
||||
* shader samples the atlas in the main command buffer. */
|
||||
* Use a dedicated staging command buffer with a recycled
|
||||
* per-submit fence to guarantee the transfer is complete
|
||||
* before the fragment shader samples the atlas in the main
|
||||
* command buffer, without serialising the entire queue or
|
||||
* holding queue_lock across the wait. */
|
||||
if (font->needs_update)
|
||||
{
|
||||
struct vk_texture *dynamic_tex = &font->texture_optimal;
|
||||
@@ -2643,27 +2672,50 @@ static void vulkan_font_render_msg(
|
||||
|
||||
vkEndCommandBuffer(staging_cmd);
|
||||
|
||||
#ifdef HAVE_THREADS
|
||||
slock_lock(vk->context->queue_lock);
|
||||
#endif
|
||||
{
|
||||
/* Lazy-create the recycled fence on first use;
|
||||
* subsequent uses reset it. If creation has failed
|
||||
* previously (or fails now), fall back to a queue
|
||||
* drain so we still have correct synchronisation. */
|
||||
VkFence fence = font->upload_fence;
|
||||
if (fence == VK_NULL_HANDLE)
|
||||
{
|
||||
VkFenceCreateInfo fence_info;
|
||||
fence_info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
|
||||
fence_info.pNext = NULL;
|
||||
fence_info.flags = 0;
|
||||
vkCreateFence(vk->context->device,
|
||||
&fence_info, NULL, &font->upload_fence);
|
||||
fence = font->upload_fence;
|
||||
}
|
||||
else
|
||||
vkResetFences(vk->context->device, 1, &fence);
|
||||
|
||||
submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
|
||||
submit_info.pNext = NULL;
|
||||
submit_info.waitSemaphoreCount = 0;
|
||||
submit_info.pWaitSemaphores = NULL;
|
||||
submit_info.pWaitDstStageMask = NULL;
|
||||
submit_info.commandBufferCount = 1;
|
||||
submit_info.pCommandBuffers = &staging_cmd;
|
||||
submit_info.signalSemaphoreCount = 0;
|
||||
submit_info.pSignalSemaphores = NULL;
|
||||
vkQueueSubmit(vk->context->queue,
|
||||
1, &submit_info, VK_NULL_HANDLE);
|
||||
|
||||
vkQueueWaitIdle(vk->context->queue);
|
||||
submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
|
||||
submit_info.pNext = NULL;
|
||||
submit_info.waitSemaphoreCount = 0;
|
||||
submit_info.pWaitSemaphores = NULL;
|
||||
submit_info.pWaitDstStageMask = NULL;
|
||||
submit_info.commandBufferCount = 1;
|
||||
submit_info.pCommandBuffers = &staging_cmd;
|
||||
submit_info.signalSemaphoreCount = 0;
|
||||
submit_info.pSignalSemaphores = NULL;
|
||||
|
||||
#ifdef HAVE_THREADS
|
||||
slock_unlock(vk->context->queue_lock);
|
||||
slock_lock(vk->context->queue_lock);
|
||||
#endif
|
||||
vkQueueSubmit(vk->context->queue,
|
||||
1, &submit_info, fence);
|
||||
if (fence == VK_NULL_HANDLE)
|
||||
vkQueueWaitIdle(vk->context->queue);
|
||||
#ifdef HAVE_THREADS
|
||||
slock_unlock(vk->context->queue_lock);
|
||||
#endif
|
||||
|
||||
if (fence != VK_NULL_HANDLE)
|
||||
vkWaitForFences(vk->context->device,
|
||||
1, &fence, VK_TRUE, UINT64_MAX);
|
||||
}
|
||||
|
||||
vkFreeCommandBuffers(vk->context->device,
|
||||
vk->staging_pool, 1, &staging_cmd);
|
||||
|
||||
Reference in new issue
Block a user