Block on the GPU fence instead of spinning on it

An unbounded wait polled vkGetFenceStatus in a tight loop with nothing but a
pause hint between calls. command_buffer::flush() takes that path for the submit
fence, so it is what a frame does while it waits on the GPU: a core pinned at
100% for the whole wait, hammering a driver entry point while the driver is
trying to do the work being waited on.

Cheap on a desktop with cores to spare. Not here, where it competes with the SPU
and PPU threads for a handful of cores. Arkham City measured 24ms of a 53ms
frame in this function with the GPU only 71-77% busy, which is what a stall
looks like when the waiter is too busy spinning to prepare the next submission.

Polls briefly first, since most waits are for a fence about to signal and
blocking would cost a syscall and a wake-up for nothing, then hands the wait to
vkWaitForFences so the driver can sleep the thread. The blocking call was
already there, three lines up, used only when a finite timeout was supplied.
Same shape as wait_for_event below, which already had this treatment.
This commit is contained in:
jpolo1224
2026-08-10 00:04:14 -04:00
parent b55cd3dd44
commit cdcf384df2
+30 -7
View File
@@ -575,20 +575,43 @@ namespace vk
}
else
{
while (auto status = vkGetFenceStatus(*g_render_device, pFence->handle))
// An unbounded wait used to poll vkGetFenceStatus in a tight loop with nothing
// but a pause hint between calls. That is a core pinned at 100% for the whole
// duration of a GPU wait, plus a driver entry point hammered while the driver is
// trying to do the very work being waited on.
//
// It costs little on a desktop with cores to spare. It is expensive here: this
// is reached from command_buffer::flush() for the submit fence, so it is the
// path a frame takes when it waits on the GPU, and it competes with the SPU and
// PPU threads for a handful of cores. Arkham City measured 24ms of a 53ms frame
// in this function with the GPU only 71-77% busy, which is what a stall looks
// like when the waiter is too busy spinning to prepare the next submission.
//
// Poll briefly first, since most waits here are for a fence that is about to
// signal and blocking would cost a syscall and a wake-up for no reason, then
// hand the wait to the driver, which can sleep the thread and free the core.
// Same shape as wait_for_event below, which already got this treatment.
constexpr u64 hot_polls = 512;
for (u64 poll = 0; poll < hot_polls; poll++)
{
switch (status)
const VkResult status = vkGetFenceStatus(*g_render_device, pFence->handle);
if (status == VK_SUCCESS)
{
return VK_SUCCESS;
}
if (status != VK_NOT_READY)
{
case VK_NOT_READY:
utils::pause();
continue;
default:
die_with_error(status);
return status;
}
utils::pause();
}
return VK_SUCCESS;
return vkWaitForFences(*g_render_device, 1, &pFence->handle, VK_FALSE, UINT64_MAX);
}
}