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

OpenGLContext: Share CVDisplayLinks with NSViewComponentPeer

This commit is contained in:
reuk 2022-08-24 21:54:34 +01:00
parent 20584cf201
commit 322aa64459
5 changed files with 387 additions and 336 deletions

View file

@ -290,6 +290,7 @@ namespace juce
#else
#include "native/accessibility/juce_mac_Accessibility.mm"
#include "native/juce_mac_PerScreenDisplayLinks.h"
#include "native/juce_mac_NSViewComponentPeer.mm"
#include "native/juce_mac_Windowing.mm"
#include "native/juce_mac_MainMenu.mm"

View file

@ -122,216 +122,6 @@ static constexpr int translateVirtualToAsciiKeyCode (int keyCode) noexcept
constexpr int extendedKeyModifier = 0x30000;
//==============================================================================
struct DisplayLinkDestructor
{
void operator() (CVDisplayLinkRef ptr) const
{
if (ptr != nullptr)
CVDisplayLinkRelease (ptr);
}
};
/* Manages the lifetime of a CVDisplayLinkRef for a single display, and automatically starts and
stops it.
*/
class ScopedDisplayLink
{
public:
ScopedDisplayLink (NSScreen* screen, std::function<void()> callback)
: ScopedDisplayLink ((CGDirectDisplayID) [[screen.deviceDescription objectForKey: @"NSScreenNumber"] unsignedIntegerValue],
std::move (callback))
{}
ScopedDisplayLink (CGDirectDisplayID display, std::function<void()> onCallbackIn)
: link ([display]
{
CVDisplayLinkRef ptr = nullptr;
const auto result = CVDisplayLinkCreateWithCGDisplay (display, &ptr);
jassertquiet (result == kCVReturnSuccess);
jassertquiet (ptr != nullptr);
return ptr;
}()),
onCallback (std::move (onCallbackIn))
{
const auto callback = [] (CVDisplayLinkRef,
const CVTimeStamp*,
const CVTimeStamp*,
CVOptionFlags,
CVOptionFlags*,
void* context) -> int
{
static_cast<const ScopedDisplayLink*> (context)->onCallback();
return kCVReturnSuccess;
};
const auto callbackResult = CVDisplayLinkSetOutputCallback (link.get(), callback, this);
jassertquiet (callbackResult == kCVReturnSuccess);
const auto startResult = CVDisplayLinkStart (link.get());
jassertquiet (startResult == kCVReturnSuccess);
}
~ScopedDisplayLink()
{
CVDisplayLinkStop (link.get());
}
private:
std::unique_ptr<std::remove_pointer_t<CVDisplayLinkRef>, DisplayLinkDestructor> link;
std::function<void()> onCallback;
};
struct DispatchSourceDestructor
{
void operator() (dispatch_source_t ptr) const
{
if (ptr != nullptr)
dispatch_source_cancel (ptr);
}
};
/* Holds a DisplayLinkRepaintDriver for each screen. When the screen configuration changes, the
DisplayLinkRepaintDrivers will be recreated automatically to match the new configuration.
*/
class PerScreenDisplayLinks
{
public:
PerScreenDisplayLinks()
{
refreshScreens();
}
using RefreshCallback = std::function<void()>;
using Factory = std::function<RefreshCallback (NSScreen*)>;
class Connection
{
public:
Connection() = default;
Connection (PerScreenDisplayLinks& linksIn, std::list<Factory>::const_iterator it)
: links (&linksIn), iter (it)
{}
~Connection()
{
if (links != nullptr)
{
const ScopedLock lock (links->mutex);
links->factories.erase (iter);
links->refreshScreens();
}
}
Connection (const Connection&) = delete;
Connection& operator= (const Connection&) = delete;
Connection (Connection&& other) noexcept
: links (std::exchange (other.links, nullptr)), iter (other.iter)
{}
Connection& operator= (Connection&& other) noexcept
{
Connection { std::move (other) }.swap (*this);
return *this;
}
private:
void swap (Connection& other) noexcept
{
std::swap (other.links, links);
std::swap (other.iter, iter);
}
PerScreenDisplayLinks* links = nullptr;
std::list<Factory>::const_iterator iter;
};
Connection registerFactory (Factory factory)
{
const ScopedLock lock (mutex);
factories.push_front (std::move (factory));
refreshScreens();
return { *this, factories.begin() };
}
private:
/* Screen configuration changes can be observed via the NotificationCenter, only
Objective-C objects can receive notifications.
This class will be used to create an object that will listen for a screen change
notification, and forward it to the PerScreenDisplayLinks instance that owns the
ScreenObserverObject.
*/
struct ScreenObserverObject : public ObjCClass<NSObject>
{
ScreenObserverObject() : ObjCClass ("JUCEScreenObserver_")
{
addIvar<PerScreenDisplayLinks*> ("owner");
addMethod (@selector (screensDidChange:), [] (id self, SEL, NSNotification*) { getOwner (self)->refreshScreens(); });
registerClass();
}
static PerScreenDisplayLinks* getOwner (id self)
{
return getIvar<PerScreenDisplayLinks*> (self, "owner");
}
};
void refreshScreens()
{
const ScopedLock lock (mutex);
links.clear();
for (NSScreen* screen in [NSScreen screens])
{
std::vector<RefreshCallback> callbacks;
for (auto& factory : factories)
callbacks.push_back (factory (screen));
links.emplace_back (screen, [callbacks = std::move (callbacks)]
{
for (const auto& callback : callbacks)
callback();
});
}
}
static SEL getSelector()
{
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
return @selector (screensDidChange:);
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
}
static ObjCClass<NSObject>& getClass()
{
static ScreenObserverObject observer;
return observer;
}
CriticalSection mutex;
std::list<Factory> factories;
std::list<ScopedDisplayLink> links;
NSUniquePtr<NSObject> observerObject
{
[this]
{
auto* result = getClass().createInstance();
object_setInstanceVariable (result, "owner", this);
return result;
}()
};
ScopedNotificationCenterObserver observer { observerObject.get(),
getSelector(),
NSApplicationDidChangeScreenParametersNotification,
nullptr };
};
//==============================================================================
class NSViewComponentPeer : public ComponentPeer
{
@ -1815,54 +1605,54 @@ public:
static const SEL resignKeySelector;
private:
// Note: the OpenGLContext also has a SharedResourcePointer<PerScreenDisplayLinks> to
// avoid unnecessarily duplicating display-link threads.
SharedResourcePointer<PerScreenDisplayLinks> sharedDisplayLinks;
/* Creates a function object that can be called from an arbitrary thread (probably a CVLink
thread). When called, this function object will trigger a call to setNeedsDisplayRectangles
as soon as possible on the main thread, for any peers currently on the provided NSScreen.
*/
static std::function<void()> asyncRepaintFactory (NSScreen* screen)
PerScreenDisplayLinks::Connection connection
{
using DispatchSource = std::remove_pointer_t<dispatch_source_t>;
std::unique_ptr<DispatchSource, DispatchSourceDestructor> dispatchSource;
dispatchSource.reset (dispatch_source_create (DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue()));
jassert (dispatchSource != nullptr);
const auto callback = [screen]
sharedDisplayLinks->registerFactory ([this] (auto* screen)
{
const auto num = ComponentPeer::getNumPeers();
struct DispatchSourceDestructor
{
void operator() (dispatch_source_t ptr) const
{
if (ptr != nullptr)
dispatch_source_cancel (ptr);
}
};
for (auto i = 0; i < num; ++i)
if (auto* peer = static_cast<NSViewComponentPeer*> (ComponentPeer::getPeer (i)))
if (auto* peerView = peer->view)
if (auto* peerWindow = [peerView window])
if (screen == [peerWindow screen])
peer->setNeedsDisplayRectangles();
};
using DispatchSource = std::remove_pointer_t<dispatch_source_t>;
std::unique_ptr<DispatchSource, DispatchSourceDestructor> dispatchSource;
dispatchSource.reset (dispatch_source_create (DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue()));
jassert (dispatchSource != nullptr);
dispatch_source_set_event_handler (dispatchSource.get(), ^() { callback(); });
const auto callback = [this, screen]
{
if (auto* peerView = this->view)
if (auto* peerWindow = [peerView window])
if (screen == [peerWindow screen])
this->setNeedsDisplayRectangles();
};
if (@available (macOS 10.12, *))
dispatch_activate (dispatchSource.get());
else
dispatch_resume (dispatchSource.get());
dispatch_source_set_event_handler (dispatchSource.get(), ^() { callback(); });
return [dispatch = std::shared_ptr<DispatchSource> (std::move (dispatchSource))]
{
dispatch_source_merge_data (dispatch.get(), 1);
};
}
if (@available (macOS 10.12, *))
dispatch_activate (dispatchSource.get());
else
dispatch_resume (dispatchSource.get());
struct ConcreteDisplayLinks
{
ConcreteDisplayLinks()
: connection (links.registerFactory (asyncRepaintFactory))
{}
PerScreenDisplayLinks links;
PerScreenDisplayLinks::Connection connection;
return [dispatch = std::shared_ptr<DispatchSource> (std::move (dispatchSource))]
{
dispatch_source_merge_data (dispatch.get(), 1);
};
})
};
SharedResourcePointer<ConcreteDisplayLinks> sharedDisplayLinks;
static NSView* createViewInstance();
static NSWindow* createWindowInstance();

View file

@ -0,0 +1,299 @@
/*
==============================================================================
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
{
//==============================================================================
/*
Forwards NSNotificationCenter callbacks to a std::function<void()>.
*/
class FunctionNotificationCenterObserver
{
public:
FunctionNotificationCenterObserver (NSNotificationName notificationName,
id objectToObserve,
std::function<void()> callback)
: onNotification (std::move (callback)),
observer (observerObject.get(), getSelector(), notificationName, objectToObserve)
{}
private:
struct ObserverClass
{
ObserverClass()
{
klass.addIvar<FunctionNotificationCenterObserver*> ("owner");
klass.addMethod (getSelector(), [] (id self, SEL, NSNotification*)
{
getIvar<FunctionNotificationCenterObserver*> (self, "owner")->onNotification();
});
klass.registerClass();
}
NSObject* createInstance() const { return klass.createInstance(); }
private:
ObjCClass<NSObject> klass { "JUCEObserverClass_" };
};
static SEL getSelector()
{
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
return @selector (notificationFired:);
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
}
std::function<void()> onNotification;
NSUniquePtr<NSObject> observerObject
{
[this]
{
static ObserverClass observerClass;
auto* result = observerClass.createInstance();
object_setInstanceVariable (result, "owner", this);
return result;
}()
};
ScopedNotificationCenterObserver observer;
// Instances can't be copied or moved, because 'this' is stored as a member of the ObserverClass
// object.
JUCE_DECLARE_NON_COPYABLE (FunctionNotificationCenterObserver)
JUCE_DECLARE_NON_MOVEABLE (FunctionNotificationCenterObserver)
};
//==============================================================================
/*
Manages the lifetime of a CVDisplayLinkRef for a single display, and automatically starts and
stops it.
*/
class ScopedDisplayLink
{
public:
ScopedDisplayLink (NSScreen* screenIn, std::function<void()> onCallbackIn)
: screen (screenIn),
link ([display = (CGDirectDisplayID) [[screen.deviceDescription objectForKey: @"NSScreenNumber"] unsignedIntegerValue]]
{
CVDisplayLinkRef ptr = nullptr;
const auto result = CVDisplayLinkCreateWithCGDisplay (display, &ptr);
jassertquiet (result == kCVReturnSuccess);
jassertquiet (ptr != nullptr);
return ptr;
}()),
onCallback (std::move (onCallbackIn))
{
const auto callback = [] (CVDisplayLinkRef,
const CVTimeStamp*,
const CVTimeStamp*,
CVOptionFlags,
CVOptionFlags*,
void* context) -> int
{
static_cast<const ScopedDisplayLink*> (context)->onCallback();
return kCVReturnSuccess;
};
const auto callbackResult = CVDisplayLinkSetOutputCallback (link.get(), callback, this);
jassertquiet (callbackResult == kCVReturnSuccess);
const auto startResult = CVDisplayLinkStart (link.get());
jassertquiet (startResult == kCVReturnSuccess);
}
~ScopedDisplayLink() noexcept
{
if (link != nullptr)
CVDisplayLinkStop (link.get());
}
NSScreen* getScreen() const { return screen; }
double getNominalVideoRefreshPeriodS() const
{
const auto nominalVideoRefreshPeriod = CVDisplayLinkGetNominalOutputVideoRefreshPeriod (link.get());
if ((nominalVideoRefreshPeriod.flags & kCVTimeIsIndefinite) == 0)
return (double) nominalVideoRefreshPeriod.timeValue / (double) nominalVideoRefreshPeriod.timeScale;
return 0.0;
}
private:
struct DisplayLinkDestructor
{
void operator() (CVDisplayLinkRef ptr) const
{
if (ptr != nullptr)
CVDisplayLinkRelease (ptr);
}
};
NSScreen* screen = nullptr;
std::unique_ptr<std::remove_pointer_t<CVDisplayLinkRef>, DisplayLinkDestructor> link;
std::function<void()> onCallback;
// Instances can't be copied or moved, because 'this' is passed as context to
// CVDisplayLinkSetOutputCallback
JUCE_DECLARE_NON_COPYABLE (ScopedDisplayLink)
JUCE_DECLARE_NON_MOVEABLE (ScopedDisplayLink)
};
//==============================================================================
/*
Holds a ScopedDisplayLink for each screen. When the screen configuration changes, the
ScopedDisplayLinks will be recreated automatically to match the new configuration.
*/
class PerScreenDisplayLinks
{
public:
PerScreenDisplayLinks()
{
refreshScreens();
}
using RefreshCallback = std::function<void()>;
using Factory = std::function<RefreshCallback (NSScreen*)>;
/*
Automatically unregisters a CVDisplayLink callback factory when ~Connection() is called.
*/
class Connection
{
public:
Connection() = default;
Connection (PerScreenDisplayLinks& linksIn, std::list<Factory>::const_iterator it)
: links (&linksIn), iter (it) {}
~Connection() noexcept
{
if (links != nullptr)
links->unregisterFactory (iter);
}
Connection (const Connection&) = delete;
Connection& operator= (const Connection&) = delete;
Connection (Connection&& other) noexcept
: links (std::exchange (other.links, nullptr)), iter (other.iter) {}
Connection& operator= (Connection&& other) noexcept
{
Connection { std::move (other) }.swap (*this);
return *this;
}
private:
void swap (Connection& other) noexcept
{
std::swap (other.links, links);
std::swap (other.iter, iter);
}
PerScreenDisplayLinks* links = nullptr;
std::list<Factory>::const_iterator iter;
};
/* Stores the provided factory for as long as the returned Connection remains alive.
Whenever the screen configuration changes, the factory function will be called for each
screen. The RefreshCallback returned by the factory will be called every time that screen's
display link callback fires.
*/
JUCE_NODISCARD Connection registerFactory (Factory factory)
{
const ScopedLock lock (mutex);
factories.push_front (std::move (factory));
refreshScreens();
return { *this, factories.begin() };
}
double getNominalVideoRefreshPeriodSForScreen (NSScreen* screen) const
{
const ScopedLock lock (mutex);
for (const auto& link : links)
if (link.getScreen() == screen)
return link.getNominalVideoRefreshPeriodS();
return 0.0;
}
private:
void unregisterFactory (std::list<Factory>::const_iterator iter)
{
const ScopedLock lock (mutex);
factories.erase (iter);
refreshScreens();
}
void refreshScreens()
{
auto newLinks = [&]
{
std::list<ScopedDisplayLink> result;
for (NSScreen* screen in [NSScreen screens])
{
std::vector<RefreshCallback> callbacks;
for (auto& factory : factories)
callbacks.push_back (factory (screen));
// This is the callback that will actually fire in response to this screen's display
// link callback.
result.emplace_back (screen, [callbacks = std::move (callbacks)]
{
for (const auto& callback : callbacks)
callback();
});
}
return result;
}();
const ScopedLock lock (mutex);
links = std::move (newLinks);
}
CriticalSection mutex;
// This is a list rather than a vector so that the iterators are stable, even when items are
// added/removed from the list. This is important because Connection objects store an iterator
// internally, and may be created/destroyed arbitrarily.
std::list<Factory> factories;
// This is a list rather than a vector because ScopedDisplayLink is non-moveable.
std::list<ScopedDisplayLink> links;
FunctionNotificationCenterObserver observer { NSApplicationDidChangeScreenParametersNotification,
nullptr,
[this] { refreshScreens(); } };
};
} // namespace juce

View file

@ -123,7 +123,7 @@ public:
void shutdownOnRenderThread() { deactivateCurrentContext(); }
bool createdOk() const noexcept { return getRawContext() != nullptr; }
void* getRawContext() const noexcept { return static_cast<void*> (renderContext); }
NSOpenGLContext* getRawContext() const noexcept { return renderContext; }
GLuint getFrameBufferID() const noexcept { return 0; }
bool makeActive() const noexcept

View file

@ -23,6 +23,10 @@
==============================================================================
*/
#if JUCE_MAC
#include <juce_gui_basics/native/juce_mac_PerScreenDisplayLinks.h>
#endif
namespace juce
{
@ -155,11 +159,6 @@ public:
{
if (nativeContext != nullptr)
{
#if JUCE_MAC
cvDisplayLinkWrapper = std::make_unique<CVDisplayLinkWrapper> (*this);
cvDisplayLinkWrapper->updateActiveDisplay();
#endif
renderThread = std::make_unique<ThreadPool> (1);
resume();
}
@ -185,10 +184,6 @@ public:
renderThread.reset();
}
#if JUCE_MAC
cvDisplayLinkWrapper = nullptr;
#endif
hasInitialised = false;
}
@ -359,12 +354,10 @@ public:
if (auto* peer = component.getPeer())
{
#if JUCE_MAC
updateScreen();
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)])
@ -651,11 +644,6 @@ public:
if (context.renderer != nullptr)
context.renderer->newOpenGLContextCreated();
#if JUCE_MAC
jassert (cvDisplayLinkWrapper != nullptr);
nativeContext->setNominalVideoRefreshPeriodS (cvDisplayLinkWrapper->getNominalVideoRefreshPeriodS());
#endif
return true;
}
@ -811,84 +799,67 @@ public:
#if JUCE_MAC
NSView* getCurrentView() const
{
JUCE_ASSERT_MESSAGE_THREAD;
if (auto* peer = component.getPeer())
return static_cast<NSView*> (peer->getNativeHandle());
return nullptr;
}
NSWindow* getCurrentWindow() const
{
JUCE_ASSERT_MESSAGE_THREAD;
if (auto* view = getCurrentView())
return [view window];
return nullptr;
}
NSScreen* getCurrentScreen() const
{
JUCE_ASSERT_MESSAGE_THREAD;
if (auto* view = getCurrentView())
if (auto* window = [view window])
return [window screen];
if (auto* window = getCurrentWindow())
return [window screen];
return nullptr;
}
struct CVDisplayLinkWrapper
void updateScreen()
{
explicit CVDisplayLinkWrapper (CachedImage& cachedImageIn)
: cachedImage (cachedImageIn),
continuousRepaint (cachedImageIn.context.continuousRepaint.load())
const auto screen = getCurrentScreen();
lastScreen = screen;
const auto newRefreshPeriod = sharedDisplayLinks->getNominalVideoRefreshPeriodSForScreen (screen);
if (newRefreshPeriod != 0.0 && std::exchange (refreshPeriod, newRefreshPeriod) != newRefreshPeriod)
nativeContext->setNominalVideoRefreshPeriodS (newRefreshPeriod);
}
std::atomic<NSScreen*> lastScreen { nullptr };
double refreshPeriod = 0.0;
FunctionNotificationCenterObserver observer { NSWindowDidChangeScreenNotification,
getCurrentWindow(),
[this] { updateScreen(); } };
// 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]
{
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;
// 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;
@ -984,16 +955,6 @@ public:
}
#endif
void update()
{
auto& comp = *getComponent();
if (canBeAttached (comp))
start();
else
stop();
}
private:
OpenGLContext& context;