From 117e32a6fffc30cdb895aa98483af7df353a8dd1 Mon Sep 17 00:00:00 2001 From: mpnico Date: Sat, 11 Sep 2021 22:08:25 +0200 Subject: [PATCH] Implement a "Pause Emulation" option & hotkey (#2428) * Add a "Pause Emulation" option and hotkey Closes Ryujinx#1604 * Refactoring how pause is handled * Applied suggested changes from review * Applied suggested fixes * Pass correct suspend type to threads for suspend/resume * Fix NRE after stoping emulation * Removing SimulateWakeUpMessage call after resuming emulation * Skip suspending non game process * Pause the tickCounter in the ExecutionContext * Refactoring tickCounter pause/resume as suggested * Fix Config migration to add pause hotkey * Fixed pausing only application threads * Fix exiting emulator while paused * Avoid pause/resume while already paused/resumed * Cleanup unused code * Avoid restarting audio if stopping emulation while in pause. * Added suggested changes * Fix ConfigurationState --- ARMeilleure/State/ExecutionContext.cs | 10 +++ .../OpenALHardwareDeviceDriver.cs | 9 +++ .../SDL2HardwareDeviceDriver.cs | 9 +++ .../SoundIoHardwareDeviceDriver.cs | 8 +++ Ryujinx.Audio/AudioManager.cs | 14 +++- .../CompatLayerHardwareDeviceDriver.cs | 5 ++ .../Dummy/DummyHardwareDeviceDriver.cs | 8 +++ .../Integration/IHardwareDeviceDriver.cs | 1 + Ryujinx.Audio/Renderer/Dsp/AudioProcessor.cs | 5 ++ .../Renderer/Server/AudioRendererManager.cs | 8 +++ .../Configuration/Hid/KeyboardHotkeys.cs | 1 + Ryujinx.HLE/HOS/Horizon.cs | 35 ++++++++++ Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs | 55 +++++++++++++++ Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs | 67 +++++++++++-------- Ryujinx/Config.json | 3 +- .../Configuration/ConfigurationFileFormat.cs | 2 +- Ryujinx/Configuration/ConfigurationState.cs | 18 ++++- Ryujinx/Ui/MainWindow.cs | 32 +++++++++ Ryujinx/Ui/MainWindow.glade | 38 ++++++++--- Ryujinx/Ui/RendererWidgetBase.cs | 16 ++++- Ryujinx/_schema.json | 21 +++--- 21 files changed, 311 insertions(+), 54 deletions(-) diff --git a/ARMeilleure/State/ExecutionContext.cs b/ARMeilleure/State/ExecutionContext.cs index 9a2215695e..a6f74cd0ed 100644 --- a/ARMeilleure/State/ExecutionContext.cs +++ b/ARMeilleure/State/ExecutionContext.cs @@ -145,6 +145,16 @@ namespace ARMeilleure.State _nativeContext.SetCounter(0); } + public static void SuspendCounter() + { + _tickCounter.Stop(); + } + + public static void ResumeCounter() + { + _tickCounter.Start(); + } + public void Dispose() { _nativeContext.Dispose(); diff --git a/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceDriver.cs b/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceDriver.cs index 60c364da9c..453208a1d5 100644 --- a/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceDriver.cs +++ b/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceDriver.cs @@ -15,6 +15,7 @@ namespace Ryujinx.Audio.Backends.OpenAL private readonly ALDevice _device; private readonly ALContext _context; private readonly ManualResetEvent _updateRequiredEvent; + private readonly ManualResetEvent _pauseEvent; private readonly ConcurrentDictionary _sessions; private bool _stillRunning; private Thread _updaterThread; @@ -24,6 +25,7 @@ namespace Ryujinx.Audio.Backends.OpenAL _device = ALC.OpenDevice(""); _context = ALC.CreateContext(_device, new ALContextAttributes()); _updateRequiredEvent = new ManualResetEvent(false); + _pauseEvent = new ManualResetEvent(true); _sessions = new ConcurrentDictionary(); _stillRunning = true; @@ -88,6 +90,11 @@ namespace Ryujinx.Audio.Backends.OpenAL return _updateRequiredEvent; } + public ManualResetEvent GetPauseEvent() + { + return _pauseEvent; + } + private void Update() { ALC.MakeContextCurrent(_context); @@ -132,6 +139,8 @@ namespace Ryujinx.Audio.Backends.OpenAL ALC.DestroyContext(_context); ALC.CloseDevice(_device); + + _pauseEvent.Dispose(); } } diff --git a/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceDriver.cs b/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceDriver.cs index 13062ad155..77545b57ea 100644 --- a/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceDriver.cs +++ b/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceDriver.cs @@ -15,11 +15,13 @@ namespace Ryujinx.Audio.Backends.SDL2 public class SDL2HardwareDeviceDriver : IHardwareDeviceDriver { private readonly ManualResetEvent _updateRequiredEvent; + private readonly ManualResetEvent _pauseEvent; private readonly ConcurrentDictionary _sessions; public SDL2HardwareDeviceDriver() { _updateRequiredEvent = new ManualResetEvent(false); + _pauseEvent = new ManualResetEvent(true); _sessions = new ConcurrentDictionary(); SDL2Driver.Instance.Initialize(); @@ -44,6 +46,11 @@ namespace Ryujinx.Audio.Backends.SDL2 return _updateRequiredEvent; } + public ManualResetEvent GetPauseEvent() + { + return _pauseEvent; + } + public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount) { if (channelCount == 0) @@ -136,6 +143,8 @@ namespace Ryujinx.Audio.Backends.SDL2 } SDL2Driver.Instance.Dispose(); + + _pauseEvent.Dispose(); } } diff --git a/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceDriver.cs b/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceDriver.cs index 20aa4cbf65..cde5b3d421 100644 --- a/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceDriver.cs +++ b/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceDriver.cs @@ -15,6 +15,7 @@ namespace Ryujinx.Audio.Backends.SoundIo private readonly SoundIO _audioContext; private readonly SoundIODevice _audioDevice; private readonly ManualResetEvent _updateRequiredEvent; + private readonly ManualResetEvent _pauseEvent; private readonly ConcurrentDictionary _sessions; private int _disposeState; @@ -22,6 +23,7 @@ namespace Ryujinx.Audio.Backends.SoundIo { _audioContext = new SoundIO(); _updateRequiredEvent = new ManualResetEvent(false); + _pauseEvent = new ManualResetEvent(true); _sessions = new ConcurrentDictionary(); _audioContext.Connect(); @@ -123,6 +125,11 @@ namespace Ryujinx.Audio.Backends.SoundIo return _updateRequiredEvent; } + public ManualResetEvent GetPauseEvent() + { + return _pauseEvent; + } + public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount) { if (channelCount == 0) @@ -218,6 +225,7 @@ namespace Ryujinx.Audio.Backends.SoundIo _audioContext.Disconnect(); _audioContext.Dispose(); + _pauseEvent.Dispose(); } } diff --git a/Ryujinx.Audio/AudioManager.cs b/Ryujinx.Audio/AudioManager.cs index ab25150a97..84e5b4f748 100644 --- a/Ryujinx.Audio/AudioManager.cs +++ b/Ryujinx.Audio/AudioManager.cs @@ -45,6 +45,8 @@ namespace Ryujinx.Audio /// private Thread _workerThread; + private bool _isRunning; + /// /// Create a new . /// @@ -52,6 +54,7 @@ namespace Ryujinx.Audio { _updateRequiredEvents = new ManualResetEvent[2]; _actions = new Action[2]; + _isRunning = false; // Termination event. _updateRequiredEvents[1] = new ManualResetEvent(false); @@ -72,6 +75,7 @@ namespace Ryujinx.Audio throw new InvalidOperationException(); } + _isRunning = true; _workerThread.Start(); } @@ -96,7 +100,7 @@ namespace Ryujinx.Audio /// private void Update() { - while (true) + while (_isRunning) { int index = WaitHandle.WaitAny(_updateRequiredEvents); @@ -118,6 +122,14 @@ namespace Ryujinx.Audio } } + /// + /// Stop updating the without stopping the worker thread. + /// + public void StopUpdates() + { + _isRunning = false; + } + public void Dispose() { Dispose(true); diff --git a/Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceDriver.cs b/Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceDriver.cs index c0305f8af8..0ae6a6206b 100644 --- a/Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceDriver.cs +++ b/Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceDriver.cs @@ -47,6 +47,11 @@ namespace Ryujinx.Audio.Backends.CompatLayer return _realDriver.GetUpdateRequiredEvent(); } + public ManualResetEvent GetPauseEvent() + { + return _realDriver.GetPauseEvent(); + } + private uint SelectHardwareChannelCount(uint targetChannelCount) { if (_realDriver.SupportsChannelCount(targetChannelCount)) diff --git a/Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceDriver.cs b/Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceDriver.cs index f24b359cde..d729d3f68d 100644 --- a/Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceDriver.cs +++ b/Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceDriver.cs @@ -27,10 +27,12 @@ namespace Ryujinx.Audio.Backends.Dummy public class DummyHardwareDeviceDriver : IHardwareDeviceDriver { private ManualResetEvent _updateRequiredEvent; + private ManualResetEvent _pauseEvent; public DummyHardwareDeviceDriver() { _updateRequiredEvent = new ManualResetEvent(false); + _pauseEvent = new ManualResetEvent(true); } public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount) @@ -60,6 +62,11 @@ namespace Ryujinx.Audio.Backends.Dummy return _updateRequiredEvent; } + public ManualResetEvent GetPauseEvent() + { + return _pauseEvent; + } + public void Dispose() { Dispose(true); @@ -70,6 +77,7 @@ namespace Ryujinx.Audio.Backends.Dummy if (disposing) { // NOTE: The _updateRequiredEvent will be disposed somewhere else. + _pauseEvent.Dispose(); } } diff --git a/Ryujinx.Audio/Integration/IHardwareDeviceDriver.cs b/Ryujinx.Audio/Integration/IHardwareDeviceDriver.cs index 70738c9065..1a53fa9bd0 100644 --- a/Ryujinx.Audio/Integration/IHardwareDeviceDriver.cs +++ b/Ryujinx.Audio/Integration/IHardwareDeviceDriver.cs @@ -36,6 +36,7 @@ namespace Ryujinx.Audio.Integration IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount); ManualResetEvent GetUpdateRequiredEvent(); + ManualResetEvent GetPauseEvent(); bool SupportsDirection(Direction direction); bool SupportsSampleRate(uint sampleRate); diff --git a/Ryujinx.Audio/Renderer/Dsp/AudioProcessor.cs b/Ryujinx.Audio/Renderer/Dsp/AudioProcessor.cs index ea97505627..e15165b932 100644 --- a/Ryujinx.Audio/Renderer/Dsp/AudioProcessor.cs +++ b/Ryujinx.Audio/Renderer/Dsp/AudioProcessor.cs @@ -55,6 +55,8 @@ namespace Ryujinx.Audio.Renderer.Dsp private long _playbackEnds; private ManualResetEvent _event; + private ManualResetEvent _pauseEvent; + public AudioProcessor() { _event = new ManualResetEvent(false); @@ -94,6 +96,7 @@ namespace Ryujinx.Audio.Renderer.Dsp _sessionCommandList = new RendererSession[Constants.AudioRendererSessionCountMax]; _event.Reset(); _lastTime = PerformanceCounter.ElapsedNanoseconds; + _pauseEvent = deviceDriver.GetPauseEvent(); StartThread(); @@ -202,6 +205,8 @@ namespace Ryujinx.Audio.Renderer.Dsp while (true) { + _pauseEvent?.WaitOne(); + MailboxMessage message = _mailbox.ReceiveMessage(); if (message == MailboxMessage.Stop) diff --git a/Ryujinx.Audio/Renderer/Server/AudioRendererManager.cs b/Ryujinx.Audio/Renderer/Server/AudioRendererManager.cs index 71d0f3182c..f471a2e713 100644 --- a/Ryujinx.Audio/Renderer/Server/AudioRendererManager.cs +++ b/Ryujinx.Audio/Renderer/Server/AudioRendererManager.cs @@ -214,6 +214,14 @@ namespace Ryujinx.Audio.Renderer.Server Logger.Info?.Print(LogClass.AudioRenderer, "Stopped audio renderer"); } + /// + /// Stop sending commands to the without stopping the worker thread. + /// + public void StopSendingCommands() + { + _isRunning = false; + } + /// /// Worker main function. This is used to dispatch audio renderer commands to the . /// diff --git a/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs b/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs index 38f5ad3688..dd0d7c2105 100644 --- a/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs +++ b/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs @@ -5,5 +5,6 @@ public Key ToggleVsync { get; set; } public Key Screenshot { get; set; } public Key ShowUi { get; set; } + public Key Pause { get; set; } } } diff --git a/Ryujinx.HLE/HOS/Horizon.cs b/Ryujinx.HLE/HOS/Horizon.cs index 851d7e1371..05ebdbb911 100644 --- a/Ryujinx.HLE/HOS/Horizon.cs +++ b/Ryujinx.HLE/HOS/Horizon.cs @@ -113,6 +113,8 @@ namespace Ryujinx.HLE.HOS internal LibHacHorizonManager LibHacHorizonManager { get; private set; } + public bool IsPaused { get; private set; } + public Horizon(Switch device) { KernelContext = new KernelContext( @@ -385,6 +387,12 @@ namespace Ryujinx.HLE.HOS { _isDisposed = true; + // "Soft" stops AudioRenderer and AudioManager to avoid some sound between resume and stop. + AudioRendererManager.StopSendingCommands(); + AudioManager.StopUpdates(); + + TogglePauseEmulation(false); + KProcess terminationProcess = new KProcess(KernelContext); KThread terminationThread = new KThread(KernelContext); @@ -444,5 +452,32 @@ namespace Ryujinx.HLE.HOS KernelContext.Dispose(); } } + + public void TogglePauseEmulation(bool pause) + { + lock (KernelContext.Processes) + { + foreach (KProcess process in KernelContext.Processes.Values) + { + if (process.Flags.HasFlag(ProcessCreationFlags.IsApplication)) + { + // Only game process should be paused. + process.SetActivity(pause); + } + } + + if (pause && !IsPaused) + { + Device.AudioDeviceDriver.GetPauseEvent().Reset(); + ARMeilleure.State.ExecutionContext.SuspendCounter(); + } + else if (!pause && IsPaused) + { + Device.AudioDeviceDriver.GetPauseEvent().Set(); + ARMeilleure.State.ExecutionContext.ResumeCounter(); + } + } + IsPaused = pause; + } } } diff --git a/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs b/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs index fbdd812e52..a3691808c4 100644 --- a/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs +++ b/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs @@ -1082,5 +1082,60 @@ namespace Ryujinx.HLE.HOS.Kernel.Process } protected override void Destroy() => Context.Dispose(); + + public KernelResult SetActivity(bool pause) + { + KernelContext.CriticalSection.Enter(); + + if (State != ProcessState.Exiting && State != ProcessState.Exited) + { + if (pause) + { + if (IsPaused) + { + KernelContext.CriticalSection.Leave(); + + return KernelResult.InvalidState; + } + + lock (_threadingLock) + { + foreach (KThread thread in _threads) + { + thread.Suspend(ThreadSchedState.ProcessPauseFlag); + } + } + + IsPaused = true; + } + else + { + if (!IsPaused) + { + KernelContext.CriticalSection.Leave(); + + return KernelResult.InvalidState; + } + + lock (_threadingLock) + { + foreach (KThread thread in _threads) + { + thread.Resume(ThreadSchedState.ProcessPauseFlag); + } + } + + IsPaused = false; + } + + KernelContext.CriticalSection.Leave(); + + return KernelResult.Success; + } + + KernelContext.CriticalSection.Leave(); + + return KernelResult.InvalidState; + } } } \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs b/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs index 7224cca121..396a79bae9 100644 --- a/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs +++ b/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs @@ -471,6 +471,29 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading KernelContext.CriticalSection.Leave(); } + public void Suspend(ThreadSchedState type) + { + _forcePauseFlags |= type; + + CombineForcePauseFlags(); + } + + public void Resume(ThreadSchedState type) + { + ThreadSchedState oldForcePauseFlags = _forcePauseFlags; + + _forcePauseFlags &= ~type; + + if ((oldForcePauseFlags & ~type) == ThreadSchedState.None) + { + ThreadSchedState oldSchedFlags = SchedFlags; + + SchedFlags &= ThreadSchedState.LowMask; + + AdjustScheduling(oldSchedFlags); + } + } + public KernelResult SetActivity(bool pause) { KernelResult result = KernelResult.Success; @@ -495,9 +518,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading // Pause, the force pause flag should be clear (thread is NOT paused). if ((_forcePauseFlags & ThreadSchedState.ThreadPauseFlag) == 0) { - _forcePauseFlags |= ThreadSchedState.ThreadPauseFlag; - - CombineForcePauseFlags(); + Suspend(ThreadSchedState.ThreadPauseFlag); } else { @@ -509,18 +530,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading // Unpause, the force pause flag should be set (thread is paused). if ((_forcePauseFlags & ThreadSchedState.ThreadPauseFlag) != 0) { - ThreadSchedState oldForcePauseFlags = _forcePauseFlags; - - _forcePauseFlags &= ~ThreadSchedState.ThreadPauseFlag; - - if ((oldForcePauseFlags & ~ThreadSchedState.ThreadPauseFlag) == ThreadSchedState.None) - { - ThreadSchedState oldSchedFlags = SchedFlags; - - SchedFlags &= ThreadSchedState.LowMask; - - AdjustScheduling(oldSchedFlags); - } + Resume(ThreadSchedState.ThreadPauseFlag); } else { @@ -832,19 +842,22 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading if (!IsSchedulable) { - // Ensure our thread is running and we have an event. - StartHostThread(); + if (!_forcedUnschedulable) + { + // Ensure our thread is running and we have an event. + StartHostThread(); - // If the thread is not schedulable, we want to just run or pause - // it directly as we don't care about priority or the core it is - // running on in this case. - if (SchedFlags == ThreadSchedState.Running) - { - _schedulerWaitEvent.Set(); - } - else - { - _schedulerWaitEvent.Reset(); + // If the thread is not schedulable, we want to just run or pause + // it directly as we don't care about priority or the core it is + // running on in this case. + if (SchedFlags == ThreadSchedState.Running) + { + _schedulerWaitEvent.Set(); + } + else + { + _schedulerWaitEvent.Reset(); + } } return; diff --git a/Ryujinx/Config.json b/Ryujinx/Config.json index d98a8e0cc5..6c8a226a9d 100644 --- a/Ryujinx/Config.json +++ b/Ryujinx/Config.json @@ -60,7 +60,8 @@ "hotkeys": { "toggle_vsync": "Tab", "screenshot": "F8", - "show_ui": "F4" + "show_ui": "F4", + "pause": "F5" }, "keyboard_config": [], "controller_config": [], diff --git a/Ryujinx/Configuration/ConfigurationFileFormat.cs b/Ryujinx/Configuration/ConfigurationFileFormat.cs index fbfa9c60d9..dda86ff581 100644 --- a/Ryujinx/Configuration/ConfigurationFileFormat.cs +++ b/Ryujinx/Configuration/ConfigurationFileFormat.cs @@ -14,7 +14,7 @@ namespace Ryujinx.Configuration /// /// The current version of the file format /// - public const int CurrentVersion = 31; + public const int CurrentVersion = 32; public int Version { get; set; } diff --git a/Ryujinx/Configuration/ConfigurationState.cs b/Ryujinx/Configuration/ConfigurationState.cs index 1476f62351..9cf3f65069 100644 --- a/Ryujinx/Configuration/ConfigurationState.cs +++ b/Ryujinx/Configuration/ConfigurationState.cs @@ -554,7 +554,8 @@ namespace Ryujinx.Configuration { ToggleVsync = Key.Tab, Screenshot = Key.F8, - ShowUi = Key.F4 + ShowUi = Key.F4, + Pause = Key.F5 }; Hid.InputConfig.Value = new List { @@ -913,6 +914,21 @@ namespace Ryujinx.Configuration configurationFileUpdated = true; } + if (configurationFileFormat.Version < 32) + { + Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 32."); + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, + Screenshot = configurationFileFormat.Hotkeys.Screenshot, + ShowUi = configurationFileFormat.Hotkeys.ShowUi, + Pause = Key.F5 + }; + + configurationFileUpdated = true; + } + Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog; Graphics.BackendThreading.Value = configurationFileFormat.BackendThreading; Graphics.ResScale.Value = configurationFileFormat.ResScale; diff --git a/Ryujinx/Ui/MainWindow.cs b/Ryujinx/Ui/MainWindow.cs index 41916b3ca4..6506f3241d 100644 --- a/Ryujinx/Ui/MainWindow.cs +++ b/Ryujinx/Ui/MainWindow.cs @@ -99,6 +99,8 @@ namespace Ryujinx.Ui [GUI] MenuItem _loadApplicationFolder; [GUI] MenuItem _appletMenu; [GUI] MenuItem _actionMenu; + [GUI] MenuItem _pauseEmulation; + [GUI] MenuItem _resumeEmulation; [GUI] MenuItem _stopEmulation; [GUI] MenuItem _simulateWakeUpMessage; [GUI] MenuItem _scanAmiibo; @@ -211,6 +213,7 @@ namespace Ryujinx.Ui } _actionMenu.Sensitive = false; + _pauseEmulation.Sensitive = false; if (ConfigurationState.Instance.Ui.GuiColumns.FavColumn) _favToggle.Active = true; if (ConfigurationState.Instance.Ui.GuiColumns.IconColumn) _iconToggle.Active = true; @@ -1281,9 +1284,38 @@ namespace Ryujinx.Ui UpdateGameMetadata(_emulationContext.Application.TitleIdText); } + _pauseEmulation.Visible = true; + _pauseEmulation.Sensitive = false; + _resumeEmulation.Visible = false; RendererWidget?.Exit(); } + private void PauseEmulation_Pressed(object sender, EventArgs args) + { + _pauseEmulation.Visible = false; + _resumeEmulation.Visible = true; + _emulationContext.System.TogglePauseEmulation(true); + } + + private void ResumeEmulation_Pressed(object sender, EventArgs args) + { + _pauseEmulation.Visible = true; + _resumeEmulation.Visible = false; + _emulationContext.System.TogglePauseEmulation(false); + } + + public void ActivatePauseMenu() + { + _pauseEmulation.Sensitive = true; + } + + public void TogglePause() + { + _pauseEmulation.Visible ^= true; + _resumeEmulation.Visible ^= true; + _emulationContext.System.TogglePauseEmulation(_resumeEmulation.Visible); + } + private void Installer_File_Pressed(object o, EventArgs args) { FileChooserDialog fileChooser = new FileChooserDialog("Choose the firmware file to open", this, FileChooserAction.Open, "Cancel", ResponseType.Cancel, "Open", ResponseType.Accept); diff --git a/Ryujinx/Ui/MainWindow.glade b/Ryujinx/Ui/MainWindow.glade index ff95506af5..2fa00688dc 100644 --- a/Ryujinx/Ui/MainWindow.glade +++ b/Ryujinx/Ui/MainWindow.glade @@ -294,15 +294,35 @@ True False - - True - False - Stop emulation of the current game and return to game selection - Stop Emulation - True - - - + + True + False + Pause emulation + Pause Emulation + True + + + + + + False + False + Resume emulation + Resume Emulation + True + + + + + + True + False + Stop emulation of the current game and return to game selection + Stop Emulation + True + + + True diff --git a/Ryujinx/Ui/RendererWidgetBase.cs b/Ryujinx/Ui/RendererWidgetBase.cs index da10ba471b..2ddd44dca8 100644 --- a/Ryujinx/Ui/RendererWidgetBase.cs +++ b/Ryujinx/Ui/RendererWidgetBase.cs @@ -389,6 +389,8 @@ namespace Ryujinx.Ui Device.Gpu.InitializeShaderCache(); Translator.IsReadyForTranslation.Set(); + (Toplevel as MainWindow)?.ActivatePauseMenu(); + while (_isActive) { if (_isStopped) @@ -590,6 +592,12 @@ namespace Ryujinx.Ui (Toplevel as MainWindow).ToggleExtraWidgets(true); } + if (currentHotkeyState.HasFlag(KeyboardHotkeyState.Pause) && + !_prevHotkeyState.HasFlag(KeyboardHotkeyState.Pause)) + { + (Toplevel as MainWindow)?.TogglePause(); + } + _prevHotkeyState = currentHotkeyState; } @@ -618,7 +626,8 @@ namespace Ryujinx.Ui None = 0, ToggleVSync = 1 << 0, Screenshot = 1 << 1, - ShowUi = 1 << 2 + ShowUi = 1 << 2, + Pause = 1 << 3 } private KeyboardHotkeyState GetHotkeyState() @@ -640,6 +649,11 @@ namespace Ryujinx.Ui state |= KeyboardHotkeyState.ShowUi; } + if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Pause)) + { + state |= KeyboardHotkeyState.Pause; + } + return state; } } diff --git a/Ryujinx/_schema.json b/Ryujinx/_schema.json index 356916a79d..3cedfcb06e 100644 --- a/Ryujinx/_schema.json +++ b/Ryujinx/_schema.json @@ -908,18 +908,6 @@ } }, "properties": { - "backend_threading": { - "$id": "#/properties/backend_threading", - "type": "string", - "title": "Backend Threading", - "description": "Whether backend threading is enabled or not. 'Auto' selects the most appropriate option for the current OS, vendor and backend.", - "default": "Auto", - "examples": [ - "Auto", - "Off", - "On" - ] - }, "res_scale": { "$id": "#/properties/res_scale", "type": "integer", @@ -1468,7 +1456,8 @@ "title": "Hotkey Controls", "required": [ "toggle_vsync", - "screenshot" + "screenshot", + "pause" ], "properties": { "toggle_vsync": { @@ -1482,6 +1471,12 @@ "$ref": "#/definitions/key", "title": "Screenshot", "default": "F8" + }, + "pause": { + "$id": "#/properties/hotkeys/properties/pause", + "$ref": "#/definitions/key", + "title": "Toggle Pause", + "default": "F5" } } },