diff --git a/CMakeLists.txt b/CMakeLists.txt index 3543bacc..15d15252 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/MobileGL/MG_Backend/BackendObject.h b/MobileGL/MG_Backend/BackendObject.h index 63ae1f90..81b401b3 100644 --- a/MobileGL/MG_Backend/BackendObject.h +++ b/MobileGL/MG_Backend/BackendObject.h @@ -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; diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index 0901b5d7..fd3c9104 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -18,6 +18,8 @@ #include #include #include +#include +#include #include namespace MobileGL::MG_Backend::DirectGLES { @@ -176,7 +178,7 @@ namespace MobileGL::MG_Backend::DirectGLES { Flags GetForcedPixelFormatNormalizeOptions() { Flags 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; diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index fc590b3b..cbdc1da0 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -25,7 +25,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -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 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(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(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(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(result); + return true; + } + + void DeleteBackendQuery(BackendQueryHandle handle) { + auto* query = static_cast(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(timestamp); + } + void Present() { g_EGLFuncs.eglSwapBuffers(g_Display, g_Surface); } diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h index eeeec512..b31f964b 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h @@ -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. diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index dc7ed7ee..0b2042c4 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -17,17 +17,14 @@ #include "MG_Util/Converters/MGToVk/TextureEnumConverter.h" #include "MG_Util/Texture/TextureFormatProcessor.h" +#include #include #include 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(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(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(std::max(width, 1)); config.SurfaceHeight = static_cast(std::max(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(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 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() { diff --git a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp index 4bae0999..b4977251 100644 --- a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp @@ -15,12 +15,35 @@ #include "MG_Util/Converters/GLToMG/TextureEnumConverter.h" #include "MG_Util/Metrics/TextureMetrics.h" #include "MG_Util/Miscellany/IndexGenerator.h" +#include #include #include namespace MobileGL::MG_Backend::DirectVulkan { UniquePtr 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 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(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(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 begin; + SharedPtr 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(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(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(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(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(); diff --git a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.h b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.h index ac518f41..7eae65c9 100644 --- a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.h +++ b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.h @@ -14,6 +14,15 @@ namespace MobileGL::MG_Backend::DirectVulkan { extern UniquePtr 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 diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.cpp index db4ce534..440740b5 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.cpp @@ -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(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"); } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.h index d5bdb785..71bb1ea5 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.h @@ -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 m_frames; Vector m_swapchainImageRenderFinishedSemaphores; Uint32 currentFrameIndex = 0; + IRecordingObserver* m_recordingObserver = nullptr; }; } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp index 00294d2b..4c42dae9 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp @@ -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 #include #include #include @@ -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, diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index 6e75e4b3..69a2b049 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -12,6 +12,7 @@ #include "MG_Util/Converters/MGToStr/TextureEnumConverter.h" #include "MG_Util/Converters/MGToVk/TextureEnumConverter.h" +#include #include #include #include @@ -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, diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTimerQueryManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTimerQueryManager.cpp new file mode 100644 index 00000000..abbbba48 --- /dev/null +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTimerQueryManager.cpp @@ -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::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(); + 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(static_cast(deltaTicks) * static_cast(m_timestampPeriodNs)); + } + + Uint64 VkTimerQueryManager::TimestampNs(const TimestampRecord& record) const { + if (!record.valid) { + return 0; + } + return static_cast(static_cast(MaskToValidBits(record.rawTicks)) * + static_cast(m_timestampPeriodNs)); + } +} // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTimerQueryManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTimerQueryManager.h new file mode 100644 index 00000000..68964ee9 --- /dev/null +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTimerQueryManager.h @@ -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 + +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 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> 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 m_pools; + }; +} // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index ee9acc1e..362b89e7 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -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 #include #include #include @@ -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(); + 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(); 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 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(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(), {}); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 96d2183d..aa5e9e82 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -20,6 +20,7 @@ #include "VkRenderPassManager.h" #include "VkSamplerManager.h" #include "VkTextureManager.h" +#include "VkTimerQueryManager.h" #include "MG_Util/Math/VectorTypes.h" #include #include @@ -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 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 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 m_renderPassManager; UniquePtr m_textureManager; UniquePtr m_samplerManager; + UniquePtr m_timerQueryManager; BlitResources m_blitResources; DepthMipmapResources m_depthMipmapResources; Vector m_deferredDepthMipmapCleanup; diff --git a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp index 34e11824..a996784a 100644 --- a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp +++ b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp @@ -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("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) diff --git a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp index 77bb4d76..00e56e17 100644 --- a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp +++ b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp @@ -793,6 +793,18 @@ namespace MobileGL::MG_Impl::GLImpl { params[0] = static_cast(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(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(INT_MAX) ? INT_MAX : static_cast(timestamp); return; + } case GL_TRANSFORM_FEEDBACK_BUFFER_BINDING: if (const auto& obj = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::TransformFeedback).GetBoundObject()) { diff --git a/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp b/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp new file mode 100644 index 00000000..64cb18a5 --- /dev/null +++ b/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp @@ -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 +#include +#include +#include + +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 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("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 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 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 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 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 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 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 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 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(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(INT_MAX); + *params = value > kMaxInt ? INT_MAX : static_cast(value); + } + + void GetQueryObjectuiv(GLuint id, GLenum pname, GLuint* params) { + Uint64 value = 0; + if (!GetQueryObjectValue(id, pname, __FUNCTION__, value) || !params) { + return; + } + *params = static_cast(value & 0xFFFFFFFFull); + } + + void GetQueryObjecti64v(GLuint id, GLenum pname, GLint64* params) { + Uint64 value = 0; + if (!GetQueryObjectValue(id, pname, __FUNCTION__, value) || !params) { + return; + } + *params = static_cast(value); + } + + void GetQueryObjectui64v(GLuint id, GLenum pname, GLuint64* params) { + Uint64 value = 0; + if (!GetQueryObjectValue(id, pname, __FUNCTION__, value) || !params) { + return; + } + *params = static_cast(value); + } +} // namespace MobileGL::MG_Impl::GLImpl diff --git a/MobileGL/MG_Impl/GLImpl/Query/GL_Query.h b/MobileGL/MG_Impl/GLImpl/Query/GL_Query.h new file mode 100644 index 00000000..604f196f --- /dev/null +++ b/MobileGL/MG_Impl/GLImpl/Query/GL_Query.h @@ -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 + +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 diff --git a/MobileGL/MG_Test/CMakeLists.txt b/MobileGL/MG_Test/CMakeLists.txt index fde0a937..24c9d084 100644 --- a/MobileGL/MG_Test/CMakeLists.txt +++ b/MobileGL/MG_Test/CMakeLists.txt @@ -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() diff --git a/MobileGL/MG_Test/Query/CMakeLists.txt b/MobileGL/MG_Test/Query/CMakeLists.txt new file mode 100644 index 00000000..7f2925ca --- /dev/null +++ b/MobileGL/MG_Test/Query/CMakeLists.txt @@ -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) diff --git a/MobileGL/MG_Test/Query/QueryTest.cpp b/MobileGL/MG_Test/Query/QueryTest.cpp new file mode 100644 index 00000000..49dc3410 --- /dev/null +++ b/MobileGL/MG_Test/Query/QueryTest.cpp @@ -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 + +#include +#include +#include + +#include "Includes.h" +#include "Init.h" +#include + +#include +#include +#include + +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(static_cast(0x51)); + } + + void StubEndTimeElapsedQuery(MG_Backend::BackendQueryHandle) { ++g_stubEndCount; } + + MG_Backend::BackendQueryHandle StubQueryCounterTimestamp() { + ++g_stubCounterCount; + return reinterpret_cast(static_cast(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(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(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(std::tolower(c)); }); + expected = value != "0" && lowered != "false"; + } + EXPECT_EQ(MG_Config::Features.DisableTimerQuery, expected); +} diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp index b7137724..df09f75d 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp @@ -8,21 +8,21 @@ #include "Loader.h" #include "MG_Util/Types.h" +#include #if defined(MOBILEGL_IOS) #include #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 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 diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h index cf3e23f4..c86e4418 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h @@ -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