From d9a3efd3cb07f8726c499b2b3dce865c7732742e Mon Sep 17 00:00:00 2001 From: attila Date: Thu, 3 Oct 2024 10:58:51 +0200 Subject: [PATCH] ComponentPeer::VBlankListener: Add timestamp parameter to the vblank callback --- BREAKING_CHANGES.md | 24 +++++++++++ .../native/juce_NSViewComponentPeer_mac.mm | 40 +++++++++++++------ .../native/juce_PerScreenDisplayLinks_mac.h | 15 +++---- .../native/juce_UIViewComponentPeer_ios.mm | 8 ++-- .../native/juce_VBlank_windows.cpp | 8 ++-- .../native/juce_Windowing_android.cpp | 3 +- .../native/juce_Windowing_linux.cpp | 3 +- .../native/juce_Windowing_windows.cpp | 4 +- .../windows/juce_ComponentPeer.cpp | 5 +++ .../windows/juce_ComponentPeer.h | 9 ++++- .../juce_opengl/opengl/juce_OpenGLContext.cpp | 2 +- 11 files changed, 88 insertions(+), 33 deletions(-) diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md index cb126d7482..b2fa6797c5 100644 --- a/BREAKING_CHANGES.md +++ b/BREAKING_CHANGES.md @@ -1,5 +1,29 @@ # JUCE breaking changes +# develop + +## Change + +The signature of VBlankListener::onVBlank() was changed to +VBlankListener::onVBlank (double), with the addition of a timestamp parameter +that corresponds to the time at which the next frame will be displayed. + +**Possible Issues** + +Code that overrides VBlankListener::onVBlank() will fail to compile. + +**Workaround** + +Add a double parameter to the function overriding VBlankListener::onVBlank(). +The behaviour will be unchanged if this new parameter is then ignored. + +**Rationale** + +A timestamp parameter has been missing from the VBlank callback since its +addition. The new parameter allows all VBlankListeners to synchronise the +content of their draw calls to the same frame timestamp. + + # Version 8.0.2 ## Change diff --git a/modules/juce_gui_basics/native/juce_NSViewComponentPeer_mac.mm b/modules/juce_gui_basics/native/juce_NSViewComponentPeer_mac.mm index ed9cd36b1b..7c13bec982 100644 --- a/modules/juce_gui_basics/native/juce_NSViewComponentPeer_mac.mm +++ b/modules/juce_gui_basics/native/juce_NSViewComponentPeer_mac.mm @@ -1075,9 +1075,9 @@ public: return areAnyWindowsInLiveResize(); } - void onVBlank() + void onVBlank (double timestampSec) { - vBlankListeners.call ([] (auto& l) { l.onVBlank(); }); + callVBlankListeners (timestampSec); setNeedsDisplayRectangles(); } @@ -1758,13 +1758,18 @@ private: explicit AsyncRepainter (NSViewComponentPeer& o) : owner (o) {} ~AsyncRepainter() override { cancelPendingUpdate(); } - void markUpdated (const CGDirectDisplayID x) + void markUpdated (const CGDirectDisplayID x, double timestampSec) { { const std::scoped_lock lock { mutex }; - if (std::find (backgroundDisplays.cbegin(), backgroundDisplays.cend(), x) == backgroundDisplays.cend()) - backgroundDisplays.push_back (x); + if (const auto it = std::find_if (backgroundVBlankEvents.cbegin(), + backgroundVBlankEvents.cend(), + [&x] (const auto& event) { return event.display == x; }); + it == backgroundVBlankEvents.cend()) + { + backgroundVBlankEvents.push_back ({ x, timestampSec }); + } } triggerAsyncUpdate(); @@ -1775,20 +1780,28 @@ private: { { const std::scoped_lock lock { mutex }; - mainThreadDisplays = backgroundDisplays; - backgroundDisplays.clear(); + mainThreadVBlankEvents = backgroundVBlankEvents; + backgroundVBlankEvents.clear(); } - for (const auto& display : mainThreadDisplays) + for (const auto& event : mainThreadVBlankEvents) + { if (auto* peerView = owner.view) if (auto* peerWindow = [peerView window]) - if (display == ScopedDisplayLink::getDisplayIdForScreen ([peerWindow screen])) - owner.onVBlank(); + if (event.display == ScopedDisplayLink::getDisplayIdForScreen ([peerWindow screen])) + owner.onVBlank (event.timeSec); + } } + struct VBlankEvent + { + CGDirectDisplayID display{}; + double timeSec{}; + }; + NSViewComponentPeer& owner; std::mutex mutex; - std::vector backgroundDisplays, mainThreadDisplays; + std::vector backgroundVBlankEvents, mainThreadVBlankEvents; }; AsyncRepainter asyncRepainter { *this }; @@ -1801,7 +1814,10 @@ private: { sharedDisplayLinks->registerFactory ([this] (CGDirectDisplayID display) { - return [this, display] { asyncRepainter.markUpdated (display); }; + return [this, display] (double timestampSec) + { + asyncRepainter.markUpdated (display, timestampSec); + }; }) }; diff --git a/modules/juce_gui_basics/native/juce_PerScreenDisplayLinks_mac.h b/modules/juce_gui_basics/native/juce_PerScreenDisplayLinks_mac.h index fbfc3ecf19..4e412beb13 100644 --- a/modules/juce_gui_basics/native/juce_PerScreenDisplayLinks_mac.h +++ b/modules/juce_gui_basics/native/juce_PerScreenDisplayLinks_mac.h @@ -119,7 +119,7 @@ public: return (CGDirectDisplayID) [[screen.deviceDescription objectForKey: @"NSScreenNumber"] unsignedIntegerValue]; } - ScopedDisplayLink (NSScreen* screenIn, std::function onCallbackIn) + ScopedDisplayLink (NSScreen* screenIn, std::function onCallbackIn) : displayId (getDisplayIdForScreen (screenIn)), link ([display = displayId] { @@ -133,12 +133,13 @@ public: { const auto callback = [] (CVDisplayLinkRef, const CVTimeStamp*, - const CVTimeStamp*, + const CVTimeStamp* outputTime, CVOptionFlags, CVOptionFlags*, void* context) -> int { - static_cast (context)->onCallback(); + const auto outputTimeSec = (double) outputTime->videoTime / (double) outputTime->videoTimeScale; + static_cast (context)->onCallback (outputTimeSec); return kCVReturnSuccess; }; @@ -179,7 +180,7 @@ private: CGDirectDisplayID displayId; std::unique_ptr, DisplayLinkDestructor> link; - std::function onCallback; + std::function onCallback; // Instances can't be copied or moved, because 'this' is passed as context to // CVDisplayLinkSetOutputCallback @@ -202,7 +203,7 @@ public: refreshScreens(); } - using RefreshCallback = std::function; + using RefreshCallback = std::function; using Factory = std::function; /* @@ -293,10 +294,10 @@ private: // This is the callback that will actually fire in response to this screen's display // link callback. - result.emplace_back (screen, [cbs = std::move (callbacks)] + result.emplace_back (screen, [cbs = std::move (callbacks)] (double timestampSec) { for (const auto& callback : cbs) - callback(); + callback (timestampSec); }); } diff --git a/modules/juce_gui_basics/native/juce_UIViewComponentPeer_ios.mm b/modules/juce_gui_basics/native/juce_UIViewComponentPeer_ios.mm index 1e0b0b073e..0be97b77ce 100644 --- a/modules/juce_gui_basics/native/juce_UIViewComponentPeer_ios.mm +++ b/modules/juce_gui_basics/native/juce_UIViewComponentPeer_ios.mm @@ -420,7 +420,7 @@ public: void setIcon (const Image& newIcon) override; StringArray getAvailableRenderingEngines() override { return StringArray ("CoreGraphics Renderer"); } - void displayLinkCallback(); + void displayLinkCallback (double timestampSec); void drawRect (CGRect); void drawRectWithContext (CGContextRef, CGRect); @@ -768,7 +768,7 @@ MultiTouchMapper UIViewComponentPeer::currentTouches; - (void) displayLinkCallback: (CADisplayLink*) dl { if (owner != nullptr) - owner->displayLinkCallback(); + owner->displayLinkCallback (dl.targetTimestamp); } #if JUCE_COREGRAPHICS_RENDER_WITH_MULTIPLE_PAINT_CALLS @@ -2160,9 +2160,9 @@ void UIViewComponentPeer::dismissPendingTextInput() } //============================================================================== -void UIViewComponentPeer::displayLinkCallback() +void UIViewComponentPeer::displayLinkCallback (double timestampSec) { - vBlankListeners.call ([] (auto& l) { l.onVBlank(); }); + callVBlankListeners (timestampSec); if (deferredRepaints.isEmpty()) return; diff --git a/modules/juce_gui_basics/native/juce_VBlank_windows.cpp b/modules/juce_gui_basics/native/juce_VBlank_windows.cpp index 4b87734015..4c941bb823 100644 --- a/modules/juce_gui_basics/native/juce_VBlank_windows.cpp +++ b/modules/juce_gui_basics/native/juce_VBlank_windows.cpp @@ -122,7 +122,7 @@ private: if (output->WaitForVBlank() == S_OK) { if (const auto now = Time::getMillisecondCounterHiRes(); - now - std::exchange (lastVBlankEvent, now) < 1.0) + now - lastVBlankEvent.exchange (now) < 1.0) { Thread::sleep (1); } @@ -145,8 +145,10 @@ private: void handleAsyncUpdate() override { + const auto timestampSec = lastVBlankEvent / 1000.0; + for (auto& listener : listeners) - listener.get().onVBlank(); + listener.get().onVBlank (timestampSec); { const std::scoped_lock lock { mutex }; @@ -170,7 +172,7 @@ private: exit, }; - double lastVBlankEvent = 0.0; + std::atomic lastVBlankEvent{}; ThreadState threadState = ThreadState::paint; std::condition_variable condvar; std::mutex mutex; diff --git a/modules/juce_gui_basics/native/juce_Windowing_android.cpp b/modules/juce_gui_basics/native/juce_Windowing_android.cpp index 2da8fa6f36..33bd0a3aa6 100644 --- a/modules/juce_gui_basics/native/juce_Windowing_android.cpp +++ b/modules/juce_gui_basics/native/juce_Windowing_android.cpp @@ -1848,7 +1848,8 @@ public: //============================================================================== static void handleDoFrameCallback (JNIEnv*, AndroidComponentPeer& t, [[maybe_unused]] int64 frameTimeNanos) { - t.vBlankListeners.call ([] (auto& l) { l.onVBlank(); }); + const auto timestampSec = (double) frameTimeNanos / (double) 1'000'000'000; + t.callVBlankListeners (timestampSec); } static void handlePaintCallback (JNIEnv* env, AndroidComponentPeer& t, jobject canvas, jobject paint) diff --git a/modules/juce_gui_basics/native/juce_Windowing_linux.cpp b/modules/juce_gui_basics/native/juce_Windowing_linux.cpp index d6d3f322da..235b13da3d 100644 --- a/modules/juce_gui_basics/native/juce_Windowing_linux.cpp +++ b/modules/juce_gui_basics/native/juce_Windowing_linux.cpp @@ -573,7 +573,8 @@ private: void onVBlank() { - vBlankListeners.call ([] (auto& l) { l.onVBlank(); }); + const auto timestampSec = Time::getMillisecondCounterHiRes() / 1000.0; + callVBlankListeners (timestampSec); if (repainter != nullptr) repainter->dispatchDeferredRepaints(); diff --git a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp index c9434b0966..479ee3efd6 100644 --- a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp +++ b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp @@ -1920,9 +1920,9 @@ public: } //============================================================================== - void onVBlank() override + void onVBlank (double timestampSec) override { - vBlankListeners.call ([] (auto& l) { l.onVBlank(); }); + callVBlankListeners (timestampSec); dispatchDeferredRepaints(); if (renderContext != nullptr) diff --git a/modules/juce_gui_basics/windows/juce_ComponentPeer.cpp b/modules/juce_gui_basics/windows/juce_ComponentPeer.cpp index eff7e63882..7111ed83e5 100644 --- a/modules/juce_gui_basics/windows/juce_ComponentPeer.cpp +++ b/modules/juce_gui_basics/windows/juce_ComponentPeer.cpp @@ -617,6 +617,11 @@ void ComponentPeer::forceDisplayUpdate() Desktop::getInstance().displays->refresh(); } +void ComponentPeer::callVBlankListeners (double timestampSec) +{ + vBlankListeners.call ([timestampSec] (auto& l) { l.onVBlank (timestampSec); }); +} + void ComponentPeer::globalFocusChanged ([[maybe_unused]] Component* comp) { refreshTextInputTarget(); diff --git a/modules/juce_gui_basics/windows/juce_ComponentPeer.h b/modules/juce_gui_basics/windows/juce_ComponentPeer.h index b39b604a96..b10de7f4f7 100644 --- a/modules/juce_gui_basics/windows/juce_ComponentPeer.h +++ b/modules/juce_gui_basics/windows/juce_ComponentPeer.h @@ -516,8 +516,12 @@ public: /** Destructor. */ virtual ~VBlankListener() = default; - /** Called on every vertical blank of the display to which the peer is associated. */ - virtual void onVBlank() = 0; + /** Called on every vertical blank of the display to which the peer is associated. + + The timestampSec parameter is a monotonically increasing value expressed in seconds + that corresponds to the time at which the next frame will be displayed. + */ + virtual void onVBlank (double timestampSec) = 0; }; /** Adds a VBlankListener. */ @@ -577,6 +581,7 @@ public: protected: //============================================================================== static void forceDisplayUpdate(); + void callVBlankListeners (double timestampSec); Component& component; const int styleFlags; diff --git a/modules/juce_opengl/opengl/juce_OpenGLContext.cpp b/modules/juce_opengl/opengl/juce_OpenGLContext.cpp index 4d6a4fed2c..cfed2105ae 100644 --- a/modules/juce_opengl/opengl/juce_OpenGLContext.cpp +++ b/modules/juce_opengl/opengl/juce_OpenGLContext.cpp @@ -899,7 +899,7 @@ public: { connection.emplace (sharedDisplayLinks->registerFactory ([this] (CGDirectDisplayID display) { - return [this, display] + return [this, display] (double) { if (display == lastDisplay) triggerRepaint();