mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 12:48:32 +09:00
[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:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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, ×tamp);
|
||||
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)
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
@@ -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, ¤tQuery);
|
||||
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, ¤tQuery);
|
||||
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, ¤tQuery);
|
||||
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, ¤tQuery);
|
||||
EXPECT_EQ(currentQuery, 0);
|
||||
|
||||
MG_Impl::GLImpl::EndQuery(GL_TIME_ELAPSED);
|
||||
MG_Impl::GLImpl::GetQueryiv(GL_TIME_ELAPSED, GL_CURRENT_QUERY, ¤tQuery);
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user