From 04e7014d0f2f36790d9b904534633e362acde63e Mon Sep 17 00:00:00 2001 From: Tom Poole Date: Tue, 22 Feb 2022 15:45:50 +0000 Subject: [PATCH] macOS/iOS: Sync repaint request rate to screen FPS and remove repaint throttling in plug-ins --- .../native/juce_ios_UIViewComponentPeer.mm | 46 +++++++- .../native/juce_mac_NSViewComponentPeer.mm | 107 ++++++++++++++---- 2 files changed, 126 insertions(+), 27 deletions(-) diff --git a/modules/juce_gui_basics/native/juce_ios_UIViewComponentPeer.mm b/modules/juce_gui_basics/native/juce_ios_UIViewComponentPeer.mm index b0e37c2b18..b3e27c1233 100644 --- a/modules/juce_gui_basics/native/juce_ios_UIViewComponentPeer.mm +++ b/modules/juce_gui_basics/native/juce_ios_UIViewComponentPeer.mm @@ -105,16 +105,28 @@ enum class MouseEventFlags using namespace juce; +struct CADisplayLinkDeleter +{ + void operator() (CADisplayLink* displayLink) const noexcept + { + [displayLink invalidate]; + [displayLink release]; + } +}; + @interface JuceUIView : UIView { @public UIViewComponentPeer* owner; UITextView* hiddenTextView; + std::unique_ptr displayLink; } - (JuceUIView*) initWithOwner: (UIViewComponentPeer*) owner withFrame: (CGRect) frame; - (void) dealloc; +- (void) displayLinkCallback: (CADisplayLink*) dl; + - (void) drawRect: (CGRect) r; - (void) touchesBegan: (NSSet*) touches withEvent: (UIEvent*) event; @@ -224,6 +236,8 @@ public: void setIcon (const Image& newIcon) override; StringArray getAvailableRenderingEngines() override { return StringArray ("CoreGraphics Renderer"); } + void displayLinkCallback(); + void drawRect (CGRect); bool canBecomeKeyWindow(); @@ -301,6 +315,8 @@ private: } }; + RectangleList deferredRepaints; + //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIViewComponentPeer) }; @@ -453,6 +469,11 @@ MultiTouchMapper UIViewComponentPeer::currentTouches; [super initWithFrame: frame]; owner = peer; + displayLink.reset ([CADisplayLink displayLinkWithTarget: self + selector: @selector (displayLinkCallback:)]); + [displayLink.get() addToRunLoop: [NSRunLoop mainRunLoop] + forMode: NSDefaultRunLoopMode]; + hiddenTextView = [[UITextView alloc] initWithFrame: CGRectZero]; [self addSubview: hiddenTextView]; hiddenTextView.delegate = self; @@ -487,9 +508,18 @@ MultiTouchMapper UIViewComponentPeer::currentTouches; [hiddenTextView removeFromSuperview]; [hiddenTextView release]; + displayLink = nullptr; + [super dealloc]; } +//============================================================================== +- (void) displayLinkCallback: (CADisplayLink*) dl +{ + if (owner != nullptr) + owner->displayLinkCallback(); +} + //============================================================================== - (void) drawRect: (CGRect) r { @@ -1137,6 +1167,15 @@ void UIViewComponentPeer::globalFocusChanged (Component*) } } +//============================================================================== +void UIViewComponentPeer::displayLinkCallback() +{ + for (const auto& r : deferredRepaints) + [view setNeedsDisplayInRect: convertToCGRect (r)]; + + deferredRepaints.clear(); +} + //============================================================================== void UIViewComponentPeer::drawRect (CGRect r) { @@ -1201,9 +1240,12 @@ void Desktop::allowedOrientationsChanged() void UIViewComponentPeer::repaint (const Rectangle& area) { if (insideDrawRect || ! MessageManager::getInstance()->isThisTheMessageThread()) + { (new AsyncRepaintMessage (this, area))->post(); - else - [view setNeedsDisplayInRect: convertToCGRect (area)]; + return; + } + + deferredRepaints.add (area.toFloat()); } void UIViewComponentPeer::performAnyPendingRepaintsNow() diff --git a/modules/juce_gui_basics/native/juce_mac_NSViewComponentPeer.mm b/modules/juce_gui_basics/native/juce_mac_NSViewComponentPeer.mm index 84aa3f9cef..4b2da33571 100644 --- a/modules/juce_gui_basics/native/juce_mac_NSViewComponentPeer.mm +++ b/modules/juce_gui_basics/native/juce_mac_NSViewComponentPeer.mm @@ -94,8 +94,7 @@ static constexpr int translateVirtualToAsciiKeyCode (int keyCode) noexcept constexpr int extendedKeyModifier = 0x30000; //============================================================================== -class NSViewComponentPeer : public ComponentPeer, - private Timer +class NSViewComponentPeer : public ComponentPeer { public: NSViewComponentPeer (Component& comp, const int windowStyleFlags, NSView* viewToAttachTo) @@ -145,6 +144,8 @@ public: } #endif + createCVDisplayLink(); + if (isSharedWindow) { window = [viewToAttachTo window]; @@ -239,6 +240,9 @@ public: ~NSViewComponentPeer() override { + CVDisplayLinkStop (displayLink); + dispatch_source_cancel (displaySource); + [notificationCenter removeObserver: view]; setOwner (view, nullptr); @@ -779,6 +783,10 @@ public: [notificationCenter removeObserver: view name: NSWindowDidBecomeKeyNotification object: currentWindow]; + + [notificationCenter removeObserver: view + name: NSWindowDidChangeScreenNotification + object: currentWindow]; } if (isSharedWindow && [view window] == window && newWindow == nullptr) @@ -1016,43 +1024,31 @@ public: // a few when there's a lot of activity. // As a work around for this, we use a RectangleList to do our own coalescing of regions before // asynchronously asking the OS to repaint them. - deferredRepaints.add ((float) area.getX(), (float) area.getY(), - (float) area.getWidth(), (float) area.getHeight()); + deferredRepaints.add (area.toFloat()); + } - if (isTimerRunning()) + static bool shouldThrottleRepaint() + { + return areAnyWindowsInLiveResize(); + } + + void setNeedsDisplayRectangles() + { + if (deferredRepaints.isEmpty()) return; auto now = Time::getMillisecondCounter(); auto msSinceLastRepaint = (lastRepaintTime >= now) ? now - lastRepaintTime : (std::numeric_limits::max() - lastRepaintTime) + now; - static uint32 minimumRepaintInterval = 1000 / 30; // 30fps + constexpr uint32 minimumRepaintInterval = 1000 / 30; // 30fps // When windows are being resized, artificially throttling high-frequency repaints helps // to stop the event queue getting clogged, and keeps everything working smoothly. // For some reason Logic also needs this throttling to record parameter events correctly. if (msSinceLastRepaint < minimumRepaintInterval && shouldThrottleRepaint()) - { - startTimer (static_cast (minimumRepaintInterval - msSinceLastRepaint)); return; - } - setNeedsDisplayRectangles(); - } - - static bool shouldThrottleRepaint() - { - return areAnyWindowsInLiveResize() || ! JUCEApplication::isStandaloneApp(); - } - - void timerCallback() override - { - setNeedsDisplayRectangles(); - stopTimer(); - } - - void setNeedsDisplayRectangles() - { for (auto& i : deferredRepaints) [view setNeedsDisplayInRect: makeNSRect (i)]; @@ -1163,6 +1159,11 @@ public: handleMovedOrResized(); } + void windowDidChangeScreen() + { + updateCVDisplayLinkScreen(); + } + void viewMovedToWindow() { if (isSharedWindow) @@ -1197,6 +1198,13 @@ public: selector: resignKeySelector name: NSWindowDidResignKeyNotification object: currentWindow]; + + [notificationCenter addObserver: view + selector: @selector (windowDidChangeScreen:) + name: NSWindowDidChangeScreenNotification + object: currentWindow]; + + updateCVDisplayLinkScreen(); } } @@ -1786,6 +1794,48 @@ private: [window setMaxFullScreenContentSize: NSMakeSize (100000, 100000)]; } + void onDisplaySourceCallback() + { + setNeedsDisplayRectangles(); + } + + void onDisplayLinkCallback() + { + dispatch_source_merge_data (displaySource, 1); + } + + static CVReturn displayLinkCallback (CVDisplayLinkRef, const CVTimeStamp*, const CVTimeStamp*, + CVOptionFlags, CVOptionFlags*, void* context) + { + static_cast (context)->onDisplayLinkCallback(); + return kCVReturnSuccess; + } + + void updateCVDisplayLinkScreen() + { + auto viewDisplayID = (CGDirectDisplayID) [window.screen.deviceDescription[@"NSScreenNumber"] unsignedIntegerValue]; + auto result = CVDisplayLinkSetCurrentCGDisplay (displayLink, viewDisplayID); + jassertquiet (result == kCVReturnSuccess); + } + + void createCVDisplayLink() + { + displaySource = dispatch_source_create (DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue()); + dispatch_source_set_event_handler (displaySource, ^(){ onDisplaySourceCallback(); }); + dispatch_resume (displaySource); + + auto cvReturn = CVDisplayLinkCreateWithActiveCGDisplays (&displayLink); + jassertquiet (cvReturn == kCVReturnSuccess); + + cvReturn = CVDisplayLinkSetOutputCallback (displayLink, &displayLinkCallback, this); + jassertquiet (cvReturn == kCVReturnSuccess); + + CVDisplayLinkStart (displayLink); + } + + CVDisplayLinkRef displayLink = nullptr; + dispatch_source_t displaySource = nullptr; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NSViewComponentPeer) }; @@ -1848,6 +1898,7 @@ struct JuceNSViewClass : public NSViewComponentPeerWrapper> addMethod (@selector (acceptsFirstMouse:), acceptsFirstMouse); addMethod (@selector (windowWillMiniaturize:), windowWillMiniaturize); addMethod (@selector (windowDidDeminiaturize:), windowDidDeminiaturize); + addMethod (@selector (windowDidChangeScreen:), windowDidChangeScreen); addMethod (@selector (wantsDefaultClipping), wantsDefaultClipping); addMethod (@selector (worksWhenModal), worksWhenModal); addMethod (@selector (viewDidMoveToWindow), viewDidMoveToWindow); @@ -2014,6 +2065,12 @@ private: } } + static void windowDidChangeScreen (id self, SEL, NSNotification*) + { + if (auto* p = getOwner (self)) + p->windowDidChangeScreen(); + } + static BOOL isOpaque (id self, SEL) { auto* owner = getOwner (self);