From c2d52a5499ad94a6c2db82c1ba447969e43e07d9 Mon Sep 17 00:00:00 2001 From: reuk Date: Tue, 24 Jun 2025 16:22:43 +0100 Subject: [PATCH] OpenGLFrameBuffer: Automatically save/restore content when the context is paused/resumed This addresses an issue where OpenGL-backed image content could get lost when putting and Android app into an inactive state. This happens because the GL context gets destroyed, freeing all associated resources. The workaround introduced here will listen for OpenGL state-change events, and attempt to save and restore framebuffer content into main memory, so that the content can be restored once the app is reactivated. --- .../juce_opengl/native/juce_OpenGL_android.h | 21 ++++++++++ modules/juce_opengl/native/juce_OpenGL_ios.h | 3 ++ .../juce_opengl/native/juce_OpenGL_linux.h | 3 ++ modules/juce_opengl/native/juce_OpenGL_mac.h | 3 ++ .../juce_opengl/native/juce_OpenGL_windows.h | 3 ++ .../juce_opengl/opengl/juce_OpenGLContext.cpp | 18 ++++++++ .../juce_opengl/opengl/juce_OpenGLContext.h | 1 + .../opengl/juce_OpenGLFrameBuffer.cpp | 42 +++++++++++++++++-- 8 files changed, 91 insertions(+), 3 deletions(-) diff --git a/modules/juce_opengl/native/juce_OpenGL_android.h b/modules/juce_opengl/native/juce_OpenGL_android.h index a67c3d8b56..a5ac7d99bd 100644 --- a/modules/juce_opengl/native/juce_OpenGL_android.h +++ b/modules/juce_opengl/native/juce_OpenGL_android.h @@ -252,6 +252,26 @@ public: const ScopedLock lock; }; + void addListener (NativeContextListener& l) + { + listeners.add (&l); + } + + void removeListener (NativeContextListener& l) + { + listeners.remove (&l); + } + + void notifyWillPause() + { + listeners.call ([&] (auto& l) { l.contextWillPause(); }); + } + + void notifyDidResume() + { + listeners.call ([&] (auto& l) { l.contextDidResume(); }); + } + Component& component; private: @@ -395,6 +415,7 @@ private: mutable std::mutex nativeHandleMutex; OpenGLContext* juceContext = nullptr; + ListenerList listeners; std::unique_ptr, SurfaceDestructor> surface { EGL_NO_SURFACE }; std::unique_ptr, ContextDestructor> context { EGL_NO_CONTEXT }; diff --git a/modules/juce_opengl/native/juce_OpenGL_ios.h b/modules/juce_opengl/native/juce_OpenGL_ios.h index d49f91907f..adb02005ec 100644 --- a/modules/juce_opengl/native/juce_OpenGL_ios.h +++ b/modules/juce_opengl/native/juce_OpenGL_ios.h @@ -213,6 +213,9 @@ public: const ScopedLock lock; }; + void addListener (NativeContextListener&) {} + void removeListener (NativeContextListener&) {} + private: CriticalSection mutex; Component& component; diff --git a/modules/juce_opengl/native/juce_OpenGL_linux.h b/modules/juce_opengl/native/juce_OpenGL_linux.h index 813d7043b2..19e81259f7 100644 --- a/modules/juce_opengl/native/juce_OpenGL_linux.h +++ b/modules/juce_opengl/native/juce_OpenGL_linux.h @@ -404,6 +404,9 @@ public: const ScopedLock lock; }; + void addListener (NativeContextListener&) {} + void removeListener (NativeContextListener&) {} + private: bool tryChooseVisual (const OpenGLPixelFormat& format, const std::vector& optionalAttribs) { diff --git a/modules/juce_opengl/native/juce_OpenGL_mac.h b/modules/juce_opengl/native/juce_OpenGL_mac.h index 0f2eaeb4a7..7e39fb4605 100644 --- a/modules/juce_opengl/native/juce_OpenGL_mac.h +++ b/modules/juce_opengl/native/juce_OpenGL_mac.h @@ -295,6 +295,9 @@ public: double videoRefreshPeriodS = 1.0 / 60.0; }; + void addListener (NativeContextListener&) {} + void removeListener (NativeContextListener&) {} + Component& owner; NSOpenGLContext* renderContext = nil; NSOpenGLView* view = nil; diff --git a/modules/juce_opengl/native/juce_OpenGL_windows.h b/modules/juce_opengl/native/juce_OpenGL_windows.h index 194544d3df..c848eecaf1 100644 --- a/modules/juce_opengl/native/juce_OpenGL_windows.h +++ b/modules/juce_opengl/native/juce_OpenGL_windows.h @@ -196,6 +196,9 @@ public: return nullptr; } + void addListener (NativeContextListener&) {} + void removeListener (NativeContextListener&) {} + private: //============================================================================== void handleAsyncUpdate() override diff --git a/modules/juce_opengl/opengl/juce_OpenGLContext.cpp b/modules/juce_opengl/opengl/juce_OpenGLContext.cpp index 88b80ffef8..dd067fd7b5 100644 --- a/modules/juce_opengl/opengl/juce_OpenGLContext.cpp +++ b/modules/juce_opengl/opengl/juce_OpenGLContext.cpp @@ -193,6 +193,10 @@ public: //============================================================================== void pause() { + #if JUCE_ANDROID + context.executeOnGLThread ([this] (auto&) { nativeContext->notifyWillPause(); }, true); + #endif + renderThread->remove (this); if ((state.fetch_and (~StateFlags::initialised) & StateFlags::initialised) == 0) @@ -670,6 +674,10 @@ public: if (context.renderer != nullptr) context.renderer->newOpenGLContextCreated(); + #if JUCE_ANDROID + nativeContext->notifyDidResume(); + #endif + return InitResult::success; } @@ -1669,6 +1677,16 @@ void OpenGLContext::copyTexture (const Rectangle& targetClipArea, JUCE_CHECK_OPENGL_ERROR } +void OpenGLContext::NativeContextListener::addListener (OpenGLContext& ctx, NativeContextListener& l) +{ + ctx.nativeContext->addListener (l); +} + +void OpenGLContext::NativeContextListener::removeListener (OpenGLContext& ctx, NativeContextListener& l) +{ + ctx.nativeContext->removeListener (l); +} + #if JUCE_ANDROID void OpenGLContext::NativeContext::surfaceCreated (LocalRef) diff --git a/modules/juce_opengl/opengl/juce_OpenGLContext.h b/modules/juce_opengl/opengl/juce_OpenGLContext.h index d8e1ff0a9a..0ea7aaa0cd 100644 --- a/modules/juce_opengl/opengl/juce_OpenGLContext.h +++ b/modules/juce_opengl/opengl/juce_OpenGLContext.h @@ -348,6 +348,7 @@ public: //============================================================================== #ifndef DOXYGEN class NativeContext; + class NativeContextListener; #endif private: diff --git a/modules/juce_opengl/opengl/juce_OpenGLFrameBuffer.cpp b/modules/juce_opengl/opengl/juce_OpenGLFrameBuffer.cpp index cdecace517..96210d02a0 100644 --- a/modules/juce_opengl/opengl/juce_OpenGLFrameBuffer.cpp +++ b/modules/juce_opengl/opengl/juce_OpenGLFrameBuffer.cpp @@ -35,7 +35,23 @@ namespace juce { -class OpenGLFrameBuffer::Pimpl +/* + Used on Android to detect when the GL context and associated resources (textures, framebuffers, + etc.) need to be destroyed/created due to the Surface changing state. +*/ +class OpenGLContext::NativeContextListener +{ +public: + virtual ~NativeContextListener() = default; + + virtual void contextWillPause() = 0; + virtual void contextDidResume() = 0; + + static void addListener (OpenGLContext& ctx, NativeContextListener& l); + static void removeListener (OpenGLContext& ctx, NativeContextListener& l); +}; + +class OpenGLFrameBuffer::Pimpl : private OpenGLContext::NativeContextListener { public: struct SavedState @@ -44,6 +60,11 @@ public: std::vector data; }; + ~Pimpl() override + { + release(); + } + bool isValid() const noexcept { return std::holds_alternative (state); @@ -63,6 +84,8 @@ public: return false; associatedContext = &context; + NativeContextListener::addListener (*associatedContext, *this); + return true; } @@ -115,7 +138,9 @@ public: void release() { - associatedContext = nullptr; + if (auto* prev = std::exchange (associatedContext, nullptr)) + NativeContextListener::removeListener (*prev, *this); + state.emplace(); } @@ -353,7 +378,7 @@ private: if (OpenGLHelpers::isContextActive()) { if (textureID != 0) - glDeleteTextures (1, &textureID); + gl::glDeleteTextures (1, &textureID); if (depthOrStencilBuffer != 0) gl::glDeleteRenderbuffers (1, &depthOrStencilBuffer); @@ -428,6 +453,17 @@ private: return nullptr; } + void contextWillPause() override + { + saveAndRelease(); + } + + void contextDidResume() override + { + if (associatedContext != nullptr) + reloadSavedCopy (*associatedContext); + } + OpenGLContext* associatedContext = nullptr; std::variant state; };