Browse Source

[X11] Implemented automatic fps detection

pull/18558/head
Nikita Tsukanov 12 months ago
parent
commit
5737b95d88
  1. 19
      src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs
  2. 219
      src/Avalonia.X11/Screens/X11Screen.Providers.cs
  3. 4
      src/Avalonia.X11/Screens/X11Screens.cs
  4. 12
      src/Avalonia.X11/X11Platform.cs
  5. 87
      src/Avalonia.X11/X11Structs.cs
  6. 10
      src/Avalonia.X11/XLib.Helpers.cs
  7. 21
      src/Avalonia.X11/XLib.cs

19
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<TimeSpan> 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)

219
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);
}
}

4
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<nint> GetAllScreenKeys() => _impl.ScreenKeys;

12
src/Avalonia.X11/X11Platform.cs

@ -88,6 +88,13 @@ namespace Avalonia.X11
.Bind<IPlatformLifetimeEventsImpl>().ToConstant(new X11PlatformLifetimeEvents(this));
Screens = X11Screens = new X11Screens(this);
if (options.AllowHighRefreshRate && AvaloniaLocator.Current.GetService<IRenderTimer>() 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
/// </summary>
public bool ShouldRenderOnUIThread { get; set; }
/// <summary>
/// Query for display refresh rates from RANDR. May or may not use the refresh rate of your best display.
/// </summary>
public bool AllowHighRefreshRate { get; set; }
public IList<GlVersion> GlProfiles { get; set; } = new List<GlVersion>
{
new GlVersion(GlProfileType.OpenGL, 4, 0),

87
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;
}
}

10
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<IntPtr>(pList, propertyCount).CopyTo(rv);
XFree(pList);
return rv;
}
}

21
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);

Loading…
Cancel
Save