From 93224ca406d21587c4d336e5d91b1c90011ab9a4 Mon Sep 17 00:00:00 2001 From: BZLZHH Date: Tue, 4 Aug 2026 19:47:39 -0400 Subject: [PATCH] [Fix] (DirectVulkan): make transform feedback writes visible to what reads them GL makes transform feedback results visible to every later command on their own, with no glMemoryBarrier in between -- unlike shader storage writes. An application replaying a capture with glDrawTransformFeedback is therefore entitled to the captured bytes without asking for them, so the barrier the Vulkan memory model requires has to come from here. It cannot be recorded where the write happens: the capturing draw runs inside a render pass that declares no self-dependency. Flag it there instead and emit the barrier at the next point that could read the buffer -- the following draw's setup, or a readback -- ending the render pass first, the same shape glMemoryBarrier already uses. The destination covers every way a captured buffer comes back: replayed as vertex attributes or indices, read through a uniform or storage binding, sourced as an indirect command, copied out, or mapped. --- .../DirectVulkan/Renderer/VulkanRenderer.cpp | 37 +++++++++++++++++++ .../DirectVulkan/Renderer/VulkanRenderer.h | 5 +++ 2 files changed, 42 insertions(+) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 8d406426..bc08fe00 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -4692,6 +4692,7 @@ void main() { // Sync each sampled texture at most once across this whole draw: the layout // probe loop, the post-transition loop, and ResolveSamplerDescriptor would // otherwise each re-run the full SyncTexture path on the same textures. + MakeXfbWritesVisible(); VkTextureManager::DrawSyncScope drawSyncScope(*m_textureManager); m_textureManager->CollectGarbage(); if (TrySetupDrawFastPath(frame, mode, aspects, drawParams, pIndexBufferView)) { @@ -6920,6 +6921,7 @@ void main() { } Bool VulkanRenderer::FinishPendingGpuWork() { + MakeXfbWritesVisible(); auto& frame = m_frameContext.GetCurrent(); if (!frame.isCommandRecording) { return true; @@ -8140,6 +8142,41 @@ void main() { s_vkCmdEndTransformFeedbackEXT(frame.commandBuffer, 0, static_cast(bufferCount), counterBuffers, counterOffsets); m_xfbCountersValid[counterSlot] = true; + m_xfbWritesPendingVisibility = true; + } + + // GL makes transform feedback results visible to every later command on their own, with no + // glMemoryBarrier in between - unlike shader storage writes, which is why the barrier the + // Vulkan memory model requires has to be supplied here rather than by the application. It + // cannot be recorded where the write happens (inside the capturing draw's render pass, which + // declares no self-dependency), so it is emitted at the next point that could read the + // captured buffer: the following draw, or a readback. + void VulkanRenderer::MakeXfbWritesVisible() { + if (!m_xfbWritesPendingVisibility) { + return; + } + m_xfbWritesPendingVisibility = false; + auto& frame = m_frameContext.GetCurrent(); + if (!frame.isCommandRecording) { + m_frameContext.BeginCommandRecording(); + } + if (VkRenderPassManager::GetActiveRenderPass() != nullptr) { + VkRenderPassManager::EndRenderPass(frame.commandBuffer); + } + VkMemoryBarrier memoryBarrier{}; + memoryBarrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER; + memoryBarrier.srcAccessMask = + VK_ACCESS_TRANSFORM_FEEDBACK_WRITE_BIT_EXT | VK_ACCESS_TRANSFORM_FEEDBACK_COUNTER_WRITE_BIT_EXT; + // Every way a captured buffer can be read back: replayed as vertex attributes or indices + // by glDrawTransformFeedback, sampled through a uniform or storage binding, sourced as an + // indirect command, copied out, or mapped. + memoryBarrier.dstAccessMask = + VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT | VK_ACCESS_INDEX_READ_BIT | VK_ACCESS_UNIFORM_READ_BIT | + VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_INDIRECT_COMMAND_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT | + VK_ACCESS_HOST_READ_BIT | VK_ACCESS_MEMORY_READ_BIT | + VK_ACCESS_TRANSFORM_FEEDBACK_COUNTER_READ_BIT_EXT; + vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_TRANSFORM_FEEDBACK_BIT_EXT, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, &memoryBarrier, 0, nullptr, 0, nullptr); } void VulkanRenderer::DrawArrays(const DrawCmd& payload) { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 307a9cae..7de35679 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -518,6 +518,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { // when GL transform feedback is active; binds capture buffers on demand. Bool BeginXfbCaptureForDraw(FrameContext::FrameData& frame); void EndXfbCaptureForDraw(FrameContext::FrameData& frame, Bool began); + // Makes the captured bytes visible to whatever reads them next. Deferred rather than + // recorded next to the capture, because the capturing draw runs inside a render pass + // that declares no self-dependency. + void MakeXfbWritesVisible(); + Bool m_xfbWritesPendingVisibility = false; // Wrap one app draw in an occlusion-query slot while a GL_SAMPLES_PASSED // query is active. Returns whether a slot was begun (End must mirror it). Bool BeginOcclusionForDraw(VkCommandBuffer commandBuffer);