Compare commits

...

8 Commits

Author SHA1 Message Date
4741a05df9 Vulkan: Include DepthMode in ProgramPipelineState (#5185) 2023-06-01 09:05:39 +02:00
c6676007bf GPU: Dispose Renderer after running deferred actions (#5144)
* GAL: Dispose Renderer after running deferred actions

Deferred actions from disposing physical memory instances always dispose the resources in their caches. The renderer can't be disposed before these resources get disposed, otherwise the dispose actions will not actually run, and the ThreadedRenderer may get stuck trying to enqueue too many commands when there is nothing consuming them.

This should fix most instances of the emulator freezing on close.

* Wait for main render commands to finish, but keep RenderThread alive til dispose

* Address some feedback.

* No parameterize needed

* Set thread name as part of constructor

* Port to Ava and SDL2
2023-05-31 21:43:20 +00:00
92b0b7d753 Avalonia UI: Fix letter "x" in Ryujinx logo being cut off (#5176)
Also make the pronunciation center-aligned
2023-05-31 21:03:11 +00:00
232237bf28 Skip draws with zero vertex count (#5149) 2023-05-31 17:51:11 -03:00
c27e453fd3 Share ResourceManager vertex vertex A and B shaders (#5181) 2023-05-31 17:17:50 -03:00
0e037d0213 macOS Headless Fixes (#5167)
* Default hypervisor to disabled

* Include MVK on macOS

* Properly sign headless builds on macOS

* Force Vulkan on macOS

* Suggestions
2023-05-31 09:08:50 +02:00
0dca1fbe12 Add Context Menu Option to Run Application (#5154) 2023-05-30 19:51:03 +01:00
35d91a0e58 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
2023-05-30 01:48:37 +02:00
35 changed files with 398 additions and 83 deletions

View File

@ -92,6 +92,8 @@ namespace Ryujinx.Ava
private bool _isActive;
private bool _renderingStarted;
private ManualResetEvent _gpuDoneEvent;
private IRenderer _renderer;
private readonly Thread _renderingThread;
private readonly CancellationTokenSource _gpuCancellationTokenSource;
@ -183,6 +185,7 @@ namespace Ryujinx.Ava
ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState;
_gpuCancellationTokenSource = new CancellationTokenSource();
_gpuDoneEvent = new ManualResetEvent(false);
}
private void TopLevel_PointerEnterOrMoved(object sender, PointerEventArgs e)
@ -423,10 +426,10 @@ namespace Ryujinx.Ava
_isActive = false;
if (_renderingThread.IsAlive)
{
_renderingThread.Join();
}
// NOTE: The render loop is allowed to stay alive until the renderer itself is disposed, as it may handle resource dispose.
// We only need to wait for all commands submitted during the main gpu loop to be processed.
_gpuDoneEvent.WaitOne();
_gpuDoneEvent.Dispose();
DisplaySleep.Restore();
@ -917,6 +920,14 @@ namespace Ryujinx.Ava
UpdateStatus();
}
}
// Make sure all commands in the run loop are fully executed before leaving the loop.
if (Device.Gpu.Renderer is ThreadedRenderer threaded)
{
threaded.FlushThreadedCommands();
}
_gpuDoneEvent.Set();
});
(_rendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.MakeCurrent(null);

View File

@ -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",
@ -282,6 +289,7 @@
"ControllerSettingsSaveProfileToolTip": "Save Profile",
"MenuBarFileToolsTakeScreenshot": "Take Screenshot",
"MenuBarFileToolsHideUi": "Hide UI",
"GameListContextMenuRunApplication": "Run Application",
"GameListContextMenuToggleFavorite": "Toggle Favorite",
"GameListContextMenuToggleFavoriteToolTip": "Toggle Favorite status of Game",
"SettingsTabGeneralTheme": "Theme",

View File

@ -18,6 +18,7 @@
<PropertyGroup Condition="'$(RuntimeIdentifier)' != ''">
<PublishSingleFile>true</PublishSingleFile>
<TrimmerSingleWarn>false</TrimmerSingleWarn>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>partial</TrimMode>
</PropertyGroup>

View File

@ -3,6 +3,9 @@
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale">
<MenuItem
Click="RunApplication_Click"
Header="{locale:Locale GameListContextMenuRunApplication}" />
<MenuItem
Click="ToggleFavorite_Click"
Header="{locale:Locale GameListContextMenuToggleFavorite}"

View File

@ -323,5 +323,15 @@ namespace Ryujinx.Ava.UI.Controls
await ApplicationHelper.ExtractSection(NcaSectionType.Logo, viewModel.SelectedApplication.Path, viewModel.SelectedApplication.TitleName);
}
}
public void RunApplication_Click(object sender, RoutedEventArgs args)
{
var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
if (viewModel?.SelectedApplication != null)
{
viewModel.LoadApplication(viewModel.SelectedApplication.Path);
}
}
}
}

View File

@ -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<UserResult> ShowContentDialog(
private async static Task<UserResult> ShowContentDialog(
string title,
object content,
string primaryButton,
@ -67,7 +66,7 @@ namespace Ryujinx.Ava.UI.Helpers
return result;
}
private async static Task<UserResult> ShowTextDialog(
public async static Task<UserResult> 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()
{

View File

@ -58,11 +58,20 @@
JustifyContent="SpaceAround"
RowSpacing="2">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="28"
FontWeight="Bold"
Text="Ryujinx"
TextAlignment="Left" />
<TextBlock Text="(REE-YOU-JINX)" TextAlignment="Left" />
TextAlignment="Center"
Width="100" />
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="11"
Text="(REE-YOU-JINX)"
TextAlignment="Center"
Width="100" />
</flex:FlexPanel>
</Grid>
<TextBlock

View File

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

View File

@ -1,5 +1,6 @@
using Ryujinx.Common.Configuration;
using System;
using System.Threading;
namespace Ryujinx.Graphics.GAL
{
@ -52,7 +53,7 @@ namespace Ryujinx.Graphics.GAL
void ResetCounter(CounterType type);
void RunLoop(Action gpuLoop)
void RunLoop(ThreadStart gpuLoop)
{
gpuLoop();
}

View File

@ -30,7 +30,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading
private IRenderer _baseRenderer;
private Thread _gpuThread;
private Thread _backendThread;
private bool _disposed;
private bool _running;
private AutoResetEvent _frameComplete = new AutoResetEvent(true);
@ -98,19 +97,17 @@ namespace Ryujinx.Graphics.GAL.Multithreading
_refQueue = new object[MaxRefsPerCommand * QueueCount];
}
public void RunLoop(Action gpuLoop)
public void RunLoop(ThreadStart gpuLoop)
{
_running = true;
_backendThread = Thread.CurrentThread;
_gpuThread = new Thread(() => {
gpuLoop();
_running = false;
_galWorkAvailable.Set();
});
_gpuThread = new Thread(gpuLoop)
{
Name = "GPU.MainThread"
};
_gpuThread.Name = "GPU.MainThread";
_gpuThread.Start();
RenderLoop();
@ -120,7 +117,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading
{
// Power through the render queue until the Gpu thread work is done.
while (_running && !_disposed)
while (_running)
{
_galWorkAvailable.Wait();
_galWorkAvailable.Reset();
@ -488,12 +485,23 @@ namespace Ryujinx.Graphics.GAL.Multithreading
return _baseRenderer.PrepareHostMapping(address, size);
}
public void FlushThreadedCommands()
{
SpinWait wait = new();
while (Volatile.Read(ref _commandCount) > 0)
{
wait.SpinOnce();
}
}
public void Dispose()
{
// Dispose must happen from the render thread, after all commands have completed.
// Stop the GPU thread.
_disposed = true;
_running = false;
_galWorkAvailable.Set();
if (_gpuThread != null && _gpuThread.IsAlive)
{

View File

@ -63,6 +63,8 @@ namespace Ryujinx.Graphics.GAL
public bool PrimitiveRestartEnable;
public uint PatchControlPoints;
public DepthMode DepthMode;
public void SetVertexAttribs(ReadOnlySpan<VertexAttribDescriptor> vertexAttribs)
{
VertexAttribCount = vertexAttribs.Length;

View File

@ -771,7 +771,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
/// </summary>
private void UpdateDepthMode()
{
_context.Renderer.Pipeline.SetDepthMode(GetDepthMode());
DepthMode mode = GetDepthMode();
_pipeline.DepthMode = mode;
_context.Renderer.Pipeline.SetDepthMode(mode);
}
/// <summary>

View File

@ -390,7 +390,6 @@ namespace Ryujinx.Graphics.Gpu
/// </summary>
public void Dispose()
{
Renderer.Dispose();
GPFifo.Dispose();
HostInitalized.Dispose();
@ -403,6 +402,8 @@ namespace Ryujinx.Graphics.Gpu
PhysicalMemoryRegistry.Clear();
RunDeferredActions();
Renderer.Dispose();
}
}
}

View File

@ -17,8 +17,6 @@ namespace Ryujinx.Graphics.Gpu.Shader
private readonly ResourceCounts _resourceCounts;
private readonly int _stageIndex;
private readonly int[] _constantBufferBindings;
/// <summary>
/// Creates a new GPU accessor.
/// </summary>
@ -28,12 +26,6 @@ namespace Ryujinx.Graphics.Gpu.Shader
_context = context;
_resourceCounts = resourceCounts;
_stageIndex = stageIndex;
if (context.Capabilities.Api != TargetApi.Vulkan)
{
_constantBufferBindings = new int[Constants.TotalGpUniformBuffers];
_constantBufferBindings.AsSpan().Fill(-1);
}
}
public int QueryBindingConstantBuffer(int index)
@ -45,15 +37,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
}
else
{
int binding = _constantBufferBindings[index];
if (binding < 0)
{
binding = _resourceCounts.UniformBuffersCount++;
_constantBufferBindings[index] = binding;
}
return binding;
return _resourceCounts.UniformBuffersCount++;
}
}

View File

@ -736,6 +736,19 @@ namespace Ryujinx.Graphics.Gpu.Shader
return MatchesTexture(specializationState, descriptor);
}
/// <summary>
/// Populates pipeline state that doesn't exist in older caches with default values
/// based on specialization state.
/// </summary>
/// <param name="pipelineState">Pipeline state to prepare</param>
private void PreparePipelineState(ref ProgramPipelineState pipelineState)
{
if (!_compute)
{
pipelineState.DepthMode = GraphicsState.DepthMode ? DepthMode.MinusOneToOne : DepthMode.ZeroToOne;
}
}
/// <summary>
/// Reads shader specialization state that has been serialized.
/// </summary>
@ -776,6 +789,8 @@ namespace Ryujinx.Graphics.Gpu.Shader
{
ProgramPipelineState pipelineState = default;
dataReader.ReadWithMagicAndSize(ref pipelineState, PgpsMagic);
specState.PreparePipelineState(ref pipelineState);
specState.PipelineState = pipelineState;
}

View File

@ -17,6 +17,8 @@ namespace Ryujinx.Graphics.Shader.Translation
private readonly HashSet<int> _usedConstantBufferBindings;
public ShaderProperties Properties => _properties;
public ResourceManager(ShaderStage stage, IGpuAccessor gpuAccessor, ShaderProperties properties)
{
_gpuAccessor = gpuAccessor;
@ -98,19 +100,6 @@ namespace Ryujinx.Graphics.Shader.Translation
_properties.AddConstantBuffer(binding, new BufferDefinition(BufferLayout.Std140, 0, binding, name, type));
}
public void InheritFrom(ResourceManager other)
{
for (int i = 0; i < other._cbSlotToBindingMap.Length; i++)
{
int binding = other._cbSlotToBindingMap[i];
if (binding >= 0)
{
_cbSlotToBindingMap[i] = binding;
}
}
}
public static string GetShaderStagePrefix(ShaderStage stage)
{
uint index = (uint)stage;

View File

@ -39,9 +39,9 @@ namespace Ryujinx.Graphics.Shader.Translation
public TranslationOptions Options { get; }
public ShaderProperties Properties { get; }
public ShaderProperties Properties => ResourceManager.Properties;
public ResourceManager ResourceManager { get; }
public ResourceManager ResourceManager { get; set; }
public bool TransformFeedbackEnabled { get; }
@ -159,8 +159,7 @@ namespace Ryujinx.Graphics.Shader.Translation
_sbSlots = new Dictionary<int, int>();
_sbSlotsReverse = new Dictionary<int, int>();
Properties = new ShaderProperties();
ResourceManager = new ResourceManager(stage, gpuAccessor, Properties);
ResourceManager = new ResourceManager(stage, gpuAccessor, new ShaderProperties());
}
public ShaderConfig(
@ -429,8 +428,6 @@ namespace Ryujinx.Graphics.Shader.Translation
public void InheritFrom(ShaderConfig other)
{
ResourceManager.InheritFrom(other.ResourceManager);
ClipDistancesWritten |= other.ClipDistancesWritten;
UsedFeatures |= other.UsedFeatures;

View File

@ -155,6 +155,9 @@ namespace Ryujinx.Graphics.Shader.Translation
{
other._config.MergeOutputUserAttributes(_config.UsedOutputAttributes, Enumerable.Empty<int>());
// We need to share the resource manager since both shaders accesses the same constant buffers.
other._config.ResourceManager = _config.ResourceManager;
FunctionCode[] otherCode = EmitShader(other._program, other._config, initializeOutputs: true, out int aStart);
code = Combine(otherCode, code, aStart);

View File

@ -358,7 +358,7 @@ namespace Ryujinx.Graphics.Vulkan
public void Draw(int vertexCount, int instanceCount, int firstVertex, int firstInstance)
{
if (!_program.IsLinked)
if (!_program.IsLinked || vertexCount == 0)
{
return;
}
@ -422,7 +422,7 @@ namespace Ryujinx.Graphics.Vulkan
public void DrawIndexed(int indexCount, int instanceCount, int firstIndex, int firstVertex, int firstInstance)
{
if (!_program.IsLinked)
if (!_program.IsLinked || indexCount == 0)
{
return;
}

View File

@ -165,6 +165,7 @@ namespace Ryujinx.Graphics.Vulkan
pipeline.DepthTestEnable = state.DepthTest.TestEnable;
pipeline.DepthWriteEnable = state.DepthTest.WriteEnable;
pipeline.DepthCompareOp = state.DepthTest.Func.Convert();
pipeline.DepthMode = state.DepthMode == DepthMode.MinusOneToOne;
pipeline.FrontFace = state.FrontFace.Convert();

View File

@ -130,7 +130,7 @@ namespace Ryujinx.Headless.SDL2
public float AudioVolume { get; set; }
[Option("use-hypervisor", Required = false, Default = true, HelpText = "Uses Hypervisor over JIT if available.")]
public bool UseHypervisor { get; set; }
public bool? UseHypervisor { get; set; }
[Option("lan-interface-id", Required = false, Default = "0", HelpText = "GUID for the network interface used by LAN.")]
public string MultiplayerLanInterfaceId { get; set; }

View File

@ -339,6 +339,15 @@ namespace Ryujinx.Headless.SDL2
GraphicsConfig.EnableShaderCache = true;
if (OperatingSystem.IsMacOS())
{
if (option.GraphicsBackend == GraphicsBackend.OpenGl)
{
option.GraphicsBackend = GraphicsBackend.Vulkan;
Logger.Warning?.Print(LogClass.Application, "OpenGL is not supported on macOS, switching to Vulkan!");
}
}
IGamepad gamepad;
if (option.ListInputIds)
@ -550,7 +559,7 @@ namespace Ryujinx.Headless.SDL2
options.IgnoreMissingServices,
options.AspectRatio,
options.AudioVolume,
options.UseHypervisor,
options.UseHypervisor ?? true,
options.MultiplayerLanInterfaceId);
return new Switch(configuration);

View File

@ -7,6 +7,7 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Version>1.0.0-dirty</Version>
<DefineConstants Condition=" '$(ExtraDefineConstants)' != '' ">$(DefineConstants);$(ExtraDefineConstants)</DefineConstants>
<SigningCertificate Condition=" '$(SigningCertificate)' == '' ">-</SigningCertificate>
<TieredPGO>true</TieredPGO>
</PropertyGroup>
@ -15,6 +16,10 @@
<PackageReference Include="Ryujinx.Graphics.Nvdec.Dependencies" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="$([MSBuild]::IsOSPlatform('OSX'))">
<Exec Command="codesign --entitlements '$(ProjectDir)..\..\distribution\macos\entitlements.xml' -f --deep -s $(SigningCertificate) '$(TargetDir)$(TargetName)'" />
</Target>
<ItemGroup>
<ProjectReference Include="..\Ryujinx.Graphics.Vulkan\Ryujinx.Graphics.Vulkan.csproj" />
<ProjectReference Include="..\Ryujinx.Input\Ryujinx.Input.csproj" />
@ -29,6 +34,7 @@
<ItemGroup>
<PackageReference Include="CommandLineParser" />
<PackageReference Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'win10-x64'" />
</ItemGroup>
<ItemGroup>

View File

@ -62,6 +62,7 @@ namespace Ryujinx.Headless.SDL2
private readonly long _ticksPerFrame;
private readonly CancellationTokenSource _gpuCancellationTokenSource;
private readonly ManualResetEvent _exitEvent;
private readonly ManualResetEvent _gpuDoneEvent;
private long _ticks;
private bool _isActive;
@ -91,6 +92,7 @@ namespace Ryujinx.Headless.SDL2
_ticksPerFrame = Stopwatch.Frequency / TargetFps;
_gpuCancellationTokenSource = new CancellationTokenSource();
_exitEvent = new ManualResetEvent(false);
_gpuDoneEvent = new ManualResetEvent(false);
_aspectRatio = aspectRatio;
_enableMouse = enableMouse;
HostUiTheme = new HeadlessHostUiTheme();
@ -275,6 +277,14 @@ namespace Ryujinx.Headless.SDL2
_ticks = Math.Min(_ticks - _ticksPerFrame, _ticksPerFrame);
}
}
// Make sure all commands in the run loop are fully executed before leaving the loop.
if (Device.Gpu.Renderer is ThreadedRenderer threaded)
{
threaded.FlushThreadedCommands();
}
_gpuDoneEvent.Set();
});
FinalizeWindowRenderer();
@ -404,7 +414,10 @@ namespace Ryujinx.Headless.SDL2
MainLoop();
renderLoopThread.Join();
// NOTE: The render loop is allowed to stay alive until the renderer itself is disposed, as it may handle resource dispose.
// We only need to wait for all commands submitted during the main gpu loop to be processed.
_gpuDoneEvent.WaitOne();
_gpuDoneEvent.Dispose();
nvStutterWorkaround?.Join();
Exit();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,9 @@
using System;
using System.Runtime.Versioning;
namespace Ryujinx.Memory.WindowsShared
{
[SupportedOSPlatform("windows")]
class WindowsApiException : Exception
{
public WindowsApiException()

View File

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

View File

@ -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<int, string>()
{
{ 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);

View File

@ -14,6 +14,7 @@
<PropertyGroup Condition="'$(RuntimeIdentifier)' != ''">
<PublishSingleFile>true</PublishSingleFile>
<TrimmerSingleWarn>false</TrimmerSingleWarn>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>partial</TrimMode>
</PropertyGroup>

View File

@ -65,6 +65,7 @@ namespace Ryujinx.Ui
private KeyboardHotkeyState _prevHotkeyState;
private readonly ManualResetEvent _exitEvent;
private readonly ManualResetEvent _gpuDoneEvent;
private readonly CancellationTokenSource _gpuCancellationTokenSource;
@ -110,6 +111,7 @@ namespace Ryujinx.Ui
| EventMask.KeyReleaseMask));
_exitEvent = new ManualResetEvent(false);
_gpuDoneEvent = new ManualResetEvent(false);
_gpuCancellationTokenSource = new CancellationTokenSource();
@ -499,6 +501,14 @@ namespace Ryujinx.Ui
_ticks = Math.Min(_ticks - _ticksPerFrame, _ticksPerFrame);
}
}
// Make sure all commands in the run loop are fully executed before leaving the loop.
if (Device.Gpu.Renderer is ThreadedRenderer threaded)
{
threaded.FlushThreadedCommands();
}
_gpuDoneEvent.Set();
});
}
@ -542,7 +552,10 @@ namespace Ryujinx.Ui
MainLoop();
renderLoopThread.Join();
// NOTE: The render loop is allowed to stay alive until the renderer itself is disposed, as it may handle resource dispose.
// We only need to wait for all commands submitted during the main gpu loop to be processed.
_gpuDoneEvent.WaitOne();
_gpuDoneEvent.Dispose();
nvStutterWorkaround?.Join();
Exit();