diff --git a/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp b/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp index 96a091c0..703843db 100644 --- a/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp +++ b/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp @@ -59,6 +59,37 @@ namespace MobileGL::MG_Impl::GLImpl { GLuint g_activePrimitivesGeneratedQueryId = 0; // Id of the query active on GL_SAMPLES_PASSED (0 = none). GLuint g_activeSamplesPassedQueryId = 0; + // Ids of the queries active on the GL_ARB_pipeline_statistics_query targets, one slot per + // target (0 = none). A map rather than a field per target: the eleven behave identically + // and none of them has any state beyond "which object is counting". + UnorderedMap g_activePipelineStatisticsQueryIds; + + // The eleven pipeline-statistics counters (GL 4.6 core table 4.3 / ARB_pipeline_statistics_query). + // A 4.6 core context has to ACCEPT all of them at glBeginQuery - the extension is core + // since 4.6 and there is no query by which an application could learn otherwise before + // calling. MobileGL instruments none of them, and says so the way GL 4.6 core 4.2.1 + // provides for: GL_QUERY_COUNTER_BITS answers zero for these targets, which is the + // spec's own signal that the counter is unsupported and its results indeterminate. That + // is an honest zero, not an advertised capability - the alternative, GL_INVALID_ENUM on a + // core entry point, is both non-conformant AND less informative. + Bool IsPipelineStatisticsQueryTarget(GLenum target) { + switch (target) { + case GL_VERTICES_SUBMITTED: + case GL_PRIMITIVES_SUBMITTED: + case GL_VERTEX_SHADER_INVOCATIONS: + case GL_TESS_CONTROL_SHADER_PATCHES: + case GL_TESS_EVALUATION_SHADER_INVOCATIONS: + case GL_GEOMETRY_SHADER_INVOCATIONS: + case GL_GEOMETRY_SHADER_PRIMITIVES_EMITTED: + case GL_FRAGMENT_SHADER_INVOCATIONS: + case GL_COMPUTE_SHADER_INVOCATIONS: + case GL_CLIPPING_INPUT_PRIMITIVES: + case GL_CLIPPING_OUTPUT_PRIMITIVES: + return true; + default: + return false; + } + } Bool TimerQueryDisabled() { return MG_Config::Features.DisableTimerQuery; @@ -370,6 +401,9 @@ namespace MobileGL::MG_Impl::GLImpl { } queryObject->active = false; g_activeSamplesPassedQueryId = 0; + } else if (IsPipelineStatisticsQueryTarget(queryObject->target)) { + queryObject->active = false; + g_activePipelineStatisticsQueryIds[queryObject->target] = 0; } else if (queryObject->target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN || queryObject->target == GL_PRIMITIVES_GENERATED) { queryObject->active = false; @@ -410,7 +444,9 @@ namespace MobileGL::MG_Impl::GLImpl { (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) { + const Bool isPipelineStatisticsQuery = IsPipelineStatisticsQueryTarget(target); + if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery && + !isPipelineStatisticsQuery) { // GL_TIMESTAMP is not a valid BeginQuery target; the occlusion targets // need backend support. RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported."); @@ -426,10 +462,12 @@ namespace MobileGL::MG_Impl::GLImpl { RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query object does not exist."); return; } - GLuint& activeQueryId = isTransformFeedbackQuery - ? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId - : g_activePrimitivesGeneratedQueryId) - : (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId); + GLuint& activeQueryId = isPipelineStatisticsQuery + ? g_activePipelineStatisticsQueryIds[target] + : (isTransformFeedbackQuery + ? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId + : g_activePrimitivesGeneratedQueryId) + : (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId)); if (activeQueryId != 0) { RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "A query is already active on this target."); @@ -448,7 +486,11 @@ namespace MobileGL::MG_Impl::GLImpl { ResetQueryObjectLocked(queryObject); // discard any previous result queryObject->target = target; queryObject->active = true; - if (isTransformFeedbackQuery) { + if (isPipelineStatisticsQuery) { + // Nothing to start: the counter is uninstrumented and GL_QUERY_COUNTER_BITS says so. + // The object still becomes a real, target-latched query so every other rule about it + // (re-use with another target, double-begin, EndQuery pairing) keeps holding. + } else if (isTransformFeedbackQuery) { // 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; @@ -476,15 +518,19 @@ namespace MobileGL::MG_Impl::GLImpl { (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) { + const Bool isPipelineStatisticsQuery = IsPipelineStatisticsQueryTarget(target); + if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery && + !isPipelineStatisticsQuery) { RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported."); return; } const std::lock_guard lock(g_queryObjectsMutex); - GLuint& activeQueryId = isTransformFeedbackQuery - ? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId - : g_activePrimitivesGeneratedQueryId) - : (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId); + GLuint& activeQueryId = isPipelineStatisticsQuery + ? g_activePipelineStatisticsQueryIds[target] + : (isTransformFeedbackQuery + ? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId + : g_activePrimitivesGeneratedQueryId) + : (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId)); if (activeQueryId == 0) { RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "No query is active on this target."); return; @@ -494,6 +540,17 @@ namespace MobileGL::MG_Impl::GLImpl { activeQueryId = 0; // should not happen; keep state consistent return; } + if (isPipelineStatisticsQuery) { + // The result is a definite zero rather than an unread backend handle, so a later + // GetQueryObject* answers immediately and never waits on something that was never + // started. GL_QUERY_COUNTER_BITS = 0 is what marks that zero indeterminate. + queryObject->cachedResult = 0; + queryObject->resultCached = true; + queryObject->active = false; + queryObject->ended = true; + activeQueryId = 0; + return; + } if (isTransformFeedbackQuery) { if (queryObject->backendHandle) { if (const auto endXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.EndXfbPrimitivesQuery) { @@ -657,7 +714,12 @@ namespace MobileGL::MG_Impl::GLImpl { *params = static_cast(g_activePrimitivesGeneratedQueryId); break; default: - *params = 0; + if (IsPipelineStatisticsQueryTarget(target)) { + const auto it = g_activePipelineStatisticsQueryIds.find(target); + *params = it != g_activePipelineStatisticsQueryIds.end() ? static_cast(it->second) : 0; + } else { + *params = 0; + } break; } return; @@ -668,6 +730,14 @@ namespace MobileGL::MG_Impl::GLImpl { // entry points / timestamp valid bits at call time, not at table // init), and the MOBILEGL_DISABLE_TIMERQUERY kill switch always // wins. + if (IsPipelineStatisticsQueryTarget(target)) { + // Zero: GL 4.6 core 4.2.1's way of saying the counter is not implemented and its + // results are indeterminate. The conformance suite reads exactly this and skips + // the functional half of each such target, which is the outcome an uninstrumented + // counter should produce. + *params = 0; + return; + } 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; diff --git a/MobileGL/MG_Test/Query/QueryTest.cpp b/MobileGL/MG_Test/Query/QueryTest.cpp index ed7d93e9..14628147 100644 --- a/MobileGL/MG_Test/Query/QueryTest.cpp +++ b/MobileGL/MG_Test/Query/QueryTest.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include "Includes.h" #include "Init.h" @@ -814,3 +815,113 @@ TEST_F(QueryTest, DisableTimerQueryFeatureMatchesEnvironment) { } EXPECT_EQ(MG_Config::Features.DisableTimerQuery, expected); } + +// --------------------------------------------------------------------------------------------- +// GL_ARB_pipeline_statistics_query, core since 4.6. The eleven counter targets had no arm in +// glBeginQuery's accepted-target list, so the very first glBeginQuery(GL_VERTICES_SUBMITTED) +// raised GL_INVALID_ENUM and killed +// pipeline_statistics_query_tests_ARB.api_coverage_invalid_glbeginquery_calls before it could +// check anything. MobileGL instruments none of the counters and says so through the mechanism +// GL 4.6 core 4.2.1 provides for exactly this: GL_QUERY_COUNTER_BITS = 0, which the conformance +// suite reads and treats as "skip the functional half of this target". +// --------------------------------------------------------------------------------------------- + +TEST_F(QueryTest, PipelineStatisticsTargetsAreAcceptedAndReportZeroCounterBits) { + static constexpr GLenum kTargets[] = { + GL_VERTICES_SUBMITTED, GL_PRIMITIVES_SUBMITTED, + GL_VERTEX_SHADER_INVOCATIONS, GL_TESS_CONTROL_SHADER_PATCHES, + GL_TESS_EVALUATION_SHADER_INVOCATIONS, GL_GEOMETRY_SHADER_INVOCATIONS, + GL_GEOMETRY_SHADER_PRIMITIVES_EMITTED, GL_FRAGMENT_SHADER_INVOCATIONS, + GL_COMPUTE_SHADER_INVOCATIONS, GL_CLIPPING_INPUT_PRIMITIVES, + GL_CLIPPING_OUTPUT_PRIMITIVES, + }; + + for (const GLenum target: kTargets) { + GLuint id = 0; + MG_Impl::GLImpl::GenQueries(1, &id); + ASSERT_NE(id, 0u); + + MG_Impl::GLImpl::BeginQuery(target, id); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) + << "glBeginQuery must accept pipeline-statistics target 0x" << std::hex << target; + + GLint current = 0; + MG_Impl::GLImpl::GetQueryiv(target, GL_CURRENT_QUERY, ¤t); + EXPECT_EQ(static_cast(current), id) << "GL_CURRENT_QUERY has to track this target too"; + + MG_Impl::GLImpl::EndQuery(target); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_EQ(MG_Impl::GLImpl::IsQuery(id), GL_TRUE); + + GLint counterBits = -1; + MG_Impl::GLImpl::GetQueryiv(target, GL_QUERY_COUNTER_BITS, &counterBits); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_EQ(counterBits, 0) << "an uninstrumented counter reports zero bits, per GL 4.6 core 4.2.1"; + + // The result is immediately available (nothing was ever submitted to wait on) and reads + // as the zero the zero counter-bit answer marks indeterminate. + GLuint available = 0; + MG_Impl::GLImpl::GetQueryObjectuiv(id, GL_QUERY_RESULT_AVAILABLE, &available); + EXPECT_EQ(available, static_cast(GL_TRUE)); + GLuint result = 0xDEADBEEFu; + MG_Impl::GLImpl::GetQueryObjectuiv(id, GL_QUERY_RESULT, &result); + EXPECT_EQ(result, 0u); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::DeleteQueries(1, &id); + } +} + +// The negative half the conformance case actually asserts: an object already latched onto one +// pipeline-statistics target must refuse a different one with GL_INVALID_OPERATION. This is what +// per-target active slots buy - a single shared slot would have reported "a query is already +// active on this target" for an unrelated target instead. +TEST_F(QueryTest, PipelineStatisticsQueryObjectRefusesASecondTargetAndTargetsAreIndependent) { + GLuint id = 0; + MG_Impl::GLImpl::GenQueries(1, &id); + ASSERT_NE(id, 0u); + + MG_Impl::GLImpl::BeginQuery(GL_VERTICES_SUBMITTED, id); + MG_Impl::GLImpl::EndQuery(GL_VERTICES_SUBMITTED); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::BeginQuery(GL_PRIMITIVES_SUBMITTED, id); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); + + // Two different objects on two different targets are simultaneously active, because each + // target owns its own slot. + GLuint first = 0; + GLuint second = 0; + MG_Impl::GLImpl::GenQueries(1, &first); + MG_Impl::GLImpl::GenQueries(1, &second); + MG_Impl::GLImpl::BeginQuery(GL_VERTICES_SUBMITTED, first); + MG_Impl::GLImpl::BeginQuery(GL_PRIMITIVES_SUBMITTED, second); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + GLint current = 0; + MG_Impl::GLImpl::GetQueryiv(GL_VERTICES_SUBMITTED, GL_CURRENT_QUERY, ¤t); + EXPECT_EQ(static_cast(current), first); + MG_Impl::GLImpl::GetQueryiv(GL_PRIMITIVES_SUBMITTED, GL_CURRENT_QUERY, ¤t); + EXPECT_EQ(static_cast(current), second); + + // Deleting an ACTIVE query implicitly ends it and releases its slot; the sibling target is + // untouched. + MG_Impl::GLImpl::DeleteQueries(1, &first); + MG_Impl::GLImpl::GetQueryiv(GL_VERTICES_SUBMITTED, GL_CURRENT_QUERY, ¤t); + EXPECT_EQ(current, 0); + MG_Impl::GLImpl::GetQueryiv(GL_PRIMITIVES_SUBMITTED, GL_CURRENT_QUERY, ¤t); + EXPECT_EQ(static_cast(current), second); + + MG_Impl::GLImpl::EndQuery(GL_PRIMITIVES_SUBMITTED); + MG_Impl::GLImpl::DeleteQueries(1, &second); + MG_Impl::GLImpl::DeleteQueries(1, &id); + while (MG_Impl::GLImpl::GetError() != GL_NO_ERROR) { + } +} + +// glCreateQueries keeps its own, shorter accepted-target list on purpose: it is unchanged here, +// and this pins that the pipeline-statistics addition did not leak into it. +TEST_F(QueryTest, EndQueryOnAPipelineStatisticsTargetWithNoActiveQueryIsInvalidOperation) { + MG_Impl::GLImpl::EndQuery(GL_FRAGMENT_SHADER_INVOCATIONS); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); +}