Files
MobileGL/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.cpp
T
swung0x48 eb090c6170 [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.
2026-07-10 10:55:10 +00:00

335 lines
15 KiB
C++

// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "FrameContext.h"
namespace MobileGL::MG_Backend::DirectVulkan {
VkResult FrameContext::Initialize(VkDevice device, VkCommandPool commandPool, Uint32 frameCount) {
Destroy(device, commandPool);
m_frames.assign(frameCount, {});
currentFrameIndex = 0;
m_device = device;
m_commandPool = commandPool;
Vector<VkCommandBuffer> commandBuffers(frameCount, VK_NULL_HANDLE);
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = frameCount;
VkResult result = vkAllocateCommandBuffers(device, &allocInfo, commandBuffers.data());
if (result != VK_SUCCESS) {
return result;
}
for (Uint32 i = 0; i < frameCount; ++i) {
m_frames[i].commandBuffer = commandBuffers[i];
}
VkSemaphoreCreateInfo semaphoreInfo{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
VkFenceCreateInfo fenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;
for (Uint32 i = 0; i < frameCount; ++i) {
result = CreateSyncObjectsForFrame(device, i, semaphoreInfo, fenceInfo);
if (result != VK_SUCCESS) {
Destroy(device, commandPool);
return result;
}
}
return VK_SUCCESS;
}
void FrameContext::Destroy(VkDevice device, VkCommandPool commandPool) {
const Uint32 frameCount = static_cast<Uint32>(m_frames.size());
Vector<VkCommandBuffer> commandBuffers(frameCount, VK_NULL_HANDLE);
for (Uint32 i = 0; i < frameCount; ++i) {
commandBuffers[i] = m_frames[i].commandBuffer;
}
for (Uint32 i = 0; i < frameCount; ++i) {
DestroySyncObjectsForFrame(device, i);
}
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() {
MOBILEGL_ASSERT(!m_frames.empty(), "FrameContext is not initialized");
return m_frames[currentFrameIndex];
}
const FrameContext::FrameData& FrameContext::GetCurrent() const {
MOBILEGL_ASSERT(!m_frames.empty(), "FrameContext is not initialized");
return m_frames[currentFrameIndex];
}
Bool FrameContext::IsCommandRecording() const {
return GetCurrent().isCommandRecording;
}
void FrameContext::AdvanceToNext() {
MOBILEGL_ASSERT(!m_frames.empty(), "FrameContext is not initialized");
currentFrameIndex = (currentFrameIndex + 1) % static_cast<Uint32>(m_frames.size());
GetCurrent().isCommandRecording = false;
GetCurrent().hasCommandBufferRecorded = false;
}
VkCommandBuffer& FrameContext::BeginCommandRecording(VkCommandBufferUsageFlags flags,
const VkCommandBufferInheritanceInfo* pInheritanceInfo) {
auto& frame = GetCurrent();
MOBILEGL_ASSERT(!frame.isCommandRecording, "BeginCommandRecording called while command buffer is already recording");
frame.hasCommandBufferRecorded = false;
VK_VERIFY(vkResetCommandBuffer(frame.commandBuffer, 0), "BeginCommandRecording, vkResetCommandBuffer");
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
beginInfo.flags = flags;
beginInfo.pInheritanceInfo = pInheritanceInfo;
VK_VERIFY(vkBeginCommandBuffer(frame.commandBuffer, &beginInfo), "BeginCommandRecording, vkBeginCommandBuffer");
frame.isCommandRecording = true;
if (m_recordingObserver != nullptr) {
m_recordingObserver->OnFrameCommandRecordingBegan(frame.commandBuffer);
}
return frame.commandBuffer;
}
void FrameContext::EndCommandRecording() {
auto& frame = GetCurrent();
MOBILEGL_ASSERT(frame.isCommandRecording, "EndCommandRecording called without active command buffer recording");
VK_VERIFY(vkEndCommandBuffer(frame.commandBuffer), "EndCommandRecording, vkEndCommandBuffer");
frame.isCommandRecording = false;
frame.hasCommandBufferRecorded = true;
}
VkResult FrameContext::InitializeSwapchainSemaphores(VkDevice device, Uint32 swapchainImageCount) {
DestroySwapchainSemaphores(device);
if (swapchainImageCount == 0) {
return VK_SUCCESS;
}
m_swapchainImageRenderFinishedSemaphores.assign(swapchainImageCount, VK_NULL_HANDLE);
VkSemaphoreCreateInfo semaphoreInfo{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
for (Uint32 imageIndex = 0; imageIndex < swapchainImageCount; ++imageIndex) {
VkResult result =
vkCreateSemaphore(device, &semaphoreInfo, nullptr, &m_swapchainImageRenderFinishedSemaphores[imageIndex]);
if (result != VK_SUCCESS) {
DestroySwapchainSemaphores(device);
return result;
}
}
return VK_SUCCESS;
}
void FrameContext::DestroySwapchainSemaphores(VkDevice device) {
if (device != VK_NULL_HANDLE) {
for (auto semaphore : m_swapchainImageRenderFinishedSemaphores) {
if (semaphore != VK_NULL_HANDLE) {
vkDestroySemaphore(device, semaphore, nullptr);
}
}
}
m_swapchainImageRenderFinishedSemaphores.clear();
}
Bool FrameContext::TransitionToPresent(VkImage image, VkImageLayout oldLayout, VkImageLayout presentLayout) {
auto& frame = GetCurrent();
if (frame.hasCommandBufferRecorded || frame.isCommandRecording || oldLayout == presentLayout ||
oldLayout == VK_IMAGE_LAYOUT_SHARED_PRESENT_KHR) {
return false;
}
auto& commandBuffer = BeginCommandRecording();
VkImageMemoryBarrier presentBarrier{};
presentBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
presentBarrier.srcAccessMask = 0;
presentBarrier.dstAccessMask = 0;
presentBarrier.oldLayout = oldLayout;
presentBarrier.newLayout = presentLayout;
presentBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
presentBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
presentBarrier.image = image;
presentBarrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
presentBarrier.subresourceRange.baseMipLevel = 0;
presentBarrier.subresourceRange.levelCount = 1;
presentBarrier.subresourceRange.baseArrayLayer = 0;
presentBarrier.subresourceRange.layerCount = 1;
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0,
nullptr, 0, nullptr, 1, &presentBarrier);
EndCommandRecording();
return true;
}
FrameContext::SubmitInfoPacket FrameContext::GetSubmitInfo(Bool shouldSubmitCommandBuffer,
Uint32 swapchainImageIndex) const {
const auto& frame = GetCurrent();
MOBILEGL_ASSERT(!frame.isCommandRecording, "GetSubmitInfo called while command buffer recording is still active");
AssertValidSwapchainImageIndex(swapchainImageIndex);
SubmitInfoPacket packet{};
packet.waitSemaphore = frame.imageAvailableSemaphore;
packet.signalSemaphore = m_swapchainImageRenderFinishedSemaphores[swapchainImageIndex];
packet.commandBuffer = frame.commandBuffer;
packet.submitInfo.waitSemaphoreCount = frame.imageAvailableSemaphoreConsumed ? 0U : 1U;
packet.submitInfo.pWaitSemaphores = frame.imageAvailableSemaphoreConsumed ? nullptr : &packet.waitSemaphore;
packet.submitInfo.pWaitDstStageMask = frame.imageAvailableSemaphoreConsumed ? nullptr : &packet.waitDstStageMask;
packet.submitInfo.commandBufferCount = shouldSubmitCommandBuffer ? 1U : 0U;
packet.submitInfo.pCommandBuffers = shouldSubmitCommandBuffer ? &packet.commandBuffer : nullptr;
packet.submitInfo.signalSemaphoreCount = 1;
packet.submitInfo.pSignalSemaphores = &packet.signalSemaphore;
return packet;
}
FrameContext::PresentInfoPacket FrameContext::GetPresentInfo(VkSwapchainKHR swapchain, Uint32 imageIndex) const {
AssertValidSwapchainImageIndex(imageIndex);
PresentInfoPacket packet{};
packet.waitSemaphore = m_swapchainImageRenderFinishedSemaphores[imageIndex];
packet.swapchain = swapchain;
packet.imageIndex = imageIndex;
packet.presentInfo.waitSemaphoreCount = 1;
packet.presentInfo.pWaitSemaphores = &packet.waitSemaphore;
packet.presentInfo.swapchainCount = 1;
packet.presentInfo.pSwapchains = &packet.swapchain;
packet.presentInfo.pImageIndices = &packet.imageIndex;
packet.presentInfo.pResults = nullptr;
return packet;
}
VkResult FrameContext::WaitAndAcquireNextImage(VkDevice device, VkSwapchainKHR swapchain, Uint32& outImageIndex,
Uint64 timeout, VkFence acquireFence) {
auto& frame = GetCurrent();
VkResult result = vkWaitForFences(device, 1, &frame.imageInFlightFence, VK_TRUE, timeout);
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);
if (result != VK_SUCCESS) {
return result;
}
frame.imageAvailableSemaphoreConsumed = false;
return vkResetFences(device, 1, &frame.imageInFlightFence);
}
Uint32 FrameContext::GetCurrentFrameIndex() const {
return currentFrameIndex;
}
Uint32 FrameContext::GetFrameCount() const {
return static_cast<Uint32>(m_frames.size());
}
void FrameContext::SetRecordingObserver(IRecordingObserver* observer) {
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");
}
void FrameContext::AssertValidSwapchainImageIndex(Uint32 imageIndex) const {
MOBILEGL_ASSERT(imageIndex < m_swapchainImageRenderFinishedSemaphores.size(),
"FrameContext swapchain image index out of range");
}
VkResult FrameContext::CreateSyncObjectsForFrame(VkDevice device, Uint32 frameIndex,
const VkSemaphoreCreateInfo& semaphoreInfo,
const VkFenceCreateInfo& fenceInfo) {
AssertValidFrameIndex(frameIndex);
DestroySyncObjectsForFrame(device, frameIndex);
auto& frame = m_frames[frameIndex];
VkResult result =
vkCreateSemaphore(device, &semaphoreInfo, nullptr, &frame.imageAvailableSemaphore);
if (result != VK_SUCCESS) {
return result;
}
result = vkCreateFence(device, &fenceInfo, nullptr, &frame.imageInFlightFence);
if (result != VK_SUCCESS) {
vkDestroySemaphore(device, frame.imageAvailableSemaphore, nullptr);
frame.imageAvailableSemaphore = VK_NULL_HANDLE;
return result;
}
frame.hasCommandBufferRecorded = false;
frame.isCommandRecording = false;
frame.imageAvailableSemaphoreConsumed = false;
return VK_SUCCESS;
}
void FrameContext::DestroySyncObjectsForFrame(VkDevice device, Uint32 frameIndex) {
AssertValidFrameIndex(frameIndex);
auto& frame = m_frames[frameIndex];
if (device != VK_NULL_HANDLE && frame.imageInFlightFence != VK_NULL_HANDLE) {
vkDestroyFence(device, frame.imageInFlightFence, nullptr);
}
frame.imageInFlightFence = VK_NULL_HANDLE;
if (device != VK_NULL_HANDLE && frame.imageAvailableSemaphore != VK_NULL_HANDLE) {
vkDestroySemaphore(device, frame.imageAvailableSemaphore, nullptr);
}
frame.imageAvailableSemaphore = VK_NULL_HANDLE;
frame.isCommandRecording = false;
frame.hasCommandBufferRecorded = false;
frame.imageAvailableSemaphoreConsumed = false;
}
} // namespace MobileGL::MG_Backend::DirectVulkan