From 117e32a6fffc30cdb895aa98483af7df353a8dd1 Mon Sep 17 00:00:00 2001
From: mpnico <mpnico@gmail.com>
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<OpenALHardwareDeviceSession, byte> _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<OpenALHardwareDeviceSession, byte>();
 
             _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<SDL2HardwareDeviceSession, byte> _sessions;
 
         public SDL2HardwareDeviceDriver()
         {
             _updateRequiredEvent = new ManualResetEvent(false);
+            _pauseEvent = new ManualResetEvent(true);
             _sessions = new ConcurrentDictionary<SDL2HardwareDeviceSession, byte>();
 
             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<SoundIoHardwareDeviceSession, byte> _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<SoundIoHardwareDeviceSession, byte>();
 
             _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
         /// </summary>
         private Thread _workerThread;
 
+        private bool _isRunning;
+
         /// <summary>
         /// Create a new <see cref="AudioManager"/>.
         /// </summary>
@@ -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
         /// </summary>
         private void Update()
         {
-            while (true)
+            while (_isRunning)
             {
                 int index = WaitHandle.WaitAny(_updateRequiredEvents);
 
@@ -118,6 +122,14 @@ namespace Ryujinx.Audio
             }
         }
 
+        /// <summary>
+        /// Stop updating the <see cref="AudioManager"/> without stopping the worker thread.
+        /// </summary>
+        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");
         }
 
+        /// <summary>
+        /// Stop sending commands to the <see cref="AudioProcessor"/> without stopping the worker thread.
+        /// </summary>
+        public void StopSendingCommands()
+        {
+            _isRunning = false;
+        }
+
         /// <summary>
         /// Worker main function. This is used to dispatch audio renderer commands to the <see cref="AudioProcessor"/>.
         /// </summary>
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
         /// <summary>
         /// The current version of the file format
         /// </summary>
-        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<InputConfig>
             {
@@ -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 @@
                     <property name="visible">True</property>
                     <property name="can_focus">False</property>
                     <child>
-                      <object class="GtkMenuItem" id="_stopEmulation">
-                        <property name="visible">True</property>
-                        <property name="can_focus">False</property>
-                        <property name="tooltip_text" translatable="yes">Stop emulation of the current game and return to game selection</property>
-                        <property name="label" translatable="yes">Stop Emulation</property>
-                        <property name="use_underline">True</property>
-                        <signal name="activate" handler="StopEmulation_Pressed" swapped="no"/>
-                      </object>
-                    </child>
+						<object class="GtkMenuItem" id="_pauseEmulation">
+							<property name="visible">True</property>
+							<property name="can_focus">False</property>
+							<property name="tooltip_text" translatable="yes">Pause emulation</property>
+							<property name="label" translatable="yes">Pause Emulation</property>
+							<property name="use_underline">True</property>
+							<signal name="activate" handler="PauseEmulation_Pressed" swapped="no"/>
+						</object>
+					</child>
+					  <child>
+						  <object class="GtkMenuItem" id="_resumeEmulation">
+							  <property name="visible">False</property>
+							  <property name="can_focus">False</property>
+							  <property name="tooltip_text" translatable="yes">Resume emulation</property>
+							  <property name="label" translatable="yes">Resume Emulation</property>
+							  <property name="use_underline">True</property>
+							  <signal name="activate" handler="ResumeEmulation_Pressed" swapped="no"/>
+						  </object>
+					  </child>
+					  <child>
+						<object class="GtkMenuItem" id="_stopEmulation">
+							<property name="visible">True</property>
+							<property name="can_focus">False</property>
+							<property name="tooltip_text" translatable="yes">Stop emulation of the current game and return to game selection</property>
+							<property name="label" translatable="yes">Stop Emulation</property>
+							<property name="use_underline">True</property>
+							<signal name="activate" handler="StopEmulation_Pressed" swapped="no"/>
+						</object>
+					</child>
                     <child>
                       <object class="GtkSeparatorMenuItem">
                         <property name="visible">True</property>
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"
         }
       }
     },