diff --git a/modules/juce_core/system/juce_StandardHeader.h b/modules/juce_core/system/juce_StandardHeader.h index 2fdba455ba..05c3fbb953 100644 --- a/modules/juce_core/system/juce_StandardHeader.h +++ b/modules/juce_core/system/juce_StandardHeader.h @@ -63,16 +63,16 @@ #include #include #include +#include #include #include #include +#include #include #include #include #include #include -#include -#include //============================================================================== #include "juce_CompilerSupport.h" diff --git a/modules/juce_opengl/native/juce_OpenGL_android.h b/modules/juce_opengl/native/juce_OpenGL_android.h index 20fbb741ec..4358a6d7e1 100644 --- a/modules/juce_opengl/native/juce_OpenGL_android.h +++ b/modules/juce_opengl/native/juce_OpenGL_android.h @@ -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 holder (env->CallObjectMethod (surfaceView.get(), JuceOpenGLViewSurface.getHolder)); - - if (holder != nullptr) - { - LocalRef 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 holder) override; - void surfaceDestroyed (LocalRef holder) override; + void surfaceCreated (LocalRef) override; + void surfaceDestroyed (LocalRef) 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 getNativeWindow() const + { + auto* env = getEnv(); + + const LocalRef holder (env->CallObjectMethod (surfaceView.get(), JuceOpenGLViewSurface.getHolder)); + + if (holder == nullptr) + return nullptr; + + const LocalRef 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 { ptr }; + + return nullptr; + } + //============================================================================== CriticalSection mutex; bool hasInitialised = false; @@ -400,9 +386,20 @@ private: GlobalRef surfaceView; Rectangle 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, SurfaceDestructor> surface { EGL_NO_SURFACE }; + std::unique_ptr, 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) { diff --git a/modules/juce_opengl/native/juce_OpenGL_ios.h b/modules/juce_opengl/native/juce_OpenGL_ios.h index 98c83c4692..cf29a2a5fe 100644 --- a/modules/juce_opengl/native/juce_OpenGL_ios.h +++ b/modules/juce_opengl/native/juce_OpenGL_ios.h @@ -108,7 +108,7 @@ public: [view release]; } - bool initialiseOnRenderThread (OpenGLContext&) { return true; } + InitResult initialiseOnRenderThread (OpenGLContext&) { return InitResult::success; } void shutdownOnRenderThread() { diff --git a/modules/juce_opengl/native/juce_OpenGL_linux_X11.h b/modules/juce_opengl/native/juce_OpenGL_linux_X11.h index f4fe4d20b8..987d32635e 100644 --- a/modules/juce_opengl/native/juce_OpenGL_linux_X11.h +++ b/modules/juce_opengl/native/juce_OpenGL_linux_X11.h @@ -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() diff --git a/modules/juce_opengl/native/juce_OpenGL_osx.h b/modules/juce_opengl/native/juce_OpenGL_osx.h index 25a03daa94..f81b220ff3 100644 --- a/modules/juce_opengl/native/juce_OpenGL_osx.h +++ b/modules/juce_opengl/native/juce_OpenGL_osx.h @@ -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; } diff --git a/modules/juce_opengl/native/juce_OpenGL_win32.h b/modules/juce_opengl/native/juce_OpenGL_win32.h index dc956b7984..db07f7a859 100644 --- a/modules/juce_opengl/native/juce_OpenGL_win32.h +++ b/modules/juce_opengl/native/juce_OpenGL_win32.h @@ -92,11 +92,11 @@ public: peer->removeScaleFactorListener (this); } - bool initialiseOnRenderThread (OpenGLContext& c) + InitResult initialiseOnRenderThread (OpenGLContext& c) { threadAwarenessSetter = std::make_unique (nativeWindow->getNativeHandle()); context = &c; - return true; + return InitResult::success; } void shutdownOnRenderThread() diff --git a/modules/juce_opengl/opengl/juce_OpenGLContext.cpp b/modules/juce_opengl/opengl/juce_OpenGLContext.cpp index d96cce6614..4f6c22c8bb 100644 --- a/modules/juce_opengl/opengl/juce_OpenGLContext.cpp +++ b/modules/juce_opengl/opengl/juce_OpenGLContext.cpp @@ -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 struct ScopeGuard : Fn { + ~ScopeGuard() { Fn::operator()(); } +}; + +template ScopeGuard (Fn) -> ScopeGuard; + +//============================================================================== +class OpenGLContext::CachedImage : public CachedComponentImage +{ + template + static constexpr bool isFlagSet (const T& t, const U& u) { return (t & u) != 0; } + struct AreaAndScale { Rectangle 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 (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 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 (c.getCachedComponentImage()); } - //============================================================================== - class ContextsWaitingForFlush : private AsyncUpdater + class RenderThread { public: - void registerContext (CachedImage* ctx) + RenderThread() = default; + + ~RenderThread() { - const std::lock_guard lock (mutex); - needsRender.insert_or_assign (ctx, false); + flags.setDestructing(); + thread.join(); } - void deregisterContext (CachedImage* ctx) + void add (CachedImage* x) { - const std::lock_guard 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 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 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 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 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; + /* 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 + 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 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; + OpenGLFrameBuffer cachedImageFrameBuffer; RectangleList validArea; Rectangle lastScreenBounds; @@ -882,15 +937,14 @@ public: StringArray associatedObjectNames; ReferenceCountedArray 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 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 to // avoid unnecessarily duplicating display-link threads. SharedResourcePointer 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 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 state { 0 }; ReferenceCountedArray workQueue; - MessageManager::Lock messageManagerLock; #if JUCE_IOS iOSBackgroundProcessCheck backgroundProcessCheck; @@ -1541,40 +1596,56 @@ void OpenGLContext::copyTexture (const Rectangle& targetClipArea, } #if JUCE_ANDROID -EGLDisplay OpenGLContext::NativeContext::display = EGL_NO_DISPLAY; -EGLDisplay OpenGLContext::NativeContext::config; -void OpenGLContext::NativeContext::surfaceCreated (LocalRef holder) +void OpenGLContext::NativeContext::surfaceCreated (LocalRef) { - 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 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) +{ + 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 diff --git a/modules/juce_opengl/opengl/juce_OpenGLContext.h b/modules/juce_opengl/opengl/juce_OpenGLContext.h index f02d70f32e..86191f842e 100644 --- a/modules/juce_opengl/opengl/juce_OpenGLContext.h +++ b/modules/juce_opengl/opengl/juce_OpenGLContext.h @@ -320,6 +320,13 @@ public: #endif private: + enum class InitResult + { + fatal, + retry, + success + }; + friend class OpenGLTexture; class CachedImage;