Merge branch 'feature/linux-hidpi' into 'master'
Draft: Implement DPI awareness for X11/Xwayland See merge request [ryubing/ryujinx!165](https://git.ryujinx.app/ryubing/ryujinx/-/merge_requests/165)merge-requests/165/merge
commit
c8ccd9899a
|
|
@ -11,25 +11,10 @@ namespace Ryujinx.Common.SystemInterop
|
|||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool SetProcessDPIAware();
|
||||
|
||||
private const string X11LibraryName = "libX11.so.6";
|
||||
|
||||
[LibraryImport(X11LibraryName)]
|
||||
private static partial nint XOpenDisplay([MarshalAs(UnmanagedType.LPStr)] string display);
|
||||
|
||||
[LibraryImport(X11LibraryName)]
|
||||
private static partial nint XGetDefault(nint display, [MarshalAs(UnmanagedType.LPStr)] string program, [MarshalAs(UnmanagedType.LPStr)] string option);
|
||||
|
||||
[LibraryImport(X11LibraryName)]
|
||||
private static partial int XDisplayWidth(nint display, int screenNumber);
|
||||
|
||||
[LibraryImport(X11LibraryName)]
|
||||
private static partial int XDisplayWidthMM(nint display, int screenNumber);
|
||||
|
||||
[LibraryImport(X11LibraryName)]
|
||||
private static partial int XCloseDisplay(nint display);
|
||||
|
||||
private const double StandardDpiScale = 96.0;
|
||||
private const double MaxScaleFactor = 1.25;
|
||||
private const double MaxScaleFactor = 3.0;
|
||||
|
||||
private static X11Helper.XSettingsListener xSettingsHelper = null;
|
||||
|
||||
/// <summary>
|
||||
/// Marks the application as DPI-Aware when running on the Windows operating system.
|
||||
|
|
@ -43,7 +28,7 @@ namespace Ryujinx.Common.SystemInterop
|
|||
}
|
||||
}
|
||||
|
||||
public static double GetActualScaleFactor()
|
||||
public static void ConfigureDPIScaling(WindowingSystemType windowingSystem)
|
||||
{
|
||||
double userDpiScale = 96.0;
|
||||
|
||||
|
|
@ -55,27 +40,50 @@ namespace Ryujinx.Common.SystemInterop
|
|||
}
|
||||
else if (OperatingSystem.IsLinux())
|
||||
{
|
||||
string xdgSessionType = Environment.GetEnvironmentVariable("XDG_SESSION_TYPE")?.ToLower();
|
||||
|
||||
if (xdgSessionType is null or "x11")
|
||||
if (windowingSystem == WindowingSystemType.X11)
|
||||
{
|
||||
nint display = XOpenDisplay(null);
|
||||
string dpiString = Marshal.PtrToStringAnsi(XGetDefault(display, "Xft", "dpi"));
|
||||
if (dpiString == null || !double.TryParse(dpiString, NumberStyles.Any, CultureInfo.InvariantCulture, out userDpiScale))
|
||||
var avaScaleFactor = Environment.GetEnvironmentVariable("AVALONIA_GLOBAL_SCALE_FACTOR");
|
||||
if (avaScaleFactor is string avaScaleStr &&
|
||||
double.TryParse(avaScaleStr, NumberStyles.Any, CultureInfo.InvariantCulture, out double avaScale) &&
|
||||
avaScale > 0)
|
||||
{
|
||||
userDpiScale = XDisplayWidth(display, 0) * 25.4 / XDisplayWidthMM(display, 0);
|
||||
// userDpiScale = avaScale * 96.0; // TODO: avalonia uses logical size?
|
||||
return userDpiScale;
|
||||
}
|
||||
|
||||
_ = XCloseDisplay(display);
|
||||
}
|
||||
else if (xdgSessionType == "wayland")
|
||||
{
|
||||
// TODO
|
||||
Logger.Warning?.Print(LogClass.Application, "Couldn't determine monitor DPI: Wayland not yet supported");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, $"Couldn't determine monitor DPI: Unrecognised XDG_SESSION_TYPE: {xdgSessionType}");
|
||||
if (xSettingsHelper == null)
|
||||
{
|
||||
var display = X11Helper.XDisplay.Open(null);
|
||||
xSettingsHelper = new X11Helper.XSettingsListener(display);
|
||||
}
|
||||
|
||||
xSettingsHelper.CurrentSettings.TryGetValue("Gdk/UnscaledDPI", out var gdkUnscaledDPI);
|
||||
xSettingsHelper.CurrentSettings.TryGetValue("Gdk/WindowScalingFactor", out var gdkWindowScalingFactor);
|
||||
xSettingsHelper.CurrentSettings.TryGetValue("Xft/DPI", out var xftDpiSetting);
|
||||
|
||||
double scaleFactor = 1.0;
|
||||
|
||||
if (gdkUnscaledDPI?.Type == X11Helper.XSettingType.Integer && gdkWindowScalingFactor?.Type == X11Helper.XSettingType.Integer)
|
||||
{
|
||||
var unscaledDPI = (int)gdkUnscaledDPI.Value / (96d * 1024);
|
||||
var windowScalingFactor = (double)(int)gdkWindowScalingFactor.Value;
|
||||
|
||||
scaleFactor = unscaledDPI * windowScalingFactor;
|
||||
}
|
||||
else
|
||||
{
|
||||
var display = xSettingsHelper.Display;
|
||||
string dpiString = Marshal.PtrToStringAnsi(display.GetDefault("Xft", "dpi"));
|
||||
if (dpiString == null || !double.TryParse(dpiString, NumberStyles.Any, CultureInfo.InvariantCulture, out userDpiScale))
|
||||
{
|
||||
userDpiScale = display.GetWidth(0) * 25.4 / display.GetWidthMM(0);
|
||||
}
|
||||
}
|
||||
|
||||
scaleFactor = Math.Max(scaleFactor, 1.0);
|
||||
// userDpiScale = 96.0 * scaleFactor; // TODO: avalonia uses logical size?
|
||||
|
||||
Environment.SetEnvironmentVariable("AVALONIA_GLOBAL_SCALE_FACTOR", scaleFactor.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -83,15 +91,6 @@ namespace Ryujinx.Common.SystemInterop
|
|||
{
|
||||
Logger.Warning?.Print(LogClass.Application, $"Couldn't determine monitor DPI: {e.Message}");
|
||||
}
|
||||
|
||||
return userDpiScale;
|
||||
}
|
||||
|
||||
public static double GetWindowScaleFactor()
|
||||
{
|
||||
double userDpiScale = GetActualScaleFactor();
|
||||
|
||||
return Math.Min(userDpiScale / StandardDpiScale, MaxScaleFactor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
namespace Ryujinx.Common.SystemInterop
|
||||
{
|
||||
public enum WindowingSystemType
|
||||
{
|
||||
Unknown,
|
||||
Win32,
|
||||
X11,
|
||||
Cocoa,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,533 @@
|
|||
using Ryujinx.Common.Memory;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text;
|
||||
|
||||
namespace Ryujinx.Common.SystemInterop
|
||||
{
|
||||
[SupportedOSPlatform("linux")]
|
||||
public static partial class X11Helper
|
||||
{
|
||||
private const string X11LibraryName = "libX11.so.6";
|
||||
|
||||
[LibraryImport(X11LibraryName)]
|
||||
private static partial nint XOpenDisplay([MarshalAs(UnmanagedType.LPStr)] string display);
|
||||
|
||||
[LibraryImport(X11LibraryName)]
|
||||
private static partial nint XGetDefault(nint display, [MarshalAs(UnmanagedType.LPStr)] string program, [MarshalAs(UnmanagedType.LPStr)] string option);
|
||||
|
||||
[LibraryImport(X11LibraryName)]
|
||||
private static partial int XDisplayWidth(nint display, int screenNumber);
|
||||
|
||||
[LibraryImport(X11LibraryName)]
|
||||
private static partial int XDisplayWidthMM(nint display, int screenNumber);
|
||||
|
||||
[LibraryImport(X11LibraryName)]
|
||||
private static partial int XCloseDisplay(nint display);
|
||||
|
||||
[LibraryImport(X11LibraryName)]
|
||||
private static partial nint XInternAtom(nint display, [MarshalAs(UnmanagedType.LPStr)] string atom_name, [MarshalAs(UnmanagedType.U4)] bool only_if_exists);
|
||||
|
||||
[LibraryImport(X11LibraryName)]
|
||||
private static partial nint XGetSelectionOwner(nint display, nint selection);
|
||||
|
||||
[LibraryImport(X11LibraryName)]
|
||||
private static partial int XGetWindowProperty(nint display, nint window, nint atom, nint long_offset,
|
||||
nint long_length, [MarshalAs(UnmanagedType.U4)] bool delete, nint req_type, out nint actual_type, out int actual_format,
|
||||
out nint nitems, out nint bytes_after, out nint prop);
|
||||
|
||||
[LibraryImport(X11LibraryName)]
|
||||
private static partial int XFree(nint data);
|
||||
|
||||
[LibraryImport(X11LibraryName)]
|
||||
private static partial int XSelectInput(nint display, nint window, nint event_mask);
|
||||
|
||||
[LibraryImport(X11LibraryName)]
|
||||
private static partial int XSync(nint display, [MarshalAs(UnmanagedType.U4)] bool discard);
|
||||
|
||||
[LibraryImport(X11LibraryName)]
|
||||
private static partial int XPending(nint display);
|
||||
|
||||
[LibraryImport(X11LibraryName)]
|
||||
private static partial int XNextEvent(nint display, out XEvent event_return);
|
||||
|
||||
[LibraryImport(X11LibraryName)]
|
||||
private static partial nint XSetErrorHandler(nint handler);
|
||||
|
||||
[LibraryImport(X11LibraryName)]
|
||||
private static partial int XDefaultScreen(nint display);
|
||||
|
||||
// X11 constants
|
||||
private const int PropertyChangeMask = (1 << 22);
|
||||
private const int StructureNotifyMask = (1 << 17);
|
||||
private const int PropertyNotify = 28;
|
||||
private const int DestroyNotify = 17;
|
||||
private const int Success = 0;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct XEvent
|
||||
{
|
||||
public int type;
|
||||
public int pad0;
|
||||
Array23<long> pads;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct XPropertyEvent
|
||||
{
|
||||
public int type;
|
||||
public nuint serial;
|
||||
public int send_event;
|
||||
public nint display;
|
||||
public nint window;
|
||||
public nint atom;
|
||||
public nint time;
|
||||
public int state;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct XDestroyWindowEvent
|
||||
{
|
||||
public int type;
|
||||
public nuint serial;
|
||||
public int send_event;
|
||||
public nint display;
|
||||
public nint eventWindow;
|
||||
public nint window;
|
||||
}
|
||||
|
||||
public enum XSettingType : byte
|
||||
{
|
||||
Integer = 0,
|
||||
String = 1,
|
||||
Color = 2
|
||||
}
|
||||
|
||||
public enum XSettingsByteOrder : byte
|
||||
{
|
||||
LittleEndian = 0,
|
||||
BigEndian = 1,
|
||||
}
|
||||
|
||||
public class XSettingValue
|
||||
{
|
||||
public XSettingType Type { get; set; }
|
||||
public object Value { get; set; }
|
||||
public uint Serial { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Type switch
|
||||
{
|
||||
XSettingType.Integer => $"XInteger({Value})",
|
||||
XSettingType.String => $"XString({Value})",
|
||||
XSettingType.Color => $"XColor{Value}",
|
||||
_ => "Unknown",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class XSettingsMap
|
||||
{
|
||||
private readonly Dictionary<string, XSettingValue> _settings = new();
|
||||
public uint Serial { get; set; }
|
||||
|
||||
public bool TryGetValue(string name, out XSettingValue value)
|
||||
{
|
||||
return _settings.TryGetValue(name, out value);
|
||||
}
|
||||
|
||||
public void Set(string name, XSettingValue value)
|
||||
{
|
||||
_settings[name] = value;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_settings.Clear();
|
||||
}
|
||||
|
||||
public IEnumerable<KeyValuePair<string, XSettingValue>> GetAll()
|
||||
{
|
||||
return _settings;
|
||||
}
|
||||
}
|
||||
|
||||
public class XSettingsListener : IDisposable
|
||||
{
|
||||
private readonly XDisplay _display;
|
||||
private readonly int _screen;
|
||||
private nint _settingsWindow;
|
||||
private nint _settingsAtom;
|
||||
private nint _selectionAtom;
|
||||
private readonly XSettingsMap _currentSettings = new();
|
||||
private readonly List<Action<XSettingsMap>> _listeners = new();
|
||||
|
||||
public XDisplay Display => _display;
|
||||
|
||||
public XSettingsMap CurrentSettings => _currentSettings;
|
||||
|
||||
public XSettingsListener(XDisplay display, int screen = -1)
|
||||
{
|
||||
_display = display;
|
||||
_screen = screen < 0 ? XDefaultScreen(_display.Handle) : screen;
|
||||
|
||||
_selectionAtom = XInternAtom(_display.Handle, $"_XSETTINGS_S{_screen}", false);
|
||||
_settingsAtom = XInternAtom(_display.Handle, "_XSETTINGS_SETTINGS", false);
|
||||
|
||||
if (_selectionAtom == nint.Zero || _settingsAtom == nint.Zero)
|
||||
{
|
||||
throw new Exception("Failed to intern required atoms");
|
||||
}
|
||||
|
||||
InitializeSettings();
|
||||
}
|
||||
|
||||
public void RegisterListener(Action<XSettingsMap> listener)
|
||||
{
|
||||
_listeners.Add(listener);
|
||||
listener?.Invoke(_currentSettings);
|
||||
}
|
||||
|
||||
public void UnregisterListener(Action<XSettingsMap> listener)
|
||||
{
|
||||
_listeners.Remove(listener);
|
||||
}
|
||||
|
||||
public void Poll()
|
||||
{
|
||||
while (XPending(_display.Handle) > 0)
|
||||
{
|
||||
XNextEvent(_display.Handle, out XEvent xevent);
|
||||
|
||||
if (xevent.type == PropertyNotify)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
var propEvent = Marshal.PtrToStructure<XPropertyEvent>(new IntPtr(&xevent));
|
||||
if (propEvent.window == _settingsWindow && propEvent.atom == _settingsAtom)
|
||||
{
|
||||
HandleSettingsChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (xevent.type == DestroyNotify)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
var destroyEvent = Marshal.PtrToStructure<XDestroyWindowEvent>(new IntPtr(&xevent));
|
||||
if (destroyEvent.window == _settingsWindow)
|
||||
{
|
||||
HandleSettingsManagerDestroyed();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeSettings()
|
||||
{
|
||||
_settingsWindow = XGetSelectionOwner(_display.Handle, _selectionAtom);
|
||||
|
||||
if (_settingsWindow == nint.Zero)
|
||||
{
|
||||
_currentSettings.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
XSelectInput(_display.Handle, _settingsWindow, PropertyChangeMask | StructureNotifyMask);
|
||||
XSync(_display.Handle, false);
|
||||
|
||||
ReadSettings();
|
||||
}
|
||||
|
||||
private void ReadSettings()
|
||||
{
|
||||
if (_settingsWindow == nint.Zero)
|
||||
{
|
||||
_currentSettings.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
bool error = false;
|
||||
nint oldHandler = XSetErrorHandler(Marshal.GetFunctionPointerForDelegate<ErrorHandler>((display, errorEvent) =>
|
||||
{
|
||||
error = true;
|
||||
return 0;
|
||||
}));
|
||||
|
||||
try
|
||||
{
|
||||
int result = XGetWindowProperty(
|
||||
_display.Handle,
|
||||
_settingsWindow,
|
||||
_settingsAtom,
|
||||
0,
|
||||
0x7FFFFFFF,
|
||||
false,
|
||||
_settingsAtom,
|
||||
out nint actualType,
|
||||
out int actualFormat,
|
||||
out nint nitems,
|
||||
out nint bytesAfter,
|
||||
out nint prop);
|
||||
|
||||
XSync(_display.Handle, false);
|
||||
|
||||
if (error || result != Success || actualType != _settingsAtom || actualFormat != 8 || prop == nint.Zero)
|
||||
{
|
||||
if (prop != nint.Zero)
|
||||
{
|
||||
XFree(prop);
|
||||
}
|
||||
|
||||
_currentSettings.Clear();
|
||||
_settingsWindow = nint.Zero;
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ParseSettings(prop, (int)nitems);
|
||||
}
|
||||
finally
|
||||
{
|
||||
XFree(prop);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
XSetErrorHandler(oldHandler);
|
||||
}
|
||||
}
|
||||
|
||||
private void ParseSettings(nint data, int length)
|
||||
{
|
||||
if (length < 12)
|
||||
return;
|
||||
|
||||
byte[] bytes = new byte[length];
|
||||
Marshal.Copy(data, bytes, 0, length);
|
||||
|
||||
int offset = 0;
|
||||
|
||||
XSettingsByteOrder byteOrder = (XSettingsByteOrder)bytes[offset++];
|
||||
offset += 3;
|
||||
uint serial = ReadUInt32(bytes, ref offset, byteOrder);
|
||||
uint numSettings = ReadUInt32(bytes, ref offset, byteOrder);
|
||||
|
||||
_currentSettings.Serial = serial;
|
||||
var newSettings = new Dictionary<string, XSettingValue>();
|
||||
|
||||
for (uint i = 0; i < numSettings && offset < length; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
var setting = ReadSetting(bytes, ref offset, byteOrder);
|
||||
if (setting.HasValue)
|
||||
{
|
||||
newSettings[setting.Value.name] = setting.Value.value;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_currentSettings.Clear();
|
||||
foreach (var kvp in newSettings)
|
||||
{
|
||||
_currentSettings.Set(kvp.Key, kvp.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private (string name, XSettingValue value)? ReadSetting(byte[] bytes, ref int offset, XSettingsByteOrder byteOrder)
|
||||
{
|
||||
if (offset >= bytes.Length)
|
||||
return null;
|
||||
|
||||
XSettingType type = (XSettingType)bytes[offset++];
|
||||
offset++;
|
||||
|
||||
ushort nameLen = ReadUInt16(bytes, ref offset, byteOrder);
|
||||
if (offset + nameLen > bytes.Length)
|
||||
return null;
|
||||
|
||||
string name = Encoding.UTF8.GetString(bytes, offset, nameLen);
|
||||
offset += nameLen;
|
||||
offset += Pad(nameLen);
|
||||
|
||||
uint lastChangeSerial = ReadUInt32(bytes, ref offset, byteOrder);
|
||||
|
||||
object value = null;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case XSettingType.Integer:
|
||||
if (offset + 4 > bytes.Length)
|
||||
return null;
|
||||
value = ReadInt32(bytes, ref offset, byteOrder);
|
||||
break;
|
||||
|
||||
case XSettingType.String:
|
||||
if (offset + 4 > bytes.Length)
|
||||
return null;
|
||||
uint strLen = ReadUInt32(bytes, ref offset, byteOrder);
|
||||
if (offset + strLen > bytes.Length)
|
||||
return null;
|
||||
value = Encoding.UTF8.GetString(bytes, offset, (int)strLen);
|
||||
offset += (int)strLen;
|
||||
offset += Pad((int)strLen);
|
||||
break;
|
||||
|
||||
case XSettingType.Color:
|
||||
if (offset + 8 > bytes.Length)
|
||||
return null;
|
||||
ushort red = ReadUInt16(bytes, ref offset, byteOrder);
|
||||
ushort green = ReadUInt16(bytes, ref offset, byteOrder);
|
||||
ushort blue = ReadUInt16(bytes, ref offset, byteOrder);
|
||||
ushort alpha = ReadUInt16(bytes, ref offset, byteOrder);
|
||||
value = (red, green, blue, alpha);
|
||||
break;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
return (name, new XSettingValue
|
||||
{
|
||||
Type = type,
|
||||
Value = value,
|
||||
Serial = lastChangeSerial
|
||||
});
|
||||
}
|
||||
|
||||
private static int Pad(int n)
|
||||
{
|
||||
return ((n + 3) & ~3) - n;
|
||||
}
|
||||
|
||||
// all of the read methods assume the system is little-endian - rest of the emulator won't likely even work on big-endian systems
|
||||
|
||||
private static uint ReadUInt32(byte[] bytes, ref int offset, XSettingsByteOrder byteOrder)
|
||||
{
|
||||
uint value = BitConverter.ToUInt32(bytes, offset);
|
||||
offset += 4;
|
||||
if (byteOrder != XSettingsByteOrder.LittleEndian)
|
||||
{
|
||||
return ReverseBytes(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static int ReadInt32(byte[] bytes, ref int offset, XSettingsByteOrder byteOrder)
|
||||
{
|
||||
return (int)ReadUInt32(bytes, ref offset, byteOrder);
|
||||
}
|
||||
|
||||
private static ushort ReadUInt16(byte[] bytes, ref int offset, XSettingsByteOrder byteOrder)
|
||||
{
|
||||
ushort value = BitConverter.ToUInt16(bytes, offset);
|
||||
offset += 2;
|
||||
if (byteOrder != XSettingsByteOrder.LittleEndian)
|
||||
{
|
||||
return ReverseBytes(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static uint ReverseBytes(uint value)
|
||||
{
|
||||
return ((value & 0x000000FFU) << 24) |
|
||||
((value & 0x0000FF00U) << 8) |
|
||||
((value & 0x00FF0000U) >> 8) |
|
||||
((value & 0xFF000000U) >> 24);
|
||||
}
|
||||
|
||||
private static ushort ReverseBytes(ushort value)
|
||||
{
|
||||
return (ushort)(((value & 0x00FF) << 8) | ((value & 0xFF00) >> 8));
|
||||
}
|
||||
|
||||
private void HandleSettingsChange()
|
||||
{
|
||||
ReadSettings();
|
||||
NotifyListeners();
|
||||
}
|
||||
|
||||
private void HandleSettingsManagerDestroyed()
|
||||
{
|
||||
_currentSettings.Clear();
|
||||
_settingsWindow = nint.Zero;
|
||||
|
||||
InitializeSettings();
|
||||
NotifyListeners();
|
||||
}
|
||||
|
||||
private void NotifyListeners()
|
||||
{
|
||||
foreach (var listener in _listeners)
|
||||
{
|
||||
try
|
||||
{
|
||||
listener?.Invoke(_currentSettings);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_display.Dispose();
|
||||
}
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate int ErrorHandler(nint display, nint errorEvent);
|
||||
}
|
||||
|
||||
public struct XDisplay : IDisposable
|
||||
{
|
||||
public nint Handle;
|
||||
|
||||
public static XDisplay Open(string display = null)
|
||||
{
|
||||
nint handle = XOpenDisplay(display);
|
||||
if (handle == nint.Zero)
|
||||
{
|
||||
throw new Exception("Couldn't open X11 display.");
|
||||
}
|
||||
|
||||
return new XDisplay { Handle = handle };
|
||||
}
|
||||
|
||||
public nint GetDefault(string program, string option)
|
||||
{
|
||||
return XGetDefault(Handle, program, option);
|
||||
}
|
||||
|
||||
public int GetWidth(int screenNumber)
|
||||
{
|
||||
return XDisplayWidth(Handle, screenNumber);
|
||||
}
|
||||
|
||||
public int GetWidthMM(int screenNumber)
|
||||
{
|
||||
return XDisplayWidthMM(Handle, screenNumber);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Handle != nint.Zero)
|
||||
{
|
||||
_ = XCloseDisplay(Handle);
|
||||
Handle = nint.Zero;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -30,7 +30,6 @@ namespace Ryujinx.Ava
|
|||
{
|
||||
internal partial class Program
|
||||
{
|
||||
public static double WindowScaleFactor { get; set; }
|
||||
public static double DesktopScaleFactor { get; set; } = 1.0;
|
||||
public static string Version { get; private set; }
|
||||
public static string ConfigurationPath { get; private set; }
|
||||
|
|
@ -63,7 +62,7 @@ namespace Ryujinx.Ava
|
|||
return 0;
|
||||
}
|
||||
|
||||
Initialize(args);
|
||||
Initialize(args, out var builder);
|
||||
|
||||
LoggerAdapter.Register();
|
||||
|
||||
|
|
@ -71,10 +70,10 @@ namespace Ryujinx.Ava
|
|||
.Register<FontAwesomeIconProvider>()
|
||||
.Register<MaterialDesignIconProvider>();
|
||||
|
||||
return BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
||||
return builder.StartWithClassicDesktopLifetime(args);
|
||||
}
|
||||
|
||||
public static AppBuilder BuildAvaloniaApp() =>
|
||||
private static AppBuilder BuildAvaloniaApp() =>
|
||||
AppBuilder.Configure<RyujinxApp>()
|
||||
.UsePlatformDetect()
|
||||
.With(new X11PlatformOptions
|
||||
|
|
@ -94,8 +93,10 @@ namespace Ryujinx.Ava
|
|||
: [Win32RenderingMode.Software]
|
||||
});
|
||||
|
||||
private static void Initialize(string[] args)
|
||||
private static void Initialize(string[] args, out AppBuilder builder)
|
||||
{
|
||||
builder = BuildAvaloniaApp();
|
||||
|
||||
// Ensure Discord presence timestamp begins at the absolute start of when Ryujinx is launched
|
||||
DiscordIntegrationModule.EmulatorStartedAt = Timestamps.Now;
|
||||
|
||||
|
|
@ -136,7 +137,7 @@ namespace Ryujinx.Ava
|
|||
|
||||
ReloadConfig();
|
||||
|
||||
WindowScaleFactor = ForceDpiAware.GetWindowScaleFactor();
|
||||
ForceDpiAware.ConfigureDPIScaling(builder.GetWindowingSystemType());
|
||||
|
||||
// Logging system information.
|
||||
PrintSystemInfo();
|
||||
|
|
|
|||
|
|
@ -232,11 +232,11 @@ namespace Ryujinx.Ava.UI.Views.Main
|
|||
);
|
||||
|
||||
// Correctly size window when 'TitleBar' is enabled (Nov. 14, 2024)
|
||||
double barsHeight = ((Window.StatusBarHeight + Window.MenuBarHeight) +
|
||||
(ConfigurationState.Instance.ShowOldUI ? (int)Window.TitleBar.Height : 0));
|
||||
double barsHeight = Window.StatusBarHeight + Window.MenuBarHeight +
|
||||
(ConfigurationState.Instance.ShowOldUI ? (int)Window.TitleBar.Height : 0);
|
||||
|
||||
double windowWidthScaled = (resolutionWidth * Program.WindowScaleFactor);
|
||||
double windowHeightScaled = ((resolutionHeight + barsHeight) * Program.WindowScaleFactor);
|
||||
double windowWidthScaled = resolutionWidth;
|
||||
double windowHeightScaled = resolutionHeight + barsHeight;
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -440,8 +440,8 @@ namespace Ryujinx.Ava.UI.Windows
|
|||
if (!ConfigurationState.Instance.RememberWindowState)
|
||||
{
|
||||
// Correctly size window when 'TitleBar' is enabled (Nov. 14, 2024)
|
||||
ViewModel.WindowHeight = (720 + StatusBarHeight + MenuBarHeight + TitleBarHeight) * Program.WindowScaleFactor;
|
||||
ViewModel.WindowWidth = 1280 * Program.WindowScaleFactor;
|
||||
ViewModel.WindowHeight = 720 + StatusBarHeight + MenuBarHeight + TitleBarHeight;
|
||||
ViewModel.WindowWidth = 1280;
|
||||
|
||||
WindowState = WindowState.Normal;
|
||||
WindowStartupLocation = WindowStartupLocation.CenterScreen;
|
||||
|
|
@ -452,8 +452,8 @@ namespace Ryujinx.Ava.UI.Windows
|
|||
PixelPoint savedPoint = new(ConfigurationState.Instance.UI.WindowStartup.WindowPositionX,
|
||||
ConfigurationState.Instance.UI.WindowStartup.WindowPositionY);
|
||||
|
||||
ViewModel.WindowHeight = ConfigurationState.Instance.UI.WindowStartup.WindowSizeHeight * Program.WindowScaleFactor;
|
||||
ViewModel.WindowWidth = ConfigurationState.Instance.UI.WindowStartup.WindowSizeWidth * Program.WindowScaleFactor;
|
||||
ViewModel.WindowHeight = ConfigurationState.Instance.UI.WindowStartup.WindowSizeHeight;
|
||||
ViewModel.WindowWidth = ConfigurationState.Instance.UI.WindowStartup.WindowSizeWidth;
|
||||
|
||||
ViewModel.WindowState = ConfigurationState.Instance.UI.WindowStartup.WindowMaximized.Value ? WindowState.Maximized : WindowState.Normal;
|
||||
|
||||
|
|
@ -475,10 +475,8 @@ namespace Ryujinx.Ava.UI.Windows
|
|||
// Only save rectangle properties if the window is not in a maximized state.
|
||||
if (WindowState != WindowState.Maximized)
|
||||
{
|
||||
// Since scaling is being applied to the loaded settings from disk (see SetWindowSizePosition() above), scaling should be removed from width/height before saving out to disk
|
||||
// as well - otherwise anyone not using a 1.0 scale factor their window will increase in size with every subsequent launch of the program when scaling is applied (Nov. 14, 2024)
|
||||
ConfigurationState.Instance.UI.WindowStartup.WindowSizeHeight.Value = (int)(Height / Program.WindowScaleFactor);
|
||||
ConfigurationState.Instance.UI.WindowStartup.WindowSizeWidth.Value = (int)(Width / Program.WindowScaleFactor);
|
||||
ConfigurationState.Instance.UI.WindowStartup.WindowSizeHeight.Value = (int)Height;
|
||||
ConfigurationState.Instance.UI.WindowStartup.WindowSizeWidth.Value = (int)Width;
|
||||
|
||||
ConfigurationState.Instance.UI.WindowStartup.WindowPositionX.Value = Position.X;
|
||||
ConfigurationState.Instance.UI.WindowStartup.WindowPositionY.Value = Position.Y;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
using Avalonia;
|
||||
using Ryujinx.Common.SystemInterop;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Ava.Utilities
|
||||
{
|
||||
public static class WindowingSystemTypeExtensions
|
||||
{
|
||||
public static WindowingSystemType GetWindowingSystemType(this AppBuilder builder)
|
||||
{
|
||||
// null means uninitialized, "" means the name isn't set.
|
||||
if (builder?.WindowingSubsystemName is null)
|
||||
{
|
||||
throw new InvalidOperationException("The windowing subsystem must be configured before calling this method.");
|
||||
}
|
||||
|
||||
if (builder.WindowingSubsystemName == "")
|
||||
{
|
||||
// TODO: They forget to set the WindowingSubsystemName for X11, we assume it's X11 because every other Linux backend sets it.
|
||||
// https://github.com/AvaloniaUI/Avalonia/blob/22c4c630ce5910343006fd58d611b286ed87c740/src/Avalonia.X11/X11Platform.cs#L414
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
return WindowingSystemType.X11;
|
||||
}
|
||||
|
||||
// TODO: Same for Cocoa.
|
||||
// https://github.com/AvaloniaUI/Avalonia/blob/22c4c630ce5910343006fd58d611b286ed87c740/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs#L26
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
return WindowingSystemType.Cocoa;
|
||||
}
|
||||
}
|
||||
|
||||
return builder.WindowingSubsystemName switch
|
||||
{
|
||||
"Win32" => WindowingSystemType.Win32,
|
||||
"X11" => WindowingSystemType.X11,
|
||||
"Cocoa" => WindowingSystemType.Cocoa,
|
||||
_ => WindowingSystemType.Unknown,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue