From d07e59bc5e8328b2edaad7f91f617c0a1ad224af Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 24 Aug 2023 01:36:03 -0700 Subject: [PATCH] Implement CVDisplayLink based timer on macOS --- .../project.pbxproj | 4 + .../src/OSX/PlatformRenderTimer.mm | 84 +++++++++++++++++++ native/Avalonia.Native/src/OSX/common.h | 1 + native/Avalonia.Native/src/OSX/main.mm | 11 +++ src/Avalonia.Native/AvaloniaNativePlatform.cs | 2 +- .../AvaloniaNativeRenderTimer.cs | 58 +++++++++++++ src/Avalonia.Native/avn.idl | 10 +++ 7 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 native/Avalonia.Native/src/OSX/PlatformRenderTimer.mm create mode 100644 src/Avalonia.Native/AvaloniaNativeRenderTimer.cs diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index f11b0271737..a07532412d6 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -59,6 +59,7 @@ BC11A5BE2608D58F0017BAD0 /* automation.h in Headers */ = {isa = PBXBuildFile; fileRef = BC11A5BC2608D58F0017BAD0 /* automation.h */; }; BC11A5BF2608D58F0017BAD0 /* automation.mm in Sources */ = {isa = PBXBuildFile; fileRef = BC11A5BD2608D58F0017BAD0 /* automation.mm */; }; ED3791C42862E1F40080BD62 /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED3791C32862E1F40080BD62 /* UniformTypeIdentifiers.framework */; }; + ED754D262A97306B0078B4DF /* PlatformRenderTimer.mm in Sources */ = {isa = PBXBuildFile; fileRef = ED754D252A97306B0078B4DF /* PlatformRenderTimer.mm */; }; EDF8CDCD2964CB01001EE34F /* PlatformSettings.mm in Sources */ = {isa = PBXBuildFile; fileRef = EDF8CDCC2964CB01001EE34F /* PlatformSettings.mm */; }; /* End PBXBuildFile section */ @@ -122,6 +123,7 @@ BC11A5BC2608D58F0017BAD0 /* automation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = automation.h; sourceTree = ""; }; BC11A5BD2608D58F0017BAD0 /* automation.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = automation.mm; sourceTree = ""; }; ED3791C32862E1F40080BD62 /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; }; + ED754D252A97306B0078B4DF /* PlatformRenderTimer.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = PlatformRenderTimer.mm; sourceTree = ""; }; EDF8CDCC2964CB01001EE34F /* PlatformSettings.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = PlatformSettings.mm; sourceTree = ""; }; /* End PBXFileReference section */ @@ -162,6 +164,7 @@ AB7A61E62147C814003C5833 = { isa = PBXGroup; children = ( + ED754D252A97306B0078B4DF /* PlatformRenderTimer.mm */, 855EDC9E28C6546F00807998 /* PlatformBehaviorInhibition.mm */, 8D2F3511292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h */, 8D300D68292E1E5D00320C49 /* AvnTextInputMethod.mm */, @@ -333,6 +336,7 @@ 18391068E48EF96E3DB5FDAB /* ResizeScope.mm in Sources */, 18391D4EB311BC7EF8B8C0A6 /* AvnView.mm in Sources */, 18391AA7E0BBA74D184C5734 /* AutoFitContentView.mm in Sources */, + ED754D262A97306B0078B4DF /* PlatformRenderTimer.mm in Sources */, 1839151F32D1BB1AB51A7BB6 /* AvnPanelWindow.mm in Sources */, 18391AC16726CBC45856233B /* AvnWindow.mm in Sources */, 18391D8CD1756DC858DC1A09 /* PopupImpl.mm in Sources */, diff --git a/native/Avalonia.Native/src/OSX/PlatformRenderTimer.mm b/native/Avalonia.Native/src/OSX/PlatformRenderTimer.mm new file mode 100644 index 00000000000..f372cc90470 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/PlatformRenderTimer.mm @@ -0,0 +1,84 @@ +#include "common.h" + +class PlatformRenderTimer : public ComSingleObject +{ +private: + ComPtr _callback; + CVDisplayLinkRef _displayLink; + +public: + FORWARD_IUNKNOWN() + virtual HRESULT RegisterTick ( + IAvnActionCallback* callback) override + { + START_COM_CALL; + + @autoreleasepool + { + if (_displayLink != nil) + { + return E_UNEXPECTED; + } + + _callback = callback; + auto result = CVDisplayLinkCreateWithActiveCGDisplays(&_displayLink); + if (result != 0) + { + return E_FAIL; + } + + result = CVDisplayLinkSetOutputCallback(_displayLink, OnTick, this); + if (result != 0) + { + return E_FAIL; + } + } + return S_OK; + } + + virtual void Start () override + { + START_COM_CALL; + + @autoreleasepool + { + if (CVDisplayLinkIsRunning(_displayLink) == false) { + CVDisplayLinkStart(_displayLink); + } + } + } + + virtual void Stop () override + { + START_COM_CALL; + + @autoreleasepool + { + if (CVDisplayLinkIsRunning(_displayLink) == true) { + CVDisplayLinkStop(_displayLink); + } + } + } + + virtual bool RunsInBackground () override + { + START_COM_CALL; + + @autoreleasepool + { + return true; + } + } + + static CVReturn OnTick(CVDisplayLinkRef displayLink, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext) + { + PlatformRenderTimer *object = (PlatformRenderTimer *)displayLinkContext; + object->_callback->Run(); + return kCVReturnSuccess; + } +}; + +extern IAvnPlatformRenderTimer* CreatePlatformRenderTimer() +{ + return new PlatformRenderTimer(); +} diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index 672525c64a8..44441ee15db 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -32,6 +32,7 @@ extern IAvnApplicationCommands* CreateApplicationCommands(); extern IAvnPlatformBehaviorInhibition* CreatePlatformBehaviorInhibition(); extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent); extern IAvnPlatformSettings* CreatePlatformSettings(); +extern IAvnPlatformRenderTimer* CreatePlatformRenderTimer(); extern void SetAppMenu(IAvnMenu *menu); extern void SetServicesMenu (IAvnMenu* menu); extern IAvnMenu* GetAppMenu (); diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 3fddb725294..41d6fd37abd 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -446,6 +446,17 @@ virtual HRESULT CreatePlatformBehaviorInhibition(IAvnPlatformBehaviorInhibition* return S_OK; } } + + virtual HRESULT CreatePlatformRenderTimer(IAvnPlatformRenderTimer** ppv) override + { + START_COM_CALL; + + @autoreleasepool + { + *ppv = ::CreatePlatformRenderTimer(); + return S_OK; + } + } }; extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative() diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index b7c58cded9d..f24c6fa96ff 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -108,7 +108,7 @@ void DoInitialize(AvaloniaNativePlatformOptions options) .Bind().ToConstant(new NativePlatformSettings(_factory.CreatePlatformSettings())) .Bind().ToConstant(this) .Bind().ToConstant(new ClipboardImpl(_factory.CreateClipboard())) - .Bind().ToConstant(new DefaultRenderTimer(60)) + .Bind().ToConstant(new AvaloniaNativeRenderTimer(_factory.CreatePlatformRenderTimer())) .Bind().ToConstant(new MacOSMountedVolumeInfoProvider()) .Bind().ToConstant(new AvaloniaNativeDragSource(_factory)) .Bind().ToConstant(applicationPlatform) diff --git a/src/Avalonia.Native/AvaloniaNativeRenderTimer.cs b/src/Avalonia.Native/AvaloniaNativeRenderTimer.cs new file mode 100644 index 00000000000..9021fdf3457 --- /dev/null +++ b/src/Avalonia.Native/AvaloniaNativeRenderTimer.cs @@ -0,0 +1,58 @@ +using System; +using System.Diagnostics; +using Avalonia.Native.Interop; +using Avalonia.Rendering; +#nullable enable + +namespace Avalonia.Native; + +internal sealed class AvaloniaNativeRenderTimer : NativeCallbackBase, IRenderTimer, IAvnActionCallback +{ + private readonly IAvnPlatformRenderTimer _platformRenderTimer; + private readonly Stopwatch _stopwatch; + private Action? _tick; + private int _subscriberCount; + private bool registered; + + public AvaloniaNativeRenderTimer(IAvnPlatformRenderTimer platformRenderTimer) + { + _platformRenderTimer = platformRenderTimer; + _stopwatch = Stopwatch.StartNew(); + } + + public event Action Tick + { + add + { + _tick += value; + + if (!registered) + { + registered = true; + _platformRenderTimer.RegisterTick(this); + } + + if (_subscriberCount++ == 0) + { + _platformRenderTimer.Start(); + } + } + + remove + { + if (--_subscriberCount == 0) + { + _platformRenderTimer.Stop(); + } + + _tick -= value; + } + } + + public bool RunsInBackground => _platformRenderTimer.RunsInBackground().FromComBool(); + + public void Run() + { + _tick?.Invoke(_stopwatch.Elapsed); + } +} diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 0911e5ffff6..7eac1d33a8a 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -507,6 +507,7 @@ interface IAvaloniaNativeFactory : IUnknown HRESULT CreateApplicationCommands(IAvnApplicationCommands** ppv); HRESULT CreatePlatformSettings(IAvnPlatformSettings** ppv); HRESULT CreatePlatformBehaviorInhibition(IAvnPlatformBehaviorInhibition** ppv); + HRESULT CreatePlatformRenderTimer(IAvnPlatformRenderTimer** ppv); } [uuid(233e094f-9b9f-44a3-9a6e-6948bbdd9fb1)] @@ -999,3 +1000,12 @@ interface IAvnPlatformBehaviorInhibition : IUnknown { void SetInhibitAppSleep(bool inhibitAppSleep, char* reason); } + +[uuid(22edf20d-5803-2d3f-9247-b4842e5e9322)] +interface IAvnPlatformRenderTimer : IUnknown +{ + HRESULT RegisterTick(IAvnActionCallback* callback); + void Start(); + void Stop(); + bool RunsInBackground(); +}