[Fix] (MG_Backend/DirectVulkan): fence-backed GL sync objects and per-frame descriptor rewind

- Track every graphics-queue submission with a real fence: pooled fences
  for mid-frame flushes, the frame slot's fence for Present and readback.
  Completion advances a submit counter via vkGetFenceStatus polls,
  slot-fence waits, and device-idle points, and raises the buffer-manager
  serial floor from the frame serial each submission carried.
- GL sync objects now capture the submission index that will carry the
  commands recorded so far; ClientWaitSync honors
  GL_SYNC_FLUSH_COMMANDS_BIT with a mid-frame submit (gated on the index
  still being unsubmitted so poll loops cannot split the render pass), and
  blocking waits flush then vkWaitForFences with the caller timeout.
- FlushPendingCommands retires the submitted command buffer and restarts
  recording on a fresh one; retired buffers are freed once the slot fence
  is next waited, so an executing buffer is never reset.
- Rewind descriptor-set cursors exactly once per frame in Present (after
  the slot-fence wait), plus after the synchronous readback drain,
  replacing the ten lazy per-draw-path rewinds.

Verified: host tests 168/168, trace-replay 70/70.
This commit is contained in:
2026-07-10 10:55:10 +00:00
parent 7b00255b11
commit eb090c6170
7 changed files with 436 additions and 58 deletions
@@ -1366,16 +1366,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
namespace {
// Backend fence handle: the VkBufferManager frame serial captured at
// fence creation. The fence is signaled once every command recorded
// under that serial has completed on the GPU (the same busy-tracking
// horizon used to recycle buffer resources).
// Backend fence handle: the queue-submission index captured at fence
// creation (see VulkanRenderer::GetSyncPointSubmitIndex). The fence is
// signaled once that submission's VkFence has been observed signaled,
// so completion tracks the GPU itself rather than the frame-count
// inference; MC 1.21.5's fence-paced ring buffers depend on this to
// recycle their space instead of growing without bound.
struct VulkanSyncObject {
Uint64 frameSerial = 0;
// Renderer generation the serial was issued under (see
Uint64 submitIndex = 0;
// Renderer generation the index was issued under (see
// g_rendererGeneration). A stale generation reports the fence
// signaled: renderer destruction waits for device idle, so the
// old renderer's GPU work is long complete, and the serial must
// old renderer's GPU work is long complete, and the index must
// not be compared against the new renderer's restarted counter.
Uint64 rendererGeneration = 0;
};
@@ -1385,26 +1387,36 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (!pVulkanRenderer) {
return nullptr;
}
return new VulkanSyncObject{pVulkanRenderer->GetCurrentFrameSerial(), GetRendererGeneration()};
return new VulkanSyncObject{pVulkanRenderer->GetSyncPointSubmitIndex(), GetRendererGeneration()};
}
GLenum ClientWaitSync(BackendSyncHandle handle, GLbitfield flags, GLuint64 timeout) {
// Commands are only submitted at Present, so GL_SYNC_FLUSH_COMMANDS_BIT
// cannot force progress mid-frame; WaitForFrameSerial reports whether
// waiting can succeed at all.
(void)flags;
const auto* sync = static_cast<VulkanSyncObject*>(handle);
if (sync == nullptr || !pVulkanRenderer || sync->rendererGeneration != GetRendererGeneration()) {
return GL_ALREADY_SIGNALED;
}
if (pVulkanRenderer->IsFrameSerialComplete(sync->frameSerial)) {
if (pVulkanRenderer->IsSubmitIndexComplete(sync->submitIndex)) {
return GL_ALREADY_SIGNALED;
}
if (timeout == 0) {
return GL_TIMEOUT_EXPIRED;
// GL_SYNC_FLUSH_COMMANDS_BIT: flush regardless of timeout, so a
// zero-timeout poll loop makes progress across calls - but only when
// the sync's batch is still unsubmitted; flushing for an already
// submitted fence cannot advance it and would split the frame's
// render pass on every poll.
if ((flags & GL_SYNC_FLUSH_COMMANDS_BIT) != 0) {
pVulkanRenderer->FlushForSyncPoint(sync->submitIndex);
}
return pVulkanRenderer->WaitForFrameSerial(sync->frameSerial, timeout) ? GL_CONDITION_SATISFIED
: GL_TIMEOUT_EXPIRED;
if (timeout == 0) {
return pVulkanRenderer->IsSubmitIndexComplete(sync->submitIndex) ? GL_ALREADY_SIGNALED
: GL_TIMEOUT_EXPIRED;
}
// Blocking wait: flush even without the flush bit - the sync's batch
// can only be submitted from this thread, so waiting on an unflushed
// fence would otherwise burn the full timeout with no chance of
// success.
return pVulkanRenderer->WaitForSubmitIndex(sync->submitIndex, timeout, /*flushIfPending=*/true)
? GL_CONDITION_SATISFIED
: GL_TIMEOUT_EXPIRED;
}
void WaitSync(BackendSyncHandle handle, GLbitfield flags, GLuint64 timeout) {
@@ -1425,7 +1437,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (sync == nullptr || !pVulkanRenderer || sync->rendererGeneration != GetRendererGeneration()) {
return true;
}
return pVulkanRenderer->IsFrameSerialComplete(sync->frameSerial);
// Pure status read (glGetSynciv must not flush).
return pVulkanRenderer->IsSubmitIndexComplete(sync->submitIndex);
}
namespace {
@@ -13,6 +13,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Destroy(device, commandPool);
m_frames.assign(frameCount, {});
currentFrameIndex = 0;
m_device = device;
m_commandPool = commandPool;
Vector<VkCommandBuffer> commandBuffers(frameCount, VK_NULL_HANDLE);
VkCommandBufferAllocateInfo allocInfo{};
@@ -55,10 +57,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
DestroySwapchainSemaphores(device);
if (device != VK_NULL_HANDLE && commandPool != VK_NULL_HANDLE && !m_frames.empty()) {
for (auto& frame : m_frames) {
FreeRetiredCommandBuffers(frame);
}
vkFreeCommandBuffers(device, commandPool, frameCount, commandBuffers.data());
}
m_frames.clear();
currentFrameIndex = 0;
m_device = VK_NULL_HANDLE;
m_commandPool = VK_NULL_HANDLE;
}
FrameContext::FrameData& FrameContext::GetCurrent() {
@@ -214,6 +221,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (result != VK_SUCCESS) {
return result;
}
// The slot's fence has been waited: every command buffer this slot
// submitted (including mid-frame flushes) has finished executing.
FreeRetiredCommandBuffers(frame);
result = vkAcquireNextImageKHR(device, swapchain, timeout, frame.imageAvailableSemaphore, acquireFence,
&outImageIndex);
@@ -237,6 +247,39 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_recordingObserver = observer;
}
VkResult FrameContext::RetireCurrentCommandBuffer() {
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_commandPool != VK_NULL_HANDLE,
"RetireCurrentCommandBuffer requires an initialized FrameContext");
auto& frame = GetCurrent();
MOBILEGL_ASSERT(!frame.isCommandRecording,
"RetireCurrentCommandBuffer called while the command buffer is still recording");
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = m_commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = 1;
VkCommandBuffer replacement = VK_NULL_HANDLE;
const VkResult result = vkAllocateCommandBuffers(m_device, &allocInfo, &replacement);
if (result != VK_SUCCESS) {
return result;
}
frame.retiredCommandBuffers.push_back(frame.commandBuffer);
frame.commandBuffer = replacement;
return VK_SUCCESS;
}
void FrameContext::FreeRetiredCommandBuffers(FrameData& frame) {
if (frame.retiredCommandBuffers.empty()) {
return;
}
if (m_device != VK_NULL_HANDLE && m_commandPool != VK_NULL_HANDLE) {
vkFreeCommandBuffers(m_device, m_commandPool, static_cast<Uint32>(frame.retiredCommandBuffers.size()),
frame.retiredCommandBuffers.data());
}
frame.retiredCommandBuffers.clear();
}
void FrameContext::AssertValidFrameIndex(Uint32 frameIndex) const {
MOBILEGL_ASSERT(frameIndex < m_frames.size(), "FrameContext index out of range");
}
@@ -47,6 +47,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool isCommandRecording = false;
Bool hasCommandBufferRecorded = false;
Bool imageAvailableSemaphoreConsumed = false;
// Command buffers submitted mid-frame (FlushPendingCommands) whose
// execution is only known complete once this slot's fence has been
// waited again; freed at that point.
Vector<VkCommandBuffer> retiredCommandBuffers;
// Submit-tracker index of this slot's most recent queue submission
// (written by the renderer at submit time).
Uint64 lastSubmitIndex = 0;
};
VkResult Initialize(VkDevice device, VkCommandPool commandPool, Uint32 frameCount);
@@ -69,6 +76,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkResult WaitAndAcquireNextImage(VkDevice device, VkSwapchainKHR swapchain, Uint32& outImageIndex,
Uint64 timeout = UINT64_MAX, VkFence acquireFence = VK_NULL_HANDLE);
// Parks the current (already ended and submitted) command buffer on the
// slot's retired list and installs a freshly allocated one, so recording
// can restart while the submitted buffer is still executing. Retired
// buffers are freed after the slot's fence is next waited.
VkResult RetireCurrentCommandBuffer();
Uint32 GetCurrentFrameIndex() const;
Uint32 GetFrameCount() const;
@@ -83,10 +96,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkSemaphoreCreateInfo& semaphoreInfo,
const VkFenceCreateInfo& fenceInfo);
void DestroySyncObjectsForFrame(VkDevice device, Uint32 frameIndex);
void FreeRetiredCommandBuffers(FrameData& frame);
Vector<FrameData> m_frames;
Vector<VkSemaphore> m_swapchainImageRenderFinishedSemaphores;
Uint32 currentFrameIndex = 0;
IRecordingObserver* m_recordingObserver = nullptr;
// Stored at Initialize for retired-command-buffer management.
VkDevice m_device = VK_NULL_HANDLE;
VkCommandPool m_commandPool = VK_NULL_HANDLE;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -129,6 +129,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
void VkBufferManager::NotifyFrameSerialComplete(Uint64 serial) {
// The current serial's work is still being recorded; a completion
// report for it (or beyond) can only come from a stale caller.
if (serial >= m_frameSerial) {
return;
}
m_completedSerialFloor = std::max(m_completedSerialFloor, serial);
}
void VkBufferManager::SetCopyCommandProvider(IBufferCopyCommandProvider* provider) {
m_copyProvider = provider;
}
@@ -75,6 +75,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void BeginFrame(Uint32 frameIndex);
// All previously submitted GPU work has completed (vkDeviceWaitIdle).
void NotifyDeviceIdle();
// A frame slot's submission fence has been waited: every serial up to
// and including `serial` is complete. Raises the completed floor so
// GetCompletedSerial reflects real fence progress instead of only the
// frameSerial-minus-frameCount inference.
void NotifyFrameSerialComplete(Uint64 serial);
void SetCopyCommandProvider(IBufferCopyCommandProvider* provider);
Bool UploadTransient(BufferKind kind, Uint32 frameIndex, const void* data, VkDeviceSize size,
@@ -1983,6 +1983,8 @@ void main() {
if (m_device != VK_NULL_HANDLE) {
VK_VERIFY(vkDeviceWaitIdle(m_device));
}
OnSubmitsCompletedUpTo(m_submitCounter);
DestroySubmitFencePool();
DestroyDeferredDepthMipmapCleanup();
DestroyComputePipelines();
@@ -3203,7 +3205,6 @@ void main() {
// Begin command recording if not yet
if (!frame.isCommandRecording) {
m_frameContext.BeginCommandRecording();
m_uniformManager->BeginFrame(m_frameContext.GetCurrentFrameIndex());
}
auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass();
@@ -3361,7 +3362,6 @@ void main() {
if (!frame.isCommandRecording) {
m_frameContext.BeginCommandRecording();
m_uniformManager->BeginFrame(m_frameContext.GetCurrentFrameIndex());
}
if (VkRenderPassManager::GetActiveRenderPass() != nullptr) {
@@ -3397,7 +3397,6 @@ void main() {
if (!frame.isCommandRecording) {
m_frameContext.BeginCommandRecording();
m_uniformManager->BeginFrame(m_frameContext.GetCurrentFrameIndex());
}
if (VkRenderPassManager::GetActiveRenderPass() != nullptr) {
@@ -3463,7 +3462,6 @@ void main() {
auto& frame = m_frameContext.GetCurrent();
if (!frame.isCommandRecording) {
m_frameContext.BeginCommandRecording();
m_uniformManager->BeginFrame(m_frameContext.GetCurrentFrameIndex());
}
if (VkRenderPassManager::GetActiveRenderPass() != nullptr) {
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
@@ -3939,7 +3937,6 @@ void main() {
auto& frame = m_frameContext.GetCurrent();
if (!frame.isCommandRecording) {
m_frameContext.BeginCommandRecording();
m_uniformManager->BeginFrame(m_frameContext.GetCurrentFrameIndex());
}
auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass();
@@ -4290,7 +4287,6 @@ void main() {
auto& frame = m_frameContext.GetCurrent();
if (!frame.isCommandRecording) {
m_frameContext.BeginCommandRecording();
m_uniformManager->BeginFrame(m_frameContext.GetCurrentFrameIndex());
}
if (VkRenderPassManager::GetActiveRenderPass() != nullptr) {
@@ -4457,7 +4453,6 @@ void main() {
auto& frame = m_frameContext.GetCurrent();
if (!frame.isCommandRecording) {
m_frameContext.BeginCommandRecording();
m_uniformManager->BeginFrame(m_frameContext.GetCurrentFrameIndex());
}
if (VkRenderPassManager::GetActiveRenderPass() != nullptr) {
@@ -4590,29 +4585,16 @@ void main() {
return true;
}
VkPipelineStageFlags waitDstStageMask = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT;
VkSubmitInfo submitInfo{VK_STRUCTURE_TYPE_SUBMIT_INFO};
VkSemaphore waitSemaphore = frame.imageAvailableSemaphore;
if (!frame.imageAvailableSemaphoreConsumed) {
submitInfo.waitSemaphoreCount = 1;
submitInfo.pWaitSemaphores = &waitSemaphore;
submitInfo.pWaitDstStageMask = &waitDstStageMask;
}
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &frame.commandBuffer;
VkResult result = vkQueueSubmit(m_graphicsQueue, 1, &submitInfo, frame.imageInFlightFence);
if (result != VK_SUCCESS) {
MGLOG_E("DirectVulkan readback: vkQueueSubmit returned %d", result);
if (!SubmitPendingCommandBuffer(frame, frame.imageInFlightFence, /*pooledFence=*/false)) {
return false;
}
frame.imageAvailableSemaphoreConsumed = true;
result = vkWaitForFences(m_device, 1, &frame.imageInFlightFence, VK_TRUE, UINT64_MAX);
VkResult result = vkWaitForFences(m_device, 1, &frame.imageInFlightFence, VK_TRUE, UINT64_MAX);
if (result != VK_SUCCESS) {
MGLOG_E("DirectVulkan readback: vkWaitForFences returned %d", result);
return false;
}
OnSubmitsCompletedUpTo(frame.lastSubmitIndex);
result = vkResetFences(m_device, 1, &frame.imageInFlightFence);
if (result != VK_SUCCESS) {
MGLOG_E("DirectVulkan readback: vkResetFences returned %d", result);
@@ -4621,6 +4603,12 @@ void main() {
frame.hasCommandBufferRecorded = false;
frame.isCommandRecording = false;
// The wait proved every descriptor set this slot has in flight idle;
// rewind the reuse cursors so present-less readback loops stay
// bounded (Present is the only other rewind point).
if (m_uniformManager) {
m_uniformManager->BeginFrame(m_frameContext.GetCurrentFrameIndex());
}
return true;
}
@@ -4639,7 +4627,6 @@ void main() {
auto& frame = m_frameContext.GetCurrent();
if (!frame.isCommandRecording) {
m_frameContext.BeginCommandRecording();
m_uniformManager->BeginFrame(m_frameContext.GetCurrentFrameIndex());
}
if (VkRenderPassManager::GetActiveRenderPass() != nullptr) {
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
@@ -4807,7 +4794,6 @@ void main() {
auto& frame = m_frameContext.GetCurrent();
if (!frame.isCommandRecording) {
m_frameContext.BeginCommandRecording();
m_uniformManager->BeginFrame(m_frameContext.GetCurrentFrameIndex());
}
if (VkRenderPassManager::GetActiveRenderPass() != nullptr) {
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
@@ -4911,7 +4897,6 @@ void main() {
auto& frame = m_frameContext.GetCurrent();
if (!frame.isCommandRecording) {
m_frameContext.BeginCommandRecording();
m_uniformManager->BeginFrame(m_frameContext.GetCurrentFrameIndex());
}
if (VkRenderPassManager::GetActiveRenderPass() != nullptr) {
@@ -5442,10 +5427,6 @@ void main() {
return frame.commandBuffer;
}
Uint64 VulkanRenderer::GetCurrentFrameSerial() const {
return m_bufferManager.GetFrameSerial();
}
Bool VulkanRenderer::IsFrameSerialComplete(Uint64 serial) const {
return serial <= m_bufferManager.GetCompletedSerial();
}
@@ -5474,6 +5455,222 @@ void main() {
return false;
}
m_bufferManager.NotifyDeviceIdle();
OnSubmitsCompletedUpTo(m_submitCounter);
return true;
}
Uint64 VulkanRenderer::GetSyncPointSubmitIndex() const {
// Commands recorded (or still recording) since the last submission are
// carried by the NEXT submission; a fence created now must wait for it.
return m_submitCounter + (HasPendingRecordedWork() ? 1 : 0);
}
Bool VulkanRenderer::HasPendingRecordedWork() const {
if (m_frameContext.GetFrameCount() == 0) {
return false;
}
const auto& frame = m_frameContext.GetCurrent();
return frame.isCommandRecording || frame.hasCommandBufferRecorded;
}
Bool VulkanRenderer::IsSubmitIndexComplete(Uint64 submitIndex) {
if (submitIndex <= m_completedSubmitCounter) {
return true;
}
if (submitIndex > m_submitCounter) {
return false; // not even submitted; no point polling fences
}
RefreshCompletedSubmits();
return submitIndex <= m_completedSubmitCounter;
}
void VulkanRenderer::RegisterSubmit(VkFence fence, Bool pooledFence) {
++m_submitCounter;
m_inFlightSubmits.push_back({m_submitCounter, m_bufferManager.GetFrameSerial(), fence, pooledFence});
}
void VulkanRenderer::RefreshCompletedSubmits() {
if (m_device == VK_NULL_HANDLE) {
return;
}
// Prefix-only scan: submissions to a single queue complete in order,
// and stopping at the first unsignaled fence stays conservative even
// if they did not.
while (!m_inFlightSubmits.empty()) {
// Copy before OnSubmitsCompletedUpTo erases the front record.
const Uint64 frontIndex = m_inFlightSubmits.front().submitIndex;
if (vkGetFenceStatus(m_device, m_inFlightSubmits.front().fence) != VK_SUCCESS) {
break;
}
OnSubmitsCompletedUpTo(frontIndex);
}
}
void VulkanRenderer::OnSubmitsCompletedUpTo(Uint64 submitIndex) {
m_completedSubmitCounter = std::max(m_completedSubmitCounter, submitIndex);
while (!m_inFlightSubmits.empty() && m_inFlightSubmits.front().submitIndex <= submitIndex) {
SubmitRecord record = m_inFlightSubmits.front();
m_inFlightSubmits.erase(m_inFlightSubmits.begin());
// Frame-serial completion piggybacks on submission completion.
// NotifyFrameSerialComplete refuses the current (still-recording)
// serial, so mid-frame flush records do not mark it early.
m_bufferManager.NotifyFrameSerialComplete(record.frameSerial);
if (!record.pooledFence || m_device == VK_NULL_HANDLE) {
continue; // frame-slot fences are reset/destroyed by FrameContext
}
if (vkResetFences(m_device, 1, &record.fence) == VK_SUCCESS) {
m_freeSubmitFences.push_back(record.fence);
} else {
vkDestroyFence(m_device, record.fence, nullptr);
}
}
}
VkFence VulkanRenderer::AcquirePooledSubmitFence() {
if (!m_freeSubmitFences.empty()) {
VkFence fence = m_freeSubmitFences.back();
m_freeSubmitFences.pop_back();
return fence;
}
VkFenceCreateInfo fenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
VkFence fence = VK_NULL_HANDLE;
const VkResult result = vkCreateFence(m_device, &fenceInfo, nullptr, &fence);
if (result != VK_SUCCESS) {
MGLOG_E("AcquirePooledSubmitFence: vkCreateFence returned %d", result);
return VK_NULL_HANDLE;
}
return fence;
}
void VulkanRenderer::DestroySubmitFencePool() {
// Callers guarantee device idle, so in-flight fences are inert.
for (const auto& record : m_inFlightSubmits) {
if (record.pooledFence && m_device != VK_NULL_HANDLE) {
vkDestroyFence(m_device, record.fence, nullptr);
}
}
m_inFlightSubmits.clear();
for (auto fence : m_freeSubmitFences) {
if (m_device != VK_NULL_HANDLE) {
vkDestroyFence(m_device, fence, nullptr);
}
}
m_freeSubmitFences.clear();
m_completedSubmitCounter = m_submitCounter;
}
Bool VulkanRenderer::SubmitPendingCommandBuffer(FrameContext::FrameData& frame, VkFence fence, Bool pooledFence) {
VkPipelineStageFlags waitDstStageMask = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT;
VkSemaphore waitSemaphore = frame.imageAvailableSemaphore;
VkSubmitInfo submitInfo{VK_STRUCTURE_TYPE_SUBMIT_INFO};
if (!frame.imageAvailableSemaphoreConsumed) {
submitInfo.waitSemaphoreCount = 1;
submitInfo.pWaitSemaphores = &waitSemaphore;
submitInfo.pWaitDstStageMask = &waitDstStageMask;
}
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &frame.commandBuffer;
const VkResult result = vkQueueSubmit(m_graphicsQueue, 1, &submitInfo, fence);
if (result != VK_SUCCESS) {
MGLOG_E("SubmitPendingCommandBuffer: vkQueueSubmit returned %d", result);
return false;
}
frame.imageAvailableSemaphoreConsumed = true;
frame.hasCommandBufferRecorded = false;
RegisterSubmit(fence, pooledFence);
frame.lastSubmitIndex = m_submitCounter;
return true;
}
Bool VulkanRenderer::FlushPendingCommands() {
if (m_device == VK_NULL_HANDLE || m_graphicsQueue == VK_NULL_HANDLE || m_frameContext.GetFrameCount() == 0) {
return false;
}
auto& frame = m_frameContext.GetCurrent();
if (!frame.isCommandRecording && !frame.hasCommandBufferRecorded) {
return false;
}
// Acquire the fence while recording is still open: failing here must
// not end recording, or the next draw's BeginCommandRecording would
// reset the command buffer and silently drop the frame's commands.
VkFence fence = AcquirePooledSubmitFence();
if (fence == VK_NULL_HANDLE) {
return false;
}
if (frame.isCommandRecording) {
if (VkRenderPassManager::GetActiveRenderPass() != nullptr) {
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
}
m_frameContext.EndCommandRecording();
}
if (!SubmitPendingCommandBuffer(frame, fence, /*pooledFence=*/true)) {
// Submit failure (device loss regime): the ended command buffer
// stays marked recorded so Present can still try to submit it.
m_freeSubmitFences.push_back(fence); // still unsignaled, reusable
return false;
}
// The submitted command buffer may still be executing; recording must
// restart on a fresh one. If none can be allocated, fall back to
// draining this submission so reusing the buffer stays legal.
const VkResult retireResult = m_frameContext.RetireCurrentCommandBuffer();
if (retireResult != VK_SUCCESS) {
MGLOG_E("FlushPendingCommands: RetireCurrentCommandBuffer returned %d; draining submission", retireResult);
if (vkWaitForFences(m_device, 1, &fence, VK_TRUE, UINT64_MAX) == VK_SUCCESS) {
OnSubmitsCompletedUpTo(m_submitCounter);
} else if (vkQueueWaitIdle(m_graphicsQueue) == VK_SUCCESS) {
m_bufferManager.NotifyDeviceIdle();
OnSubmitsCompletedUpTo(m_submitCounter);
} else {
// Device is effectively lost; the command buffer may still be
// pending, but no recovery can make reuse legal.
MGLOG_E("FlushPendingCommands: drain failed; command buffer reuse is unsafe");
}
}
return true;
}
Bool VulkanRenderer::FlushForSyncPoint(Uint64 submitIndex) {
// A flush only helps a sync point whose commands are not submitted
// yet; for an already-submitted index it would just split the frame's
// render pass (a full tile load/store on TBDR GPUs) without advancing
// the fence.
if (submitIndex <= m_submitCounter) {
return false;
}
return FlushPendingCommands();
}
Bool VulkanRenderer::WaitForSubmitIndex(Uint64 submitIndex, Uint64 timeoutNs, Bool flushIfPending) {
if (IsSubmitIndexComplete(submitIndex)) {
return true;
}
if (submitIndex > m_submitCounter) {
if (!flushIfPending) {
return false;
}
FlushPendingCommands();
if (submitIndex > m_submitCounter) {
// Nothing could be submitted (empty batch or submit failure);
// the index cannot complete yet.
return false;
}
}
for (const auto& record : m_inFlightSubmits) {
if (record.submitIndex >= submitIndex) {
const VkResult result = vkWaitForFences(m_device, 1, &record.fence, VK_TRUE, timeoutNs);
if (result == VK_SUCCESS) {
OnSubmitsCompletedUpTo(record.submitIndex);
return true;
}
if (result != VK_TIMEOUT) {
MGLOG_E("WaitForSubmitIndex: vkWaitForFences returned %d", result);
}
return false;
}
}
// No in-flight record at or beyond the index: it was already observed
// complete via a fence wait on a later submission.
return true;
}
@@ -5516,9 +5713,10 @@ void main() {
if (IsTimerQueryResultReady(record)) {
return true;
}
// Mirrors ClientWaitSync: WaitForFrameSerial refuses serials that
// cannot complete without further submissions (a timestamp written
// this frame only executes once Present submits the command buffer).
// WaitForFrameSerial refuses serials that cannot complete without
// further submissions (a timestamp written this frame only executes
// once Present submits the command buffer), so this returns false
// instead of deadlocking; the record resolves after a later Present.
if (!WaitForFrameSerial(record.frameSerial, UINT64_MAX)) {
return false;
}
@@ -5547,8 +5745,14 @@ void main() {
const VkExtent2D presentStatsExtent = m_swapchainObject.GetExtent();
const char* presentDumpPath = PresentDumpPath();
const Bool shouldDumpPresent = presentDumpPath != nullptr && PresentDumpMatchesTargetCall();
const Bool collectPresentStats = (PresentStatsEnabled() || shouldDumpPresent) && frame.isCommandRecording &&
presentStatsExtent.width > 0 && presentStatsExtent.height > 0;
const Bool wantPresentStats = (PresentStatsEnabled() || shouldDumpPresent) &&
presentStatsExtent.width > 0 && presentStatsExtent.height > 0;
if (wantPresentStats && !frame.isCommandRecording) {
// A mid-frame flush may have closed the frame's recording; the
// stats copy needs an open command buffer.
m_frameContext.BeginCommandRecording();
}
const Bool collectPresentStats = wantPresentStats && frame.isCommandRecording;
if (PresentStatsEnabled() && presentDumpPath != nullptr) {
// Live getenv on purpose (not MG_Config::Features): the retrace
// harness mutates these two variables at runtime via setenv.
@@ -5623,9 +5827,12 @@ void main() {
// 1) Submit current frame work.
auto submitPacket = m_frameContext.GetSubmitInfo(shouldSubmitCommandBuffer, m_imageIndexAcquired);
VK_VERIFY(vkQueueSubmit(m_graphicsQueue, 1, &submitPacket.submitInfo, frame.imageInFlightFence));
RegisterSubmit(frame.imageInFlightFence, /*pooledFence=*/false);
frame.lastSubmitIndex = m_submitCounter;
if (collectPresentStats) {
VK_VERIFY(vkWaitForFences(m_device, 1, &frame.imageInFlightFence, VK_TRUE, UINT64_MAX),
"Present stats, vkWaitForFences");
OnSubmitsCompletedUpTo(frame.lastSubmitIndex);
const auto* pixels = static_cast<const Uint8*>(presentStatsReadback.Map());
MOBILEGL_ASSERT(pixels != nullptr, "Present stats: failed to map readback buffer");
SizeT nonBlack = 0;
@@ -5699,9 +5906,21 @@ void main() {
m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
}
VK_VERIFY(result, "Present, vkAcquireNextImageKHR");
// The acquired slot's fence has been waited: its last submission
// (and, in queue order, everything before it) is complete. The frame
// serials those submissions carried advance the buffer-manager floor
// inside OnSubmitsCompletedUpTo.
OnSubmitsCompletedUpTo(m_frameContext.GetCurrent().lastSubmitIndex);
CollectDeferredDepthMipmapCleanup(m_frameContext.GetCurrentFrameIndex());
m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex());
m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex());
// Descriptor-set reuse cursors rewind exactly once per frame, here,
// after the slot's fence wait proved its previous sets GPU-idle. (The
// per-draw-path lazy rewind missed frames whose recording was opened
// by a staged buffer copy or timer-query timestamp, leaking a fresh
// descriptor set per draw for the whole frame; it would also be unsafe
// after a mid-frame FlushPendingCommands, which does not wait.)
m_uniformManager->BeginFrame(m_frameContext.GetCurrentFrameIndex());
}
void VulkanRenderer::CreateInstance() {
@@ -6420,6 +6639,7 @@ void main() {
}
vkDeviceWaitIdle(m_device);
OnSubmitsCompletedUpTo(m_submitCounter);
if (m_timerQueryManager) {
// The in-progress command buffer is abandoned below (its recording
@@ -173,11 +173,35 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkInstance GetInstance() const;
Bool IsDrawIndirectCountExtensionEnabled() const;
// GL fence support, expressed in VkBufferManager frame serials: a fence
// captures GetCurrentFrameSerial() at creation and is signaled once
// IsFrameSerialComplete() reports that serial complete (the same
// busy-tracking horizon used to recycle buffer resources).
Uint64 GetCurrentFrameSerial() const;
// GL fence support, expressed in queue-submission indices backed by
// real VkFences. A GL fence captures GetSyncPointSubmitIndex() at
// creation: the index of the submission that will carry the commands
// recorded so far (m_submitCounter + 1 while work is pending, or
// m_submitCounter when nothing has been recorded since the last
// submit). It is signaled once that submission's fence is observed
// signaled - unlike the frame-serial heuristic, this makes fences
// signal as soon as the GPU actually finishes, which MC 1.21.5's
// fence-paced ring buffers rely on to recycle their space.
Uint64 GetSyncPointSubmitIndex() const;
// Non-blocking: polls outstanding submission fences and reports
// whether every submission up to `submitIndex` has completed.
Bool IsSubmitIndexComplete(Uint64 submitIndex);
// Submits the commands recorded so far without waiting (GL flush).
// Recording restarts lazily on a fresh command buffer; the submitted
// one is retired until the frame slot's fence is next waited. Returns
// true when a submission was made.
Bool FlushPendingCommands();
// Flush gated on usefulness: only flushes when `submitIndex` is still
// unsubmitted, so poll loops on already-submitted fences do not split
// the frame's render pass (a full tile load/store on TBDR GPUs).
Bool FlushForSyncPoint(Uint64 submitIndex);
// Blocking wait for a submission index with a nanosecond timeout.
// When the index is still unsubmitted and flushIfPending is set, the
// pending commands are flushed first so the wait can make progress.
Bool WaitForSubmitIndex(Uint64 submitIndex, Uint64 timeoutNs, Bool flushIfPending);
// Frame-serial completion, still used by the timer-query paths (their
// records are bucketed per frame slot).
Bool IsFrameSerialComplete(Uint64 serial) const;
// Blocking wait for a submitted serial. Returns false when the serial
// cannot complete without further submissions (it belongs to the
@@ -245,6 +269,53 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLenum buffer, GLint drawbuffer,
const ClearAttachmentPayload& clearPayload);
// ---- Submission fence tracking (GL sync objects) ----
// One record per vkQueueSubmit still in flight, in ascending submit
// order. Present/readback submissions reference the frame slot's
// fence (not pool-owned); mid-frame flushes use pooled fences that are
// recycled once their submission is observed complete.
// Not thread-safe: like the rest of the renderer, the tracker relies
// on GL calls being serialized (launchers migrate the context across
// threads, but calls never run concurrently), so sync-object polls
// may mutate it without locking.
struct SubmitRecord {
Uint64 submitIndex = 0;
// Buffer-manager frame serial the submission was made under; its
// completion raises the completed-serial floor (timer queries and
// buffer busy-tracking live in frame-serial space).
Uint64 frameSerial = 0;
VkFence fence = VK_NULL_HANDLE;
Bool pooledFence = false;
};
// Registers a submission that vkQueueSubmit just made with `fence`.
// Invariant: every graphics-queue submission that outlives its call
// site must be registered so GL fences observe it. Exempt are the
// texture-upload/preserve submits in VkTextureManager, which
// vkWaitForFences inline before returning.
void RegisterSubmit(VkFence fence, Bool pooledFence);
// Builds the submit packet for the frame's pending command buffer
// (consuming the acquire semaphore on the slot's first submission),
// submits it with `fence`, and registers the submission. On failure
// the frame state is left untouched. Shared by the mid-frame flush
// and the readback path so the semaphore-consumption invariant lives
// in one place.
Bool SubmitPendingCommandBuffer(FrameContext::FrameData& frame, VkFence fence, Bool pooledFence);
// Polls in-flight submission fences (prefix order) and advances the
// completed counter past every fence observed signaled.
void RefreshCompletedSubmits();
// All submissions up to `submitIndex` are known complete (their fence
// was waited or the device was idled); drops their records and
// recycles pooled fences.
void OnSubmitsCompletedUpTo(Uint64 submitIndex);
VkFence AcquirePooledSubmitFence();
void DestroySubmitFencePool();
Bool HasPendingRecordedWork() const;
Vector<SubmitRecord> m_inFlightSubmits;
Vector<VkFence> m_freeSubmitFences;
Uint64 m_submitCounter = 0;
Uint64 m_completedSubmitCounter = 0;
NativeWindowType m_window = 0;
void* m_platformDisplay = nullptr;
void* m_platformLibrary = nullptr;