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 \