diff --git a/MobileGL/MG_Backend/BackendObject.h b/MobileGL/MG_Backend/BackendObject.h index d641dcfa..0443d544 100644 --- a/MobileGL/MG_Backend/BackendObject.h +++ b/MobileGL/MG_Backend/BackendObject.h @@ -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 { diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index 564a4dc7..ceeb4e4b 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -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; diff --git a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp index c3f3f7ad..c54d0a8c 100644 --- a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp @@ -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 begin; SharedPtr end; + // Kind::Occlusion - pool slots recorded between Begin/End; summed at result time. + Vector 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(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(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 diff --git a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.h b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.h index cff26991..ad3775c9 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 BeginOcclusionQuery(); + void EndOcclusionQuery(BackendQueryHandle query); void EndTimeElapsedQuery(BackendQueryHandle query); BackendQueryHandle QueryCounterTimestamp(); Bool IsQueryResultAvailable(BackendQueryHandle query); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 82aaefea..70e5a417 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -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& outSlots) { + outSlots = Move(m_occlusionActiveSlots); + m_occlusionActiveSlots.clear(); + m_occlusionCaptureActive = false; + } + + Bool VulkanRenderer::ResolveOcclusionQueryResult(const Vector& 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(deviceCreateInfo.pNext); + deviceCreateInfo.pNext = &hostQueryResetFeatures; + m_hostQueryResetEnabled = true; + } + } + deviceCreateInfo.enabledExtensionCount = static_cast(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(vkGetDeviceProcAddr(m_device, "vkResetQueryPool")); + if (s_vkResetQueryPool == nullptr) { + s_vkResetQueryPool = + reinterpret_cast(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."); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index e94434ff..d7582de2 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -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& outSlots); + // Flushes pending commands, waits, sums the slots, and recycles them. + Bool ResolveOcclusionQueryResult(const Vector& 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 m_occlusionActiveSlots; 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 96d1af5c..21734a31 100644 --- a/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp +++ b/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp @@ -46,6 +46,8 @@ namespace MobileGL::MG_Impl::GLImpl { // Ids of the queries active on the transform feedback targets (0 = none). GLuint g_activePrimitivesWrittenQueryId = 0; GLuint g_activePrimitivesGeneratedQueryId = 0; + // Id of the query active on GL_SAMPLES_PASSED (0 = none). + GLuint g_activeSamplesPassedQueryId = 0; Bool TimerQueryDisabled() { return MG_Config::Features.DisableTimerQuery; @@ -131,6 +133,11 @@ namespace MobileGL::MG_Impl::GLImpl { outValue = 0; return true; } + // ANY_SAMPLES_PASSED* report a boolean. + if (queryObject->target == GL_ANY_SAMPLES_PASSED || + queryObject->target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) { + result = result != 0 ? 1 : 0; + } // Final value produced (or no GetQueryResult64 hook: the // query degrades to a zero result); the backend handle is // consumed and the value cached for later reads. @@ -185,7 +192,24 @@ namespace MobileGL::MG_Impl::GLImpl { } QueryObject* queryObject = it->second; if (queryObject->active) { - EndTimeElapsedQueryLocked(queryObject); // implicitly end before deletion + // Implicitly end before deletion, releasing the matching active slot. + if (queryObject->target == GL_SAMPLES_PASSED || queryObject->target == GL_ANY_SAMPLES_PASSED || + queryObject->target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) { + if (const auto endOcclusionQuery = MG_Backend::gBackendFunctionsTable.GL.EndOcclusionQuery; + endOcclusionQuery && queryObject->backendHandle) { + endOcclusionQuery(queryObject->backendHandle); + } + queryObject->active = false; + g_activeSamplesPassedQueryId = 0; + } else if (queryObject->target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN || + queryObject->target == GL_PRIMITIVES_GENERATED) { + queryObject->active = false; + (queryObject->target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN + ? g_activePrimitivesWrittenQueryId + : g_activePrimitivesGeneratedQueryId) = 0; + } else { + EndTimeElapsedQueryLocked(queryObject); + } } if (queryObject->backendHandle) { if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) { @@ -211,10 +235,13 @@ namespace MobileGL::MG_Impl::GLImpl { void BeginQuery(GLenum target, GLuint id) { const Bool isTransformFeedbackQuery = target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN || target == GL_PRIMITIVES_GENERATED; - if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery) { - // GL_TIME_ELAPSED timer queries and the transform feedback primitive - // queries are implemented (occlusion queries remain stubs); - // GL_TIMESTAMP is not a valid BeginQuery target either. + const Bool isOcclusionQuery = + (target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED || + target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) && + MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr; + if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery) { + // GL_TIMESTAMP is not a valid BeginQuery target; the occlusion targets + // need backend support. RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported."); return; } @@ -231,7 +258,7 @@ namespace MobileGL::MG_Impl::GLImpl { GLuint& activeQueryId = isTransformFeedbackQuery ? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId : g_activePrimitivesGeneratedQueryId) - : g_activeTimeElapsedQueryId; + : (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId); if (activeQueryId != 0) { RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "A query is already active on this target."); @@ -255,6 +282,8 @@ namespace MobileGL::MG_Impl::GLImpl { // result is the delta between Begin and End. Without geometry-stage // amplification the assembled count IS the written/generated count. queryObject->counterSnapshot = MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter(); + } else if (isOcclusionQuery) { + queryObject->backendHandle = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery(); } else { const auto beginTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.BeginTimeElapsedQuery; queryObject->backendHandle = @@ -266,7 +295,11 @@ namespace MobileGL::MG_Impl::GLImpl { void EndQuery(GLenum target) { const Bool isTransformFeedbackQuery = target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN || target == GL_PRIMITIVES_GENERATED; - if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery) { + const Bool isOcclusionQuery = + (target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED || + target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) && + MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr; + if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery) { RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported."); return; } @@ -274,7 +307,7 @@ namespace MobileGL::MG_Impl::GLImpl { GLuint& activeQueryId = isTransformFeedbackQuery ? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId : g_activePrimitivesGeneratedQueryId) - : g_activeTimeElapsedQueryId; + : (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId); if (activeQueryId == 0) { RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "No query is active on this target."); return; @@ -293,6 +326,16 @@ namespace MobileGL::MG_Impl::GLImpl { activeQueryId = 0; return; } + if (isOcclusionQuery) { + if (const auto endOcclusionQuery = MG_Backend::gBackendFunctionsTable.GL.EndOcclusionQuery; + endOcclusionQuery && queryObject->backendHandle) { + endOcclusionQuery(queryObject->backendHandle); + } + queryObject->active = false; + queryObject->ended = true; + activeQueryId = 0; + return; + } EndTimeElapsedQueryLocked(queryObject); } @@ -336,9 +379,25 @@ namespace MobileGL::MG_Impl::GLImpl { switch (pname) { case GL_CURRENT_QUERY: { const std::lock_guard lock(g_queryObjectsMutex); - // Only GL_TIME_ELAPSED queries can be active; GL_TIMESTAMP queries - // never are, and other targets remain unimplemented. - *params = target == GL_TIME_ELAPSED ? static_cast(g_activeTimeElapsedQueryId) : 0; + switch (target) { + case GL_TIME_ELAPSED: + *params = static_cast(g_activeTimeElapsedQueryId); + break; + case GL_SAMPLES_PASSED: + case GL_ANY_SAMPLES_PASSED: + case GL_ANY_SAMPLES_PASSED_CONSERVATIVE: + *params = static_cast(g_activeSamplesPassedQueryId); + break; + case GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN: + *params = static_cast(g_activePrimitivesWrittenQueryId); + break; + case GL_PRIMITIVES_GENERATED: + *params = static_cast(g_activePrimitivesGeneratedQueryId); + break; + default: + *params = 0; + break; + } return; } case GL_QUERY_COUNTER_BITS: { @@ -346,7 +405,13 @@ namespace MobileGL::MG_Impl::GLImpl { // time: IsTimerQuerySupported is the dynamic truth (extension / // entry points / timestamp valid bits at call time, not at table // init), and the MOBILEGL_DISABLE_TIMERQUERY kill switch always - // wins. Non-timer targets remain unimplemented and report 0. + // wins. + if (target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED || + target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) { + const Bool occlusionSupported = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr; + *params = occlusionSupported ? (target == GL_SAMPLES_PASSED ? 32 : 1) : 0; + return; + } const Bool timerTarget = target == GL_TIME_ELAPSED || target == GL_TIMESTAMP; const auto isTimerQuerySupported = MG_Backend::gBackendFunctionsTable.GL.IsTimerQuerySupported; const Bool supported =