From 39049a67fd83863a86e362329d9f9b82a2e57c78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 28 Sep 2026 15:05:26 -0600 Subject: [PATCH] GLES: Free textures on device lost, and unsubmitted step data at exit - The texture and fragment test caches dropped their GLRTexture objects on DeviceLost without queueing them for deletion, leaking them on every Android background/resume. The deleter already skips the GL calls when the context is gone. - GLRenderManager::ThreadEnd cleared unsubmitted init and render steps without freeing the data they own. Run them through the dry run instead, which now also frees stereo matrices and shader code. Co-Authored-By: Claude Opus 5.5 (1M context) --- Common/GPU/OpenGL/GLQueueRunner.cpp | 4 ++++ Common/GPU/OpenGL/GLRenderManager.cpp | 8 ++++---- GPU/GLES/FragmentTestCacheGLES.cpp | 2 +- GPU/GLES/FragmentTestCacheGLES.h | 3 ++- GPU/GLES/TextureCacheGLES.cpp | 8 ++++---- 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/Common/GPU/OpenGL/GLQueueRunner.cpp b/Common/GPU/OpenGL/GLQueueRunner.cpp index 4c27693f75..fdf71f6dd6 100644 --- a/Common/GPU/OpenGL/GLQueueRunner.cpp +++ b/Common/GPU/OpenGL/GLQueueRunner.cpp @@ -147,6 +147,7 @@ void GLQueueRunner::RunInitSteps(const FastVec &steps, bool skipGLC case GLRInitStepType::CREATE_SHADER: { WARN_LOG(Log::G3D, "CREATE_SHADER found with skipGLCalls, not good"); + delete[] step.create_shader.code; break; } default: @@ -678,6 +679,9 @@ void GLQueueRunner::RunSteps(const std::vector &steps, GLFrameData &f } } break; + case GLRRenderCommand::UNIFORMSTEREOMATRIX: + delete[] c.uniformStereoMatrix4.mData; + break; default: break; } diff --git a/Common/GPU/OpenGL/GLRenderManager.cpp b/Common/GPU/OpenGL/GLRenderManager.cpp index 50216756b5..4a62896e03 100644 --- a/Common/GPU/OpenGL/GLRenderManager.cpp +++ b/Common/GPU/OpenGL/GLRenderManager.cpp @@ -110,11 +110,11 @@ void GLRenderManager::ThreadEnd() { frameData_[i].deleter_prev.Perform(this, skipGLCalls_); } deleter_.Perform(this, skipGLCalls_); - for (int i = 0; i < (int)steps_.size(); i++) { - delete steps_[i]; - } - steps_.clear(); + // Steps that never got submitted. A dry run frees the data they own (texture uploads etc), and the steps. + queueRunner_.RunInitSteps(initSteps_, true); initSteps_.clear(); + queueRunner_.RunSteps(steps_, frameData_[0], true, false, false); + steps_.clear(); INFO_LOG(Log::G3D, "GLRenderManager::ThreadEnd end"); } diff --git a/GPU/GLES/FragmentTestCacheGLES.cpp b/GPU/GLES/FragmentTestCacheGLES.cpp index 973a3e58a2..066ca0dfae 100644 --- a/GPU/GLES/FragmentTestCacheGLES.cpp +++ b/GPU/GLES/FragmentTestCacheGLES.cpp @@ -144,7 +144,7 @@ GLRTexture *FragmentTestCacheGLES::CreateTestTexture(const GEComparison funcs[4] } void FragmentTestCacheGLES::Clear(bool deleteThem) { - if (deleteThem) { + if (deleteThem && render_) { for (const auto &[_, v] : cache_) { render_->DeleteTexture(v.texture); } diff --git a/GPU/GLES/FragmentTestCacheGLES.h b/GPU/GLES/FragmentTestCacheGLES.h index ddb9eca154..174c1bae6d 100644 --- a/GPU/GLES/FragmentTestCacheGLES.h +++ b/GPU/GLES/FragmentTestCacheGLES.h @@ -68,7 +68,8 @@ public: void BindTestTexture(int slot); void DeviceLost() { - Clear(false); + // Queue the deletes anyway, the deleter frees the GLRTexture objects and skips the GL calls if needed. + Clear(true); render_ = nullptr; } void DeviceRestore(Draw::DrawContext *draw); diff --git a/GPU/GLES/TextureCacheGLES.cpp b/GPU/GLES/TextureCacheGLES.cpp index 8cd71a4dec..e1a61021ca 100644 --- a/GPU/GLES/TextureCacheGLES.cpp +++ b/GPU/GLES/TextureCacheGLES.cpp @@ -51,10 +51,10 @@ void TextureCacheGLES::SetFramebufferManager(FramebufferManagerGLES *fbManager) } void TextureCacheGLES::ReleaseTexture(TexCacheEntry *entry, bool delete_them) { - if (delete_them) { - if (entry->textureName) { - render_->DeleteTexture(entry->textureName); - } + // Delete even when !delete_them (device lost): the GLRTexture is a heap object that only the deleter + // frees, and the deleter skips the GL calls itself once the context is gone. + if (entry->textureName && render_) { + render_->DeleteTexture(entry->textureName); } entry->textureName = nullptr; }