Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Linux: Automatically increase vm.max_map_count if it's too low #4702

Merged
merged 8 commits into from
May 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion distribution/linux/Ryujinx.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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" "$@"
7 changes: 7 additions & 0 deletions src/Ryujinx.Ava/Assets/Locales/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion src/Ryujinx.Ava/Ryujinx.Ava.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

<PropertyGroup Condition="'$(RuntimeIdentifier)' != ''">
<PublishSingleFile>true</PublishSingleFile>
<TrimmerSingleWarn>false</TrimmerSingleWarn>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>partial</TrimMode>
</PropertyGroup>
Expand Down Expand Up @@ -147,4 +148,4 @@
<ItemGroup>
<AdditionalFiles Include="Assets\Locales\en_US.json" />
</ItemGroup>
</Project>
</Project>
7 changes: 3 additions & 4 deletions src/Ryujinx.Ava/UI/Helpers/ContentDialogHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
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;
Expand All @@ -19,7 +18,7 @@ public static class ContentDialogHelper
{
private static bool _isChoiceDialogOpen;

public async static Task<UserResult> ShowContentDialog(
private async static Task<UserResult> ShowContentDialog(
string title,
object content,
string primaryButton,
Expand Down Expand Up @@ -67,7 +66,7 @@ public static class ContentDialogHelper
return result;
}

private async static Task<UserResult> ShowTextDialog(
public async static Task<UserResult> ShowTextDialog(
string title,
string primaryText,
string secondaryText,
Expand Down Expand Up @@ -319,7 +318,7 @@ public static async Task<ContentDialogResult> ShowAsync(ContentDialog contentDia

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()
{
Expand Down
74 changes: 73 additions & 1 deletion src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
using System;
using System.ComponentModel;
using System.IO;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using InputManager = Ryujinx.Input.HLE.InputManager;

Expand Down Expand Up @@ -258,7 +259,64 @@ private void Initialize()
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)
TSRBerry marked this conversation as resolved.
Show resolved Hide resolved
{
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)
{
Expand All @@ -268,6 +326,20 @@ protected void CheckLaunchState()
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;
Expand Down
29 changes: 23 additions & 6 deletions src/Ryujinx.Memory/MemoryManagementUnix.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;

Expand Down Expand Up @@ -53,9 +54,9 @@ private static IntPtr AllocateInternal(ulong size, MmapProts prot, bool forJit,

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))
Expand All @@ -76,17 +77,33 @@ public static bool Commit(IntPtr address, ulong size, bool forJit)
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());
}

if (mprotect(address, size, MmapProts.PROT_NONE) != 0)
{
throw new SystemException(Marshal.GetLastPInvokeErrorMessage());
}

return mprotect(address, size, MmapProts.PROT_NONE) == 0;
return true;
}

public static bool Reprotect(IntPtr address, ulong size, MemoryPermission permission)
Expand Down
5 changes: 3 additions & 2 deletions src/Ryujinx.Memory/MemoryManagementWindows.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Ryujinx.Memory.WindowsShared;
using System;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;

namespace Ryujinx.Memory
Expand Down Expand Up @@ -36,7 +37,7 @@ private static IntPtr AllocateInternal(IntPtr size, AllocationType flags = 0)

if (ptr == IntPtr.Zero)
{
throw new OutOfMemoryException();
throw new SystemException(Marshal.GetLastPInvokeErrorMessage());
}

return ptr;
Expand All @@ -48,7 +49,7 @@ private static IntPtr AllocateInternal2(IntPtr size, AllocationType flags = 0)

if (ptr == IntPtr.Zero)
{
throw new OutOfMemoryException();
throw new SystemException(Marshal.GetLastPInvokeErrorMessage());
}

return ptr;
Expand Down
5 changes: 5 additions & 0 deletions src/Ryujinx.Memory/MemoryManagerUnixHelper.cs
Original file line number Diff line number Diff line change
@@ -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]
Expand Down Expand Up @@ -41,6 +44,8 @@ public enum OpenFlags : uint
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;
Expand Down
3 changes: 2 additions & 1 deletion src/Ryujinx.Memory/MemoryProtectionException.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Runtime.InteropServices;

namespace Ryujinx.Memory
{
Expand All @@ -8,7 +9,7 @@ public MemoryProtectionException()
{
}

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()}")
{
}

Expand Down
2 changes: 2 additions & 0 deletions src/Ryujinx.Memory/WindowsShared/WindowsApi.cs
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
2 changes: 2 additions & 0 deletions src/Ryujinx.Memory/WindowsShared/WindowsApiException.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System;
using System.Runtime.Versioning;

namespace Ryujinx.Memory.WindowsShared
{
[SupportedOSPlatform("windows")]
class WindowsApiException : Exception
{
public WindowsApiException()
Expand Down
62 changes: 62 additions & 0 deletions src/Ryujinx.Ui.Common/Helper/LinuxHelper.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}