diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index fcb8c2aff2..ca1e0f1d04 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -50,7 +50,7 @@ jobs:
       - uses: actions/checkout@v2
       - uses: actions/setup-dotnet@v1
         with:
-          dotnet-version: 5.0.x
+          dotnet-version: 6.0.x
       - name: Ensure NuGet Source
         uses: fabriciomurta/ensure-nuget-source@v1
       - name: Get git short hash
@@ -63,10 +63,10 @@ jobs:
       - name: Test
         run: dotnet test -c "${{ matrix.configuration }}"
       - name: Publish Ryujinx
-        run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.DOTNET_RUNTIME_IDENTIFIER }}" -o ./publish /p:Version="1.0.0" /p:DebugType=embedded /p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" /p:ExtraDefineConstants=DISABLE_UPDATER Ryujinx
+        run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.DOTNET_RUNTIME_IDENTIFIER }}" -o ./publish /p:Version="1.0.0" /p:DebugType=embedded /p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" /p:ExtraDefineConstants=DISABLE_UPDATER Ryujinx --self-contained
         if: github.event_name == 'pull_request'
       - name: Publish Ryujinx.Headless.SDL2
-        run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.DOTNET_RUNTIME_IDENTIFIER }}" -o ./publish_sdl2_headless /p:Version="1.0.0" /p:DebugType=embedded /p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" /p:ExtraDefineConstants=DISABLE_UPDATER Ryujinx.Headless.SDL2
+        run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.DOTNET_RUNTIME_IDENTIFIER }}" -o ./publish_sdl2_headless /p:Version="1.0.0" /p:DebugType=embedded /p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" /p:ExtraDefineConstants=DISABLE_UPDATER Ryujinx.Headless.SDL2 --self-contained
         if: github.event_name == 'pull_request'
       - name: Upload Ryujinx artifact
         uses: actions/upload-artifact@v2
diff --git a/ARMeilleure/ARMeilleure.csproj b/ARMeilleure/ARMeilleure.csproj
index ebc4433a12..1fd95ad026 100644
--- a/ARMeilleure/ARMeilleure.csproj
+++ b/ARMeilleure/ARMeilleure.csproj
@@ -1,12 +1,12 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
+    <PackageReference Include="Mono.Posix.NETStandard" Version="5.20.1-preview" />
   </ItemGroup>
 
   <ItemGroup>
diff --git a/README.md b/README.md
index fc1c7917f5..121c537e70 100644
--- a/README.md
+++ b/README.md
@@ -35,7 +35,7 @@ The latest automatic build for Windows, macOS, and Linux can be found on the [Of
 
 If you wish to build the emulator yourself  you will need to:
 
-**Step one:** Install the X64 version of [.NET 5.0 (or higher) SDK](https://dotnet.microsoft.com/download/dotnet/5.0).
+**Step one:** Install the X64 version of [.NET 6.0 (or higher) SDK](https://dotnet.microsoft.com/download/dotnet/6.0).
 
 **Step two (choose one):**  
 **(Variant one)**
diff --git a/Ryujinx.Audio.Backends.OpenAL/Ryujinx.Audio.Backends.OpenAL.csproj b/Ryujinx.Audio.Backends.OpenAL/Ryujinx.Audio.Backends.OpenAL.csproj
index 4619d73d9c..fd648715ac 100644
--- a/Ryujinx.Audio.Backends.OpenAL/Ryujinx.Audio.Backends.OpenAL.csproj
+++ b/Ryujinx.Audio.Backends.OpenAL/Ryujinx.Audio.Backends.OpenAL.csproj
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
   </PropertyGroup>
 
   <ItemGroup>
diff --git a/Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj b/Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj
index 6619a50004..fa70d34102 100644
--- a/Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj
+++ b/Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 
diff --git a/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj b/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj
index a9a2fe752c..49d142aaf8 100644
--- a/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj
+++ b/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 
diff --git a/Ryujinx.Audio.Backends/Ryujinx.Audio.Backends.csproj b/Ryujinx.Audio.Backends/Ryujinx.Audio.Backends.csproj
index 431187edea..83088f2776 100644
--- a/Ryujinx.Audio.Backends/Ryujinx.Audio.Backends.csproj
+++ b/Ryujinx.Audio.Backends/Ryujinx.Audio.Backends.csproj
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 
diff --git a/Ryujinx.Audio/Ryujinx.Audio.csproj b/Ryujinx.Audio/Ryujinx.Audio.csproj
index ccdeae3e69..2499bb44ee 100644
--- a/Ryujinx.Audio/Ryujinx.Audio.csproj
+++ b/Ryujinx.Audio/Ryujinx.Audio.csproj
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 
diff --git a/Ryujinx.Common/Ryujinx.Common.csproj b/Ryujinx.Common/Ryujinx.Common.csproj
index a7e9c66c93..e0cc2d56c2 100644
--- a/Ryujinx.Common/Ryujinx.Common.csproj
+++ b/Ryujinx.Common/Ryujinx.Common.csproj
@@ -1,14 +1,14 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 
   <ItemGroup>
     <PackageReference Include="MsgPack.Cli" Version="1.0.1" />
-    <PackageReference Include="System.Drawing.Common" Version="5.0.1" />
-    <PackageReference Include="System.Management" Version="5.0.0" />
+    <PackageReference Include="System.Drawing.Common" Version="6.0.0" />
+    <PackageReference Include="System.Management" Version="6.0.0" />
   </ItemGroup>
 
 </Project>
diff --git a/Ryujinx.Common/System/ForceDpiAware.cs b/Ryujinx.Common/System/ForceDpiAware.cs
index 81c69376cb..dc513307a4 100644
--- a/Ryujinx.Common/System/ForceDpiAware.cs
+++ b/Ryujinx.Common/System/ForceDpiAware.cs
@@ -2,6 +2,7 @@
 using System;
 using System.Drawing;
 using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
 
 namespace Ryujinx.Common.System
 {
@@ -19,7 +20,7 @@ namespace Ryujinx.Common.System
         public static void Windows()
         {
             // Make process DPI aware for proper window sizing on high-res screens.
-            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && Environment.OSVersion.Version.Major >= 6)
+            if (OperatingSystem.IsWindowsVersionAtLeast(6))
             {
                 SetProcessDPIAware();
             }
@@ -27,16 +28,22 @@ namespace Ryujinx.Common.System
 
         public static double GetWindowScaleFactor()
         {
-            double userDpiScale;
+            double userDpiScale = 96.0;
 
             try
             {
-                userDpiScale = Graphics.FromHwnd(IntPtr.Zero).DpiX;
+                if (OperatingSystem.IsWindows())
+                {
+                    userDpiScale = Graphics.FromHwnd(IntPtr.Zero).DpiX;
+                }
+                else
+                {
+                    // TODO: Linux support
+                }
             }
             catch (Exception e)
             {
                 Logger.Warning?.Print(LogClass.Application, $"Couldn't determine monitor DPI: {e.Message}");
-                userDpiScale = 96.0;
             }
 
             return Math.Min(userDpiScale / _standardDpiScale, _maxScaleFactor);
diff --git a/Ryujinx.Cpu/Ryujinx.Cpu.csproj b/Ryujinx.Cpu/Ryujinx.Cpu.csproj
index ef33dd1851..84972af118 100644
--- a/Ryujinx.Cpu/Ryujinx.Cpu.csproj
+++ b/Ryujinx.Cpu/Ryujinx.Cpu.csproj
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 
diff --git a/Ryujinx.Graphics.Device/Ryujinx.Graphics.Device.csproj b/Ryujinx.Graphics.Device/Ryujinx.Graphics.Device.csproj
index 2f002aa338..fff78129b3 100644
--- a/Ryujinx.Graphics.Device/Ryujinx.Graphics.Device.csproj
+++ b/Ryujinx.Graphics.Device/Ryujinx.Graphics.Device.csproj
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
   </PropertyGroup>
 
 </Project>
diff --git a/Ryujinx.Graphics.GAL/Ryujinx.Graphics.GAL.csproj b/Ryujinx.Graphics.GAL/Ryujinx.Graphics.GAL.csproj
index e8b3f52dd6..725f48eab0 100644
--- a/Ryujinx.Graphics.GAL/Ryujinx.Graphics.GAL.csproj
+++ b/Ryujinx.Graphics.GAL/Ryujinx.Graphics.GAL.csproj
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
   </PropertyGroup>
 
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
diff --git a/Ryujinx.Graphics.Gpu/Ryujinx.Graphics.Gpu.csproj b/Ryujinx.Graphics.Gpu/Ryujinx.Graphics.Gpu.csproj
index 01e8e235d3..e364566876 100644
--- a/Ryujinx.Graphics.Gpu/Ryujinx.Graphics.Gpu.csproj
+++ b/Ryujinx.Graphics.Gpu/Ryujinx.Graphics.Gpu.csproj
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 
@@ -14,4 +14,8 @@
     <ProjectReference Include="..\Ryujinx.Graphics.Shader\Ryujinx.Graphics.Shader.csproj" />
   </ItemGroup>
 
+  <ItemGroup>
+	<PackageReference Include="SharpZipLib" Version="1.3.3" />
+  </ItemGroup>
+
 </Project>
diff --git a/Ryujinx.Graphics.Gpu/Shader/Cache/CacheCollection.cs b/Ryujinx.Graphics.Gpu/Shader/Cache/CacheCollection.cs
index 316e027f74..a98531f69e 100644
--- a/Ryujinx.Graphics.Gpu/Shader/Cache/CacheCollection.cs
+++ b/Ryujinx.Graphics.Gpu/Shader/Cache/CacheCollection.cs
@@ -1,11 +1,11 @@
-using Ryujinx.Common;
+using ICSharpCode.SharpZipLib.Zip;
+using Ryujinx.Common;
 using Ryujinx.Common.Logging;
 using Ryujinx.Graphics.Gpu.Shader.Cache.Definition;
 using System;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.IO;
-using System.IO.Compression;
 using System.Linq;
 using System.Runtime.CompilerServices;
 using System.Runtime.InteropServices;
@@ -119,7 +119,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
         /// <summary>
         /// Main storage of the cache collection.
         /// </summary>
-        private ZipArchive _cacheArchive;
+        private ZipFile _cacheArchive;
 
         /// <summary>
         /// Indicates if the cache collection supports modification.
@@ -324,7 +324,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
             EnsureArchiveUpToDate();
 
             // Open the zip in readonly to avoid anyone modifying/corrupting it during normal operations.
-            _cacheArchive = ZipFile.Open(GetArchivePath(), ZipArchiveMode.Read);
+            _cacheArchive = new ZipFile(File.OpenRead(GetArchivePath()));
         }
 
         /// <summary>
@@ -336,7 +336,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
             // First close previous opened instance if found.
             if (_cacheArchive != null)
             {
-                _cacheArchive.Dispose();
+                _cacheArchive.Close();
             }
 
             string archivePath = GetArchivePath();
@@ -355,8 +355,18 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
                 return;
             }
 
+            if (!File.Exists(archivePath))
+            {
+                using (ZipFile newZip = ZipFile.Create(archivePath))
+                {
+                    // Workaround for SharpZipLib issue #395
+                    newZip.BeginUpdate();
+                    newZip.CommitUpdate();
+                }
+            }
+
             // Open the zip in read/write.
-            _cacheArchive = ZipFile.Open(archivePath, ZipArchiveMode.Update);
+            _cacheArchive = new ZipFile(File.Open(archivePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None));
 
             Logger.Info?.Print(LogClass.Gpu, $"Updating cache collection archive {archivePath}...");
 
@@ -366,7 +376,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
                 CacheHelper.EnsureArchiveUpToDate(_cacheDirectory, _cacheArchive, _hashTable);
 
                 // Close the instance to force a flush.
-                _cacheArchive.Dispose();
+                _cacheArchive.Close();
                 _cacheArchive = null;
 
                 string cacheTempDataPath = GetCacheTempDataPath();
diff --git a/Ryujinx.Graphics.Gpu/Shader/Cache/CacheHelper.cs b/Ryujinx.Graphics.Gpu/Shader/Cache/CacheHelper.cs
index 09107346e0..ee4e126578 100644
--- a/Ryujinx.Graphics.Gpu/Shader/Cache/CacheHelper.cs
+++ b/Ryujinx.Graphics.Gpu/Shader/Cache/CacheHelper.cs
@@ -1,4 +1,5 @@
-using Ryujinx.Common;
+using ICSharpCode.SharpZipLib.Zip;
+using Ryujinx.Common;
 using Ryujinx.Common.Configuration;
 using Ryujinx.Common.Logging;
 using Ryujinx.Graphics.GAL;
@@ -9,7 +10,6 @@ using Ryujinx.Graphics.Shader.Translation;
 using System;
 using System.Collections.Generic;
 using System.IO;
-using System.IO.Compression;
 using System.Runtime.CompilerServices;
 using System.Runtime.InteropServices;
 
@@ -192,19 +192,19 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
         /// <param name="entry">The given hash</param>
         /// <returns>The cached file if present or null</returns>
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        public static byte[] ReadFromArchive(ZipArchive archive, Hash128 entry)
+        public static byte[] ReadFromArchive(ZipFile archive, Hash128 entry)
         {
             if (archive != null)
             {
-                ZipArchiveEntry archiveEntry = archive.GetEntry($"{entry}");
+                ZipEntry archiveEntry = archive.GetEntry($"{entry}");
 
                 if (archiveEntry != null)
                 {
                     try
                     {
-                        byte[] result = new byte[archiveEntry.Length];
+                        byte[] result = new byte[archiveEntry.Size];
 
-                        using (Stream archiveStream = archiveEntry.Open())
+                        using (Stream archiveStream = archive.GetInputStream(archiveEntry))
                         {
                             archiveStream.Read(result);
 
@@ -538,8 +538,12 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
         /// <param name="archive">The archive to use</param>
         /// <param name="entries">The entries in the cache</param>
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        public static void EnsureArchiveUpToDate(string baseCacheDirectory, ZipArchive archive, HashSet<Hash128> entries)
+        public static void EnsureArchiveUpToDate(string baseCacheDirectory, ZipFile archive, HashSet<Hash128> entries)
         {
+            List<string> filesToDelete = new List<string>();
+
+            archive.BeginUpdate();
+
             foreach (Hash128 hash in entries)
             {
                 string cacheTempFilePath = GenCacheTempFilePath(baseCacheDirectory, hash);
@@ -548,15 +552,25 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
                 {
                     string cacheHash = $"{hash}";
 
-                    ZipArchiveEntry entry = archive.GetEntry(cacheHash);
+                    ZipEntry entry = archive.GetEntry(cacheHash);
 
-                    entry?.Delete();
+                    if (entry != null)
+                    {
+                        archive.Delete(entry);
+                    }
 
-                    archive.CreateEntryFromFile(cacheTempFilePath, cacheHash);
-
-                    File.Delete(cacheTempFilePath);
+                    // We enforce deflate compression here to avoid possible incompatibilities on older version of Ryujinx that use System.IO.Compression.
+                    archive.Add(new StaticDiskDataSource(cacheTempFilePath), cacheHash, CompressionMethod.Deflated);
+                    filesToDelete.Add(cacheTempFilePath);
                 }
             }
+
+            archive.CommitUpdate();
+
+            foreach (string filePath in filesToDelete)
+            {
+                File.Delete(filePath);
+            }
         }
 
         public static bool IsArchiveReadOnly(string archivePath)
diff --git a/Ryujinx.Graphics.Gpu/Shader/Cache/CacheMigration.cs b/Ryujinx.Graphics.Gpu/Shader/Cache/CacheMigration.cs
index e726bc2c06..5b4a171354 100644
--- a/Ryujinx.Graphics.Gpu/Shader/Cache/CacheMigration.cs
+++ b/Ryujinx.Graphics.Gpu/Shader/Cache/CacheMigration.cs
@@ -1,11 +1,11 @@
-using Ryujinx.Common;
+using ICSharpCode.SharpZipLib.Zip;
+using Ryujinx.Common;
 using Ryujinx.Common.Logging;
 using Ryujinx.Graphics.GAL;
 using Ryujinx.Graphics.Gpu.Shader.Cache.Definition;
 using System;
 using System.Collections.Generic;
 using System.IO;
-using System.IO.Compression;
 
 namespace Ryujinx.Graphics.Gpu.Shader.Cache
 {
@@ -35,27 +35,36 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
             return false;
         }
 
+        private class StreamZipEntryDataSource : IStaticDataSource
+        {
+            private readonly ZipFile Archive;
+            private readonly ZipEntry Entry;
+            public StreamZipEntryDataSource(ZipFile archive, ZipEntry entry)
+            {
+                Archive = archive;
+                Entry = entry;
+            }
+
+            public Stream GetSource()
+            {
+                return Archive.GetInputStream(Entry);
+            }
+        }
+
         /// <summary>
         /// Move a file with the name of a given hash to another in the cache archive.
         /// </summary>
         /// <param name="archive">The archive in use</param>
         /// <param name="oldKey">The old key</param>
         /// <param name="newKey">The new key</param>
-        private static void MoveEntry(ZipArchive archive, Hash128 oldKey, Hash128 newKey)
+        private static void MoveEntry(ZipFile archive, Hash128 oldKey, Hash128 newKey)
         {
-            ZipArchiveEntry oldGuestEntry = archive.GetEntry($"{oldKey}");
+            ZipEntry oldGuestEntry = archive.GetEntry($"{oldKey}");
 
             if (oldGuestEntry != null)
             {
-                ZipArchiveEntry newGuestEntry = archive.CreateEntry($"{newKey}");
-
-                using (Stream oldStream = oldGuestEntry.Open())
-                using (Stream newStream = newGuestEntry.Open())
-                {
-                    oldStream.CopyTo(newStream);
-                }
-
-                oldGuestEntry.Delete();
+                archive.Add(new StreamZipEntryDataSource(archive, oldGuestEntry), $"{newKey}", CompressionMethod.Deflated);
+                archive.Delete(oldGuestEntry);
             }
         }
 
@@ -81,8 +90,8 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
                 string guestArchivePath = CacheHelper.GetArchivePath(guestBaseCacheDirectory);
                 string hostArchivePath = CacheHelper.GetArchivePath(hostBaseCacheDirectory);
 
-                ZipArchive guestArchive = ZipFile.Open(guestArchivePath, ZipArchiveMode.Update);
-                ZipArchive hostArchive = ZipFile.Open(hostArchivePath, ZipArchiveMode.Update);
+                ZipFile guestArchive = new ZipFile(File.Open(guestArchivePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None));
+                ZipFile hostArchive = new ZipFile(File.Open(hostArchivePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None));
 
                 CacheHelper.EnsureArchiveUpToDate(guestBaseCacheDirectory, guestArchive, guestEntries);
                 CacheHelper.EnsureArchiveUpToDate(hostBaseCacheDirectory, hostArchive, hostEntries);
@@ -129,8 +138,11 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
                 File.WriteAllBytes(guestManifestPath, newGuestManifestContent);
                 File.WriteAllBytes(hostManifestPath, newHostManifestContent);
 
-                guestArchive.Dispose();
-                hostArchive.Dispose();
+                guestArchive.CommitUpdate();
+                hostArchive.CommitUpdate();
+
+                guestArchive.Close();
+                hostArchive.Close();
             }
         }
 
diff --git a/Ryujinx.Graphics.Host1x/Ryujinx.Graphics.Host1x.csproj b/Ryujinx.Graphics.Host1x/Ryujinx.Graphics.Host1x.csproj
index 69b6103f32..49ed1a5c57 100644
--- a/Ryujinx.Graphics.Host1x/Ryujinx.Graphics.Host1x.csproj
+++ b/Ryujinx.Graphics.Host1x/Ryujinx.Graphics.Host1x.csproj
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
   </PropertyGroup>
 
   <ItemGroup>
diff --git a/Ryujinx.Graphics.Nvdec.FFmpeg/Ryujinx.Graphics.Nvdec.FFmpeg.csproj b/Ryujinx.Graphics.Nvdec.FFmpeg/Ryujinx.Graphics.Nvdec.FFmpeg.csproj
index b437f36e69..4e90eb205c 100644
--- a/Ryujinx.Graphics.Nvdec.FFmpeg/Ryujinx.Graphics.Nvdec.FFmpeg.csproj
+++ b/Ryujinx.Graphics.Nvdec.FFmpeg/Ryujinx.Graphics.Nvdec.FFmpeg.csproj
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 
diff --git a/Ryujinx.Graphics.Nvdec.Vp9/Ryujinx.Graphics.Nvdec.Vp9.csproj b/Ryujinx.Graphics.Nvdec.Vp9/Ryujinx.Graphics.Nvdec.Vp9.csproj
index 51e880252a..b30e6aea0f 100644
--- a/Ryujinx.Graphics.Nvdec.Vp9/Ryujinx.Graphics.Nvdec.Vp9.csproj
+++ b/Ryujinx.Graphics.Nvdec.Vp9/Ryujinx.Graphics.Nvdec.Vp9.csproj
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 
diff --git a/Ryujinx.Graphics.Nvdec/Ryujinx.Graphics.Nvdec.csproj b/Ryujinx.Graphics.Nvdec/Ryujinx.Graphics.Nvdec.csproj
index 095e0e5995..68c0c2af13 100644
--- a/Ryujinx.Graphics.Nvdec/Ryujinx.Graphics.Nvdec.csproj
+++ b/Ryujinx.Graphics.Nvdec/Ryujinx.Graphics.Nvdec.csproj
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 
diff --git a/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj b/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj
index aff59d9773..3f4bcdee48 100644
--- a/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj
+++ b/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 
diff --git a/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj b/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj
index d59efc2ef2..c604902f3e 100644
--- a/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj
+++ b/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
   </PropertyGroup>
 
   <ItemGroup>
diff --git a/Ryujinx.Graphics.Texture/Ryujinx.Graphics.Texture.csproj b/Ryujinx.Graphics.Texture/Ryujinx.Graphics.Texture.csproj
index b74938c0ce..6af7e775e6 100644
--- a/Ryujinx.Graphics.Texture/Ryujinx.Graphics.Texture.csproj
+++ b/Ryujinx.Graphics.Texture/Ryujinx.Graphics.Texture.csproj
@@ -1,6 +1,6 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 
diff --git a/Ryujinx.Graphics.Vic/Ryujinx.Graphics.Vic.csproj b/Ryujinx.Graphics.Vic/Ryujinx.Graphics.Vic.csproj
index fe9b834bba..0e564d0293 100644
--- a/Ryujinx.Graphics.Vic/Ryujinx.Graphics.Vic.csproj
+++ b/Ryujinx.Graphics.Vic/Ryujinx.Graphics.Vic.csproj
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 
diff --git a/Ryujinx.Graphics.Video/Ryujinx.Graphics.Video.csproj b/Ryujinx.Graphics.Video/Ryujinx.Graphics.Video.csproj
index a7f8f74614..484b71774c 100644
--- a/Ryujinx.Graphics.Video/Ryujinx.Graphics.Video.csproj
+++ b/Ryujinx.Graphics.Video/Ryujinx.Graphics.Video.csproj
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
   </PropertyGroup>
 
   <ItemGroup>
diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs
index 523fa5de50..3cfd192c7f 100644
--- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs
+++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs
@@ -170,7 +170,9 @@ namespace Ryujinx.HLE.HOS.Applets
         {
             _npads?.Update();
 
-            return _keyboardRenderer?.DrawTo(surfaceInfo, destination, position) ?? false;
+            _keyboardRenderer?.SetSurfaceInfo(surfaceInfo);
+
+            return _keyboardRenderer?.DrawTo(destination, position) ?? false;
         }
 
         private void ExecuteForegroundKeyboard()
diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRenderer.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRenderer.cs
index dfd1092562..c30ad11b01 100644
--- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRenderer.cs
+++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRenderer.cs
@@ -1,717 +1,164 @@
 using Ryujinx.HLE.Ui;
 using Ryujinx.Memory;
 using System;
-using System.Buffers.Binary;
-using System.Diagnostics;
-using System.Drawing;
-using System.Drawing.Drawing2D;
-using System.Drawing.Imaging;
-using System.Drawing.Text;
-using System.IO;
-using System.Numerics;
-using System.Reflection;
-using System.Runtime.InteropServices;
 using System.Threading;
 
 namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
 {
     /// <summary>
-    /// Class that generates the graphics for the software keyboard applet during inline mode.
+    /// Class that manages the renderer base class and its state in a multithreaded context.
     /// </summary>
     internal class SoftwareKeyboardRenderer : IDisposable
     {
-        const int TextBoxBlinkThreshold            = 8;
-        const int TextBoxBlinkSleepMilliseconds    = 100;
-        const int TextBoxBlinkJoinWaitMilliseconds = 1000;
+        private const int TextBoxBlinkSleepMilliseconds   = 100;
+        private const int RendererWaitTimeoutMilliseconds = 100;
 
-        const string MessageText          = "Please use the keyboard to input text";
-        const string AcceptText           = "Accept";
-        const string CancelText           = "Cancel";
-        const string ControllerToggleText = "Toggle input";
+        private readonly object _stateLock  = new object();
 
-        private RenderingSurfaceInfo _surfaceInfo;
-        private Bitmap               _surface    = null;
-        private object               _renderLock = new object();
+        private SoftwareKeyboardUiState      _state = new SoftwareKeyboardUiState();
+        private SoftwareKeyboardRendererBase _renderer;
 
-        private string _inputText         = "";
-        private int    _cursorStart       = 0;
-        private int    _cursorEnd         = 0;
-        private bool   _acceptPressed     = false;
-        private bool   _cancelPressed     = false;
-        private bool   _overwriteMode     = false;
-        private bool   _typingEnabled     = true;
-        private bool   _controllerEnabled = true;
-
-        private Image _ryujinxLogo   = null;
-        private Image _padAcceptIcon = null;
-        private Image _padCancelIcon = null;
-        private Image _keyModeIcon   = null;
-
-        private float _textBoxOutlineWidth;
-        private float _padPressedPenWidth;
-
-        private Brush _panelBrush;
-        private Brush _disabledBrush;
-        private Brush _textNormalBrush;
-        private Brush _textSelectedBrush;
-        private Brush _textOverCursorBrush;
-        private Brush _cursorBrush;
-        private Brush _selectionBoxBrush;
-        private Brush _keyCapBrush;
-        private Brush _keyProgressBrush;
-
-        private Pen _gridSeparatorPen;
-        private Pen _textBoxOutlinePen;
-        private Pen _cursorPen;
-        private Pen _selectionBoxPen;
-        private Pen _padPressedPen;
-
-        private int  _inputTextFontSize;
-        private int  _padButtonFontSize;
-        private Font _messageFont;
-        private Font _inputTextFont;
-        private Font _labelsTextFont;
-        private Font _padSymbolFont;
-        private Font _keyCapFont;
-
-        private float      _inputTextCalibrationHeight;
-        private float      _panelPositionY;
-        private RectangleF _panelRectangle;
-        private PointF     _logoPosition;
-        private float      _messagePositionY;
-
-        private TRef<int>   _textBoxBlinkCounter     = new TRef<int>(0);
         private TimedAction _textBoxBlinkTimedAction = new TimedAction();
+        private TimedAction _renderAction            = new TimedAction();
 
         public SoftwareKeyboardRenderer(IHostUiTheme uiTheme)
         {
-            _surfaceInfo = new RenderingSurfaceInfo(0, 0, 0, 0, 0);
+            _renderer = new SoftwareKeyboardRendererBase(uiTheme);
 
-            string ryujinxLogoPath = "Ryujinx.Ui.Resources.Logo_Ryujinx.png";
-            int    ryujinxLogoSize = 32;
-
-            _ryujinxLogo = LoadResource(Assembly.GetEntryAssembly(), ryujinxLogoPath, ryujinxLogoSize, ryujinxLogoSize);
-
-            string padAcceptIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Icon_BtnA.png";
-            string padCancelIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Icon_BtnB.png";
-            string keyModeIconPath   = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Icon_KeyF6.png";
-
-            _padAcceptIcon = LoadResource(Assembly.GetExecutingAssembly(), padAcceptIconPath  , 0, 0);
-            _padCancelIcon = LoadResource(Assembly.GetExecutingAssembly(), padCancelIconPath  , 0, 0);
-            _keyModeIcon   = LoadResource(Assembly.GetExecutingAssembly(), keyModeIconPath    , 0, 0);
-
-            Color panelColor               = ToColor(uiTheme.DefaultBackgroundColor, 255);
-            Color panelTransparentColor    = ToColor(uiTheme.DefaultBackgroundColor, 150);
-            Color normalTextColor          = ToColor(uiTheme.DefaultForegroundColor);
-            Color invertedTextColor        = ToColor(uiTheme.DefaultForegroundColor, null, true);
-            Color selectedTextColor        = ToColor(uiTheme.SelectionForegroundColor);
-            Color borderColor              = ToColor(uiTheme.DefaultBorderColor);
-            Color selectionBackgroundColor = ToColor(uiTheme.SelectionBackgroundColor);
-            Color gridSeparatorColor       = Color.FromArgb(180, 255, 255, 255);
-
-            float cursorWidth = 2;
-
-            _textBoxOutlineWidth = 2;
-            _padPressedPenWidth  = 2;
-
-            _panelBrush          = new SolidBrush(panelColor);
-            _disabledBrush       = new SolidBrush(panelTransparentColor);
-            _textNormalBrush     = new SolidBrush(normalTextColor);
-            _textSelectedBrush   = new SolidBrush(selectedTextColor);
-            _textOverCursorBrush = new SolidBrush(invertedTextColor);
-            _cursorBrush         = new SolidBrush(normalTextColor);
-            _selectionBoxBrush   = new SolidBrush(selectionBackgroundColor);
-            _keyCapBrush         = Brushes.White;
-            _keyProgressBrush    = new SolidBrush(borderColor);
-
-            _gridSeparatorPen    = new Pen(gridSeparatorColor, 2);
-            _textBoxOutlinePen   = new Pen(borderColor, _textBoxOutlineWidth);
-            _cursorPen           = new Pen(normalTextColor, cursorWidth);
-            _selectionBoxPen     = new Pen(selectionBackgroundColor, cursorWidth);
-            _padPressedPen       = new Pen(borderColor, _padPressedPenWidth);
-
-            _inputTextFontSize = 20;
-            _padButtonFontSize = 24;
-
-            string font = uiTheme.FontFamily;
-
-            _messageFont    = new Font(font, 26,                 FontStyle.Regular, GraphicsUnit.Pixel);
-            _inputTextFont  = new Font(font, _inputTextFontSize, FontStyle.Regular, GraphicsUnit.Pixel);
-            _labelsTextFont = new Font(font, 24,                 FontStyle.Regular, GraphicsUnit.Pixel);
-            _padSymbolFont  = new Font(font, _padButtonFontSize, FontStyle.Regular, GraphicsUnit.Pixel);
-            _keyCapFont     = new Font(font, 15,                 FontStyle.Regular, GraphicsUnit.Pixel);
-
-            // System.Drawing has serious problems measuring strings, so it requires a per-pixel calibration
-            // to ensure we are rendering text inside the proper region
-            _inputTextCalibrationHeight = CalibrateTextHeight(_inputTextFont);
-
-            StartTextBoxBlinker(_textBoxBlinkTimedAction, _textBoxBlinkCounter);
+            StartTextBoxBlinker(_textBoxBlinkTimedAction, _state, _stateLock);
+            StartRenderer(_renderAction, _renderer, _state, _stateLock);
         }
 
-        private static void StartTextBoxBlinker(TimedAction timedAction, TRef<int> blinkerCounter)
+        private static void StartTextBoxBlinker(TimedAction timedAction, SoftwareKeyboardUiState state, object stateLock)
         {
             timedAction.Reset(() =>
             {
-                // The blinker is on falf of the time and events such as input
-                // changes can reset the blinker.
-                var value = Volatile.Read(ref blinkerCounter.Value);
-                value = (value + 1) % (2 * TextBoxBlinkThreshold);
-                Volatile.Write(ref blinkerCounter.Value, value);
+                lock (stateLock)
+                {
+                    // The blinker is on half of the time and events such as input
+                    // changes can reset the blinker.
+                    state.TextBoxBlinkCounter = (state.TextBoxBlinkCounter + 1) % (2 * SoftwareKeyboardRendererBase.TextBoxBlinkThreshold);
 
+                    // Tell the render thread there is something new to render.
+                    Monitor.PulseAll(stateLock);
+                }
             }, TextBoxBlinkSleepMilliseconds);
         }
 
-        private Color ToColor(ThemeColor color, byte? overrideAlpha = null, bool flipRgb = false)
+        private static void StartRenderer(TimedAction timedAction, SoftwareKeyboardRendererBase renderer, SoftwareKeyboardUiState state, object stateLock)
         {
-            var a = (byte)(color.A * 255);
-            var r = (byte)(color.R * 255);
-            var g = (byte)(color.G * 255);
-            var b = (byte)(color.B * 255);
+            SoftwareKeyboardUiState internalState = new SoftwareKeyboardUiState();
 
-            if (flipRgb)
+            bool canCreateSurface = false;
+            bool needsUpdate      = true;
+
+            timedAction.Reset(() =>
             {
-                r = (byte)(255 - r);
-                g = (byte)(255 - g);
-                b = (byte)(255 - b);
-            }
+                lock (stateLock)
+                {
+                    if (!Monitor.Wait(stateLock, RendererWaitTimeoutMilliseconds))
+                    {
+                        return;
+                    }
 
-            return Color.FromArgb(overrideAlpha.GetValueOrDefault(a), r, g, b);
+                    needsUpdate  = UpdateStateField(ref state.InputText,           ref internalState.InputText);
+                    needsUpdate |= UpdateStateField(ref state.CursorBegin,         ref internalState.CursorBegin);
+                    needsUpdate |= UpdateStateField(ref state.CursorEnd,           ref internalState.CursorEnd);
+                    needsUpdate |= UpdateStateField(ref state.AcceptPressed,       ref internalState.AcceptPressed);
+                    needsUpdate |= UpdateStateField(ref state.CancelPressed,       ref internalState.CancelPressed);
+                    needsUpdate |= UpdateStateField(ref state.OverwriteMode,       ref internalState.OverwriteMode);
+                    needsUpdate |= UpdateStateField(ref state.TypingEnabled,       ref internalState.TypingEnabled);
+                    needsUpdate |= UpdateStateField(ref state.ControllerEnabled,   ref internalState.ControllerEnabled);
+                    needsUpdate |= UpdateStateField(ref state.TextBoxBlinkCounter, ref internalState.TextBoxBlinkCounter);
+
+                    canCreateSurface = state.SurfaceInfo != null && internalState.SurfaceInfo == null;
+
+                    if (canCreateSurface)
+                    {
+                        internalState.SurfaceInfo = state.SurfaceInfo;
+                    }
+                }
+
+                if (canCreateSurface)
+                {
+                    renderer.CreateSurface(internalState.SurfaceInfo);
+                }
+
+                if (needsUpdate)
+                {
+                    renderer.DrawMutableElements(internalState);
+                    renderer.CopyImageToBuffer();
+                    needsUpdate = false;
+                }
+            });
         }
 
-        private Image LoadResource(Assembly assembly, string resourcePath, int newWidth, int newHeight)
+        private static bool UpdateStateField<T>(ref T source, ref T destination) where T : IEquatable<T>
         {
-            Stream resourceStream = assembly.GetManifestResourceStream(resourcePath);
-
-            Debug.Assert(resourceStream != null);
-
-            var originalImage = Image.FromStream(resourceStream);
-
-            if (newHeight == 0 || newWidth == 0)
+            if (!source.Equals(destination))
             {
-                return originalImage;
+                destination = source;
+                return true;
             }
 
-            var newSize = new Rectangle(0, 0, newWidth, newHeight);
-            var newImage = new Bitmap(newWidth, newHeight);
-
-            using (var graphics = System.Drawing.Graphics.FromImage(newImage))
-            using (var wrapMode = new ImageAttributes())
-            {
-                graphics.InterpolationMode  = InterpolationMode.HighQualityBicubic;
-                graphics.CompositingQuality = CompositingQuality.HighQuality;
-                graphics.CompositingMode    = CompositingMode.SourceCopy;
-                graphics.PixelOffsetMode    = PixelOffsetMode.HighQuality;
-                graphics.SmoothingMode      = SmoothingMode.HighQuality;
-
-                wrapMode.SetWrapMode(WrapMode.TileFlipXY);
-                graphics.DrawImage(originalImage, newSize, 0, 0, originalImage.Width, originalImage.Height, GraphicsUnit.Pixel, wrapMode);
-            }
-
-            return newImage;
+            return false;
         }
 
 #pragma warning disable CS8632
-        public void UpdateTextState(string? inputText, int? cursorStart, int? cursorEnd, bool? overwriteMode, bool? typingEnabled)
+        public void UpdateTextState(string? inputText, int? cursorBegin, int? cursorEnd, bool? overwriteMode, bool? typingEnabled)
 #pragma warning restore CS8632
         {
-            lock (_renderLock)
+            lock (_stateLock)
             {
                 // Update the parameters that were provided.
-                _inputText     = inputText != null ? inputText : _inputText;
-                _cursorStart   = cursorStart.GetValueOrDefault(_cursorStart);
-                _cursorEnd     = cursorEnd.GetValueOrDefault(_cursorEnd);
-                _overwriteMode = overwriteMode.GetValueOrDefault(_overwriteMode);
-                _typingEnabled = typingEnabled.GetValueOrDefault(_typingEnabled);
+                _state.InputText     = inputText != null ? inputText : _state.InputText;
+                _state.CursorBegin   = cursorBegin.GetValueOrDefault(_state.CursorBegin);
+                _state.CursorEnd     = cursorEnd.GetValueOrDefault(_state.CursorEnd);
+                _state.OverwriteMode = overwriteMode.GetValueOrDefault(_state.OverwriteMode);
+                _state.TypingEnabled = typingEnabled.GetValueOrDefault(_state.TypingEnabled);
 
                 // Reset the cursor blink.
-                Volatile.Write(ref _textBoxBlinkCounter.Value, 0);
+                _state.TextBoxBlinkCounter = 0;
+
+                // Tell the render thread there is something new to render.
+                Monitor.PulseAll(_stateLock);
             }
         }
 
         public void UpdateCommandState(bool? acceptPressed, bool? cancelPressed, bool? controllerEnabled)
         {
-            lock (_renderLock)
+            lock (_stateLock)
             {
                 // Update the parameters that were provided.
-                _acceptPressed     = acceptPressed.GetValueOrDefault(_acceptPressed);
-                _cancelPressed     = cancelPressed.GetValueOrDefault(_cancelPressed);
-                _controllerEnabled = controllerEnabled.GetValueOrDefault(_controllerEnabled);
+                _state.AcceptPressed     = acceptPressed.GetValueOrDefault(_state.AcceptPressed);
+                _state.CancelPressed     = cancelPressed.GetValueOrDefault(_state.CancelPressed);
+                _state.ControllerEnabled = controllerEnabled.GetValueOrDefault(_state.ControllerEnabled);
+
+                // Tell the render thread there is something new to render.
+                Monitor.PulseAll(_stateLock);
             }
         }
 
-        private void Redraw()
+        public void SetSurfaceInfo(RenderingSurfaceInfo surfaceInfo)
         {
-            if (_surface == null)
+            lock (_stateLock)
             {
-                return;
-            }
+                _state.SurfaceInfo = surfaceInfo;
 
-            using (var graphics = CreateGraphics())
-            {
-                var    messageRectangle = MeasureString(graphics, MessageText, _messageFont);
-                float  messagePositionX = (_panelRectangle.Width - messageRectangle.Width) / 2 - messageRectangle.X;
-                float  messagePositionY = _messagePositionY - messageRectangle.Y;
-                PointF messagePosition  = new PointF(messagePositionX, messagePositionY);
-
-                graphics.Clear(Color.Transparent);
-                graphics.TranslateTransform(0, _panelPositionY);
-                graphics.FillRectangle(_panelBrush, _panelRectangle);
-                graphics.DrawImage(_ryujinxLogo, _logoPosition);
-
-                DrawString(graphics, MessageText, _messageFont, _textNormalBrush, messagePosition);
-
-                if (!_typingEnabled)
-                {
-                    // Just draw a semi-transparent rectangle on top to fade the component with the background.
-                    // TODO (caian): This will not work if one decides to add make background semi-transparent as well.
-                    graphics.FillRectangle(_disabledBrush, messagePositionX, messagePositionY, messageRectangle.Width, messageRectangle.Height);
-                }
-
-                DrawTextBox(graphics);
-
-                float halfWidth = _panelRectangle.Width / 2;
-
-                PointF acceptButtonPosition  = new PointF(halfWidth - 180, 185);
-                PointF cancelButtonPosition  = new PointF(halfWidth      , 185);
-                PointF disableButtonPosition = new PointF(halfWidth + 180, 185);
-
-                DrawPadButton       (graphics, acceptButtonPosition , _padAcceptIcon, AcceptText, _acceptPressed, _controllerEnabled);
-                DrawPadButton       (graphics, cancelButtonPosition , _padCancelIcon, CancelText, _cancelPressed, _controllerEnabled);
-                DrawControllerToggle(graphics, disableButtonPosition, _controllerEnabled);
+                // Tell the render thread there is something new to render.
+                Monitor.PulseAll(_stateLock);
             }
         }
 
-        private void RecreateSurface()
+        internal bool DrawTo(IVirtualMemoryManager destination, ulong position)
         {
-            Debug.Assert(_surfaceInfo.ColorFormat == Services.SurfaceFlinger.ColorFormat.A8B8G8R8);
-
-            // Use the whole area of the image to draw, even the alignment, otherwise it may shear the final
-            // image if the pitch is different.
-            uint totalWidth  = _surfaceInfo.Pitch / 4;
-            uint totalHeight = _surfaceInfo.Size / _surfaceInfo.Pitch;
-
-            Debug.Assert(_surfaceInfo.Width <= totalWidth);
-            Debug.Assert(_surfaceInfo.Height <= totalHeight);
-            Debug.Assert(_surfaceInfo.Pitch * _surfaceInfo.Height <= _surfaceInfo.Size);
-
-            _surface = new Bitmap((int)totalWidth, (int)totalHeight, PixelFormat.Format32bppArgb);
-        }
-
-        private void RecomputeConstants()
-        {
-            float totalWidth  = _surfaceInfo.Width;
-            float totalHeight = _surfaceInfo.Height;
-
-            float panelHeight = 240;
-
-            _panelPositionY = totalHeight - panelHeight;
-            _panelRectangle = new RectangleF(0, 0, totalWidth, panelHeight);
-
-            _messagePositionY = 60;
-
-            float logoPositionX = (totalWidth - _ryujinxLogo.Width) / 2;
-            float logoPositionY = 18;
-
-            _logoPosition = new PointF(logoPositionX, logoPositionY);
-        }
-
-        private StringFormat CreateStringFormat(string text)
-        {
-            StringFormat format = new StringFormat(StringFormat.GenericTypographic);
-            format.FormatFlags |= StringFormatFlags.MeasureTrailingSpaces;
-            format.SetMeasurableCharacterRanges(new CharacterRange[] { new CharacterRange(0, text.Length) });
-
-            return format;
-        }
-
-        private RectangleF MeasureString(System.Drawing.Graphics graphics, string text, System.Drawing.Font font)
-        {
-            bool isEmpty = false;
-
-            if (string.IsNullOrEmpty(text))
-            {
-                isEmpty = true;
-                text = " ";
-            }
-
-            var format    = CreateStringFormat(text);
-            var rectangle = new RectangleF(0, 0, float.PositiveInfinity, float.PositiveInfinity);
-            var regions   = graphics.MeasureCharacterRanges(text, font, rectangle, format);
-
-            Debug.Assert(regions.Length == 1);
-
-            rectangle = regions[0].GetBounds(graphics);
-
-            if (isEmpty)
-            {
-                rectangle.Width = 0;
-            }
-            else
-            {
-                rectangle.Width += 1.0f;
-            }
-
-            return rectangle;
-        }
-
-        private float CalibrateTextHeight(Font font)
-        {
-            // This is a pixel-wise calibration that tests the offset of a reference character because Windows text measurement
-            // is horrible when compared to other frameworks like Cairo and diverge across systems and fonts.
-
-            Debug.Assert(font.Unit == GraphicsUnit.Pixel);
-
-            var surfaceSize = (int)Math.Ceiling(2 * font.Size);
-
-            string calibrationText = "|";
-
-            using (var surface = new Bitmap(surfaceSize, surfaceSize, PixelFormat.Format32bppArgb))
-            using (var graphics = CreateGraphics(surface))
-            {
-                var measuredRectangle = MeasureString(graphics, calibrationText, font);
-
-                Debug.Assert(measuredRectangle.Right  <= surfaceSize);
-                Debug.Assert(measuredRectangle.Bottom <= surfaceSize);
-
-                var textPosition = new PointF(0, 0);
-
-                graphics.Clear(Color.Transparent);
-                DrawString(graphics, calibrationText, font, Brushes.White, textPosition);
-
-                var lockRectangle = new Rectangle(0, 0, surface.Width, surface.Height);
-                var surfaceData   = surface.LockBits(lockRectangle, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
-                var surfaceBytes  = new byte[surfaceData.Stride * surfaceData.Height];
-
-                Marshal.Copy(surfaceData.Scan0, surfaceBytes, 0, surfaceBytes.Length);
-
-                Point topLeft    = new Point();
-                Point bottomLeft = new Point();
-
-                bool foundTopLeft = false;
-
-                for (int y = 0; y < surfaceData.Height; y++)
-                {
-                    for (int x = 0; x < surfaceData.Stride; x += 4)
-                    {
-                        int position = y * surfaceData.Stride + x;
-
-                        if (surfaceBytes[position] != 0)
-                        {
-                            if (!foundTopLeft)
-                            {
-                                topLeft.X    = x;
-                                topLeft.Y    = y;
-                                foundTopLeft = true;
-
-                                break;
-                            }
-                            else
-                            {
-                                bottomLeft.X = x;
-                                bottomLeft.Y = y;
-
-                                break;
-                            }
-                        }
-                    }
-                }
-
-                return bottomLeft.Y - topLeft.Y;
-            }
-        }
-
-        private void DrawString(System.Drawing.Graphics graphics, string text, Font font, Brush brush, PointF point)
-        {
-            var format = CreateStringFormat(text);
-            graphics.DrawString(text, font, brush, point, format);
-        }
-
-        private System.Drawing.Graphics CreateGraphics()
-        {
-            return CreateGraphics(_surface);
-        }
-
-        private System.Drawing.Graphics CreateGraphics(Image surface)
-        {
-            var graphics = System.Drawing.Graphics.FromImage(surface);
-
-            graphics.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
-            graphics.InterpolationMode = InterpolationMode.NearestNeighbor;
-            graphics.CompositingQuality = CompositingQuality.HighSpeed;
-            graphics.CompositingMode = CompositingMode.SourceOver;
-            graphics.PixelOffsetMode = PixelOffsetMode.HighSpeed;
-            graphics.SmoothingMode = SmoothingMode.HighSpeed;
-
-            return graphics;
-        }
-
-        private void DrawTextBox(System.Drawing.Graphics graphics)
-        {
-            var inputTextRectangle = MeasureString(graphics, _inputText, _inputTextFont);
-
-            float boxWidth  = (int)(Math.Max(300, inputTextRectangle.Width + inputTextRectangle.X + 8));
-            float boxHeight = 32;
-            float boxY      = 110;
-            float boxX      = (int)((_panelRectangle.Width - boxWidth) / 2);
-
-            graphics.DrawRectangle(_textBoxOutlinePen, boxX, boxY, boxWidth, boxHeight);
-
-            float inputTextX = (_panelRectangle.Width - inputTextRectangle.Width) / 2 - inputTextRectangle.X;
-            float inputTextY = boxY + boxHeight - inputTextRectangle.Bottom - 5;
-
-            var inputTextPosition = new PointF(inputTextX, inputTextY);
-
-            DrawString(graphics, _inputText, _inputTextFont, _textNormalBrush, inputTextPosition);
-
-            // Draw the cursor on top of the text and redraw the text with a different color if necessary.
-
-            Brush cursorTextBrush;
-            Brush cursorBrush;
-            Pen   cursorPen;
-
-            float cursorPositionYBottom = inputTextY + inputTextRectangle.Bottom;
-            float cursorPositionYTop    = cursorPositionYBottom - _inputTextCalibrationHeight - 2;
-            float cursorPositionXLeft;
-            float cursorPositionXRight;
-
-            bool cursorVisible = false;
-
-            if (_cursorStart != _cursorEnd)
-            {
-                cursorTextBrush = _textSelectedBrush;
-                cursorBrush     = _selectionBoxBrush;
-                cursorPen       = _selectionBoxPen;
-
-                string textUntilBegin = _inputText.Substring(0, _cursorStart);
-                string textUntilEnd   = _inputText.Substring(0, _cursorEnd);
-
-                RectangleF selectionBeginRectangle = MeasureString(graphics, textUntilBegin, _inputTextFont);
-                RectangleF selectionEndRectangle   = MeasureString(graphics, textUntilEnd  , _inputTextFont);
-
-                cursorVisible         = true;
-                cursorPositionXLeft   = inputTextX + selectionBeginRectangle.Width + selectionBeginRectangle.X;
-                cursorPositionXRight  = inputTextX + selectionEndRectangle.Width   + selectionEndRectangle.X;
-            }
-            else
-            {
-                cursorTextBrush = _textOverCursorBrush;
-                cursorBrush     = _cursorBrush;
-                cursorPen       = _cursorPen;
-
-                if (Volatile.Read(ref _textBoxBlinkCounter.Value) < TextBoxBlinkThreshold)
-                {
-                    // Show the blinking cursor.
-
-                    int        cursorStart         = Math.Min(_inputText.Length, _cursorStart);
-                    string     textUntilCursor     = _inputText.Substring(0, cursorStart);
-                    RectangleF cursorTextRectangle = MeasureString(graphics, textUntilCursor, _inputTextFont);
-
-                    cursorVisible       = true;
-                    cursorPositionXLeft = inputTextX + cursorTextRectangle.Width + cursorTextRectangle.X;
-
-                    if (_overwriteMode)
-                    {
-                        // The blinking cursor is in overwrite mode so it takes the size of a character.
-
-                        if (_cursorStart < _inputText.Length)
-                        {
-                            textUntilCursor      = _inputText.Substring(0, cursorStart + 1);
-                            cursorTextRectangle  = MeasureString(graphics, textUntilCursor, _inputTextFont);
-                            cursorPositionXRight = inputTextX + cursorTextRectangle.Width + cursorTextRectangle.X;
-                        }
-                        else
-                        {
-                            cursorPositionXRight = cursorPositionXLeft + _inputTextFontSize / 2;
-                        }
-                    }
-                    else
-                    {
-                        // The blinking cursor is in insert mode so it is only a line.
-                        cursorPositionXRight = cursorPositionXLeft;
-                    }
-                }
-                else
-                {
-                    cursorPositionXLeft  = inputTextX;
-                    cursorPositionXRight = inputTextX;
-                }
-            }
-
-            if (_typingEnabled && cursorVisible)
-            {
-                float cursorWidth  = cursorPositionXRight  - cursorPositionXLeft;
-                float cursorHeight = cursorPositionYBottom - cursorPositionYTop;
-
-                if (cursorWidth == 0)
-                {
-                    graphics.DrawLine(cursorPen, cursorPositionXLeft, cursorPositionYTop, cursorPositionXLeft, cursorPositionYBottom);
-                }
-                else
-                {
-                    graphics.DrawRectangle(cursorPen,   cursorPositionXLeft, cursorPositionYTop, cursorWidth, cursorHeight);
-                    graphics.FillRectangle(cursorBrush, cursorPositionXLeft, cursorPositionYTop, cursorWidth, cursorHeight);
-
-                    var cursorRectangle = new RectangleF(cursorPositionXLeft, cursorPositionYTop, cursorWidth, cursorHeight);
-
-                    var oldClip   = graphics.Clip;
-                    graphics.Clip = new Region(cursorRectangle);
-
-                    DrawString(graphics, _inputText, _inputTextFont, cursorTextBrush, inputTextPosition);
-
-                    graphics.Clip = oldClip;
-                }
-            }
-            else if (!_typingEnabled)
-            {
-                // Just draw a semi-transparent rectangle on top to fade the component with the background.
-                // TODO (caian): This will not work if one decides to add make background semi-transparent as well.
-                graphics.FillRectangle(_disabledBrush, boxX - _textBoxOutlineWidth, boxY - _textBoxOutlineWidth,
-                    boxWidth + 2* _textBoxOutlineWidth, boxHeight + 2* _textBoxOutlineWidth);
-            }
-        }
-
-        private void DrawPadButton(System.Drawing.Graphics graphics, PointF point, Image icon, string label, bool pressed, bool enabled)
-        {
-            // Use relative positions so we can center the the entire drawing later.
-
-            float iconX      = 0;
-            float iconY      = 0;
-            float iconWidth  = icon.Width;
-            float iconHeight = icon.Height;
-
-            var labelRectangle = MeasureString(graphics, label, _labelsTextFont);
-
-            float labelPositionX = iconWidth + 8 - labelRectangle.X;
-            float labelPositionY = (iconHeight - labelRectangle.Height) / 2 - labelRectangle.Y - 1;
-
-            float fullWidth  = labelPositionX + labelRectangle.Width + labelRectangle.X;
-            float fullHeight = iconHeight;
-
-            // Convert all relative positions into absolute.
-
-            float originX = (int)(point.X - fullWidth  / 2);
-            float originY = (int)(point.Y - fullHeight / 2);
-
-            iconX += originX;
-            iconY += originY;
-
-            var labelPosition = new PointF(labelPositionX + originX, labelPositionY + originY);
-
-            graphics.DrawImageUnscaled(icon, (int)iconX, (int)iconY);
-
-            DrawString(graphics, label, _labelsTextFont, _textNormalBrush, labelPosition);
-
-            GraphicsPath frame = new GraphicsPath();
-            frame.AddRectangle(new RectangleF(originX - 2 * _padPressedPenWidth, originY - 2 * _padPressedPenWidth,
-                fullWidth + 4 * _padPressedPenWidth, fullHeight + 4 * _padPressedPenWidth));
-
-            if (enabled)
-            {
-                if (pressed)
-                {
-                    graphics.DrawPath(_padPressedPen, frame);
-                }
-            }
-            else
-            {
-                // Just draw a semi-transparent rectangle on top to fade the component with the background.
-                // TODO (caian): This will not work if one decides to add make background semi-transparent as well.
-                graphics.FillPath(_disabledBrush, frame);
-            }
-        }
-
-        private void DrawControllerToggle(System.Drawing.Graphics graphics, PointF point, bool enabled)
-        {
-            var labelRectangle = MeasureString(graphics, ControllerToggleText, _labelsTextFont);
-
-            // Use relative positions so we can center the the entire drawing later.
-
-            float keyWidth  = _keyModeIcon.Width;
-            float keyHeight = _keyModeIcon.Height;
-
-            float labelPositionX = keyWidth + 8 - labelRectangle.X;
-            float labelPositionY = -labelRectangle.Y - 1;
-
-            float keyX = 0;
-            float keyY = (int)((labelPositionY + labelRectangle.Height - keyHeight) / 2);
-
-            float fullWidth  = labelPositionX + labelRectangle.Width;
-            float fullHeight = Math.Max(labelPositionY + labelRectangle.Height, keyHeight);
-
-            // Convert all relative positions into absolute.
-
-            float originX = (int)(point.X - fullWidth  / 2);
-            float originY = (int)(point.Y - fullHeight / 2);
-
-            keyX += originX;
-            keyY += originY;
-
-            var labelPosition   = new PointF(labelPositionX + originX, labelPositionY + originY);
-            var overlayPosition = new Point((int)keyX, (int)keyY);
-
-            graphics.DrawImageUnscaled(_keyModeIcon, overlayPosition);
-
-            DrawString(graphics, ControllerToggleText, _labelsTextFont, _textNormalBrush, labelPosition);
-        }
-
-        private bool TryCopyTo(IVirtualMemoryManager destination, ulong position)
-        {
-            if (_surface == null)
-            {
-                return false;
-            }
-
-            Rectangle lockRectangle = new Rectangle(0, 0, _surface.Width, _surface.Height);
-            BitmapData surfaceData  = _surface.LockBits(lockRectangle, ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
-
-            Debug.Assert(surfaceData.Stride                      == _surfaceInfo.Pitch);
-            Debug.Assert(surfaceData.Stride * surfaceData.Height == _surfaceInfo.Size);
-
-            // Convert the pixel format used in System.Drawing to the one required by a Switch Surface.
-            int dataLength = surfaceData.Stride * surfaceData.Height;
-
-            byte[] data = new byte[dataLength];
-            Span<uint> dataConvert = MemoryMarshal.Cast<byte, uint>(data);
-
-            Marshal.Copy(surfaceData.Scan0, data, 0, dataLength);
-
-            for (int i = 0; i < dataConvert.Length; i++)
-            {
-                dataConvert[i] = BitOperations.RotateRight(BinaryPrimitives.ReverseEndianness(dataConvert[i]), 8);
-            }
-
-            try
-            {
-                destination.Write(position, data);
-            }
-            finally
-            {
-                _surface.UnlockBits(surfaceData);
-            }
-
-            return true;
-        }
-
-        internal bool DrawTo(RenderingSurfaceInfo surfaceInfo, IVirtualMemoryManager destination, ulong position)
-        {
-            lock (_renderLock)
-            {
-                if (!_surfaceInfo.Equals(surfaceInfo))
-                {
-                    _surfaceInfo = surfaceInfo;
-                    RecreateSurface();
-                    RecomputeConstants();
-                }
-
-                Redraw();
-
-                return TryCopyTo(destination, position);
-            }
+            return _renderer.WriteBufferToMemory(destination, position);
         }
 
         public void Dispose()
         {
             _textBoxBlinkTimedAction.RequestCancel();
+            _renderAction.RequestCancel();
         }
     }
 }
diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRendererBase.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRendererBase.cs
new file mode 100644
index 0000000000..b059200dd2
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRendererBase.cs
@@ -0,0 +1,585 @@
+using Ryujinx.HLE.Ui;
+using Ryujinx.Memory;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Processing;
+using SixLabors.ImageSharp.Drawing.Processing;
+using SixLabors.Fonts;
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Numerics;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
+{
+    /// <summary>
+    /// Base class that generates the graphics for the software keyboard applet during inline mode.
+    /// </summary>
+    internal class SoftwareKeyboardRendererBase
+    {
+        public const int TextBoxBlinkThreshold = 8;
+
+        const string MessageText          = "Please use the keyboard to input text";
+        const string AcceptText           = "Accept";
+        const string CancelText           = "Cancel";
+        const string ControllerToggleText = "Toggle input";
+
+        private readonly object _bufferLock = new object();
+
+        private RenderingSurfaceInfo _surfaceInfo = null;
+        private Image<Argb32>        _surface     = null;
+        private byte[]               _bufferData  = null;
+
+        private Image _ryujinxLogo   = null;
+        private Image _padAcceptIcon = null;
+        private Image _padCancelIcon = null;
+        private Image _keyModeIcon   = null;
+
+        private float _textBoxOutlineWidth;
+        private float _padPressedPenWidth;
+
+        private Color _textNormalColor;
+        private Color _textSelectedColor;
+        private Color _textOverCursorColor;
+
+        private IBrush _panelBrush;
+        private IBrush _disabledBrush;
+        private IBrush _cursorBrush;
+        private IBrush _selectionBoxBrush;
+
+        private Pen _textBoxOutlinePen;
+        private Pen _cursorPen;
+        private Pen _selectionBoxPen;
+        private Pen _padPressedPen;
+
+        private int  _inputTextFontSize;
+        private Font _messageFont;
+        private Font _inputTextFont;
+        private Font _labelsTextFont;
+
+        private RectangleF _panelRectangle;
+        private Point      _logoPosition;
+        private float      _messagePositionY;
+
+        public SoftwareKeyboardRendererBase(IHostUiTheme uiTheme)
+        {
+            string ryujinxLogoPath = "Ryujinx.Ui.Resources.Logo_Ryujinx.png";
+            int    ryujinxLogoSize = 32;
+
+            _ryujinxLogo = LoadResource(Assembly.GetEntryAssembly(), ryujinxLogoPath, ryujinxLogoSize, ryujinxLogoSize);
+
+            string padAcceptIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Icon_BtnA.png";
+            string padCancelIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Icon_BtnB.png";
+            string keyModeIconPath   = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Icon_KeyF6.png";
+
+            _padAcceptIcon = LoadResource(Assembly.GetExecutingAssembly(), padAcceptIconPath  , 0, 0);
+            _padCancelIcon = LoadResource(Assembly.GetExecutingAssembly(), padCancelIconPath  , 0, 0);
+            _keyModeIcon   = LoadResource(Assembly.GetExecutingAssembly(), keyModeIconPath    , 0, 0);
+
+            Color panelColor               = ToColor(uiTheme.DefaultBackgroundColor, 255);
+            Color panelTransparentColor    = ToColor(uiTheme.DefaultBackgroundColor, 150);
+            Color borderColor              = ToColor(uiTheme.DefaultBorderColor);
+            Color selectionBackgroundColor = ToColor(uiTheme.SelectionBackgroundColor);
+
+            _textNormalColor     = ToColor(uiTheme.DefaultForegroundColor);
+            _textSelectedColor   = ToColor(uiTheme.SelectionForegroundColor);
+            _textOverCursorColor = ToColor(uiTheme.DefaultForegroundColor, null, true);
+
+            float cursorWidth = 2;
+
+            _textBoxOutlineWidth = 2;
+            _padPressedPenWidth  = 2;
+
+            _panelBrush        = new SolidBrush(panelColor);
+            _disabledBrush     = new SolidBrush(panelTransparentColor);
+            _cursorBrush       = new SolidBrush(_textNormalColor);
+            _selectionBoxBrush = new SolidBrush(selectionBackgroundColor);
+
+            _textBoxOutlinePen = new Pen(borderColor, _textBoxOutlineWidth);
+            _cursorPen         = new Pen(_textNormalColor, cursorWidth);
+            _selectionBoxPen   = new Pen(selectionBackgroundColor, cursorWidth);
+            _padPressedPen     = new Pen(borderColor, _padPressedPenWidth);
+
+            _inputTextFontSize = 20;
+
+            CreateFonts(uiTheme.FontFamily);
+        }
+
+        private void CreateFonts(string uiThemeFontFamily)
+        {
+            // Try a list of fonts in case any of them is not available in the system.
+
+            string[] availableFonts = new string[]
+            {
+                uiThemeFontFamily,
+                "Liberation Sans",
+                "FreeSans",
+                "DejaVu Sans"
+            };
+
+            foreach (string fontFamily in availableFonts)
+            {
+                try
+                {
+                    _messageFont    = SystemFonts.CreateFont(fontFamily, 26,                 FontStyle.Regular);
+                    _inputTextFont  = SystemFonts.CreateFont(fontFamily, _inputTextFontSize, FontStyle.Regular);
+                    _labelsTextFont = SystemFonts.CreateFont(fontFamily, 24,                 FontStyle.Regular);
+
+                    return;
+                }
+                catch
+                {
+                }
+            }
+
+            throw new Exception($"None of these fonts were found in the system: {String.Join(", ", availableFonts)}!");
+        }
+
+        private Color ToColor(ThemeColor color, byte? overrideAlpha = null, bool flipRgb = false)
+        {
+            var a = (byte)(color.A * 255);
+            var r = (byte)(color.R * 255);
+            var g = (byte)(color.G * 255);
+            var b = (byte)(color.B * 255);
+
+            if (flipRgb)
+            {
+                r = (byte)(255 - r);
+                g = (byte)(255 - g);
+                b = (byte)(255 - b);
+            }
+
+            return Color.FromRgba(r, g, b, overrideAlpha.GetValueOrDefault(a));
+        }
+
+        private Image LoadResource(Assembly assembly, string resourcePath, int newWidth, int newHeight)
+        {
+            Stream resourceStream = assembly.GetManifestResourceStream(resourcePath);
+
+            Debug.Assert(resourceStream != null);
+
+            var image = Image.Load(resourceStream);
+
+            if (newHeight != 0 && newWidth != 0)
+            {
+                image.Mutate(x => x.Resize(newWidth, newHeight, KnownResamplers.Lanczos3));
+            }
+
+            return image;
+        }
+
+        private void SetGraphicsOptions(IImageProcessingContext context)
+        {
+            context.GetGraphicsOptions().Antialias = true;
+            context.GetShapeGraphicsOptions().GraphicsOptions.Antialias = true;
+        }
+
+        private void DrawImmutableElements()
+        {
+            if (_surface == null)
+            {
+                return;
+            }
+
+            _surface.Mutate(context =>
+            {
+                SetGraphicsOptions(context);
+
+                context.Clear(Color.Transparent);
+                context.Fill(_panelBrush, _panelRectangle);
+                context.DrawImage(_ryujinxLogo, _logoPosition, 1);
+
+                float halfWidth = _panelRectangle.Width / 2;
+                float buttonsY  = _panelRectangle.Y + 185;
+
+                PointF disableButtonPosition = new PointF(halfWidth + 180, buttonsY);
+
+                DrawControllerToggle(context, disableButtonPosition);
+            });
+        }
+
+        public void DrawMutableElements(SoftwareKeyboardUiState state)
+        {
+            if (_surface == null)
+            {
+                return;
+            }
+
+            _surface.Mutate(context =>
+            {
+                var    messageRectangle      = MeasureString(MessageText, _messageFont);
+                float  messagePositionX      = (_panelRectangle.Width - messageRectangle.Width) / 2 - messageRectangle.X;
+                float  messagePositionY      = _messagePositionY - messageRectangle.Y;
+                var    messagePosition       = new PointF(messagePositionX, messagePositionY);
+                var    messageBoundRectangle = new RectangleF(messagePositionX, messagePositionY, messageRectangle.Width, messageRectangle.Height);
+
+                SetGraphicsOptions(context);
+
+                context.Fill(_panelBrush, messageBoundRectangle);
+
+                context.DrawText(MessageText, _messageFont, _textNormalColor, messagePosition);
+
+                if (!state.TypingEnabled)
+                {
+                    // Just draw a semi-transparent rectangle on top to fade the component with the background.
+                    // TODO (caian): This will not work if one decides to add make background semi-transparent as well.
+
+                    context.Fill(_disabledBrush, messageBoundRectangle);
+                }
+
+                DrawTextBox(context, state);
+
+                float halfWidth = _panelRectangle.Width / 2;
+                float buttonsY  = _panelRectangle.Y + 185;
+
+                PointF acceptButtonPosition  = new PointF(halfWidth - 180, buttonsY);
+                PointF cancelButtonPosition  = new PointF(halfWidth      , buttonsY);
+                PointF disableButtonPosition = new PointF(halfWidth + 180, buttonsY);
+
+                DrawPadButton(context, acceptButtonPosition, _padAcceptIcon, AcceptText, state.AcceptPressed, state.ControllerEnabled);
+                DrawPadButton(context, cancelButtonPosition, _padCancelIcon, CancelText, state.CancelPressed, state.ControllerEnabled);
+            });
+        }
+
+        public void CreateSurface(RenderingSurfaceInfo surfaceInfo)
+        {
+            if (_surfaceInfo != null)
+            {
+                return;
+            }
+
+            _surfaceInfo = surfaceInfo;
+
+            Debug.Assert(_surfaceInfo.ColorFormat == Services.SurfaceFlinger.ColorFormat.A8B8G8R8);
+
+            // Use the whole area of the image to draw, even the alignment, otherwise it may shear the final
+            // image if the pitch is different.
+            uint totalWidth  = _surfaceInfo.Pitch / 4;
+            uint totalHeight = _surfaceInfo.Size / _surfaceInfo.Pitch;
+
+            Debug.Assert(_surfaceInfo.Width <= totalWidth);
+            Debug.Assert(_surfaceInfo.Height <= totalHeight);
+            Debug.Assert(_surfaceInfo.Pitch * _surfaceInfo.Height <= _surfaceInfo.Size);
+
+            _surface = new Image<Argb32>((int)totalWidth, (int)totalHeight);
+
+            ComputeConstants();
+            DrawImmutableElements();
+        }
+
+        private void ComputeConstants()
+        {
+            int totalWidth  = (int)_surfaceInfo.Width;
+            int totalHeight = (int)_surfaceInfo.Height;
+
+            int panelHeight    = 240;
+            int panelPositionY = totalHeight - panelHeight;
+
+            _panelRectangle = new RectangleF(0, panelPositionY, totalWidth, panelHeight);
+
+            _messagePositionY = panelPositionY + 60;
+
+            int logoPositionX = (totalWidth - _ryujinxLogo.Width) / 2;
+            int logoPositionY = panelPositionY + 18;
+
+            _logoPosition = new Point(logoPositionX, logoPositionY);
+        }
+
+        private RectangleF MeasureString(string text, Font font)
+        {
+            RendererOptions options = new RendererOptions(font);
+            FontRectangle rectangle = TextMeasurer.Measure(text == "" ? " " : text, options);
+
+            if (text == "")
+            {
+                return new RectangleF(0, rectangle.Y, 0, rectangle.Height);
+            }
+            else
+            {
+                return new RectangleF(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
+            }
+        }
+
+        private void DrawTextBox(IImageProcessingContext context, SoftwareKeyboardUiState state)
+        {
+            var inputTextRectangle = MeasureString(state.InputText, _inputTextFont);
+
+            float boxWidth  = (int)(Math.Max(300, inputTextRectangle.Width + inputTextRectangle.X + 8));
+            float boxHeight = 32;
+            float boxY      = _panelRectangle.Y + 110;
+            float boxX      = (int)((_panelRectangle.Width - boxWidth) / 2);
+
+            RectangleF boxRectangle = new RectangleF(boxX, boxY, boxWidth, boxHeight);
+
+            RectangleF boundRectangle = new RectangleF(_panelRectangle.X, boxY - _textBoxOutlineWidth,
+                    _panelRectangle.Width, boxHeight + 2 * _textBoxOutlineWidth);
+
+            context.Fill(_panelBrush, boundRectangle);
+
+            context.Draw(_textBoxOutlinePen, boxRectangle);
+
+            float inputTextX = (_panelRectangle.Width - inputTextRectangle.Width) / 2 - inputTextRectangle.X;
+            float inputTextY = boxY + 5;
+
+            var inputTextPosition = new PointF(inputTextX, inputTextY);
+
+            context.DrawText(state.InputText, _inputTextFont, _textNormalColor, inputTextPosition);
+
+            // Draw the cursor on top of the text and redraw the text with a different color if necessary.
+
+            Color  cursorTextColor;
+            IBrush cursorBrush;
+            Pen    cursorPen;
+
+            float cursorPositionYTop    = inputTextY + 1;
+            float cursorPositionYBottom = cursorPositionYTop + _inputTextFontSize + 1;
+            float cursorPositionXLeft;
+            float cursorPositionXRight;
+
+            bool cursorVisible = false;
+
+            if (state.CursorBegin != state.CursorEnd)
+            {
+                Debug.Assert(state.InputText.Length > 0);
+
+                cursorTextColor = _textSelectedColor;
+                cursorBrush     = _selectionBoxBrush;
+                cursorPen       = _selectionBoxPen;
+
+                string textUntilBegin = state.InputText.Substring(0, state.CursorBegin);
+                string textUntilEnd   = state.InputText.Substring(0, state.CursorEnd);
+
+                var selectionBeginRectangle = MeasureString(textUntilBegin, _inputTextFont);
+                var selectionEndRectangle   = MeasureString(textUntilEnd  , _inputTextFont);
+
+                cursorVisible         = true;
+                cursorPositionXLeft   = inputTextX + selectionBeginRectangle.Width + selectionBeginRectangle.X;
+                cursorPositionXRight  = inputTextX + selectionEndRectangle.Width   + selectionEndRectangle.X;
+            }
+            else
+            {
+                cursorTextColor = _textOverCursorColor;
+                cursorBrush     = _cursorBrush;
+                cursorPen       = _cursorPen;
+
+                if (state.TextBoxBlinkCounter < TextBoxBlinkThreshold)
+                {
+                    // Show the blinking cursor.
+
+                    int    cursorBegin         = Math.Min(state.InputText.Length, state.CursorBegin);
+                    string textUntilCursor     = state.InputText.Substring(0, cursorBegin);
+                    var    cursorTextRectangle = MeasureString(textUntilCursor, _inputTextFont);
+
+                    cursorVisible       = true;
+                    cursorPositionXLeft = inputTextX + cursorTextRectangle.Width + cursorTextRectangle.X;
+
+                    if (state.OverwriteMode)
+                    {
+                        // The blinking cursor is in overwrite mode so it takes the size of a character.
+
+                        if (state.CursorBegin < state.InputText.Length)
+                        {
+                            textUntilCursor      = state.InputText.Substring(0, cursorBegin + 1);
+                            cursorTextRectangle  = MeasureString(textUntilCursor, _inputTextFont);
+                            cursorPositionXRight = inputTextX + cursorTextRectangle.Width + cursorTextRectangle.X;
+                        }
+                        else
+                        {
+                            cursorPositionXRight = cursorPositionXLeft + _inputTextFontSize / 2;
+                        }
+                    }
+                    else
+                    {
+                        // The blinking cursor is in insert mode so it is only a line.
+                        cursorPositionXRight = cursorPositionXLeft;
+                    }
+                }
+                else
+                {
+                    cursorPositionXLeft  = inputTextX;
+                    cursorPositionXRight = inputTextX;
+                }
+            }
+
+            if (state.TypingEnabled && cursorVisible)
+            {
+                float cursorWidth  = cursorPositionXRight  - cursorPositionXLeft;
+                float cursorHeight = cursorPositionYBottom - cursorPositionYTop;
+
+                if (cursorWidth == 0)
+                {
+                    PointF[] points = new PointF[]
+                    {
+                        new PointF(cursorPositionXLeft, cursorPositionYTop),
+                        new PointF(cursorPositionXLeft, cursorPositionYBottom),
+                    };
+
+                    context.DrawLines(cursorPen, points);
+                }
+                else
+                {
+                    var cursorRectangle = new RectangleF(cursorPositionXLeft, cursorPositionYTop, cursorWidth, cursorHeight);
+
+                    context.Draw(cursorPen  , cursorRectangle);
+                    context.Fill(cursorBrush, cursorRectangle);
+
+                    Image<Argb32> textOverCursor = new Image<Argb32>((int)cursorRectangle.Width, (int)cursorRectangle.Height);
+                    textOverCursor.Mutate(context =>
+                    {
+                        var textRelativePosition = new PointF(inputTextPosition.X - cursorRectangle.X, inputTextPosition.Y - cursorRectangle.Y);
+                        context.DrawText(state.InputText, _inputTextFont, cursorTextColor, textRelativePosition);
+                    });
+
+                    var cursorPosition = new Point((int)cursorRectangle.X, (int)cursorRectangle.Y);
+                    context.DrawImage(textOverCursor, cursorPosition, 1);
+                }
+            }
+            else if (!state.TypingEnabled)
+            {
+                // Just draw a semi-transparent rectangle on top to fade the component with the background.
+                // TODO (caian): This will not work if one decides to add make background semi-transparent as well.
+
+                context.Fill(_disabledBrush, boundRectangle);
+            }
+        }
+
+        private void DrawPadButton(IImageProcessingContext context, PointF point, Image icon, string label, bool pressed, bool enabled)
+        {
+            // Use relative positions so we can center the the entire drawing later.
+
+            float iconX      = 0;
+            float iconY      = 0;
+            float iconWidth  = icon.Width;
+            float iconHeight = icon.Height;
+
+            var labelRectangle = MeasureString(label, _labelsTextFont);
+
+            float labelPositionX = iconWidth + 8 - labelRectangle.X;
+            float labelPositionY = 3;
+
+            float fullWidth  = labelPositionX + labelRectangle.Width + labelRectangle.X;
+            float fullHeight = iconHeight;
+
+            // Convert all relative positions into absolute.
+
+            float originX = (int)(point.X - fullWidth  / 2);
+            float originY = (int)(point.Y - fullHeight / 2);
+
+            iconX += originX;
+            iconY += originY;
+
+            var iconPosition  = new Point((int)iconX, (int)iconY);
+            var labelPosition = new PointF(labelPositionX + originX, labelPositionY + originY);
+
+            var selectedRectangle = new RectangleF(originX - 2 * _padPressedPenWidth, originY - 2 * _padPressedPenWidth,
+                fullWidth + 4 * _padPressedPenWidth, fullHeight + 4 * _padPressedPenWidth);
+
+            var boundRectangle = new RectangleF(originX, originY, fullWidth, fullHeight);
+            boundRectangle.Inflate(4 * _padPressedPenWidth, 4 * _padPressedPenWidth);
+
+            context.Fill(_panelBrush, boundRectangle);
+            context.DrawImage(icon, iconPosition, 1);
+            context.DrawText(label, _labelsTextFont, _textNormalColor, labelPosition);
+
+            if (enabled)
+            {
+                if (pressed)
+                {
+                    context.Draw(_padPressedPen, selectedRectangle);
+                }
+            }
+            else
+            {
+                // Just draw a semi-transparent rectangle on top to fade the component with the background.
+                // TODO (caian): This will not work if one decides to add make background semi-transparent as well.
+
+                context.Fill(_disabledBrush, boundRectangle);
+            }
+        }
+
+        private void DrawControllerToggle(IImageProcessingContext context, PointF point)
+        {
+            var labelRectangle = MeasureString(ControllerToggleText, _labelsTextFont);
+
+            // Use relative positions so we can center the the entire drawing later.
+
+            float keyWidth  = _keyModeIcon.Width;
+            float keyHeight = _keyModeIcon.Height;
+
+            float labelPositionX = keyWidth + 8 - labelRectangle.X;
+            float labelPositionY = -labelRectangle.Y - 1;
+
+            float keyX = 0;
+            float keyY = (int)((labelPositionY + labelRectangle.Height - keyHeight) / 2);
+
+            float fullWidth  = labelPositionX + labelRectangle.Width;
+            float fullHeight = Math.Max(labelPositionY + labelRectangle.Height, keyHeight);
+
+            // Convert all relative positions into absolute.
+
+            float originX = (int)(point.X - fullWidth  / 2);
+            float originY = (int)(point.Y - fullHeight / 2);
+
+            keyX += originX;
+            keyY += originY;
+
+            var labelPosition   = new PointF(labelPositionX + originX, labelPositionY + originY);
+            var overlayPosition = new Point((int)keyX, (int)keyY);
+
+            context.DrawImage(_keyModeIcon, overlayPosition, 1);
+            context.DrawText(ControllerToggleText, _labelsTextFont, _textNormalColor, labelPosition);
+        }
+
+        public void CopyImageToBuffer()
+        {
+            lock (_bufferLock)
+            {
+                if (_surface == null)
+                {
+                    return;
+                }
+
+                // Convert the pixel format used in the image to the one used in the Switch surface.
+
+                if (!_surface.TryGetSinglePixelSpan(out Span<Argb32> pixels))
+                {
+                    return;
+                }
+
+                _bufferData = MemoryMarshal.AsBytes(pixels).ToArray();
+                Span<uint> dataConvert = MemoryMarshal.Cast<byte, uint>(_bufferData);
+
+                Debug.Assert(_bufferData.Length == _surfaceInfo.Size);
+
+                for (int i = 0; i < dataConvert.Length; i++)
+                {
+                    dataConvert[i] = BitOperations.RotateRight(dataConvert[i], 8);
+                }
+            }
+        }
+
+        public bool WriteBufferToMemory(IVirtualMemoryManager destination, ulong position)
+        {
+            lock (_bufferLock)
+            {
+                if (_bufferData == null)
+                {
+                    return false;
+                }
+
+                try
+                {
+                    destination.Write(position, _bufferData);
+                }
+                catch
+                {
+                    return false;
+                }
+
+                return true;
+            }
+        }
+    }
+}
diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUiState.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUiState.cs
new file mode 100644
index 0000000000..e6131e62d0
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUiState.cs
@@ -0,0 +1,22 @@
+using Ryujinx.HLE.Ui;
+
+namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
+{
+    /// <summary>
+    /// TODO
+    /// </summary>
+    internal class SoftwareKeyboardUiState
+    {
+        public string InputText           = "";
+        public int    CursorBegin         = 0;
+        public int    CursorEnd           = 0;
+        public bool   AcceptPressed       = false;
+        public bool   CancelPressed       = false;
+        public bool   OverwriteMode       = false;
+        public bool   TypingEnabled       = true;
+        public bool   ControllerEnabled   = true;
+        public int    TextBoxBlinkCounter = 0;
+
+        public RenderingSurfaceInfo SurfaceInfo = null;
+    }
+}
diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/TimedAction.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/TimedAction.cs
index 8884bdcf43..0de78a0e52 100644
--- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/TimedAction.cs
+++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/TimedAction.cs
@@ -144,6 +144,20 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
             }), cancelled);
         }
 
+        public void Reset(Action action)
+        {
+            // Create a dedicated cancel token for each task.
+            var cancelled = new TRef<bool>(false);
+
+            Reset(new Thread(() =>
+            {
+                while (!Volatile.Read(ref cancelled.Value))
+                {
+                    action();
+                }
+            }), cancelled);
+        }
+
         private static bool SleepWithSubstep(SleepSubstepData substepData, TRef<bool> cancelled)
         {
             for (int i = 0; i < substepData.SleepCount; i++)
diff --git a/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs
index 9510ef4c5a..3eb88950a7 100644
--- a/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs
+++ b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs
@@ -1,5 +1,6 @@
 using Ryujinx.Common.Configuration;
 using Ryujinx.Common.Memory;
+using Ryujinx.Common.Utilities;
 using Ryujinx.HLE.HOS.Services.Mii;
 using Ryujinx.HLE.HOS.Services.Mii.Types;
 using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager;
@@ -172,7 +173,7 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
 
             if (File.Exists(filePath))
             {
-                virtualAmiiboFile = JsonSerializer.Deserialize<VirtualAmiiboFile>(File.ReadAllText(filePath));
+                virtualAmiiboFile = JsonHelper.DeserializeFromFile<VirtualAmiiboFile>(filePath);
             }
             else
             {
diff --git a/Ryujinx.HLE/HOS/Services/Spl/IRandomInterface.cs b/Ryujinx.HLE/HOS/Services/Spl/IRandomInterface.cs
index 0f38e685bf..3de746aca3 100644
--- a/Ryujinx.HLE/HOS/Services/Spl/IRandomInterface.cs
+++ b/Ryujinx.HLE/HOS/Services/Spl/IRandomInterface.cs
@@ -1,18 +1,17 @@
-using System;
-using System.Security.Cryptography;
+using System.Security.Cryptography;
 
 namespace Ryujinx.HLE.HOS.Services.Spl
 {
     [Service("csrng")]
     class IRandomInterface : DisposableIpcService
     {
-        private RNGCryptoServiceProvider _rng;
+        private RandomNumberGenerator _rng;
 
         private object _lock = new object();
 
         public IRandomInterface(ServiceCtx context)
         {
-            _rng = new RNGCryptoServiceProvider();
+            _rng = RandomNumberGenerator.Create();
         }
 
         [CommandHipc(0)]
diff --git a/Ryujinx.HLE/HOS/Services/Vi/RootService/IApplicationDisplayService.cs b/Ryujinx.HLE/HOS/Services/Vi/RootService/IApplicationDisplayService.cs
index 267548ddf8..49e6614b29 100644
--- a/Ryujinx.HLE/HOS/Services/Vi/RootService/IApplicationDisplayService.cs
+++ b/Ryujinx.HLE/HOS/Services/Vi/RootService/IApplicationDisplayService.cs
@@ -396,7 +396,7 @@ namespace Ryujinx.HLE.HOS.Services.Vi.RootService
 
             if (!applet.DrawTo(surfaceInfo, context.Memory, layerBuffPosition))
             {
-                Logger.Error?.Print(LogClass.ServiceVi, $"Applet did not draw on indirect layer handle {layerHandle}");
+                Logger.Warning?.Print(LogClass.ServiceVi, $"Applet did not draw on indirect layer handle {layerHandle}");
 
                 return ResultCode.Success;
             }
diff --git a/Ryujinx.HLE/Ryujinx.HLE.csproj b/Ryujinx.HLE/Ryujinx.HLE.csproj
index 76e934bbfe..22555df794 100644
--- a/Ryujinx.HLE/Ryujinx.HLE.csproj
+++ b/Ryujinx.HLE/Ryujinx.HLE.csproj
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
   </PropertyGroup>
 
   <ItemGroup>
@@ -21,6 +21,7 @@
     <PackageReference Include="Concentus" Version="1.1.7" />
     <PackageReference Include="LibHac" Version="0.13.3" />
     <PackageReference Include="MsgPack.Cli" Version="1.0.1" />
+    <PackageReference Include="SixLabors.ImageSharp" Version="1.0.4" />
     <PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta11" />
   </ItemGroup>
 
diff --git a/Ryujinx.HLE/Ui/RenderingSurfaceInfo.cs b/Ryujinx.HLE/Ui/RenderingSurfaceInfo.cs
index 0903ffdd43..0ba116add3 100644
--- a/Ryujinx.HLE/Ui/RenderingSurfaceInfo.cs
+++ b/Ryujinx.HLE/Ui/RenderingSurfaceInfo.cs
@@ -1,11 +1,12 @@
 using Ryujinx.HLE.HOS.Services.SurfaceFlinger;
+using System;
 
 namespace Ryujinx.HLE.Ui
 {
     /// <summary>
     /// Information about the indirect layer that is being drawn to.
     /// </summary>
-    class RenderingSurfaceInfo
+    class RenderingSurfaceInfo : IEquatable<RenderingSurfaceInfo>
     {
         public ColorFormat ColorFormat { get; }
         public uint Width { get; }
diff --git a/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj b/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj
index a40e23cb57..0c92a22737 100644
--- a/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj
+++ b/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
     <RuntimeIdentifiers>win-x64;osx-x64;linux-x64</RuntimeIdentifiers>
     <OutputType>Exe</OutputType>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
diff --git a/Ryujinx.Input.SDL2/Ryujinx.Input.SDL2.csproj b/Ryujinx.Input.SDL2/Ryujinx.Input.SDL2.csproj
index 2d61dfb86d..e93c02ee77 100644
--- a/Ryujinx.Input.SDL2/Ryujinx.Input.SDL2.csproj
+++ b/Ryujinx.Input.SDL2/Ryujinx.Input.SDL2.csproj
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 
diff --git a/Ryujinx.Input/Ryujinx.Input.csproj b/Ryujinx.Input/Ryujinx.Input.csproj
index b2de3ac266..c7c76abc6a 100644
--- a/Ryujinx.Input/Ryujinx.Input.csproj
+++ b/Ryujinx.Input/Ryujinx.Input.csproj
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 
diff --git a/Ryujinx.Memory.Tests/Ryujinx.Memory.Tests.csproj b/Ryujinx.Memory.Tests/Ryujinx.Memory.Tests.csproj
index 38a241b94c..90a7e54a1c 100644
--- a/Ryujinx.Memory.Tests/Ryujinx.Memory.Tests.csproj
+++ b/Ryujinx.Memory.Tests/Ryujinx.Memory.Tests.csproj
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
     <IsPackable>false</IsPackable>
   </PropertyGroup>
 
diff --git a/Ryujinx.Memory/Ryujinx.Memory.csproj b/Ryujinx.Memory/Ryujinx.Memory.csproj
index f6d19b99f5..5ece9322a5 100644
--- a/Ryujinx.Memory/Ryujinx.Memory.csproj
+++ b/Ryujinx.Memory/Ryujinx.Memory.csproj
@@ -1,12 +1,12 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
+    <PackageReference Include="Mono.Posix.NETStandard" Version="5.20.1-preview" />
   </ItemGroup>
 
   <ItemGroup>
diff --git a/Ryujinx.SDL2.Common/Ryujinx.SDL2.Common.csproj b/Ryujinx.SDL2.Common/Ryujinx.SDL2.Common.csproj
index 16eb3b3c04..de9f7cf701 100644
--- a/Ryujinx.SDL2.Common/Ryujinx.SDL2.Common.csproj
+++ b/Ryujinx.SDL2.Common/Ryujinx.SDL2.Common.csproj
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
   </PropertyGroup>
 
   <ItemGroup>
diff --git a/Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj b/Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj
index e11559d5bb..803129c87c 100644
--- a/Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj
+++ b/Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
     <RuntimeIdentifiers>win-x64;osx-x64;linux-x64</RuntimeIdentifiers>
     <OutputType>Exe</OutputType>
     <Configurations>Debug;Release</Configurations>
diff --git a/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj b/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj
index 55cb85cd63..2005e4fb49 100644
--- a/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj
+++ b/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
     <Configurations>Debug;Release</Configurations>
   </PropertyGroup>
diff --git a/Ryujinx.Tests/Ryujinx.Tests.csproj b/Ryujinx.Tests/Ryujinx.Tests.csproj
index 58b11f5708..82047ca016 100644
--- a/Ryujinx.Tests/Ryujinx.Tests.csproj
+++ b/Ryujinx.Tests/Ryujinx.Tests.csproj
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
     <RuntimeIdentifiers>win-x64;osx-x64;linux-x64</RuntimeIdentifiers>
     <OutputType>Exe</OutputType>
     <IsPackable>false</IsPackable>
diff --git a/Ryujinx/Modules/Updater/Updater.cs b/Ryujinx/Modules/Updater/Updater.cs
index 524060d4b0..320928e4c8 100644
--- a/Ryujinx/Modules/Updater/Updater.cs
+++ b/Ryujinx/Modules/Updater/Updater.cs
@@ -4,7 +4,6 @@ using ICSharpCode.SharpZipLib.Tar;
 using ICSharpCode.SharpZipLib.Zip;
 using Mono.Unix;
 using Newtonsoft.Json.Linq;
-using Ryujinx.Common.Configuration;
 using Ryujinx.Common.Logging;
 using Ryujinx.Ui;
 using Ryujinx.Ui.Widgets;
@@ -13,6 +12,7 @@ using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Net;
+using System.Net.Http;
 using System.Net.NetworkInformation;
 using System.Runtime.InteropServices;
 using System.Text;
@@ -92,10 +92,10 @@ namespace Ryujinx.Modules
             // Get latest version number from Appveyor
             try
             {
-                using (WebClient jsonClient = new WebClient())
+                using (HttpClient jsonClient = new HttpClient())
                 {
                     // Fetch latest build information
-                    string  fetchedJson = await jsonClient.DownloadStringTaskAsync($"{AppveyorApiUrl}/projects/gdkchan/ryujinx/branch/master");
+                    string  fetchedJson = await jsonClient.GetStringAsync($"{AppveyorApiUrl}/projects/gdkchan/ryujinx/branch/master");
                     JObject jsonRoot    = JObject.Parse(fetchedJson);
                     JToken  buildToken  = jsonRoot["build"];
 
@@ -149,15 +149,15 @@ namespace Ryujinx.Modules
             }
 
             // Fetch build size information to learn chunk sizes.
-            using (WebClient buildSizeClient = new WebClient()) 
-            { 
+            using (HttpClient buildSizeClient = new HttpClient())
+            {
                 try
                 {
-                    buildSizeClient.Headers.Add("Range", "bytes=0-0");
-                    await buildSizeClient.DownloadDataTaskAsync(new Uri(_buildUrl));
+                    buildSizeClient.DefaultRequestHeaders.Add("Range", "bytes=0-0");
 
-                    string contentRange = buildSizeClient.ResponseHeaders["Content-Range"];
-                    _buildSize = long.Parse(contentRange.Substring(contentRange.IndexOf('/') + 1));
+                    HttpResponseMessage message = await buildSizeClient.GetAsync(new Uri(_buildUrl), HttpCompletionOption.ResponseHeadersRead);
+
+                    _buildSize = message.Content.Headers.ContentRange.Length.Value;
                 }
                 catch (Exception ex)
                 {
@@ -220,7 +220,10 @@ namespace Ryujinx.Modules
 
             for (int i = 0; i < ConnectionCount; i++)
             {
+#pragma warning disable SYSLIB0014
+                // TODO: WebClient is obsolete and need to be replaced with a more complex logic using HttpClient.
                 using (WebClient client = new WebClient())
+#pragma warning restore SYSLIB0014
                 {
                     webClients.Add(client);
 
@@ -307,31 +310,56 @@ namespace Ryujinx.Modules
             }
         }
 
-        private static void DoUpdateWithSingleThread(UpdateDialog updateDialog, string downloadUrl, string updateFile)
+        private static void DoUpdateWithSingleThreadWorker(UpdateDialog updateDialog, string downloadUrl, string updateFile)
         {
-            // Single-Threaded Updater
-            using (WebClient client = new WebClient())
+            using (HttpClient client = new HttpClient())
             {
-                client.DownloadProgressChanged += (_, args) =>
-                {
-                    updateDialog.ProgressBar.Value = args.ProgressPercentage;
-                };
+                // We do not want to timeout while downloading
+                client.Timeout = TimeSpan.FromDays(1);
 
-                client.DownloadDataCompleted += (_, args) =>
+                using (HttpResponseMessage response = client.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead).Result)
+                using (Stream remoteFileStream = response.Content.ReadAsStreamAsync().Result)
                 {
-                    File.WriteAllBytes(updateFile, args.Result);
-                    InstallUpdate(updateDialog, updateFile);
-                };
+                    using (Stream updateFileStream = File.Open(updateFile, FileMode.Create))
+                    {
+                        long totalBytes = response.Content.Headers.ContentLength.Value;
+                        long byteWritten = 0;
 
-                client.DownloadDataAsync(new Uri(downloadUrl));
+                        byte[] buffer = new byte[32 * 1024];
+
+                        while (true)
+                        {
+                            int readSize = remoteFileStream.Read(buffer);
+
+                            if (readSize == 0)
+                            {
+                                break;
+                            }
+
+                            byteWritten += readSize;
+
+                            updateDialog.ProgressBar.Value = ((double)byteWritten / totalBytes) * 100;
+                            updateFileStream.Write(buffer, 0, readSize);
+                        }
+                    }
+                }
+
+                InstallUpdate(updateDialog, updateFile);
             }
         }
-        
+
+        private static void DoUpdateWithSingleThread(UpdateDialog updateDialog, string downloadUrl, string updateFile)
+        {
+            Thread worker = new Thread(() => DoUpdateWithSingleThreadWorker(updateDialog, downloadUrl, updateFile));
+            worker.Name = "Updater.SingleThreadWorker";
+            worker.Start();
+        }
+
         private static void SetUnixPermissions()
         {
-            string ryuBin = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Ryujinx");
+            string ryuBin = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Ryujinx");
 
-            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            if (!OperatingSystem.IsWindows())
             {
                 UnixFileInfo unixFileInfo = new UnixFileInfo(ryuBin);
                 unixFileInfo.FileAccessPermissions |= FileAccessPermissions.UserExecute;
diff --git a/Ryujinx/Program.cs b/Ryujinx/Program.cs
index 1e0fdd3afd..5b67e2b92b 100644
--- a/Ryujinx/Program.cs
+++ b/Ryujinx/Program.cs
@@ -61,6 +61,9 @@ namespace Ryujinx
                 }
             }
 
+            // Enforce loading of Mono.Posix.NETStandard to avoid .NET runtime lazy loading it during an update.
+            Assembly.Load("Mono.Posix.NETStandard");
+
             // Make process DPI aware for proper window sizing on high-res screens.
             ForceDpiAware.Windows();
             WindowScaleFactor = ForceDpiAware.GetWindowScaleFactor();
diff --git a/Ryujinx/Ryujinx.csproj b/Ryujinx/Ryujinx.csproj
index aba9b53c5b..e747dc5bc1 100644
--- a/Ryujinx/Ryujinx.csproj
+++ b/Ryujinx/Ryujinx.csproj
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
     <RuntimeIdentifiers>win-x64;osx-x64;linux-x64</RuntimeIdentifiers>
     <OutputType>Exe</OutputType>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
@@ -24,7 +24,8 @@
     <PackageReference Include="Ryujinx.Audio.OpenAL.Dependencies" Version="1.21.0.1" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'osx-x64'" />
     <PackageReference Include="OpenTK.Graphics" Version="4.5.0" />
     <PackageReference Include="SPB" Version="0.0.3-build15" />
-    <PackageReference Include="SharpZipLib" Version="1.3.0" />
+    <PackageReference Include="SharpZipLib" Version="1.3.3" />
+    <PackageReference Include="Mono.Posix.NETStandard" Version="5.20.1-preview" />
   </ItemGroup>
 
   <ItemGroup>
diff --git a/Ryujinx/Ui/Windows/AmiiboWindow.cs b/Ryujinx/Ui/Windows/AmiiboWindow.cs
index c949d2202a..7b2a61b973 100644
--- a/Ryujinx/Ui/Windows/AmiiboWindow.cs
+++ b/Ryujinx/Ui/Windows/AmiiboWindow.cs
@@ -1,6 +1,7 @@
 using Gtk;
 using Ryujinx.Common;
 using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Utilities;
 using Ryujinx.Ui.Widgets;
 using System;
 using System.Collections.Generic;
@@ -9,7 +10,6 @@ using System.Linq;
 using System.Net.Http;
 using System.Reflection;
 using System.Text;
-using System.Text.Json;
 using System.Text.Json.Serialization;
 using System.Threading.Tasks;
 
@@ -128,7 +128,7 @@ namespace Ryujinx.Ui.Windows
             {
                 amiiboJsonString = File.ReadAllText(_amiiboJsonPath);
 
-                if (await NeedsUpdate(JsonSerializer.Deserialize<AmiiboJson>(amiiboJsonString).LastUpdated))
+                if (await NeedsUpdate(JsonHelper.Deserialize<AmiiboJson>(amiiboJsonString).LastUpdated))
                 {
                     amiiboJsonString = await DownloadAmiiboJson();
                 }
@@ -147,7 +147,7 @@ namespace Ryujinx.Ui.Windows
                 }
             }
 
-            _amiiboList = JsonSerializer.Deserialize<AmiiboJson>(amiiboJsonString).Amiibo;
+            _amiiboList = JsonHelper.Deserialize<AmiiboJson>(amiiboJsonString).Amiibo;
             _amiiboList = _amiiboList.OrderBy(amiibo => amiibo.AmiiboSeries).ToList();
 
             if (LastScannedAmiiboShowAll)
diff --git a/appveyor.yml b/appveyor.yml
index 4ccb0337af..b56a36d01e 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -2,9 +2,9 @@ version: 1.0.{build}
 branches:
   only:
   - master
-image: Visual Studio 2019
+image: Visual Studio 2022
 environment:
-  appveyor_dotnet_runtime: net5.0
+  appveyor_dotnet_runtime: net6.0
   matrix:
     - config: Release
       config_name: '-'
@@ -12,9 +12,9 @@ build_script:
 - ps: >-
     dotnet --version
 
-    dotnet publish -c $env:config -r win-x64 /p:Version=$env:APPVEYOR_BUILD_VERSION /p:DebugType=embedded
+    dotnet publish -c $env:config -r win-x64 /p:Version=$env:APPVEYOR_BUILD_VERSION /p:DebugType=embedded --self-contained
 
-    dotnet publish -c $env:config -r linux-x64 /p:Version=$env:APPVEYOR_BUILD_VERSION /p:DebugType=embedded
+    dotnet publish -c $env:config -r linux-x64 /p:Version=$env:APPVEYOR_BUILD_VERSION /p:DebugType=embedded --self-contained
 
     7z a ryujinx$env:config_name$env:APPVEYOR_BUILD_VERSION-win_x64.zip $env:APPVEYOR_BUILD_FOLDER\Ryujinx\bin\$env:config\$env:appveyor_dotnet_runtime\win-x64\publish\
 
diff --git a/global.json b/global.json
index d129334e9b..d6c2c37f77 100644
--- a/global.json
+++ b/global.json
@@ -1,6 +1,6 @@
 {
   "sdk": {
-    "version": "5.0.100",
-	"rollForward": "latestFeature"
+    "version": "6.0.100",
+    "rollForward": "latestFeature"
   }
 }
\ No newline at end of file