diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fcb8c2af..ca1e0f1d 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 ebc4433a..1fd95ad0 100644 --- a/ARMeilleure/ARMeilleure.csproj +++ b/ARMeilleure/ARMeilleure.csproj @@ -1,12 +1,12 @@ - net5.0 + net6.0 true - + diff --git a/README.md b/README.md index fc1c7917..121c537e 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 4619d73d..fd648715 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 @@  - net5.0 + net6.0 diff --git a/Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj b/Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj index 6619a500..fa70d341 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 @@ - net5.0 + net6.0 true diff --git a/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj b/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj index a9a2fe75..49d142aa 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 @@ - net5.0 + net6.0 true diff --git a/Ryujinx.Audio.Backends/Ryujinx.Audio.Backends.csproj b/Ryujinx.Audio.Backends/Ryujinx.Audio.Backends.csproj index 431187ed..83088f27 100644 --- a/Ryujinx.Audio.Backends/Ryujinx.Audio.Backends.csproj +++ b/Ryujinx.Audio.Backends/Ryujinx.Audio.Backends.csproj @@ -1,7 +1,7 @@  - net5.0 + net6.0 true diff --git a/Ryujinx.Audio/Ryujinx.Audio.csproj b/Ryujinx.Audio/Ryujinx.Audio.csproj index ccdeae3e..2499bb44 100644 --- a/Ryujinx.Audio/Ryujinx.Audio.csproj +++ b/Ryujinx.Audio/Ryujinx.Audio.csproj @@ -1,7 +1,7 @@  - net5.0 + net6.0 true diff --git a/Ryujinx.Common/Ryujinx.Common.csproj b/Ryujinx.Common/Ryujinx.Common.csproj index a7e9c66c..e0cc2d56 100644 --- a/Ryujinx.Common/Ryujinx.Common.csproj +++ b/Ryujinx.Common/Ryujinx.Common.csproj @@ -1,14 +1,14 @@ - net5.0 + net6.0 true - - + + diff --git a/Ryujinx.Common/System/ForceDpiAware.cs b/Ryujinx.Common/System/ForceDpiAware.cs index 81c69376..dc513307 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 ef33dd18..84972af1 100644 --- a/Ryujinx.Cpu/Ryujinx.Cpu.csproj +++ b/Ryujinx.Cpu/Ryujinx.Cpu.csproj @@ -1,7 +1,7 @@  - net5.0 + net6.0 true diff --git a/Ryujinx.Graphics.Device/Ryujinx.Graphics.Device.csproj b/Ryujinx.Graphics.Device/Ryujinx.Graphics.Device.csproj index 2f002aa3..fff78129 100644 --- a/Ryujinx.Graphics.Device/Ryujinx.Graphics.Device.csproj +++ b/Ryujinx.Graphics.Device/Ryujinx.Graphics.Device.csproj @@ -1,7 +1,7 @@  - net5.0 + net6.0 diff --git a/Ryujinx.Graphics.GAL/Ryujinx.Graphics.GAL.csproj b/Ryujinx.Graphics.GAL/Ryujinx.Graphics.GAL.csproj index e8b3f52d..725f48ea 100644 --- a/Ryujinx.Graphics.GAL/Ryujinx.Graphics.GAL.csproj +++ b/Ryujinx.Graphics.GAL/Ryujinx.Graphics.GAL.csproj @@ -1,7 +1,7 @@  - net5.0 + net6.0 diff --git a/Ryujinx.Graphics.Gpu/Ryujinx.Graphics.Gpu.csproj b/Ryujinx.Graphics.Gpu/Ryujinx.Graphics.Gpu.csproj index 01e8e235..e3645668 100644 --- a/Ryujinx.Graphics.Gpu/Ryujinx.Graphics.Gpu.csproj +++ b/Ryujinx.Graphics.Gpu/Ryujinx.Graphics.Gpu.csproj @@ -1,7 +1,7 @@  - net5.0 + net6.0 true @@ -14,4 +14,8 @@ + + + + diff --git a/Ryujinx.Graphics.Gpu/Shader/Cache/CacheCollection.cs b/Ryujinx.Graphics.Gpu/Shader/Cache/CacheCollection.cs index 316e027f..a98531f6 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 /// /// Main storage of the cache collection. /// - private ZipArchive _cacheArchive; + private ZipFile _cacheArchive; /// /// 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())); } /// @@ -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 09107346..ee4e1265 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 /// The given hash /// The cached file if present or null [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 /// The archive to use /// The entries in the cache [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void EnsureArchiveUpToDate(string baseCacheDirectory, ZipArchive archive, HashSet entries) + public static void EnsureArchiveUpToDate(string baseCacheDirectory, ZipFile archive, HashSet entries) { + List filesToDelete = new List(); + + 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 e726bc2c..5b4a1713 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); + } + } + /// /// Move a file with the name of a given hash to another in the cache archive. /// /// The archive in use /// The old key /// The new key - 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 69b6103f..49ed1a5c 100644 --- a/Ryujinx.Graphics.Host1x/Ryujinx.Graphics.Host1x.csproj +++ b/Ryujinx.Graphics.Host1x/Ryujinx.Graphics.Host1x.csproj @@ -1,7 +1,7 @@ - net5.0 + net6.0 diff --git a/Ryujinx.Graphics.Nvdec.FFmpeg/Ryujinx.Graphics.Nvdec.FFmpeg.csproj b/Ryujinx.Graphics.Nvdec.FFmpeg/Ryujinx.Graphics.Nvdec.FFmpeg.csproj index b437f36e..4e90eb20 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 @@ - net5.0 + net6.0 true diff --git a/Ryujinx.Graphics.Nvdec.Vp9/Ryujinx.Graphics.Nvdec.Vp9.csproj b/Ryujinx.Graphics.Nvdec.Vp9/Ryujinx.Graphics.Nvdec.Vp9.csproj index 51e88025..b30e6aea 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 @@  - net5.0 + net6.0 true diff --git a/Ryujinx.Graphics.Nvdec/Ryujinx.Graphics.Nvdec.csproj b/Ryujinx.Graphics.Nvdec/Ryujinx.Graphics.Nvdec.csproj index 095e0e59..68c0c2af 100644 --- a/Ryujinx.Graphics.Nvdec/Ryujinx.Graphics.Nvdec.csproj +++ b/Ryujinx.Graphics.Nvdec/Ryujinx.Graphics.Nvdec.csproj @@ -1,7 +1,7 @@  - net5.0 + net6.0 true diff --git a/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj b/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj index aff59d97..3f4bcdee 100644 --- a/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj +++ b/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj @@ -1,7 +1,7 @@ - net5.0 + net6.0 true diff --git a/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj b/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj index d59efc2e..c604902f 100644 --- a/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj +++ b/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj @@ -1,7 +1,7 @@ - net5.0 + net6.0 diff --git a/Ryujinx.Graphics.Texture/Ryujinx.Graphics.Texture.csproj b/Ryujinx.Graphics.Texture/Ryujinx.Graphics.Texture.csproj index b74938c0..6af7e775 100644 --- a/Ryujinx.Graphics.Texture/Ryujinx.Graphics.Texture.csproj +++ b/Ryujinx.Graphics.Texture/Ryujinx.Graphics.Texture.csproj @@ -1,6 +1,6 @@ - net5.0 + net6.0 true diff --git a/Ryujinx.Graphics.Vic/Ryujinx.Graphics.Vic.csproj b/Ryujinx.Graphics.Vic/Ryujinx.Graphics.Vic.csproj index fe9b834b..0e564d02 100644 --- a/Ryujinx.Graphics.Vic/Ryujinx.Graphics.Vic.csproj +++ b/Ryujinx.Graphics.Vic/Ryujinx.Graphics.Vic.csproj @@ -1,7 +1,7 @@ - net5.0 + net6.0 true diff --git a/Ryujinx.Graphics.Video/Ryujinx.Graphics.Video.csproj b/Ryujinx.Graphics.Video/Ryujinx.Graphics.Video.csproj index a7f8f746..484b7177 100644 --- a/Ryujinx.Graphics.Video/Ryujinx.Graphics.Video.csproj +++ b/Ryujinx.Graphics.Video/Ryujinx.Graphics.Video.csproj @@ -1,7 +1,7 @@ - net5.0 + net6.0 diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs index 523fa5de..3cfd192c 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 dfd10925..c30ad11b 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 { /// - /// 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. /// 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 _textBoxBlinkCounter = new TRef(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 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(ref T source, ref T destination) where T : IEquatable { - 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 dataConvert = MemoryMarshal.Cast(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 00000000..b059200d --- /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 +{ + /// + /// Base class that generates the graphics for the software keyboard applet during inline mode. + /// + 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 _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((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 textOverCursor = new Image((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 pixels)) + { + return; + } + + _bufferData = MemoryMarshal.AsBytes(pixels).ToArray(); + Span dataConvert = MemoryMarshal.Cast(_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 00000000..e6131e62 --- /dev/null +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUiState.cs @@ -0,0 +1,22 @@ +using Ryujinx.HLE.Ui; + +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// + /// TODO + /// + 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 8884bdcf..0de78a0e 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(false); + + Reset(new Thread(() => + { + while (!Volatile.Read(ref cancelled.Value)) + { + action(); + } + }), cancelled); + } + private static bool SleepWithSubstep(SleepSubstepData substepData, TRef 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 9510ef4c..3eb88950 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(File.ReadAllText(filePath)); + virtualAmiiboFile = JsonHelper.DeserializeFromFile(filePath); } else { diff --git a/Ryujinx.HLE/HOS/Services/Spl/IRandomInterface.cs b/Ryujinx.HLE/HOS/Services/Spl/IRandomInterface.cs index 0f38e685..3de746ac 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 267548dd..49e6614b 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 76e934bb..22555df7 100644 --- a/Ryujinx.HLE/Ryujinx.HLE.csproj +++ b/Ryujinx.HLE/Ryujinx.HLE.csproj @@ -1,7 +1,7 @@  - net5.0 + net6.0 @@ -21,6 +21,7 @@ + diff --git a/Ryujinx.HLE/Ui/RenderingSurfaceInfo.cs b/Ryujinx.HLE/Ui/RenderingSurfaceInfo.cs index 0903ffdd..0ba116ad 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 { /// /// Information about the indirect layer that is being drawn to. /// - class RenderingSurfaceInfo + class RenderingSurfaceInfo : IEquatable { 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 a40e23cb..0c92a227 100644 --- a/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj +++ b/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj @@ -1,7 +1,7 @@ - net5.0 + net6.0 win-x64;osx-x64;linux-x64 Exe true diff --git a/Ryujinx.Input.SDL2/Ryujinx.Input.SDL2.csproj b/Ryujinx.Input.SDL2/Ryujinx.Input.SDL2.csproj index 2d61dfb8..e93c02ee 100644 --- a/Ryujinx.Input.SDL2/Ryujinx.Input.SDL2.csproj +++ b/Ryujinx.Input.SDL2/Ryujinx.Input.SDL2.csproj @@ -1,7 +1,7 @@  - net5.0 + net6.0 true diff --git a/Ryujinx.Input/Ryujinx.Input.csproj b/Ryujinx.Input/Ryujinx.Input.csproj index b2de3ac2..c7c76abc 100644 --- a/Ryujinx.Input/Ryujinx.Input.csproj +++ b/Ryujinx.Input/Ryujinx.Input.csproj @@ -1,7 +1,7 @@ - net5.0 + net6.0 true diff --git a/Ryujinx.Memory.Tests/Ryujinx.Memory.Tests.csproj b/Ryujinx.Memory.Tests/Ryujinx.Memory.Tests.csproj index 38a241b9..90a7e54a 100644 --- a/Ryujinx.Memory.Tests/Ryujinx.Memory.Tests.csproj +++ b/Ryujinx.Memory.Tests/Ryujinx.Memory.Tests.csproj @@ -1,7 +1,7 @@ - net5.0 + net6.0 false diff --git a/Ryujinx.Memory/Ryujinx.Memory.csproj b/Ryujinx.Memory/Ryujinx.Memory.csproj index f6d19b99..5ece9322 100644 --- a/Ryujinx.Memory/Ryujinx.Memory.csproj +++ b/Ryujinx.Memory/Ryujinx.Memory.csproj @@ -1,12 +1,12 @@  - net5.0 + net6.0 true - + diff --git a/Ryujinx.SDL2.Common/Ryujinx.SDL2.Common.csproj b/Ryujinx.SDL2.Common/Ryujinx.SDL2.Common.csproj index 16eb3b3c..de9f7cf7 100644 --- a/Ryujinx.SDL2.Common/Ryujinx.SDL2.Common.csproj +++ b/Ryujinx.SDL2.Common/Ryujinx.SDL2.Common.csproj @@ -1,7 +1,7 @@ - net5.0 + net6.0 diff --git a/Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj b/Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj index e11559d5..803129c8 100644 --- a/Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj +++ b/Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj @@ -1,7 +1,7 @@ - net5.0 + net6.0 win-x64;osx-x64;linux-x64 Exe Debug;Release diff --git a/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj b/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj index 55cb85cd..2005e4fb 100644 --- a/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj +++ b/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj @@ -1,7 +1,7 @@ - net5.0 + net6.0 true Debug;Release diff --git a/Ryujinx.Tests/Ryujinx.Tests.csproj b/Ryujinx.Tests/Ryujinx.Tests.csproj index 58b11f57..82047ca0 100644 --- a/Ryujinx.Tests/Ryujinx.Tests.csproj +++ b/Ryujinx.Tests/Ryujinx.Tests.csproj @@ -1,7 +1,7 @@ - net5.0 + net6.0 win-x64;osx-x64;linux-x64 Exe false diff --git a/Ryujinx/Modules/Updater/Updater.cs b/Ryujinx/Modules/Updater/Updater.cs index 524060d4..320928e4 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 1e0fdd3a..5b67e2b9 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 aba9b53c..e747dc5b 100644 --- a/Ryujinx/Ryujinx.csproj +++ b/Ryujinx/Ryujinx.csproj @@ -1,7 +1,7 @@  - net5.0 + net6.0 win-x64;osx-x64;linux-x64 Exe true @@ -24,7 +24,8 @@ - + + diff --git a/Ryujinx/Ui/Windows/AmiiboWindow.cs b/Ryujinx/Ui/Windows/AmiiboWindow.cs index c949d220..7b2a61b9 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(amiiboJsonString).LastUpdated)) + if (await NeedsUpdate(JsonHelper.Deserialize(amiiboJsonString).LastUpdated)) { amiiboJsonString = await DownloadAmiiboJson(); } @@ -147,7 +147,7 @@ namespace Ryujinx.Ui.Windows } } - _amiiboList = JsonSerializer.Deserialize(amiiboJsonString).Amiibo; + _amiiboList = JsonHelper.Deserialize(amiiboJsonString).Amiibo; _amiiboList = _amiiboList.OrderBy(amiibo => amiibo.AmiiboSeries).ToList(); if (LastScannedAmiiboShowAll) diff --git a/appveyor.yml b/appveyor.yml index 4ccb0337..b56a36d0 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 d129334e..d6c2c37f 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