Files
MobileGL/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp
T
swung0x48 a196ada4c1 [Feat] (Impl): call MGP_FILL before every GLFunctionsTable entry - 83 statements over 69 verbs, a no-op in the pull build
- Every call through gBackendFunctionsTable.GL in the seven MG_Impl TUs (Drawing 36, Framebuffer 11, Texture 8, Getter 3, Program 1, Query 18, Sync 6) is preceded by MGP_FILL(<Verb>); placed after every early return the call sits behind - the conditional-render check, the null-entry guards, the loop bodies - so a verb whose entry is null on this backend never bumps the serial.
- Seven calls sit on a continuation line of a guarded expression (GetQueryResult64 x2, BeginXfbPrimitivesQuery, BeginTimeElapsedQuery, QueryCounterTimestamp, IsTimerQuerySupported, GetSyncStatus) and two more fold the null guard into the same expression (IsQueryResultAvailable, ClientWaitSync's guarded return); there the fill precedes the statement, so a null entry bumps the serial once with nothing to read it - harmless for the poison, recorded for the record.
- Each TU includes <MG_Impl/Pipe/PipeFill.h> after its last MG_State/MG_Backend include; under MOBILEGL_PIPE_PUSH=OFF the macro is ((void)0) and the pull library is symbol-identical with a .text delta of zero.
2026-09-06 01:51:16 -04:00

941 lines
48 KiB
C++

// MobileGL - MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "GL_Query.h"
#include "../Getter/GL_Getter.h"
#include <Config.h>
#include <MG_Backend/BackendObjects.h>
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
#include <MG_Impl/Pipe/PipeFill.h>
namespace MobileGL::MG_Impl::GLImpl {
namespace {
// Frontend query object (GL_ARB_timer_query): wraps an optional backend
// timer-query handle. A null backend handle (backend has no timer-query
// support, timer queries are disabled by config, or the backend could
// not create a query at call time) keeps a graceful fallback: the query
// result is immediately available and reads as zero.
struct QueryObject {
GLuint id = 0;
GLenum target = 0; // 0 = gen'd but never used with BeginQuery/QueryCounter
// glCreateQueries makes the object outright; glGenQueries only reserves the name,
// and the object appears when the name is first used (GL 4.6 core 4.2.1).
Bool created = false;
MG_Backend::BackendQueryHandle backendHandle = nullptr;
Bool active = false;
Bool ended = false;
Bool resultCached = false;
Uint64 cachedResult = 0;
// The transform feedback primitive counter matching this query's target, at
// BeginQuery time.
Uint64 counterSnapshot = 0;
// Capture-draw counters at BeginQuery time: how many capture draws the CPU
// accounting had reproduced exactly, and how many of those it could not (a
// geometry stage amplifies). Their deltas decide whether the CPU result may
// stand in for the backend's.
Uint64 accountedCaptureDrawSnapshot = 0;
Uint64 geometryCaptureDrawSnapshot = 0;
};
// Query calls may arrive from any thread (launchers migrate the context
// across JVM threads), so the live-object registry is mutex-guarded,
// like the sync-object registry in GL_Sync.cpp. Entries left at process
// shutdown are simply dropped; their backend handles die with the
// backend.
std::mutex g_queryObjectsMutex;
UnorderedMap<GLuint, QueryObject*> g_liveQueryObjects;
// Monotonically increasing id allocator; ids are valid query objects
// immediately after GenQueries.
GLuint g_nextQueryId = 1;
// Id of the query currently active on GL_TIME_ELAPSED (0 = none).
GLuint g_activeTimeElapsedQueryId = 0;
// 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;
// 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;
// Whether MobileGL puts GL_ARB_tessellation_shader in its extension string. Read from the
// ADVERTISED list rather than from a capability bit for the same reason
// BackendSupportsTextureViews does (GL_Texture.cpp): it makes "MobileGL claims tessellation
// support" and "the tessellation-conditional API surface is open" the same fact by
// construction, so the day a backend starts advertising the string the surface below opens
// with it and no second edit is owed.
Bool AdvertisesTessellationShaderExtension() {
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
if (!activeBackendObject) return false;
const auto& extensions = activeBackendObject->GetRendererInfo().RendererGLInfo.Extensions;
return std::find(extensions.begin(), extensions.end(), E_GL_ARB_tessellation_shader) != extensions.end();
}
// The eleven pipeline-statistics counters (GL 4.6 core table 4.3 / ARB_pipeline_statistics_query).
// A 4.6 core context ACCEPTS the nine unconditional ones at glBeginQuery - 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.
//
// The two TESSELLATION targets are the exception, because ARB_pipeline_statistics_query
// makes them conditional on tessellation support rather than unconditional, and the only
// thing an application (or the conformance suite) can read to decide whether an
// implementation has it is the GL_ARB_tessellation_shader string. MobileGL does not emit it
// today, so these two answer GL_INVALID_ENUM: an API surface that accepts a
// tessellation-conditional token while withholding the string that announces the condition
// is self-contradictory, and it is the contradiction the suite catches
// (KHR-GL46.pipeline_statistics_query_tests_ARB.api_coverage_unsupported_calls, whose
// support probe is gl4cPipelineStatisticsQueryTests.cpp:1166-1176). The gate is the
// advertisement itself, not a hardcoded "no", so this is one switch and not two.
Bool IsPipelineStatisticsQueryTarget(GLenum target) {
switch (target) {
case GL_VERTICES_SUBMITTED:
case GL_PRIMITIVES_SUBMITTED:
case GL_VERTEX_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;
case GL_TESS_CONTROL_SHADER_PATCHES:
case GL_TESS_EVALUATION_SHADER_INVOCATIONS:
return AdvertisesTessellationShaderExtension();
default:
return false;
}
}
Bool TimerQueryDisabled() {
return MG_Config::Features.DisableTimerQuery;
}
void RecordQueryError(ErrorCode code, const char* function, const char* message) {
MG_State::pGLContext->RecordError(code,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", function, message));
}
// The by-buffer query getters write the result into a buffer object instead of client
// memory. Everything about the query itself - the name, whether it is still active, the
// parameter - is checked by GetQueryObjectValue; what is left is the destination, so this
// resolves the buffer and confirms the write lands inside it (GL 4.6 core 4.2.1).
Bool ResolveQueryResultDestination(GLuint buffer, GLintptr offset, SizeT writeSize, const char* function,
SharedPtr<MG_State::GLState::BufferObject>& outBuffer) {
if (offset < 0) {
RecordQueryError(ErrorCode::InvalidValue, function, "Offset cannot be negative.");
return false;
}
if (!MG_State::pGLContext->ValidateBufferObject(buffer)) {
RecordQueryError(ErrorCode::InvalidOperation, function, "Buffer object does not exist.");
return false;
}
auto bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
if (!bufferObject) {
RecordQueryError(ErrorCode::InvalidOperation, function, "Buffer object does not exist.");
return false;
}
if (static_cast<SizeT>(offset) + writeSize > bufferObject->GetSize()) {
RecordQueryError(ErrorCode::InvalidOperation, function,
"The query result does not fit in the buffer object at this offset.");
return false;
}
outBuffer = bufferObject;
return true;
}
// Callers must hold g_queryObjectsMutex.
QueryObject* FindQueryObjectLocked(GLuint id) {
const auto it = g_liveQueryObjects.find(id);
return it != g_liveQueryObjects.end() ? it->second : nullptr;
}
// Callers must hold g_queryObjectsMutex. Releases the backend handle
// (if any) and clears any cached result, so the object can be reused.
void ResetQueryObjectLocked(QueryObject* queryObject) {
if (queryObject->backendHandle) {
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
MGP_FILL(DeleteBackendQuery);
deleteBackendQuery(queryObject->backendHandle);
}
queryObject->backendHandle = nullptr;
}
queryObject->active = false;
queryObject->ended = false;
queryObject->resultCached = false;
queryObject->cachedResult = 0;
}
// Callers must hold g_queryObjectsMutex.
void EndTimeElapsedQueryLocked(QueryObject* queryObject) {
const auto endTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.EndTimeElapsedQuery;
if (endTimeElapsedQuery && queryObject->backendHandle) {
MGP_FILL(EndTimeElapsedQuery);
endTimeElapsedQuery(queryObject->backendHandle);
}
queryObject->active = false;
queryObject->ended = true;
g_activeTimeElapsedQueryId = 0;
}
// The CPU accounting counter a transform feedback query target reads: what the capture
// buffers took for GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, and everything the capture
// stage assembled - a paused span included - for GL_PRIMITIVES_GENERATED. One counter
// for both targets would report the clamped written count as the generated one.
Uint64 TransformFeedbackCounterForTarget(GLenum target) {
return target == GL_PRIMITIVES_GENERATED
? MG_State::pGLContext->GetTransformFeedbackGeneratedCounter()
: MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter();
}
// The span's CPU accounting delta. Saturating: a snapshot left above its counter (a
// context switch between Begin and End, a counter that never moved) would otherwise
// wrap to 2^64-1, which GetQueryObjectuiv hands the app as 4294967295.
Uint64 TransformFeedbackCpuResult(const QueryObject* queryObject) {
const Uint64 counter = TransformFeedbackCounterForTarget(queryObject->target);
return counter > queryObject->counterSnapshot ? counter - queryObject->counterSnapshot : 0;
}
// Whether this ended span's result should come from the CPU accounting rather than from
// the backend query it also ran. Three conditions, all necessary:
// * the backend asked for it (DirectGLES, whose ES driver counter is the unreliable
// one; DirectVulkan never sets the bit and so is untouched by any of this);
// * the target is PRIMITIVES_WRITTEN. GL_PRIMITIVES_GENERATED counts primitives
// whether or not a capture is active, and the accounting only ever sees capture
// draws, so the backend's counter is the more complete answer there;
// * the span was fully accounted: at least one capture draw reached the accounting
// (the instanced, indirect and multi-draw entry points do not call it at all, so a
// span made of those is invisible to it) and none of them amplified through a
// geometry stage, which the CPU cannot model.
Bool PrefersCpuTransformFeedbackResult(const QueryObject* queryObject) {
if (!MG_Backend::gBackendFunctionsTable.GL.PrefersCpuXfbPrimitiveAccounting) return false;
if (queryObject->target != GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN) return false;
if (MG_State::pGLContext->GetTransformFeedbackGeometryCaptureDraws() !=
queryObject->geometryCaptureDrawSnapshot) {
return false;
}
return MG_State::pGLContext->GetTransformFeedbackAccountedCaptureDraws() !=
queryObject->accountedCaptureDrawSnapshot;
}
// Shared GetQueryObject* implementation. Returns false when an error
// was recorded and no value should be written back. `outValueProduced`, when given,
// additionally distinguishes "succeeded with a value" from "succeeded but the result is not
// ready" - the GL_QUERY_RESULT_NO_WAIT case, where GL_ARB_query_buffer_object says the
// destination is left alone rather than written with a placeholder.
Bool GetQueryObjectValue(GLuint id, GLenum pname, const char* function, Uint64& outValue,
Bool* outValueProduced = nullptr) {
if (outValueProduced) *outValueProduced = true;
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
auto* queryObject = FindQueryObjectLocked(id);
if (!queryObject) {
RecordQueryError(ErrorCode::InvalidOperation, function, "Query object does not exist.");
return false;
}
if (queryObject->active) {
RecordQueryError(ErrorCode::InvalidOperation, function, "Query object is still active.");
return false;
}
switch (pname) {
case GL_QUERY_TARGET:
// The target a query was begun with (or created with, for glCreateQueries) - state
// the object has carried all along, GL 4.6 core table 23.35.
outValue = queryObject->target;
return true;
case GL_QUERY_RESULT_NO_WAIT: {
if (queryObject->resultCached) {
outValue = queryObject->cachedResult;
return true;
}
Uint64 result = 0;
const auto getQueryResult64 = MG_Backend::gBackendFunctionsTable.GL.GetQueryResult64;
MGP_FILL(GetQueryResult64);
if (queryObject->backendHandle && getQueryResult64 &&
!getQueryResult64(queryObject->backendHandle, /*wait=*/false, &result)) {
// Not ready. The whole point of the no-wait form is that the caller's
// destination keeps whatever it already held.
if (outValueProduced) *outValueProduced = false;
outValue = 0;
return true;
}
if (queryObject->target == GL_ANY_SAMPLES_PASSED ||
queryObject->target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
result = result != 0 ? 1 : 0;
}
if (queryObject->backendHandle) {
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
MGP_FILL(DeleteBackendQuery);
deleteBackendQuery(queryObject->backendHandle);
}
queryObject->backendHandle = nullptr;
}
queryObject->cachedResult = result;
queryObject->resultCached = true;
outValue = result;
return true;
}
case GL_QUERY_RESULT_AVAILABLE: {
if (queryObject->resultCached || !queryObject->backendHandle) {
outValue = 1;
return true;
}
const auto isQueryResultAvailable = MG_Backend::gBackendFunctionsTable.GL.IsQueryResultAvailable;
MGP_FILL(IsQueryResultAvailable);
outValue = (!isQueryResultAvailable || isQueryResultAvailable(queryObject->backendHandle)) ? 1 : 0;
return true;
}
case GL_QUERY_RESULT: {
if (queryObject->resultCached) {
outValue = queryObject->cachedResult;
return true;
}
Uint64 result = 0;
if (queryObject->backendHandle) {
const auto getQueryResult64 = MG_Backend::gBackendFunctionsTable.GL.GetQueryResult64;
MGP_FILL(GetQueryResult64);
if (getQueryResult64 &&
!getQueryResult64(queryObject->backendHandle, /*wait=*/true, &result)) {
// The backend could not produce the result YET (e.g. a
// Vulkan wait refusing to block on a not-yet-submitted
// frame serial). Per the documented no-stall tradeoff
// this call reads 0, but the value is NOT cached and
// the backend handle is kept, so a later AVAILABLE
// poll / RESULT read still produces the real value.
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.
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
MGP_FILL(DeleteBackendQuery);
deleteBackendQuery(queryObject->backendHandle);
}
queryObject->backendHandle = nullptr;
}
queryObject->cachedResult = result;
queryObject->resultCached = true;
outValue = result;
return true;
}
default:
RecordQueryError(ErrorCode::InvalidEnum, function, "Unsupported query object parameter.");
return false;
}
}
template <typename T>
void GetQueryBufferObject(GLuint id, GLuint buffer, GLenum pname, GLintptr offset, const char* function) {
SharedPtr<MG_State::GLState::BufferObject> bufferObject;
if (!ResolveQueryResultDestination(buffer, offset, sizeof(T), function, bufferObject)) return;
Uint64 value = 0;
Bool valueProduced = false;
if (!GetQueryObjectValue(id, pname, function, value, &valueProduced)) return;
// GL_QUERY_RESULT_NO_WAIT on a result that has not landed writes nothing at all.
if (!valueProduced) return;
const T narrowed = static_cast<T>(value);
bufferObject->UploadSubData({const_cast<T*>(&narrowed), sizeof(T)}, static_cast<SizeT>(offset));
}
} // namespace
void GenQueries(GLsizei n, GLuint* ids) {
if (n < 0) {
RecordQueryError(ErrorCode::InvalidValue, __FUNCTION__, "n cannot be negative.");
return;
}
if (!ids) {
return;
}
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
for (GLsizei i = 0; i < n; ++i) {
const GLuint id = g_nextQueryId++;
auto* queryObject = new QueryObject;
queryObject->id = id;
g_liveQueryObjects[id] = queryObject;
ids[i] = id;
}
}
// glCreateQueries differs from glGenQueries in creating the objects outright, with their
// target already fixed and the rest of their state at the defaults (GL 4.6 core 4.2.1).
void CreateQueries(GLenum target, GLsizei n, GLuint* ids) {
switch (target) {
case GL_SAMPLES_PASSED:
case GL_ANY_SAMPLES_PASSED:
case GL_ANY_SAMPLES_PASSED_CONSERVATIVE:
case GL_TIME_ELAPSED:
case GL_TIMESTAMP:
case GL_PRIMITIVES_GENERATED:
case GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN:
break;
default:
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not accepted.");
return;
}
if (n < 0) {
RecordQueryError(ErrorCode::InvalidValue, __FUNCTION__, "n cannot be negative.");
return;
}
if (!ids) {
return;
}
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
for (GLsizei i = 0; i < n; ++i) {
const GLuint id = g_nextQueryId++;
auto* queryObject = new QueryObject;
queryObject->id = id;
queryObject->target = target;
queryObject->created = true;
g_liveQueryObjects[id] = queryObject;
ids[i] = id;
}
}
void DeleteQueries(GLsizei n, const GLuint* ids) {
if (n < 0) {
RecordQueryError(ErrorCode::InvalidValue, __FUNCTION__, "n cannot be negative.");
return;
}
if (!ids) {
return;
}
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
for (GLsizei i = 0; i < n; ++i) {
const auto it = g_liveQueryObjects.find(ids[i]);
if (it == g_liveQueryObjects.end()) {
continue; // unknown ids are silently ignored
}
QueryObject* queryObject = it->second;
if (queryObject->active) {
// 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) {
MGP_FILL(EndOcclusionQuery);
endOcclusionQuery(queryObject->backendHandle);
}
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;
(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) {
MGP_FILL(DeleteBackendQuery);
deleteBackendQuery(queryObject->backendHandle);
}
queryObject->backendHandle = nullptr;
}
g_liveQueryObjects.erase(it);
delete queryObject;
}
}
GLboolean IsQuery(GLuint id) {
if (id == 0) {
return GL_FALSE;
}
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
// A name from glGenQueries is not yet a query object: it becomes one when it is first
// used with BeginQuery/QueryCounter (which is what a non-zero target records), or
// immediately if it came from glCreateQueries.
const auto* queryObject = FindQueryObjectLocked(id);
return (queryObject != nullptr && (queryObject->created || queryObject->target != 0)) ? GL_TRUE : GL_FALSE;
}
void BeginQuery(GLenum target, GLuint id) {
const Bool isTransformFeedbackQuery =
target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN || target == GL_PRIMITIVES_GENERATED;
const Bool isOcclusionQuery =
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
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.");
return;
}
if (id == 0) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query id 0 cannot be used.");
return;
}
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
auto* queryObject = FindQueryObjectLocked(id);
if (!queryObject) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query object does not exist.");
return;
}
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.");
return;
}
if (queryObject->active) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query object is already active.");
return;
}
if (queryObject->target != 0 && queryObject->target != target) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__,
"Query object was already used with a different target.");
return;
}
ResetQueryObjectLocked(queryObject); // discard any previous result
queryObject->target = target;
queryObject->active = true;
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;
MGP_FILL(BeginXfbPrimitivesQuery);
queryObject->backendHandle =
beginXfbPrimitivesQuery ? beginXfbPrimitivesQuery(target == GL_PRIMITIVES_GENERATED) : nullptr;
queryObject->counterSnapshot = TransformFeedbackCounterForTarget(target);
queryObject->accountedCaptureDrawSnapshot =
MG_State::pGLContext->GetTransformFeedbackAccountedCaptureDraws();
queryObject->geometryCaptureDrawSnapshot =
MG_State::pGLContext->GetTransformFeedbackGeometryCaptureDraws();
} else if (isOcclusionQuery) {
MGP_FILL(BeginOcclusionQuery);
queryObject->backendHandle = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery();
} else {
const auto beginTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.BeginTimeElapsedQuery;
MGP_FILL(BeginTimeElapsedQuery);
queryObject->backendHandle =
(!TimerQueryDisabled() && beginTimeElapsedQuery) ? beginTimeElapsedQuery() : nullptr;
}
activeQueryId = id;
}
void EndQuery(GLenum target) {
const Bool isTransformFeedbackQuery =
target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN || target == GL_PRIMITIVES_GENERATED;
const Bool isOcclusionQuery =
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
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 = 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;
}
auto* queryObject = FindQueryObjectLocked(activeQueryId);
if (!queryObject) {
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) {
MGP_FILL(EndXfbPrimitivesQuery);
endXfbPrimitivesQuery(queryObject->backendHandle);
}
}
// A backend query that is not going to be read is released here, not left to be
// collected later: the span is over, the driver object has nothing left to say.
// Ending it first is what makes that legal.
if (!queryObject->backendHandle || PrefersCpuTransformFeedbackResult(queryObject)) {
if (queryObject->backendHandle) {
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
MGP_FILL(DeleteBackendQuery);
deleteBackendQuery(queryObject->backendHandle);
}
queryObject->backendHandle = nullptr;
}
queryObject->cachedResult = TransformFeedbackCpuResult(queryObject);
queryObject->resultCached = true;
}
// Otherwise the result comes from the GPU query at read time.
queryObject->active = false;
queryObject->ended = true;
activeQueryId = 0;
return;
}
if (isOcclusionQuery) {
if (const auto endOcclusionQuery = MG_Backend::gBackendFunctionsTable.GL.EndOcclusionQuery;
endOcclusionQuery && queryObject->backendHandle) {
MGP_FILL(EndOcclusionQuery);
endOcclusionQuery(queryObject->backendHandle);
}
queryObject->active = false;
queryObject->ended = true;
activeQueryId = 0;
return;
}
EndTimeElapsedQueryLocked(queryObject);
}
void QueryCounter(GLuint id, GLenum target) {
if (target != GL_TIMESTAMP) {
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "QueryCounter target must be GL_TIMESTAMP.");
return;
}
if (id == 0) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query id 0 cannot be used.");
return;
}
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
auto* queryObject = FindQueryObjectLocked(id);
if (!queryObject) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query object does not exist.");
return;
}
if (queryObject->active) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query object is currently active.");
return;
}
if (queryObject->target != 0 && queryObject->target != target) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__,
"Query object was already used with a different target.");
return;
}
ResetQueryObjectLocked(queryObject); // discard any previous result
queryObject->target = target;
const auto queryCounterTimestamp = MG_Backend::gBackendFunctionsTable.GL.QueryCounterTimestamp;
MGP_FILL(QueryCounterTimestamp);
queryObject->backendHandle =
(!TimerQueryDisabled() && queryCounterTimestamp) ? queryCounterTimestamp() : nullptr;
queryObject->ended = true;
}
void BeginConditionalRender(GLuint id, GLenum mode) {
// GL 4.6 core 10.9's eight modes. The _INVERTED half flips the sense of the predicate;
// the BY_REGION half only narrows WHERE an implementation is permitted to discard, so
// treating it as its whole-framebuffer sibling is what an implementation without region
// granularity does. The _NO_WAIT half is a permission to render rather than stall, not an
// obligation - see the resolve below.
Bool inverted = false;
switch (mode) {
case GL_QUERY_WAIT:
case GL_QUERY_NO_WAIT:
case GL_QUERY_BY_REGION_WAIT:
case GL_QUERY_BY_REGION_NO_WAIT:
inverted = false;
break;
case GL_QUERY_WAIT_INVERTED:
case GL_QUERY_NO_WAIT_INVERTED:
case GL_QUERY_BY_REGION_WAIT_INVERTED:
case GL_QUERY_BY_REGION_NO_WAIT_INVERTED:
inverted = true;
break;
default:
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "mode is not a conditional render mode.");
return;
}
if (MG_State::pGLContext->IsConditionalRenderActive()) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Conditional rendering is already active.");
return;
}
{
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
const auto* queryObject = FindQueryObjectLocked(id);
// A generated NAME is not yet a query object; it becomes one at its first use with a
// target (the same rule glIsQuery answers by).
if (!queryObject || (!queryObject->created && queryObject->target == 0)) {
RecordQueryError(ErrorCode::InvalidValue, __FUNCTION__, "id is not the name of a query object.");
return;
}
if (queryObject->active) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "The query object is still active.");
return;
}
if (queryObject->target != GL_SAMPLES_PASSED && queryObject->target != GL_ANY_SAMPLES_PASSED &&
queryObject->target != GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__,
"Conditional rendering requires an occlusion query object.");
return;
}
}
// Resolved ONCE, here, and by WAITING even for the _NO_WAIT modes: the spec lets those
// render instead of stalling, so always waiting is conforming and is the only choice that
// gives the whole block one deterministic verdict. Reading it per command instead would
// let a result that lands mid-block change the answer half way through.
Uint64 samplesPassed = 0;
if (!GetQueryObjectValue(id, GL_QUERY_RESULT, __FUNCTION__, samplesPassed)) return;
const Bool passed = samplesPassed != 0;
MG_State::pGLContext->BeginConditionalRender(id, mode, inverted ? passed : !passed);
}
void EndConditionalRender() {
if (!MG_State::pGLContext->IsConditionalRenderActive()) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Conditional rendering is not active.");
return;
}
MG_State::pGLContext->EndConditionalRender();
}
void GetQueryiv(GLenum target, GLenum pname, GLint* params) {
if (!params) {
return;
}
switch (pname) {
case GL_CURRENT_QUERY: {
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
switch (target) {
case GL_TIME_ELAPSED:
*params = static_cast<GLint>(g_activeTimeElapsedQueryId);
break;
case GL_SAMPLES_PASSED:
case GL_ANY_SAMPLES_PASSED:
case GL_ANY_SAMPLES_PASSED_CONSERVATIVE:
*params = static_cast<GLint>(g_activeSamplesPassedQueryId);
break;
case GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN:
*params = static_cast<GLint>(g_activePrimitivesWrittenQueryId);
break;
case GL_PRIMITIVES_GENERATED:
*params = static_cast<GLint>(g_activePrimitivesGeneratedQueryId);
break;
default:
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;
}
case GL_QUERY_COUNTER_BITS: {
// 64 bits are advertised only while the live backend can actually
// 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.
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;
*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;
MGP_FILL(IsTimerQuerySupported);
const Bool supported =
timerTarget && !TimerQueryDisabled() && isTimerQuerySupported && isTimerQuerySupported();
*params = supported ? 64 : 0;
return;
}
default:
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Unsupported query parameter.");
return;
}
}
void GetQueryBufferObjectiv(GLuint id, GLuint buffer, GLenum pname, GLintptr offset) {
GetQueryBufferObject<GLint>(id, buffer, pname, offset, __FUNCTION__);
}
void GetQueryBufferObjectuiv(GLuint id, GLuint buffer, GLenum pname, GLintptr offset) {
GetQueryBufferObject<GLuint>(id, buffer, pname, offset, __FUNCTION__);
}
void GetQueryBufferObjecti64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset) {
GetQueryBufferObject<GLint64>(id, buffer, pname, offset, __FUNCTION__);
}
void GetQueryBufferObjectui64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset) {
GetQueryBufferObject<GLuint64>(id, buffer, pname, offset, __FUNCTION__);
}
void GetQueryObjectiv(GLuint id, GLenum pname, GLint* params) {
Uint64 value = 0;
Bool valueProduced = false;
if (!GetQueryObjectValue(id, pname, __FUNCTION__, value, &valueProduced) || !valueProduced || !params) {
return;
}
constexpr Uint64 kMaxInt = static_cast<Uint64>(INT_MAX);
*params = value > kMaxInt ? INT_MAX : static_cast<GLint>(value);
}
void GetQueryObjectuiv(GLuint id, GLenum pname, GLuint* params) {
Uint64 value = 0;
Bool valueProduced = false;
if (!GetQueryObjectValue(id, pname, __FUNCTION__, value, &valueProduced) || !valueProduced || !params) {
return;
}
*params = static_cast<GLuint>(value & 0xFFFFFFFFull);
}
void GetQueryObjecti64v(GLuint id, GLenum pname, GLint64* params) {
Uint64 value = 0;
Bool valueProduced = false;
if (!GetQueryObjectValue(id, pname, __FUNCTION__, value, &valueProduced) || !valueProduced || !params) {
return;
}
*params = static_cast<GLint64>(value);
}
void GetQueryObjectui64v(GLuint id, GLenum pname, GLuint64* params) {
Uint64 value = 0;
Bool valueProduced = false;
if (!GetQueryObjectValue(id, pname, __FUNCTION__, value, &valueProduced) || !valueProduced || !params) {
return;
}
*params = static_cast<GLuint64>(value);
}
namespace {
Bool IsPerVertexStreamQueryTarget(GLenum target) {
return target == GL_PRIMITIVES_GENERATED || target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN;
}
// The indexed query entry points differ from the plain ones only in the vertex
// stream they address (GL 4.6 core 4.2.1): index must be below GL_MAX_VERTEX_STREAMS
// for the two transform feedback targets and zero for every other target. MobileGL
// implements ONE vertex stream, so both bounds are 1 and a valid call is always index 0 -
// which is what makes the three forwards below equivalent to the unindexed entry points.
//
// THAT EQUIVALENCE IS THE WHOLE JUSTIFICATION, and it is read out of the getter rather
// than assumed: the moment GL_MAX_VERTEX_STREAMS answers more than one, index 1..3 starts
// reaching EndQueryIndexed and GetQueryIndexediv, which resolve the active query from
// per-TARGET globals and would end - or report - a query begun on a different stream.
// Raising that limit therefore means giving each active query a stream index and
// comparing it here, not just changing the number.
Bool ValidateQueryStreamIndex(const char* function, GLenum target, GLuint index) {
const Bool perStreamTarget = IsPerVertexStreamQueryTarget(target);
GLint maxVertexStreams = 1;
if (perStreamTarget) {
GetIntegerv(GL_MAX_VERTEX_STREAMS, &maxVertexStreams);
}
if (index < static_cast<GLuint>(std::max(maxVertexStreams, 1))) {
return true;
}
RecordQueryError(ErrorCode::InvalidValue, function,
perStreamTarget ? "index is not less than GL_MAX_VERTEX_STREAMS."
: "index must be zero for this query target.");
return false;
}
} // namespace
void BeginQueryIndexed(GLenum target, GLuint index, GLuint id) {
if (!ValidateQueryStreamIndex(__FUNCTION__, target, index)) return;
BeginQuery(target, id);
}
void EndQueryIndexed(GLenum target, GLuint index) {
if (!ValidateQueryStreamIndex(__FUNCTION__, target, index)) return;
EndQuery(target);
}
void GetQueryIndexediv(GLenum target, GLuint index, GLenum pname, GLint* params) {
if (!ValidateQueryStreamIndex(__FUNCTION__, target, index)) return;
GetQueryiv(target, pname, params);
}
void DestroyAllQueryObjects() {
// Detach the registry under the lock, release outside it - same discipline
// (and the same accepted teardown race) as DestroyAllSyncObjects. Without
// this drain, every query the app left undeleted survived full library
// teardown in the process-global registry: the objects and their backend
// wrappers leaked across Destroy/Initialize cycles, stale ids kept
// answering IsQuery == GL_TRUE in the re-initialized library, and a later
// glDeleteQueries could hand the OLD backend's handle to a DIFFERENT
// backend's DeleteBackendQuery, which casts it to the wrong wrapper type.
UnorderedMap<GLuint, QueryObject*> orphans;
{
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
orphans.swap(g_liveQueryObjects);
g_activeTimeElapsedQueryId = 0;
g_activePrimitivesWrittenQueryId = 0;
g_activePrimitivesGeneratedQueryId = 0;
g_activeSamplesPassedQueryId = 0;
}
if (orphans.empty()) {
return;
}
// Backend handles must be released by the backend that created them, so
// this runs while the function table is still populated. Both backends'
// DeleteBackendQuery are generation-guarded, so a handle whose renderer
// or ES context is already gone frees only the wrapper.
const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery;
for (const auto& [_, queryObject] : orphans) {
if (deleteBackendQuery && queryObject->backendHandle) {
MGP_FILL(DeleteBackendQuery);
deleteBackendQuery(queryObject->backendHandle);
}
delete queryObject;
}
MGLOG_D("DestroyAllQueryObjects: reclaimed %zu query object(s) the app left undeleted", orphans.size());
}
} // namespace MobileGL::MG_Impl::GLImpl