From b9a15aed6109b6aab0ddc163314c31f5807805ff Mon Sep 17 00:00:00 2001 From: BZLZHH Date: Sat, 1 Aug 2026 02:02:35 -0400 Subject: [PATCH] [Feat] (DirectVulkan, MG_Impl): GPU transform feedback primitive queries The TF primitive queries now ride VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT pools when the device reports transformFeedbackQueries: each captured draw is wrapped in a slot (shared between both GL targets when active together), and results sum the (written, needed) pairs - GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN from the first, GL_PRIMITIVES_GENERATED from the second. This is exact through geometry shaders, so KHR-GL33.transform_feedback.query_geometry_* pass; the CPU accounting delta remains the fallback for backends without the feature. --- MobileGL/MG_Backend/BackendObject.h | 4 + .../BackendObject_DirectVulkan.cpp | 2 + .../MG_Backend/DirectVulkan/DirectVulkan.cpp | 34 +++++- .../MG_Backend/DirectVulkan/DirectVulkan.h | 2 + .../DirectVulkan/Renderer/VulkanRenderer.cpp | 107 ++++++++++++++++++ .../DirectVulkan/Renderer/VulkanRenderer.h | 24 ++++ MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp | 21 +++- 7 files changed, 187 insertions(+), 7 deletions(-) diff --git a/MobileGL/MG_Backend/BackendObject.h b/MobileGL/MG_Backend/BackendObject.h index 0443d544..3082f013 100644 --- a/MobileGL/MG_Backend/BackendObject.h +++ b/MobileGL/MG_Backend/BackendObject.h @@ -225,6 +225,10 @@ namespace MobileGL { // GetQueryResult64 / DeleteBackendQuery like timer queries. BackendQueryHandle (*BeginOcclusionQuery)(); void (*EndOcclusionQuery)(BackendQueryHandle query); + // Transform feedback primitive queries backed by real GPU query pools + // (optional; null = frontend falls back to CPU accounting). + BackendQueryHandle (*BeginXfbPrimitivesQuery)(Bool generated); + void (*EndXfbPrimitivesQuery)(BackendQueryHandle query); Int64 (*GetGpuTimestampNs)(); // glGetInteger64v(GL_TIMESTAMP); 0 if unsupported }; struct GlobalBackendFunctionsTable { diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index ceeb4e4b..0c2f8654 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -639,6 +639,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { // exist even when timer queries are disabled. funcsTable.GL.BeginOcclusionQuery = BeginOcclusionQuery; funcsTable.GL.EndOcclusionQuery = EndOcclusionQuery; + funcsTable.GL.BeginXfbPrimitivesQuery = BeginXfbPrimitivesQuery; + funcsTable.GL.EndXfbPrimitivesQuery = EndXfbPrimitivesQuery; funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable; funcsTable.GL.GetQueryResult64 = GetQueryResult64; funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery; diff --git a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp index c54d0a8c..d5f4b2fc 100644 --- a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp @@ -1564,7 +1564,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { // records are shared (SharedPtr) with the owning pool's pending list, // so deleting the query while results are still in flight is safe. struct VulkanTimerQuery { - enum class Kind : Uint8 { Timer, Occlusion }; + enum class Kind : Uint8 { Timer, Occlusion, XfbWritten, XfbGenerated }; Kind kind = Kind::Timer; SharedPtr begin; SharedPtr end; @@ -1668,6 +1668,17 @@ namespace MobileGL::MG_Backend::DirectVulkan { *outNanoseconds = samples; return true; } + if (query->kind == VulkanTimerQuery::Kind::XfbWritten || + query->kind == VulkanTimerQuery::Kind::XfbGenerated) { + Uint64 primitives = 0; + if (!pVulkanRenderer->ResolveXfbQueryResult(query->occlusionSlots, + query->kind == VulkanTimerQuery::Kind::XfbGenerated, + primitives)) { + return false; + } + *outNanoseconds = primitives; + return true; + } // With wait, mirrors ClientWaitSync: a query ended this frame cannot // complete until Present submits the commands, so the wait refuses to // block on the current unsubmitted serial. Returning false keeps the @@ -1700,6 +1711,27 @@ namespace MobileGL::MG_Backend::DirectVulkan { delete static_cast(handle); } + BackendQueryHandle BeginXfbPrimitivesQuery(Bool generated) { + MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::BeginXfbPrimitivesQuery called with null VulkanRenderer"); + if (!pVulkanRenderer->StartXfbQueryCapture(generated ? 1u : 0u)) { + return nullptr; + } + auto* query = new VulkanTimerQuery{}; + query->kind = generated ? VulkanTimerQuery::Kind::XfbGenerated : VulkanTimerQuery::Kind::XfbWritten; + query->rendererGeneration = GetRendererGeneration(); + return query; + } + + void EndXfbPrimitivesQuery(BackendQueryHandle handle) { + auto* query = static_cast(handle); + MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::EndXfbPrimitivesQuery called with null VulkanRenderer"); + if (query == nullptr || query->rendererGeneration != GetRendererGeneration()) { + return; + } + pVulkanRenderer->StopXfbQueryCapture( + query->kind == VulkanTimerQuery::Kind::XfbGenerated ? 1u : 0u, query->occlusionSlots); + } + BackendQueryHandle BeginOcclusionQuery() { MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::BeginOcclusionQuery called with null VulkanRenderer"); if (!pVulkanRenderer->StartOcclusionQueryCapture()) { diff --git a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.h b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.h index ad3775c9..06ad546a 100644 --- a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.h +++ b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.h @@ -123,6 +123,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { // only while a live renderer exists whose device can actually time. Bool IsTimerQuerySupported(); BackendQueryHandle BeginTimeElapsedQuery(); + BackendQueryHandle BeginXfbPrimitivesQuery(Bool generated); + void EndXfbPrimitivesQuery(BackendQueryHandle query); BackendQueryHandle BeginOcclusionQuery(); void EndOcclusionQuery(BackendQueryHandle query); void EndTimeElapsedQuery(BackendQueryHandle query); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 70e5a417..97950132 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -2786,6 +2786,10 @@ void main() { vkDestroyQueryPool(m_device, m_occlusionQueryPool, nullptr); m_occlusionQueryPool = VK_NULL_HANDLE; } + if (m_xfbQueryPool != VK_NULL_HANDLE) { + vkDestroyQueryPool(m_device, m_xfbQueryPool, nullptr); + m_xfbQueryPool = VK_NULL_HANDLE; + } m_bufferManager.Shutdown(); // Device is idle (vkDeviceWaitIdle above); query pools can be destroyed. @@ -7788,6 +7792,7 @@ void main() { VkCommandBuffer& commandBuffer = frame.commandBuffer; const Bool xfbActive = BeginXfbCaptureForDraw(frame); + BeginXfbQueryForDraw(commandBuffer); const Bool occlusionActive = BeginOcclusionForDraw(commandBuffer); vkCmdDraw(commandBuffer, payload.params.vertexCount, @@ -7796,6 +7801,7 @@ void main() { payload.params.firstInstance); EndOcclusionForDraw(commandBuffer, occlusionActive); EndXfbCaptureForDraw(frame, xfbActive); + EndXfbQueryForDraw(commandBuffer); } Bool VulkanRenderer::StartOcclusionQueryCapture() { @@ -7855,6 +7861,91 @@ void main() { return true; } + Bool VulkanRenderer::StartXfbQueryCapture(Uint32 kind) { + if (!m_xfbQueriesSupported || !m_hostQueryResetEnabled || s_vkResetQueryPool == nullptr || + s_vkCmdBeginQueryIndexedEXT == nullptr || kind > 1) { + return false; + } + if (m_xfbQueryPool == VK_NULL_HANDLE) { + VkQueryPoolCreateInfo poolInfo{}; + poolInfo.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO; + poolInfo.queryType = VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT; + poolInfo.queryCount = kXfbQuerySlots; + if (vkCreateQueryPool(m_device, &poolInfo, nullptr, &m_xfbQueryPool) != VK_SUCCESS) { + MGLOG_E("StartXfbQueryCapture: vkCreateQueryPool failed"); + m_xfbQueryPool = VK_NULL_HANDLE; + return false; + } + s_vkResetQueryPool(m_device, m_xfbQueryPool, 0, kXfbQuerySlots); + } + m_xfbQueryActiveSlots[kind].clear(); + m_xfbQueryCaptureActive[kind] = true; + return true; + } + + void VulkanRenderer::StopXfbQueryCapture(Uint32 kind, Vector& outSlots) { + if (kind > 1) { + return; + } + outSlots = Move(m_xfbQueryActiveSlots[kind]); + m_xfbQueryActiveSlots[kind].clear(); + m_xfbQueryCaptureActive[kind] = false; + } + + Bool VulkanRenderer::ResolveXfbQueryResult(const Vector& slots, Bool wantGenerated, Uint64& outPrimitives) { + outPrimitives = 0; + if (slots.empty() || m_xfbQueryPool == VK_NULL_HANDLE) { + return true; + } + auto& frame = m_frameContext.GetCurrent(); + if (frame.isCommandRecording) { + if (VkRenderPassManager::GetActiveRenderPass() != nullptr) { + VkRenderPassManager::EndRenderPass(frame.commandBuffer); + } + if (!SubmitReadbackCommandsAndWait(frame)) { + return false; + } + } + for (const Uint32 slot : slots) { + Uint64 pair[2] = {0, 0}; // {primitivesWritten, primitivesNeeded} + const VkResult result = + vkGetQueryPoolResults(m_device, m_xfbQueryPool, slot, 1, sizeof(pair), pair, sizeof(pair), + VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT); + if (result == VK_SUCCESS) { + outPrimitives += pair[wantGenerated ? 1 : 0]; + } + } + return true; + } + + void VulkanRenderer::BeginXfbQueryForDraw(VkCommandBuffer commandBuffer) { + m_xfbQuerySlotOpen = false; + if ((!m_xfbQueryCaptureActive[0] && !m_xfbQueryCaptureActive[1]) || m_xfbQueryPool == VK_NULL_HANDLE) { + return; + } + const Uint32 slot = m_xfbQuerySlotCursor; + m_xfbQuerySlotCursor = (m_xfbQuerySlotCursor + 1) % kXfbQuerySlots; + // Slots are never host-reset at read time (both GL targets may reference one + // slot); recycle them here instead. + s_vkResetQueryPool(m_device, m_xfbQueryPool, slot, 1); + s_vkCmdBeginQueryIndexedEXT(commandBuffer, m_xfbQueryPool, slot, 0, 0); + for (Uint32 kind = 0; kind < 2; ++kind) { + if (m_xfbQueryCaptureActive[kind]) { + m_xfbQueryActiveSlots[kind].push_back(slot); + } + } + m_xfbQuerySlotOpen = true; + m_xfbQueryOpenSlot = slot; + } + + void VulkanRenderer::EndXfbQueryForDraw(VkCommandBuffer commandBuffer) { + if (!m_xfbQuerySlotOpen) { + return; + } + s_vkCmdEndQueryIndexedEXT(commandBuffer, m_xfbQueryPool, m_xfbQueryOpenSlot, 0); + m_xfbQuerySlotOpen = false; + } + Bool VulkanRenderer::BeginOcclusionForDraw(VkCommandBuffer commandBuffer) { if (!m_occlusionCaptureActive || m_occlusionQueryPool == VK_NULL_HANDLE) { return false; @@ -7902,6 +7993,7 @@ void main() { VkCommandBuffer& commandBuffer = frame.commandBuffer; const Bool xfbActive = BeginXfbCaptureForDraw(frame); + BeginXfbQueryForDraw(commandBuffer); const Bool occlusionActive = BeginOcclusionForDraw(commandBuffer); vkCmdDrawIndexed(commandBuffer, payload.params.indexCount, @@ -7911,6 +8003,7 @@ void main() { payload.params.firstInstance); EndOcclusionForDraw(commandBuffer, occlusionActive); EndXfbCaptureForDraw(frame, xfbActive); + EndXfbQueryForDraw(commandBuffer); } void VulkanRenderer::MultiDrawArrays(const MultiDrawCmd& payload) { @@ -9518,6 +9611,20 @@ void main() { m_hostQueryResetEnabled = false; } } + if (m_transformFeedbackFeatureEnabled) { + s_vkCmdBeginQueryIndexedEXT = reinterpret_cast( + vkGetDeviceProcAddr(m_device, "vkCmdBeginQueryIndexedEXT")); + s_vkCmdEndQueryIndexedEXT = reinterpret_cast( + vkGetDeviceProcAddr(m_device, "vkCmdEndQueryIndexedEXT")); + VkPhysicalDeviceTransformFeedbackPropertiesEXT xfbProperties{}; + xfbProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TRANSFORM_FEEDBACK_PROPERTIES_EXT; + VkPhysicalDeviceProperties2 properties2{}; + properties2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2; + properties2.pNext = &xfbProperties; + vkGetPhysicalDeviceProperties2(m_physicalDevice.handle, &properties2); + m_xfbQueriesSupported = xfbProperties.transformFeedbackQueries == VK_TRUE && + s_vkCmdBeginQueryIndexedEXT != nullptr && s_vkCmdEndQueryIndexedEXT != nullptr; + } MGLOG_I("index type uint8 enabled: %s", m_indexTypeUint8ExtensionEnabled ? "true" : "false"); MGLOG_I("Logical device created."); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index d7582de2..d0377e90 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -503,6 +503,30 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint32 m_occlusionSlotCursor = 0; Bool m_occlusionCaptureActive = false; Vector m_occlusionActiveSlots; + // Transform feedback primitive queries: one pool slot per captured draw yields + // the (written, needed) pair; GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN sums the + // first, GL_PRIMITIVES_GENERATED the second - exact with geometry shaders, + // unlike the CPU fallback accounting. + Bool m_xfbQueriesSupported = false; + PFN_vkCmdBeginQueryIndexedEXT s_vkCmdBeginQueryIndexedEXT = nullptr; + PFN_vkCmdEndQueryIndexedEXT s_vkCmdEndQueryIndexedEXT = nullptr; + VkQueryPool m_xfbQueryPool = VK_NULL_HANDLE; + static constexpr Uint32 kXfbQuerySlots = 8192; + Uint32 m_xfbQuerySlotCursor = 0; + Bool m_xfbQueryCaptureActive[2] = {false, false}; // [0]=written, [1]=generated + Vector m_xfbQueryActiveSlots[2]; + Bool m_xfbQuerySlotOpen = false; + Uint32 m_xfbQueryOpenSlot = 0; + + public: + // kind: 0 = PRIMITIVES_WRITTEN, 1 = PRIMITIVES_GENERATED. + Bool StartXfbQueryCapture(Uint32 kind); + void StopXfbQueryCapture(Uint32 kind, Vector& outSlots); + Bool ResolveXfbQueryResult(const Vector& slots, Bool wantGenerated, Uint64& outPrimitives); + + private: + void BeginXfbQueryForDraw(VkCommandBuffer commandBuffer); + void EndXfbQueryForDraw(VkCommandBuffer commandBuffer); VkCommandPool m_commandPool = VK_NULL_HANDLE; diff --git a/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp b/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp index 21734a31..d123d187 100644 --- a/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp +++ b/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp @@ -278,9 +278,11 @@ namespace MobileGL::MG_Impl::GLImpl { queryObject->target = target; queryObject->active = true; if (isTransformFeedbackQuery) { - // CPU accounting: captured draws bump the context counter; the query - // result is the delta between Begin and End. Without geometry-stage - // amplification the assembled count IS the written/generated count. + // Prefer real GPU transform-feedback queries (exact with geometry shaders); + // the CPU accounting delta stays as the fallback when the backend lacks them. + const auto beginXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.BeginXfbPrimitivesQuery; + queryObject->backendHandle = + beginXfbPrimitivesQuery ? beginXfbPrimitivesQuery(target == GL_PRIMITIVES_GENERATED) : nullptr; queryObject->counterSnapshot = MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter(); } else if (isOcclusionQuery) { queryObject->backendHandle = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery(); @@ -318,9 +320,16 @@ namespace MobileGL::MG_Impl::GLImpl { return; } if (isTransformFeedbackQuery) { - queryObject->cachedResult = - MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter() - queryObject->counterSnapshot; - queryObject->resultCached = true; + if (queryObject->backendHandle) { + if (const auto endXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.EndXfbPrimitivesQuery) { + endXfbPrimitivesQuery(queryObject->backendHandle); + } + // Result comes from the GPU query at read time. + } else { + queryObject->cachedResult = + MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter() - queryObject->counterSnapshot; + queryObject->resultCached = true; + } queryObject->active = false; queryObject->ended = true; activeQueryId = 0;