From 856e6ed49b35aeea895aa682446380c87c0f918a Mon Sep 17 00:00:00 2001 From: Chris Healy Date: Sun, 8 Mar 2026 21:13:39 -0700 Subject: [PATCH 1/3] OpenGL: Add timer query extension detection Add detection for GL_EXT_disjoint_timer_query (OpenGL ES) and GL_ARB_timer_query (desktop GL) extensions. These extensions provide GPU timestamp queries for profiling render pass durations. Also adds function pointer declarations for glQueryCounter, glGetQueryObjecti64v, and glGetQueryObjectui64v. Signed-off-by: Chris Healy --- Common/GPU/OpenGL/GLFeatures.cpp | 2 ++ Common/GPU/OpenGL/GLFeatures.h | 2 ++ Common/GPU/OpenGL/gl3stub.c | 8 ++++++++ Common/GPU/OpenGL/gl3stub.h | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/Common/GPU/OpenGL/GLFeatures.cpp b/Common/GPU/OpenGL/GLFeatures.cpp index bfaeef3631..0c4137cffe 100644 --- a/Common/GPU/OpenGL/GLFeatures.cpp +++ b/Common/GPU/OpenGL/GLFeatures.cpp @@ -384,6 +384,7 @@ bool CheckGLExtensions() { gl_extensions.ARB_explicit_attrib_location = g_set_gl_extensions.count("GL_ARB_explicit_attrib_location") != 0; gl_extensions.ARB_texture_non_power_of_two = g_set_gl_extensions.count("GL_ARB_texture_non_power_of_two") != 0; gl_extensions.ARB_shader_stencil_export = g_set_gl_extensions.count("GL_ARB_shader_stencil_export") != 0; + gl_extensions.ARB_timer_query = g_set_gl_extensions.count("GL_ARB_timer_query") != 0; gl_extensions.ARB_texture_compression_bptc = g_set_gl_extensions.count("GL_ARB_texture_compression_bptc") != 0; gl_extensions.ARB_texture_compression_rgtc = g_set_gl_extensions.count("GL_ARB_texture_compression_rgtc") != 0; gl_extensions.KHR_texture_compression_astc_ldr = g_set_gl_extensions.count("GL_KHR_texture_compression_astc_ldr") != 0; @@ -406,6 +407,7 @@ bool CheckGLExtensions() { gl_extensions.EXT_buffer_storage = g_set_gl_extensions.count("GL_EXT_buffer_storage") != 0; gl_extensions.EXT_clip_cull_distance = g_set_gl_extensions.count("GL_EXT_clip_cull_distance") != 0; gl_extensions.EXT_depth_clamp = g_set_gl_extensions.count("GL_EXT_depth_clamp") != 0; + gl_extensions.EXT_disjoint_timer_query = g_set_gl_extensions.count("GL_EXT_disjoint_timer_query") != 0; gl_extensions.APPLE_clip_distance = g_set_gl_extensions.count("GL_APPLE_clip_distance") != 0; #if defined(__ANDROID__) diff --git a/Common/GPU/OpenGL/GLFeatures.h b/Common/GPU/OpenGL/GLFeatures.h index acd6cd75bf..bdcfa4565a 100644 --- a/Common/GPU/OpenGL/GLFeatures.h +++ b/Common/GPU/OpenGL/GLFeatures.h @@ -86,6 +86,8 @@ struct GLExtensions { bool KHR_texture_compression_astc_ldr; // EXT + bool EXT_disjoint_timer_query; + bool ARB_timer_query; bool EXT_texture_compression_s3tc; bool EXT_swap_control_tear; bool EXT_discard_framebuffer; diff --git a/Common/GPU/OpenGL/gl3stub.c b/Common/GPU/OpenGL/gl3stub.c index 1caf54b685..9f47314c35 100644 --- a/Common/GPU/OpenGL/gl3stub.c +++ b/Common/GPU/OpenGL/gl3stub.c @@ -38,6 +38,10 @@ GLboolean gl3stubInit() { FIND_PROC(glEndQuery); FIND_PROC(glGetQueryiv); FIND_PROC(glGetQueryObjectuiv); + // GL_EXT_disjoint_timer_query - load EXT suffix versions into non-suffix pointers + glQueryCounter = (void*)eglGetProcAddress("glQueryCounterEXT"); + glGetQueryObjecti64v = (void*)eglGetProcAddress("glGetQueryObjecti64vEXT"); + glGetQueryObjectui64v = (void*)eglGetProcAddress("glGetQueryObjectui64vEXT"); FIND_PROC(glUnmapBuffer); FIND_PROC(glGetBufferPointerv); FIND_PROC(glDrawBuffers); @@ -271,6 +275,10 @@ GL_APICALL void (* GL_APIENTRY glBeginQuery) (GLenum target, GLuint id GL_APICALL void (* GL_APIENTRY glEndQuery) (GLenum target); GL_APICALL void (* GL_APIENTRY glGetQueryiv) (GLenum target, GLenum pname, GLint* params); GL_APICALL void (* GL_APIENTRY glGetQueryObjectuiv) (GLuint id, GLenum pname, GLuint* params); +// GL_EXT_disjoint_timer_query / GL_ARB_timer_query +GL_APICALL void (* GL_APIENTRY glQueryCounter) (GLuint id, GLenum target); +GL_APICALL void (* GL_APIENTRY glGetQueryObjecti64v) (GLuint id, GLenum pname, GLint64* params); +GL_APICALL void (* GL_APIENTRY glGetQueryObjectui64v) (GLuint id, GLenum pname, GLuint64* params); GL_APICALL GLboolean (* GL_APIENTRY glUnmapBuffer) (GLenum target); GL_APICALL void (* GL_APIENTRY glGetBufferPointerv) (GLenum target, GLenum pname, GLvoid** params); GL_APICALL void (* GL_APIENTRY glDrawBuffers) (GLsizei n, const GLenum* bufs); diff --git a/Common/GPU/OpenGL/gl3stub.h b/Common/GPU/OpenGL/gl3stub.h index ba99aec6b2..70f0217f77 100644 --- a/Common/GPU/OpenGL/gl3stub.h +++ b/Common/GPU/OpenGL/gl3stub.h @@ -409,6 +409,10 @@ extern GL_APICALL void (* GL_APIENTRY glBeginQuery) (GLenum target, GL extern GL_APICALL void (* GL_APIENTRY glEndQuery) (GLenum target); extern GL_APICALL void (* GL_APIENTRY glGetQueryiv) (GLenum target, GLenum pname, GLint* params); extern GL_APICALL void (* GL_APIENTRY glGetQueryObjectuiv) (GLuint id, GLenum pname, GLuint* params); +// GL_EXT_disjoint_timer_query / GL_ARB_timer_query +extern GL_APICALL void (* GL_APIENTRY glQueryCounter) (GLuint id, GLenum target); +extern GL_APICALL void (* GL_APIENTRY glGetQueryObjecti64v) (GLuint id, GLenum pname, GLint64* params); +extern GL_APICALL void (* GL_APIENTRY glGetQueryObjectui64v) (GLuint id, GLenum pname, GLuint64* params); extern GL_APICALL GLboolean (* GL_APIENTRY glUnmapBuffer) (GLenum target); extern GL_APICALL void (* GL_APIENTRY glGetBufferPointerv) (GLenum target, GLenum pname, GLvoid** params); extern GL_APICALL void (* GL_APIENTRY glDrawBuffers) (GLsizei n, const GLenum* bufs); From ba59170b0bbb131ac0ed07f99269f05d4b62a961 Mon Sep 17 00:00:00 2001 From: Chris Healy Date: Wed, 18 Mar 2026 09:48:15 -0700 Subject: [PATCH 2/3] OpenGL: Add GLProfiler for GPU timestamp profiling Adds a new GLProfiler class for GPU-side timestamp profiling, similar to VulkanProfiler. Uses GL_EXT_disjoint_timer_query (GLES) or GL_ARB_timer_query (desktop GL). Features: - Scoped Begin()/End() profiling with printf-style naming - Automatic GPU disjoint detection (frequency changes invalidate results) - Millisecond timing output via INFO_LOG - Pre-allocated query pool (1024 queries max per frame) - Nested scope support with indented output Signed-off-by: Chris Healy --- CMakeLists.txt | 2 + Common/Common.vcxproj | 2 + Common/Common.vcxproj.filters | 6 ++ Common/GPU/OpenGL/GLProfiler.cpp | 174 +++++++++++++++++++++++++++++++ Common/GPU/OpenGL/GLProfiler.h | 53 ++++++++++ android/jni/Android.mk | 1 + libretro/Makefile.common | 1 + 7 files changed, 239 insertions(+) create mode 100644 Common/GPU/OpenGL/GLProfiler.cpp create mode 100644 Common/GPU/OpenGL/GLProfiler.h diff --git a/CMakeLists.txt b/CMakeLists.txt index dcdb2f55ce..6cf9c097e0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -771,6 +771,8 @@ add_library(Common STATIC Common/GPU/OpenGL/GLRenderManager.h Common/GPU/OpenGL/GLQueueRunner.cpp Common/GPU/OpenGL/GLQueueRunner.h + Common/GPU/OpenGL/GLProfiler.cpp + Common/GPU/OpenGL/GLProfiler.h Common/GPU/OpenGL/DataFormatGL.cpp Common/GPU/OpenGL/DataFormatGL.h Common/GPU/Vulkan/VulkanBarrier.cpp diff --git a/Common/Common.vcxproj b/Common/Common.vcxproj index e83a32ab33..6332411939 100644 --- a/Common/Common.vcxproj +++ b/Common/Common.vcxproj @@ -418,6 +418,7 @@ + @@ -885,6 +886,7 @@ + diff --git a/Common/Common.vcxproj.filters b/Common/Common.vcxproj.filters index 75df93f473..25bf4d40a9 100644 --- a/Common/Common.vcxproj.filters +++ b/Common/Common.vcxproj.filters @@ -261,6 +261,9 @@ GPU\OpenGL + + GPU\OpenGL + GPU\OpenGL @@ -966,6 +969,9 @@ GPU\OpenGL + + GPU\OpenGL + GPU\OpenGL diff --git a/Common/GPU/OpenGL/GLProfiler.cpp b/Common/GPU/OpenGL/GLProfiler.cpp new file mode 100644 index 0000000000..73c690e013 --- /dev/null +++ b/Common/GPU/OpenGL/GLProfiler.cpp @@ -0,0 +1,174 @@ +#include + +#include "Common/GPU/OpenGL/GLProfiler.h" +#include "Common/GPU/OpenGL/GLCommon.h" +#include "Common/GPU/OpenGL/GLFeatures.h" +#include "Common/GPU/OpenGL/GLDebugLog.h" +#include "Common/Log.h" + +// For iOS, define function pointer types and variables locally since gl3stub.h +// content is excluded on iOS. These will remain NULL since iOS doesn't support +// GL_EXT_disjoint_timer_query. +#if PPSSPP_PLATFORM(IOS) +typedef void (*PFNGLQUERYCOUNTERPROC)(GLuint id, GLenum target); +typedef void (*PFNGLGETQUERYOBJECTUI64VPROC)(GLuint id, GLenum pname, GLuint64 *params); +static PFNGLQUERYCOUNTERPROC glQueryCounter = nullptr; +static PFNGLGETQUERYOBJECTUI64VPROC glGetQueryObjectui64v = nullptr; +#endif + +// Constants - same values for both EXT and ARB versions +#ifndef GL_TIMESTAMP +#define GL_TIMESTAMP 0x8E28 +#endif +#ifndef GL_QUERY_RESULT +#define GL_QUERY_RESULT 0x8866 +#endif +#ifndef GL_GPU_DISJOINT_EXT +#define GL_GPU_DISJOINT_EXT 0x8FBB +#endif + +// GLCommon.h provides access to GL functions: +// - On GLES: function pointers from gl3stub.h, loaded via eglGetProcAddress +// - On desktop GL: GLEW provides the functions + +void GLProfiler::Init() { + supported_ = false; + firstFrame_ = true; + numQueries_ = 0; + scopes_.clear(); + scopeStack_.clear(); + + // Check for extension support + // Function pointers are declared in gl3stub.h and loaded appropriately per platform + if (gl_extensions.EXT_disjoint_timer_query) { + // GLES path - use function pointers loaded with EXT suffix + if (glQueryCounter && glGetQueryObjectui64v) { + supported_ = true; + INFO_LOG(Log::G3D, "GLProfiler: Using GL_EXT_disjoint_timer_query"); + } + } else if (gl_extensions.ARB_timer_query) { + // Desktop GL path - use function pointers (no suffix) + if (glQueryCounter && glGetQueryObjectui64v) { + supported_ = true; + INFO_LOG(Log::G3D, "GLProfiler: Using GL_ARB_timer_query"); + } + } + + if (supported_) { + // Pre-allocate query objects + queries_.resize(MAX_QUERY_COUNT); + glGenQueries(MAX_QUERY_COUNT, queries_.data()); + CHECK_GL_ERROR_IF_DEBUG(); + } +} + +void GLProfiler::Shutdown() { + if (supported_ && !queries_.empty()) { + glDeleteQueries((GLsizei)queries_.size(), queries_.data()); + queries_.clear(); + } + supported_ = false; + scopes_.clear(); + scopeStack_.clear(); +} + +void GLProfiler::BeginFrame() { + if (!supported_) { + return; + } + + // Check if profiling is enabled + if (enabledPtr_ && !*enabledPtr_) { + scopes_.clear(); + scopeStack_.clear(); + numQueries_ = 0; + return; + } + + // Check for disjoint operation (GPU frequency changed) - GLES only + if (gl_extensions.EXT_disjoint_timer_query) { + GLint disjoint = 0; + glGetIntegerv(GL_GPU_DISJOINT_EXT, &disjoint); + if (disjoint) { + // Results are invalid, just clear and start fresh + WARN_LOG(Log::G3D, "GLProfiler: GPU disjoint detected, timing results discarded"); + scopes_.clear(); + scopeStack_.clear(); + numQueries_ = 0; + firstFrame_ = true; + return; + } + } + + // Read results from previous frame (guaranteed complete now) + if (numQueries_ > 0 && !firstFrame_) { + static const char * const indent[4] = { "", " ", " ", " " }; + + if (!scopes_.empty()) { + INFO_LOG(Log::G3D, "OpenGL profiling events this frame:"); + } + + // Log results + for (auto &scope : scopes_) { + if (scope.endQueryId == -1) { + WARN_LOG(Log::G3D, "Unclosed scope: %s", scope.name); + continue; + } + + GLuint64 startTime = 0, endTime = 0; + glGetQueryObjectui64v(queries_[scope.startQueryId], GL_QUERY_RESULT, &startTime); + glGetQueryObjectui64v(queries_[scope.endQueryId], GL_QUERY_RESULT, &endTime); + + // Times are in nanoseconds, convert to milliseconds + double milliseconds = (double)(endTime - startTime) / 1000000.0; + + INFO_LOG(Log::G3D, "%s%s (%0.3f ms)", indent[scope.level & 3], scope.name, milliseconds); + } + } + + firstFrame_ = false; + scopes_.clear(); + scopeStack_.clear(); + numQueries_ = 0; +} + +void GLProfiler::Begin(const char *fmt, ...) { + if (!supported_ || (enabledPtr_ && !*enabledPtr_) || numQueries_ >= MAX_QUERY_COUNT - 1) { + return; + } + + GLProfilerScope scope; + va_list args; + va_start(args, fmt); + vsnprintf(scope.name, sizeof(scope.name), fmt, args); + va_end(args); + scope.startQueryId = numQueries_; + scope.endQueryId = -1; + scope.level = (int)scopeStack_.size(); + + scopeStack_.push_back(scopes_.size()); + scopes_.push_back(scope); + + glQueryCounter(queries_[numQueries_], GL_TIMESTAMP); + numQueries_++; +} + +void GLProfiler::End() { + if (!supported_ || (enabledPtr_ && !*enabledPtr_) || numQueries_ >= MAX_QUERY_COUNT - 1) { + return; + } + + if (scopeStack_.empty()) { + WARN_LOG(Log::G3D, "GLProfiler::End called without matching Begin"); + return; + } + + size_t scopeId = scopeStack_.back(); + scopeStack_.pop_back(); + + GLProfilerScope &scope = scopes_[scopeId]; + scope.endQueryId = numQueries_; + + glQueryCounter(queries_[numQueries_], GL_TIMESTAMP); + numQueries_++; +} diff --git a/Common/GPU/OpenGL/GLProfiler.h b/Common/GPU/OpenGL/GLProfiler.h new file mode 100644 index 0000000000..3290134547 --- /dev/null +++ b/Common/GPU/OpenGL/GLProfiler.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include + +#include "Common/GPU/OpenGL/GLCommon.h" +#include "Common/GPU/OpenGL/gl3stub.h" + +// Simple scoped based profiler for OpenGL, similar to VulkanProfiler. +// Uses GL_EXT_disjoint_timer_query (GLES) or GL_ARB_timer_query (desktop GL). +// Put the whole thing in a FrameData to allow for overlap. + +struct GLProfilerScope { + char name[52]; // to make a struct size of 64, just because + int startQueryId; + int endQueryId; + int level; +}; + +class GLProfiler { +public: + void Init(); + void Shutdown(); + + void BeginFrame(); + + void Begin(const char *fmt, ...) +#ifdef __GNUC__ + __attribute__((format(printf, 2, 3))) +#endif + ; + void End(); + + void SetEnabledPtr(bool *enabledPtr) { + enabledPtr_ = enabledPtr; + } + + bool IsSupported() const { return supported_; } + +private: + bool supported_ = false; + bool firstFrame_ = true; + bool *enabledPtr_ = nullptr; + + std::vector queries_; + std::vector scopes_; + int numQueries_ = 0; + + std::vector scopeStack_; + + static const int MAX_QUERY_COUNT = 1024; +}; diff --git a/android/jni/Android.mk b/android/jni/Android.mk index 9ec54ad1f8..4384151c07 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -115,6 +115,7 @@ NATIVE_FILES :=\ $(SRC)/Common/GPU/OpenGL/GLMemory.cpp \ $(SRC)/Common/GPU/OpenGL/GLRenderManager.cpp \ $(SRC)/Common/GPU/OpenGL/GLQueueRunner.cpp \ + $(SRC)/Common/GPU/OpenGL/GLProfiler.cpp \ $(SRC)/Common/GPU/OpenGL/DataFormatGL.cpp VULKAN_FILES := \ diff --git a/libretro/Makefile.common b/libretro/Makefile.common index 5e305855b2..6bdb4effac 100644 --- a/libretro/Makefile.common +++ b/libretro/Makefile.common @@ -476,6 +476,7 @@ SOURCES_CXX += \ $(COMMONDIR)/GPU/OpenGL/GLRenderManager.cpp \ $(COMMONDIR)/GPU/OpenGL/GLMemory.cpp \ $(COMMONDIR)/GPU/OpenGL/GLQueueRunner.cpp \ + $(COMMONDIR)/GPU/OpenGL/GLProfiler.cpp \ $(COMMONDIR)/GPU/OpenGL/DataFormatGL.cpp \ $(COMMONDIR)/GPU/Vulkan/thin3d_vulkan.cpp \ $(COMMONDIR)/GPU/Vulkan/VulkanQueueRunner.cpp \ From 49bfbd49cd72882ae8aec935617895746b5f7452 Mon Sep 17 00:00:00 2001 From: Chris Healy Date: Sun, 8 Mar 2026 21:14:04 -0700 Subject: [PATCH 3/3] OpenGL: Integrate GLProfiler into render pass execution Integrates the GLProfiler into GLQueueRunner to provide GPU timestamp profiling for OpenGL render passes, similar to the Vulkan backend. Profiled operations: - RenderPass (with tag name for identification) - Copy operations - Blit operations - Readback operations - ReadbackImage operations The profiler is initialized in CreateDeviceObjects() and shutdown in DestroyDeviceObjects(). Timing results are logged each frame when the GPU profile debug overlay is enabled. Signed-off-by: Chris Healy --- Common/GPU/OpenGL/GLQueueRunner.cpp | 19 +++++++++++++++++++ Common/GPU/OpenGL/GLQueueRunner.h | 3 +++ 2 files changed, 22 insertions(+) diff --git a/Common/GPU/OpenGL/GLQueueRunner.cpp b/Common/GPU/OpenGL/GLQueueRunner.cpp index c5e2cb4d5e..f2f9ba914f 100644 --- a/Common/GPU/OpenGL/GLQueueRunner.cpp +++ b/Common/GPU/OpenGL/GLQueueRunner.cpp @@ -75,9 +75,12 @@ void GLQueueRunner::CreateDeviceObjects() { #if !PPSSPP_ARCH(X86) // Doesn't work on AMD for some reason. See issue #17787 useDebugGroups_ = !gl_extensions.IsGLES && gl_extensions.VersionGEThan(4, 3); #endif + + profiler_.Init(); } void GLQueueRunner::DestroyDeviceObjects() { + profiler_.Shutdown(); CHECK_GL_ERROR_IF_DEBUG(); if (gl_extensions.ARB_vertex_array_object) { glDeleteVertexArrays(1, &globalVAO_); @@ -667,6 +670,12 @@ void GLQueueRunner::RunSteps(const std::vector &steps, GLFrameData &f } } + // GPU timestamp profiling - read results from previous frame and prepare for new queries + if (frameData.profile.enabled) { + profiler_.SetEnabledPtr(&frameData.profile.enabled); + } + profiler_.BeginFrame(); + CHECK_GL_ERROR_IF_DEBUG(); size_t renderCount = 0; for (size_t i = 0; i < steps.size(); i++) { @@ -680,24 +689,34 @@ void GLQueueRunner::RunSteps(const std::vector &steps, GLFrameData &f switch (step.stepType) { case GLRStepType::RENDER: renderCount++; + profiler_.Begin("RenderPass %s", step.tag); if (IsVREnabled()) { PreprocessStepVR(&step); PerformRenderPass(step, renderCount == 1, renderCount == totalRenderCount, frameData.profile); } else { PerformRenderPass(step, renderCount == 1, renderCount == totalRenderCount, frameData.profile); } + profiler_.End(); break; case GLRStepType::COPY: + profiler_.Begin("Copy"); PerformCopy(step); + profiler_.End(); break; case GLRStepType::BLIT: + profiler_.Begin("Blit"); PerformBlit(step); + profiler_.End(); break; case GLRStepType::READBACK: + profiler_.Begin("Readback"); PerformReadback(step); + profiler_.End(); break; case GLRStepType::READBACK_IMAGE: + profiler_.Begin("ReadbackImage"); PerformReadbackImage(step); + profiler_.End(); break; case GLRStepType::RENDER_SKIP: break; diff --git a/Common/GPU/OpenGL/GLQueueRunner.h b/Common/GPU/OpenGL/GLQueueRunner.h index 1261bb9ab2..6c36f247df 100644 --- a/Common/GPU/OpenGL/GLQueueRunner.h +++ b/Common/GPU/OpenGL/GLQueueRunner.h @@ -7,6 +7,7 @@ #include "Common/GPU/OpenGL/GLCommon.h" #include "Common/GPU/OpenGL/GLFrameData.h" +#include "Common/GPU/OpenGL/GLProfiler.h" #include "Common/GPU/DataFormat.h" #include "Common/GPU/Shader.h" #include "Common/GPU/thin3d.h" @@ -417,6 +418,8 @@ private: bool sawOutOfMemory_ = false; bool useDebugGroups_ = false; + GLProfiler profiler_; + ErrorCallbackFn errorCallback_ = nullptr; void *errorCallbackUserData_ = nullptr; };