[Feat] (MG_Impl, MG_Backend): GL_ARB_timer_query on both backends

Implements GL timer queries end to end: a frontend query registry
(modeled on the sync module - mutex-guarded objects wrapping opaque
backend handles behind optional function pointers) serving
glGenQueries/glBeginQuery/glEndQuery(GL_TIME_ELAPSED)/glQueryCounter
(GL_TIMESTAMP)/glGetQueryObject*/glGetQueryiv with GL 3.3 error
semantics and a graceful zero-result fallback when a backend cannot
time.

DirectGLES backs spans with GL_EXT_disjoint_timer_query (context-
generation-stamped handles, bounded result waits). DirectVulkan gets a
VkTimerQueryManager: per-frame-in-flight timestamp query pools reset at
command-buffer begin (outside render passes), records harvested by
frame serial before their pool recycles, elapsed = masked tick delta x
timestampPeriod; handles are stamped with a renderer generation that
also now guards fence syncs across renderer recreation. GL_QUERY_
COUNTER_BITS reports 0 unless the live backend can actually time
(dynamic IsTimerQuerySupported hook), and a failed blocking read keeps
the handle alive so the real value stays reachable once the frame
submits.

GL_ARB_timer_query is advertised only when the device supports timing
and MOBILEGL_DISABLE_TIMERQUERY is unset - LWJGL keys Minecraft's F3
'GPU: x%' line off exactly that extension string; verified on device
(Adreno 830) on both backends.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 01:08:38 +00:00
co-authored by Claude Fable 5
parent 7d31a6fcd7
commit 7eb3994b02
25 changed files with 1917 additions and 91 deletions
+2
View File
@@ -222,6 +222,7 @@ set(SOURCE_FILES
MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp
MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp
MobileGL/MG_Impl/GLImpl/Sync/GL_Sync.cpp
MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp
MobileGL/MG_Impl/Init.cpp
MobileGL/MG_Impl/GetProcAddress.cpp
@@ -249,6 +250,7 @@ set(SOURCE_FILES
MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferObject.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VkTimerQueryManager.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp
+30
View File
@@ -76,6 +76,11 @@ namespace MobileGL {
// and released by GLFunctionsTable::DeleteSync.
using BackendSyncHandle = void*;
// Opaque backend timer-query handle, created by
// GLFunctionsTable::BeginTimeElapsedQuery / QueryCounterTimestamp and
// released by GLFunctionsTable::DeleteBackendQuery.
using BackendQueryHandle = void*;
struct GLFunctionsTable {
void (*DrawArrays)(GLenum mode, GLint first, GLsizei count);
void (*DrawElements)(GLenum mode, GLsizei count, GLenum type, const void* indices);
@@ -171,6 +176,31 @@ namespace MobileGL {
void (*WaitSync)(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout);
void (*DeleteSync)(BackendSyncHandle sync);
Bool (*GetSyncStatus)(BackendSyncHandle sync); // true = signaled
// GL timer-query objects (GL_ARB_timer_query). All entries are
// optional (may be null); the frontend then falls back to zero
// results and reports GL_QUERY_COUNTER_BITS == 0.
// BeginTimeElapsedQuery / QueryCounterTimestamp may themselves
// return null when the backend cannot create a query right now;
// the frontend treats such a query as immediately available with
// a zero result.
// Dynamic support check: true only when the live backend can
// actually time at the moment of the call (extension / entry
// points / timestamp valid bits are known then, not at table
// init). Gates the advertised GL_QUERY_COUNTER_BITS.
Bool (*IsTimerQuerySupported)();
BackendQueryHandle (*BeginTimeElapsedQuery)(); // starts a TIME_ELAPSED span
void (*EndTimeElapsedQuery)(BackendQueryHandle query); // ends the span
BackendQueryHandle (*QueryCounterTimestamp)(); // glQueryCounter(GL_TIMESTAMP) one-shot
Bool (*IsQueryResultAvailable)(BackendQueryHandle query); // non-blocking
// Returns true when a final value was produced (*outNanoseconds
// written; the frontend may cache it and release the handle).
// Returns false when the result could not be obtained YET - e.g.
// a Vulkan wait that refuses to block on a not-yet-submitted
// frame serial - in which case the frontend must keep the handle
// and leave the query readable later.
Bool (*GetQueryResult64)(BackendQueryHandle query, Bool wait, Uint64* outNanoseconds);
void (*DeleteBackendQuery)(BackendQueryHandle query);
Int64 (*GetGpuTimestampNs)(); // glGetInteger64v(GL_TIMESTAMP); 0 if unsupported
};
struct GlobalBackendFunctionsTable {
GLFunctionsTable GL;
@@ -18,6 +18,8 @@
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToStr/TextureEnumConverter.h>
#include <MG_Util/Texture/TextureFormatProcessor.h>
#include <Config.h>
#include <algorithm>
#include <format>
namespace MobileGL::MG_Backend::DirectGLES {
@@ -176,7 +178,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
Flags<PixelFormatNormalizeOptionBit> GetForcedPixelFormatNormalizeOptions() {
Flags<PixelFormatNormalizeOptionBit> options;
if (g_GLESCapabilities.GLESRendererString.find("ANGLE") != String::npos) {
if (g_GLESCapabilities.IsAngleRenderer) {
options |= PixelFormatNormalizeOptionBit::NoRgb16;
options |= PixelFormatNormalizeOptionBit::NoSnorm16;
options |= PixelFormatNormalizeOptionBit::NoSnorm8;
@@ -612,6 +614,57 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
}
// The advertised renderer info must be mutable after its first use:
// E_GL_ARB_timer_query can only be decided once the ES capabilities
// are known, long after the list is first read (see
// UpdateAdvertisedTimerQueryExtension below).
RendererInfo& MutableRendererInfo() {
static RendererInfo rendererInfo = {
.RendererName = "Espryt", // Renderer Name
.BackendName = "Direct (OpenGL ES)", // Backend Name
.ExtraVendor = Nullopt, // Extra vendor
.RendererGLInfo =
{
.TargetGLVersion = {3, 3, 0}, // Target OpenGL Version
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
.Extensions = {V_OpenGL30, V_OpenGL31, V_OpenGL32, // OpenGL Extensions
V_OpenGL33, E_GL_ARB_draw_buffers_blend, E_GL_ARB_compute_shader,
E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object,
E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
E_GL_ARB_texture_storage, E_GL_ARB_direct_state_access,
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters,
E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind,
E_GL_ARB_shading_language_420pack, E_GL_ARB_vertex_attrib_binding,
E_GL_ARB_shader_image_size},
.IsCompatibilityProfile = false // Is Compatibility Profile
},
.StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability
};
return rendererInfo;
}
// GL_ARB_timer_query gates MC's F3 GPU% (LWJGL checks the extension
// string via glGetStringi plus non-null glQueryCounter and
// glGetQueryObject(u)i64v entries). GetRendererInfo is first invoked
// from LogBackendInfo() during MG_Backend::Init, BEFORE any ES
// context or capabilities exist, so the advertisement cannot be baked
// into the static initializer above; it is reconciled here at the end
// of InitCapabilities instead (mirroring DirectVulkan's mutable
// m_rendererInfo + UpdateAdvertisedExtensions). InitCapabilities
// completes inside the first MakeEGLCurrent on a context, so an app
// thread can only observe the extension string after the
// advertisement for its context has settled; erase-then-append keeps
// the re-run after a context recreation idempotent.
void UpdateAdvertisedTimerQueryExtension() {
auto& extensions = MutableRendererInfo().RendererGLInfo.Extensions;
extensions.erase(std::remove(extensions.begin(), extensions.end(), E_GL_ARB_timer_query),
extensions.end());
if (AreTimerQueriesSupported() && !MG_Config::Features.DisableTimerQuery) {
extensions.push_back(E_GL_ARB_timer_query);
}
}
} // namespace
BackendObject_DirectGLES::~BackendObject_DirectGLES() {
@@ -648,6 +701,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
return false;
}
DirectGLES::SetGLESCapabilities(m_GLESCapabilities);
// Now that g_GLESCapabilities knows about GL_EXT_disjoint_timer_query,
// reconcile the E_GL_ARB_timer_query advertisement (see the comment on
// UpdateAdvertisedTimerQueryExtension for why it cannot happen when
// the extension list is first built).
UpdateAdvertisedTimerQueryExtension();
UpdateDynamicBackendParameters();
ProbeGLESFormatCapabilities(m_GLESFunctions, MutableFormatCapabilities(), m_dynamicParameters);
PrintFormatCapabilities(GetFormatCapabilities());
@@ -773,29 +831,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
const RendererInfo& BackendObject_DirectGLES::GetRendererInfo() const {
static RendererInfo RendererInfo = {
.RendererName = "Espryt", // Renderer Name
.BackendName = "Direct (OpenGL ES)", // Backend Name
.ExtraVendor = Nullopt, // Extra vendor
.RendererGLInfo =
{
.TargetGLVersion = {3, 3, 0}, // Target OpenGL Version
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
.Extensions = {V_OpenGL30, V_OpenGL31, V_OpenGL32, // OpenGL Extensions
V_OpenGL33, E_GL_ARB_draw_buffers_blend, E_GL_ARB_compute_shader,
E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object,
E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
E_GL_ARB_texture_storage, E_GL_ARB_direct_state_access,
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters,
E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind,
E_GL_ARB_shading_language_420pack, E_GL_ARB_vertex_attrib_binding,
E_GL_ARB_shader_image_size},
.IsCompatibilityProfile = false // Is Compatibility Profile
},
.StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability
};
return RendererInfo;
return MutableRendererInfo();
}
String BackendObject_DirectGLES::GetBackendAPIVersionString() const {
@@ -872,6 +908,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
funcsTable.GL.WaitSync = WaitSync;
funcsTable.GL.DeleteSync = DeleteSync;
funcsTable.GL.GetSyncStatus = GetSyncStatus;
// Optional timer-query group: left null (the frontend then falls
// back) when disabled via MOBILEGL_DISABLE_TIMERQUERY. The hooks
// themselves additionally degrade to null handles / zero results
// when GL_EXT_disjoint_timer_query or its entry points are
// missing, or when the calling thread does not own the ES
// context.
if (!MG_Config::Features.DisableTimerQuery) {
// AreTimerQueriesSupported is a pure capability read (no
// current ES context required, false until the caps are
// filled in), which is exactly the dynamic support check
// the frontend wants from IsTimerQuerySupported.
funcsTable.GL.IsTimerQuerySupported = AreTimerQueriesSupported;
funcsTable.GL.BeginTimeElapsedQuery = BeginTimeElapsedQuery;
funcsTable.GL.EndTimeElapsedQuery = EndTimeElapsedQuery;
funcsTable.GL.QueryCounterTimestamp = QueryCounterTimestamp;
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
funcsTable.GL.GetGpuTimestampNs = GetGpuTimestampNs;
}
funcsTableInitialized = true;
}
return funcsTable;
+169 -6
View File
@@ -25,7 +25,9 @@
#include <MG_Util/Converters/MGToGL/RenderStateEnumConverter.h>
#include <MG_Util/Metrics/BufferMetrics.h>
#include <MG_Util/Texture/PixelStoreProcessor.h>
#include <Config.h>
#include <atomic>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
@@ -2445,7 +2447,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
void MemoryBarrier(GLbitfield barriers) {
g_GLESFuncs.glMemoryBarrier(barriers);
if (g_GLESCapabilities.GLESRendererString.find("ANGLE") != String::npos) {
if (g_GLESCapabilities.IsAngleRenderer) {
g_GLESFuncs.glFlush();
}
}
@@ -3334,8 +3336,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
static Bool PresentStatsEnabled() {
const char* value = std::getenv("MOBILEGL_GLES_PRESENT_STATS");
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
// MOBILEGL_GLES_PRESENT_STATS, parsed once in MG_ConfigLoader::Init.
return MG_Config::Features.GlesPresentStats;
}
static void DumpDefaultFramebufferStats() {
@@ -3627,9 +3629,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
// desynchronizing backend buffer state.
std::atomic<std::thread::id> g_backendContextOwnerThread{};
// Bumped whenever the backend ES context is destroyed; fence handles
// created under an older generation belong to a dead context and must
// never be passed back to GL (mirrors BufferImpl's context tracking).
// Bumped whenever the backend ES context is destroyed; fence and
// timer-query handles created under an older generation belong to a
// dead context and must never be passed back to GL (mirrors
// BufferImpl's context tracking).
Uint g_syncContextGeneration = 1;
// Backend fence handle: a native ES sync plus the ES context
@@ -3638,6 +3641,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
GLsync esSync = nullptr;
Uint contextGeneration = 0;
};
// Backend timer-query handle: a native GL query object name plus the
// ES context generation it was created under. Stale-generation
// handles read as available with a zero result, and deleting them
// only frees the wrapper (the dead ES context already reclaimed the
// query object).
struct GLESQueryObject {
GLuint queryId = 0;
Uint contextGeneration = 0;
};
}
Bool MakeCurrent() {
@@ -3771,6 +3784,156 @@ namespace MobileGL::MG_Backend::DirectGLES {
return status == GL_SIGNALED;
}
// GL timer queries, backed by GL_EXT_disjoint_timer_query. The desktop
// tokens from glext.h are used throughout: GL_TIME_ELAPSED (0x88BF),
// GL_TIMESTAMP (0x8E28), GL_QUERY_RESULT (0x8866) and
// GL_QUERY_RESULT_AVAILABLE (0x8867) are numerically identical to their
// _EXT counterparts.
Bool AreTimerQueriesSupported() {
return g_GLESCapabilities.SupportsDisjointTimerQuery && g_GLESFuncs.glGenQueries &&
g_GLESFuncs.glDeleteQueries && g_GLESFuncs.glBeginQuery && g_GLESFuncs.glEndQuery &&
g_GLESFuncs.glGetQueryObjectuiv && g_GLESFuncs.glQueryCounterEXT &&
g_GLESFuncs.glGetQueryObjectui64vEXT;
}
BackendQueryHandle BeginTimeElapsedQuery() {
// Query objects can only be created on the thread that owns the ES
// context (MC's F3 profiler queries on the render thread, which
// does). Returning null makes the frontend fall back to an
// immediately available zero result.
if (!IsBackendContextCurrentOnThisThread() || !AreTimerQueriesSupported()) {
return nullptr;
}
GLuint queryId = 0;
g_GLESFuncs.glGenQueries(1, &queryId);
if (queryId == 0) {
return nullptr;
}
g_GLESFuncs.glBeginQuery(GL_TIME_ELAPSED, queryId);
return new GLESQueryObject{queryId, g_syncContextGeneration};
}
void EndTimeElapsedQuery(BackendQueryHandle handle) {
const auto* query = static_cast<GLESQueryObject*>(handle);
if (query == nullptr || query->contextGeneration != g_syncContextGeneration ||
!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glEndQuery) {
return;
}
// ES tracks the active query per target, not per object, so the
// handle only guards the degraded paths above.
g_GLESFuncs.glEndQuery(GL_TIME_ELAPSED);
}
BackendQueryHandle QueryCounterTimestamp() {
if (!IsBackendContextCurrentOnThisThread() || !AreTimerQueriesSupported()) {
return nullptr;
}
GLuint queryId = 0;
g_GLESFuncs.glGenQueries(1, &queryId);
if (queryId == 0) {
return nullptr;
}
g_GLESFuncs.glQueryCounterEXT(queryId, GL_TIMESTAMP);
return new GLESQueryObject{queryId, g_syncContextGeneration};
}
Bool IsQueryResultAvailable(BackendQueryHandle handle) {
const auto* query = static_cast<GLESQueryObject*>(handle);
// Null/stale handles report available so the frontend proceeds to
// GetQueryResult64, which finalizes them as zero. A thread that does
// not own the ES context also reports available: GetQueryResult64
// then returns false and the frontend keeps the handle for a later
// read from the owning thread.
if (query == nullptr || query->contextGeneration != g_syncContextGeneration ||
!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glGetQueryObjectuiv) {
return true;
}
GLuint available = GL_FALSE;
g_GLESFuncs.glGetQueryObjectuiv(query->queryId, GL_QUERY_RESULT_AVAILABLE, &available);
return available != GL_FALSE;
}
Bool GetQueryResult64(BackendQueryHandle handle, Bool wait, Uint64* outNanoseconds) {
*outNanoseconds = 0;
const auto* query = static_cast<GLESQueryObject*>(handle);
// Null handles never had a GL query object, handles from a
// since-destroyed ES context lost theirs, and missing entry points
// can never produce a reading (belt and braces: the creators already
// require them): zero is the FINAL result in all three cases, so
// report it as produced and let the frontend cache it and release
// the handle.
if (query == nullptr || query->contextGeneration != g_syncContextGeneration ||
!g_GLESFuncs.glGetQueryObjectuiv || !g_GLESFuncs.glGetQueryObjectui64vEXT) {
return true;
}
// A thread that does not own the ES context cannot issue GL calls,
// but the result still lands on the owning context eventually: report
// "not obtainable yet" so the frontend keeps the handle and a later
// availability poll / result read from the owning thread can still
// produce the real value.
if (!IsBackendContextCurrentOnThisThread()) {
return false;
}
if (wait) {
// Reading GL_QUERY_RESULT blocks in the driver until the result
// lands, but only after the commands were flushed; flush once,
// then poll availability for a bounded ~100ms before dropping to
// a glFinish as the last resort (ClientWaitSync has no polling
// loop to mirror - it delegates its timeout to the driver, which
// a query-object read cannot do).
if (g_GLESFuncs.glFlush) {
g_GLESFuncs.glFlush();
}
constexpr Int kMaxAvailabilityPolls = 1000; // ~100ms at 100us per poll
GLuint available = GL_FALSE;
for (Int i = 0; i < kMaxAvailabilityPolls && available == GL_FALSE; ++i) {
g_GLESFuncs.glGetQueryObjectuiv(query->queryId, GL_QUERY_RESULT_AVAILABLE, &available);
if (available == GL_FALSE) {
std::this_thread::sleep_for(std::chrono::microseconds(100));
}
}
if (available == GL_FALSE && g_GLESFuncs.glFinish) {
g_GLESFuncs.glFinish();
}
}
// GL_EXT_disjoint_timer_query's GPU_DISJOINT_EXT signal is
// deliberately ignored: after a disjoint event (power state change,
// context switch) the result may be garbage, which is tolerable for
// an F3 GPU% readout, and consuming the latched flag here could hide
// the event from another observer.
GLuint64 result = 0;
g_GLESFuncs.glGetQueryObjectui64vEXT(query->queryId, GL_QUERY_RESULT, &result);
*outNanoseconds = static_cast<Uint64>(result);
return true;
}
void DeleteBackendQuery(BackendQueryHandle handle) {
auto* query = static_cast<GLESQueryObject*>(handle);
if (query == nullptr) {
return;
}
if (query->contextGeneration == g_syncContextGeneration && IsBackendContextCurrentOnThisThread() &&
g_GLESFuncs.glDeleteQueries) {
g_GLESFuncs.glDeleteQueries(1, &query->queryId);
}
// Otherwise the GL query object is abandoned; the ES context reclaims
// all of its query objects when it is destroyed.
delete query;
}
Int64 GetGpuTimestampNs() {
// Synchronous GPU clock sample; 0 tells the frontend GL_TIMESTAMP
// getter to fall back.
if (!IsBackendContextCurrentOnThisThread() || !AreTimerQueriesSupported() ||
!g_GLESFuncs.glGetInteger64v) {
return 0;
}
GLint64 timestamp = 0;
g_GLESFuncs.glGetInteger64v(GL_TIMESTAMP, &timestamp);
return static_cast<Int64>(timestamp);
}
void Present() {
g_EGLFuncs.eglSwapBuffers(g_Display, g_Surface);
}
@@ -109,6 +109,31 @@ namespace MobileGL::MG_Backend::DirectGLES {
void WaitSync(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout);
void DeleteSync(BackendSyncHandle sync);
Bool GetSyncStatus(BackendSyncHandle sync);
// True when GL_EXT_disjoint_timer_query and every entry point the timer
// hooks below need are present. Also gates the E_GL_ARB_timer_query
// advertisement in BackendObject_DirectGLES::InitCapabilities, and is
// registered as the GLFunctionsTable::IsTimerQuerySupported hook: a pure
// capability read needs no current ES context, and it stays false until
// the ES capabilities have been filled in.
Bool AreTimerQueriesSupported();
// GL timer-query objects, backed by GL_EXT_disjoint_timer_query. The
// creators return null (the frontend then falls back to an immediately
// available zero result) when the calling thread does not own the ES
// context or the extension/entry points are missing, and handles created
// under a since-destroyed ES context are always treated as complete with
// a zero result (mirrors the fence-sync handles above).
BackendQueryHandle BeginTimeElapsedQuery();
void EndTimeElapsedQuery(BackendQueryHandle query);
BackendQueryHandle QueryCounterTimestamp();
Bool IsQueryResultAvailable(BackendQueryHandle query);
// Returns true when a final value landed in *outNanoseconds (a zero for
// null or stale-generation handles IS final: the frontend may cache it
// and release the handle). Returns false only when the calling thread
// does not own the ES context, so the value is genuinely unobtainable
// right now; the handle stays alive and readable later.
Bool GetQueryResult64(BackendQueryHandle query, Bool wait, Uint64* outNanoseconds);
void DeleteBackendQuery(BackendQueryHandle query);
Int64 GetGpuTimestampNs();
void Present();
// Applies (or defers until the window surface exists) the app-requested
// eglSwapInterval on the native EGL surface.
@@ -17,17 +17,14 @@
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
#include "MG_Util/Texture/TextureFormatProcessor.h"
#include <Config.h>
#include <cstdlib>
#include <cstring>
namespace MobileGL::MG_Backend::DirectVulkan {
namespace {
Bool IsR11G11B10FFallbackEnabled() {
static const Bool enabled = [] {
const char* value = std::getenv("MOBILEGL_VULKAN_R11G11B10F_FALLBACK");
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
}();
return enabled;
return MG_Config::Features.VulkanR11G11B10FFallback;
}
Bool IsReleaseCurrentRequest(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
@@ -374,6 +371,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto nativeWindow = reinterpret_cast<NativeWindowType>(m_windowHandle.Handle);
// Any renderer instance this assignment replaces is destroyed here;
// fence/timer-query handles stamped with the old generation go stale.
BumpRendererGeneration();
pVulkanRenderer = MakeUnique<MG_Backend::DirectVulkan::VulkanRenderer>(nativeWindow);
MOBILEGL_ASSERT(pVulkanRenderer != nullptr, "InitWindowSurface: VulkanRenderer creation failed");
pVulkanRenderer->Initialize();
@@ -384,6 +384,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VulkanRendererConfig config;
config.SurfaceWidth = static_cast<Uint32>(std::max<EGLint>(width, 1));
config.SurfaceHeight = static_cast<Uint32>(std::max<EGLint>(height, 1));
// Any renderer instance this assignment replaces is destroyed here;
// fence/timer-query handles stamped with the old generation go stale.
BumpRendererGeneration();
pVulkanRenderer = MakeUnique<MG_Backend::DirectVulkan::VulkanRenderer>(NativeWindowType{}, config);
MOBILEGL_ASSERT(pVulkanRenderer != nullptr, "InitPbufferSurface: VulkanRenderer creation failed");
pVulkanRenderer->Initialize();
@@ -486,12 +489,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void BackendObject_DirectVulkan::ReleaseEGLResources() {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
// Outstanding fence/timer-query handles now refer to a dead renderer;
// treat them as signaled/available with zero results from here on.
BumpRendererGeneration();
pVulkanRenderer.reset();
BackendObject::ReleaseEGLResources();
}
void BackendObject_DirectVulkan::OnEGLSurfaceReleased(EGLSurface surface) {
(void)surface;
// Outstanding fence/timer-query handles now refer to a dead renderer;
// treat them as signaled/available with zero results from here on.
BumpRendererGeneration();
pVulkanRenderer.reset();
}
@@ -574,6 +583,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
funcsTable.GL.WaitSync = WaitSync;
funcsTable.GL.DeleteSync = DeleteSync;
funcsTable.GL.GetSyncStatus = GetSyncStatus;
// Optional timer-query group: left null (the frontend then falls
// back) when disabled via MOBILEGL_DISABLE_TIMERQUERY. The hooks
// themselves additionally degrade to null handles when the device
// lacks timestamp support.
if (!MG_Config::Features.DisableTimerQuery) {
funcsTable.GL.IsTimerQuerySupported = IsTimerQuerySupported;
funcsTable.GL.BeginTimeElapsedQuery = BeginTimeElapsedQuery;
funcsTable.GL.EndTimeElapsedQuery = EndTimeElapsedQuery;
funcsTable.GL.QueryCounterTimestamp = QueryCounterTimestamp;
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
funcsTable.GL.GetGpuTimestampNs = GetGpuTimestampNs;
}
funcsTableInitialized = true;
}
return funcsTable;
@@ -599,6 +622,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (m_vulkanCaps.SupportsShaderSubgroup) {
extensions.push_back(E_GL_KHR_shader_subgroup);
}
// GL_ARB_timer_query gates MC's F3 GPU% (LWJGL checks the extension
// string). InitCapabilities runs after InitWindowSurface has created
// and initialized the renderer, so the advertisement can be gated on
// real device timestamp support. ApplyVulkanCapabilitiesForTesting may
// run without a renderer; nothing is advertised then.
extensions.erase(std::remove(extensions.begin(), extensions.end(), E_GL_ARB_timer_query),
extensions.end());
if (pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported() &&
!MG_Config::Features.DisableTimerQuery) {
extensions.push_back(E_GL_ARB_timer_query);
}
}
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
@@ -15,12 +15,35 @@
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
#include "MG_Util/Metrics/TextureMetrics.h"
#include "MG_Util/Miscellany/IndexGenerator.h"
#include <atomic>
#include <cstring>
#include <spirv_reflect.h>
namespace MobileGL::MG_Backend::DirectVulkan {
UniquePtr<VulkanRenderer> pVulkanRenderer = nullptr;
namespace {
// Generation of the live VulkanRenderer instance, mirroring
// DirectGLES's g_syncContextGeneration. BackendObject_DirectVulkan
// bumps it (BumpRendererGeneration) wherever pVulkanRenderer is reset
// or recreated. Fence and timer-query handles are stamped with the
// generation they were created under: a stale stamp means the frame
// serials and query-pool slots the handle refers to belong to a
// destroyed renderer and must never be dereferenced against the
// current one (a new renderer restarts its frame-serial counter and
// reuses pool indices). Atomic because handles may be polled from a
// thread other than the EGL thread that recreates the renderer.
std::atomic<Uint64> g_rendererGeneration{1};
} // namespace
Uint64 GetRendererGeneration() {
return g_rendererGeneration.load(std::memory_order_acquire);
}
void BumpRendererGeneration() {
g_rendererGeneration.fetch_add(1, std::memory_order_acq_rel);
}
namespace {
struct BufferVariableResource {
String name;
@@ -1349,6 +1372,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// horizon used to recycle buffer resources).
struct VulkanSyncObject {
Uint64 frameSerial = 0;
// Renderer generation the serial was issued under (see
// g_rendererGeneration). A stale generation reports the fence
// signaled: renderer destruction waits for device idle, so the
// old renderer's GPU work is long complete, and the serial must
// not be compared against the new renderer's restarted counter.
Uint64 rendererGeneration = 0;
};
} // namespace
@@ -1356,7 +1385,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (!pVulkanRenderer) {
return nullptr;
}
return new VulkanSyncObject{pVulkanRenderer->GetCurrentFrameSerial()};
return new VulkanSyncObject{pVulkanRenderer->GetCurrentFrameSerial(), GetRendererGeneration()};
}
GLenum ClientWaitSync(BackendSyncHandle handle, GLbitfield flags, GLuint64 timeout) {
@@ -1365,7 +1394,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// waiting can succeed at all.
(void)flags;
const auto* sync = static_cast<VulkanSyncObject*>(handle);
if (sync == nullptr || !pVulkanRenderer) {
if (sync == nullptr || !pVulkanRenderer || sync->rendererGeneration != GetRendererGeneration()) {
return GL_ALREADY_SIGNALED;
}
if (pVulkanRenderer->IsFrameSerialComplete(sync->frameSerial)) {
@@ -1393,12 +1422,143 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool GetSyncStatus(BackendSyncHandle handle) {
const auto* sync = static_cast<VulkanSyncObject*>(handle);
if (sync == nullptr || !pVulkanRenderer) {
if (sync == nullptr || !pVulkanRenderer || sync->rendererGeneration != GetRendererGeneration()) {
return true;
}
return pVulkanRenderer->IsFrameSerialComplete(sync->frameSerial);
}
namespace {
// Backend timer-query handle: a TIME_ELAPSED span holds a begin and an
// end timestamp record; a GL_TIMESTAMP one-shot holds only `end`. The
// 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 {
SharedPtr<VkTimerQueryManager::TimestampRecord> begin;
SharedPtr<VkTimerQueryManager::TimestampRecord> end;
// 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
// serials refer to a destroyed renderer and must never be handed
// to the current one. DeleteBackendQuery only frees the wrapper
// (and, via the SharedPtrs, the records), never pool slots, so
// stale queries are always safe to delete.
Uint64 rendererGeneration = 0;
};
} // namespace
Bool IsTimerQuerySupported() {
return pVulkanRenderer != nullptr && pVulkanRenderer->IsTimerQuerySupported();
}
BackendQueryHandle BeginTimeElapsedQuery() {
if (!pVulkanRenderer || !pVulkanRenderer->IsTimerQuerySupported()) {
return nullptr;
}
auto begin = pVulkanRenderer->WriteTimerQueryTimestamp();
if (!begin) {
// Pool exhausted this frame; the frontend falls back on a null handle.
return nullptr;
}
auto* query = new VulkanTimerQuery{};
query->begin = std::move(begin);
query->rendererGeneration = GetRendererGeneration();
return query;
}
void EndTimeElapsedQuery(BackendQueryHandle handle) {
auto* query = static_cast<VulkanTimerQuery*>(handle);
if (query == nullptr || !pVulkanRenderer) {
return;
}
if (query->rendererGeneration != GetRendererGeneration()) {
// The span began under a renderer that has since been destroyed;
// never write into the new renderer's pools on its behalf. The
// query resolves as available with a zero result.
return;
}
// May be null on pool exhaustion; the query then reads back as 0.
query->end = pVulkanRenderer->WriteTimerQueryTimestamp();
}
BackendQueryHandle QueryCounterTimestamp() {
if (!pVulkanRenderer || !pVulkanRenderer->IsTimerQuerySupported()) {
return nullptr;
}
auto record = pVulkanRenderer->WriteTimerQueryTimestamp();
if (!record) {
return nullptr;
}
auto* query = new VulkanTimerQuery{};
query->end = std::move(record);
query->rendererGeneration = GetRendererGeneration();
return query;
}
Bool IsQueryResultAvailable(BackendQueryHandle handle) {
auto* query = static_cast<VulkanTimerQuery*>(handle);
// Degraded/stale handles report available; GetQueryResult64 then
// resolves them with a final zero result.
if (query == nullptr || !pVulkanRenderer || query->rendererGeneration != GetRendererGeneration()) {
return true;
}
if (query->begin && !pVulkanRenderer->IsTimerQueryResultReady(*query->begin)) {
return false;
}
if (query->end && !pVulkanRenderer->IsTimerQueryResultReady(*query->end)) {
return false;
}
return true;
}
Bool GetQueryResult64(BackendQueryHandle handle, Bool wait, Uint64* outNanoseconds) {
*outNanoseconds = 0;
auto* query = static_cast<VulkanTimerQuery*>(handle);
if (query == nullptr || !pVulkanRenderer || query->rendererGeneration != GetRendererGeneration()) {
// No renderer, or the records belong to a destroyed renderer: no
// real value can ever be produced, so resolve with a final 0.
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
// handle alive in the frontend; the query stays readable once a later
// Present submits the frame.
const auto ensureReady = [&](VkTimerQueryManager::TimestampRecord& record) {
return wait ? pVulkanRenderer->WaitForTimerQueryResult(record)
: pVulkanRenderer->IsTimerQueryResultReady(record);
};
if (query->begin && query->end) {
if (!ensureReady(*query->begin) || !ensureReady(*query->end)) {
return false;
}
*outNanoseconds = pVulkanRenderer->GetTimerQueryElapsedNs(*query->begin, *query->end);
return true;
}
if (query->end) {
if (!ensureReady(*query->end)) {
return false;
}
*outNanoseconds = pVulkanRenderer->GetTimerQueryTimestampNs(*query->end);
return true;
}
// TIME_ELAPSED span that never got its end timestamp (pool
// exhaustion): nothing further can arrive, resolve with a final 0.
return true;
}
void DeleteBackendQuery(BackendQueryHandle handle) {
delete static_cast<VulkanTimerQuery*>(handle);
}
Int64 GetGpuTimestampNs() {
// Vulkan cannot synchronously sample the GPU clock: timestamps only
// exist as vkCmdWriteTimestamp results read back later, and
// VK_EXT_calibrated_timestamps is not wired up. Returning 0 tells the
// frontend GL_TIMESTAMP getter to fall back.
return 0;
}
void Present() {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::Present called with null VulkanRenderer");
pVulkanRenderer->Present();
@@ -14,6 +14,15 @@
namespace MobileGL::MG_Backend::DirectVulkan {
extern UniquePtr<VulkanRenderer> pVulkanRenderer;
// Generation of the live VulkanRenderer instance, mirroring DirectGLES's
// g_syncContextGeneration. BackendObject_DirectVulkan bumps it wherever
// pVulkanRenderer is reset or recreated; fence and timer-query handles
// stamped with an older generation are stale and resolve as signaled /
// available with zero results instead of dereferencing the destroyed
// renderer's frame serials and query-pool slots.
Uint64 GetRendererGeneration();
void BumpRendererGeneration();
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
@@ -99,5 +108,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void WaitSync(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout);
void DeleteSync(BackendSyncHandle sync);
Bool GetSyncStatus(BackendSyncHandle sync);
// GPU timer queries (GL_TIME_ELAPSED spans and GL_TIMESTAMP one-shots),
// backed by per-frame VkQueryPool timestamp slots. All hooks degrade
// gracefully: null handles when the renderer is absent, the device lacks
// timestamp support, or the frame's pool is exhausted.
// Dynamic support check (GLFunctionsTable::IsTimerQuerySupported): true
// only while a live renderer exists whose device can actually time.
Bool IsTimerQuerySupported();
BackendQueryHandle BeginTimeElapsedQuery();
void EndTimeElapsedQuery(BackendQueryHandle query);
BackendQueryHandle QueryCounterTimestamp();
Bool IsQueryResultAvailable(BackendQueryHandle query);
// Returns true when a final value was produced (outNanoseconds set; the
// frontend may cache it and release the handle), false when the result
// cannot be obtained yet (e.g. a wait refused because the records' frame
// serial is the current unsubmitted frame) - the handle then stays
// readable later.
Bool GetQueryResult64(BackendQueryHandle query, Bool wait, Uint64* outNanoseconds);
void DeleteBackendQuery(BackendQueryHandle query);
// Always 0: Vulkan cannot synchronously sample the GPU clock (timestamps
// only exist as vkCmdWriteTimestamp results); the frontend falls back.
Int64 GetGpuTimestampNs();
void Present();
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -97,6 +97,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VK_VERIFY(vkBeginCommandBuffer(frame.commandBuffer, &beginInfo), "BeginCommandRecording, vkBeginCommandBuffer");
frame.isCommandRecording = true;
if (m_recordingObserver != nullptr) {
m_recordingObserver->OnFrameCommandRecordingBegan(frame.commandBuffer);
}
return frame.commandBuffer;
}
@@ -230,6 +233,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return static_cast<Uint32>(m_frames.size());
}
void FrameContext::SetRecordingObserver(IRecordingObserver* observer) {
m_recordingObserver = observer;
}
void FrameContext::AssertValidFrameIndex(Uint32 frameIndex) const {
MOBILEGL_ASSERT(frameIndex < m_frames.size(), "FrameContext index out of range");
}
@@ -14,6 +14,17 @@
namespace MobileGL::MG_Backend::DirectVulkan {
class FrameContext {
public:
// Notified immediately after a frame command buffer begins recording
// (before any render pass has been begun); every BeginCommandRecording
// caller funnels through this single seam. Implemented by the renderer
// to prepare per-frame timer-query pools (vkCmdResetQueryPool must be
// recorded outside a render pass).
class IRecordingObserver {
public:
virtual ~IRecordingObserver() = default;
virtual void OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) = 0;
};
struct SubmitInfoPacket {
VkPipelineStageFlags waitDstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkSemaphore waitSemaphore = VK_NULL_HANDLE;
@@ -61,6 +72,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 GetCurrentFrameIndex() const;
Uint32 GetFrameCount() const;
// Observer may be null (no notifications). Not owned.
void SetRecordingObserver(IRecordingObserver* observer);
private:
void AssertValidFrameIndex(Uint32 frameIndex) const;
void AssertValidSwapchainImageIndex(Uint32 imageIndex) const;
@@ -73,5 +87,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<FrameData> m_frames;
Vector<VkSemaphore> m_swapchainImageRenderFinishedSemaphores;
Uint32 currentFrameIndex = 0;
IRecordingObserver* m_recordingObserver = nullptr;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -16,6 +16,7 @@
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
#include "MG_Util/Metrics/TextureMetrics.h"
#include <Config.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
@@ -78,11 +79,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
static Bool ShouldDumpDescriptorStats() {
static const Bool enabled = [] {
const char* value = std::getenv("MOBILEGL_DESCRIPTOR_STATS");
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
}();
return enabled;
return MG_Config::Features.DescriptorStats;
}
Bool UniformManager::Initialize(VkDevice device, VkBufferManager* bufferManager,
@@ -12,6 +12,7 @@
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
#include <Config.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
@@ -52,11 +53,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
};
static Bool IsR11G11B10FFallbackEnabled() {
static const Bool enabled = [] {
const char* value = std::getenv("MOBILEGL_VULKAN_R11G11B10F_FALLBACK");
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
}();
return enabled;
return MG_Config::Features.VulkanR11G11B10FFallback;
}
static Bool IsMultisampleTextureUploadTarget(TextureUploadTarget target) {
@@ -387,11 +384,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
static Bool ShouldDumpTextureUploadStats() {
static const Bool enabled = [] {
const char* value = std::getenv("MOBILEGL_TEXTURE_UPLOAD_STATS");
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
}();
return enabled;
return MG_Config::Features.TextureUploadStats;
}
static void DumpTextureSyncStats(Int textureId, TextureInternalFormat format, TextureUploadTarget uploadTarget,
@@ -0,0 +1,179 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VkTimerQueryManager.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 "VkTimerQueryManager.h"
namespace MobileGL::MG_Backend::DirectVulkan {
Bool VkTimerQueryManager::Initialize(const InitInfo& initInfo) {
Shutdown();
MOBILEGL_ASSERT(initInfo.device != VK_NULL_HANDLE, "VkTimerQueryManager::Initialize requires valid VkDevice");
MOBILEGL_ASSERT(initInfo.frameCount > 0, "VkTimerQueryManager::Initialize requires non-zero frame count");
if (initInfo.timestampValidBits == 0 || initInfo.timestampPeriodNs <= 0.0f || initInfo.slotsPerPool == 0) {
MGLOG_W("VkTimerQueryManager: timestamps unsupported (validBits=%u, period=%f, slots=%u)",
initInfo.timestampValidBits, initInfo.timestampPeriodNs, initInfo.slotsPerPool);
return false;
}
m_device = initInfo.device;
m_timestampPeriodNs = initInfo.timestampPeriodNs;
m_validBitsMask = initInfo.timestampValidBits >= 64
? ~0ull
: ((1ull << initInfo.timestampValidBits) - 1ull);
m_slotsPerPool = initInfo.slotsPerPool;
m_pools.resize(initInfo.frameCount);
VkQueryPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO;
poolInfo.queryType = VK_QUERY_TYPE_TIMESTAMP;
poolInfo.queryCount = m_slotsPerPool;
for (auto& poolState : m_pools) {
const VkResult result = vkCreateQueryPool(m_device, &poolInfo, nullptr, &poolState.pool);
if (result != VK_SUCCESS) {
MGLOG_E("VkTimerQueryManager: vkCreateQueryPool failed with %s", VkResultToString(result));
Shutdown();
return false;
}
}
return true;
}
void VkTimerQueryManager::Shutdown() {
if (m_device != VK_NULL_HANDLE) {
for (auto& poolState : m_pools) {
if (poolState.pool != VK_NULL_HANDLE) {
vkDestroyQueryPool(m_device, poolState.pool, nullptr);
}
}
}
// Records the frontend still holds simply stay unharvested; their
// results read back as 0.
m_pools.clear();
m_device = VK_NULL_HANDLE;
m_timestampPeriodNs = 0.0f;
m_validBitsMask = 0;
m_slotsPerPool = 0;
}
void VkTimerQueryManager::OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer, Uint32 frameIndex,
Uint64 frameSerial) {
MOBILEGL_ASSERT(frameIndex < m_pools.size(), "VkTimerQueryManager frame index out of range");
auto& poolState = m_pools[frameIndex];
if (poolState.preparedFrameSerial == frameSerial) {
// Recording re-began within the same frame (mid-frame readback
// submit or the Present layout transition); the pool was already
// harvested and reset for this cycle, and resetting again would
// clobber timestamps written earlier in the frame.
return;
}
// Harvest what the pool's previous cycle left behind. The frame slot's
// fence was waited before re-recording, so every executed query is
// already available and the reads return immediately.
DrainPoolPending(poolState);
vkCmdResetQueryPool(commandBuffer, poolState.pool, 0, m_slotsPerPool);
poolState.cursor = 0;
poolState.exhaustionWarned = false;
poolState.preparedFrameSerial = frameSerial;
}
SharedPtr<VkTimerQueryManager::TimestampRecord> VkTimerQueryManager::WriteTimestamp(VkCommandBuffer commandBuffer,
Uint32 frameIndex,
Uint64 frameSerial) {
MOBILEGL_ASSERT(frameIndex < m_pools.size(), "VkTimerQueryManager frame index out of range");
auto& poolState = m_pools[frameIndex];
if (poolState.cursor >= m_slotsPerPool) {
if (!poolState.exhaustionWarned) {
MGLOG_W("VkTimerQueryManager: frame %u timestamp pool exhausted (%u slots); further timer queries "
"this frame fall back to the frontend path",
frameIndex, m_slotsPerPool);
poolState.exhaustionWarned = true;
}
return nullptr;
}
auto record = MakeShared<TimestampRecord>();
record->poolIndex = frameIndex;
record->slot = poolState.cursor++;
record->frameSerial = frameSerial;
vkCmdWriteTimestamp(commandBuffer, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, poolState.pool, record->slot);
poolState.pendingRecords.push_back(record);
return record;
}
Bool VkTimerQueryManager::TryHarvest(TimestampRecord& record) {
if (record.harvested) {
return true;
}
if (m_device == VK_NULL_HANDLE || record.poolIndex >= m_pools.size()) {
return false;
}
Uint64 resultWithAvailability[2] = {0, 0};
const VkResult result = vkGetQueryPoolResults(
m_device, m_pools[record.poolIndex].pool, record.slot, 1, sizeof(resultWithAvailability),
resultWithAvailability, sizeof(Uint64), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WITH_AVAILABILITY_BIT);
if (result != VK_SUCCESS && result != VK_NOT_READY) {
MGLOG_E("VkTimerQueryManager: vkGetQueryPoolResults failed with %s", VkResultToString(result));
return false;
}
if (resultWithAvailability[1] == 0) {
return false;
}
record.rawTicks = resultWithAvailability[0];
record.harvested = true;
return true;
}
void VkTimerQueryManager::InvalidatePendingRecords() {
for (auto& poolState : m_pools) {
DrainPoolPending(poolState);
// Force a harvest-free reset cycle the next time this pool's frame
// begins recording.
poolState.preparedFrameSerial = 0;
}
}
void VkTimerQueryManager::DrainPoolPending(PoolState& poolState) {
for (auto& record : poolState.pendingRecords) {
if (record->harvested) {
continue;
}
if (!TryHarvest(*record)) {
// The commands carrying this timestamp never executed (they
// were dropped, e.g. by a swapchain recreation mid-frame).
// Mark the record resolved-as-invalid so waits on it cannot
// hang; its result reads back as 0.
record->harvested = true;
record->valid = false;
}
}
poolState.pendingRecords.clear();
}
Uint64 VkTimerQueryManager::MaskToValidBits(Uint64 ticks) const {
return ticks & m_validBitsMask;
}
Uint64 VkTimerQueryManager::ElapsedNs(const TimestampRecord& begin, const TimestampRecord& end) const {
if (!begin.valid || !end.valid) {
return 0;
}
const Uint64 deltaTicks = MaskToValidBits(end.rawTicks - begin.rawTicks);
return static_cast<Uint64>(static_cast<double>(deltaTicks) * static_cast<double>(m_timestampPeriodNs));
}
Uint64 VkTimerQueryManager::TimestampNs(const TimestampRecord& record) const {
if (!record.valid) {
return 0;
}
return static_cast<Uint64>(static_cast<double>(MaskToValidBits(record.rawTicks)) *
static_cast<double>(m_timestampPeriodNs));
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -0,0 +1,111 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VkTimerQueryManager.h
// 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
#pragma once
#include "../VkIncludes.h"
#include <Includes.h>
namespace MobileGL::MG_Backend::DirectVulkan {
// GPU timestamp storage backing the GL timer-query frontend (GL_TIME_ELAPSED
// spans and GL_TIMESTAMP one-shots): one VkQueryPool of timestamp slots per
// frame in flight.
//
// Per-frame lifecycle: right after a frame slot's command buffer begins
// recording (and before any render pass, since vkCmdResetQueryPool must be
// recorded outside one), OnFrameCommandRecordingBegan harvests every
// not-yet-read slot of the pool about to be reused (the slot's frame fence
// was waited before re-recording, so the results are already available),
// records a reset of the whole pool, and rewinds the allocation cursor.
class VkTimerQueryManager {
public:
// One vkCmdWriteTimestamp landing spot. Shared (via SharedPtr) between
// the frontend-held query object and the owning pool's pending list, so
// deleting a query while its result is still in flight never leaves the
// pool with a dangling record.
struct TimestampRecord {
Uint32 poolIndex = 0;
Uint32 slot = 0;
// VkBufferManager frame serial current when the timestamp was
// recorded; result availability is bounded by its completion.
Uint64 frameSerial = 0;
Bool harvested = false;
// Cleared when the recorded commands were dropped before they could
// execute (swapchain recreation abandons the in-progress command
// buffer); the result then reads back as 0.
Bool valid = true;
Uint64 rawTicks = 0;
};
struct InitInfo {
VkDevice device = VK_NULL_HANDLE;
Uint32 frameCount = 0;
Uint32 timestampValidBits = 0;
Float timestampPeriodNs = 0.0f; // nanoseconds per timestamp tick
Uint32 slotsPerPool = 128;
};
Bool Initialize(const InitInfo& initInfo);
// The caller guarantees the device is idle (same contract as the other
// DirectVulkan managers' Shutdown paths).
void Shutdown();
// The per-frame hook described in the class comment. Re-begins within
// the same frame serial (mid-frame readback submits, the Present layout
// transition) are skipped so already-written slots survive.
void OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer, Uint32 frameIndex, Uint64 frameSerial);
// Allocates a slot from the frame's pool and records a bottom-of-pipe
// vkCmdWriteTimestamp (valid both inside and outside a render pass).
// Returns null on pool exhaustion, with one warning per pool cycle; the
// frontend falls back gracefully on a null handle.
SharedPtr<TimestampRecord> WriteTimestamp(VkCommandBuffer commandBuffer, Uint32 frameIndex,
Uint64 frameSerial);
// Non-blocking single-slot read (WITH_AVAILABILITY, no WAIT). Returns
// true once the record holds its raw ticks. Callers gate this on the
// record's frame serial being complete.
Bool TryHarvest(TimestampRecord& record);
// Reads every pending result that is available (the caller guarantees
// the device is idle) and marks the rest invalid. Called when recorded
// but unsubmitted commands are dropped (swapchain recreation), which
// would otherwise leave slots that never become available. Each pool is
// reset lazily on its next OnFrameCommandRecordingBegan.
void InvalidatePendingRecords();
// end - begin using unsigned wrap arithmetic masked to the queue's
// timestampValidBits, converted to nanoseconds. 0 if either record was
// invalidated.
Uint64 ElapsedNs(const TimestampRecord& begin, const TimestampRecord& end) const;
// Raw GPU timestamp converted to nanoseconds. 0 if invalidated.
Uint64 TimestampNs(const TimestampRecord& record) const;
private:
struct PoolState {
VkQueryPool pool = VK_NULL_HANDLE;
Uint32 cursor = 0;
// Frame serial the pool was last harvested + reset for; guards
// against double resets when recording re-begins mid-frame.
Uint64 preparedFrameSerial = 0;
Bool exhaustionWarned = false;
Vector<SharedPtr<TimestampRecord>> pendingRecords;
};
Uint64 MaskToValidBits(Uint64 ticks) const;
// Harvest (or invalidate, when the result never became available)
// every pending record of a pool and clear its pending list.
void DrainPoolPending(PoolState& pool);
VkDevice m_device = VK_NULL_HANDLE;
Float m_timestampPeriodNs = 0.0f;
Uint64 m_validBitsMask = 0;
Uint32 m_slotsPerPool = 0;
Vector<PoolState> m_pools;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -20,6 +20,7 @@
#include "MG_Util/Converters/MGToVk/RenderStateEnumConverter.h"
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
#include "MG_Util/Metrics/TextureMetrics.h"
#include <Config.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
@@ -617,19 +618,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
static Bool ShouldDumpVertexInputStats() {
static const Bool enabled = [] {
const char* value = std::getenv("MOBILEGL_VERTEX_INPUT_STATS");
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
}();
return enabled;
return MG_Config::Features.VertexInputStats;
}
static const char* PresentDumpPath() {
const char* value = std::getenv("MOBILEGL_PRESENT_DUMP_PATH");
return value != nullptr && value[0] != '\0' ? value : nullptr;
const String& path = MG_Config::Features.PresentDumpPath;
return path.empty() ? nullptr : path.c_str();
}
static Bool PresentDumpMatchesTargetCall() {
// MOBILEGL_PRESENT_DUMP_CALL / MOBILEGL_PRESENT_CURRENT_CALL stay live
// getenv on purpose: the retrace harness mutates them at runtime via
// setenv to select which eglSwapBuffers call gets dumped, so they must
// not be snapshotted into MG_Config::Features at init time.
const char* target = std::getenv("MOBILEGL_PRESENT_DUMP_CALL");
if (target == nullptr || target[0] == '\0') {
return true;
@@ -1573,8 +1574,7 @@ void main() {
}
static Bool PresentStatsEnabled() {
const char* value = std::getenv("MOBILEGL_PRESENT_STATS");
return value != nullptr && value[0] == '1' && value[1] == '\0';
return MG_Config::Features.PresentStats;
}
static Bool IsBgraVkFormat(VkFormat format) {
@@ -1894,6 +1894,19 @@ void main() {
});
MOBILEGL_ASSERT(succeeded, "VkBufferManager initialization failed.");
m_bufferManager.SetCopyCommandProvider(this);
if (m_timerQuerySupported) {
m_timerQueryManager = MakeUnique<VkTimerQueryManager>();
if (m_timerQueryManager->Initialize({.device = m_device,
.frameCount = m_frameContext.GetFrameCount(),
.timestampValidBits = m_timestampValidBits,
.timestampPeriodNs = m_timestampPeriodNs})) {
m_frameContext.SetRecordingObserver(this);
} else {
MGLOG_W("VkTimerQueryManager initialization failed; timer queries disabled");
m_timerQueryManager.reset();
m_timerQuerySupported = false;
}
}
m_textureManager = MakeUnique<VkTextureManager>();
MOBILEGL_ASSERT(m_textureManager != nullptr, "VkTextureManager creation failed.");
succeeded = m_textureManager->Initialize(
@@ -1988,6 +2001,13 @@ void main() {
m_vertexInputStateFactory.reset();
m_bufferManager.Shutdown();
// Device is idle (vkDeviceWaitIdle above); query pools can be destroyed.
m_frameContext.SetRecordingObserver(nullptr);
if (m_timerQueryManager) {
m_timerQueryManager->Shutdown();
m_timerQueryManager.reset();
}
if (m_device != VK_NULL_HANDLE) {
m_frameContext.Destroy(m_device, m_commandPool);
}
@@ -5457,6 +5477,63 @@ void main() {
return true;
}
void VulkanRenderer::OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) {
if (m_timerQueryManager) {
m_timerQueryManager->OnFrameCommandRecordingBegan(commandBuffer, m_frameContext.GetCurrentFrameIndex(),
m_bufferManager.GetFrameSerial());
}
}
Bool VulkanRenderer::IsTimerQuerySupported() const {
return m_timerQuerySupported && m_timerQueryManager != nullptr;
}
SharedPtr<VkTimerQueryManager::TimestampRecord> VulkanRenderer::WriteTimerQueryTimestamp() {
if (!IsTimerQuerySupported() || m_device == VK_NULL_HANDLE || m_frameContext.GetFrameCount() == 0) {
return nullptr;
}
auto& frame = m_frameContext.GetCurrent();
if (!frame.isCommandRecording) {
m_frameContext.BeginCommandRecording();
}
// vkCmdWriteTimestamp is valid both inside and outside a render pass,
// so any active render pass is left untouched.
return m_timerQueryManager->WriteTimestamp(frame.commandBuffer, m_frameContext.GetCurrentFrameIndex(),
m_bufferManager.GetFrameSerial());
}
Bool VulkanRenderer::IsTimerQueryResultReady(VkTimerQueryManager::TimestampRecord& record) {
if (record.harvested) {
return true;
}
if (!m_timerQueryManager || !IsFrameSerialComplete(record.frameSerial)) {
return false;
}
return m_timerQueryManager->TryHarvest(record);
}
Bool VulkanRenderer::WaitForTimerQueryResult(VkTimerQueryManager::TimestampRecord& record) {
if (IsTimerQueryResultReady(record)) {
return true;
}
// Mirrors ClientWaitSync: WaitForFrameSerial refuses serials that
// cannot complete without further submissions (a timestamp written
// this frame only executes once Present submits the command buffer).
if (!WaitForFrameSerial(record.frameSerial, UINT64_MAX)) {
return false;
}
return IsTimerQueryResultReady(record);
}
Uint64 VulkanRenderer::GetTimerQueryElapsedNs(const VkTimerQueryManager::TimestampRecord& begin,
const VkTimerQueryManager::TimestampRecord& end) const {
return m_timerQueryManager ? m_timerQueryManager->ElapsedNs(begin, end) : 0;
}
Uint64 VulkanRenderer::GetTimerQueryTimestampNs(const VkTimerQueryManager::TimestampRecord& record) const {
return m_timerQueryManager ? m_timerQueryManager->TimestampNs(record) : 0;
}
void VulkanRenderer::Present() {
MOBILEGL_ASSERT(m_imageIndexAcquired < m_swapchainObject.GetImageCount(),
"Present, acquired image index out of range");
@@ -5473,6 +5550,8 @@ void main() {
const Bool collectPresentStats = (PresentStatsEnabled() || shouldDumpPresent) && frame.isCommandRecording &&
presentStatsExtent.width > 0 && presentStatsExtent.height > 0;
if (PresentStatsEnabled() && presentDumpPath != nullptr) {
// Live getenv on purpose (not MG_Config::Features): the retrace
// harness mutates these two variables at runtime via setenv.
const char* targetCall = std::getenv("MOBILEGL_PRESENT_DUMP_CALL");
const char* currentCall = std::getenv("MOBILEGL_PRESENT_CURRENT_CALL");
std::fprintf(stderr,
@@ -6075,6 +6154,21 @@ void main() {
vkGetDeviceQueue(m_device, m_physicalDevice.queueFamilies.graphicsFamily, 0, &m_graphicsQueue);
vkGetDeviceQueue(m_device, m_physicalDevice.queueFamilies.presentFamily, 0, &m_presentQueue);
MGLOG_I("Queues got successfully.");
// Timestamp (timer query) support: re-enumerate the graphics queue
// family's properties for its timestampValidBits (0 means the queue
// cannot write timestamps) and take timestampPeriod (ns per tick) from
// the device limits.
const auto timestampQueueFamilies = GetQueueFamilyFromPhysicalDevice(m_physicalDevice.handle);
m_timestampValidBits = 0;
const Int32 graphicsFamilyIndex = m_physicalDevice.queueFamilies.graphicsFamily;
if (graphicsFamilyIndex >= 0 && static_cast<SizeT>(graphicsFamilyIndex) < timestampQueueFamilies.size()) {
m_timestampValidBits = timestampQueueFamilies[graphicsFamilyIndex].timestampValidBits;
}
m_timestampPeriodNs = m_physicalDevice.properties.limits.timestampPeriod;
m_timerQuerySupported = m_timestampValidBits > 0 && m_timestampPeriodNs > 0.0f;
MGLOG_I("Timer queries %s (timestampValidBits=%u, timestampPeriod=%f ns/tick)",
m_timerQuerySupported ? "supported" : "not supported", m_timestampValidBits, m_timestampPeriodNs);
}
void VulkanRenderer::CreateAllocator() {
@@ -6327,6 +6421,14 @@ void main() {
vkDeviceWaitIdle(m_device);
if (m_timerQueryManager) {
// The in-progress command buffer is abandoned below (its recording
// flags are force-cleared), so timestamp writes recorded into it
// will never execute; resolve or invalidate all pending records now
// to keep later waits from hanging on never-available queries.
m_timerQueryManager->InvalidatePendingRecords();
}
DestroyDeferredDepthMipmapCleanup();
m_deferredDepthMipmapCleanup.assign(m_frameContext.GetFrameCount(), {});
@@ -20,6 +20,7 @@
#include "VkRenderPassManager.h"
#include "VkSamplerManager.h"
#include "VkTextureManager.h"
#include "VkTimerQueryManager.h"
#include "MG_Util/Math/VectorTypes.h"
#include <Includes.h>
#include <vk_mem_alloc.h>
@@ -101,7 +102,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
};
class VulkanRenderer : public IBufferCopyCommandProvider {
class VulkanRenderer : public IBufferCopyCommandProvider, public FrameContext::IRecordingObserver {
public:
VulkanRenderer(NativeWindowType window, const VulkanRendererConfig& cfg = {});
~VulkanRenderer();
@@ -113,6 +114,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// render pass, for immediate staged buffer copies.
VkCommandBuffer AcquireBufferCopyCommandBuffer() override;
// FrameContext::IRecordingObserver: prepares the frame's timer-query
// pool (harvest + reset) right after the frame command buffer begins
// recording, before any render pass.
void OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) override;
Bool SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
const DrawCmdParam& drawParams,
const IndexBufferView* pIndexBufferView = nullptr);
@@ -178,6 +184,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// current, not-yet-presented frame) or when the wait failed.
Bool WaitForFrameSerial(Uint64 serial, Uint64 timeoutNs);
// GPU timer queries, backing the GL_TIME_ELAPSED / GL_TIMESTAMP
// frontend. Timestamp support (queue timestampValidBits > 0 and a
// non-zero timestampPeriod) is cached at device creation.
Bool IsTimerQuerySupported() const;
// Ensures the frame command buffer is recording (same lazy pattern as
// SetupDraw) and writes a bottom-of-pipe timestamp into the current
// frame's pool. Null when unsupported or the pool is exhausted.
SharedPtr<VkTimerQueryManager::TimestampRecord> WriteTimerQueryTimestamp();
// Non-blocking: true once the record's raw ticks are on the CPU
// (harvests the slot once its frame serial has completed).
Bool IsTimerQueryResultReady(VkTimerQueryManager::TimestampRecord& record);
// Blocking wait, mirroring ClientWaitSync's caveat: a record written
// this frame cannot complete until Present submits the commands, so
// this returns false (result reads as 0) instead of deadlocking.
Bool WaitForTimerQueryResult(VkTimerQueryManager::TimestampRecord& record);
Uint64 GetTimerQueryElapsedNs(const VkTimerQueryManager::TimestampRecord& begin,
const VkTimerQueryManager::TimestampRecord& end) const;
Uint64 GetTimerQueryTimestampNs(const VkTimerQueryManager::TimestampRecord& record) const;
void RequestSwapchainResize(Uint32 width, Uint32 height);
void RecreateSwapchain();
@@ -246,6 +271,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool m_multiDrawIndirectFeatureEnabled = false;
Bool m_shaderDrawParametersExtensionEnabled = false;
Bool m_shaderDrawParametersFeatureEnabled = false;
// Cached at device creation from the graphics queue family properties
// and device limits; drives timer-query support.
Uint32 m_timestampValidBits = 0;
Float m_timestampPeriodNs = 0.0f;
Bool m_timerQuerySupported = false;
using PFNDrawIndexedIndirectCountFunc = void(VKAPI_PTR*)(VkCommandBuffer commandBuffer, VkBuffer buffer,
VkDeviceSize offset, VkBuffer countBuffer,
VkDeviceSize countBufferOffset, Uint32 maxDrawCount,
@@ -268,6 +298,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
UniquePtr<VkRenderPassManager> m_renderPassManager;
UniquePtr<VkTextureManager> m_textureManager;
UniquePtr<VkSamplerManager> m_samplerManager;
UniquePtr<VkTimerQueryManager> m_timerQueryManager;
BlitResources m_blitResources;
DepthMipmapResources m_depthMipmapResources;
Vector<DeferredDepthMipmapCleanup> m_deferredDepthMipmapCleanup;
@@ -11,6 +11,7 @@
#include "../Getter/GL_Getter.h"
#include "../Sampler/GL_Sampler.h"
#include "../Sync/GL_Sync.h"
#include "../Query/GL_Query.h"
#include "../Texture/GL_Texture.h"
#include "../Drawing/GL_Drawing.h"
#include "../Program/GL_Program.h"
@@ -209,21 +210,13 @@ DECLARE_GL_FUNCTION_HEAD(void, TexSubImage3D, GLenum target, GLint level, GLint
DECLARE_GL_FUNCTION_HEAD(void, CopyTexSubImage3D, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTexSubImage3D, target, level, xoffset, yoffset, zoffset, x, y, width, height)
DECLARE_GL_FUNCTION_HEAD(void, CompressedTexImage3D, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTexImage3D, target, level, internalformat, width, height, depth, border, imageSize, data)
DECLARE_GL_FUNCTION_HEAD(void, CompressedTexSubImage3D, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTexSubImage3D, target, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GenQueries, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GenQueries, n, ids)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteQueries, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteQueries, n, ids)
DECLARE_GL_FUNCTION_STUB_HEAD(GLboolean, IsQuery, GLuint id) DECLARE_GL_FUNCTION_STUB_END(GLboolean, IsQuery, id)
MOBILEGL_GL_API void glBeginQuery(GLenum target, GLuint id) {
MGLOG_W("Stub function: %s(...)", __FUNCTION__);
if (id != 0) {
MobileGL::MG_State::pGLContext->RecordError(
MobileGL::ErrorCode::InvalidOperation,
MobileGL::MakeUnique<MobileGL::GenericErrorInfo>("MG_Impl/GLImpl", __FUNCTION__,
"Query object does not exist."));
}
}
DECLARE_GL_FUNCTION_STUB_HEAD(void, EndQuery, GLenum target) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, EndQuery, target)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryiv, GLenum target, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryiv, target, pname, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryObjectuiv, GLuint id, GLenum pname, GLuint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryObjectuiv, id, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, GenQueries, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GenQueries, n, ids)
DECLARE_GL_FUNCTION_HEAD(void, DeleteQueries, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DeleteQueries, n, ids)
DECLARE_GL_FUNCTION_HEAD(GLboolean, IsQuery, GLuint id) DECLARE_GL_FUNCTION_END(GLboolean, IsQuery, id)
DECLARE_GL_FUNCTION_HEAD(void, BeginQuery, GLenum target, GLuint id) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BeginQuery, target, id)
DECLARE_GL_FUNCTION_HEAD(void, EndQuery, GLenum target) DECLARE_GL_FUNCTION_END_NO_RETURN(void, EndQuery, target)
DECLARE_GL_FUNCTION_HEAD(void, GetQueryiv, GLenum target, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetQueryiv, target, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, GetQueryObjectuiv, GLuint id, GLenum pname, GLuint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetQueryObjectuiv, id, pname, params)
DECLARE_GL_FUNCTION_HEAD(GLboolean, UnmapBuffer, GLenum target) DECLARE_GL_FUNCTION_END(GLboolean, UnmapBuffer, target)
DECLARE_GL_FUNCTION_HEAD(void, GetBufferPointerv, GLenum target, GLenum pname, void** params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetBufferPointerv, target, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, DrawBuffers, GLsizei n, const GLenum* bufs) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawBuffers, n, bufs)
@@ -836,7 +829,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, WindowPos3i, GLint x, GLint y, GLint z) DECL
DECLARE_GL_FUNCTION_STUB_HEAD(void, WindowPos3iv, const GLint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, WindowPos3iv, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, WindowPos3s, GLshort x, GLshort y, GLshort z) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, WindowPos3s, x, y, z)
DECLARE_GL_FUNCTION_STUB_HEAD(void, WindowPos3sv, const GLshort* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, WindowPos3sv, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryObjectiv, GLuint id, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryObjectiv, id, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, GetQueryObjectiv, GLuint id, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetQueryObjectiv, id, pname, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetBufferSubData, GLenum target, GLintptr offset, GLsizeiptr size, void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetBufferSubData, target, offset, size, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetVertexAttribdv, GLuint index, GLenum pname, GLdouble* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetVertexAttribdv, index, pname, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib1d, GLuint index, GLdouble x) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib1d, index, x)
@@ -875,9 +868,9 @@ DECLARE_GL_FUNCTION_HEAD(void, TexImage2DMultisample, GLenum target, GLsizei sam
DECLARE_GL_FUNCTION_HEAD(void, TexImage3DMultisample, GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLboolean fixedsamplelocations) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexImage3DMultisample, target, samples, internalformat, width, height, depth, fixedsamplelocations)
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindFragDataLocationIndexed, GLuint program, GLuint colorNumber, GLuint index, const GLchar* name) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindFragDataLocationIndexed, program, colorNumber, index, name)
DECLARE_GL_FUNCTION_STUB_HEAD(GLint, GetFragDataIndex, GLuint program, const GLchar* name) DECLARE_GL_FUNCTION_STUB_END(GLint, GetFragDataIndex, program, name)
DECLARE_GL_FUNCTION_STUB_HEAD(void, QueryCounter, GLuint id, GLenum target) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, QueryCounter, id, target)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryObjecti64v, GLuint id, GLenum pname, GLint64* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryObjecti64v, id, pname, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryObjectui64v, GLuint id, GLenum pname, GLuint64* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryObjectui64v, id, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, QueryCounter, GLuint id, GLenum target) DECLARE_GL_FUNCTION_END_NO_RETURN(void, QueryCounter, id, target)
DECLARE_GL_FUNCTION_HEAD(void, GetQueryObjecti64v, GLuint id, GLenum pname, GLint64* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetQueryObjecti64v, id, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, GetQueryObjectui64v, GLuint id, GLenum pname, GLuint64* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetQueryObjectui64v, id, pname, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribP1ui, GLuint index, GLenum type, GLboolean normalized, GLuint value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribP1ui, index, type, normalized, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribP1uiv, GLuint index, GLenum type, GLboolean normalized, const GLuint* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribP1uiv, index, type, normalized, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribP2ui, GLuint index, GLenum type, GLboolean normalized, GLuint value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribP2ui, index, type, normalized, value)
@@ -2111,8 +2104,8 @@ DECLARE_GL_FUNCTION_STUB_HEAD(GLboolean, AreTexturesResidentEXT, GLsizei n, cons
DECLARE_GL_FUNCTION_STUB_HEAD(void, PrioritizeTexturesEXT, GLsizei n, const GLuint* textures, const GLclampf* priorities) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PrioritizeTexturesEXT, n, textures, priorities)
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureNormalEXT, GLenum mode) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureNormalEXT, mode)
DECLARE_GL_FUNCTION_HEAD(void, TexStorage1DEXT, GLenum target, GLsizei levels, GLenum internalformat, GLsizei width) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexStorage1D, target, levels, internalformat, width)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryObjecti64vEXT, GLuint id, GLenum pname, GLint64* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryObjecti64vEXT, id, pname, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryObjectui64vEXT, GLuint id, GLenum pname, GLuint64* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryObjectui64vEXT, id, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, GetQueryObjecti64vEXT, GLuint id, GLenum pname, GLint64* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetQueryObjecti64v, id, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, GetQueryObjectui64vEXT, GLuint id, GLenum pname, GLuint64* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetQueryObjectui64v, id, pname, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindBufferOffsetEXT, GLenum target, GLuint index, GLuint buffer, GLintptr offset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindBufferOffsetEXT, target, index, buffer, offset)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ArrayElementEXT, GLint i) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ArrayElementEXT, i)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ColorPointerEXT, GLint size, GLenum type, GLsizei stride, GLsizei count, const void* pointer) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ColorPointerEXT, size, type, stride, count, pointer)
+22 -2
View File
@@ -793,6 +793,18 @@ namespace MobileGL::MG_Impl::GLImpl {
params[0] = static_cast<GLint64>(value);
return;
}
case GL_TIMESTAMP: {
// Handled here (not via the 32-bit GetIntegerv fallback) so the
// full 64-bit GPU timestamp survives; LWJGL reads it this way.
Int64 timestamp = 0;
if (!MG_Config::Features.DisableTimerQuery) {
if (const auto getGpuTimestampNs = MG_Backend::gBackendFunctionsTable.GL.GetGpuTimestampNs) {
timestamp = getGpuTimestampNs();
}
}
params[0] = static_cast<GLint64>(timestamp);
return;
}
default:
break;
}
@@ -1563,9 +1575,17 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT:
*params = 0; // texture-buffer range entrypoints are stubbed
return;
case GL_TIMESTAMP:
*params = 0; // timer-query entrypoints are stubbed
case GL_TIMESTAMP: {
Int64 timestamp = 0;
if (!MG_Config::Features.DisableTimerQuery) {
if (const auto getGpuTimestampNs = MG_Backend::gBackendFunctionsTable.GL.GetGpuTimestampNs) {
timestamp = getGpuTimestampNs();
}
}
// 32-bit query: clamp per the GL state-query conversion rules.
*params = timestamp > static_cast<Int64>(INT_MAX) ? INT_MAX : static_cast<GLint>(timestamp);
return;
}
case GL_TRANSFORM_FEEDBACK_BUFFER_BINDING:
if (const auto& obj =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::TransformFeedback).GetBoundObject()) {
+362
View File
@@ -0,0 +1,362 @@
// 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 <Config.h>
#include <MG_Backend/BackendObjects.h>
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/ErrorState/ErrorInfo.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
MG_Backend::BackendQueryHandle backendHandle = nullptr;
Bool active = false;
Bool ended = false;
Bool resultCached = false;
Uint64 cachedResult = 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;
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));
}
// 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) {
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) {
endTimeElapsedQuery(queryObject->backendHandle);
}
queryObject->active = false;
queryObject->ended = true;
g_activeTimeElapsedQueryId = 0;
}
// Shared GetQueryObject* implementation. Returns false when an error
// was recorded and no value should be written back.
Bool GetQueryObjectValue(GLuint id, GLenum pname, const char* function, Uint64& outValue) {
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_RESULT_AVAILABLE: {
if (queryObject->resultCached || !queryObject->backendHandle) {
outValue = 1;
return true;
}
const auto isQueryResultAvailable = MG_Backend::gBackendFunctionsTable.GL.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;
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;
}
// 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) {
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;
}
}
} // 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;
}
}
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) {
EndTimeElapsedQueryLocked(queryObject); // implicitly end before deletion
}
if (queryObject->backendHandle) {
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.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);
// Gen'd ids count as query objects here: the registry creates live
// objects at GenQueries time.
return FindQueryObjectLocked(id) != nullptr ? GL_TRUE : GL_FALSE;
}
void BeginQuery(GLenum target, GLuint id) {
if (target != GL_TIME_ELAPSED) {
// Only GL_TIME_ELAPSED timer queries are implemented (occlusion and
// primitive queries remain stubs); GL_TIMESTAMP is not a valid
// BeginQuery target either.
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;
}
if (g_activeTimeElapsedQueryId != 0) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__,
"A query is already active on GL_TIME_ELAPSED.");
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;
const auto beginTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.BeginTimeElapsedQuery;
queryObject->backendHandle =
(!TimerQueryDisabled() && beginTimeElapsedQuery) ? beginTimeElapsedQuery() : nullptr;
g_activeTimeElapsedQueryId = id;
}
void EndQuery(GLenum target) {
if (target != GL_TIME_ELAPSED) {
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported.");
return;
}
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
if (g_activeTimeElapsedQueryId == 0) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "No query is active on GL_TIME_ELAPSED.");
return;
}
auto* queryObject = FindQueryObjectLocked(g_activeTimeElapsedQueryId);
if (!queryObject) {
g_activeTimeElapsedQueryId = 0; // should not happen; keep state consistent
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;
queryObject->backendHandle =
(!TimerQueryDisabled() && queryCounterTimestamp) ? queryCounterTimestamp() : nullptr;
queryObject->ended = true;
}
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);
// 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<GLint>(g_activeTimeElapsedQueryId) : 0;
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. Non-timer targets remain unimplemented and report 0.
const Bool timerTarget = target == GL_TIME_ELAPSED || target == GL_TIMESTAMP;
const auto isTimerQuerySupported = MG_Backend::gBackendFunctionsTable.GL.IsTimerQuerySupported;
const Bool supported =
timerTarget && !TimerQueryDisabled() && isTimerQuerySupported && isTimerQuerySupported();
*params = supported ? 64 : 0;
return;
}
default:
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Unsupported query parameter.");
return;
}
}
void GetQueryObjectiv(GLuint id, GLenum pname, GLint* params) {
Uint64 value = 0;
if (!GetQueryObjectValue(id, pname, __FUNCTION__, value) || !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;
if (!GetQueryObjectValue(id, pname, __FUNCTION__, value) || !params) {
return;
}
*params = static_cast<GLuint>(value & 0xFFFFFFFFull);
}
void GetQueryObjecti64v(GLuint id, GLenum pname, GLint64* params) {
Uint64 value = 0;
if (!GetQueryObjectValue(id, pname, __FUNCTION__, value) || !params) {
return;
}
*params = static_cast<GLint64>(value);
}
void GetQueryObjectui64v(GLuint id, GLenum pname, GLuint64* params) {
Uint64 value = 0;
if (!GetQueryObjectValue(id, pname, __FUNCTION__, value) || !params) {
return;
}
*params = static_cast<GLuint64>(value);
}
} // namespace MobileGL::MG_Impl::GLImpl
+24
View File
@@ -0,0 +1,24 @@
// MobileGL - MobileGL/MG_Impl/GLImpl/Query/GL_Query.h
// 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
#pragma once
#include <Includes.h>
namespace MobileGL::MG_Impl::GLImpl {
void GenQueries(GLsizei n, GLuint* ids);
void DeleteQueries(GLsizei n, const GLuint* ids);
GLboolean IsQuery(GLuint id);
void BeginQuery(GLenum target, GLuint id);
void EndQuery(GLenum target);
void GetQueryiv(GLenum target, GLenum pname, GLint* params);
void GetQueryObjectiv(GLuint id, GLenum pname, GLint* params);
void GetQueryObjectuiv(GLuint id, GLenum pname, GLuint* params);
void GetQueryObjecti64v(GLuint id, GLenum pname, GLint64* params);
void GetQueryObjectui64v(GLuint id, GLenum pname, GLuint64* params);
void QueryCounter(GLuint id, GLenum target);
} // namespace MobileGL::MG_Impl::GLImpl
+1
View File
@@ -71,6 +71,7 @@ add_subdirectory(Framebuffer)
add_subdirectory(Texture)
add_subdirectory(VertexArray)
add_subdirectory(Program)
add_subdirectory(Query)
if (ENABLE_INTEGRATION_TESTS)
add_subdirectory(Backend/DirectVulkan)
endif()
+20
View File
@@ -0,0 +1,20 @@
cmake_minimum_required(VERSION 3.14)
add_executable(
QueryTest
QueryTest.cpp
)
target_include_directories(QueryTest PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
)
target_link_libraries(
QueryTest PRIVATE
GTest::gtest_main
${LINK_LIBRARIES}
)
include(GoogleTest)
gtest_discover_tests(QueryTest DISCOVERY_TIMEOUT 30)
+439
View File
@@ -0,0 +1,439 @@
// MobileGL - MobileGL/MG_Test/Query/QueryTest.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 <gtest/gtest.h>
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include "Includes.h"
#include "Init.h"
#include <Config.h>
#include <MG_Backend/BackendObjects.h>
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
#include <MG_Impl/GLImpl/Query/GL_Query.h>
using namespace MobileGL;
namespace {
// On the ctest host no ES context is ever current, but the timer-query
// POINTERS of gBackendFunctionsTable.GL are NOT null: MG_Backend::Init
// populates the real DirectGLES hooks (see GetBackendFunctions in
// BackendObject_DirectGLES.cpp, which installs them whenever
// MOBILEGL_DISABLE_TIMERQUERY is unset). Without a current ES context
// those hooks degrade to returning null HANDLES, so the fallback these
// tests exercise is the frontend's null-handle path (results immediately
// available and zero), not a null-pointer path. Tests that need
// controllable backend behavior install stubs and restore the table
// afterwards.
// Snapshots MG_Config::Features on construction and restores it on
// destruction, so a test that flips DisableTimerQuery cannot leak the
// setting into later tests even if an assertion unwinds the test body
// early (same pattern as SanityTest's ScopedGLESCapabilitiesOverride).
struct ScopedFeaturesOverride {
ScopedFeaturesOverride(): m_snapshot(MG_Config::Features) {}
~ScopedFeaturesOverride() { MG_Config::Features = m_snapshot; }
ScopedFeaturesOverride(const ScopedFeaturesOverride&) = delete;
ScopedFeaturesOverride& operator=(const ScopedFeaturesOverride&) = delete;
private:
MG_Config::FeaturesTable m_snapshot;
};
// Snapshots the global backend function table on construction and restores
// it on destruction, so stub timer-query pointers cannot leak into later
// tests.
struct ScopedBackendFunctionsOverride {
ScopedBackendFunctionsOverride(): m_snapshot(MG_Backend::gBackendFunctionsTable) {}
~ScopedBackendFunctionsOverride() { MG_Backend::gBackendFunctionsTable = m_snapshot; }
ScopedBackendFunctionsOverride(const ScopedBackendFunctionsOverride&) = delete;
ScopedBackendFunctionsOverride& operator=(const ScopedBackendFunctionsOverride&) = delete;
private:
MG_Backend::GlobalBackendFunctionsTable m_snapshot;
};
// Stub backend timer-query implementation (plain function pointers, so the
// observable state lives in file-scope globals).
Int g_stubBeginCount = 0;
Int g_stubEndCount = 0;
Int g_stubCounterCount = 0;
Int g_stubDeleteCount = 0;
Bool g_stubTimerQuerySupported = true;
Bool g_stubResultAvailable = true;
// false = GetQueryResult64 cannot produce the result YET (returns false
// without writing outNanoseconds), mirroring e.g. a Vulkan wait on a
// not-yet-submitted frame serial.
Bool g_stubResultObtainable = true;
Uint64 g_stubResultNs = 0;
Bool StubIsTimerQuerySupported() { return g_stubTimerQuerySupported; }
MG_Backend::BackendQueryHandle StubBeginTimeElapsedQuery() {
++g_stubBeginCount;
return reinterpret_cast<MG_Backend::BackendQueryHandle>(static_cast<uintptr_t>(0x51));
}
void StubEndTimeElapsedQuery(MG_Backend::BackendQueryHandle) { ++g_stubEndCount; }
MG_Backend::BackendQueryHandle StubQueryCounterTimestamp() {
++g_stubCounterCount;
return reinterpret_cast<MG_Backend::BackendQueryHandle>(static_cast<uintptr_t>(0x52));
}
Bool StubIsQueryResultAvailable(MG_Backend::BackendQueryHandle) { return g_stubResultAvailable; }
Bool StubGetQueryResult64(MG_Backend::BackendQueryHandle, Bool, Uint64* outNanoseconds) {
if (!g_stubResultObtainable) {
return false;
}
*outNanoseconds = g_stubResultNs;
return true;
}
void StubDeleteBackendQuery(MG_Backend::BackendQueryHandle) { ++g_stubDeleteCount; }
void InstallStubBackendTimerQueries() {
auto& backendGL = MG_Backend::gBackendFunctionsTable.GL;
backendGL.IsTimerQuerySupported = StubIsTimerQuerySupported;
backendGL.BeginTimeElapsedQuery = StubBeginTimeElapsedQuery;
backendGL.EndTimeElapsedQuery = StubEndTimeElapsedQuery;
backendGL.QueryCounterTimestamp = StubQueryCounterTimestamp;
backendGL.IsQueryResultAvailable = StubIsQueryResultAvailable;
backendGL.GetQueryResult64 = StubGetQueryResult64;
backendGL.DeleteBackendQuery = StubDeleteBackendQuery;
g_stubBeginCount = 0;
g_stubEndCount = 0;
g_stubCounterCount = 0;
g_stubDeleteCount = 0;
g_stubTimerQuerySupported = true;
g_stubResultAvailable = true;
g_stubResultObtainable = true;
g_stubResultNs = 0;
}
} // namespace
class QueryTest : public ::testing::Test {
protected:
void SetUp() override {
MobileGL::Initialize();
// Drain errors recorded by earlier tests so assertions here are
// attributable to this test alone (GetError pops one queued error
// per call).
while (MG_Impl::GLImpl::GetError() != GL_NO_ERROR) {
}
}
};
TEST_F(QueryTest, GenQueriesReturnsDistinctNonzeroIdsAndTracksLiveness) {
GLuint ids[3] = {0, 0, 0};
MG_Impl::GLImpl::GenQueries(3, ids);
EXPECT_NE(ids[0], 0u);
EXPECT_NE(ids[1], 0u);
EXPECT_NE(ids[2], 0u);
EXPECT_NE(ids[0], ids[1]);
EXPECT_NE(ids[0], ids[2]);
EXPECT_NE(ids[1], ids[2]);
EXPECT_EQ(MG_Impl::GLImpl::IsQuery(0), GL_FALSE);
EXPECT_EQ(MG_Impl::GLImpl::IsQuery(ids[0]), GL_TRUE);
EXPECT_EQ(MG_Impl::GLImpl::IsQuery(ids[1]), GL_TRUE);
EXPECT_EQ(MG_Impl::GLImpl::IsQuery(ids[2]), GL_TRUE);
MG_Impl::GLImpl::DeleteQueries(3, ids);
EXPECT_EQ(MG_Impl::GLImpl::IsQuery(ids[0]), GL_FALSE);
EXPECT_EQ(MG_Impl::GLImpl::IsQuery(ids[1]), GL_FALSE);
EXPECT_EQ(MG_Impl::GLImpl::IsQuery(ids[2]), GL_FALSE);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(QueryTest, TimeElapsedSpanFallsBackToImmediateZeroResult) {
GLuint id = 0;
MG_Impl::GLImpl::GenQueries(1, &id);
ASSERT_NE(id, 0u);
MG_Impl::GLImpl::BeginQuery(GL_TIME_ELAPSED, id);
MG_Impl::GLImpl::EndQuery(GL_TIME_ELAPSED);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// With null backend timer-query pointers (no ES context on the test host)
// the result is immediately available and reads as zero.
GLint available = -1;
MG_Impl::GLImpl::GetQueryObjectiv(id, GL_QUERY_RESULT_AVAILABLE, &available);
EXPECT_EQ(available, 1);
GLint64 result = -1;
MG_Impl::GLImpl::GetQueryObjecti64v(id, GL_QUERY_RESULT, &result);
EXPECT_EQ(result, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::DeleteQueries(1, &id);
}
TEST_F(QueryTest, NestedBeginQueryRecordsInvalidOperation) {
GLuint ids[2] = {0, 0};
MG_Impl::GLImpl::GenQueries(2, ids);
MG_Impl::GLImpl::BeginQuery(GL_TIME_ELAPSED, ids[0]);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::BeginQuery(GL_TIME_ELAPSED, ids[1]);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
// The failed nested begin must not have displaced the active query.
GLint currentQuery = -1;
MG_Impl::GLImpl::GetQueryiv(GL_TIME_ELAPSED, GL_CURRENT_QUERY, &currentQuery);
EXPECT_EQ(currentQuery, static_cast<GLint>(ids[0]));
MG_Impl::GLImpl::EndQuery(GL_TIME_ELAPSED);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::DeleteQueries(2, ids);
}
TEST_F(QueryTest, EndQueryWithoutActiveQueryRecordsInvalidOperation) {
MG_Impl::GLImpl::EndQuery(GL_TIME_ELAPSED);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
}
TEST_F(QueryTest, QueryCounterTimestampResultImmediatelyAvailable) {
GLuint id = 0;
MG_Impl::GLImpl::GenQueries(1, &id);
ASSERT_NE(id, 0u);
MG_Impl::GLImpl::QueryCounter(id, GL_TIMESTAMP);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
GLint available = -1;
MG_Impl::GLImpl::GetQueryObjectiv(id, GL_QUERY_RESULT_AVAILABLE, &available);
EXPECT_EQ(available, 1);
GLuint64 result = 123u;
MG_Impl::GLImpl::GetQueryObjectui64v(id, GL_QUERY_RESULT, &result);
EXPECT_EQ(result, 0u);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::DeleteQueries(1, &id);
}
TEST_F(QueryTest, GetQueryivCurrentQueryTracksActiveId) {
GLint currentQuery = -1;
MG_Impl::GLImpl::GetQueryiv(GL_TIME_ELAPSED, GL_CURRENT_QUERY, &currentQuery);
EXPECT_EQ(currentQuery, 0);
GLuint id = 0;
MG_Impl::GLImpl::GenQueries(1, &id);
ASSERT_NE(id, 0u);
MG_Impl::GLImpl::BeginQuery(GL_TIME_ELAPSED, id);
MG_Impl::GLImpl::GetQueryiv(GL_TIME_ELAPSED, GL_CURRENT_QUERY, &currentQuery);
EXPECT_EQ(currentQuery, static_cast<GLint>(id));
// GL_TIMESTAMP queries are never active, so GL_CURRENT_QUERY stays 0 there.
MG_Impl::GLImpl::GetQueryiv(GL_TIMESTAMP, GL_CURRENT_QUERY, &currentQuery);
EXPECT_EQ(currentQuery, 0);
MG_Impl::GLImpl::EndQuery(GL_TIME_ELAPSED);
MG_Impl::GLImpl::GetQueryiv(GL_TIME_ELAPSED, GL_CURRENT_QUERY, &currentQuery);
EXPECT_EQ(currentQuery, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::DeleteQueries(1, &id);
}
TEST_F(QueryTest, QueryCounterBitsReportsZeroWhenTimerQueryDisabled) {
const ScopedFeaturesOverride featuresGuard;
const ScopedBackendFunctionsOverride backendGuard;
InstallStubBackendTimerQueries();
// With the stub backend reporting live timer-query support and the
// feature enabled, the frontend advertises 64-bit counters for both timer
// targets.
MG_Config::Features.DisableTimerQuery = false;
GLint counterBits = -1;
MG_Impl::GLImpl::GetQueryiv(GL_TIME_ELAPSED, GL_QUERY_COUNTER_BITS, &counterBits);
EXPECT_EQ(counterBits, 64);
MG_Impl::GLImpl::GetQueryiv(GL_TIMESTAMP, GL_QUERY_COUNTER_BITS, &counterBits);
EXPECT_EQ(counterBits, 64);
// MOBILEGL_DISABLE_TIMERQUERY zeroes the advertised counter bits even when
// the backend supports timer queries.
MG_Config::Features.DisableTimerQuery = true;
MG_Impl::GLImpl::GetQueryiv(GL_TIME_ELAPSED, GL_QUERY_COUNTER_BITS, &counterBits);
EXPECT_EQ(counterBits, 0);
MG_Impl::GLImpl::GetQueryiv(GL_TIMESTAMP, GL_QUERY_COUNTER_BITS, &counterBits);
EXPECT_EQ(counterBits, 0);
// A disabled span must not touch the backend and must read back as an
// immediately available zero result.
GLuint id = 0;
MG_Impl::GLImpl::GenQueries(1, &id);
MG_Impl::GLImpl::BeginQuery(GL_TIME_ELAPSED, id);
MG_Impl::GLImpl::EndQuery(GL_TIME_ELAPSED);
EXPECT_EQ(g_stubBeginCount, 0);
GLint available = -1;
MG_Impl::GLImpl::GetQueryObjectiv(id, GL_QUERY_RESULT_AVAILABLE, &available);
EXPECT_EQ(available, 1);
GLuint64 result = 123u;
MG_Impl::GLImpl::GetQueryObjectui64v(id, GL_QUERY_RESULT, &result);
EXPECT_EQ(result, 0u);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::DeleteQueries(1, &id);
}
TEST_F(QueryTest, QueryCounterBitsReportsZeroWhenBackendUnsupported) {
const ScopedFeaturesOverride featuresGuard;
const ScopedBackendFunctionsOverride backendGuard;
InstallStubBackendTimerQueries();
MG_Config::Features.DisableTimerQuery = false;
// All backend hooks are installed and the feature is enabled, but the
// dynamic support check says the live backend cannot time right now
// (e.g. missing extension or zero timestamp valid bits) - the advertised
// counter bits must read 0 for both timer targets.
g_stubTimerQuerySupported = false;
GLint counterBits = -1;
MG_Impl::GLImpl::GetQueryiv(GL_TIME_ELAPSED, GL_QUERY_COUNTER_BITS, &counterBits);
EXPECT_EQ(counterBits, 0);
counterBits = -1;
MG_Impl::GLImpl::GetQueryiv(GL_TIMESTAMP, GL_QUERY_COUNTER_BITS, &counterBits);
EXPECT_EQ(counterBits, 0);
// Support coming back (fresh caps after a context recreation) flips the
// advertisement back to 64 without reinstalling the table.
g_stubTimerQuerySupported = true;
MG_Impl::GLImpl::GetQueryiv(GL_TIME_ELAPSED, GL_QUERY_COUNTER_BITS, &counterBits);
EXPECT_EQ(counterBits, 64);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(QueryTest, FailedResultWaitKeepsHandleUntilResultLands) {
const ScopedFeaturesOverride featuresGuard;
const ScopedBackendFunctionsOverride backendGuard;
InstallStubBackendTimerQueries();
MG_Config::Features.DisableTimerQuery = false;
g_stubResultObtainable = false;
g_stubResultNs = 777;
GLuint id = 0;
MG_Impl::GLImpl::GenQueries(1, &id);
ASSERT_NE(id, 0u);
MG_Impl::GLImpl::BeginQuery(GL_TIME_ELAPSED, id);
MG_Impl::GLImpl::EndQuery(GL_TIME_ELAPSED);
// A wait the backend cannot satisfy yet (e.g. Vulkan refusing to block on
// the current unsubmitted frame serial) reads 0 for this call - without
// recording an error - and must NOT consume the backend handle or cache
// the zero.
GLuint64 result = 123u;
MG_Impl::GLImpl::GetQueryObjectui64v(id, GL_QUERY_RESULT, &result);
EXPECT_EQ(result, 0u);
EXPECT_EQ(g_stubDeleteCount, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// Repeated failed waits behave identically...
result = 123u;
MG_Impl::GLImpl::GetQueryObjectui64v(id, GL_QUERY_RESULT, &result);
EXPECT_EQ(result, 0u);
EXPECT_EQ(g_stubDeleteCount, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// ...and availability still polls the backend afterwards (the failed
// read did not force-complete the query).
GLint available = -1;
g_stubResultAvailable = false;
MG_Impl::GLImpl::GetQueryObjectiv(id, GL_QUERY_RESULT_AVAILABLE, &available);
EXPECT_EQ(available, 0);
g_stubResultAvailable = true;
MG_Impl::GLImpl::GetQueryObjectiv(id, GL_QUERY_RESULT_AVAILABLE, &available);
EXPECT_EQ(available, 1);
// Once the backend can produce the value, the same query returns the
// real result; only then is the backend handle released and the value
// cached for later reads.
g_stubResultObtainable = true;
result = 0;
MG_Impl::GLImpl::GetQueryObjectui64v(id, GL_QUERY_RESULT, &result);
EXPECT_EQ(result, 777u);
EXPECT_EQ(g_stubDeleteCount, 1);
result = 0;
MG_Impl::GLImpl::GetQueryObjectui64v(id, GL_QUERY_RESULT, &result);
EXPECT_EQ(result, 777u);
EXPECT_EQ(g_stubDeleteCount, 1);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::DeleteQueries(1, &id);
EXPECT_EQ(g_stubDeleteCount, 1); // handle already released by the result read
}
TEST_F(QueryTest, BackendResultsPropagateThroughFrontend) {
const ScopedFeaturesOverride featuresGuard;
const ScopedBackendFunctionsOverride backendGuard;
InstallStubBackendTimerQueries();
MG_Config::Features.DisableTimerQuery = false;
g_stubResultAvailable = false;
g_stubResultNs = 42;
GLuint id = 0;
MG_Impl::GLImpl::GenQueries(1, &id);
MG_Impl::GLImpl::BeginQuery(GL_TIME_ELAPSED, id);
MG_Impl::GLImpl::EndQuery(GL_TIME_ELAPSED);
EXPECT_EQ(g_stubBeginCount, 1);
EXPECT_EQ(g_stubEndCount, 1);
// Availability follows the backend while the result has not been read.
GLint available = -1;
MG_Impl::GLImpl::GetQueryObjectiv(id, GL_QUERY_RESULT_AVAILABLE, &available);
EXPECT_EQ(available, 0);
g_stubResultAvailable = true;
MG_Impl::GLImpl::GetQueryObjectiv(id, GL_QUERY_RESULT_AVAILABLE, &available);
EXPECT_EQ(available, 1);
// Reading the result consumes the backend handle exactly once and caches
// the value for later reads.
GLuint64 result = 0;
MG_Impl::GLImpl::GetQueryObjectui64v(id, GL_QUERY_RESULT, &result);
EXPECT_EQ(result, 42u);
EXPECT_EQ(g_stubDeleteCount, 1);
result = 0;
MG_Impl::GLImpl::GetQueryObjectui64v(id, GL_QUERY_RESULT, &result);
EXPECT_EQ(result, 42u);
EXPECT_EQ(g_stubDeleteCount, 1);
MG_Impl::GLImpl::DeleteQueries(1, &id);
EXPECT_EQ(g_stubDeleteCount, 1); // handle already released by the result read
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// Environment-agnostic property test for the env -> ConfigLoader -> Features
// chain: whatever MOBILEGL_DISABLE_TIMERQUERY is set to in the environment of
// this test process, MG_ConfigLoader::Init must have parsed it with the
// unified truthy rule (set, non-empty, not "0", case-insensitive not "false").
// Running the binary under MOBILEGL_DISABLE_TIMERQUERY=1 therefore exercises
// the real end-to-end path rather than the struct field alone.
TEST_F(QueryTest, DisableTimerQueryFeatureMatchesEnvironment) {
const char* raw = std::getenv("MOBILEGL_DISABLE_TIMERQUERY");
Bool expected = false;
if (raw != nullptr && raw[0] != '\0') {
String value = raw;
String lowered = value;
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
expected = value != "0" && lowered != "false";
}
EXPECT_EQ(MG_Config::Features.DisableTimerQuery, expected);
}
@@ -8,21 +8,21 @@
#include "Loader.h"
#include "MG_Util/Types.h"
#include <Config.h>
#if defined(MOBILEGL_IOS)
#include <dlfcn.h>
#endif
namespace MobileGL::MG_Util::BackendLoader {
static Bool UseRetraceAngle() {
const char* value = std::getenv("MOBILEGL_RETRACE_USE_ANGLE");
return value != nullptr && std::strcmp(value, "1") == 0;
return MG_Config::Features.RetraceUseAngle;
}
static Vector<String> AngleLibNames(const char* name) {
const char* angleDir = std::getenv("MOBILEGL_RETRACE_ANGLE_DIR");
if (angleDir != nullptr && angleDir[0] != '\0') {
const String& angleDir = MG_Config::Features.RetraceAngleDir;
if (!angleDir.empty()) {
String path = angleDir;
if (!path.empty() && path.back() != '/') {
if (path.back() != '/') {
path += "/";
}
path += name;
@@ -450,6 +450,8 @@ namespace MobileGL::MG_Util::BackendLoader {
INIT_GLES_FUNC(glBufferStorageEXT)
INIT_GLES_FUNC(glGetQueryObjectivEXT)
INIT_GLES_FUNC(glGetQueryObjecti64vEXT)
INIT_GLES_FUNC(glQueryCounterEXT)
INIT_GLES_FUNC(glGetQueryObjectui64vEXT)
INIT_GLES_FUNC(glBindFragDataLocationEXT)
INIT_GLES_FUNC(glMapBufferOES)
INIT_GLES_FUNC(glMultiDrawArraysIndirectEXT)
@@ -726,6 +728,9 @@ namespace MobileGL::MG_Util::BackendLoader {
if (std::strcmp(extension, "GL_EXT_base_instance") == 0) {
caps.SupportsBaseInstance = true;
}
if (std::strcmp(extension, "GL_EXT_disjoint_timer_query") == 0) {
caps.SupportsDisjointTimerQuery = true;
}
}
}
@@ -911,6 +916,18 @@ namespace MobileGL::MG_Util::BackendLoader {
MGLOG_I(" Indirect draw gl_InstanceID includes baseInstance: %s",
caps.IndirectDrawInstanceIdIncludesBaseInstance ? "true" : "false");
caps.IsAngleRenderer = caps.GLESRendererString.find("ANGLE") != String::npos;
caps.IsAngleLlvmpipeRenderer =
caps.IsAngleRenderer && caps.GLESRendererString.find("llvmpipe") != String::npos;
caps.AvoidSamplerMipmapMinFilter =
caps.IsAngleLlvmpipeRenderer && MG_Config::Features.AvoidAngleLlvmpipeSamplerMipmapMinFilter;
MGLOG_I(" GL_EXT_disjoint_timer_query supported: %s",
caps.SupportsDisjointTimerQuery ? "true" : "false");
MGLOG_I(" ANGLE renderer: %s", caps.IsAngleRenderer ? "true" : "false");
MGLOG_I(" ANGLE llvmpipe renderer: %s", caps.IsAngleLlvmpipeRenderer ? "true" : "false");
MGLOG_I(" Avoid sampler mipmap min filter: %s",
caps.AvoidSamplerMipmapMinFilter ? "true" : "false");
return true;
}
} // namespace MobileGL::MG_Util::BackendLoader
@@ -611,6 +611,8 @@ namespace MobileGL {
GLbitfield flags)
GL_FUNC_TYPEDEF(void, glGetQueryObjectivEXT, GLuint id, GLenum pname, GLint* params)
GL_FUNC_TYPEDEF(void, glGetQueryObjecti64vEXT, GLuint id, GLenum pname, GLint64* params)
GL_FUNC_TYPEDEF(void, glQueryCounterEXT, GLuint id, GLenum target)
GL_FUNC_TYPEDEF(void, glGetQueryObjectui64vEXT, GLuint id, GLenum pname, GLuint64* params)
GL_FUNC_TYPEDEF(void, glBindFragDataLocationEXT, GLuint program, GLuint colorNumber, const GLchar* name)
GL_FUNC_TYPEDEF(void*, glMapBufferOES, GLenum target, GLenum access)
@@ -1000,6 +1002,8 @@ namespace MobileGL {
GL_FUNC_DECL(glBufferStorageEXT)
GL_FUNC_DECL(glGetQueryObjectivEXT)
GL_FUNC_DECL(glGetQueryObjecti64vEXT)
GL_FUNC_DECL(glQueryCounterEXT)
GL_FUNC_DECL(glGetQueryObjectui64vEXT)
GL_FUNC_DECL(glBindFragDataLocationEXT)
GL_FUNC_DECL(glMapBufferOES)
@@ -1019,6 +1023,16 @@ namespace MobileGL {
Bool SupportsPersistentMapping = false;
Bool SupportsNorm16Texture = false;
Bool SupportsBaseInstance = false;
// GL_EXT_disjoint_timer_query is present in the extension string.
Bool SupportsDisjointTimerQuery = false;
// GL_RENDERER contains "ANGLE".
Bool IsAngleRenderer = false;
// GL_RENDERER contains both "ANGLE" and "llvmpipe".
Bool IsAngleLlvmpipeRenderer = false;
// IsAngleLlvmpipeRenderer combined with the
// MOBILEGL_ANGLE_LLVMPIPE_AVOID_SAMPLER_MIPMAP_MIN_FILTER feature toggle:
// sampler min filters should drop their mipmap component.
Bool AvoidSamplerMipmapMinFilter = false;
// True when indirect draws leak the command's baseInstance word ("reserved,
// must be zero" in unextended ES) into gl_InstanceID. Conforming ES drivers
// keep gl_InstanceID zero-based; ANGLE's Vulkan backend hands the command