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)]
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
private static partial bool SetProcessDPIAware();
|
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 StandardDpiScale = 96.0;
|
||||||
private const double MaxScaleFactor = 1.25;
|
private const double MaxScaleFactor = 3.0;
|
||||||
|
|
||||||
|
private static X11Helper.XSettingsListener xSettingsHelper = null;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Marks the application as DPI-Aware when running on the Windows operating system.
|
/// 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;
|
double userDpiScale = 96.0;
|
||||||
|
|
||||||
|
|
@ -55,27 +40,50 @@ namespace Ryujinx.Common.SystemInterop
|
||||||
}
|
}
|
||||||
else if (OperatingSystem.IsLinux())
|
else if (OperatingSystem.IsLinux())
|
||||||
{
|
{
|
||||||
string xdgSessionType = Environment.GetEnvironmentVariable("XDG_SESSION_TYPE")?.ToLower();
|
if (windowingSystem == WindowingSystemType.X11)
|
||||||
|
|
||||||
if (xdgSessionType is null or "x11")
|
|
||||||
{
|
{
|
||||||
nint display = XOpenDisplay(null);
|
var avaScaleFactor = Environment.GetEnvironmentVariable("AVALONIA_GLOBAL_SCALE_FACTOR");
|
||||||
string dpiString = Marshal.PtrToStringAnsi(XGetDefault(display, "Xft", "dpi"));
|
if (avaScaleFactor is string avaScaleStr &&
|
||||||
if (dpiString == null || !double.TryParse(dpiString, NumberStyles.Any, CultureInfo.InvariantCulture, out userDpiScale))
|
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);
|
if (xSettingsHelper == null)
|
||||||
}
|
|
||||||
else if (xdgSessionType == "wayland")
|
|
||||||
{
|
{
|
||||||
// TODO
|
var display = X11Helper.XDisplay.Open(null);
|
||||||
Logger.Warning?.Print(LogClass.Application, "Couldn't determine monitor DPI: Wayland not yet supported");
|
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
|
else
|
||||||
{
|
{
|
||||||
Logger.Warning?.Print(LogClass.Application, $"Couldn't determine monitor DPI: Unrecognised XDG_SESSION_TYPE: {xdgSessionType}");
|
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}");
|
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
|
internal partial class Program
|
||||||
{
|
{
|
||||||
public static double WindowScaleFactor { get; set; }
|
|
||||||
public static double DesktopScaleFactor { get; set; } = 1.0;
|
public static double DesktopScaleFactor { get; set; } = 1.0;
|
||||||
public static string Version { get; private set; }
|
public static string Version { get; private set; }
|
||||||
public static string ConfigurationPath { get; private set; }
|
public static string ConfigurationPath { get; private set; }
|
||||||
|
|
@ -63,7 +62,7 @@ namespace Ryujinx.Ava
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Initialize(args);
|
Initialize(args, out var builder);
|
||||||
|
|
||||||
LoggerAdapter.Register();
|
LoggerAdapter.Register();
|
||||||
|
|
||||||
|
|
@ -71,10 +70,10 @@ namespace Ryujinx.Ava
|
||||||
.Register<FontAwesomeIconProvider>()
|
.Register<FontAwesomeIconProvider>()
|
||||||
.Register<MaterialDesignIconProvider>();
|
.Register<MaterialDesignIconProvider>();
|
||||||
|
|
||||||
return BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
return builder.StartWithClassicDesktopLifetime(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static AppBuilder BuildAvaloniaApp() =>
|
private static AppBuilder BuildAvaloniaApp() =>
|
||||||
AppBuilder.Configure<RyujinxApp>()
|
AppBuilder.Configure<RyujinxApp>()
|
||||||
.UsePlatformDetect()
|
.UsePlatformDetect()
|
||||||
.With(new X11PlatformOptions
|
.With(new X11PlatformOptions
|
||||||
|
|
@ -94,8 +93,10 @@ namespace Ryujinx.Ava
|
||||||
: [Win32RenderingMode.Software]
|
: [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
|
// Ensure Discord presence timestamp begins at the absolute start of when Ryujinx is launched
|
||||||
DiscordIntegrationModule.EmulatorStartedAt = Timestamps.Now;
|
DiscordIntegrationModule.EmulatorStartedAt = Timestamps.Now;
|
||||||
|
|
||||||
|
|
@ -136,7 +137,7 @@ namespace Ryujinx.Ava
|
||||||
|
|
||||||
ReloadConfig();
|
ReloadConfig();
|
||||||
|
|
||||||
WindowScaleFactor = ForceDpiAware.GetWindowScaleFactor();
|
ForceDpiAware.ConfigureDPIScaling(builder.GetWindowingSystemType());
|
||||||
|
|
||||||
// Logging system information.
|
// Logging system information.
|
||||||
PrintSystemInfo();
|
PrintSystemInfo();
|
||||||
|
|
|
||||||
|
|
@ -232,11 +232,11 @@ namespace Ryujinx.Ava.UI.Views.Main
|
||||||
);
|
);
|
||||||
|
|
||||||
// Correctly size window when 'TitleBar' is enabled (Nov. 14, 2024)
|
// Correctly size window when 'TitleBar' is enabled (Nov. 14, 2024)
|
||||||
double barsHeight = ((Window.StatusBarHeight + Window.MenuBarHeight) +
|
double barsHeight = Window.StatusBarHeight + Window.MenuBarHeight +
|
||||||
(ConfigurationState.Instance.ShowOldUI ? (int)Window.TitleBar.Height : 0));
|
(ConfigurationState.Instance.ShowOldUI ? (int)Window.TitleBar.Height : 0);
|
||||||
|
|
||||||
double windowWidthScaled = (resolutionWidth * Program.WindowScaleFactor);
|
double windowWidthScaled = resolutionWidth;
|
||||||
double windowHeightScaled = ((resolutionHeight + barsHeight) * Program.WindowScaleFactor);
|
double windowHeightScaled = resolutionHeight + barsHeight;
|
||||||
|
|
||||||
Dispatcher.UIThread.Post(() =>
|
Dispatcher.UIThread.Post(() =>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -440,8 +440,8 @@ namespace Ryujinx.Ava.UI.Windows
|
||||||
if (!ConfigurationState.Instance.RememberWindowState)
|
if (!ConfigurationState.Instance.RememberWindowState)
|
||||||
{
|
{
|
||||||
// Correctly size window when 'TitleBar' is enabled (Nov. 14, 2024)
|
// Correctly size window when 'TitleBar' is enabled (Nov. 14, 2024)
|
||||||
ViewModel.WindowHeight = (720 + StatusBarHeight + MenuBarHeight + TitleBarHeight) * Program.WindowScaleFactor;
|
ViewModel.WindowHeight = 720 + StatusBarHeight + MenuBarHeight + TitleBarHeight;
|
||||||
ViewModel.WindowWidth = 1280 * Program.WindowScaleFactor;
|
ViewModel.WindowWidth = 1280;
|
||||||
|
|
||||||
WindowState = WindowState.Normal;
|
WindowState = WindowState.Normal;
|
||||||
WindowStartupLocation = WindowStartupLocation.CenterScreen;
|
WindowStartupLocation = WindowStartupLocation.CenterScreen;
|
||||||
|
|
@ -452,8 +452,8 @@ namespace Ryujinx.Ava.UI.Windows
|
||||||
PixelPoint savedPoint = new(ConfigurationState.Instance.UI.WindowStartup.WindowPositionX,
|
PixelPoint savedPoint = new(ConfigurationState.Instance.UI.WindowStartup.WindowPositionX,
|
||||||
ConfigurationState.Instance.UI.WindowStartup.WindowPositionY);
|
ConfigurationState.Instance.UI.WindowStartup.WindowPositionY);
|
||||||
|
|
||||||
ViewModel.WindowHeight = ConfigurationState.Instance.UI.WindowStartup.WindowSizeHeight * Program.WindowScaleFactor;
|
ViewModel.WindowHeight = ConfigurationState.Instance.UI.WindowStartup.WindowSizeHeight;
|
||||||
ViewModel.WindowWidth = ConfigurationState.Instance.UI.WindowStartup.WindowSizeWidth * Program.WindowScaleFactor;
|
ViewModel.WindowWidth = ConfigurationState.Instance.UI.WindowStartup.WindowSizeWidth;
|
||||||
|
|
||||||
ViewModel.WindowState = ConfigurationState.Instance.UI.WindowStartup.WindowMaximized.Value ? WindowState.Maximized : WindowState.Normal;
|
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.
|
// Only save rectangle properties if the window is not in a maximized state.
|
||||||
if (WindowState != WindowState.Maximized)
|
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
|
ConfigurationState.Instance.UI.WindowStartup.WindowSizeHeight.Value = (int)Height;
|
||||||
// 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.WindowSizeWidth.Value = (int)Width;
|
||||||
ConfigurationState.Instance.UI.WindowStartup.WindowSizeHeight.Value = (int)(Height / Program.WindowScaleFactor);
|
|
||||||
ConfigurationState.Instance.UI.WindowStartup.WindowSizeWidth.Value = (int)(Width / Program.WindowScaleFactor);
|
|
||||||
|
|
||||||
ConfigurationState.Instance.UI.WindowStartup.WindowPositionX.Value = Position.X;
|
ConfigurationState.Instance.UI.WindowStartup.WindowPositionX.Value = Position.X;
|
||||||
ConfigurationState.Instance.UI.WindowStartup.WindowPositionY.Value = Position.Y;
|
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