1
1
mirror of https://github.com/ryujinx-mirror/ryujinx.git synced 2025-03-13 10:49:37 -05:00

infra: Migrate to .NET 6 ()

* infra: Migrate to .NET 6

* Rollback version naming change

* Workaround .NET 6 ZipArchive API issues

* ci: Switch to VS 2022 for AppVeyor

CI is now ready for .NET 6

* Suppress WebClient warning in DoUpdateWithMultipleThreads

* Attempt to workaround System.Drawing.Common changes on 6.0.0

* Change keyboard rendering from System.Drawing to ImageSharp

* Make the software keyboard renderer multithreaded

* Bump ImageSharp version to 1.0.4 to fix a bug in Image.Load

* Add fallback fonts to the keyboard renderer

* Fix warnings

* Address caian's comment

* Clean up linux workaround as it's uneeded now

* Update readme

Co-authored-by: Caian Benedicto <caianbene@gmail.com>
This commit is contained in:
Mary 2021-11-28 21:24:17 +01:00 committed by GitHub
parent 7b040e51b0
commit 57d3296ba4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 917 additions and 766 deletions
.github/workflows
ARMeilleure
README.md
Ryujinx.Audio.Backends.OpenAL
Ryujinx.Audio.Backends.SDL2
Ryujinx.Audio.Backends.SoundIo
Ryujinx.Audio.Backends
Ryujinx.Audio
Ryujinx.Common
Ryujinx.Cpu
Ryujinx.Graphics.Device
Ryujinx.Graphics.GAL
Ryujinx.Graphics.Gpu
Ryujinx.Graphics.Host1x
Ryujinx.Graphics.Nvdec.FFmpeg
Ryujinx.Graphics.Nvdec.Vp9
Ryujinx.Graphics.Nvdec
Ryujinx.Graphics.OpenGL
Ryujinx.Graphics.Shader
Ryujinx.Graphics.Texture
Ryujinx.Graphics.Vic
Ryujinx.Graphics.Video
Ryujinx.HLE
Ryujinx.Headless.SDL2
Ryujinx.Input.SDL2
Ryujinx.Input
Ryujinx.Memory.Tests
Ryujinx.Memory
Ryujinx.SDL2.Common
Ryujinx.ShaderTools
Ryujinx.Tests.Unicorn
Ryujinx.Tests
Ryujinx
appveyor.ymlglobal.json

@ -50,7 +50,7 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-dotnet@v1 - uses: actions/setup-dotnet@v1
with: with:
dotnet-version: 5.0.x dotnet-version: 6.0.x
- name: Ensure NuGet Source - name: Ensure NuGet Source
uses: fabriciomurta/ensure-nuget-source@v1 uses: fabriciomurta/ensure-nuget-source@v1
- name: Get git short hash - name: Get git short hash
@ -63,10 +63,10 @@ jobs:
- name: Test - name: Test
run: dotnet test -c "${{ matrix.configuration }}" run: dotnet test -c "${{ matrix.configuration }}"
- name: Publish Ryujinx - 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' if: github.event_name == 'pull_request'
- name: Publish Ryujinx.Headless.SDL2 - 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' if: github.event_name == 'pull_request'
- name: Upload Ryujinx artifact - name: Upload Ryujinx artifact
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2

@ -1,12 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" /> <PackageReference Include="Mono.Posix.NETStandard" Version="5.20.1-preview" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

@ -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: 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):** **Step two (choose one):**
**(Variant one)** **(Variant one)**

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>

@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MsgPack.Cli" Version="1.0.1" /> <PackageReference Include="MsgPack.Cli" Version="1.0.1" />
<PackageReference Include="System.Drawing.Common" Version="5.0.1" /> <PackageReference Include="System.Drawing.Common" Version="6.0.0" />
<PackageReference Include="System.Management" Version="5.0.0" /> <PackageReference Include="System.Management" Version="6.0.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

@ -2,6 +2,7 @@
using System; using System;
using System.Drawing; using System.Drawing;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Versioning;
namespace Ryujinx.Common.System namespace Ryujinx.Common.System
{ {
@ -19,7 +20,7 @@ namespace Ryujinx.Common.System
public static void Windows() public static void Windows()
{ {
// Make process DPI aware for proper window sizing on high-res screens. // 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(); SetProcessDPIAware();
} }
@ -27,16 +28,22 @@ namespace Ryujinx.Common.System
public static double GetWindowScaleFactor() public static double GetWindowScaleFactor()
{ {
double userDpiScale; double userDpiScale = 96.0;
try try
{ {
userDpiScale = Graphics.FromHwnd(IntPtr.Zero).DpiX; if (OperatingSystem.IsWindows())
{
userDpiScale = Graphics.FromHwnd(IntPtr.Zero).DpiX;
}
else
{
// TODO: Linux support
}
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Warning?.Print(LogClass.Application, $"Couldn't determine monitor DPI: {e.Message}"); Logger.Warning?.Print(LogClass.Application, $"Couldn't determine monitor DPI: {e.Message}");
userDpiScale = 96.0;
} }
return Math.Min(userDpiScale / _standardDpiScale, _maxScaleFactor); return Math.Min(userDpiScale / _standardDpiScale, _maxScaleFactor);

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>
@ -14,4 +14,8 @@
<ProjectReference Include="..\Ryujinx.Graphics.Shader\Ryujinx.Graphics.Shader.csproj" /> <ProjectReference Include="..\Ryujinx.Graphics.Shader\Ryujinx.Graphics.Shader.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="SharpZipLib" Version="1.3.3" />
</ItemGroup>
</Project> </Project>

@ -1,11 +1,11 @@
using Ryujinx.Common; using ICSharpCode.SharpZipLib.Zip;
using Ryujinx.Common;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Graphics.Gpu.Shader.Cache.Definition; using Ryujinx.Graphics.Gpu.Shader.Cache.Definition;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.IO.Compression;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@ -119,7 +119,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
/// <summary> /// <summary>
/// Main storage of the cache collection. /// Main storage of the cache collection.
/// </summary> /// </summary>
private ZipArchive _cacheArchive; private ZipFile _cacheArchive;
/// <summary> /// <summary>
/// Indicates if the cache collection supports modification. /// Indicates if the cache collection supports modification.
@ -324,7 +324,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
EnsureArchiveUpToDate(); EnsureArchiveUpToDate();
// Open the zip in readonly to avoid anyone modifying/corrupting it during normal operations. // 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> /// <summary>
@ -336,7 +336,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
// First close previous opened instance if found. // First close previous opened instance if found.
if (_cacheArchive != null) if (_cacheArchive != null)
{ {
_cacheArchive.Dispose(); _cacheArchive.Close();
} }
string archivePath = GetArchivePath(); string archivePath = GetArchivePath();
@ -355,8 +355,18 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
return; 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. // 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}..."); 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); CacheHelper.EnsureArchiveUpToDate(_cacheDirectory, _cacheArchive, _hashTable);
// Close the instance to force a flush. // Close the instance to force a flush.
_cacheArchive.Dispose(); _cacheArchive.Close();
_cacheArchive = null; _cacheArchive = null;
string cacheTempDataPath = GetCacheTempDataPath(); string cacheTempDataPath = GetCacheTempDataPath();

@ -1,4 +1,5 @@
using Ryujinx.Common; using ICSharpCode.SharpZipLib.Zip;
using Ryujinx.Common;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.GAL;
@ -9,7 +10,6 @@ using Ryujinx.Graphics.Shader.Translation;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.IO.Compression;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@ -192,19 +192,19 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
/// <param name="entry">The given hash</param> /// <param name="entry">The given hash</param>
/// <returns>The cached file if present or null</returns> /// <returns>The cached file if present or null</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte[] ReadFromArchive(ZipArchive archive, Hash128 entry) public static byte[] ReadFromArchive(ZipFile archive, Hash128 entry)
{ {
if (archive != null) if (archive != null)
{ {
ZipArchiveEntry archiveEntry = archive.GetEntry($"{entry}"); ZipEntry archiveEntry = archive.GetEntry($"{entry}");
if (archiveEntry != null) if (archiveEntry != null)
{ {
try 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); archiveStream.Read(result);
@ -538,8 +538,12 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
/// <param name="archive">The archive to use</param> /// <param name="archive">The archive to use</param>
/// <param name="entries">The entries in the cache</param> /// <param name="entries">The entries in the cache</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [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) foreach (Hash128 hash in entries)
{ {
string cacheTempFilePath = GenCacheTempFilePath(baseCacheDirectory, hash); string cacheTempFilePath = GenCacheTempFilePath(baseCacheDirectory, hash);
@ -548,15 +552,25 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
{ {
string cacheHash = $"{hash}"; 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); // 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);
File.Delete(cacheTempFilePath); filesToDelete.Add(cacheTempFilePath);
} }
} }
archive.CommitUpdate();
foreach (string filePath in filesToDelete)
{
File.Delete(filePath);
}
} }
public static bool IsArchiveReadOnly(string archivePath) public static bool IsArchiveReadOnly(string archivePath)

@ -1,11 +1,11 @@
using Ryujinx.Common; using ICSharpCode.SharpZipLib.Zip;
using Ryujinx.Common;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Gpu.Shader.Cache.Definition; using Ryujinx.Graphics.Gpu.Shader.Cache.Definition;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.IO.Compression;
namespace Ryujinx.Graphics.Gpu.Shader.Cache namespace Ryujinx.Graphics.Gpu.Shader.Cache
{ {
@ -35,27 +35,36 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
return false; 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> /// <summary>
/// Move a file with the name of a given hash to another in the cache archive. /// Move a file with the name of a given hash to another in the cache archive.
/// </summary> /// </summary>
/// <param name="archive">The archive in use</param> /// <param name="archive">The archive in use</param>
/// <param name="oldKey">The old key</param> /// <param name="oldKey">The old key</param>
/// <param name="newKey">The new 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) if (oldGuestEntry != null)
{ {
ZipArchiveEntry newGuestEntry = archive.CreateEntry($"{newKey}"); archive.Add(new StreamZipEntryDataSource(archive, oldGuestEntry), $"{newKey}", CompressionMethod.Deflated);
archive.Delete(oldGuestEntry);
using (Stream oldStream = oldGuestEntry.Open())
using (Stream newStream = newGuestEntry.Open())
{
oldStream.CopyTo(newStream);
}
oldGuestEntry.Delete();
} }
} }
@ -81,8 +90,8 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
string guestArchivePath = CacheHelper.GetArchivePath(guestBaseCacheDirectory); string guestArchivePath = CacheHelper.GetArchivePath(guestBaseCacheDirectory);
string hostArchivePath = CacheHelper.GetArchivePath(hostBaseCacheDirectory); string hostArchivePath = CacheHelper.GetArchivePath(hostBaseCacheDirectory);
ZipArchive guestArchive = ZipFile.Open(guestArchivePath, ZipArchiveMode.Update); ZipFile guestArchive = new ZipFile(File.Open(guestArchivePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None));
ZipArchive hostArchive = ZipFile.Open(hostArchivePath, ZipArchiveMode.Update); ZipFile hostArchive = new ZipFile(File.Open(hostArchivePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None));
CacheHelper.EnsureArchiveUpToDate(guestBaseCacheDirectory, guestArchive, guestEntries); CacheHelper.EnsureArchiveUpToDate(guestBaseCacheDirectory, guestArchive, guestEntries);
CacheHelper.EnsureArchiveUpToDate(hostBaseCacheDirectory, hostArchive, hostEntries); CacheHelper.EnsureArchiveUpToDate(hostBaseCacheDirectory, hostArchive, hostEntries);
@ -129,8 +138,11 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
File.WriteAllBytes(guestManifestPath, newGuestManifestContent); File.WriteAllBytes(guestManifestPath, newGuestManifestContent);
File.WriteAllBytes(hostManifestPath, newHostManifestContent); File.WriteAllBytes(hostManifestPath, newHostManifestContent);
guestArchive.Dispose(); guestArchive.CommitUpdate();
hostArchive.Dispose(); hostArchive.CommitUpdate();
guestArchive.Close();
hostArchive.Close();
} }
} }

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

@ -170,7 +170,9 @@ namespace Ryujinx.HLE.HOS.Applets
{ {
_npads?.Update(); _npads?.Update();
return _keyboardRenderer?.DrawTo(surfaceInfo, destination, position) ?? false; _keyboardRenderer?.SetSurfaceInfo(surfaceInfo);
return _keyboardRenderer?.DrawTo(destination, position) ?? false;
} }
private void ExecuteForegroundKeyboard() private void ExecuteForegroundKeyboard()

@ -1,717 +1,164 @@
using Ryujinx.HLE.Ui; using Ryujinx.HLE.Ui;
using Ryujinx.Memory; using Ryujinx.Memory;
using System; 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; using System.Threading;
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{ {
/// <summary> /// <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> /// </summary>
internal class SoftwareKeyboardRenderer : IDisposable internal class SoftwareKeyboardRenderer : IDisposable
{ {
const int TextBoxBlinkThreshold = 8; private const int TextBoxBlinkSleepMilliseconds = 100;
const int TextBoxBlinkSleepMilliseconds = 100; private const int RendererWaitTimeoutMilliseconds = 100;
const int TextBoxBlinkJoinWaitMilliseconds = 1000;
const string MessageText = "Please use the keyboard to input text"; private readonly object _stateLock = new object();
const string AcceptText = "Accept";
const string CancelText = "Cancel";
const string ControllerToggleText = "Toggle input";
private RenderingSurfaceInfo _surfaceInfo; private SoftwareKeyboardUiState _state = new SoftwareKeyboardUiState();
private Bitmap _surface = null; private SoftwareKeyboardRendererBase _renderer;
private object _renderLock = new object();
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 _textBoxBlinkTimedAction = new TimedAction();
private TimedAction _renderAction = new TimedAction();
public SoftwareKeyboardRenderer(IHostUiTheme uiTheme) public SoftwareKeyboardRenderer(IHostUiTheme uiTheme)
{ {
_surfaceInfo = new RenderingSurfaceInfo(0, 0, 0, 0, 0); _renderer = new SoftwareKeyboardRendererBase(uiTheme);
string ryujinxLogoPath = "Ryujinx.Ui.Resources.Logo_Ryujinx.png"; StartTextBoxBlinker(_textBoxBlinkTimedAction, _state, _stateLock);
int ryujinxLogoSize = 32; StartRenderer(_renderAction, _renderer, _state, _stateLock);
_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);
} }
private static void StartTextBoxBlinker(TimedAction timedAction, TRef<int> blinkerCounter) private static void StartTextBoxBlinker(TimedAction timedAction, SoftwareKeyboardUiState state, object stateLock)
{ {
timedAction.Reset(() => timedAction.Reset(() =>
{ {
// The blinker is on falf of the time and events such as input lock (stateLock)
// changes can reset the blinker. {
var value = Volatile.Read(ref blinkerCounter.Value); // The blinker is on half of the time and events such as input
value = (value + 1) % (2 * TextBoxBlinkThreshold); // changes can reset the blinker.
Volatile.Write(ref blinkerCounter.Value, value); state.TextBoxBlinkCounter = (state.TextBoxBlinkCounter + 1) % (2 * SoftwareKeyboardRendererBase.TextBoxBlinkThreshold);
// Tell the render thread there is something new to render.
Monitor.PulseAll(stateLock);
}
}, TextBoxBlinkSleepMilliseconds); }, 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); SoftwareKeyboardUiState internalState = new SoftwareKeyboardUiState();
var r = (byte)(color.R * 255);
var g = (byte)(color.G * 255);
var b = (byte)(color.B * 255);
if (flipRgb) bool canCreateSurface = false;
bool needsUpdate = true;
timedAction.Reset(() =>
{ {
r = (byte)(255 - r); lock (stateLock)
g = (byte)(255 - g); {
b = (byte)(255 - b); 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); if (!source.Equals(destination))
Debug.Assert(resourceStream != null);
var originalImage = Image.FromStream(resourceStream);
if (newHeight == 0 || newWidth == 0)
{ {
return originalImage; destination = source;
return true;
} }
var newSize = new Rectangle(0, 0, newWidth, newHeight); return false;
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;
} }
#pragma warning disable CS8632 #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 #pragma warning restore CS8632
{ {
lock (_renderLock) lock (_stateLock)
{ {
// Update the parameters that were provided. // Update the parameters that were provided.
_inputText = inputText != null ? inputText : _inputText; _state.InputText = inputText != null ? inputText : _state.InputText;
_cursorStart = cursorStart.GetValueOrDefault(_cursorStart); _state.CursorBegin = cursorBegin.GetValueOrDefault(_state.CursorBegin);
_cursorEnd = cursorEnd.GetValueOrDefault(_cursorEnd); _state.CursorEnd = cursorEnd.GetValueOrDefault(_state.CursorEnd);
_overwriteMode = overwriteMode.GetValueOrDefault(_overwriteMode); _state.OverwriteMode = overwriteMode.GetValueOrDefault(_state.OverwriteMode);
_typingEnabled = typingEnabled.GetValueOrDefault(_typingEnabled); _state.TypingEnabled = typingEnabled.GetValueOrDefault(_state.TypingEnabled);
// Reset the cursor blink. // 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) public void UpdateCommandState(bool? acceptPressed, bool? cancelPressed, bool? controllerEnabled)
{ {
lock (_renderLock) lock (_stateLock)
{ {
// Update the parameters that were provided. // Update the parameters that were provided.
_acceptPressed = acceptPressed.GetValueOrDefault(_acceptPressed); _state.AcceptPressed = acceptPressed.GetValueOrDefault(_state.AcceptPressed);
_cancelPressed = cancelPressed.GetValueOrDefault(_cancelPressed); _state.CancelPressed = cancelPressed.GetValueOrDefault(_state.CancelPressed);
_controllerEnabled = controllerEnabled.GetValueOrDefault(_controllerEnabled); _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()) // Tell the render thread there is something new to render.
{ Monitor.PulseAll(_stateLock);
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);
} }
} }
private void RecreateSurface() internal bool DrawTo(IVirtualMemoryManager destination, ulong position)
{ {
Debug.Assert(_surfaceInfo.ColorFormat == Services.SurfaceFlinger.ColorFormat.A8B8G8R8); return _renderer.WriteBufferToMemory(destination, position);
// 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);
}
} }
public void Dispose() public void Dispose()
{ {
_textBoxBlinkTimedAction.RequestCancel(); _textBoxBlinkTimedAction.RequestCancel();
_renderAction.RequestCancel();
} }
} }
} }

@ -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;
}
}
}
}

@ -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;
}
}

@ -144,6 +144,20 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
}), cancelled); }), 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) private static bool SleepWithSubstep(SleepSubstepData substepData, TRef<bool> cancelled)
{ {
for (int i = 0; i < substepData.SleepCount; i++) for (int i = 0; i < substepData.SleepCount; i++)

@ -1,5 +1,6 @@
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Common.Memory; using Ryujinx.Common.Memory;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.HOS.Services.Mii; using Ryujinx.HLE.HOS.Services.Mii;
using Ryujinx.HLE.HOS.Services.Mii.Types; using Ryujinx.HLE.HOS.Services.Mii.Types;
using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager; using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager;
@ -172,7 +173,7 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
if (File.Exists(filePath)) if (File.Exists(filePath))
{ {
virtualAmiiboFile = JsonSerializer.Deserialize<VirtualAmiiboFile>(File.ReadAllText(filePath)); virtualAmiiboFile = JsonHelper.DeserializeFromFile<VirtualAmiiboFile>(filePath);
} }
else else
{ {

@ -1,18 +1,17 @@
using System; using System.Security.Cryptography;
using System.Security.Cryptography;
namespace Ryujinx.HLE.HOS.Services.Spl namespace Ryujinx.HLE.HOS.Services.Spl
{ {
[Service("csrng")] [Service("csrng")]
class IRandomInterface : DisposableIpcService class IRandomInterface : DisposableIpcService
{ {
private RNGCryptoServiceProvider _rng; private RandomNumberGenerator _rng;
private object _lock = new object(); private object _lock = new object();
public IRandomInterface(ServiceCtx context) public IRandomInterface(ServiceCtx context)
{ {
_rng = new RNGCryptoServiceProvider(); _rng = RandomNumberGenerator.Create();
} }
[CommandHipc(0)] [CommandHipc(0)]

@ -396,7 +396,7 @@ namespace Ryujinx.HLE.HOS.Services.Vi.RootService
if (!applet.DrawTo(surfaceInfo, context.Memory, layerBuffPosition)) 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; return ResultCode.Success;
} }

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -21,6 +21,7 @@
<PackageReference Include="Concentus" Version="1.1.7" /> <PackageReference Include="Concentus" Version="1.1.7" />
<PackageReference Include="LibHac" Version="0.13.3" /> <PackageReference Include="LibHac" Version="0.13.3" />
<PackageReference Include="MsgPack.Cli" Version="1.0.1" /> <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" /> <PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta11" />
</ItemGroup> </ItemGroup>

@ -1,11 +1,12 @@
using Ryujinx.HLE.HOS.Services.SurfaceFlinger; using Ryujinx.HLE.HOS.Services.SurfaceFlinger;
using System;
namespace Ryujinx.HLE.Ui namespace Ryujinx.HLE.Ui
{ {
/// <summary> /// <summary>
/// Information about the indirect layer that is being drawn to. /// Information about the indirect layer that is being drawn to.
/// </summary> /// </summary>
class RenderingSurfaceInfo class RenderingSurfaceInfo : IEquatable<RenderingSurfaceInfo>
{ {
public ColorFormat ColorFormat { get; } public ColorFormat ColorFormat { get; }
public uint Width { get; } public uint Width { get; }

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<RuntimeIdentifiers>win-x64;osx-x64;linux-x64</RuntimeIdentifiers> <RuntimeIdentifiers>win-x64;osx-x64;linux-x64</RuntimeIdentifiers>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
</PropertyGroup> </PropertyGroup>

@ -1,12 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" /> <PackageReference Include="Mono.Posix.NETStandard" Version="5.20.1-preview" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<RuntimeIdentifiers>win-x64;osx-x64;linux-x64</RuntimeIdentifiers> <RuntimeIdentifiers>win-x64;osx-x64;linux-x64</RuntimeIdentifiers>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<Configurations>Debug;Release</Configurations> <Configurations>Debug;Release</Configurations>

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Configurations>Debug;Release</Configurations> <Configurations>Debug;Release</Configurations>
</PropertyGroup> </PropertyGroup>

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<RuntimeIdentifiers>win-x64;osx-x64;linux-x64</RuntimeIdentifiers> <RuntimeIdentifiers>win-x64;osx-x64;linux-x64</RuntimeIdentifiers>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>

@ -4,7 +4,6 @@ using ICSharpCode.SharpZipLib.Tar;
using ICSharpCode.SharpZipLib.Zip; using ICSharpCode.SharpZipLib.Zip;
using Mono.Unix; using Mono.Unix;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Ui; using Ryujinx.Ui;
using Ryujinx.Ui.Widgets; using Ryujinx.Ui.Widgets;
@ -13,6 +12,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http;
using System.Net.NetworkInformation; using System.Net.NetworkInformation;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
@ -92,10 +92,10 @@ namespace Ryujinx.Modules
// Get latest version number from Appveyor // Get latest version number from Appveyor
try try
{ {
using (WebClient jsonClient = new WebClient()) using (HttpClient jsonClient = new HttpClient())
{ {
// Fetch latest build information // 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); JObject jsonRoot = JObject.Parse(fetchedJson);
JToken buildToken = jsonRoot["build"]; JToken buildToken = jsonRoot["build"];
@ -149,15 +149,15 @@ namespace Ryujinx.Modules
} }
// Fetch build size information to learn chunk sizes. // Fetch build size information to learn chunk sizes.
using (WebClient buildSizeClient = new WebClient()) using (HttpClient buildSizeClient = new HttpClient())
{ {
try try
{ {
buildSizeClient.Headers.Add("Range", "bytes=0-0"); buildSizeClient.DefaultRequestHeaders.Add("Range", "bytes=0-0");
await buildSizeClient.DownloadDataTaskAsync(new Uri(_buildUrl));
string contentRange = buildSizeClient.ResponseHeaders["Content-Range"]; HttpResponseMessage message = await buildSizeClient.GetAsync(new Uri(_buildUrl), HttpCompletionOption.ResponseHeadersRead);
_buildSize = long.Parse(contentRange.Substring(contentRange.IndexOf('/') + 1));
_buildSize = message.Content.Headers.ContentRange.Length.Value;
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -220,7 +220,10 @@ namespace Ryujinx.Modules
for (int i = 0; i < ConnectionCount; i++) 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()) using (WebClient client = new WebClient())
#pragma warning restore SYSLIB0014
{ {
webClients.Add(client); webClients.Add(client);
@ -307,31 +310,56 @@ namespace Ryujinx.Modules
} }
} }
private static void DoUpdateWithSingleThreadWorker(UpdateDialog updateDialog, string downloadUrl, string updateFile)
{
using (HttpClient client = new HttpClient())
{
// We do not want to timeout while downloading
client.Timeout = TimeSpan.FromDays(1);
using (HttpResponseMessage response = client.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead).Result)
using (Stream remoteFileStream = response.Content.ReadAsStreamAsync().Result)
{
using (Stream updateFileStream = File.Open(updateFile, FileMode.Create))
{
long totalBytes = response.Content.Headers.ContentLength.Value;
long byteWritten = 0;
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) private static void DoUpdateWithSingleThread(UpdateDialog updateDialog, string downloadUrl, string updateFile)
{ {
// Single-Threaded Updater Thread worker = new Thread(() => DoUpdateWithSingleThreadWorker(updateDialog, downloadUrl, updateFile));
using (WebClient client = new WebClient()) worker.Name = "Updater.SingleThreadWorker";
{ worker.Start();
client.DownloadProgressChanged += (_, args) =>
{
updateDialog.ProgressBar.Value = args.ProgressPercentage;
};
client.DownloadDataCompleted += (_, args) =>
{
File.WriteAllBytes(updateFile, args.Result);
InstallUpdate(updateDialog, updateFile);
};
client.DownloadDataAsync(new Uri(downloadUrl));
}
} }
private static void SetUnixPermissions() 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 unixFileInfo = new UnixFileInfo(ryuBin);
unixFileInfo.FileAccessPermissions |= FileAccessPermissions.UserExecute; unixFileInfo.FileAccessPermissions |= FileAccessPermissions.UserExecute;

@ -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. // Make process DPI aware for proper window sizing on high-res screens.
ForceDpiAware.Windows(); ForceDpiAware.Windows();
WindowScaleFactor = ForceDpiAware.GetWindowScaleFactor(); WindowScaleFactor = ForceDpiAware.GetWindowScaleFactor();

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<RuntimeIdentifiers>win-x64;osx-x64;linux-x64</RuntimeIdentifiers> <RuntimeIdentifiers>win-x64;osx-x64;linux-x64</RuntimeIdentifiers>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <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="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="OpenTK.Graphics" Version="4.5.0" />
<PackageReference Include="SPB" Version="0.0.3-build15" /> <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>
<ItemGroup> <ItemGroup>

@ -1,6 +1,7 @@
using Gtk; using Gtk;
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Common.Utilities;
using Ryujinx.Ui.Widgets; using Ryujinx.Ui.Widgets;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -9,7 +10,6 @@ using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -128,7 +128,7 @@ namespace Ryujinx.Ui.Windows
{ {
amiiboJsonString = File.ReadAllText(_amiiboJsonPath); amiiboJsonString = File.ReadAllText(_amiiboJsonPath);
if (await NeedsUpdate(JsonSerializer.Deserialize<AmiiboJson>(amiiboJsonString).LastUpdated)) if (await NeedsUpdate(JsonHelper.Deserialize<AmiiboJson>(amiiboJsonString).LastUpdated))
{ {
amiiboJsonString = await DownloadAmiiboJson(); 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(); _amiiboList = _amiiboList.OrderBy(amiibo => amiibo.AmiiboSeries).ToList();
if (LastScannedAmiiboShowAll) if (LastScannedAmiiboShowAll)

@ -2,9 +2,9 @@ version: 1.0.{build}
branches: branches:
only: only:
- master - master
image: Visual Studio 2019 image: Visual Studio 2022
environment: environment:
appveyor_dotnet_runtime: net5.0 appveyor_dotnet_runtime: net6.0
matrix: matrix:
- config: Release - config: Release
config_name: '-' config_name: '-'
@ -12,9 +12,9 @@ build_script:
- ps: >- - ps: >-
dotnet --version 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\ 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\

@ -1,6 +1,6 @@
{ {
"sdk": { "sdk": {
"version": "5.0.100", "version": "6.0.100",
"rollForward": "latestFeature" "rollForward": "latestFeature"
} }
} }