1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-10 23:44:24 +00:00

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.
This commit is contained in:
reuk 2025-06-24 16:22:43 +01:00
parent fc2caf0a4d
commit c2d52a5499
No known key found for this signature in database
8 changed files with 91 additions and 3 deletions

View file

@ -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<NativeContextListener> listeners;
std::unique_ptr<std::remove_pointer_t<EGLSurface>, SurfaceDestructor> surface { EGL_NO_SURFACE };
std::unique_ptr<std::remove_pointer_t<EGLContext>, ContextDestructor> context { EGL_NO_CONTEXT };

View file

@ -213,6 +213,9 @@ public:
const ScopedLock lock;
};
void addListener (NativeContextListener&) {}
void removeListener (NativeContextListener&) {}
private:
CriticalSection mutex;
Component& component;

View file

@ -404,6 +404,9 @@ public:
const ScopedLock lock;
};
void addListener (NativeContextListener&) {}
void removeListener (NativeContextListener&) {}
private:
bool tryChooseVisual (const OpenGLPixelFormat& format, const std::vector<GLint>& optionalAttribs)
{

View file

@ -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;

View file

@ -196,6 +196,9 @@ public:
return nullptr;
}
void addListener (NativeContextListener&) {}
void removeListener (NativeContextListener&) {}
private:
//==============================================================================
void handleAsyncUpdate() override

View file

@ -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<int>& 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<jobject>)

View file

@ -348,6 +348,7 @@ public:
//==============================================================================
#ifndef DOXYGEN
class NativeContext;
class NativeContextListener;
#endif
private:

View file

@ -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<PixelARGB> data;
};
~Pimpl() override
{
release();
}
bool isValid() const noexcept
{
return std::holds_alternative<TransientState> (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<std::monostate>();
}
@ -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<std::monostate, TransientState, SavedState> state;
};