From 5737b95d887c2d36c7e2021fa3a8ad80bba19cb3 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 31 Mar 2025 16:00:33 +0500 Subject: [PATCH] [X11] Implemented automatic fps detection --- .../Rendering/SleepLoopRenderTimer.cs | 19 +- .../Screens/X11Screen.Providers.cs | 219 +++++++++++++----- src/Avalonia.X11/Screens/X11Screens.cs | 4 + src/Avalonia.X11/X11Platform.cs | 12 + src/Avalonia.X11/X11Structs.cs | 87 ++++++- src/Avalonia.X11/XLib.Helpers.cs | 10 + src/Avalonia.X11/XLib.cs | 21 ++ 7 files changed, 311 insertions(+), 61 deletions(-) diff --git a/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs b/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs index 3ad4ea94d0..bdbada0e42 100644 --- a/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs +++ b/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs @@ -13,13 +13,25 @@ namespace Avalonia.Rendering private readonly object _lock = new object(); private bool _running; private readonly Stopwatch _st = Stopwatch.StartNew(); - private readonly TimeSpan _timeBetweenTicks; + private volatile int _desiredFps; + public SleepLoopRenderTimer(int fps) { - _timeBetweenTicks = TimeSpan.FromSeconds(1d / fps); + DesiredFps = fps; } + public int DesiredFps + { + get => _desiredFps; + set + { + if (value < 1) + throw new ArgumentOutOfRangeException(); + _desiredFps = value; + } + } + public event Action Tick { add @@ -53,7 +65,8 @@ namespace Avalonia.Rendering while (true) { var now = _st.Elapsed; - var timeTillNextTick = lastTick + _timeBetweenTicks - now; + var tickInterval = TimeSpan.FromSeconds(1d / _desiredFps); + var timeTillNextTick = lastTick + tickInterval - now; if (timeTillNextTick.TotalMilliseconds > 1) Thread.Sleep(timeTillNextTick); lastTick = now = _st.Elapsed; lock (_lock) diff --git a/src/Avalonia.X11/Screens/X11Screen.Providers.cs b/src/Avalonia.X11/Screens/X11Screen.Providers.cs index 82eb21b287..0be7dff9c4 100644 --- a/src/Avalonia.X11/Screens/X11Screen.Providers.cs +++ b/src/Avalonia.X11/Screens/X11Screen.Providers.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using Avalonia.Platform; +using Avalonia.Threading; using static Avalonia.X11.XLib; namespace Avalonia.X11.Screens; @@ -12,8 +13,6 @@ internal partial class X11Screens internal unsafe class X11Screen(MonitorInfo info, X11Info x11, IScalingProvider? scalingProvider, int id) : PlatformScreen(new PlatformHandle(info.Name, "XRandRMonitorName")) { public Size? PhysicalSize { get; set; } - // Length of a EDID-Block-Length(128 bytes), XRRGetOutputProperty multiplies offset and length by 4 - private const int EDIDStructureLength = 32; public virtual void Refresh() { @@ -26,56 +25,11 @@ internal partial class X11Screens DisplayName = name; IsPrimary = info.IsPrimary; Bounds = new PixelRect(info.X, info.Y, info.Width, info.Height); - - Size? pSize = null; - for (int o = 0; o < info.Outputs.Length; o++) - { - var outputSize = GetPhysicalMonitorSizeFromEDID(info.Outputs[o]); - if (outputSize != null) - { - pSize = outputSize; - break; - } - } - PhysicalSize = pSize; + PhysicalSize = info.PhysicalSize; UpdateWorkArea(); Scaling = scalingProvider.GetScaling(this, id); } - - private unsafe Size? GetPhysicalMonitorSizeFromEDID(IntPtr rrOutput) - { - if (rrOutput == IntPtr.Zero) - return null; - var properties = XRRListOutputProperties(x11.Display, rrOutput, out int propertyCount); - var hasEDID = false; - for (var pc = 0; pc < propertyCount; pc++) - { - if (properties[pc] == x11.Atoms.EDID) - hasEDID = true; - } - - if (!hasEDID) - return null; - XRRGetOutputProperty(x11.Display, rrOutput, x11.Atoms.EDID, 0, EDIDStructureLength, false, false, - x11.Atoms.AnyPropertyType, out IntPtr actualType, out int actualFormat, out int bytesAfter, out _, - out IntPtr prop); - if (actualType != x11.Atoms.XA_INTEGER) - return null; - if (actualFormat != 8) // Expecting an byte array - return null; - - var edid = new byte[bytesAfter]; - Marshal.Copy(prop, edid, 0, bytesAfter); - XFree(prop); - XFree(new IntPtr(properties)); - if (edid.Length < 22) - return null; - var width = edid[21]; // 0x15 1 Max. Horizontal Image Size cm. - var height = edid[22]; // 0x16 1 Max. Vertical Image Size cm. - if (width == 0 && height == 0) - return null; - return new Size(width * 10, height * 10); - } + protected unsafe void UpdateWorkArea() { @@ -132,6 +86,11 @@ internal partial class X11Screens event Action? Changed; X11Screen CreateScreenFromKey(nint key); } + + internal interface IX11RawScreenInfoProviderWithRefreshRate : IX11RawScreenInfoProvider + { + int MaxRefreshRate { get; } + } internal unsafe struct MonitorInfo { @@ -141,10 +100,12 @@ internal partial class X11Screens public int Y; public int Width; public int Height; - public IntPtr[] Outputs; + public int RefreshRate; + public Size? PhysicalSize; + public int SharedRefreshRate; } - private class Randr15ScreensImpl : IX11RawScreenInfoProvider + private class Randr15ScreensImpl : IX11RawScreenInfoProviderWithRefreshRate { private MonitorInfo[]? _cache; private readonly X11Info _x11; @@ -160,17 +121,23 @@ internal partial class X11Screens _x11 = platform.Info; _window = CreateEventWindow(platform, OnEvent); _scalingProvider = GetScalingProvider(platform); - XRRSelectInput(_x11.Display, _window, RandrEventMask.RRScreenChangeNotify); + XRRSelectInput(_x11.Display, _window, + RandrEventMask.RRScreenChangeNotify + | RandrEventMask.RROutputChangeNotifyMask + | RandrEventMask.RROutputPropertyNotifyMask + | RandrEventMask.RRCrtcChangeNotifyMask); + if (_scalingProvider is IScalingProviderWithChanges scalingWithChanges) scalingWithChanges.SettingsChanged += () => Changed?.Invoke(); } private void OnEvent(ref XEvent ev) { - if ((int)ev.type == _x11.RandrEventBase + (int)RandrEvent.RRScreenChangeNotify) + if (((int)ev.type - _x11.RandrEventBase) is (int)RandrEvent.RRScreenChangeNotify or (int)RandrEvent.RRNotify) { _cache = null; - Changed?.Invoke(); + // Delay triggering the update event + Dispatcher.UIThread.Post(() => Changed?.Invoke(), DispatcherPriority.Normal); } } @@ -181,6 +148,7 @@ internal partial class X11Screens if (_cache != null) return _cache; var monitors = XRRGetMonitors(_x11.Display, _window, true, out var count); + var resources = XRRGetScreenResources(_x11.Display, _window); var screens = new MonitorInfo[count]; for (var c = 0; c < count; c++) @@ -201,13 +169,150 @@ internal partial class X11Screens Y = mon.Y, Width = mon.Width, Height = mon.Height, - Outputs = outputs + PhysicalSize = GetPhysicalMonitorSizeFromFirstEligibleOutput(outputs), + SharedRefreshRate = GetSharedRefreshRateForOutputs(resources, outputs) }; } - XFree(new IntPtr(monitors)); + XRRFreeScreenResources(resources); + XRRFreeMonitors(monitors); + + return _cache = screens; + } + } + + private unsafe int GetSharedRefreshRateForOutputs(XRRScreenResources* resources, IntPtr[] outputs) + { + int? minRate = null; + foreach (var output in outputs) + { + var rate = GetRefreshRateForOutput(resources, output); + if (rate.HasValue) + minRate = minRate.HasValue ? Math.Min(minRate.Value, rate.Value) : rate; + } + + return minRate ?? 60; + } + + private unsafe int? GetRefreshRateForOutput(XRRScreenResources* resources, IntPtr output) + { + // Check if output exists in resources + var foundOutput = false; + for (var c = 0; c < resources->noutput; c++) + { + if (resources->outputs[c] == output) + { + foundOutput = true; + break; + } + } + + if (!foundOutput) + return null; + + var outputInfo = XRRGetOutputInfo(_x11.Display, resources, output); + if (outputInfo == null) + return null; + try + { + if (outputInfo->crtc == IntPtr.Zero) + return null; + var crtc = XRRGetCrtcInfo(_x11.Display, resources, outputInfo->crtc); + if (crtc == null) + return null; + try + { + if (crtc->mode == IntPtr.Zero) + return null; + for (var c = 0; c < resources->nmode; c++) + { + var mode = resources->modes[c]; + if (mode.id == crtc->mode) + { + var multiplier = 1d; + if (mode.modeFlags.HasAnyFlag(RRModeFlags.RR_Interlace)) + multiplier *= 2; + if (mode.modeFlags.HasAnyFlag(RRModeFlags.RR_DoubleScan)) + multiplier /= 2; + if (mode.hTotal == 0 || mode.vTotal == 0 || mode.dotClock == 0) + return null; + var hz = mode.dotClock / ((double)mode.hTotal * mode.vTotal) * multiplier; + return (int)Math.Round(hz, MidpointRounding.ToEven); + } + } + } + finally + { + XRRFreeCrtcInfo(crtc); + } + } + finally + { + XRRFreeOutputInfo(outputInfo); + } + + return null; + } + + private Size? GetPhysicalMonitorSizeFromFirstEligibleOutput(IntPtr[] outputs) + { + Size? pSize = null; + for (int o = 0; o < outputs.Length; o++) + { + var outputSize = GetPhysicalMonitorSizeFromEDID(outputs[o]); + if (outputSize != null) + { + pSize = outputSize; + break; + } + } + + return pSize; + } + + private unsafe Size? GetPhysicalMonitorSizeFromEDID(IntPtr rrOutput) + { + if (rrOutput == IntPtr.Zero) + return null; + var properties = XRRListOutputPropertiesAsArray(_x11.Display, rrOutput); + var hasEDID = false; + for (var pc = 0; pc < properties.Length; pc++) + { + if (properties[pc] == _x11.Atoms.EDID) + hasEDID = true; + } + + if (!hasEDID) + return null; + + // Length of a EDID-Block-Length(128 bytes), XRRGetOutputProperty multiplies offset and length by 4 + const int EDIDStructureLength = 32; + XRRGetOutputProperty(_x11.Display, rrOutput, _x11.Atoms.EDID, 0, EDIDStructureLength, false, false, + _x11.Atoms.AnyPropertyType, out IntPtr actualType, out int actualFormat, out int bytesAfter, out _, + out IntPtr prop); + if (actualType != _x11.Atoms.XA_INTEGER) + return null; + if (actualFormat != 8) // Expecting an byte array + return null; + + var edid = new byte[bytesAfter]; + Marshal.Copy(prop, edid, 0, bytesAfter); + XFree(prop); + if (edid.Length < 22) + return null; + var width = edid[21]; // 0x15 1 Max. Horizontal Image Size cm. + var height = edid[22]; // 0x16 1 Max. Vertical Image Size cm. + if (width == 0 && height == 0) + return null; + return new Size(width * 10, height * 10); + } - return screens; + public int MaxRefreshRate + { + get + { + var monitors = MonitorInfos; + return monitors.Length == 0 ? 60 : monitors.Max(x => x.SharedRefreshRate); } } diff --git a/src/Avalonia.X11/Screens/X11Screens.cs b/src/Avalonia.X11/Screens/X11Screens.cs index b8ff80734c..46c8bd150a 100644 --- a/src/Avalonia.X11/Screens/X11Screens.cs +++ b/src/Avalonia.X11/Screens/X11Screens.cs @@ -18,6 +18,10 @@ namespace Avalonia.X11.Screens _impl.Changed += () => Changed?.Invoke(); } + internal int MaxRefreshRate => _impl is IX11RawScreenInfoProviderWithRefreshRate refreshProvider + ? Math.Max(60, refreshProvider.MaxRefreshRate) + : 60; + protected override int GetScreenCount() => _impl.ScreenKeys.Length; protected override IReadOnlyList GetAllScreenKeys() => _impl.ScreenKeys; diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index bebae3f0ae..47a37e3f8c 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -88,6 +88,13 @@ namespace Avalonia.X11 .Bind().ToConstant(new X11PlatformLifetimeEvents(this)); Screens = X11Screens = new X11Screens(this); + + if (options.AllowHighRefreshRate && AvaloniaLocator.Current.GetService() is SleepLoopRenderTimer loopTimer) + { + X11Screens.Changed += () => { loopTimer.DesiredFps = X11Screens.MaxRefreshRate; }; + loopTimer.DesiredFps = X11Screens.MaxRefreshRate; + } + if (Info.XInputVersion != null) { XI2 = XI2Manager.TryCreate(this); @@ -328,6 +335,11 @@ namespace Avalonia /// public bool ShouldRenderOnUIThread { get; set; } + /// + /// Query for display refresh rates from RANDR. May or may not use the refresh rate of your best display. + /// + public bool AllowHighRefreshRate { get; set; } + public IList GlProfiles { get; set; } = new List { new GlVersion(GlProfileType.OpenGL, 4, 0), diff --git a/src/Avalonia.X11/X11Structs.cs b/src/Avalonia.X11/X11Structs.cs index 1682ad4bb5..e15dcc5499 100644 --- a/src/Avalonia.X11/X11Structs.cs +++ b/src/Avalonia.X11/X11Structs.cs @@ -1907,5 +1907,90 @@ namespace Avalonia.X11 { public int MWidth; public int MHeight; public IntPtr* Outputs; - } + } + + internal unsafe struct XRRScreenResources + { + public long timestamp; + public long configTimestamp; + public int ncrtc; + public IntPtr crtcs; + public int noutput; + public IntPtr* outputs; + public int nmode; + public XRRModeInfo* modes; + } + + [Flags] + enum RRModeFlags : ulong + { + RR_HSyncPositive = 0x00000001, + RR_HSyncNegative = 0x00000002, + RR_VSyncPositive = 0x00000004, + RR_VSyncNegative = 0x00000008, + RR_Interlace = 0x00000010, + RR_DoubleScan = 0x00000020, + RR_CSync = 0x00000040, + RR_CSyncPositive = 0x00000080, + RR_CSyncNegative = 0x00000100, + RR_HSkewPresent = 0x00000200, + RR_BCast = 0x00000400, + RR_PixelMultiplex = 0x00000800, + RR_DoubleClock = 0x00001000, + RR_ClockDivideBy2 = 0x00002000, + } + + internal unsafe struct XRRModeInfo + { + public IntPtr id; + public uint width; + public uint height; + public ulong dotClock; + public uint hSyncStart; + public uint hSyncEnd; + public uint hTotal; + public uint hSkew; + public uint vSyncStart; + public uint vSyncEnd; + public uint vTotal; + public byte* name; + public uint nameLength; + public RRModeFlags modeFlags; + } + + internal unsafe struct XRROutputInfo + { + public long timestamp; + public IntPtr crtc; + public char* name; + public int nameLen; + public long mm_width; + public long mm_height; + public ushort connection; + public ushort subpixel_order; + public int ncrtc; + public IntPtr* crtcs; + public int nclone; + public IntPtr* clones; + public int nmode; + public int npreferred; + public IntPtr* modes; + } + + internal unsafe struct XRRCrtcInfo + { + public ulong timestamp; + public int x, y; + public uint width, height; + public IntPtr mode; + public ushort rotation; + public int noutput; + public IntPtr* outputs; + public ushort rotations; + public int npossible; + public IntPtr* possible; + } + + + } diff --git a/src/Avalonia.X11/XLib.Helpers.cs b/src/Avalonia.X11/XLib.Helpers.cs index b32d5e3b26..7fb90e4595 100644 --- a/src/Avalonia.X11/XLib.Helpers.cs +++ b/src/Avalonia.X11/XLib.Helpers.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace Avalonia.X11; @@ -26,4 +27,13 @@ internal static partial class XLib XFree(prop); } } + + public static unsafe IntPtr[] XRRListOutputPropertiesAsArray(IntPtr display, IntPtr output) + { + var pList = XRRListOutputProperties(display, output, out int propertyCount); + var rv = new IntPtr[propertyCount]; + new Span(pList, propertyCount).CopyTo(rv); + XFree(pList); + return rv; + } } \ No newline at end of file diff --git a/src/Avalonia.X11/XLib.cs b/src/Avalonia.X11/XLib.cs index cfd3a03c8f..50d0bff0cf 100644 --- a/src/Avalonia.X11/XLib.cs +++ b/src/Avalonia.X11/XLib.cs @@ -584,6 +584,27 @@ namespace Avalonia.X11 [DllImport(libX11Randr)] public static extern XRRMonitorInfo* XRRGetMonitors(IntPtr dpy, IntPtr window, bool get_active, out int nmonitors); + + [DllImport(libX11Randr)] + public static extern void XRRFreeMonitors(XRRMonitorInfo* monitors); + + [DllImport(libX11Randr)] + public static extern XRRScreenResources * XRRGetScreenResources (IntPtr dpy, IntPtr window); + + [DllImport(libX11Randr)] + public static extern void XRRFreeScreenResources(XRRScreenResources* resources); + + [DllImport(libX11Randr)] + public static extern XRROutputInfo * XRRGetOutputInfo (IntPtr dpy, XRRScreenResources *resources, IntPtr output); + + [DllImport(libX11Randr)] + public static extern void XRRFreeOutputInfo(XRROutputInfo* outputInfo); + + [DllImport(libX11Randr)] + public static extern XRRCrtcInfo* XRRGetCrtcInfo(IntPtr dpy, XRRScreenResources* resources, IntPtr crtc); + + [DllImport(libX11Randr)] + public static extern void XRRFreeCrtcInfo(XRRCrtcInfo* crtcInfo); [DllImport(libX11Randr)] public static extern IntPtr* XRRListOutputProperties(IntPtr dpy, IntPtr output, out int count);