From 35d91a0e58cb0b2916b7a4f138c63fcc12b71112 Mon Sep 17 00:00:00 2001
From: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
Date: Tue, 30 May 2023 01:48:37 +0200
Subject: [PATCH] Linux: Automatically increase vm.max_map_count if it's too
low (#4702)
* memory: Check results of pinvoke calls
* Increase vm.max_map_count when running Ryujinx
* Add SupportedOSPlatform attribute for WindowsApiException
* Revert increasing vm.max_map_count via script
* Add LinuxHelper to detect and increase vm.max_map_count
With GUI dialogs, this should be a bit more user-friendly.
* Supply arguments as a list to RunPkExec
* Add error logging in case RunPkExec() fails
* Prevent Gtk from crashing
---
distribution/linux/Ryujinx.sh | 2 +-
src/Ryujinx.Ava/Assets/Locales/en_US.json | 7 ++
src/Ryujinx.Ava/Ryujinx.Ava.csproj | 3 +-
.../UI/Helpers/ContentDialogHelper.cs | 7 +-
.../UI/Windows/MainWindow.axaml.cs | 74 ++++++++++++++++++-
src/Ryujinx.Memory/MemoryManagementUnix.cs | 29 ++++++--
src/Ryujinx.Memory/MemoryManagementWindows.cs | 5 +-
src/Ryujinx.Memory/MemoryManagerUnixHelper.cs | 5 ++
.../MemoryProtectionException.cs | 3 +-
.../WindowsShared/WindowsApi.cs | 2 +
.../WindowsShared/WindowsApiException.cs | 2 +
src/Ryujinx.Ui.Common/Helper/LinuxHelper.cs | 62 ++++++++++++++++
src/Ryujinx/Program.cs | 65 ++++++++++++++++
src/Ryujinx/Ryujinx.csproj | 3 +-
14 files changed, 252 insertions(+), 17 deletions(-)
create mode 100644 src/Ryujinx.Ui.Common/Helper/LinuxHelper.cs
diff --git a/distribution/linux/Ryujinx.sh b/distribution/linux/Ryujinx.sh
index a80cdcaec..f356cad01 100644
--- a/distribution/linux/Ryujinx.sh
+++ b/distribution/linux/Ryujinx.sh
@@ -17,4 +17,4 @@ if command -v gamemoderun > /dev/null 2>&1; then
COMMAND="$COMMAND gamemoderun"
fi
-$COMMAND "$SCRIPT_DIR/$RYUJINX_BIN" "$@"
+$COMMAND "$SCRIPT_DIR/$RYUJINX_BIN" "$@"
\ No newline at end of file
diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json
index 03cddcc84..a68f91975 100644
--- a/src/Ryujinx.Ava/Assets/Locales/en_US.json
+++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json
@@ -74,6 +74,13 @@
"GameListContextMenuExtractDataLogoToolTip": "Extract the Logo section from Application's current config (including updates)",
"StatusBarGamesLoaded": "{0}/{1} Games Loaded",
"StatusBarSystemVersion": "System Version: {0}",
+ "LinuxVmMaxMapCountDialogTitle": "Low limit for memory mappings detected",
+ "LinuxVmMaxMapCountDialogTextPrimary": "Would you like to increase the value of vm.max_map_count to {0}",
+ "LinuxVmMaxMapCountDialogTextSecondary": "Some games might try to create more memory mappings than currently allowed. Ryujinx will crash as soon as this limit gets exceeded.",
+ "LinuxVmMaxMapCountDialogButtonUntilRestart": "Yes, until the next restart",
+ "LinuxVmMaxMapCountDialogButtonPersistent": "Yes, permanently",
+ "LinuxVmMaxMapCountWarningTextPrimary": "Max amount of memory mappings is lower than recommended.",
+ "LinuxVmMaxMapCountWarningTextSecondary": "The current value of vm.max_map_count ({0}) is lower than {1}. Some games might try to create more memory mappings than currently allowed. Ryujinx will crash as soon as this limit gets exceeded.\n\nYou might want to either manually increase the limit or install pkexec, which allows Ryujinx to assist with that.",
"Settings": "Settings",
"SettingsTabGeneral": "User Interface",
"SettingsTabGeneralGeneral": "General",
diff --git a/src/Ryujinx.Ava/Ryujinx.Ava.csproj b/src/Ryujinx.Ava/Ryujinx.Ava.csproj
index 2b6432e9b..1fac5400e 100644
--- a/src/Ryujinx.Ava/Ryujinx.Ava.csproj
+++ b/src/Ryujinx.Ava/Ryujinx.Ava.csproj
@@ -18,6 +18,7 @@
true
+ false
true
partial
@@ -147,4 +148,4 @@
-
+
\ No newline at end of file
diff --git a/src/Ryujinx.Ava/UI/Helpers/ContentDialogHelper.cs b/src/Ryujinx.Ava/UI/Helpers/ContentDialogHelper.cs
index cb474506b..d85895fc8 100644
--- a/src/Ryujinx.Ava/UI/Helpers/ContentDialogHelper.cs
+++ b/src/Ryujinx.Ava/UI/Helpers/ContentDialogHelper.cs
@@ -6,7 +6,6 @@ using Avalonia.Threading;
using FluentAvalonia.Core;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale;
-using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common.Logging;
using System;
@@ -19,7 +18,7 @@ namespace Ryujinx.Ava.UI.Helpers
{
private static bool _isChoiceDialogOpen;
- public async static Task ShowContentDialog(
+ private async static Task ShowContentDialog(
string title,
object content,
string primaryButton,
@@ -67,7 +66,7 @@ namespace Ryujinx.Ava.UI.Helpers
return result;
}
- private async static Task ShowTextDialog(
+ public async static Task ShowTextDialog(
string title,
string primaryText,
string secondaryText,
@@ -319,7 +318,7 @@ namespace Ryujinx.Ava.UI.Helpers
Window parent = GetMainWindow();
- if (parent != null && parent.IsActive && parent is MainWindow window && window.ViewModel.IsGameRunning)
+ if (parent is { IsActive: true } and MainWindow window && window.ViewModel.IsGameRunning)
{
contentDialogOverlayWindow = new()
{
diff --git a/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs
index eec77479e..cf84807e3 100644
--- a/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs
+++ b/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs
@@ -23,6 +23,7 @@ using Ryujinx.Ui.Common.Helper;
using System;
using System.ComponentModel;
using System.IO;
+using System.Runtime.Versioning;
using System.Threading.Tasks;
using InputManager = Ryujinx.Input.HLE.InputManager;
@@ -258,7 +259,64 @@ namespace Ryujinx.Ava.UI.Windows
ApplicationHelper.Initialize(VirtualFileSystem, AccountManager, LibHacHorizonManager.RyujinxClient, this);
}
- protected void CheckLaunchState()
+ [SupportedOSPlatform("linux")]
+ private static async void ShowVmMaxMapCountWarning()
+ {
+ LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LinuxVmMaxMapCountWarningTextSecondary,
+ LinuxHelper.VmMaxMapCount, LinuxHelper.RecommendedVmMaxMapCount);
+
+ await ContentDialogHelper.CreateWarningDialog(
+ LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountWarningTextPrimary],
+ LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountWarningTextSecondary]
+ );
+ }
+
+ [SupportedOSPlatform("linux")]
+ private static async void ShowVmMaxMapCountDialog()
+ {
+ LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LinuxVmMaxMapCountDialogTextPrimary,
+ LinuxHelper.RecommendedVmMaxMapCount);
+
+ UserResult response = await ContentDialogHelper.ShowTextDialog(
+ $"Ryujinx - {LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountDialogTitle]}",
+ LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountDialogTextPrimary],
+ LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountDialogTextSecondary],
+ LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountDialogButtonUntilRestart],
+ LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountDialogButtonPersistent],
+ LocaleManager.Instance[LocaleKeys.InputDialogNo],
+ (int)Symbol.Help
+ );
+
+ int rc;
+
+ switch (response)
+ {
+ case UserResult.Ok:
+ rc = LinuxHelper.RunPkExec($"echo {LinuxHelper.RecommendedVmMaxMapCount} > {LinuxHelper.VmMaxMapCountPath}");
+ if (rc == 0)
+ {
+ Logger.Info?.Print(LogClass.Application, $"vm.max_map_count set to {LinuxHelper.VmMaxMapCount} until the next restart.");
+ }
+ else
+ {
+ Logger.Error?.Print(LogClass.Application, $"Unable to change vm.max_map_count. Process exited with code: {rc}");
+ }
+ break;
+ case UserResult.No:
+ rc = LinuxHelper.RunPkExec($"echo \"vm.max_map_count = {LinuxHelper.RecommendedVmMaxMapCount}\" > {LinuxHelper.SysCtlConfigPath} && sysctl -p {LinuxHelper.SysCtlConfigPath}");
+ if (rc == 0)
+ {
+ Logger.Info?.Print(LogClass.Application, $"vm.max_map_count set to {LinuxHelper.VmMaxMapCount}. Written to config: {LinuxHelper.SysCtlConfigPath}");
+ }
+ else
+ {
+ Logger.Error?.Print(LogClass.Application, $"Unable to write new value for vm.max_map_count to config. Process exited with code: {rc}");
+ }
+ break;
+ }
+ }
+
+ private void CheckLaunchState()
{
if (ShowKeyErrorOnLoad)
{
@@ -268,6 +326,20 @@ namespace Ryujinx.Ava.UI.Windows
UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys, this));
}
+ if (OperatingSystem.IsLinux() && LinuxHelper.VmMaxMapCount < LinuxHelper.RecommendedVmMaxMapCount)
+ {
+ Logger.Warning?.Print(LogClass.Application, $"The value of vm.max_map_count is lower than {LinuxHelper.RecommendedVmMaxMapCount}. ({LinuxHelper.VmMaxMapCount})");
+
+ if (LinuxHelper.PkExecPath is not null)
+ {
+ Dispatcher.UIThread.Post(ShowVmMaxMapCountDialog);
+ }
+ else
+ {
+ Dispatcher.UIThread.Post(ShowVmMaxMapCountWarning);
+ }
+ }
+
if (_deferLoad)
{
_deferLoad = false;
diff --git a/src/Ryujinx.Memory/MemoryManagementUnix.cs b/src/Ryujinx.Memory/MemoryManagementUnix.cs
index affcff92b..30baf0353 100644
--- a/src/Ryujinx.Memory/MemoryManagementUnix.cs
+++ b/src/Ryujinx.Memory/MemoryManagementUnix.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Concurrent;
+using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
@@ -53,9 +54,9 @@ namespace Ryujinx.Memory
IntPtr ptr = mmap(IntPtr.Zero, size, prot, flags, -1, 0);
- if (ptr == new IntPtr(-1L))
+ if (ptr == MAP_FAILED)
{
- throw new OutOfMemoryException();
+ throw new SystemException(Marshal.GetLastPInvokeErrorMessage());
}
if (!_allocations.TryAdd(ptr, size))
@@ -76,17 +77,33 @@ namespace Ryujinx.Memory
prot |= MmapProts.PROT_EXEC;
}
- return mprotect(address, size, prot) == 0;
+ if (mprotect(address, size, prot) != 0)
+ {
+ throw new SystemException(Marshal.GetLastPInvokeErrorMessage());
+ }
+
+ return true;
}
public static bool Decommit(IntPtr address, ulong size)
{
// Must be writable for madvise to work properly.
- mprotect(address, size, MmapProts.PROT_READ | MmapProts.PROT_WRITE);
+ if (mprotect(address, size, MmapProts.PROT_READ | MmapProts.PROT_WRITE) != 0)
+ {
+ throw new SystemException(Marshal.GetLastPInvokeErrorMessage());
+ }
- madvise(address, size, MADV_REMOVE);
+ if (madvise(address, size, MADV_REMOVE) != 0)
+ {
+ throw new SystemException(Marshal.GetLastPInvokeErrorMessage());
+ }
- return mprotect(address, size, MmapProts.PROT_NONE) == 0;
+ if (mprotect(address, size, MmapProts.PROT_NONE) != 0)
+ {
+ throw new SystemException(Marshal.GetLastPInvokeErrorMessage());
+ }
+
+ return true;
}
public static bool Reprotect(IntPtr address, ulong size, MemoryPermission permission)
diff --git a/src/Ryujinx.Memory/MemoryManagementWindows.cs b/src/Ryujinx.Memory/MemoryManagementWindows.cs
index 2f89a921c..cbf3ecbac 100644
--- a/src/Ryujinx.Memory/MemoryManagementWindows.cs
+++ b/src/Ryujinx.Memory/MemoryManagementWindows.cs
@@ -1,5 +1,6 @@
using Ryujinx.Memory.WindowsShared;
using System;
+using System.Runtime.InteropServices;
using System.Runtime.Versioning;
namespace Ryujinx.Memory
@@ -36,7 +37,7 @@ namespace Ryujinx.Memory
if (ptr == IntPtr.Zero)
{
- throw new OutOfMemoryException();
+ throw new SystemException(Marshal.GetLastPInvokeErrorMessage());
}
return ptr;
@@ -48,7 +49,7 @@ namespace Ryujinx.Memory
if (ptr == IntPtr.Zero)
{
- throw new OutOfMemoryException();
+ throw new SystemException(Marshal.GetLastPInvokeErrorMessage());
}
return ptr;
diff --git a/src/Ryujinx.Memory/MemoryManagerUnixHelper.cs b/src/Ryujinx.Memory/MemoryManagerUnixHelper.cs
index 204f1ca4d..a7b207ab0 100644
--- a/src/Ryujinx.Memory/MemoryManagerUnixHelper.cs
+++ b/src/Ryujinx.Memory/MemoryManagerUnixHelper.cs
@@ -1,8 +1,11 @@
using System;
using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
namespace Ryujinx.Memory
{
+ [SupportedOSPlatform("linux")]
+ [SupportedOSPlatform("macos")]
public static partial class MemoryManagerUnixHelper
{
[Flags]
@@ -41,6 +44,8 @@ namespace Ryujinx.Memory
O_SYNC = 256,
}
+ public const IntPtr MAP_FAILED = -1;
+
private const int MAP_ANONYMOUS_LINUX_GENERIC = 0x20;
private const int MAP_NORESERVE_LINUX_GENERIC = 0x4000;
private const int MAP_UNLOCKED_LINUX_GENERIC = 0x80000;
diff --git a/src/Ryujinx.Memory/MemoryProtectionException.cs b/src/Ryujinx.Memory/MemoryProtectionException.cs
index 27e950a16..e5606e99f 100644
--- a/src/Ryujinx.Memory/MemoryProtectionException.cs
+++ b/src/Ryujinx.Memory/MemoryProtectionException.cs
@@ -1,4 +1,5 @@
using System;
+using System.Runtime.InteropServices;
namespace Ryujinx.Memory
{
@@ -8,7 +9,7 @@ namespace Ryujinx.Memory
{
}
- public MemoryProtectionException(MemoryPermission permission) : base($"Failed to set memory protection to \"{permission}\".")
+ public MemoryProtectionException(MemoryPermission permission) : base($"Failed to set memory protection to \"{permission}\": {Marshal.GetLastPInvokeErrorMessage()}")
{
}
diff --git a/src/Ryujinx.Memory/WindowsShared/WindowsApi.cs b/src/Ryujinx.Memory/WindowsShared/WindowsApi.cs
index 67e704ea4..c554e3205 100644
--- a/src/Ryujinx.Memory/WindowsShared/WindowsApi.cs
+++ b/src/Ryujinx.Memory/WindowsShared/WindowsApi.cs
@@ -1,8 +1,10 @@
using System;
using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
namespace Ryujinx.Memory.WindowsShared
{
+ [SupportedOSPlatform("windows")]
static partial class WindowsApi
{
public static readonly IntPtr InvalidHandleValue = new IntPtr(-1);
diff --git a/src/Ryujinx.Memory/WindowsShared/WindowsApiException.cs b/src/Ryujinx.Memory/WindowsShared/WindowsApiException.cs
index 3140d705b..330c1842a 100644
--- a/src/Ryujinx.Memory/WindowsShared/WindowsApiException.cs
+++ b/src/Ryujinx.Memory/WindowsShared/WindowsApiException.cs
@@ -1,7 +1,9 @@
using System;
+using System.Runtime.Versioning;
namespace Ryujinx.Memory.WindowsShared
{
+ [SupportedOSPlatform("windows")]
class WindowsApiException : Exception
{
public WindowsApiException()
diff --git a/src/Ryujinx.Ui.Common/Helper/LinuxHelper.cs b/src/Ryujinx.Ui.Common/Helper/LinuxHelper.cs
new file mode 100644
index 000000000..cfbf4b57d
--- /dev/null
+++ b/src/Ryujinx.Ui.Common/Helper/LinuxHelper.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Ui.Common.Helper
+{
+ [SupportedOSPlatform("linux")]
+ public static class LinuxHelper
+ {
+ // NOTE: This value was determined by manual tests and might need to be increased again.
+ public const int RecommendedVmMaxMapCount = 524288;
+ public const string VmMaxMapCountPath = "/proc/sys/vm/max_map_count";
+ public const string SysCtlConfigPath = "/etc/sysctl.d/99-Ryujinx.conf";
+ public static int VmMaxMapCount => int.Parse(File.ReadAllText(VmMaxMapCountPath));
+ public static string PkExecPath { get; } = GetBinaryPath("pkexec");
+
+ private static string GetBinaryPath(string binary)
+ {
+ string pathVar = Environment.GetEnvironmentVariable("PATH");
+
+ if (pathVar is null || string.IsNullOrEmpty(binary))
+ {
+ return null;
+ }
+
+ foreach (var searchPath in pathVar.Split(":", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
+ {
+ string binaryPath = Path.Combine(searchPath, binary);
+
+ if (File.Exists(binaryPath))
+ {
+ return binaryPath;
+ }
+ }
+
+ return null;
+ }
+
+ public static int RunPkExec(string command)
+ {
+ if (PkExecPath == null)
+ {
+ return 1;
+ }
+
+ using Process process = new()
+ {
+ StartInfo =
+ {
+ FileName = PkExecPath,
+ ArgumentList = { "sh", "-c", command }
+ }
+ };
+
+ process.Start();
+ process.WaitForExit();
+
+ return process.ExitCode;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs
index 836483d89..96024548f 100644
--- a/src/Ryujinx/Program.cs
+++ b/src/Ryujinx/Program.cs
@@ -264,6 +264,71 @@ namespace Ryujinx
MainWindow mainWindow = new MainWindow();
mainWindow.Show();
+ if (OperatingSystem.IsLinux())
+ {
+ int currentVmMaxMapCount = LinuxHelper.VmMaxMapCount;
+
+ if (LinuxHelper.VmMaxMapCount < LinuxHelper.RecommendedVmMaxMapCount)
+ {
+ Logger.Warning?.Print(LogClass.Application, $"The value of vm.max_map_count is lower than {LinuxHelper.RecommendedVmMaxMapCount}. ({currentVmMaxMapCount})");
+
+ if (LinuxHelper.PkExecPath is not null)
+ {
+ var buttonTexts = new Dictionary()
+ {
+ { 0, "Yes, until the next restart" },
+ { 1, "Yes, permanently" },
+ { 2, "No" }
+ };
+
+ ResponseType response = GtkDialog.CreateCustomDialog(
+ "Ryujinx - Low limit for memory mappings detected",
+ $"Would you like to increase the value of vm.max_map_count to {LinuxHelper.RecommendedVmMaxMapCount}?",
+ "Some games might try to create more memory mappings than currently allowed. " +
+ "Ryujinx will crash as soon as this limit gets exceeded.",
+ buttonTexts,
+ MessageType.Question);
+
+ int rc;
+
+ switch ((int)response)
+ {
+ case 0:
+ rc = LinuxHelper.RunPkExec($"echo {LinuxHelper.RecommendedVmMaxMapCount} > {LinuxHelper.VmMaxMapCountPath}");
+ if (rc == 0)
+ {
+ Logger.Info?.Print(LogClass.Application, $"vm.max_map_count set to {LinuxHelper.VmMaxMapCount} until the next restart.");
+ }
+ else
+ {
+ Logger.Error?.Print(LogClass.Application, $"Unable to change vm.max_map_count. Process exited with code: {rc}");
+ }
+ break;
+ case 1:
+ rc = LinuxHelper.RunPkExec($"echo \"vm.max_map_count = {LinuxHelper.RecommendedVmMaxMapCount}\" > {LinuxHelper.SysCtlConfigPath} && sysctl -p {LinuxHelper.SysCtlConfigPath}");
+ if (rc == 0)
+ {
+ Logger.Info?.Print(LogClass.Application, $"vm.max_map_count set to {LinuxHelper.VmMaxMapCount}. Written to config: {LinuxHelper.SysCtlConfigPath}");
+ }
+ else
+ {
+ Logger.Error?.Print(LogClass.Application, $"Unable to write new value for vm.max_map_count to config. Process exited with code: {rc}");
+ }
+ break;
+ }
+ }
+ else
+ {
+ GtkDialog.CreateWarningDialog(
+ "Max amount of memory mappings is lower than recommended.",
+ $"The current value of vm.max_map_count ({currentVmMaxMapCount}) is lower than {LinuxHelper.RecommendedVmMaxMapCount}." +
+ "Some games might try to create more memory mappings than currently allowed. " +
+ "Ryujinx will crash as soon as this limit gets exceeded.\n\n" +
+ "You might want to either manually increase the limit or install pkexec, which allows Ryujinx to assist with that.");
+ }
+ }
+ }
+
if (CommandLineState.LaunchPathArg != null)
{
mainWindow.RunApplication(CommandLineState.LaunchPathArg, CommandLineState.StartFullscreenArg);
diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj
index 027cdd129..cf4435e57 100644
--- a/src/Ryujinx/Ryujinx.csproj
+++ b/src/Ryujinx/Ryujinx.csproj
@@ -14,6 +14,7 @@
true
+ false
true
partial
@@ -100,4 +101,4 @@
-
+
\ No newline at end of file