mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-10 23:44:24 +00:00
On Windows, the OpenGL context window sometimes receives a repaint request after moving between screens with different scale factors. If the screen has changed size/scale since the last paint operation, failing to invalidate the painted area may cause the screen contents to be drawn at the wrong scale until paint is next called.
1523 lines
47 KiB
C++
1523 lines
47 KiB
C++
/*
|
|
==============================================================================
|
|
|
|
This file is part of the JUCE library.
|
|
Copyright (c) 2022 - Raw Material Software Limited
|
|
|
|
JUCE is an open source library subject to commercial or open-source
|
|
licensing.
|
|
|
|
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
|
Agreement and JUCE Privacy Policy.
|
|
|
|
End User License Agreement: www.juce.com/juce-7-licence
|
|
Privacy Policy: www.juce.com/juce-privacy-policy
|
|
|
|
Or: You may also use this code under the terms of the GPL v3 (see
|
|
www.gnu.org/licenses).
|
|
|
|
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
|
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
|
DISCLAIMED.
|
|
|
|
==============================================================================
|
|
*/
|
|
|
|
namespace juce
|
|
{
|
|
|
|
#if JUCE_IOS
|
|
struct AppInactivityCallback // NB: this is a duplicate of an internal declaration in juce_core
|
|
{
|
|
virtual ~AppInactivityCallback() {}
|
|
virtual void appBecomingInactive() = 0;
|
|
};
|
|
|
|
extern Array<AppInactivityCallback*> appBecomingInactiveCallbacks;
|
|
|
|
// On iOS, all GL calls will crash when the app is running in the background, so
|
|
// this prevents them from happening (which some messy locking behaviour)
|
|
struct iOSBackgroundProcessCheck : public AppInactivityCallback
|
|
{
|
|
iOSBackgroundProcessCheck() { isBackgroundProcess(); appBecomingInactiveCallbacks.add (this); }
|
|
~iOSBackgroundProcessCheck() override { appBecomingInactiveCallbacks.removeAllInstancesOf (this); }
|
|
|
|
bool isBackgroundProcess()
|
|
{
|
|
const bool b = Process::isForegroundProcess();
|
|
isForeground.set (b ? 1 : 0);
|
|
return ! b;
|
|
}
|
|
|
|
void appBecomingInactive() override
|
|
{
|
|
int counter = 2000;
|
|
|
|
while (--counter > 0 && isForeground.get() != 0)
|
|
Thread::sleep (1);
|
|
}
|
|
|
|
private:
|
|
Atomic<int> isForeground;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (iOSBackgroundProcessCheck)
|
|
};
|
|
|
|
#endif
|
|
|
|
#if JUCE_WINDOWS && JUCE_WIN_PER_MONITOR_DPI_AWARE
|
|
extern JUCE_API double getScaleFactorForWindow (HWND);
|
|
#endif
|
|
|
|
static bool contextHasTextureNpotFeature()
|
|
{
|
|
if (getOpenGLVersion() >= Version (2))
|
|
return true;
|
|
|
|
// If the version is < 2, we can't use the newer extension-checking API
|
|
// so we have to use glGetString
|
|
const auto* extensionsBegin = glGetString (GL_EXTENSIONS);
|
|
|
|
if (extensionsBegin == nullptr)
|
|
return false;
|
|
|
|
const auto* extensionsEnd = findNullTerminator (extensionsBegin);
|
|
const std::string extensionsString (extensionsBegin, extensionsEnd);
|
|
const auto stringTokens = StringArray::fromTokens (extensionsString.c_str(), false);
|
|
return stringTokens.contains ("GL_ARB_texture_non_power_of_two");
|
|
}
|
|
|
|
//==============================================================================
|
|
class OpenGLContext::CachedImage : public CachedComponentImage,
|
|
private ThreadPoolJob
|
|
{
|
|
struct AreaAndScale
|
|
{
|
|
Rectangle<int> area;
|
|
double scale;
|
|
|
|
auto tie() const { return std::tie (area, scale); }
|
|
|
|
auto operator== (const AreaAndScale& other) const { return tie() == other.tie(); }
|
|
auto operator!= (const AreaAndScale& other) const { return tie() != other.tie(); }
|
|
};
|
|
|
|
class LockedAreaAndScale
|
|
{
|
|
public:
|
|
auto get() const
|
|
{
|
|
const ScopedLock lock (mutex);
|
|
return data;
|
|
}
|
|
|
|
template <typename Fn>
|
|
void set (const AreaAndScale& d, Fn&& ifDifferent)
|
|
{
|
|
const auto old = [&]
|
|
{
|
|
const ScopedLock lock (mutex);
|
|
return std::exchange (data, d);
|
|
}();
|
|
|
|
if (old != d)
|
|
ifDifferent();
|
|
}
|
|
|
|
private:
|
|
CriticalSection mutex;
|
|
AreaAndScale data { {}, 1.0 };
|
|
};
|
|
|
|
public:
|
|
CachedImage (OpenGLContext& c, Component& comp,
|
|
const OpenGLPixelFormat& pixFormat, void* contextToShare)
|
|
: ThreadPoolJob ("OpenGL Rendering"),
|
|
context (c),
|
|
component (comp)
|
|
{
|
|
nativeContext.reset (new NativeContext (component, pixFormat, contextToShare,
|
|
c.useMultisampling, c.versionRequired));
|
|
|
|
if (nativeContext->createdOk())
|
|
context.nativeContext = nativeContext.get();
|
|
else
|
|
nativeContext.reset();
|
|
}
|
|
|
|
~CachedImage() override
|
|
{
|
|
stop();
|
|
}
|
|
|
|
//==============================================================================
|
|
void start()
|
|
{
|
|
if (nativeContext != nullptr)
|
|
{
|
|
#if JUCE_MAC
|
|
cvDisplayLinkWrapper = std::make_unique<CVDisplayLinkWrapper> (*this);
|
|
cvDisplayLinkWrapper->updateActiveDisplay();
|
|
#endif
|
|
|
|
renderThread = std::make_unique<ThreadPool> (1);
|
|
resume();
|
|
}
|
|
}
|
|
|
|
void stop()
|
|
{
|
|
if (renderThread != nullptr)
|
|
{
|
|
// make sure everything has finished executing
|
|
destroying = true;
|
|
|
|
if (workQueue.size() > 0)
|
|
{
|
|
if (! renderThread->contains (this))
|
|
resume();
|
|
|
|
while (workQueue.size() != 0)
|
|
Thread::sleep (20);
|
|
}
|
|
|
|
pause();
|
|
renderThread.reset();
|
|
}
|
|
|
|
#if JUCE_MAC
|
|
cvDisplayLinkWrapper = nullptr;
|
|
#endif
|
|
|
|
hasInitialised = false;
|
|
}
|
|
|
|
//==============================================================================
|
|
void pause()
|
|
{
|
|
signalJobShouldExit();
|
|
messageManagerLock.abort();
|
|
|
|
if (renderThread != nullptr)
|
|
{
|
|
repaintEvent.signal();
|
|
renderThread->removeJob (this, true, -1);
|
|
}
|
|
}
|
|
|
|
void resume()
|
|
{
|
|
if (renderThread != nullptr)
|
|
renderThread->addJob (this, false);
|
|
}
|
|
|
|
//==============================================================================
|
|
void paint (Graphics&) override
|
|
{
|
|
updateViewportSize();
|
|
}
|
|
|
|
bool invalidateAll() override
|
|
{
|
|
validArea.clear();
|
|
triggerRepaint();
|
|
return false;
|
|
}
|
|
|
|
bool invalidate (const Rectangle<int>& area) override
|
|
{
|
|
validArea.subtract (area.toFloat().transformedBy (transform).getSmallestIntegerContainer());
|
|
triggerRepaint();
|
|
return false;
|
|
}
|
|
|
|
void releaseResources() override
|
|
{
|
|
stop();
|
|
}
|
|
|
|
void triggerRepaint()
|
|
{
|
|
needsUpdate = 1;
|
|
repaintEvent.signal();
|
|
}
|
|
|
|
//==============================================================================
|
|
bool ensureFrameBufferSize (Rectangle<int> viewportArea)
|
|
{
|
|
JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED
|
|
|
|
auto fbW = cachedImageFrameBuffer.getWidth();
|
|
auto fbH = cachedImageFrameBuffer.getHeight();
|
|
|
|
if (fbW != viewportArea.getWidth() || fbH != viewportArea.getHeight() || ! cachedImageFrameBuffer.isValid())
|
|
{
|
|
if (! cachedImageFrameBuffer.initialise (context, viewportArea.getWidth(), viewportArea.getHeight()))
|
|
return false;
|
|
|
|
validArea.clear();
|
|
JUCE_CHECK_OPENGL_ERROR
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void clearRegionInFrameBuffer (const RectangleList<int>& list)
|
|
{
|
|
glClearColor (0, 0, 0, 0);
|
|
glEnable (GL_SCISSOR_TEST);
|
|
|
|
auto previousFrameBufferTarget = OpenGLFrameBuffer::getCurrentFrameBufferTarget();
|
|
cachedImageFrameBuffer.makeCurrentRenderingTarget();
|
|
auto imageH = cachedImageFrameBuffer.getHeight();
|
|
|
|
for (auto& r : list)
|
|
{
|
|
glScissor (r.getX(), imageH - r.getBottom(), r.getWidth(), r.getHeight());
|
|
glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
|
}
|
|
|
|
glDisable (GL_SCISSOR_TEST);
|
|
context.extensions.glBindFramebuffer (GL_FRAMEBUFFER, previousFrameBufferTarget);
|
|
JUCE_CHECK_OPENGL_ERROR
|
|
}
|
|
|
|
bool renderFrame()
|
|
{
|
|
MessageManager::Lock::ScopedTryLockType mmLock (messageManagerLock, false);
|
|
|
|
auto isUpdatingTestValue = true;
|
|
auto isUpdating = needsUpdate.compare_exchange_strong (isUpdatingTestValue, false);
|
|
|
|
if (context.renderComponents && isUpdating)
|
|
{
|
|
// This avoids hogging the message thread when doing intensive rendering.
|
|
if (lastMMLockReleaseTime + 1 >= Time::getMillisecondCounter())
|
|
Thread::sleep (2);
|
|
|
|
while (! shouldExit())
|
|
{
|
|
doWorkWhileWaitingForLock (false);
|
|
|
|
if (mmLock.retryLock())
|
|
break;
|
|
}
|
|
|
|
if (shouldExit())
|
|
return false;
|
|
}
|
|
|
|
if (! context.makeActive())
|
|
return false;
|
|
|
|
NativeContext::Locker locker (*nativeContext);
|
|
|
|
JUCE_CHECK_OPENGL_ERROR
|
|
|
|
doWorkWhileWaitingForLock (true);
|
|
|
|
const auto currentAreaAndScale = areaAndScale.get();
|
|
const auto viewportArea = currentAreaAndScale.area;
|
|
|
|
if (context.renderer != nullptr)
|
|
{
|
|
glViewport (0, 0, viewportArea.getWidth(), viewportArea.getHeight());
|
|
context.currentRenderScale = currentAreaAndScale.scale;
|
|
context.renderer->renderOpenGL();
|
|
clearGLError();
|
|
|
|
bindVertexArray();
|
|
}
|
|
|
|
if (context.renderComponents)
|
|
{
|
|
if (isUpdating)
|
|
{
|
|
paintComponent (currentAreaAndScale);
|
|
|
|
if (! hasInitialised)
|
|
return false;
|
|
|
|
messageManagerLock.exit();
|
|
lastMMLockReleaseTime = Time::getMillisecondCounter();
|
|
}
|
|
|
|
glViewport (0, 0, viewportArea.getWidth(), viewportArea.getHeight());
|
|
drawComponentBuffer();
|
|
}
|
|
|
|
context.swapBuffers();
|
|
|
|
OpenGLContext::deactivateCurrentContext();
|
|
return true;
|
|
}
|
|
|
|
void updateViewportSize()
|
|
{
|
|
JUCE_ASSERT_MESSAGE_THREAD
|
|
|
|
if (auto* peer = component.getPeer())
|
|
{
|
|
#if JUCE_MAC
|
|
const auto displayScale = Desktop::getInstance().getGlobalScaleFactor() * [this]
|
|
{
|
|
if (auto* wrapper = cvDisplayLinkWrapper.get())
|
|
if (wrapper->updateActiveDisplay())
|
|
nativeContext->setNominalVideoRefreshPeriodS (wrapper->getNominalVideoRefreshPeriodS());
|
|
|
|
if (auto* view = getCurrentView())
|
|
{
|
|
if ([view respondsToSelector: @selector (backingScaleFactor)])
|
|
return [(id) view backingScaleFactor];
|
|
|
|
if (auto* window = [view window])
|
|
return [window backingScaleFactor];
|
|
}
|
|
|
|
return areaAndScale.get().scale;
|
|
}();
|
|
#else
|
|
const auto displayScale = Desktop::getInstance().getDisplays().getDisplayForRect (component.getTopLevelComponent()->getScreenBounds())->scale;
|
|
#endif
|
|
|
|
auto localBounds = component.getLocalBounds();
|
|
auto newArea = peer->getComponent().getLocalArea (&component, localBounds).withZeroOrigin() * displayScale;
|
|
|
|
#if JUCE_WINDOWS && JUCE_WIN_PER_MONITOR_DPI_AWARE
|
|
auto newScale = getScaleFactorForWindow (nativeContext->getNativeHandle());
|
|
auto desktopScale = Desktop::getInstance().getGlobalScaleFactor();
|
|
|
|
if (! approximatelyEqual (1.0f, desktopScale))
|
|
newScale *= desktopScale;
|
|
#else
|
|
auto newScale = displayScale;
|
|
#endif
|
|
|
|
areaAndScale.set ({ newArea, newScale }, [&]
|
|
{
|
|
// Transform is only accessed when the message manager is locked
|
|
transform = AffineTransform::scale ((float) newArea.getWidth() / (float) localBounds.getWidth(),
|
|
(float) newArea.getHeight() / (float) localBounds.getHeight());
|
|
|
|
nativeContext->updateWindowPosition (peer->getAreaCoveredBy (component));
|
|
invalidateAll();
|
|
});
|
|
}
|
|
}
|
|
|
|
void bindVertexArray() noexcept
|
|
{
|
|
if (shouldUseCustomVAO())
|
|
if (vertexArrayObject != 0)
|
|
context.extensions.glBindVertexArray (vertexArrayObject);
|
|
}
|
|
|
|
void checkViewportBounds()
|
|
{
|
|
auto screenBounds = component.getTopLevelComponent()->getScreenBounds();
|
|
|
|
if (lastScreenBounds != screenBounds)
|
|
{
|
|
updateViewportSize();
|
|
lastScreenBounds = screenBounds;
|
|
}
|
|
}
|
|
|
|
void paintComponent (const AreaAndScale& currentAreaAndScale)
|
|
{
|
|
JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED
|
|
|
|
// you mustn't set your own cached image object when attaching a GL context!
|
|
jassert (get (component) == this);
|
|
|
|
if (! ensureFrameBufferSize (currentAreaAndScale.area))
|
|
return;
|
|
|
|
RectangleList<int> invalid (currentAreaAndScale.area);
|
|
invalid.subtract (validArea);
|
|
validArea = currentAreaAndScale.area;
|
|
|
|
if (! invalid.isEmpty())
|
|
{
|
|
clearRegionInFrameBuffer (invalid);
|
|
|
|
{
|
|
std::unique_ptr<LowLevelGraphicsContext> g (createOpenGLGraphicsContext (context, cachedImageFrameBuffer));
|
|
g->clipToRectangleList (invalid);
|
|
g->addTransform (transform);
|
|
|
|
paintOwner (*g);
|
|
JUCE_CHECK_OPENGL_ERROR
|
|
}
|
|
|
|
if (! context.isActive())
|
|
context.makeActive();
|
|
}
|
|
|
|
JUCE_CHECK_OPENGL_ERROR
|
|
}
|
|
|
|
void drawComponentBuffer()
|
|
{
|
|
if (contextRequiresTexture2DEnableDisable())
|
|
glEnable (GL_TEXTURE_2D);
|
|
|
|
#if JUCE_WINDOWS
|
|
// some stupidly old drivers are missing this function, so try to at least avoid a crash here,
|
|
// but if you hit this assertion you may want to have your own version check before using the
|
|
// component rendering stuff on such old drivers.
|
|
jassert (context.extensions.glActiveTexture != nullptr);
|
|
if (context.extensions.glActiveTexture != nullptr)
|
|
#endif
|
|
{
|
|
context.extensions.glActiveTexture (GL_TEXTURE0);
|
|
}
|
|
|
|
glBindTexture (GL_TEXTURE_2D, cachedImageFrameBuffer.getTextureID());
|
|
bindVertexArray();
|
|
|
|
const Rectangle<int> cacheBounds (cachedImageFrameBuffer.getWidth(), cachedImageFrameBuffer.getHeight());
|
|
context.copyTexture (cacheBounds, cacheBounds, cacheBounds.getWidth(), cacheBounds.getHeight(), false);
|
|
glBindTexture (GL_TEXTURE_2D, 0);
|
|
JUCE_CHECK_OPENGL_ERROR
|
|
}
|
|
|
|
void paintOwner (LowLevelGraphicsContext& llgc)
|
|
{
|
|
Graphics g (llgc);
|
|
|
|
#if JUCE_ENABLE_REPAINT_DEBUGGING
|
|
#ifdef JUCE_IS_REPAINT_DEBUGGING_ACTIVE
|
|
if (JUCE_IS_REPAINT_DEBUGGING_ACTIVE)
|
|
#endif
|
|
{
|
|
g.saveState();
|
|
}
|
|
#endif
|
|
|
|
JUCE_TRY
|
|
{
|
|
component.paintEntireComponent (g, false);
|
|
}
|
|
JUCE_CATCH_EXCEPTION
|
|
|
|
#if JUCE_ENABLE_REPAINT_DEBUGGING
|
|
#ifdef JUCE_IS_REPAINT_DEBUGGING_ACTIVE
|
|
if (JUCE_IS_REPAINT_DEBUGGING_ACTIVE)
|
|
#endif
|
|
{
|
|
// enabling this code will fill all areas that get repainted with a colour overlay, to show
|
|
// clearly when things are being repainted.
|
|
g.restoreState();
|
|
|
|
static Random rng;
|
|
g.fillAll (Colour ((uint8) rng.nextInt (255),
|
|
(uint8) rng.nextInt (255),
|
|
(uint8) rng.nextInt (255),
|
|
(uint8) 0x50));
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void handleResize()
|
|
{
|
|
updateViewportSize();
|
|
|
|
#if JUCE_MAC
|
|
if (hasInitialised)
|
|
{
|
|
[nativeContext->view update];
|
|
renderFrame();
|
|
}
|
|
#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;
|
|
}
|
|
|
|
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()
|
|
{
|
|
// On android, this can get called twice, so drop any previous state.
|
|
associatedObjectNames.clear();
|
|
associatedObjects.clear();
|
|
cachedImageFrameBuffer.release();
|
|
|
|
context.makeActive();
|
|
|
|
if (! nativeContext->initialiseOnRenderThread (context))
|
|
return false;
|
|
|
|
#if JUCE_ANDROID
|
|
// On android the context may be created in initialiseOnRenderThread
|
|
// and we therefore need to call makeActive again
|
|
context.makeActive();
|
|
#endif
|
|
|
|
gl::loadFunctions();
|
|
|
|
if (shouldUseCustomVAO())
|
|
{
|
|
context.extensions.glGenVertexArrays (1, &vertexArrayObject);
|
|
bindVertexArray();
|
|
}
|
|
|
|
#if JUCE_DEBUG
|
|
if (getOpenGLVersion() >= Version { 4, 3 } && glDebugMessageCallback != nullptr)
|
|
{
|
|
glEnable (GL_DEBUG_OUTPUT);
|
|
glDebugMessageCallback ([] (GLenum, GLenum, GLuint, GLenum, GLsizei, const GLchar* message, const void*)
|
|
{
|
|
// This may reiterate issues that are also flagged by JUCE_CHECK_OPENGL_ERROR.
|
|
// The advantage of this callback is that it will catch *all* errors, even if we
|
|
// forget to check manually.
|
|
DBG ("OpenGL DBG message: " << message);
|
|
jassertfalse;
|
|
}, nullptr);
|
|
}
|
|
#endif
|
|
|
|
const auto currentViewportArea = areaAndScale.get().area;
|
|
glViewport (0, 0, currentViewportArea.getWidth(), currentViewportArea.getHeight());
|
|
|
|
nativeContext->setSwapInterval (1);
|
|
|
|
#if ! JUCE_OPENGL_ES
|
|
JUCE_CHECK_OPENGL_ERROR
|
|
shadersAvailable = OpenGLShaderProgram::getLanguageVersion() > 0;
|
|
clearGLError();
|
|
#endif
|
|
|
|
textureNpotSupported = contextHasTextureNpotFeature();
|
|
|
|
if (context.renderer != nullptr)
|
|
context.renderer->newOpenGLContextCreated();
|
|
|
|
#if JUCE_MAC
|
|
jassert (cvDisplayLinkWrapper != nullptr);
|
|
nativeContext->setNominalVideoRefreshPeriodS (cvDisplayLinkWrapper->getNominalVideoRefreshPeriodS());
|
|
#endif
|
|
|
|
return true;
|
|
}
|
|
|
|
void shutdownOnThread()
|
|
{
|
|
if (context.renderer != nullptr)
|
|
context.renderer->openGLContextClosing();
|
|
|
|
if (vertexArrayObject != 0)
|
|
context.extensions.glDeleteVertexArrays (1, &vertexArrayObject);
|
|
|
|
associatedObjectNames.clear();
|
|
associatedObjects.clear();
|
|
cachedImageFrameBuffer.release();
|
|
nativeContext->shutdownOnRenderThread();
|
|
}
|
|
|
|
/* Returns true if the context requires a non-zero vertex array object (VAO) to be bound.
|
|
|
|
If the context is a compatibility context, we can just pretend that VAOs don't exist,
|
|
and use the default VAO all the time instead. This provides a more consistent experience
|
|
in user code, which might make calls (like glVertexPointer()) that only work when VAO 0 is
|
|
bound in OpenGL 3.2+.
|
|
*/
|
|
bool shouldUseCustomVAO() const
|
|
{
|
|
#if JUCE_OPENGL_ES
|
|
return false;
|
|
#else
|
|
clearGLError();
|
|
GLint mask = 0;
|
|
glGetIntegerv (GL_CONTEXT_PROFILE_MASK, &mask);
|
|
|
|
// The context isn't aware of the profile mask, so it pre-dates the core profile
|
|
if (glGetError() == GL_INVALID_ENUM)
|
|
return false;
|
|
|
|
// Also assumes a compatibility profile if the mask is completely empty for some reason
|
|
return (mask & (GLint) GL_CONTEXT_CORE_PROFILE_BIT) != 0;
|
|
#endif
|
|
}
|
|
|
|
//==============================================================================
|
|
struct BlockingWorker : public OpenGLContext::AsyncWorker
|
|
{
|
|
BlockingWorker (OpenGLContext::AsyncWorker::Ptr && workerToUse)
|
|
: originalWorker (std::move (workerToUse))
|
|
{}
|
|
|
|
void operator() (OpenGLContext& calleeContext)
|
|
{
|
|
if (originalWorker != nullptr)
|
|
(*originalWorker) (calleeContext);
|
|
|
|
finishedSignal.signal();
|
|
}
|
|
|
|
void block() noexcept { finishedSignal.wait(); }
|
|
|
|
OpenGLContext::AsyncWorker::Ptr originalWorker;
|
|
WaitableEvent finishedSignal;
|
|
};
|
|
|
|
bool doWorkWhileWaitingForLock (bool contextIsAlreadyActive)
|
|
{
|
|
bool contextActivated = false;
|
|
|
|
for (OpenGLContext::AsyncWorker::Ptr work = workQueue.removeAndReturn (0);
|
|
work != nullptr && (! shouldExit()); work = workQueue.removeAndReturn (0))
|
|
{
|
|
if ((! contextActivated) && (! contextIsAlreadyActive))
|
|
{
|
|
if (! context.makeActive())
|
|
break;
|
|
|
|
contextActivated = true;
|
|
}
|
|
|
|
NativeContext::Locker locker (*nativeContext);
|
|
|
|
(*work) (context);
|
|
clearGLError();
|
|
}
|
|
|
|
if (contextActivated)
|
|
OpenGLContext::deactivateCurrentContext();
|
|
|
|
return shouldExit();
|
|
}
|
|
|
|
void execute (OpenGLContext::AsyncWorker::Ptr workerToUse, bool shouldBlock, bool calledFromDestructor = false)
|
|
{
|
|
if (calledFromDestructor || ! destroying)
|
|
{
|
|
if (shouldBlock)
|
|
{
|
|
auto blocker = new BlockingWorker (std::move (workerToUse));
|
|
OpenGLContext::AsyncWorker::Ptr worker (*blocker);
|
|
workQueue.add (worker);
|
|
|
|
messageManagerLock.abort();
|
|
context.triggerRepaint();
|
|
|
|
blocker->block();
|
|
}
|
|
else
|
|
{
|
|
workQueue.add (std::move (workerToUse));
|
|
|
|
messageManagerLock.abort();
|
|
context.triggerRepaint();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
jassertfalse; // you called execute AFTER you detached your OpenGLContext
|
|
}
|
|
}
|
|
|
|
//==============================================================================
|
|
static CachedImage* get (Component& c) noexcept
|
|
{
|
|
return dynamic_cast<CachedImage*> (c.getCachedComponentImage());
|
|
}
|
|
|
|
//==============================================================================
|
|
friend class NativeContext;
|
|
std::unique_ptr<NativeContext> nativeContext;
|
|
|
|
OpenGLContext& context;
|
|
Component& component;
|
|
|
|
OpenGLFrameBuffer cachedImageFrameBuffer;
|
|
RectangleList<int> validArea;
|
|
Rectangle<int> lastScreenBounds;
|
|
AffineTransform transform;
|
|
GLuint vertexArrayObject = 0;
|
|
LockedAreaAndScale areaAndScale;
|
|
|
|
StringArray associatedObjectNames;
|
|
ReferenceCountedArray<ReferenceCountedObject> associatedObjects;
|
|
|
|
WaitableEvent canPaintNowFlag, finishedPaintingFlag, repaintEvent { true };
|
|
#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;
|
|
|
|
#if JUCE_MAC
|
|
NSView* getCurrentView() const
|
|
{
|
|
if (auto* peer = component.getPeer())
|
|
return static_cast<NSView*> (peer->getNativeHandle());
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
NSScreen* getCurrentScreen() const
|
|
{
|
|
JUCE_ASSERT_MESSAGE_THREAD;
|
|
|
|
if (auto* view = getCurrentView())
|
|
if (auto* window = [view window])
|
|
return [window screen];
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
struct CVDisplayLinkWrapper
|
|
{
|
|
explicit CVDisplayLinkWrapper (CachedImage& cachedImageIn)
|
|
: cachedImage (cachedImageIn),
|
|
continuousRepaint (cachedImageIn.context.continuousRepaint.load())
|
|
{
|
|
CVDisplayLinkCreateWithActiveCGDisplays (&displayLink);
|
|
CVDisplayLinkSetOutputCallback (displayLink, &displayLinkCallback, this);
|
|
CVDisplayLinkStart (displayLink);
|
|
}
|
|
|
|
double getNominalVideoRefreshPeriodS() const
|
|
{
|
|
const auto nominalVideoRefreshPeriod = CVDisplayLinkGetNominalOutputVideoRefreshPeriod (displayLink);
|
|
|
|
if ((nominalVideoRefreshPeriod.flags & kCVTimeIsIndefinite) == 0)
|
|
return (double) nominalVideoRefreshPeriod.timeValue / (double) nominalVideoRefreshPeriod.timeScale;
|
|
|
|
return 0.0;
|
|
}
|
|
|
|
/* Returns true if updated, or false otherwise. */
|
|
bool updateActiveDisplay()
|
|
{
|
|
auto* oldScreen = std::exchange (currentScreen, cachedImage.getCurrentScreen());
|
|
|
|
if (oldScreen == currentScreen)
|
|
return false;
|
|
|
|
for (NSScreen* screen in [NSScreen screens])
|
|
if (screen == currentScreen)
|
|
if (NSNumber* number = [[screen deviceDescription] objectForKey: @"NSScreenNumber"])
|
|
CVDisplayLinkSetCurrentCGDisplay (displayLink, [number unsignedIntValue]);
|
|
|
|
return true;
|
|
}
|
|
|
|
~CVDisplayLinkWrapper()
|
|
{
|
|
CVDisplayLinkStop (displayLink);
|
|
CVDisplayLinkRelease (displayLink);
|
|
}
|
|
|
|
static CVReturn displayLinkCallback (CVDisplayLinkRef, const CVTimeStamp*, const CVTimeStamp*,
|
|
CVOptionFlags, CVOptionFlags*, void* displayLinkContext)
|
|
{
|
|
auto* self = reinterpret_cast<CVDisplayLinkWrapper*> (displayLinkContext);
|
|
|
|
if (self->continuousRepaint)
|
|
self->cachedImage.repaintEvent.signal();
|
|
|
|
return kCVReturnSuccess;
|
|
}
|
|
|
|
CachedImage& cachedImage;
|
|
const bool continuousRepaint;
|
|
CVDisplayLinkRef displayLink;
|
|
NSScreen* currentScreen = nullptr;
|
|
};
|
|
|
|
std::unique_ptr<CVDisplayLinkWrapper> cvDisplayLinkWrapper;
|
|
#endif
|
|
|
|
std::unique_ptr<ThreadPool> renderThread;
|
|
ReferenceCountedArray<OpenGLContext::AsyncWorker, CriticalSection> workQueue;
|
|
MessageManager::Lock messageManagerLock;
|
|
|
|
#if JUCE_IOS
|
|
iOSBackgroundProcessCheck backgroundProcessCheck;
|
|
#endif
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CachedImage)
|
|
};
|
|
|
|
//==============================================================================
|
|
class OpenGLContext::Attachment : public ComponentMovementWatcher,
|
|
private Timer
|
|
{
|
|
public:
|
|
Attachment (OpenGLContext& c, Component& comp)
|
|
: ComponentMovementWatcher (&comp), context (c)
|
|
{
|
|
if (canBeAttached (comp))
|
|
attach();
|
|
}
|
|
|
|
~Attachment() override
|
|
{
|
|
detach();
|
|
}
|
|
|
|
void detach()
|
|
{
|
|
auto& comp = *getComponent();
|
|
stop();
|
|
comp.setCachedComponentImage (nullptr);
|
|
context.nativeContext = nullptr;
|
|
}
|
|
|
|
void componentMovedOrResized (bool /*wasMoved*/, bool /*wasResized*/) override
|
|
{
|
|
auto& comp = *getComponent();
|
|
|
|
if (isAttached (comp) != canBeAttached (comp))
|
|
componentVisibilityChanged();
|
|
|
|
if (comp.getWidth() > 0 && comp.getHeight() > 0
|
|
&& context.nativeContext != nullptr)
|
|
{
|
|
if (auto* c = CachedImage::get (comp))
|
|
c->handleResize();
|
|
|
|
if (auto* peer = comp.getTopLevelComponent()->getPeer())
|
|
context.nativeContext->updateWindowPosition (peer->getAreaCoveredBy (comp));
|
|
}
|
|
}
|
|
|
|
using ComponentMovementWatcher::componentMovedOrResized;
|
|
|
|
void componentPeerChanged() override
|
|
{
|
|
detach();
|
|
componentVisibilityChanged();
|
|
}
|
|
|
|
void componentVisibilityChanged() override
|
|
{
|
|
auto& comp = *getComponent();
|
|
|
|
if (canBeAttached (comp))
|
|
{
|
|
if (isAttached (comp))
|
|
comp.repaint(); // (needed when windows are un-minimised)
|
|
else
|
|
attach();
|
|
}
|
|
else
|
|
{
|
|
detach();
|
|
}
|
|
}
|
|
|
|
using ComponentMovementWatcher::componentVisibilityChanged;
|
|
|
|
#if JUCE_DEBUG || JUCE_LOG_ASSERTIONS
|
|
void componentBeingDeleted (Component& c) override
|
|
{
|
|
/* You must call detach() or delete your OpenGLContext to remove it
|
|
from a component BEFORE deleting the component that it is using!
|
|
*/
|
|
jassertfalse;
|
|
|
|
ComponentMovementWatcher::componentBeingDeleted (c);
|
|
}
|
|
#endif
|
|
|
|
void update()
|
|
{
|
|
auto& comp = *getComponent();
|
|
|
|
if (canBeAttached (comp))
|
|
start();
|
|
else
|
|
stop();
|
|
}
|
|
|
|
private:
|
|
OpenGLContext& context;
|
|
|
|
bool canBeAttached (const Component& comp) noexcept
|
|
{
|
|
return (! context.overrideCanAttach) && comp.getWidth() > 0 && comp.getHeight() > 0 && isShowingOrMinimised (comp);
|
|
}
|
|
|
|
static bool isShowingOrMinimised (const Component& c)
|
|
{
|
|
if (! c.isVisible())
|
|
return false;
|
|
|
|
if (auto* p = c.getParentComponent())
|
|
return isShowingOrMinimised (*p);
|
|
|
|
return c.getPeer() != nullptr;
|
|
}
|
|
|
|
static bool isAttached (const Component& comp) noexcept
|
|
{
|
|
return comp.getCachedComponentImage() != nullptr;
|
|
}
|
|
|
|
void attach()
|
|
{
|
|
auto& comp = *getComponent();
|
|
auto* newCachedImage = new CachedImage (context, comp,
|
|
context.openGLPixelFormat,
|
|
context.contextToShareWith);
|
|
comp.setCachedComponentImage (newCachedImage);
|
|
|
|
start();
|
|
}
|
|
|
|
void stop()
|
|
{
|
|
stopTimer();
|
|
|
|
auto& comp = *getComponent();
|
|
|
|
#if JUCE_MAC
|
|
[[(NSView*) comp.getWindowHandle() window] disableScreenUpdatesUntilFlush];
|
|
#endif
|
|
|
|
if (auto* oldCachedImage = CachedImage::get (comp))
|
|
oldCachedImage->stop(); // (must stop this before detaching it from the component)
|
|
}
|
|
|
|
void start()
|
|
{
|
|
auto& comp = *getComponent();
|
|
|
|
if (auto* cachedImage = CachedImage::get (comp))
|
|
{
|
|
cachedImage->start(); // (must wait until this is attached before starting its thread)
|
|
cachedImage->updateViewportSize();
|
|
|
|
startTimer (400);
|
|
}
|
|
}
|
|
|
|
void timerCallback() override
|
|
{
|
|
if (auto* cachedImage = CachedImage::get (*getComponent()))
|
|
cachedImage->checkViewportBounds();
|
|
}
|
|
};
|
|
|
|
//==============================================================================
|
|
OpenGLContext::OpenGLContext()
|
|
{
|
|
}
|
|
|
|
OpenGLContext::~OpenGLContext()
|
|
{
|
|
detach();
|
|
}
|
|
|
|
void OpenGLContext::setRenderer (OpenGLRenderer* rendererToUse) noexcept
|
|
{
|
|
// This method must not be called when the context has already been attached!
|
|
// Call it before attaching your context, or use detach() first, before calling this!
|
|
jassert (nativeContext == nullptr);
|
|
|
|
renderer = rendererToUse;
|
|
}
|
|
|
|
void OpenGLContext::setComponentPaintingEnabled (bool shouldPaintComponent) noexcept
|
|
{
|
|
// This method must not be called when the context has already been attached!
|
|
// Call it before attaching your context, or use detach() first, before calling this!
|
|
jassert (nativeContext == nullptr);
|
|
|
|
renderComponents = shouldPaintComponent;
|
|
}
|
|
|
|
void OpenGLContext::setContinuousRepainting (bool shouldContinuouslyRepaint) noexcept
|
|
{
|
|
continuousRepaint = shouldContinuouslyRepaint;
|
|
|
|
#if JUCE_MAC
|
|
if (auto* component = getTargetComponent())
|
|
{
|
|
detach();
|
|
attachment.reset (new Attachment (*this, *component));
|
|
}
|
|
#endif
|
|
|
|
triggerRepaint();
|
|
}
|
|
|
|
void OpenGLContext::setPixelFormat (const OpenGLPixelFormat& preferredPixelFormat) noexcept
|
|
{
|
|
// This method must not be called when the context has already been attached!
|
|
// Call it before attaching your context, or use detach() first, before calling this!
|
|
jassert (nativeContext == nullptr);
|
|
|
|
openGLPixelFormat = preferredPixelFormat;
|
|
}
|
|
|
|
void OpenGLContext::setTextureMagnificationFilter (OpenGLContext::TextureMagnificationFilter magFilterMode) noexcept
|
|
{
|
|
texMagFilter = magFilterMode;
|
|
}
|
|
|
|
void OpenGLContext::setNativeSharedContext (void* nativeContextToShareWith) noexcept
|
|
{
|
|
// This method must not be called when the context has already been attached!
|
|
// Call it before attaching your context, or use detach() first, before calling this!
|
|
jassert (nativeContext == nullptr);
|
|
|
|
contextToShareWith = nativeContextToShareWith;
|
|
}
|
|
|
|
void OpenGLContext::setMultisamplingEnabled (bool b) noexcept
|
|
{
|
|
// This method must not be called when the context has already been attached!
|
|
// Call it before attaching your context, or use detach() first, before calling this!
|
|
jassert (nativeContext == nullptr);
|
|
|
|
useMultisampling = b;
|
|
}
|
|
|
|
void OpenGLContext::setOpenGLVersionRequired (OpenGLVersion v) noexcept
|
|
{
|
|
versionRequired = v;
|
|
}
|
|
|
|
void OpenGLContext::attachTo (Component& component)
|
|
{
|
|
component.repaint();
|
|
|
|
if (getTargetComponent() != &component)
|
|
{
|
|
detach();
|
|
attachment.reset (new Attachment (*this, component));
|
|
}
|
|
}
|
|
|
|
void OpenGLContext::detach()
|
|
{
|
|
if (auto* a = attachment.get())
|
|
{
|
|
a->detach(); // must detach before nulling our pointer
|
|
attachment.reset();
|
|
}
|
|
|
|
nativeContext = nullptr;
|
|
}
|
|
|
|
bool OpenGLContext::isAttached() const noexcept
|
|
{
|
|
return nativeContext != nullptr;
|
|
}
|
|
|
|
Component* OpenGLContext::getTargetComponent() const noexcept
|
|
{
|
|
return attachment != nullptr ? attachment->getComponent() : nullptr;
|
|
}
|
|
|
|
OpenGLContext* OpenGLContext::getContextAttachedTo (Component& c) noexcept
|
|
{
|
|
if (auto* ci = CachedImage::get (c))
|
|
return &(ci->context);
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
static ThreadLocalValue<OpenGLContext*> currentThreadActiveContext;
|
|
|
|
OpenGLContext* OpenGLContext::getCurrentContext()
|
|
{
|
|
return currentThreadActiveContext.get();
|
|
}
|
|
|
|
bool OpenGLContext::makeActive() const noexcept
|
|
{
|
|
auto& current = currentThreadActiveContext.get();
|
|
|
|
if (nativeContext != nullptr && nativeContext->makeActive())
|
|
{
|
|
current = const_cast<OpenGLContext*> (this);
|
|
return true;
|
|
}
|
|
|
|
current = nullptr;
|
|
return false;
|
|
}
|
|
|
|
bool OpenGLContext::isActive() const noexcept
|
|
{
|
|
return nativeContext != nullptr && nativeContext->isActive();
|
|
}
|
|
|
|
void OpenGLContext::deactivateCurrentContext()
|
|
{
|
|
NativeContext::deactivateCurrentContext();
|
|
currentThreadActiveContext.get() = nullptr;
|
|
}
|
|
|
|
void OpenGLContext::triggerRepaint()
|
|
{
|
|
if (auto* cachedImage = getCachedImage())
|
|
cachedImage->triggerRepaint();
|
|
}
|
|
|
|
void OpenGLContext::swapBuffers()
|
|
{
|
|
if (nativeContext != nullptr)
|
|
nativeContext->swapBuffers();
|
|
}
|
|
|
|
unsigned int OpenGLContext::getFrameBufferID() const noexcept
|
|
{
|
|
return nativeContext != nullptr ? nativeContext->getFrameBufferID() : 0;
|
|
}
|
|
|
|
bool OpenGLContext::setSwapInterval (int numFramesPerSwap)
|
|
{
|
|
return nativeContext != nullptr && nativeContext->setSwapInterval (numFramesPerSwap);
|
|
}
|
|
|
|
int OpenGLContext::getSwapInterval() const
|
|
{
|
|
return nativeContext != nullptr ? nativeContext->getSwapInterval() : 0;
|
|
}
|
|
|
|
void* OpenGLContext::getRawContext() const noexcept
|
|
{
|
|
return nativeContext != nullptr ? nativeContext->getRawContext() : nullptr;
|
|
}
|
|
|
|
OpenGLContext::CachedImage* OpenGLContext::getCachedImage() const noexcept
|
|
{
|
|
if (auto* comp = getTargetComponent())
|
|
return CachedImage::get (*comp);
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
bool OpenGLContext::areShadersAvailable() const
|
|
{
|
|
auto* c = getCachedImage();
|
|
return c != nullptr && c->shadersAvailable;
|
|
}
|
|
|
|
bool OpenGLContext::isTextureNpotSupported() const
|
|
{
|
|
auto* c = getCachedImage();
|
|
return c != nullptr && c->textureNpotSupported;
|
|
}
|
|
|
|
ReferenceCountedObject* OpenGLContext::getAssociatedObject (const char* name) const
|
|
{
|
|
jassert (name != nullptr);
|
|
|
|
auto* c = getCachedImage();
|
|
|
|
// This method must only be called from an openGL rendering callback.
|
|
jassert (c != nullptr && nativeContext != nullptr);
|
|
jassert (getCurrentContext() != nullptr);
|
|
|
|
auto index = c->associatedObjectNames.indexOf (name);
|
|
return index >= 0 ? c->associatedObjects.getUnchecked (index).get() : nullptr;
|
|
}
|
|
|
|
void OpenGLContext::setAssociatedObject (const char* name, ReferenceCountedObject* newObject)
|
|
{
|
|
jassert (name != nullptr);
|
|
|
|
if (auto* c = getCachedImage())
|
|
{
|
|
// This method must only be called from an openGL rendering callback.
|
|
jassert (nativeContext != nullptr);
|
|
jassert (getCurrentContext() != nullptr);
|
|
|
|
const int index = c->associatedObjectNames.indexOf (name);
|
|
|
|
if (index >= 0)
|
|
{
|
|
if (newObject != nullptr)
|
|
{
|
|
c->associatedObjects.set (index, newObject);
|
|
}
|
|
else
|
|
{
|
|
c->associatedObjectNames.remove (index);
|
|
c->associatedObjects.remove (index);
|
|
}
|
|
}
|
|
else if (newObject != nullptr)
|
|
{
|
|
c->associatedObjectNames.add (name);
|
|
c->associatedObjects.add (newObject);
|
|
}
|
|
}
|
|
}
|
|
|
|
void OpenGLContext::setImageCacheSize (size_t newSize) noexcept { imageCacheMaxSize = newSize; }
|
|
size_t OpenGLContext::getImageCacheSize() const noexcept { return imageCacheMaxSize; }
|
|
|
|
void OpenGLContext::execute (OpenGLContext::AsyncWorker::Ptr workerToUse, bool shouldBlock)
|
|
{
|
|
if (auto* c = getCachedImage())
|
|
c->execute (std::move (workerToUse), shouldBlock);
|
|
else
|
|
jassertfalse; // You must have attached the context to a component
|
|
}
|
|
|
|
//==============================================================================
|
|
struct DepthTestDisabler
|
|
{
|
|
DepthTestDisabler() noexcept
|
|
{
|
|
glGetBooleanv (GL_DEPTH_TEST, &wasEnabled);
|
|
|
|
if (wasEnabled)
|
|
glDisable (GL_DEPTH_TEST);
|
|
}
|
|
|
|
~DepthTestDisabler() noexcept
|
|
{
|
|
if (wasEnabled)
|
|
glEnable (GL_DEPTH_TEST);
|
|
}
|
|
|
|
GLboolean wasEnabled;
|
|
};
|
|
|
|
//==============================================================================
|
|
void OpenGLContext::copyTexture (const Rectangle<int>& targetClipArea,
|
|
const Rectangle<int>& anchorPosAndTextureSize,
|
|
const int contextWidth, const int contextHeight,
|
|
bool flippedVertically)
|
|
{
|
|
if (contextWidth <= 0 || contextHeight <= 0)
|
|
return;
|
|
|
|
JUCE_CHECK_OPENGL_ERROR
|
|
glBlendFunc (GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
|
|
glEnable (GL_BLEND);
|
|
|
|
DepthTestDisabler depthDisabler;
|
|
|
|
if (areShadersAvailable())
|
|
{
|
|
struct OverlayShaderProgram : public ReferenceCountedObject
|
|
{
|
|
OverlayShaderProgram (OpenGLContext& context)
|
|
: program (context), builder (program), params (program)
|
|
{}
|
|
|
|
static const OverlayShaderProgram& select (OpenGLContext& context)
|
|
{
|
|
static const char programValueID[] = "juceGLComponentOverlayShader";
|
|
OverlayShaderProgram* program = static_cast<OverlayShaderProgram*> (context.getAssociatedObject (programValueID));
|
|
|
|
if (program == nullptr)
|
|
{
|
|
program = new OverlayShaderProgram (context);
|
|
context.setAssociatedObject (programValueID, program);
|
|
}
|
|
|
|
program->program.use();
|
|
return *program;
|
|
}
|
|
|
|
struct ProgramBuilder
|
|
{
|
|
ProgramBuilder (OpenGLShaderProgram& prog)
|
|
{
|
|
prog.addVertexShader (OpenGLHelpers::translateVertexShaderToV3 (
|
|
"attribute " JUCE_HIGHP " vec2 position;"
|
|
"uniform " JUCE_HIGHP " vec2 screenSize;"
|
|
"uniform " JUCE_HIGHP " float textureBounds[4];"
|
|
"uniform " JUCE_HIGHP " vec2 vOffsetAndScale;"
|
|
"varying " JUCE_HIGHP " vec2 texturePos;"
|
|
"void main()"
|
|
"{"
|
|
JUCE_HIGHP " vec2 scaled = position / (0.5 * screenSize.xy);"
|
|
"gl_Position = vec4 (scaled.x - 1.0, 1.0 - scaled.y, 0, 1.0);"
|
|
"texturePos = (position - vec2 (textureBounds[0], textureBounds[1])) / vec2 (textureBounds[2], textureBounds[3]);"
|
|
"texturePos = vec2 (texturePos.x, vOffsetAndScale.x + vOffsetAndScale.y * texturePos.y);"
|
|
"}"));
|
|
|
|
prog.addFragmentShader (OpenGLHelpers::translateFragmentShaderToV3 (
|
|
"uniform sampler2D imageTexture;"
|
|
"varying " JUCE_HIGHP " vec2 texturePos;"
|
|
"void main()"
|
|
"{"
|
|
"gl_FragColor = texture2D (imageTexture, texturePos);"
|
|
"}"));
|
|
|
|
prog.link();
|
|
}
|
|
};
|
|
|
|
struct Params
|
|
{
|
|
Params (OpenGLShaderProgram& prog)
|
|
: positionAttribute (prog, "position"),
|
|
screenSize (prog, "screenSize"),
|
|
imageTexture (prog, "imageTexture"),
|
|
textureBounds (prog, "textureBounds"),
|
|
vOffsetAndScale (prog, "vOffsetAndScale")
|
|
{}
|
|
|
|
void set (const float targetWidth, const float targetHeight, const Rectangle<float>& bounds, bool flipVertically) const
|
|
{
|
|
const GLfloat m[] = { bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight() };
|
|
textureBounds.set (m, 4);
|
|
imageTexture.set (0);
|
|
screenSize.set (targetWidth, targetHeight);
|
|
|
|
vOffsetAndScale.set (flipVertically ? 0.0f : 1.0f,
|
|
flipVertically ? 1.0f : -1.0f);
|
|
}
|
|
|
|
OpenGLShaderProgram::Attribute positionAttribute;
|
|
OpenGLShaderProgram::Uniform screenSize, imageTexture, textureBounds, vOffsetAndScale;
|
|
};
|
|
|
|
OpenGLShaderProgram program;
|
|
ProgramBuilder builder;
|
|
Params params;
|
|
};
|
|
|
|
auto left = (GLshort) targetClipArea.getX();
|
|
auto top = (GLshort) targetClipArea.getY();
|
|
auto right = (GLshort) targetClipArea.getRight();
|
|
auto bottom = (GLshort) targetClipArea.getBottom();
|
|
const GLshort vertices[] = { left, bottom, right, bottom, left, top, right, top };
|
|
|
|
auto& program = OverlayShaderProgram::select (*this);
|
|
program.params.set ((float) contextWidth, (float) contextHeight, anchorPosAndTextureSize.toFloat(), flippedVertically);
|
|
|
|
GLuint vertexBuffer = 0;
|
|
extensions.glGenBuffers (1, &vertexBuffer);
|
|
extensions.glBindBuffer (GL_ARRAY_BUFFER, vertexBuffer);
|
|
extensions.glBufferData (GL_ARRAY_BUFFER, sizeof (vertices), vertices, GL_STATIC_DRAW);
|
|
|
|
auto index = (GLuint) program.params.positionAttribute.attributeID;
|
|
extensions.glVertexAttribPointer (index, 2, GL_SHORT, GL_FALSE, 4, nullptr);
|
|
extensions.glEnableVertexAttribArray (index);
|
|
JUCE_CHECK_OPENGL_ERROR
|
|
|
|
if (extensions.glCheckFramebufferStatus (GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE)
|
|
{
|
|
glDrawArrays (GL_TRIANGLE_STRIP, 0, 4);
|
|
|
|
extensions.glBindBuffer (GL_ARRAY_BUFFER, 0);
|
|
extensions.glUseProgram (0);
|
|
extensions.glDisableVertexAttribArray (index);
|
|
extensions.glDeleteBuffers (1, &vertexBuffer);
|
|
}
|
|
else
|
|
{
|
|
clearGLError();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
jassert (attachment == nullptr); // Running on an old graphics card!
|
|
}
|
|
|
|
JUCE_CHECK_OPENGL_ERROR
|
|
}
|
|
|
|
#if JUCE_ANDROID
|
|
EGLDisplay OpenGLContext::NativeContext::display = EGL_NO_DISPLAY;
|
|
EGLDisplay OpenGLContext::NativeContext::config;
|
|
|
|
void OpenGLContext::NativeContext::surfaceCreated (LocalRef<jobject> holder)
|
|
{
|
|
ignoreUnused (holder);
|
|
|
|
if (auto* cachedImage = CachedImage::get (component))
|
|
{
|
|
if (auto* pool = cachedImage->renderThread.get())
|
|
{
|
|
if (! pool->contains (cachedImage))
|
|
{
|
|
cachedImage->resume();
|
|
cachedImage->context.triggerRepaint();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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))
|
|
{
|
|
cachedImage->pause();
|
|
|
|
if (auto* threadPool = cachedImage->renderThread.get())
|
|
threadPool->waitForJobToFinish (cachedImage, -1);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
} // namespace juce
|