mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[Feat] (Query): accept the ARB_pipeline_statistics_query targets and report zero counter bits for them
This commit is contained in:
@@ -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<GLenum, GLuint> 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<std::mutex> 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<GLint>(g_activePrimitivesGeneratedQueryId);
|
||||
break;
|
||||
default:
|
||||
*params = 0;
|
||||
if (IsPipelineStatisticsQueryTarget(target)) {
|
||||
const auto it = g_activePipelineStatisticsQueryIds.find(target);
|
||||
*params = it != g_activePipelineStatisticsQueryIds.end() ? static_cast<GLint>(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;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <ios>
|
||||
|
||||
#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<GLuint>(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<GLuint>(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<GLuint>(current), first);
|
||||
MG_Impl::GLImpl::GetQueryiv(GL_PRIMITIVES_SUBMITTED, GL_CURRENT_QUERY, ¤t);
|
||||
EXPECT_EQ(static_cast<GLuint>(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<GLuint>(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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user