mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 20:58:31 +09:00
[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.
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<VkTimerQueryManager::TimestampRecord> begin;
|
||||
SharedPtr<VkTimerQueryManager::TimestampRecord> 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<VulkanTimerQuery*>(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<VulkanTimerQuery*>(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()) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<Uint32>& outSlots) {
|
||||
if (kind > 1) {
|
||||
return;
|
||||
}
|
||||
outSlots = Move(m_xfbQueryActiveSlots[kind]);
|
||||
m_xfbQueryActiveSlots[kind].clear();
|
||||
m_xfbQueryCaptureActive[kind] = false;
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::ResolveXfbQueryResult(const Vector<Uint32>& 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<PFN_vkCmdBeginQueryIndexedEXT>(
|
||||
vkGetDeviceProcAddr(m_device, "vkCmdBeginQueryIndexedEXT"));
|
||||
s_vkCmdEndQueryIndexedEXT = reinterpret_cast<PFN_vkCmdEndQueryIndexedEXT>(
|
||||
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.");
|
||||
|
||||
|
||||
@@ -503,6 +503,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 m_occlusionSlotCursor = 0;
|
||||
Bool m_occlusionCaptureActive = false;
|
||||
Vector<Uint32> 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<Uint32> 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<Uint32>& outSlots);
|
||||
Bool ResolveXfbQueryResult(const Vector<Uint32>& slots, Bool wantGenerated, Uint64& outPrimitives);
|
||||
|
||||
private:
|
||||
void BeginXfbQueryForDraw(VkCommandBuffer commandBuffer);
|
||||
void EndXfbQueryForDraw(VkCommandBuffer commandBuffer);
|
||||
|
||||
VkCommandPool m_commandPool = VK_NULL_HANDLE;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user