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
Alula 2025-10-27 16:01:24 -05:00
commit c8ccd9899a
7 changed files with 647 additions and 63 deletions

View File

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

View File

@ -0,0 +1,10 @@
namespace Ryujinx.Common.SystemInterop
{
public enum WindowingSystemType
{
Unknown,
Win32,
X11,
Cocoa,
}
}

View File

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

View File

@ -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();

View File

@ -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(() =>
{ {

View File

@ -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;

View File

@ -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,
};
}
}
}