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

OpenGL: Carry out all GL rendering on a single thread

This commit is contained in:
reuk 2022-09-13 14:32:44 +01:00
parent ae3bfdb1d2
commit 2ae87f95f1
No known key found for this signature in database
GPG key ID: FCB43929F012EE5C
8 changed files with 415 additions and 337 deletions

View file

@ -63,16 +63,16 @@
#include <memory>
#include <mutex>
#include <numeric>
#include <optional>
#include <queue>
#include <set>
#include <sstream>
#include <thread>
#include <typeindex>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
#include <set>
#include <optional>
//==============================================================================
#include "juce_CompilerSupport.h"

View file

@ -124,7 +124,7 @@ DECLARE_JNI_CLASS_WITH_BYTECODE (JuceOpenGLViewSurface, "com/rmsl/juce/JuceOpenG
#undef JNI_CLASS_MEMBERS
//==============================================================================
class OpenGLContext::NativeContext : private SurfaceHolderCallback
class OpenGLContext::NativeContext : private SurfaceHolderCallback
{
public:
NativeContext (Component& comp,
@ -132,8 +132,7 @@ public:
void* /*contextToShareWith*/,
bool useMultisamplingIn,
OpenGLVersion)
: component (comp),
surface (EGL_NO_SURFACE), context (EGL_NO_CONTEXT)
: component (comp)
{
auto env = getEnv();
@ -175,88 +174,45 @@ public:
}
//==============================================================================
bool initialiseOnRenderThread (OpenGLContext& aContext)
InitResult initialiseOnRenderThread (OpenGLContext& ctx)
{
jassert (hasInitialised);
// The "real" initialisation happens when the surface is created. Here, we'll
// just return true if the initialisation happened successfully, or false if
// it hasn't happened yet, or was unsuccessful.
const std::lock_guard lock { nativeHandleMutex };
// has the context already attached?
jassert (surface == EGL_NO_SURFACE && context == EGL_NO_CONTEXT);
if (! hasInitialised)
return InitResult::fatal;
auto env = getEnv();
if (context.get() == EGL_NO_CONTEXT && surface.get() == EGL_NO_SURFACE)
return InitResult::retry;
ANativeWindow* window = nullptr;
LocalRef<jobject> holder (env->CallObjectMethod (surfaceView.get(), JuceOpenGLViewSurface.getHolder));
if (holder != nullptr)
{
LocalRef<jobject> jSurface (env->CallObjectMethod (holder.get(), AndroidSurfaceHolder.getSurface));
if (jSurface != nullptr)
{
window = ANativeWindow_fromSurface(env, jSurface.get());
// if we didn't succeed the first time, wait 25ms and try again
if (window == nullptr)
{
Thread::sleep (200);
window = ANativeWindow_fromSurface (env, jSurface.get());
}
}
}
if (window == nullptr)
{
// failed to get a pointer to the native window after second try so bail out
jassertfalse;
return false;
}
// create the surface
surface = eglCreateWindowSurface (display, config, window, nullptr);
jassert (surface != EGL_NO_SURFACE);
ANativeWindow_release (window);
// create the OpenGL context
EGLint contextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE };
context = eglCreateContext (display, config, EGL_NO_CONTEXT, contextAttribs);
jassert (context != EGL_NO_CONTEXT);
juceContext = &aContext;
return true;
juceContext = &ctx;
return InitResult::success;
}
void shutdownOnRenderThread()
{
jassert (hasInitialised);
// is there a context available to detach?
jassert (surface != EGL_NO_SURFACE && context != EGL_NO_CONTEXT);
eglDestroyContext (display, context);
context = EGL_NO_CONTEXT;
eglDestroySurface (display, surface);
surface = EGL_NO_SURFACE;
const std::lock_guard lock { nativeHandleMutex };
juceContext = nullptr;
}
//==============================================================================
bool makeActive() const noexcept
{
if (! hasInitialised)
return false;
const std::lock_guard lock { nativeHandleMutex };
if (surface == EGL_NO_SURFACE || context == EGL_NO_CONTEXT)
return false;
if (! eglMakeCurrent (display, surface, surface, context))
return false;
return true;
return hasInitialised
&& surface.get() != EGL_NO_SURFACE
&& context.get() != EGL_NO_CONTEXT
&& eglMakeCurrent (display, surface.get(), surface.get(), context.get());
}
bool isActive() const noexcept { return eglGetCurrentContext() == context; }
bool isActive() const noexcept
{
const std::lock_guard lock { nativeHandleMutex };
return eglGetCurrentContext() == context.get();
}
static void deactivateCurrentContext()
{
@ -264,7 +220,7 @@ public:
}
//==============================================================================
void swapBuffers() const noexcept { eglSwapBuffers (display, surface); }
void swapBuffers() const noexcept { eglSwapBuffers (display, surface.get()); }
bool setSwapInterval (const int) { return false; }
int getSwapInterval() const { return 0; }
@ -295,8 +251,8 @@ public:
ignoreUnused (holder, format, width, height);
}
void surfaceCreated (LocalRef<jobject> holder) override;
void surfaceDestroyed (LocalRef<jobject> holder) override;
void surfaceCreated (LocalRef<jobject>) override;
void surfaceDestroyed (LocalRef<jobject>) override;
//==============================================================================
struct Locker
@ -338,6 +294,8 @@ private:
void dispatchDraw (jobject /*canvas*/)
{
const std::lock_guard lock { nativeHandleMutex };
if (juceContext != nullptr)
juceContext->triggerRepaint();
}
@ -393,6 +351,34 @@ private:
return false;
}
struct NativeWindowReleaser
{
void operator() (ANativeWindow* ptr) const { if (ptr != nullptr) ANativeWindow_release (ptr); }
};
std::unique_ptr<ANativeWindow, NativeWindowReleaser> getNativeWindow() const
{
auto* env = getEnv();
const LocalRef<jobject> holder (env->CallObjectMethod (surfaceView.get(), JuceOpenGLViewSurface.getHolder));
if (holder == nullptr)
return nullptr;
const LocalRef<jobject> jSurface (env->CallObjectMethod (holder.get(), AndroidSurfaceHolder.getSurface));
if (jSurface == nullptr)
return nullptr;
constexpr auto numAttempts = 2;
for (auto i = 0; i < numAttempts; Thread::sleep (200), ++i)
if (auto* ptr = ANativeWindow_fromSurface (env, jSurface.get()))
return std::unique_ptr<ANativeWindow, NativeWindowReleaser> { ptr };
return nullptr;
}
//==============================================================================
CriticalSection mutex;
bool hasInitialised = false;
@ -400,9 +386,20 @@ private:
GlobalRef surfaceView;
Rectangle<int> lastBounds;
struct SurfaceDestructor
{
void operator() (EGLSurface x) const { if (x != EGL_NO_SURFACE) eglDestroySurface (display, x); }
};
struct ContextDestructor
{
void operator() (EGLContext x) const { if (x != EGL_NO_CONTEXT) eglDestroyContext (display, x); }
};
mutable std::mutex nativeHandleMutex;
OpenGLContext* juceContext = nullptr;
EGLSurface surface;
EGLContext context;
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 };
GlobalRef surfaceHolderCallback;
@ -412,6 +409,9 @@ private:
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NativeContext)
};
EGLDisplay OpenGLContext::NativeContext::display = EGL_NO_DISPLAY;
EGLDisplay OpenGLContext::NativeContext::config;
//==============================================================================
void AndroidGLCallbacks::attachedToWindow (JNIEnv*, jobject /*this*/, jlong host)
{

View file

@ -108,7 +108,7 @@ public:
[view release];
}
bool initialiseOnRenderThread (OpenGLContext&) { return true; }
InitResult initialiseOnRenderThread (OpenGLContext&) { return InitResult::success; }
void shutdownOnRenderThread()
{

View file

@ -149,7 +149,7 @@ public:
}
}
bool initialiseOnRenderThread (OpenGLContext& c)
InitResult initialiseOnRenderThread (OpenGLContext& c)
{
XWindowSystemUtilities::ScopedXLock xLock;
@ -196,11 +196,11 @@ public:
renderContext = glXCreateNewContext (display, *bestConfig, GLX_RGBA_TYPE, (GLXContext) contextToShareWith, GL_TRUE);
if (renderContext == nullptr)
return false;
return InitResult::fatal;
c.makeActive();
context = &c;
return true;
return InitResult::success;
}
void shutdownOnRenderThread()

View file

@ -119,8 +119,8 @@ public:
}
}
bool initialiseOnRenderThread (OpenGLContext&) { return true; }
void shutdownOnRenderThread() { deactivateCurrentContext(); }
InitResult initialiseOnRenderThread (OpenGLContext&) { return InitResult::success; }
void shutdownOnRenderThread() { deactivateCurrentContext(); }
bool createdOk() const noexcept { return getRawContext() != nullptr; }
NSOpenGLContext* getRawContext() const noexcept { return renderContext; }

View file

@ -92,11 +92,11 @@ public:
peer->removeScaleFactorListener (this);
}
bool initialiseOnRenderThread (OpenGLContext& c)
InitResult initialiseOnRenderThread (OpenGLContext& c)
{
threadAwarenessSetter = std::make_unique<ScopedThreadDPIAwarenessSetter> (nativeWindow->getNativeHandle());
context = &c;
return true;
return InitResult::success;
}
void shutdownOnRenderThread()

View file

@ -91,10 +91,19 @@ static bool contextHasTextureNpotFeature()
return stringTokens.contains ("GL_ARB_texture_non_power_of_two");
}
//==============================================================================
class OpenGLContext::CachedImage : public CachedComponentImage,
private ThreadPoolJob
template <typename Fn> struct ScopeGuard : Fn
{
~ScopeGuard() { Fn::operator()(); }
};
template <typename Fn> ScopeGuard (Fn) -> ScopeGuard<Fn>;
//==============================================================================
class OpenGLContext::CachedImage : public CachedComponentImage
{
template <typename T, typename U>
static constexpr bool isFlagSet (const T& t, const U& u) { return (t & u) != 0; }
struct AreaAndScale
{
Rectangle<int> area;
@ -136,8 +145,7 @@ class OpenGLContext::CachedImage : public CachedComponentImage,
public:
CachedImage (OpenGLContext& c, Component& comp,
const OpenGLPixelFormat& pixFormat, void* contextToShare)
: ThreadPoolJob ("OpenGL Rendering"),
context (c),
: context (c),
component (comp)
{
nativeContext.reset (new NativeContext (component, pixFormat, contextToShare,
@ -158,59 +166,61 @@ public:
void start()
{
if (nativeContext != nullptr)
{
renderThread = std::make_unique<ThreadPool> (1);
resume();
}
}
void stop()
{
if (renderThread != nullptr)
// make sure everything has finished executing
state |= StateFlags::pendingDestruction;
if (workQueue.size() > 0)
{
// make sure everything has finished executing
destroying = true;
if (! renderThread->contains (this))
resume();
if (workQueue.size() > 0)
{
if (! renderThread->contains (this))
resume();
while (workQueue.size() != 0)
Thread::sleep (20);
}
pause();
renderThread.reset();
while (workQueue.size() != 0)
Thread::sleep (20);
}
hasInitialised = false;
pause();
}
//==============================================================================
void pause()
{
signalJobShouldExit();
contextsWaitingForFlush->deregisterContext (this);
messageManagerLock.abort();
renderThread->remove (this);
if (renderThread != nullptr)
if ((state.fetch_and (~StateFlags::initialised) & StateFlags::initialised) != 0)
{
repaintEvent.signal();
renderThread->removeJob (this, true, -1);
context.makeActive();
shutdownOnThread();
OpenGLContext::deactivateCurrentContext();
}
}
void resume()
{
if (renderThread != nullptr)
renderThread->addJob (this, false);
renderThread->add (this);
}
//==============================================================================
void paint (Graphics&) override
{
updateViewportSize();
if (MessageManager::getInstance()->isThisTheMessageThread())
{
updateViewportSize();
}
else
{
// If you hit this assertion, it's because paint has been called from a thread other
// than the message thread. This commonly happens when nesting OpenGL contexts, because
// the 'outer' OpenGL renderer will attempt to call paint on the 'inner' context's
// component from the OpenGL thread.
// Nesting OpenGL contexts is not directly supported, however there is a workaround:
// https://forum.juce.com/t/opengl-how-do-3d-with-custom-shaders-and-2d-with-juce-paint-methods-work-together/28026/7
jassertfalse;
}
}
bool invalidateAll() override
@ -234,8 +244,8 @@ public:
void triggerRepaint()
{
needsUpdate = 1;
repaintEvent.signal();
state |= (StateFlags::pendingRender | StateFlags::paintComponents);
renderThread->triggerRepaint();
}
//==============================================================================
@ -278,39 +288,99 @@ public:
JUCE_CHECK_OPENGL_ERROR
}
bool renderFrame()
struct ScopedContextActivator
{
MessageManager::Lock::ScopedTryLockType mmLock (messageManagerLock, false);
bool activate (OpenGLContext& ctx)
{
if (! active)
active = ctx.makeActive();
const auto isUpdating = needsUpdate.exchange (false);
return active;
}
~ScopedContextActivator()
{
if (active)
OpenGLContext::deactivateCurrentContext();
}
private:
bool active = false;
};
enum class RenderStatus
{
nominal,
messageThreadAborted,
noWork,
};
RenderStatus renderFrame (MessageManager::Lock& mmLock)
{
if (! isFlagSet (state, StateFlags::initialised))
{
switch (initialiseOnThread())
{
case InitResult::fatal:
case InitResult::retry: return RenderStatus::noWork;
case InitResult::success: break;
}
}
state |= StateFlags::initialised;
#if JUCE_IOS
if (backgroundProcessCheck.isBackgroundProcess())
return RenderStatus::noWork;
#endif
std::optional<MessageManager::Lock::ScopedTryLockType> scopedLock;
ScopedContextActivator contextActivator;
const auto stateToUse = state.fetch_and (StateFlags::persistent);
if (! isFlagSet (stateToUse, StateFlags::pendingRender) && ! context.continuousRepaint)
return RenderStatus::noWork;
const auto isUpdating = isFlagSet (stateToUse, StateFlags::paintComponents);
if (context.renderComponents && isUpdating)
{
bool abortScope = false;
// If we early-exit here, we need to restore these flags so that the render is
// attempted again in the next time slice.
const ScopeGuard scope { [&] { if (! abortScope) state |= stateToUse; } };
// This avoids hogging the message thread when doing intensive rendering.
if (lastMMLockReleaseTime + 1 >= Time::getMillisecondCounter())
Thread::sleep (2);
std::this_thread::sleep_until (lastMMLockReleaseTime + std::chrono::milliseconds { 2 });
while (! shouldExit())
{
doWorkWhileWaitingForLock (false);
if (renderThread->isListChanging())
return RenderStatus::messageThreadAborted;
if (mmLock.retryLock())
break;
}
doWorkWhileWaitingForLock (contextActivator);
if (shouldExit())
return false;
scopedLock.emplace (mmLock);
// If we can't get the lock here, it's probably because a context has been removed
// on the main thread.
// We return, just in case this renderer needs to be removed from the rendering thread.
// If another renderer is being removed instead, then we should be able to get the lock
// next time round.
if (! scopedLock->isLocked())
return RenderStatus::messageThreadAborted;
abortScope = true;
}
if (! context.makeActive())
return false;
if (! contextActivator.activate (context))
return RenderStatus::noWork;
{
NativeContext::Locker locker (*nativeContext);
JUCE_CHECK_OPENGL_ERROR
doWorkWhileWaitingForLock (true);
doWorkWhileWaitingForLock (contextActivator);
const auto currentAreaAndScale = areaAndScale.get();
const auto viewportArea = currentAreaAndScale.area;
@ -331,23 +401,20 @@ public:
{
paintComponent (currentAreaAndScale);
if (! hasInitialised)
return false;
if (! isFlagSet (state, StateFlags::initialised))
return RenderStatus::noWork;
messageManagerLock.exit();
lastMMLockReleaseTime = Time::getMillisecondCounter();
scopedLock.reset();
lastMMLockReleaseTime = std::chrono::steady_clock::now();
}
glViewport (0, 0, viewportArea.getWidth(), viewportArea.getHeight());
drawComponentBuffer();
}
OpenGLContext::deactivateCurrentContext();
}
contextsWaitingForFlush->flush (this);
return true;
nativeContext->swapBuffers();
return RenderStatus::nominal;
}
void updateViewportSize()
@ -520,79 +587,19 @@ public:
updateViewportSize();
#if JUCE_MAC
if (hasInitialised)
if (isFlagSet (state, StateFlags::initialised))
{
[nativeContext->view update];
renderFrame();
// We're already on the message thread, no need to lock it again.
MessageManager::Lock mml;
renderFrame (mml);
}
#endif
}
//==============================================================================
JobStatus runJob() override
{
{
// Allow the message thread to finish setting-up the context before using it.
MessageManager::Lock::ScopedTryLockType mmLock (messageManagerLock, false);
do
{
if (shouldExit())
return ThreadPoolJob::jobHasFinished;
} while (! mmLock.retryLock());
}
if (! initialiseOnThread())
{
hasInitialised = false;
return ThreadPoolJob::jobHasFinished;
}
contextsWaitingForFlush->registerContext (this);
hasInitialised = true;
while (! shouldExit())
{
#if JUCE_IOS
if (backgroundProcessCheck.isBackgroundProcess())
{
repaintEvent.wait (300);
repaintEvent.reset();
continue;
}
#endif
if (shouldExit())
break;
#if JUCE_MAC
if (context.continuousRepaint)
{
repaintEvent.wait (-1);
renderFrame();
}
else
#endif
if (! renderFrame())
repaintEvent.wait (5); // failed to render, so avoid a tight fail-loop.
else if (! context.continuousRepaint && ! shouldExit())
repaintEvent.wait (-1);
repaintEvent.reset();
}
hasInitialised = false;
context.makeActive();
shutdownOnThread();
OpenGLContext::deactivateCurrentContext();
return ThreadPoolJob::jobHasFinished;
}
bool initialiseOnThread()
InitResult initialiseOnThread()
{
// On android, this can get called twice, so drop any previous state.
associatedObjectNames.clear();
@ -601,8 +608,8 @@ public:
context.makeActive();
if (! nativeContext->initialiseOnRenderThread (context))
return false;
if (const auto nativeResult = nativeContext->initialiseOnRenderThread (context); nativeResult != InitResult::success)
return nativeResult;
#if JUCE_ANDROID
// On android the context may be created in initialiseOnRenderThread
@ -649,7 +656,7 @@ public:
if (context.renderer != nullptr)
context.renderer->newOpenGLContextCreated();
return true;
return InitResult::success;
}
void shutdownOnThread()
@ -712,36 +719,23 @@ public:
WaitableEvent finishedSignal;
};
bool doWorkWhileWaitingForLock (bool contextIsAlreadyActive)
void doWorkWhileWaitingForLock (ScopedContextActivator& contextActivator)
{
bool contextActivated = false;
for (OpenGLContext::AsyncWorker::Ptr work = workQueue.removeAndReturn (0);
work != nullptr && (! shouldExit()); work = workQueue.removeAndReturn (0))
while (const auto work = workQueue.removeAndReturn (0))
{
if ((! contextActivated) && (! contextIsAlreadyActive))
{
if (! context.makeActive())
break;
contextActivated = true;
}
if (renderThread->isListChanging() || ! contextActivator.activate (context))
break;
NativeContext::Locker locker (*nativeContext);
(*work) (context);
clearGLError();
}
if (contextActivated)
OpenGLContext::deactivateCurrentContext();
return shouldExit();
}
void execute (OpenGLContext::AsyncWorker::Ptr workerToUse, bool shouldBlock, bool calledFromDestructor = false)
void execute (OpenGLContext::AsyncWorker::Ptr workerToUse, bool shouldBlock)
{
if (calledFromDestructor || ! destroying)
if (! isFlagSet (state, StateFlags::pendingDestruction))
{
if (shouldBlock)
{
@ -749,7 +743,7 @@ public:
OpenGLContext::AsyncWorker::Ptr worker (*blocker);
workQueue.add (worker);
messageManagerLock.abort();
renderThread->abortLock();
context.triggerRepaint();
blocker->block();
@ -758,7 +752,7 @@ public:
{
workQueue.add (std::move (workerToUse));
messageManagerLock.abort();
renderThread->abortLock();
context.triggerRepaint();
}
}
@ -774,96 +768,155 @@ public:
return dynamic_cast<CachedImage*> (c.getCachedComponentImage());
}
//==============================================================================
class ContextsWaitingForFlush : private AsyncUpdater
class RenderThread
{
public:
void registerContext (CachedImage* ctx)
RenderThread() = default;
~RenderThread()
{
const std::lock_guard<std::mutex> lock (mutex);
needsRender.insert_or_assign (ctx, false);
flags.setDestructing();
thread.join();
}
void deregisterContext (CachedImage* ctx)
void add (CachedImage* x)
{
const std::lock_guard<std::mutex> lock (mutex);
needsRender.erase (ctx);
condvar.notify_all();
const std::scoped_lock lock { listMutex };
images.push_back (x);
}
/* Ask to swap the CachedImage's buffers on the main thread.
Will block until the buffers have been swapped, or until the swap has been cancelled.
*/
void flush (CachedImage* ctx)
void remove (CachedImage* x)
{
JUCE_ASSERT_MESSAGE_THREAD;
flags.setSafe (false);
abortLock();
{
const std::lock_guard<std::mutex> lock (mutex);
if (const auto iter = needsRender.find (ctx); iter != needsRender.cend())
iter->second = true;
const std::scoped_lock lock { callbackMutex, listMutex };
images.remove (x);
}
if (MessageManager::getInstance()->isThisTheMessageThread())
{
handleAsyncUpdate();
}
else
{
triggerAsyncUpdate();
std::unique_lock<std::mutex> lock { mutex };
condvar.wait (lock, [this, ctx]
{
// If the context is still registered, continue once it no longer needs rendering.
if (const auto iter = needsRender.find (ctx); iter != needsRender.cend())
return ! iter->second;
// If the context is no longer registered, continue so that the thread can be destroyed.
return true;
});
}
flags.setSafe (true);
}
~ContextsWaitingForFlush() override
bool contains (CachedImage* x)
{
cancelPendingUpdate();
// There definitely shouldn't still be active CachedImages if this object is being destroyed!
jassert (needsRender.empty());
const std::scoped_lock lock { listMutex };
return std::find (images.cbegin(), images.cend(), x) != images.cend();
}
void triggerRepaint() { flags.setRenderRequested(); }
void abortLock() { messageManagerLock.abort(); }
bool isListChanging() { return ! flags.isSafe(); }
private:
std::mutex mutex;
std::condition_variable condvar;
std::map<CachedImage*, bool> needsRender;
/* Swaps the buffers for each of the pending contexts, then notifies all rendering threads
that they may continue.
*/
void handleAsyncUpdate() override
RenderStatus renderAll()
{
const std::unique_lock<std::mutex> lock (mutex);
auto result = RenderStatus::noWork;
for (auto& [ctx, shouldSwap] : needsRender)
const std::scoped_lock lock { callbackMutex, listMutex };
for (auto* x : images)
{
if (! shouldSwap)
continue;
listMutex.unlock();
const ScopeGuard scope { [&] { listMutex.lock(); } };
auto& native = *ctx->nativeContext;
const auto status = x->renderFrame (messageManagerLock);
switch (status)
{
case RenderStatus::noWork: break;
case RenderStatus::nominal: result = RenderStatus::nominal; break;
case RenderStatus::messageThreadAborted: return RenderStatus::messageThreadAborted;
}
const OpenGLContext::NativeContext::Locker nativeContextLocker { native };
native.makeActive();
native.swapBuffers();
native.deactivateCurrentContext();
shouldSwap = false;
}
condvar.notify_all();
return result;
}
};
SharedResourcePointer<ContextsWaitingForFlush> contextsWaitingForFlush;
/* Allows the main thread to communicate changes to the render thread.
When the render thread needs to change in some way (asked to resume rendering,
a renderer is added/removed, or the thread needs to stop prior to destruction),
the main thread can set the appropriate flag on this structure. The render thread
will call waitForWork() repeatedly, pausing when the render thread has no work to do,
and resuming when requested by the main thread.
*/
class Flags
{
public:
void setDestructing() { update ([] (auto& f) { f |= destructorCalled; }); }
void setRenderRequested() { update ([] (auto& f) { f |= renderRequested; }); }
void setSafe (const bool safe)
{
update ([safe] (auto& f)
{
if (safe)
f |= listSafe;
else
f &= ~listSafe;
});
}
bool isSafe()
{
const std::scoped_lock lock { mutex };
return (flags & listSafe) != 0;
}
/* Blocks until the 'safe' flag is set, and at least one other flag is set.
After returning, the renderRequested flag will be unset.
Returns true if rendering should continue.
*/
bool waitForWork (bool requestRender)
{
std::unique_lock lock { mutex };
flags |= (requestRender ? renderRequested : 0);
condvar.wait (lock, [this] { return flags > listSafe; });
flags &= ~renderRequested;
return ((flags & destructorCalled) == 0);
}
private:
template <typename Fn>
void update (Fn fn)
{
{
const std::scoped_lock lock { mutex };
fn (flags);
}
condvar.notify_one();
}
enum
{
renderRequested = 1 << 0,
destructorCalled = 1 << 1,
listSafe = 1 << 2
};
std::mutex mutex;
std::condition_variable condvar;
int flags = listSafe;
};
MessageManager::Lock messageManagerLock;
std::mutex listMutex, callbackMutex;
std::list<CachedImage*> images;
Flags flags;
std::thread thread { [this]
{
Thread::setCurrentThreadName ("OpenGL Renderer");
while (flags.waitForWork (renderAll() != RenderStatus::noWork)) {}
} };
};
//==============================================================================
friend class NativeContext;
@ -872,6 +925,8 @@ public:
OpenGLContext& context;
Component& component;
SharedResourcePointer<RenderThread> renderThread;
OpenGLFrameBuffer cachedImageFrameBuffer;
RectangleList<int> validArea;
Rectangle<int> lastScreenBounds;
@ -882,15 +937,14 @@ public:
StringArray associatedObjectNames;
ReferenceCountedArray<ReferenceCountedObject> associatedObjects;
WaitableEvent canPaintNowFlag, finishedPaintingFlag, repaintEvent { true };
WaitableEvent canPaintNowFlag, finishedPaintingFlag;
#if JUCE_OPENGL_ES
bool shadersAvailable = true;
#else
bool shadersAvailable = false;
#endif
bool textureNpotSupported = false;
std::atomic<bool> hasInitialised { false }, needsUpdate { true }, destroying { false };
uint32 lastMMLockReleaseTime = 0;
std::chrono::steady_clock::time_point lastMMLockReleaseTime{};
#if JUCE_MAC
NSView* getCurrentView() const
@ -944,23 +998,24 @@ public:
// Note: the NSViewComponentPeer also has a SharedResourcePointer<PerScreenDisplayLinks> to
// avoid unnecessarily duplicating display-link threads.
SharedResourcePointer<PerScreenDisplayLinks> sharedDisplayLinks;
PerScreenDisplayLinks::Connection connection { sharedDisplayLinks->registerFactory ([this] (auto* screen)
{
return [this, screen]
{
// Note: check against lastScreen rather than trying to access the component's peer here,
// because this function is not called on the main thread.
if (context.continuousRepaint)
if (screen == lastScreen)
repaintEvent.signal();
};
}) };
#endif
std::unique_ptr<ThreadPool> renderThread;
enum StateFlags
{
pendingRender = 1 << 0,
paintComponents = 1 << 1,
pendingDestruction = 1 << 2,
initialised = 1 << 3,
// Flags that may change state after each frame
transient = pendingRender | paintComponents,
// Flags that should retain their state after each frame
persistent = initialised | pendingDestruction
};
std::atomic<int> state { 0 };
ReferenceCountedArray<OpenGLContext::AsyncWorker, CriticalSection> workQueue;
MessageManager::Lock messageManagerLock;
#if JUCE_IOS
iOSBackgroundProcessCheck backgroundProcessCheck;
@ -1541,40 +1596,56 @@ void OpenGLContext::copyTexture (const Rectangle<int>& targetClipArea,
}
#if JUCE_ANDROID
EGLDisplay OpenGLContext::NativeContext::display = EGL_NO_DISPLAY;
EGLDisplay OpenGLContext::NativeContext::config;
void OpenGLContext::NativeContext::surfaceCreated (LocalRef<jobject> holder)
void OpenGLContext::NativeContext::surfaceCreated (LocalRef<jobject>)
{
ignoreUnused (holder);
if (auto* cachedImage = CachedImage::get (component))
{
if (auto* pool = cachedImage->renderThread.get())
const std::lock_guard lock { nativeHandleMutex };
jassert (hasInitialised);
// has the context already attached?
jassert (surface.get() == EGL_NO_SURFACE && context.get() == EGL_NO_CONTEXT);
const auto window = getNativeWindow();
if (window == nullptr)
{
if (! pool->contains (cachedImage))
{
cachedImage->resume();
cachedImage->context.triggerRepaint();
}
// failed to get a pointer to the native window so bail out
jassertfalse;
return;
}
// create the surface
surface.reset (eglCreateWindowSurface (display, config, window.get(), nullptr));
jassert (surface.get() != EGL_NO_SURFACE);
// create the OpenGL context
EGLint contextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE };
context.reset (eglCreateContext (display, config, EGL_NO_CONTEXT, contextAttribs));
jassert (context.get() != EGL_NO_CONTEXT);
}
}
void OpenGLContext::NativeContext::surfaceDestroyed (LocalRef<jobject> holder)
{
ignoreUnused (holder);
// unlike the name suggests this will be called just before the
// surface is destroyed. We need to pause the render thread.
if (auto* cachedImage = CachedImage::get (component))
if (auto* cached = CachedImage::get (component))
{
cachedImage->pause();
if (auto* threadPool = cachedImage->renderThread.get())
threadPool->waitForJobToFinish (cachedImage, -1);
cached->resume();
cached->triggerRepaint();
}
}
void OpenGLContext::NativeContext::surfaceDestroyed (LocalRef<jobject>)
{
if (auto* cached = CachedImage::get (component))
cached->pause();
{
const std::lock_guard lock { nativeHandleMutex };
context.reset (EGL_NO_CONTEXT);
surface.reset (EGL_NO_SURFACE);
}
}
#endif
} // namespace juce

View file

@ -320,6 +320,13 @@ public:
#endif
private:
enum class InitResult
{
fatal,
retry,
success
};
friend class OpenGLTexture;
class CachedImage;