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/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/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/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;
};
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);
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 \