[Feat] (DirectVulkan, MG_Impl): occlusion queries via Vulkan query pools

GL_SAMPLES_PASSED / GL_ANY_SAMPLES_PASSED(_CONSERVATIVE) now work: every
app draw between Begin/EndQuery is wrapped in a slot of a host-reset
occlusion query pool (precise counts when occlusionQueryPrecise is
granted), and the result flush ends any active render pass before
submitting, waits, sums the slots and recycles them. ANY_* targets
report the boolean form; GL_QUERY_COUNTER_BITS and GL_CURRENT_QUERY
answer for the occlusion targets, and deleting an active query releases
its slot. Draw-time depth/stencil state also honors attachment absence:
a framebuffer without a depth (stencil) attachment behaves as if that
test always passes, even when a packed depth-stencil image is attached
through only one half (verify_partial_attachments.*).
This commit is contained in:
BZLZHH
2026-08-01 01:50:34 -04:00
parent 22b749dd37
commit 107b56d603
7 changed files with 285 additions and 12 deletions
+5
View File
@@ -220,6 +220,11 @@ namespace MobileGL {
// and leave the query readable later.
Bool (*GetQueryResult64)(BackendQueryHandle query, Bool wait, Uint64* outNanoseconds);
void (*DeleteBackendQuery)(BackendQueryHandle query);
// GL_SAMPLES_PASSED occlusion queries (optional; null = unsupported,
// the frontend then rejects the target). Results/deletion flow through
// GetQueryResult64 / DeleteBackendQuery like timer queries.
BackendQueryHandle (*BeginOcclusionQuery)();
void (*EndOcclusionQuery)(BackendQueryHandle query);
Int64 (*GetGpuTimestampNs)(); // glGetInteger64v(GL_TIMESTAMP); 0 if unsupported
};
struct GlobalBackendFunctionsTable {
@@ -635,6 +635,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
funcsTable.GL.GetGpuTimestampNs = GetGpuTimestampNs;
}
// Occlusion queries share the handle-based result/delete entries, which must
// exist even when timer queries are disabled.
funcsTable.GL.BeginOcclusionQuery = BeginOcclusionQuery;
funcsTable.GL.EndOcclusionQuery = EndOcclusionQuery;
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
funcsTableInitialized = true;
}
return funcsTable;
@@ -1564,8 +1564,12 @@ 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 };
Kind kind = Kind::Timer;
SharedPtr<VkTimerQueryManager::TimestampRecord> begin;
SharedPtr<VkTimerQueryManager::TimestampRecord> end;
// Kind::Occlusion - pool slots recorded between Begin/End; summed at result time.
Vector<Uint32> occlusionSlots;
// Renderer generation the records were written under (see
// g_rendererGeneration). A stale generation resolves as available
// with a final zero result: the records' pool indices and frame
@@ -1655,6 +1659,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// ever be produced, so resolve with a final 0.
return true;
}
if (query->kind == VulkanTimerQuery::Kind::Occlusion) {
Uint64 samples = 0;
if (!pVulkanRenderer->ResolveOcclusionQueryResult(query->occlusionSlots, samples)) {
return false;
}
query->occlusionSlots.clear(); // slots are recycled by the resolve
*outNanoseconds = samples;
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
@@ -1687,6 +1700,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
delete static_cast<VulkanTimerQuery*>(handle);
}
BackendQueryHandle BeginOcclusionQuery() {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::BeginOcclusionQuery called with null VulkanRenderer");
if (!pVulkanRenderer->StartOcclusionQueryCapture()) {
return nullptr;
}
auto* query = new VulkanTimerQuery{};
query->kind = VulkanTimerQuery::Kind::Occlusion;
query->rendererGeneration = GetRendererGeneration();
return query;
}
void EndOcclusionQuery(BackendQueryHandle handle) {
auto* query = static_cast<VulkanTimerQuery*>(handle);
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::EndOcclusionQuery called with null VulkanRenderer");
if (query == nullptr || query->rendererGeneration != GetRendererGeneration()) {
return;
}
pVulkanRenderer->StopOcclusionQueryCapture(query->occlusionSlots);
}
Int64 GetGpuTimestampNs() {
// Vulkan cannot synchronously sample the GPU clock: timestamps only
// exist as vkCmdWriteTimestamp results read back later, and
@@ -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 BeginOcclusionQuery();
void EndOcclusionQuery(BackendQueryHandle query);
void EndTimeElapsedQuery(BackendQueryHandle query);
BackendQueryHandle QueryCounterTimestamp();
Bool IsQueryResultAvailable(BackendQueryHandle query);
@@ -2782,6 +2782,10 @@ void main() {
}
m_vertexInputStateFactory.reset();
m_xfbCounterBuffer.Destroy();
if (m_occlusionQueryPool != VK_NULL_HANDLE) {
vkDestroyQueryPool(m_device, m_occlusionQueryPool, nullptr);
m_occlusionQueryPool = VK_NULL_HANDLE;
}
m_bufferManager.Shutdown();
// Device is idle (vkDeviceWaitIdle above); query pools can be destroyed.
@@ -4059,6 +4063,23 @@ void main() {
auto colorLogicOpEnabled =
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ColorLogicOp) && m_logicOpFeatureEnabled;
auto stencilTestEnabled = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::StencilTest);
// A framebuffer without a depth (stencil) attachment behaves as if the depth
// (stencil) test always passes and nothing is written - even when the bound
// image is a packed depth-stencil texture attached through only one half.
{
const auto& gatingFbo =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
if (gatingFbo != nullptr && !gatingFbo->IsDefaultFramebuffer()) {
const auto& depthAtt = gatingFbo->GetAttachment(MobileGL::FramebufferAttachmentType::Depth);
const auto& stencilAtt = gatingFbo->GetAttachment(MobileGL::FramebufferAttachmentType::Stencil);
if (!depthAtt.IsValid() || depthAtt.IsEmpty()) {
depthTestEnabled = false;
}
if (!stencilAtt.IsValid() || stencilAtt.IsEmpty()) {
stencilTestEnabled = false;
}
}
}
const StencilFaceState& frontStencil = MG_State::pGLContext->GetStencilState(StencilFace::Front);
const StencilFaceState& backStencil = MG_State::pGLContext->GetStencilState(StencilFace::Back);
const VkPolygonMode requestedPolygonMode =
@@ -7767,14 +7788,95 @@ void main() {
VkCommandBuffer& commandBuffer = frame.commandBuffer;
const Bool xfbActive = BeginXfbCaptureForDraw(frame);
const Bool occlusionActive = BeginOcclusionForDraw(commandBuffer);
vkCmdDraw(commandBuffer,
payload.params.vertexCount,
payload.params.instanceCount,
payload.params.firstVertex,
payload.params.firstInstance);
EndOcclusionForDraw(commandBuffer, occlusionActive);
EndXfbCaptureForDraw(frame, xfbActive);
}
Bool VulkanRenderer::StartOcclusionQueryCapture() {
if (!m_hostQueryResetEnabled || s_vkResetQueryPool == nullptr) {
return false;
}
if (m_occlusionQueryPool == VK_NULL_HANDLE) {
VkQueryPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO;
poolInfo.queryType = VK_QUERY_TYPE_OCCLUSION;
poolInfo.queryCount = kOcclusionQuerySlots;
if (vkCreateQueryPool(m_device, &poolInfo, nullptr, &m_occlusionQueryPool) != VK_SUCCESS) {
MGLOG_E("StartOcclusionQueryCapture: vkCreateQueryPool failed");
m_occlusionQueryPool = VK_NULL_HANDLE;
return false;
}
s_vkResetQueryPool(m_device, m_occlusionQueryPool, 0, kOcclusionQuerySlots);
}
m_occlusionActiveSlots.clear();
m_occlusionCaptureActive = true;
return true;
}
void VulkanRenderer::StopOcclusionQueryCapture(Vector<Uint32>& outSlots) {
outSlots = Move(m_occlusionActiveSlots);
m_occlusionActiveSlots.clear();
m_occlusionCaptureActive = false;
}
Bool VulkanRenderer::ResolveOcclusionQueryResult(const Vector<Uint32>& slots, Uint64& outSamples) {
outSamples = 0;
if (slots.empty()) {
return true;
}
if (m_occlusionQueryPool == 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 value = 0;
const VkResult result =
vkGetQueryPoolResults(m_device, m_occlusionQueryPool, slot, 1, sizeof(value), &value, sizeof(value),
VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT);
if (result == VK_SUCCESS) {
outSamples += value;
}
s_vkResetQueryPool(m_device, m_occlusionQueryPool, slot, 1);
}
return true;
}
Bool VulkanRenderer::BeginOcclusionForDraw(VkCommandBuffer commandBuffer) {
if (!m_occlusionCaptureActive || m_occlusionQueryPool == VK_NULL_HANDLE) {
return false;
}
const Uint32 slot = m_occlusionSlotCursor;
m_occlusionSlotCursor = (m_occlusionSlotCursor + 1) % kOcclusionQuerySlots;
// Slots recycle after their read; a wrapped-past unread slot is stale, so
// reset it here (host reset - the slot's prior GPU use has long retired).
s_vkResetQueryPool(m_device, m_occlusionQueryPool, slot, 1);
vkCmdBeginQuery(commandBuffer, m_occlusionQueryPool, slot,
m_occlusionQueryPreciseEnabled ? VK_QUERY_CONTROL_PRECISE_BIT : 0);
m_occlusionActiveSlots.push_back(slot);
return true;
}
void VulkanRenderer::EndOcclusionForDraw(VkCommandBuffer commandBuffer, Bool began) {
if (!began) {
return;
}
vkCmdEndQuery(commandBuffer, m_occlusionQueryPool, m_occlusionActiveSlots.back());
}
void VulkanRenderer::DrawElements(const DrawIndexedCmd& payload) {
auto& frame = m_frameContext.GetCurrent();
@@ -7800,12 +7902,14 @@ void main() {
VkCommandBuffer& commandBuffer = frame.commandBuffer;
const Bool xfbActive = BeginXfbCaptureForDraw(frame);
const Bool occlusionActive = BeginOcclusionForDraw(commandBuffer);
vkCmdDrawIndexed(commandBuffer,
payload.params.indexCount,
payload.params.instanceCount,
payload.params.firstIndex,
payload.params.vertexOffset,
payload.params.firstInstance);
EndOcclusionForDraw(commandBuffer, occlusionActive);
EndXfbCaptureForDraw(frame, xfbActive);
}
@@ -9163,6 +9267,10 @@ void main() {
// to isotropic filtering (and the extension goes unadvertised) when the device lacks it.
deviceFeatures.samplerAnisotropy = supportedDeviceFeatures.samplerAnisotropy;
m_samplerAnisotropyFeatureEnabled = deviceFeatures.samplerAnisotropy == VK_TRUE;
// GL_SAMPLES_PASSED needs exact sample counts; without the feature the boolean
// occlusion result still satisfies any-samples-style consumers.
deviceFeatures.occlusionQueryPrecise = supportedDeviceFeatures.occlusionQueryPrecise;
m_occlusionQueryPreciseEnabled = deviceFeatures.occlusionQueryPrecise == VK_TRUE;
VkDeviceCreateInfo deviceCreateInfo{};
deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
@@ -9309,6 +9417,27 @@ void main() {
MGLOG_W("VK_EXT_transform_feedback is unavailable; transform feedback capture will not work");
}
// Host query reset lets the occlusion-query ring recycle slots without a
// command-buffer round trip.
m_hostQueryResetEnabled = false;
VkPhysicalDeviceHostQueryResetFeatures hostQueryResetFeatures{};
hostQueryResetFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_HOST_QUERY_RESET_FEATURES;
if (IsExtensionSupported(availableExtensions, VK_EXT_HOST_QUERY_RESET_EXTENSION_NAME) &&
getPhysicalDeviceFeatures2 != nullptr) {
VkPhysicalDeviceFeatures2 featureQuery{};
featureQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
featureQuery.pNext = &hostQueryResetFeatures;
getPhysicalDeviceFeatures2(m_physicalDevice.handle, &featureQuery);
if (hostQueryResetFeatures.hostQueryReset == VK_TRUE) {
if (!IsExtensionAlreadyEnabled(enabledDeviceExtensions, VK_EXT_HOST_QUERY_RESET_EXTENSION_NAME)) {
enabledDeviceExtensions.push_back(VK_EXT_HOST_QUERY_RESET_EXTENSION_NAME);
}
hostQueryResetFeatures.pNext = const_cast<void*>(deviceCreateInfo.pNext);
deviceCreateInfo.pNext = &hostQueryResetFeatures;
m_hostQueryResetEnabled = true;
}
}
deviceCreateInfo.enabledExtensionCount = static_cast<Uint32>(enabledDeviceExtensions.size());
deviceCreateInfo.ppEnabledExtensionNames = enabledDeviceExtensions.data();
MGLOG_I("Device feature support: robustBufferAccess=%s geometryShader=%s independentBlend=%s logicOp=%s shaderClipDistance=%s "
@@ -9378,6 +9507,17 @@ void main() {
m_transformFeedbackFeatureEnabled = false;
}
}
if (m_hostQueryResetEnabled) {
s_vkResetQueryPool =
reinterpret_cast<PFN_vkResetQueryPool>(vkGetDeviceProcAddr(m_device, "vkResetQueryPool"));
if (s_vkResetQueryPool == nullptr) {
s_vkResetQueryPool =
reinterpret_cast<PFN_vkResetQueryPool>(vkGetDeviceProcAddr(m_device, "vkResetQueryPoolEXT"));
}
if (s_vkResetQueryPool == nullptr) {
m_hostQueryResetEnabled = false;
}
}
MGLOG_I("index type uint8 enabled: %s", m_indexTypeUint8ExtensionEnabled ? "true" : "false");
MGLOG_I("Logical device created.");
@@ -296,6 +296,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkTimerQueryManager::TimestampRecord& end) const;
Uint64 GetTimerQueryTimestampNs(const VkTimerQueryManager::TimestampRecord& record) const;
// GL_SAMPLES_PASSED occlusion queries: every app draw between Start and Stop is
// wrapped in a Vulkan occlusion query slot; the result is the slot sum. Requires
// hostQueryReset for slot recycling - Start fails (frontend keeps the query
// unsupported) when the device lacks it.
Bool StartOcclusionQueryCapture();
void StopOcclusionQueryCapture(Vector<Uint32>& outSlots);
// Flushes pending commands, waits, sums the slots, and recycles them.
Bool ResolveOcclusionQueryResult(const Vector<Uint32>& slots, Uint64& outSamples);
void RequestSwapchainResize(Uint32 width, Uint32 height);
// Re-query the surface and report whether the live swapchain no longer matches it
// (size or orientation). This - not a VK_SUBOPTIMAL_KHR result - is what decides a
@@ -482,6 +491,18 @@ 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);
// 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);
void EndOcclusionForDraw(VkCommandBuffer commandBuffer, Bool began);
Bool m_occlusionQueryPreciseEnabled = false;
Bool m_hostQueryResetEnabled = false;
PFN_vkResetQueryPool s_vkResetQueryPool = nullptr;
VkQueryPool m_occlusionQueryPool = VK_NULL_HANDLE;
static constexpr Uint32 kOcclusionQuerySlots = 8192;
Uint32 m_occlusionSlotCursor = 0;
Bool m_occlusionCaptureActive = false;
Vector<Uint32> m_occlusionActiveSlots;
VkCommandPool m_commandPool = VK_NULL_HANDLE;