From d6b9babe1d73a78e963455a5a6ea3e7431a44ece Mon Sep 17 00:00:00 2001
From: Thog <me@thog.eu>
Date: Tue, 21 Jan 2020 23:23:11 +0100
Subject: [PATCH] Keep the GUI alive when closing a game (#888)

* Keep the GUI alive when closing a game

Make HLE.Switch init when starting a game and dispose it when closing
the GlScreen.

This also make HLE in charge of disposing the audio and gpu backend.

* Address Ac_k's comments

* Make sure to dispose the Discord module and use GTK quit method

Also update Discord Precense when closing a game.

* Make sure to dispose MainWindow

* Address gdk's comments
---
 Ryujinx.Graphics.Gpu/GpuContext.cs            |   1 +
 .../FileSystem/Content/ContentManager.cs      |  74 ++++-----
 Ryujinx.HLE/FileSystem/StorageId.cs           |   2 +-
 Ryujinx.HLE/FileSystem/VirtualFileSystem.cs   |  79 +++++++++-
 Ryujinx.HLE/HOS/Font/SharedFontManager.cs     |  16 +-
 Ryujinx.HLE/HOS/Horizon.cs                    |  89 +----------
 .../ApplicationProxy/IApplicationFunctions.cs |   2 +-
 .../HOS/Services/Fs/IFileSystemProxy.cs       |   2 +-
 .../HOS/Services/Sdb/Pl/ISharedFontManager.cs |   2 +-
 Ryujinx.HLE/Switch.cs                         |  25 ++--
 .../Configuration/DiscordIntegrationModule.cs |  18 +++
 Ryujinx/Program.cs                            |   1 -
 Ryujinx/Ui/ApplicationLibrary.cs              |  14 +-
 Ryujinx/Ui/GLScreen.cs                        |  13 +-
 Ryujinx/Ui/MainWindow.cs                      | 140 +++++++++++-------
 Ryujinx/Ui/Migration.cs                       |  21 ++-
 16 files changed, 284 insertions(+), 215 deletions(-)

diff --git a/Ryujinx.Graphics.Gpu/GpuContext.cs b/Ryujinx.Graphics.Gpu/GpuContext.cs
index b644c54ddb..a6ccc73524 100644
--- a/Ryujinx.Graphics.Gpu/GpuContext.cs
+++ b/Ryujinx.Graphics.Gpu/GpuContext.cs
@@ -116,6 +116,7 @@ namespace Ryujinx.Graphics.Gpu
             Methods.ShaderCache.Dispose();
             Methods.BufferManager.Dispose();
             Methods.TextureManager.Dispose();
+            Renderer.Dispose();
         }
     }
 }
\ No newline at end of file
diff --git a/Ryujinx.HLE/FileSystem/Content/ContentManager.cs b/Ryujinx.HLE/FileSystem/Content/ContentManager.cs
index 680ebd522e..1c6364c7df 100644
--- a/Ryujinx.HLE/FileSystem/Content/ContentManager.cs
+++ b/Ryujinx.HLE/FileSystem/Content/ContentManager.cs
@@ -14,7 +14,7 @@ using System.Linq;
 
 namespace Ryujinx.HLE.FileSystem.Content
 {
-    internal class ContentManager
+    public class ContentManager
     {
         private const ulong SystemVersionTitleId = 0x0100000000000809;
         private const ulong SystemUpdateTitleId  = 0x0100000000000816;
@@ -26,11 +26,11 @@ namespace Ryujinx.HLE.FileSystem.Content
 
         private SortedDictionary<(ulong titleId, NcaContentType type), string> _contentDictionary;
 
-        private Switch _device;
+        private VirtualFileSystem _virtualFileSystem;
 
         private readonly object _lock = new object();
 
-        public ContentManager(Switch device)
+        public ContentManager(VirtualFileSystem virtualFileSystem)
         {
             _contentDictionary = new SortedDictionary<(ulong, NcaContentType), string>();
             _locationEntries   = new Dictionary<StorageId, LinkedList<LocationEntry>>();
@@ -55,10 +55,10 @@ namespace Ryujinx.HLE.FileSystem.Content
                 { "FontNintendoExtended",          "nintendo_ext_003.bfttf" }
             };
 
-            _device = device;
+            _virtualFileSystem = virtualFileSystem;
         }
 
-        public void LoadEntries(bool ignoreMissingFonts = false)
+        public void LoadEntries(Switch device = null)
         {
             lock (_lock)
             {
@@ -74,7 +74,7 @@ namespace Ryujinx.HLE.FileSystem.Content
                     try
                     {
                         contentPathString   = LocationHelper.GetContentRoot(storageId);
-                        contentDirectory    = LocationHelper.GetRealPath(_device.FileSystem, contentPathString);
+                        contentDirectory    = LocationHelper.GetRealPath(_virtualFileSystem, contentPathString);
                         registeredDirectory = Path.Combine(contentDirectory, "registered");
                     }
                     catch (NotSupportedException)
@@ -99,7 +99,7 @@ namespace Ryujinx.HLE.FileSystem.Content
 
                             using (FileStream ncaFile = File.OpenRead(Directory.GetFiles(directoryPath)[0]))
                             {
-                                Nca nca = new Nca(_device.System.KeySet, ncaFile.AsStorage());
+                                Nca nca = new Nca(_virtualFileSystem.KeySet, ncaFile.AsStorage());
 
                                 string switchPath = contentPathString + ":/" + ncaFile.Name.Replace(contentDirectory, string.Empty).TrimStart(Path.DirectorySeparatorChar);
 
@@ -126,7 +126,7 @@ namespace Ryujinx.HLE.FileSystem.Content
 
                             using (FileStream ncaFile = new FileStream(filePath, FileMode.Open, FileAccess.Read))
                             {
-                                Nca nca = new Nca(_device.System.KeySet, ncaFile.AsStorage());
+                                Nca nca = new Nca(_virtualFileSystem.KeySet, ncaFile.AsStorage());
 
                                 string switchPath = contentPathString + ":/" + filePath.Replace(contentDirectory, string.Empty).TrimStart(Path.DirectorySeparatorChar);
 
@@ -156,9 +156,11 @@ namespace Ryujinx.HLE.FileSystem.Content
                     }
                 }
 
-                TimeManager.Instance.InitializeTimeZone(_device);
-
-                _device.System.Font.Initialize(this, ignoreMissingFonts);
+                if (device != null)
+                {
+                    TimeManager.Instance.InitializeTimeZone(device);
+                    device.System.Font.Initialize(this);
+                }
             }
         }
 
@@ -271,7 +273,7 @@ namespace Ryujinx.HLE.FileSystem.Content
                 return false;
             }
             
-            string installedPath = _device.FileSystem.SwitchPathToSystemPath(locationEntry.ContentPath);
+            string installedPath = _virtualFileSystem.SwitchPathToSystemPath(locationEntry.ContentPath);
 
             if (!string.IsNullOrWhiteSpace(installedPath))
             {
@@ -279,7 +281,7 @@ namespace Ryujinx.HLE.FileSystem.Content
                 {
                     using (FileStream file = new FileStream(installedPath, FileMode.Open, FileAccess.Read))
                     {
-                        Nca nca = new Nca(_device.System.KeySet, file.AsStorage());
+                        Nca nca = new Nca(_virtualFileSystem.KeySet, file.AsStorage());
                         bool contentCheck = nca.Header.ContentType == contentType;
 
                         return contentCheck;
@@ -351,7 +353,7 @@ namespace Ryujinx.HLE.FileSystem.Content
         public void InstallFirmware(string firmwareSource)
         {
             string contentPathString   = LocationHelper.GetContentRoot(StorageId.NandSystem);
-            string contentDirectory    = LocationHelper.GetRealPath(_device.FileSystem, contentPathString);
+            string contentDirectory    = LocationHelper.GetRealPath(_virtualFileSystem, contentPathString);
             string registeredDirectory = Path.Combine(contentDirectory, "registered");
             string temporaryDirectory  = Path.Combine(contentDirectory, "temp");
 
@@ -386,7 +388,7 @@ namespace Ryujinx.HLE.FileSystem.Content
                         }
                         break;
                     case ".xci":
-                        Xci xci = new Xci(_device.System.KeySet, file.AsStorage());
+                        Xci xci = new Xci(_virtualFileSystem.KeySet, file.AsStorage());
                         InstallFromCart(xci, temporaryDirectory);
                         break;
                     default:
@@ -418,7 +420,7 @@ namespace Ryujinx.HLE.FileSystem.Content
         {
             foreach (var entry in filesystem.EnumerateEntries("/", "*.nca"))
             {
-                Nca nca = new Nca(_device.System.KeySet, OpenPossibleFragmentedFile(filesystem, entry.FullPath, OpenMode.Read).AsStorage());
+                Nca nca = new Nca(_virtualFileSystem.KeySet, OpenPossibleFragmentedFile(filesystem, entry.FullPath, OpenMode.Read).AsStorage());
 
                 SaveNca(nca, entry.Name.Remove(entry.Name.IndexOf('.')), temporaryDirectory);
             }
@@ -537,7 +539,7 @@ namespace Ryujinx.HLE.FileSystem.Content
                             return VerifyAndGetVersionZip(archive);
                         }
                     case ".xci":
-                        Xci xci = new Xci(_device.System.KeySet, file.AsStorage());
+                        Xci xci = new Xci(_virtualFileSystem.KeySet, file.AsStorage());
 
                         if (xci.HasPartition(XciPartitionType.Update))
                         {
@@ -561,6 +563,8 @@ namespace Ryujinx.HLE.FileSystem.Content
 
             SystemVersion VerifyAndGetVersionZip(ZipArchive archive)
             {
+                IntegrityCheckLevel integrityCheckLevel = Switch.GetIntigrityCheckLevel();
+
                 SystemVersion systemVersion = null;
 
                 foreach (var entry in archive.Entries)
@@ -571,7 +575,7 @@ namespace Ryujinx.HLE.FileSystem.Content
                         {
                             IStorage storage = ncaStream.AsStorage();
 
-                            Nca nca = new Nca(_device.System.KeySet, storage);
+                            Nca nca = new Nca(_virtualFileSystem.KeySet, storage);
 
                             if (updateNcas.ContainsKey(nca.Header.TitleId))
                             {
@@ -598,9 +602,9 @@ namespace Ryujinx.HLE.FileSystem.Content
 
                     using (Stream ncaStream = GetZipStream(fileEntry))
                     {
-                        Nca metaNca = new Nca(_device.System.KeySet, ncaStream.AsStorage());
+                        Nca metaNca = new Nca(_virtualFileSystem.KeySet, ncaStream.AsStorage());
 
-                        IFileSystem fs = metaNca.OpenFileSystem(NcaSectionType.Data, _device.System.FsIntegrityCheckLevel);
+                        IFileSystem fs = metaNca.OpenFileSystem(NcaSectionType.Data, integrityCheckLevel);
 
                         string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath;
 
@@ -628,9 +632,9 @@ namespace Ryujinx.HLE.FileSystem.Content
 
                         using (Stream ncaStream = GetZipStream(archive.GetEntry(versionEntry)))
                         {
-                            Nca nca = new Nca(_device.System.KeySet, ncaStream.AsStorage());
+                            Nca nca = new Nca(_virtualFileSystem.KeySet, ncaStream.AsStorage());
 
-                            var romfs = nca.OpenFileSystem(NcaSectionType.Data, _device.System.FsIntegrityCheckLevel);
+                            var romfs = nca.OpenFileSystem(NcaSectionType.Data, integrityCheckLevel);
 
                             if (romfs.OpenFile(out IFile systemVersionFile, "/file", OpenMode.Read).IsSuccess())
                             {
@@ -663,9 +667,9 @@ namespace Ryujinx.HLE.FileSystem.Content
                             {
                                 using (Stream contentNcaStream = GetZipStream(contentZipEntry))
                                 {
-                                    Nca metaNca = new Nca(_device.System.KeySet, metaNcaStream.AsStorage());
+                                    Nca metaNca = new Nca(_virtualFileSystem.KeySet, metaNcaStream.AsStorage());
 
-                                    IFileSystem fs = metaNca.OpenFileSystem(NcaSectionType.Data, _device.System.FsIntegrityCheckLevel);
+                                    IFileSystem fs = metaNca.OpenFileSystem(NcaSectionType.Data, integrityCheckLevel);
 
                                     string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath;
 
@@ -722,6 +726,8 @@ namespace Ryujinx.HLE.FileSystem.Content
 
             SystemVersion VerifyAndGetVersion(IFileSystem filesystem)
             {
+                IntegrityCheckLevel integrityCheckLevel = Switch.GetIntigrityCheckLevel();
+
                 SystemVersion systemVersion = null;
 
                 CnmtContentMetaEntry[] metaEntries = null;
@@ -730,11 +736,11 @@ namespace Ryujinx.HLE.FileSystem.Content
                 {
                     IStorage ncaStorage = OpenPossibleFragmentedFile(filesystem, entry.FullPath, OpenMode.Read).AsStorage();
 
-                    Nca nca = new Nca(_device.System.KeySet, ncaStorage);
+                    Nca nca = new Nca(_virtualFileSystem.KeySet, ncaStorage);
 
                     if (nca.Header.TitleId == SystemUpdateTitleId && nca.Header.ContentType == NcaContentType.Meta)
                     {
-                        IFileSystem fs = nca.OpenFileSystem(NcaSectionType.Data, _device.System.FsIntegrityCheckLevel);
+                        IFileSystem fs = nca.OpenFileSystem(NcaSectionType.Data, integrityCheckLevel);
 
                         string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath;
 
@@ -752,7 +758,7 @@ namespace Ryujinx.HLE.FileSystem.Content
                     }
                     else if (nca.Header.TitleId == SystemVersionTitleId && nca.Header.ContentType == NcaContentType.Data)
                     {
-                        var romfs = nca.OpenFileSystem(NcaSectionType.Data, _device.System.FsIntegrityCheckLevel);
+                        var romfs = nca.OpenFileSystem(NcaSectionType.Data, integrityCheckLevel);
 
                         if (romfs.OpenFile(out IFile systemVersionFile, "/file", OpenMode.Read).IsSuccess())
                         {
@@ -797,9 +803,9 @@ namespace Ryujinx.HLE.FileSystem.Content
                         IStorage metaStorage = OpenPossibleFragmentedFile(filesystem, metaNcaEntry.path, OpenMode.Read).AsStorage();
                         IStorage contentStorage = OpenPossibleFragmentedFile(filesystem, contentPath, OpenMode.Read).AsStorage();
 
-                        Nca metaNca = new Nca(_device.System.KeySet, metaStorage);
+                        Nca metaNca = new Nca(_virtualFileSystem.KeySet, metaStorage);
 
-                        IFileSystem fs = metaNca.OpenFileSystem(NcaSectionType.Data, _device.System.FsIntegrityCheckLevel);
+                        IFileSystem fs = metaNca.OpenFileSystem(NcaSectionType.Data, integrityCheckLevel);
 
                         string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath;
 
@@ -851,7 +857,9 @@ namespace Ryujinx.HLE.FileSystem.Content
 
         public SystemVersion GetCurrentFirmwareVersion()
         {
-            LoadEntries(true);
+            IntegrityCheckLevel integrityCheckLevel = Switch.GetIntigrityCheckLevel();
+
+            LoadEntries();
 
             lock (_lock)
             {
@@ -861,15 +869,15 @@ namespace Ryujinx.HLE.FileSystem.Content
                 {
                     if (entry.ContentType == NcaContentType.Data)
                     {
-                        var path = _device.FileSystem.SwitchPathToSystemPath(entry.ContentPath);
+                        var path = _virtualFileSystem.SwitchPathToSystemPath(entry.ContentPath);
 
                         using (FileStream fileStream = File.OpenRead(path))
                         {
-                            Nca nca = new Nca(_device.System.KeySet, fileStream.AsStorage());
+                            Nca nca = new Nca(_virtualFileSystem.KeySet, fileStream.AsStorage());
 
                             if (nca.Header.TitleId == SystemVersionTitleId && nca.Header.ContentType == NcaContentType.Data)
                             {
-                                var romfs = nca.OpenFileSystem(NcaSectionType.Data, _device.System.FsIntegrityCheckLevel);
+                                var romfs = nca.OpenFileSystem(NcaSectionType.Data, integrityCheckLevel);
 
                                 if (romfs.OpenFile(out IFile systemVersionFile, "/file", OpenMode.Read).IsSuccess())
                                 {
diff --git a/Ryujinx.HLE/FileSystem/StorageId.cs b/Ryujinx.HLE/FileSystem/StorageId.cs
index 1ef38e0167..d4043e2c79 100644
--- a/Ryujinx.HLE/FileSystem/StorageId.cs
+++ b/Ryujinx.HLE/FileSystem/StorageId.cs
@@ -1,6 +1,6 @@
 namespace Ryujinx.HLE.FileSystem
 {
-    internal enum StorageId
+    public enum StorageId
     {
         None,
         Host,
diff --git a/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs b/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs
index 257a55a22f..070ec3bc0b 100644
--- a/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs
+++ b/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs
@@ -1,3 +1,7 @@
+using LibHac;
+using LibHac.Fs;
+using LibHac.FsService;
+using LibHac.FsSystem;
 using Ryujinx.HLE.FileSystem.Content;
 using Ryujinx.HLE.HOS;
 using System;
@@ -16,6 +20,16 @@ namespace Ryujinx.HLE.FileSystem
         public static string SystemNandPath = Path.Combine(NandPath, "system");
         public static string UserNandPath   = Path.Combine(NandPath, "user");
 
+        public Keyset           KeySet   { get; private set; }
+        public FileSystemServer FsServer { get; private set; }
+        public FileSystemClient FsClient { get; private set; }
+        public EmulatedGameCard GameCard { get; private set; }
+
+        public VirtualFileSystem()
+        {
+            Reload();
+        }
+
         public Stream RomFs { get; private set; }
 
         public void LoadRomFs(string fileName)
@@ -183,6 +197,69 @@ namespace Ryujinx.HLE.FileSystem
             return Path.Combine(appDataPath, BasePath);
         }
 
+        public void Reload()
+        {
+            ReloadKeySet();
+
+            LocalFileSystem serverBaseFs = new LocalFileSystem(GetBasePath());
+
+            DefaultFsServerObjects fsServerObjects = DefaultFsServerObjects.GetDefaultEmulatedCreators(serverBaseFs, KeySet);
+
+            GameCard = fsServerObjects.GameCard;
+
+            FileSystemServerConfig fsServerConfig = new FileSystemServerConfig
+            {
+                FsCreators     = fsServerObjects.FsCreators,
+                DeviceOperator = fsServerObjects.DeviceOperator,
+                ExternalKeySet = KeySet.ExternalKeySet
+            };
+
+            FsServer = new FileSystemServer(fsServerConfig);
+            FsClient = FsServer.CreateFileSystemClient();
+        }
+
+
+        private void ReloadKeySet()
+        {
+            string keyFile        = null;
+            string titleKeyFile   = null;
+            string consoleKeyFile = null;
+
+            string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+
+            LoadSetAtPath(Path.Combine(home, ".switch"));
+            LoadSetAtPath(GetSystemPath());
+
+            void LoadSetAtPath(string basePath)
+            {
+                string localKeyFile        = Path.Combine(basePath, "prod.keys");
+                string localTitleKeyFile   = Path.Combine(basePath, "title.keys");
+                string localConsoleKeyFile = Path.Combine(basePath, "console.keys");
+
+                if (File.Exists(localKeyFile))
+                {
+                    keyFile = localKeyFile;
+                }
+
+                if (File.Exists(localTitleKeyFile))
+                {
+                    titleKeyFile = localTitleKeyFile;
+                }
+
+                if (File.Exists(localConsoleKeyFile))
+                {
+                    consoleKeyFile = localConsoleKeyFile;
+                }
+            }
+
+            KeySet = ExternalKeyReader.ReadKeyFile(keyFile, titleKeyFile, consoleKeyFile);
+        }
+
+        public void Unload()
+        {
+            RomFs?.Dispose();
+        }
+
         public void Dispose()
         {
             Dispose(true);
@@ -192,7 +269,7 @@ namespace Ryujinx.HLE.FileSystem
         {
             if (disposing)
             {
-                RomFs?.Dispose();
+                Unload();
             }
         }
     }
diff --git a/Ryujinx.HLE/HOS/Font/SharedFontManager.cs b/Ryujinx.HLE/HOS/Font/SharedFontManager.cs
index e126cd5799..c54c8194bc 100644
--- a/Ryujinx.HLE/HOS/Font/SharedFontManager.cs
+++ b/Ryujinx.HLE/HOS/Font/SharedFontManager.cs
@@ -44,15 +44,15 @@ namespace Ryujinx.HLE.HOS.Font
             _fontsPath = Path.Combine(device.FileSystem.GetSystemPath(), "fonts");
         }
 
-        public void Initialize(ContentManager contentManager, bool ignoreMissingFonts)
+        public void Initialize(ContentManager contentManager)
         {
             _fontData?.Clear();
             _fontData = null;
 
-            EnsureInitialized(contentManager, ignoreMissingFonts);
+            EnsureInitialized(contentManager);
         }
 
-        public void EnsureInitialized(ContentManager contentManager, bool ignoreMissingFonts)
+        public void EnsureInitialized(ContentManager contentManager)
         {
             if (_fontData == null)
             {
@@ -120,12 +120,10 @@ namespace Ryujinx.HLE.HOS.Font
 
                         return info;
                     }
-                    else if (!ignoreMissingFonts)
+                    else
                     {
                         throw new InvalidSystemResourceException($"Font \"{name}.ttf\" not found. Please provide it in \"{_fontsPath}\".");
                     }
-
-                    return new FontInfo();
                 }
 
                 _fontData = new Dictionary<SharedFontType, FontInfo>
@@ -138,7 +136,7 @@ namespace Ryujinx.HLE.HOS.Font
                     { SharedFontType.NintendoEx,          CreateFont("FontNintendoExtended")          }
                 };
 
-                if (fontOffset > Horizon.FontSize && !ignoreMissingFonts)
+                if (fontOffset > Horizon.FontSize)
                 {
                     throw new InvalidSystemResourceException(
                         $"The sum of all fonts size exceed the shared memory size. " +
@@ -161,14 +159,14 @@ namespace Ryujinx.HLE.HOS.Font
 
         public int GetFontSize(SharedFontType fontType)
         {
-            EnsureInitialized(_device.System.ContentManager, false);
+            EnsureInitialized(_device.System.ContentManager);
 
             return _fontData[fontType].Size;
         }
 
         public int GetSharedMemoryAddressOffset(SharedFontType fontType)
         {
-            EnsureInitialized(_device.System.ContentManager, false);
+            EnsureInitialized(_device.System.ContentManager);
 
             return _fontData[fontType].Offset + 8;
         }
diff --git a/Ryujinx.HLE/HOS/Horizon.cs b/Ryujinx.HLE/HOS/Horizon.cs
index 428e19227d..e4e0894340 100644
--- a/Ryujinx.HLE/HOS/Horizon.cs
+++ b/Ryujinx.HLE/HOS/Horizon.cs
@@ -2,7 +2,6 @@ using LibHac;
 using LibHac.Account;
 using LibHac.Common;
 using LibHac.Fs;
-using LibHac.FsService;
 using LibHac.FsSystem;
 using LibHac.FsSystem.NcaUtils;
 using LibHac.Ncm;
@@ -105,7 +104,7 @@ namespace Ryujinx.HLE.HOS
 
         internal KEvent VsyncEvent { get; private set; }
 
-        public Keyset KeySet { get; private set; }
+        public Keyset KeySet => Device.FileSystem.KeySet;
 
         private bool _hasStarted;
 
@@ -122,12 +121,7 @@ namespace Ryujinx.HLE.HOS
 
         internal long HidBaseAddress { get; private set; }
 
-        internal FileSystemServer FsServer { get; private set; }
-        public FileSystemClient FsClient { get; private set; }
-
-        internal EmulatedGameCard GameCard { get; private set; }
-
-        public Horizon(Switch device)
+        public Horizon(Switch device, ContentManager contentManager)
         {
             ControlData = new BlitStruct<ApplicationControlProperty>(1);
 
@@ -211,9 +205,7 @@ namespace Ryujinx.HLE.HOS
 
             VsyncEvent = new KEvent(this);
 
-            LoadKeySet();
-
-            ContentManager = new ContentManager(device);
+            ContentManager = contentManager;
 
             // TODO: use set:sys (and get external clock source id from settings)
             // TODO: use "time!standard_steady_clock_rtc_update_interval_minutes" and implement a worker thread to be accurate.
@@ -239,22 +231,6 @@ namespace Ryujinx.HLE.HOS
             // FIXME: TimeZone shoud be init here but it's actually done in ContentManager
 
             TimeServiceManager.Instance.SetupEphemeralNetworkSystemClock();
-
-            LocalFileSystem serverBaseFs = new LocalFileSystem(device.FileSystem.GetBasePath());
-
-            DefaultFsServerObjects fsServerObjects = DefaultFsServerObjects.GetDefaultEmulatedCreators(serverBaseFs, KeySet);
-
-            GameCard = fsServerObjects.GameCard;
-
-            FileSystemServerConfig fsServerConfig = new FileSystemServerConfig
-            {
-                FsCreators     = fsServerObjects.FsCreators,
-                DeviceOperator = fsServerObjects.DeviceOperator,
-                ExternalKeySet = KeySet.ExternalKeySet
-            };
-
-            FsServer = new FileSystemServer(fsServerConfig);
-            FsClient = FsServer.CreateFileSystemClient();
         }
 
         public void LoadCart(string exeFsDir, string romFsFile = null)
@@ -284,7 +260,7 @@ namespace Ryujinx.HLE.HOS
                 return;
             }
 
-            ContentManager.LoadEntries();
+            ContentManager.LoadEntries(Device);
 
             LoadNca(mainNca, patchNca, controlNca);
         }
@@ -578,7 +554,7 @@ namespace Ryujinx.HLE.HOS
             LoadNso("subsdk");
             LoadNso("sdk");
 
-            ContentManager.LoadEntries();
+            ContentManager.LoadEntries(Device);
 
             ProgramLoader.LoadStaticObjects(this, metaData, staticObjects.ToArray());
         }
@@ -671,7 +647,7 @@ namespace Ryujinx.HLE.HOS
                 staticObject = new NxStaticObject(input);
             }
 
-            ContentManager.LoadEntries();
+            ContentManager.LoadEntries(Device);
 
             TitleName = metaData.TitleName;
             TitleId   = metaData.Aci0.TitleId;
@@ -712,7 +688,7 @@ namespace Ryujinx.HLE.HOS
                     "No control file was found for this game. Using a dummy one instead. This may cause inaccuracies in some games.");
             }
 
-            Result rc = EnsureApplicationSaveData(FsClient, out _, titleId, ref ControlData.Value, ref user);
+            Result rc = EnsureApplicationSaveData(Device.FileSystem.FsClient, out _, titleId, ref ControlData.Value, ref user);
 
             if (rc.IsFailure())
             {
@@ -722,57 +698,6 @@ namespace Ryujinx.HLE.HOS
             return rc;
         }
 
-        public void LoadKeySet()
-        {
-            string keyFile        = null;
-            string titleKeyFile   = null;
-            string consoleKeyFile = null;
-
-            string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
-
-            LoadSetAtPath(Path.Combine(home, ".switch"));
-            LoadSetAtPath(Device.FileSystem.GetSystemPath());
-
-            KeySet = ExternalKeyReader.ReadKeyFile(keyFile, titleKeyFile, consoleKeyFile);
-
-            void LoadSetAtPath(string basePath)
-            {
-                string localKeyFile        = Path.Combine(basePath,    "prod.keys");
-                string localTitleKeyFile   = Path.Combine(basePath,   "title.keys");
-                string localConsoleKeyFile = Path.Combine(basePath, "console.keys");
-
-                if (File.Exists(localKeyFile))
-                {
-                    keyFile = localKeyFile;
-                }
-
-                if (File.Exists(localTitleKeyFile))
-                {
-                    titleKeyFile = localTitleKeyFile;
-                }
-
-                if (File.Exists(localConsoleKeyFile))
-                {
-                    consoleKeyFile = localConsoleKeyFile;
-                }
-            }
-        }
-
-        public SystemVersion VerifyFirmwarePackage(string firmwarePackage)
-        {
-            return ContentManager.VerifyFirmwarePackage(firmwarePackage);
-        }
-
-        public SystemVersion GetCurrentFirmwareVersion()
-        {
-            return ContentManager.GetCurrentFirmwareVersion();
-        }
-
-        public void InstallFirmware(string firmwarePackage)
-        {
-            ContentManager.InstallFirmware(firmwarePackage);
-        }
-
         public void SignalVsync()
         {
             VsyncEvent.ReadableEvent.Signal();
diff --git a/Ryujinx.HLE/HOS/Services/Am/AppletOE/ApplicationProxyService/ApplicationProxy/IApplicationFunctions.cs b/Ryujinx.HLE/HOS/Services/Am/AppletOE/ApplicationProxyService/ApplicationProxy/IApplicationFunctions.cs
index 904264aa87..0840a91372 100644
--- a/Ryujinx.HLE/HOS/Services/Am/AppletOE/ApplicationProxyService/ApplicationProxy/IApplicationFunctions.cs
+++ b/Ryujinx.HLE/HOS/Services/Am/AppletOE/ApplicationProxyService/ApplicationProxy/IApplicationFunctions.cs
@@ -60,7 +60,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
                     "No control file was found for this game. Using a dummy one instead. This may cause inaccuracies in some games.");
             }
 
-            Result result = EnsureApplicationSaveData(context.Device.System.FsClient, out long requiredSize, titleId,
+            Result result = EnsureApplicationSaveData(context.Device.FileSystem.FsClient, out long requiredSize, titleId,
                 ref context.Device.System.ControlData.Value, ref userId);
 
             context.ResponseData.Write(requiredSize);
diff --git a/Ryujinx.HLE/HOS/Services/Fs/IFileSystemProxy.cs b/Ryujinx.HLE/HOS/Services/Fs/IFileSystemProxy.cs
index 02743a47b7..7c31814f30 100644
--- a/Ryujinx.HLE/HOS/Services/Fs/IFileSystemProxy.cs
+++ b/Ryujinx.HLE/HOS/Services/Fs/IFileSystemProxy.cs
@@ -21,7 +21,7 @@ namespace Ryujinx.HLE.HOS.Services.Fs
 
         public IFileSystemProxy(ServiceCtx context)
         {
-            _baseFileSystemProxy = context.Device.System.FsServer.CreateFileSystemProxyService();
+            _baseFileSystemProxy = context.Device.FileSystem.FsServer.CreateFileSystemProxyService();
         }
 
         [Command(1)]
diff --git a/Ryujinx.HLE/HOS/Services/Sdb/Pl/ISharedFontManager.cs b/Ryujinx.HLE/HOS/Services/Sdb/Pl/ISharedFontManager.cs
index 418c15f2c2..4560d9545c 100644
--- a/Ryujinx.HLE/HOS/Services/Sdb/Pl/ISharedFontManager.cs
+++ b/Ryujinx.HLE/HOS/Services/Sdb/Pl/ISharedFontManager.cs
@@ -61,7 +61,7 @@ namespace Ryujinx.HLE.HOS.Services.Sdb.Pl
         // GetSharedMemoryNativeHandle() -> handle<copy>
         public ResultCode GetSharedMemoryNativeHandle(ServiceCtx context)
         {
-            context.Device.System.Font.EnsureInitialized(context.Device.System.ContentManager, false);
+            context.Device.System.Font.EnsureInitialized(context.Device.System.ContentManager);
 
             if (context.Process.HandleTable.GenerateHandle(context.Device.System.FontSharedMem, out int handle) != KernelResult.Success)
             {
diff --git a/Ryujinx.HLE/Switch.cs b/Ryujinx.HLE/Switch.cs
index 90723c4caf..b7db5c6251 100644
--- a/Ryujinx.HLE/Switch.cs
+++ b/Ryujinx.HLE/Switch.cs
@@ -4,6 +4,7 @@ using Ryujinx.Configuration;
 using Ryujinx.Graphics.GAL;
 using Ryujinx.Graphics.Gpu;
 using Ryujinx.HLE.FileSystem;
+using Ryujinx.HLE.FileSystem.Content;
 using Ryujinx.HLE.HOS;
 using Ryujinx.HLE.HOS.Services;
 using Ryujinx.HLE.HOS.SystemState;
@@ -15,11 +16,11 @@ namespace Ryujinx.HLE
 {
     public class Switch : IDisposable
     {
-        internal IAalOutput AudioOut { get; private set; }
+        public IAalOutput AudioOut { get; private set; }
 
         internal DeviceMemory Memory { get; private set; }
 
-        internal GpuContext Gpu { get; private set; }
+        public GpuContext Gpu { get; private set; }
 
         public VirtualFileSystem FileSystem { get; private set; }
 
@@ -35,7 +36,7 @@ namespace Ryujinx.HLE
 
         public event EventHandler Finish;
 
-        public Switch(IRenderer renderer, IAalOutput audioOut)
+        public Switch(VirtualFileSystem fileSystem, ContentManager contentManager, IRenderer renderer, IAalOutput audioOut)
         {
             if (renderer == null)
             {
@@ -53,9 +54,9 @@ namespace Ryujinx.HLE
 
             Gpu = new GpuContext(renderer);
 
-            FileSystem = new VirtualFileSystem();
+            FileSystem = fileSystem;
 
-            System = new Horizon(this);
+            System = new Horizon(this, contentManager);
 
             Statistics = new PerformanceStatistics();
 
@@ -78,15 +79,20 @@ namespace Ryujinx.HLE
                 System.EnableMultiCoreScheduling();
             }
 
-            System.FsIntegrityCheckLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
-                ? IntegrityCheckLevel.ErrorOnInvalid
-                : IntegrityCheckLevel.None;
+            System.FsIntegrityCheckLevel = GetIntigrityCheckLevel();
 
             System.GlobalAccessLogMode = ConfigurationState.Instance.System.FsGlobalAccessLogMode;
 
             ServiceConfiguration.IgnoreMissingServices = ConfigurationState.Instance.System.IgnoreMissingServices;
         }
 
+        public static IntegrityCheckLevel GetIntigrityCheckLevel()
+        {
+            return ConfigurationState.Instance.System.EnableFsIntegrityChecks
+                ? IntegrityCheckLevel.ErrorOnInvalid
+                : IntegrityCheckLevel.None;
+        }
+
         public void LoadCart(string exeFsDir, string romFsFile = null)
         {
             System.LoadCart(exeFsDir, romFsFile);
@@ -129,7 +135,7 @@ namespace Ryujinx.HLE
 
         internal void Unload()
         {
-            FileSystem.Dispose();
+            FileSystem.Unload();
 
             Memory.Dispose();
         }
@@ -150,6 +156,7 @@ namespace Ryujinx.HLE
             {
                 System.Dispose();
                 VsyncEvent.Dispose();
+                AudioOut.Dispose();
             }
         }
     }
diff --git a/Ryujinx/Configuration/DiscordIntegrationModule.cs b/Ryujinx/Configuration/DiscordIntegrationModule.cs
index 15540a1c82..87cff00806 100644
--- a/Ryujinx/Configuration/DiscordIntegrationModule.cs
+++ b/Ryujinx/Configuration/DiscordIntegrationModule.cs
@@ -88,5 +88,23 @@ namespace Ryujinx.Configuration
 
             DiscordClient?.SetPresence(DiscordPresence);
         }
+
+        public static void SwitchToMainMenu()
+        {
+            DiscordPresence.Details               = "Main Menu";
+            DiscordPresence.State                 = "Idling";
+            DiscordPresence.Assets.LargeImageKey  = "ryujinx";
+            DiscordPresence.Assets.LargeImageText = LargeDescription;
+            DiscordPresence.Assets.SmallImageKey  = null;
+            DiscordPresence.Assets.SmallImageText = null;
+            DiscordPresence.Timestamps            = new Timestamps(DateTime.UtcNow);
+
+            DiscordClient?.SetPresence(DiscordPresence);
+        }
+
+        public static void Exit()
+        {
+            DiscordClient?.Dispose();
+        }
     }
 }
diff --git a/Ryujinx/Program.cs b/Ryujinx/Program.cs
index fbf1196d0c..9bab74a265 100644
--- a/Ryujinx/Program.cs
+++ b/Ryujinx/Program.cs
@@ -43,7 +43,6 @@ namespace Ryujinx
                 ConfigurationState.Instance.ToFileFormat().SaveConfig(configurationPath);
             }
 
-
             Profile.Initialize();
 
             Application.Init();
diff --git a/Ryujinx/Ui/ApplicationLibrary.cs b/Ryujinx/Ui/ApplicationLibrary.cs
index 2ab6107776..3984b78dc7 100644
--- a/Ryujinx/Ui/ApplicationLibrary.cs
+++ b/Ryujinx/Ui/ApplicationLibrary.cs
@@ -7,6 +7,7 @@ using LibHac.FsSystem.NcaUtils;
 using LibHac.Ncm;
 using LibHac.Spl;
 using Ryujinx.Common.Logging;
+using Ryujinx.Configuration.System;
 using Ryujinx.HLE.FileSystem;
 using Ryujinx.HLE.Loaders.Npdm;
 using System;
@@ -20,7 +21,6 @@ using Utf8Json;
 using Utf8Json.Resolvers;
 
 using RightsId = LibHac.Fs.RightsId;
-using TitleLanguage = Ryujinx.HLE.HOS.SystemState.TitleLanguage;
 
 namespace Ryujinx.Ui
 {
@@ -34,15 +34,15 @@ namespace Ryujinx.Ui
         private static readonly byte[] _nroIcon = GetResourceBytes("Ryujinx.Ui.assets.NROIcon.png");
         private static readonly byte[] _nsoIcon = GetResourceBytes("Ryujinx.Ui.assets.NSOIcon.png");
 
-        private static Keyset        _keySet;
-        private static TitleLanguage _desiredTitleLanguage;
+        private static Keyset   _keySet;
+        private static Language _desiredTitleLanguage;
 
-        public static void LoadApplications(List<string> appDirs, Keyset keySet, TitleLanguage desiredTitleLanguage, FileSystemClient fsClient = null, VirtualFileSystem vfs = null)
+        public static void LoadApplications(List<string> appDirs, VirtualFileSystem virtualFileSystem, Language desiredTitleLanguage)
         {
             int numApplicationsFound  = 0;
             int numApplicationsLoaded = 0;
 
-            _keySet               = keySet;
+            _keySet               = virtualFileSystem.KeySet;
             _desiredTitleLanguage = desiredTitleLanguage;
 
             // Builds the applications list with paths to found applications
@@ -346,11 +346,11 @@ namespace Ryujinx.Ui
                     filter.SetUserId(new UserId(1, 0));
                     filter.SetTitleId(new TitleId(titleIdNum));
 
-                    Result result = fsClient.FindSaveDataWithFilter(out SaveDataInfo saveDataInfo, SaveDataSpaceId.User, ref filter);
+                    Result result = virtualFileSystem.FsClient.FindSaveDataWithFilter(out SaveDataInfo saveDataInfo, SaveDataSpaceId.User, ref filter);
 
                     if (result.IsSuccess())
                     {
-                        saveDataPath = Path.Combine(vfs.GetNandPath(), $"user/save/{saveDataInfo.SaveDataId:x16}");
+                        saveDataPath = Path.Combine(virtualFileSystem.GetNandPath(), $"user/save/{saveDataInfo.SaveDataId:x16}");
                     }
                 }
 
diff --git a/Ryujinx/Ui/GLScreen.cs b/Ryujinx/Ui/GLScreen.cs
index d32ddb5ca5..295cf22f49 100644
--- a/Ryujinx/Ui/GLScreen.cs
+++ b/Ryujinx/Ui/GLScreen.cs
@@ -45,14 +45,20 @@ namespace Ryujinx.Ui
         private ProfileWindowManager _profileWindow;
 #endif
 
-        public GlScreen(Switch device, Renderer renderer)
+        public GlScreen(Switch device)
             : base(1280, 720,
             new GraphicsMode(), "Ryujinx", 0,
             DisplayDevice.Default, 3, 3,
             GraphicsContextFlags.ForwardCompatible)
         {
-            _device   = device;
-            _renderer = renderer;
+            _device = device;
+
+            if (!(device.Gpu.Renderer is Renderer))
+            {
+                throw new NotSupportedException($"GPU renderer must be an OpenGL renderer when using GlScreen!");
+            }
+
+            _renderer = (Renderer)device.Gpu.Renderer;
 
             _primaryController = new Input.NpadController(ConfigurationState.Instance.Hid.JoystickControls);
 
@@ -108,7 +114,6 @@ namespace Ryujinx.Ui
             }
 
             _device.DisposeGpu();
-            _renderer.Dispose();
         }
 
         public void MainLoop()
diff --git a/Ryujinx/Ui/MainWindow.cs b/Ryujinx/Ui/MainWindow.cs
index 451df2fd95..f9f3657606 100644
--- a/Ryujinx/Ui/MainWindow.cs
+++ b/Ryujinx/Ui/MainWindow.cs
@@ -3,8 +3,10 @@ using JsonPrettyPrinterPlus;
 using Ryujinx.Audio;
 using Ryujinx.Common.Logging;
 using Ryujinx.Configuration;
+using Ryujinx.Graphics.GAL;
 using Ryujinx.Graphics.OpenGL;
 using Ryujinx.HLE.FileSystem;
+using Ryujinx.HLE.FileSystem.Content;
 using Ryujinx.Profiler;
 using System;
 using System.Diagnostics;
@@ -22,11 +24,10 @@ namespace Ryujinx.Ui
 {
     public class MainWindow : Window
     {
-        private static HLE.Switch _device;
+        private static VirtualFileSystem _virtualFileSystem;
+        private static ContentManager    _contentManager;
 
-        private static Renderer _renderer;
-
-        private static IAalOutput _audioOut;
+        private static HLE.Switch _emulationContext;
 
         private static GlScreen _screen;
 
@@ -75,29 +76,29 @@ namespace Ryujinx.Ui
 
             _gameTable.ButtonReleaseEvent += Row_Clicked;
 
+            // First we check that a migration isn't needed. (because VirtualFileSystem will create the new directory otherwise)
             bool continueWithStartup = Migration.PromptIfMigrationNeededForStartup(this, out bool migrationNeeded);
             if (!continueWithStartup)
             {
-                End();
+                End(null);
             }
 
-            _renderer = new Renderer();
-
-            _audioOut = InitializeAudioEngine();
-
-            // TODO: Initialization and dispose of HLE.Switch when starting/stoping emulation.
-            _device = InitializeSwitchInstance();
+            _virtualFileSystem = new VirtualFileSystem();
+            _contentManager    = new ContentManager(_virtualFileSystem);
 
             if (migrationNeeded)
             {
-                bool migrationSuccessful = Migration.DoMigrationForStartup(this, _device);
+                bool migrationSuccessful = Migration.DoMigrationForStartup(this, _virtualFileSystem);
 
                 if (!migrationSuccessful)
                 {
-                    End();
+                    End(null);
                 }
             }
 
+            // Make sure that everything is loaded.
+            _virtualFileSystem.Reload();
+
             _treeView = _gameTable;
 
             ApplyTheme();
@@ -199,7 +200,9 @@ namespace Ryujinx.Ui
 
         private HLE.Switch InitializeSwitchInstance()
         {
-            HLE.Switch instance = new HLE.Switch(_renderer, _audioOut);
+            _virtualFileSystem.Reload();
+
+            HLE.Switch instance = new HLE.Switch(_virtualFileSystem, _contentManager, InitializeRenderer(), InitializeAudioEngine());
 
             instance.Initialize();
 
@@ -218,8 +221,7 @@ namespace Ryujinx.Ui
             _tableStore.Clear();
 
             await Task.Run(() => ApplicationLibrary.LoadApplications(ConfigurationState.Instance.Ui.GameDirs,
-                _device.System.KeySet, _device.System.State.DesiredTitleLanguage, _device.System.FsClient,
-                _device.FileSystem));
+                _virtualFileSystem, ConfigurationState.Instance.System.Language));
 
             _updatingGameTable = false;
         }
@@ -234,8 +236,10 @@ namespace Ryujinx.Ui
             {
                 Logger.RestartTime();
 
+                HLE.Switch device = InitializeSwitchInstance();
+
                 // TODO: Move this somewhere else + reloadable?
-                Ryujinx.Graphics.Gpu.GraphicsConfig.ShadersDumpPath = ConfigurationState.Instance.Graphics.ShadersDumpPath;
+                Graphics.Gpu.GraphicsConfig.ShadersDumpPath = ConfigurationState.Instance.Graphics.ShadersDumpPath;
 
                 if (Directory.Exists(path))
                 {
@@ -249,12 +253,12 @@ namespace Ryujinx.Ui
                     if (romFsFiles.Length > 0)
                     {
                         Logger.PrintInfo(LogClass.Application, "Loading as cart with RomFS.");
-                        _device.LoadCart(path, romFsFiles[0]);
+                        device.LoadCart(path, romFsFiles[0]);
                     }
                     else
                     {
                         Logger.PrintInfo(LogClass.Application, "Loading as cart WITHOUT RomFS.");
-                        _device.LoadCart(path);
+                        device.LoadCart(path);
                     }
                 }
                 else if (File.Exists(path))
@@ -263,22 +267,22 @@ namespace Ryujinx.Ui
                     {
                         case ".xci":
                             Logger.PrintInfo(LogClass.Application, "Loading as XCI.");
-                            _device.LoadXci(path);
+                            device.LoadXci(path);
                             break;
                         case ".nca":
                             Logger.PrintInfo(LogClass.Application, "Loading as NCA.");
-                            _device.LoadNca(path);
+                            device.LoadNca(path);
                             break;
                         case ".nsp":
                         case ".pfs0":
                             Logger.PrintInfo(LogClass.Application, "Loading as NSP.");
-                            _device.LoadNsp(path);
+                            device.LoadNsp(path);
                             break;
                         default:
                             Logger.PrintInfo(LogClass.Application, "Loading as homebrew.");
                             try
                             {
-                                _device.LoadProgram(path);
+                                device.LoadProgram(path);
                             }
                             catch (ArgumentOutOfRangeException)
                             {
@@ -290,13 +294,15 @@ namespace Ryujinx.Ui
                 else
                 {
                     Logger.PrintWarning(LogClass.Application, "Please specify a valid XCI/NCA/NSP/PFS0/NRO file.");
-                    End();
+                    End(device);
                 }
 
+                _emulationContext = device;
+
 #if MACOS_BUILD
-                CreateGameWindow();
+                CreateGameWindow(device);
 #else
-                new Thread(CreateGameWindow).Start();
+                new Thread(() => CreateGameWindow(device)).Start();
 #endif
 
                 _gameLoaded              = true;
@@ -305,28 +311,55 @@ namespace Ryujinx.Ui
                 _firmwareInstallFile.Sensitive      = false;
                 _firmwareInstallDirectory.Sensitive = false;
 
-                DiscordIntegrationModule.SwitchToPlayingState(_device.System.TitleIdText, _device.System.TitleName);
+                DiscordIntegrationModule.SwitchToPlayingState(device.System.TitleIdText, device.System.TitleName);
 
-                ApplicationLibrary.LoadAndSaveMetaData(_device.System.TitleIdText, appMetadata =>
+                ApplicationLibrary.LoadAndSaveMetaData(device.System.TitleIdText, appMetadata =>
                 {
                     appMetadata.LastPlayed = DateTime.UtcNow.ToString();
                 });
             }
         }
 
-        private static void CreateGameWindow()
+        private void CreateGameWindow(HLE.Switch device)
         {
-            _device.Hid.InitializePrimaryController(ConfigurationState.Instance.Hid.ControllerType);
+            device.Hid.InitializePrimaryController(ConfigurationState.Instance.Hid.ControllerType);
 
-            using (_screen = new GlScreen(_device, _renderer))
+            using (_screen = new GlScreen(device))
             {
                 _screen.MainLoop();
+            }
 
-                End();
+            device.Dispose();
+
+            _emulationContext = null;
+            _screen           = null;
+            _gameLoaded       = false;
+
+            DiscordIntegrationModule.SwitchToMainMenu();
+
+            Application.Invoke(delegate
+            {
+                _stopEmulation.Sensitive            = false;
+                _firmwareInstallFile.Sensitive      = true;
+                _firmwareInstallDirectory.Sensitive = true;
+            });
+        }
+
+        private static void UpdateGameMetadata(string titleId)
+        {
+            if (_gameLoaded)
+            {
+                ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata =>
+                {
+                    DateTime lastPlayedDateTime = DateTime.Parse(appMetadata.LastPlayed);
+                    double   sessionTimePlayed  = DateTime.UtcNow.Subtract(lastPlayedDateTime).TotalSeconds;
+
+                    appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero);
+                });
             }
         }
 
-        private static void End()
+        private void End(HLE.Switch device)
         {
             if (_ending)
             {
@@ -335,22 +368,23 @@ namespace Ryujinx.Ui
 
             _ending = true;
 
-            if (_gameLoaded)
+            if (device != null)
             {
-                ApplicationLibrary.LoadAndSaveMetaData(_device.System.TitleIdText, appMetadata =>
-                {
-                    DateTime lastPlayedDateTime = DateTime.Parse(appMetadata.LastPlayed);
-                    double sessionTimePlayed = DateTime.UtcNow.Subtract(lastPlayedDateTime).TotalSeconds;
-
-                    appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero);
-                });
+                UpdateGameMetadata(device.System.TitleIdText);
             }
 
+            Dispose();
+
             Profile.FinishProfiling();
-            _device?.Dispose();
-            _audioOut?.Dispose();
+            device?.Dispose();
+            DiscordIntegrationModule.Exit();
             Logger.Shutdown();
-            Environment.Exit(0);
+            Application.Quit();
+        }
+
+        private static IRenderer InitializeRenderer()
+        {
+            return new Renderer();
         }
 
         /// <summary>
@@ -427,7 +461,7 @@ namespace Ryujinx.Ui
 
             if (treeIter.UserData == IntPtr.Zero) return;
 
-            GameTableContextMenu contextMenu = new GameTableContextMenu(_tableStore, treeIter, _device.System.FsClient);
+            GameTableContextMenu contextMenu = new GameTableContextMenu(_tableStore, treeIter, _virtualFileSystem.FsClient);
             contextMenu.ShowAll();
             contextMenu.PopupAtPointer(null);
         }
@@ -477,20 +511,18 @@ namespace Ryujinx.Ui
         private void Exit_Pressed(object sender, EventArgs args)
         {
             _screen?.Exit();
-            End();
+            End(_emulationContext);
         }
 
         private void Window_Close(object sender, DeleteEventArgs args)
         {
             _screen?.Exit();
-            End();
+            End(_emulationContext);
         }
 
         private void StopEmulation_Pressed(object sender, EventArgs args)
         {
-            // TODO: Write logic to kill running game
-
-            _gameLoaded = false;
+            _screen?.Exit();
         }
 
         private void Installer_File_Pressed(object o, EventArgs args)
@@ -525,7 +557,7 @@ namespace Ryujinx.Ui
 
         private void RefreshFirmwareLabel()
         {
-            var currentFirmware = _device.System.GetCurrentFirmwareVersion();
+            var currentFirmware = _contentManager.GetCurrentFirmwareVersion();
 
             GLib.Idle.Add(new GLib.IdleHandler(() =>
             {
@@ -547,7 +579,7 @@ namespace Ryujinx.Ui
 
                     fileChooser.Dispose();
 
-                    var firmwareVersion = _device.System.VerifyFirmwarePackage(filename);
+                    var firmwareVersion = _contentManager.VerifyFirmwarePackage(filename);
 
                     if (firmwareVersion == null)
                     {
@@ -566,7 +598,7 @@ namespace Ryujinx.Ui
                         return;
                     }
 
-                    var currentVersion = _device.System.GetCurrentFirmwareVersion();
+                    var currentVersion = _contentManager.GetCurrentFirmwareVersion();
 
                     string dialogMessage = $"System version {firmwareVersion.VersionString} will be installed.";
 
@@ -606,7 +638,7 @@ namespace Ryujinx.Ui
 
                             try
                             {
-                                _device.System.InstallFirmware(filename);
+                                _contentManager.InstallFirmware(filename);
 
                                 GLib.Idle.Add(new GLib.IdleHandler(() =>
                                 {
diff --git a/Ryujinx/Ui/Migration.cs b/Ryujinx/Ui/Migration.cs
index 9e9b80001d..20d8106477 100644
--- a/Ryujinx/Ui/Migration.cs
+++ b/Ryujinx/Ui/Migration.cs
@@ -1,21 +1,20 @@
 using Gtk;
 using LibHac;
+using Ryujinx.HLE.FileSystem;
 using System;
 using System.IO;
 using System.Linq;
 using System.Reflection;
 
-using Switch = Ryujinx.HLE.Switch;
-
 namespace Ryujinx.Ui
 {
     internal class Migration
     {
-        private Switch Device { get; }
+        private VirtualFileSystem _virtualFileSystem;
 
-        public Migration(Switch device)
+        public Migration(VirtualFileSystem virtualFileSystem)
         {
-            Device = device;
+            _virtualFileSystem = virtualFileSystem;
         }
 
         public static bool PromptIfMigrationNeededForStartup(Window parentWindow, out bool isMigrationNeeded)
@@ -48,11 +47,11 @@ namespace Ryujinx.Ui
             return dialogResponse == (int)ResponseType.Yes;
         }
 
-        public static bool DoMigrationForStartup(Window parentWindow, Switch device)
+        public static bool DoMigrationForStartup(MainWindow parentWindow, VirtualFileSystem virtualFileSystem)
         {
             try
             {
-                Migration migration = new Migration(device);
+                Migration migration = new Migration(virtualFileSystem);
                 int saveCount = migration.Migrate();
 
                 using MessageDialog dialogSuccess = new MessageDialog(parentWindow, DialogFlags.Modal, MessageType.Info, ButtonsType.Ok, null)
@@ -64,9 +63,6 @@ namespace Ryujinx.Ui
 
                 dialogSuccess.Run();
 
-                // Reload key set after migration to be sure to catch the keys in the system directory.
-                device.System.LoadKeySet();
-
                 return true;
             }
             catch (HorizonResultException ex)
@@ -80,6 +76,9 @@ namespace Ryujinx.Ui
         // Returns the number of saves migrated
         public int Migrate()
         {
+            // Make sure FsClient is initialized
+            _virtualFileSystem.Reload();
+
             string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
 
             string oldBasePath = Path.Combine(appDataPath, "RyuFs");
@@ -89,7 +88,7 @@ namespace Ryujinx.Ui
 
             CopyRyuFs(oldBasePath, newBasePath);
 
-            SaveImporter importer = new SaveImporter(oldSaveDir, Device.System.FsClient);
+            SaveImporter importer = new SaveImporter(oldSaveDir, _virtualFileSystem.FsClient);
 
             return importer.Import();
         }