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

View file

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

View file

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

View file

@ -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)**

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,11 +1,11 @@
using Ryujinx.Common;
using ICSharpCode.SharpZipLib.Zip;
using Ryujinx.Common;
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.Gpu.Shader.Cache.Definition;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@ -119,7 +119,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
/// <summary>
/// Main storage of the cache collection.
/// </summary>
private ZipArchive _cacheArchive;
private ZipFile _cacheArchive;
/// <summary>
/// Indicates if the cache collection supports modification.
@ -324,7 +324,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
EnsureArchiveUpToDate();
// Open the zip in readonly to avoid anyone modifying/corrupting it during normal operations.
_cacheArchive = ZipFile.Open(GetArchivePath(), ZipArchiveMode.Read);
_cacheArchive = new ZipFile(File.OpenRead(GetArchivePath()));
}
/// <summary>
@ -336,7 +336,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
// First close previous opened instance if found.
if (_cacheArchive != null)
{
_cacheArchive.Dispose();
_cacheArchive.Close();
}
string archivePath = GetArchivePath();
@ -355,8 +355,18 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
return;
}
if (!File.Exists(archivePath))
{
using (ZipFile newZip = ZipFile.Create(archivePath))
{
// Workaround for SharpZipLib issue #395
newZip.BeginUpdate();
newZip.CommitUpdate();
}
}
// Open the zip in read/write.
_cacheArchive = ZipFile.Open(archivePath, ZipArchiveMode.Update);
_cacheArchive = new ZipFile(File.Open(archivePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None));
Logger.Info?.Print(LogClass.Gpu, $"Updating cache collection archive {archivePath}...");
@ -366,7 +376,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
CacheHelper.EnsureArchiveUpToDate(_cacheDirectory, _cacheArchive, _hashTable);
// Close the instance to force a flush.
_cacheArchive.Dispose();
_cacheArchive.Close();
_cacheArchive = null;
string cacheTempDataPath = GetCacheTempDataPath();

View file

@ -1,4 +1,5 @@
using Ryujinx.Common;
using ICSharpCode.SharpZipLib.Zip;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.GAL;
@ -9,7 +10,6 @@ using Ryujinx.Graphics.Shader.Translation;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@ -192,19 +192,19 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
/// <param name="entry">The given hash</param>
/// <returns>The cached file if present or null</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte[] ReadFromArchive(ZipArchive archive, Hash128 entry)
public static byte[] ReadFromArchive(ZipFile archive, Hash128 entry)
{
if (archive != null)
{
ZipArchiveEntry archiveEntry = archive.GetEntry($"{entry}");
ZipEntry archiveEntry = archive.GetEntry($"{entry}");
if (archiveEntry != null)
{
try
{
byte[] result = new byte[archiveEntry.Length];
byte[] result = new byte[archiveEntry.Size];
using (Stream archiveStream = archiveEntry.Open())
using (Stream archiveStream = archive.GetInputStream(archiveEntry))
{
archiveStream.Read(result);
@ -538,8 +538,12 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
/// <param name="archive">The archive to use</param>
/// <param name="entries">The entries in the cache</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void EnsureArchiveUpToDate(string baseCacheDirectory, ZipArchive archive, HashSet<Hash128> entries)
public static void EnsureArchiveUpToDate(string baseCacheDirectory, ZipFile archive, HashSet<Hash128> entries)
{
List<string> filesToDelete = new List<string>();
archive.BeginUpdate();
foreach (Hash128 hash in entries)
{
string cacheTempFilePath = GenCacheTempFilePath(baseCacheDirectory, hash);
@ -548,15 +552,25 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
{
string cacheHash = $"{hash}";
ZipArchiveEntry entry = archive.GetEntry(cacheHash);
ZipEntry entry = archive.GetEntry(cacheHash);
entry?.Delete();
if (entry != null)
{
archive.Delete(entry);
}
archive.CreateEntryFromFile(cacheTempFilePath, cacheHash);
File.Delete(cacheTempFilePath);
// We enforce deflate compression here to avoid possible incompatibilities on older version of Ryujinx that use System.IO.Compression.
archive.Add(new StaticDiskDataSource(cacheTempFilePath), cacheHash, CompressionMethod.Deflated);
filesToDelete.Add(cacheTempFilePath);
}
}
archive.CommitUpdate();
foreach (string filePath in filesToDelete)
{
File.Delete(filePath);
}
}
public static bool IsArchiveReadOnly(string archivePath)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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()

View file

@ -1,717 +1,164 @@
using Ryujinx.HLE.Ui;
using Ryujinx.Memory;
using System;
using System.Buffers.Binary;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Drawing.Text;
using System.IO;
using System.Numerics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
/// <summary>
/// Class that generates the graphics for the software keyboard applet during inline mode.
/// Class that manages the renderer base class and its state in a multithreaded context.
/// </summary>
internal class SoftwareKeyboardRenderer : IDisposable
{
const int TextBoxBlinkThreshold = 8;
const int TextBoxBlinkSleepMilliseconds = 100;
const int TextBoxBlinkJoinWaitMilliseconds = 1000;
private const int TextBoxBlinkSleepMilliseconds = 100;
private const int RendererWaitTimeoutMilliseconds = 100;
const string MessageText = "Please use the keyboard to input text";
const string AcceptText = "Accept";
const string CancelText = "Cancel";
const string ControllerToggleText = "Toggle input";
private readonly object _stateLock = new object();
private RenderingSurfaceInfo _surfaceInfo;
private Bitmap _surface = null;
private object _renderLock = new object();
private SoftwareKeyboardUiState _state = new SoftwareKeyboardUiState();
private SoftwareKeyboardRendererBase _renderer;
private string _inputText = "";
private int _cursorStart = 0;
private int _cursorEnd = 0;
private bool _acceptPressed = false;
private bool _cancelPressed = false;
private bool _overwriteMode = false;
private bool _typingEnabled = true;
private bool _controllerEnabled = true;
private Image _ryujinxLogo = null;
private Image _padAcceptIcon = null;
private Image _padCancelIcon = null;
private Image _keyModeIcon = null;
private float _textBoxOutlineWidth;
private float _padPressedPenWidth;
private Brush _panelBrush;
private Brush _disabledBrush;
private Brush _textNormalBrush;
private Brush _textSelectedBrush;
private Brush _textOverCursorBrush;
private Brush _cursorBrush;
private Brush _selectionBoxBrush;
private Brush _keyCapBrush;
private Brush _keyProgressBrush;
private Pen _gridSeparatorPen;
private Pen _textBoxOutlinePen;
private Pen _cursorPen;
private Pen _selectionBoxPen;
private Pen _padPressedPen;
private int _inputTextFontSize;
private int _padButtonFontSize;
private Font _messageFont;
private Font _inputTextFont;
private Font _labelsTextFont;
private Font _padSymbolFont;
private Font _keyCapFont;
private float _inputTextCalibrationHeight;
private float _panelPositionY;
private RectangleF _panelRectangle;
private PointF _logoPosition;
private float _messagePositionY;
private TRef<int> _textBoxBlinkCounter = new TRef<int>(0);
private TimedAction _textBoxBlinkTimedAction = new TimedAction();
private TimedAction _renderAction = new TimedAction();
public SoftwareKeyboardRenderer(IHostUiTheme uiTheme)
{
_surfaceInfo = new RenderingSurfaceInfo(0, 0, 0, 0, 0);
_renderer = new SoftwareKeyboardRendererBase(uiTheme);
string ryujinxLogoPath = "Ryujinx.Ui.Resources.Logo_Ryujinx.png";
int ryujinxLogoSize = 32;
_ryujinxLogo = LoadResource(Assembly.GetEntryAssembly(), ryujinxLogoPath, ryujinxLogoSize, ryujinxLogoSize);
string padAcceptIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Icon_BtnA.png";
string padCancelIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Icon_BtnB.png";
string keyModeIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Icon_KeyF6.png";
_padAcceptIcon = LoadResource(Assembly.GetExecutingAssembly(), padAcceptIconPath , 0, 0);
_padCancelIcon = LoadResource(Assembly.GetExecutingAssembly(), padCancelIconPath , 0, 0);
_keyModeIcon = LoadResource(Assembly.GetExecutingAssembly(), keyModeIconPath , 0, 0);
Color panelColor = ToColor(uiTheme.DefaultBackgroundColor, 255);
Color panelTransparentColor = ToColor(uiTheme.DefaultBackgroundColor, 150);
Color normalTextColor = ToColor(uiTheme.DefaultForegroundColor);
Color invertedTextColor = ToColor(uiTheme.DefaultForegroundColor, null, true);
Color selectedTextColor = ToColor(uiTheme.SelectionForegroundColor);
Color borderColor = ToColor(uiTheme.DefaultBorderColor);
Color selectionBackgroundColor = ToColor(uiTheme.SelectionBackgroundColor);
Color gridSeparatorColor = Color.FromArgb(180, 255, 255, 255);
float cursorWidth = 2;
_textBoxOutlineWidth = 2;
_padPressedPenWidth = 2;
_panelBrush = new SolidBrush(panelColor);
_disabledBrush = new SolidBrush(panelTransparentColor);
_textNormalBrush = new SolidBrush(normalTextColor);
_textSelectedBrush = new SolidBrush(selectedTextColor);
_textOverCursorBrush = new SolidBrush(invertedTextColor);
_cursorBrush = new SolidBrush(normalTextColor);
_selectionBoxBrush = new SolidBrush(selectionBackgroundColor);
_keyCapBrush = Brushes.White;
_keyProgressBrush = new SolidBrush(borderColor);
_gridSeparatorPen = new Pen(gridSeparatorColor, 2);
_textBoxOutlinePen = new Pen(borderColor, _textBoxOutlineWidth);
_cursorPen = new Pen(normalTextColor, cursorWidth);
_selectionBoxPen = new Pen(selectionBackgroundColor, cursorWidth);
_padPressedPen = new Pen(borderColor, _padPressedPenWidth);
_inputTextFontSize = 20;
_padButtonFontSize = 24;
string font = uiTheme.FontFamily;
_messageFont = new Font(font, 26, FontStyle.Regular, GraphicsUnit.Pixel);
_inputTextFont = new Font(font, _inputTextFontSize, FontStyle.Regular, GraphicsUnit.Pixel);
_labelsTextFont = new Font(font, 24, FontStyle.Regular, GraphicsUnit.Pixel);
_padSymbolFont = new Font(font, _padButtonFontSize, FontStyle.Regular, GraphicsUnit.Pixel);
_keyCapFont = new Font(font, 15, FontStyle.Regular, GraphicsUnit.Pixel);
// System.Drawing has serious problems measuring strings, so it requires a per-pixel calibration
// to ensure we are rendering text inside the proper region
_inputTextCalibrationHeight = CalibrateTextHeight(_inputTextFont);
StartTextBoxBlinker(_textBoxBlinkTimedAction, _textBoxBlinkCounter);
StartTextBoxBlinker(_textBoxBlinkTimedAction, _state, _stateLock);
StartRenderer(_renderAction, _renderer, _state, _stateLock);
}
private static void StartTextBoxBlinker(TimedAction timedAction, TRef<int> blinkerCounter)
private static void StartTextBoxBlinker(TimedAction timedAction, SoftwareKeyboardUiState state, object stateLock)
{
timedAction.Reset(() =>
{
// The blinker is on falf of the time and events such as input
// changes can reset the blinker.
var value = Volatile.Read(ref blinkerCounter.Value);
value = (value + 1) % (2 * TextBoxBlinkThreshold);
Volatile.Write(ref blinkerCounter.Value, value);
lock (stateLock)
{
// The blinker is on half of the time and events such as input
// changes can reset the blinker.
state.TextBoxBlinkCounter = (state.TextBoxBlinkCounter + 1) % (2 * SoftwareKeyboardRendererBase.TextBoxBlinkThreshold);
// Tell the render thread there is something new to render.
Monitor.PulseAll(stateLock);
}
}, TextBoxBlinkSleepMilliseconds);
}
private Color ToColor(ThemeColor color, byte? overrideAlpha = null, bool flipRgb = false)
private static void StartRenderer(TimedAction timedAction, SoftwareKeyboardRendererBase renderer, SoftwareKeyboardUiState state, object stateLock)
{
var a = (byte)(color.A * 255);
var r = (byte)(color.R * 255);
var g = (byte)(color.G * 255);
var b = (byte)(color.B * 255);
SoftwareKeyboardUiState internalState = new SoftwareKeyboardUiState();
if (flipRgb)
bool canCreateSurface = false;
bool needsUpdate = true;
timedAction.Reset(() =>
{
r = (byte)(255 - r);
g = (byte)(255 - g);
b = (byte)(255 - b);
}
lock (stateLock)
{
if (!Monitor.Wait(stateLock, RendererWaitTimeoutMilliseconds))
{
return;
}
return Color.FromArgb(overrideAlpha.GetValueOrDefault(a), r, g, b);
needsUpdate = UpdateStateField(ref state.InputText, ref internalState.InputText);
needsUpdate |= UpdateStateField(ref state.CursorBegin, ref internalState.CursorBegin);
needsUpdate |= UpdateStateField(ref state.CursorEnd, ref internalState.CursorEnd);
needsUpdate |= UpdateStateField(ref state.AcceptPressed, ref internalState.AcceptPressed);
needsUpdate |= UpdateStateField(ref state.CancelPressed, ref internalState.CancelPressed);
needsUpdate |= UpdateStateField(ref state.OverwriteMode, ref internalState.OverwriteMode);
needsUpdate |= UpdateStateField(ref state.TypingEnabled, ref internalState.TypingEnabled);
needsUpdate |= UpdateStateField(ref state.ControllerEnabled, ref internalState.ControllerEnabled);
needsUpdate |= UpdateStateField(ref state.TextBoxBlinkCounter, ref internalState.TextBoxBlinkCounter);
canCreateSurface = state.SurfaceInfo != null && internalState.SurfaceInfo == null;
if (canCreateSurface)
{
internalState.SurfaceInfo = state.SurfaceInfo;
}
}
if (canCreateSurface)
{
renderer.CreateSurface(internalState.SurfaceInfo);
}
if (needsUpdate)
{
renderer.DrawMutableElements(internalState);
renderer.CopyImageToBuffer();
needsUpdate = false;
}
});
}
private Image LoadResource(Assembly assembly, string resourcePath, int newWidth, int newHeight)
private static bool UpdateStateField<T>(ref T source, ref T destination) where T : IEquatable<T>
{
Stream resourceStream = assembly.GetManifestResourceStream(resourcePath);
Debug.Assert(resourceStream != null);
var originalImage = Image.FromStream(resourceStream);
if (newHeight == 0 || newWidth == 0)
if (!source.Equals(destination))
{
return originalImage;
destination = source;
return true;
}
var newSize = new Rectangle(0, 0, newWidth, newHeight);
var newImage = new Bitmap(newWidth, newHeight);
using (var graphics = System.Drawing.Graphics.FromImage(newImage))
using (var wrapMode = new ImageAttributes())
{
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
graphics.CompositingQuality = CompositingQuality.HighQuality;
graphics.CompositingMode = CompositingMode.SourceCopy;
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
graphics.SmoothingMode = SmoothingMode.HighQuality;
wrapMode.SetWrapMode(WrapMode.TileFlipXY);
graphics.DrawImage(originalImage, newSize, 0, 0, originalImage.Width, originalImage.Height, GraphicsUnit.Pixel, wrapMode);
}
return newImage;
return false;
}
#pragma warning disable CS8632
public void UpdateTextState(string? inputText, int? cursorStart, int? cursorEnd, bool? overwriteMode, bool? typingEnabled)
public void UpdateTextState(string? inputText, int? cursorBegin, int? cursorEnd, bool? overwriteMode, bool? typingEnabled)
#pragma warning restore CS8632
{
lock (_renderLock)
lock (_stateLock)
{
// Update the parameters that were provided.
_inputText = inputText != null ? inputText : _inputText;
_cursorStart = cursorStart.GetValueOrDefault(_cursorStart);
_cursorEnd = cursorEnd.GetValueOrDefault(_cursorEnd);
_overwriteMode = overwriteMode.GetValueOrDefault(_overwriteMode);
_typingEnabled = typingEnabled.GetValueOrDefault(_typingEnabled);
_state.InputText = inputText != null ? inputText : _state.InputText;
_state.CursorBegin = cursorBegin.GetValueOrDefault(_state.CursorBegin);
_state.CursorEnd = cursorEnd.GetValueOrDefault(_state.CursorEnd);
_state.OverwriteMode = overwriteMode.GetValueOrDefault(_state.OverwriteMode);
_state.TypingEnabled = typingEnabled.GetValueOrDefault(_state.TypingEnabled);
// Reset the cursor blink.
Volatile.Write(ref _textBoxBlinkCounter.Value, 0);
_state.TextBoxBlinkCounter = 0;
// Tell the render thread there is something new to render.
Monitor.PulseAll(_stateLock);
}
}
public void UpdateCommandState(bool? acceptPressed, bool? cancelPressed, bool? controllerEnabled)
{
lock (_renderLock)
lock (_stateLock)
{
// Update the parameters that were provided.
_acceptPressed = acceptPressed.GetValueOrDefault(_acceptPressed);
_cancelPressed = cancelPressed.GetValueOrDefault(_cancelPressed);
_controllerEnabled = controllerEnabled.GetValueOrDefault(_controllerEnabled);
_state.AcceptPressed = acceptPressed.GetValueOrDefault(_state.AcceptPressed);
_state.CancelPressed = cancelPressed.GetValueOrDefault(_state.CancelPressed);
_state.ControllerEnabled = controllerEnabled.GetValueOrDefault(_state.ControllerEnabled);
// Tell the render thread there is something new to render.
Monitor.PulseAll(_stateLock);
}
}
private void Redraw()
public void SetSurfaceInfo(RenderingSurfaceInfo surfaceInfo)
{
if (_surface == null)
lock (_stateLock)
{
return;
}
_state.SurfaceInfo = surfaceInfo;
using (var graphics = CreateGraphics())
{
var messageRectangle = MeasureString(graphics, MessageText, _messageFont);
float messagePositionX = (_panelRectangle.Width - messageRectangle.Width) / 2 - messageRectangle.X;
float messagePositionY = _messagePositionY - messageRectangle.Y;
PointF messagePosition = new PointF(messagePositionX, messagePositionY);
graphics.Clear(Color.Transparent);
graphics.TranslateTransform(0, _panelPositionY);
graphics.FillRectangle(_panelBrush, _panelRectangle);
graphics.DrawImage(_ryujinxLogo, _logoPosition);
DrawString(graphics, MessageText, _messageFont, _textNormalBrush, messagePosition);
if (!_typingEnabled)
{
// Just draw a semi-transparent rectangle on top to fade the component with the background.
// TODO (caian): This will not work if one decides to add make background semi-transparent as well.
graphics.FillRectangle(_disabledBrush, messagePositionX, messagePositionY, messageRectangle.Width, messageRectangle.Height);
}
DrawTextBox(graphics);
float halfWidth = _panelRectangle.Width / 2;
PointF acceptButtonPosition = new PointF(halfWidth - 180, 185);
PointF cancelButtonPosition = new PointF(halfWidth , 185);
PointF disableButtonPosition = new PointF(halfWidth + 180, 185);
DrawPadButton (graphics, acceptButtonPosition , _padAcceptIcon, AcceptText, _acceptPressed, _controllerEnabled);
DrawPadButton (graphics, cancelButtonPosition , _padCancelIcon, CancelText, _cancelPressed, _controllerEnabled);
DrawControllerToggle(graphics, disableButtonPosition, _controllerEnabled);
// Tell the render thread there is something new to render.
Monitor.PulseAll(_stateLock);
}
}
private void RecreateSurface()
internal bool DrawTo(IVirtualMemoryManager destination, ulong position)
{
Debug.Assert(_surfaceInfo.ColorFormat == Services.SurfaceFlinger.ColorFormat.A8B8G8R8);
// Use the whole area of the image to draw, even the alignment, otherwise it may shear the final
// image if the pitch is different.
uint totalWidth = _surfaceInfo.Pitch / 4;
uint totalHeight = _surfaceInfo.Size / _surfaceInfo.Pitch;
Debug.Assert(_surfaceInfo.Width <= totalWidth);
Debug.Assert(_surfaceInfo.Height <= totalHeight);
Debug.Assert(_surfaceInfo.Pitch * _surfaceInfo.Height <= _surfaceInfo.Size);
_surface = new Bitmap((int)totalWidth, (int)totalHeight, PixelFormat.Format32bppArgb);
}
private void RecomputeConstants()
{
float totalWidth = _surfaceInfo.Width;
float totalHeight = _surfaceInfo.Height;
float panelHeight = 240;
_panelPositionY = totalHeight - panelHeight;
_panelRectangle = new RectangleF(0, 0, totalWidth, panelHeight);
_messagePositionY = 60;
float logoPositionX = (totalWidth - _ryujinxLogo.Width) / 2;
float logoPositionY = 18;
_logoPosition = new PointF(logoPositionX, logoPositionY);
}
private StringFormat CreateStringFormat(string text)
{
StringFormat format = new StringFormat(StringFormat.GenericTypographic);
format.FormatFlags |= StringFormatFlags.MeasureTrailingSpaces;
format.SetMeasurableCharacterRanges(new CharacterRange[] { new CharacterRange(0, text.Length) });
return format;
}
private RectangleF MeasureString(System.Drawing.Graphics graphics, string text, System.Drawing.Font font)
{
bool isEmpty = false;
if (string.IsNullOrEmpty(text))
{
isEmpty = true;
text = " ";
}
var format = CreateStringFormat(text);
var rectangle = new RectangleF(0, 0, float.PositiveInfinity, float.PositiveInfinity);
var regions = graphics.MeasureCharacterRanges(text, font, rectangle, format);
Debug.Assert(regions.Length == 1);
rectangle = regions[0].GetBounds(graphics);
if (isEmpty)
{
rectangle.Width = 0;
}
else
{
rectangle.Width += 1.0f;
}
return rectangle;
}
private float CalibrateTextHeight(Font font)
{
// This is a pixel-wise calibration that tests the offset of a reference character because Windows text measurement
// is horrible when compared to other frameworks like Cairo and diverge across systems and fonts.
Debug.Assert(font.Unit == GraphicsUnit.Pixel);
var surfaceSize = (int)Math.Ceiling(2 * font.Size);
string calibrationText = "|";
using (var surface = new Bitmap(surfaceSize, surfaceSize, PixelFormat.Format32bppArgb))
using (var graphics = CreateGraphics(surface))
{
var measuredRectangle = MeasureString(graphics, calibrationText, font);
Debug.Assert(measuredRectangle.Right <= surfaceSize);
Debug.Assert(measuredRectangle.Bottom <= surfaceSize);
var textPosition = new PointF(0, 0);
graphics.Clear(Color.Transparent);
DrawString(graphics, calibrationText, font, Brushes.White, textPosition);
var lockRectangle = new Rectangle(0, 0, surface.Width, surface.Height);
var surfaceData = surface.LockBits(lockRectangle, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
var surfaceBytes = new byte[surfaceData.Stride * surfaceData.Height];
Marshal.Copy(surfaceData.Scan0, surfaceBytes, 0, surfaceBytes.Length);
Point topLeft = new Point();
Point bottomLeft = new Point();
bool foundTopLeft = false;
for (int y = 0; y < surfaceData.Height; y++)
{
for (int x = 0; x < surfaceData.Stride; x += 4)
{
int position = y * surfaceData.Stride + x;
if (surfaceBytes[position] != 0)
{
if (!foundTopLeft)
{
topLeft.X = x;
topLeft.Y = y;
foundTopLeft = true;
break;
}
else
{
bottomLeft.X = x;
bottomLeft.Y = y;
break;
}
}
}
}
return bottomLeft.Y - topLeft.Y;
}
}
private void DrawString(System.Drawing.Graphics graphics, string text, Font font, Brush brush, PointF point)
{
var format = CreateStringFormat(text);
graphics.DrawString(text, font, brush, point, format);
}
private System.Drawing.Graphics CreateGraphics()
{
return CreateGraphics(_surface);
}
private System.Drawing.Graphics CreateGraphics(Image surface)
{
var graphics = System.Drawing.Graphics.FromImage(surface);
graphics.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
graphics.InterpolationMode = InterpolationMode.NearestNeighbor;
graphics.CompositingQuality = CompositingQuality.HighSpeed;
graphics.CompositingMode = CompositingMode.SourceOver;
graphics.PixelOffsetMode = PixelOffsetMode.HighSpeed;
graphics.SmoothingMode = SmoothingMode.HighSpeed;
return graphics;
}
private void DrawTextBox(System.Drawing.Graphics graphics)
{
var inputTextRectangle = MeasureString(graphics, _inputText, _inputTextFont);
float boxWidth = (int)(Math.Max(300, inputTextRectangle.Width + inputTextRectangle.X + 8));
float boxHeight = 32;
float boxY = 110;
float boxX = (int)((_panelRectangle.Width - boxWidth) / 2);
graphics.DrawRectangle(_textBoxOutlinePen, boxX, boxY, boxWidth, boxHeight);
float inputTextX = (_panelRectangle.Width - inputTextRectangle.Width) / 2 - inputTextRectangle.X;
float inputTextY = boxY + boxHeight - inputTextRectangle.Bottom - 5;
var inputTextPosition = new PointF(inputTextX, inputTextY);
DrawString(graphics, _inputText, _inputTextFont, _textNormalBrush, inputTextPosition);
// Draw the cursor on top of the text and redraw the text with a different color if necessary.
Brush cursorTextBrush;
Brush cursorBrush;
Pen cursorPen;
float cursorPositionYBottom = inputTextY + inputTextRectangle.Bottom;
float cursorPositionYTop = cursorPositionYBottom - _inputTextCalibrationHeight - 2;
float cursorPositionXLeft;
float cursorPositionXRight;
bool cursorVisible = false;
if (_cursorStart != _cursorEnd)
{
cursorTextBrush = _textSelectedBrush;
cursorBrush = _selectionBoxBrush;
cursorPen = _selectionBoxPen;
string textUntilBegin = _inputText.Substring(0, _cursorStart);
string textUntilEnd = _inputText.Substring(0, _cursorEnd);
RectangleF selectionBeginRectangle = MeasureString(graphics, textUntilBegin, _inputTextFont);
RectangleF selectionEndRectangle = MeasureString(graphics, textUntilEnd , _inputTextFont);
cursorVisible = true;
cursorPositionXLeft = inputTextX + selectionBeginRectangle.Width + selectionBeginRectangle.X;
cursorPositionXRight = inputTextX + selectionEndRectangle.Width + selectionEndRectangle.X;
}
else
{
cursorTextBrush = _textOverCursorBrush;
cursorBrush = _cursorBrush;
cursorPen = _cursorPen;
if (Volatile.Read(ref _textBoxBlinkCounter.Value) < TextBoxBlinkThreshold)
{
// Show the blinking cursor.
int cursorStart = Math.Min(_inputText.Length, _cursorStart);
string textUntilCursor = _inputText.Substring(0, cursorStart);
RectangleF cursorTextRectangle = MeasureString(graphics, textUntilCursor, _inputTextFont);
cursorVisible = true;
cursorPositionXLeft = inputTextX + cursorTextRectangle.Width + cursorTextRectangle.X;
if (_overwriteMode)
{
// The blinking cursor is in overwrite mode so it takes the size of a character.
if (_cursorStart < _inputText.Length)
{
textUntilCursor = _inputText.Substring(0, cursorStart + 1);
cursorTextRectangle = MeasureString(graphics, textUntilCursor, _inputTextFont);
cursorPositionXRight = inputTextX + cursorTextRectangle.Width + cursorTextRectangle.X;
}
else
{
cursorPositionXRight = cursorPositionXLeft + _inputTextFontSize / 2;
}
}
else
{
// The blinking cursor is in insert mode so it is only a line.
cursorPositionXRight = cursorPositionXLeft;
}
}
else
{
cursorPositionXLeft = inputTextX;
cursorPositionXRight = inputTextX;
}
}
if (_typingEnabled && cursorVisible)
{
float cursorWidth = cursorPositionXRight - cursorPositionXLeft;
float cursorHeight = cursorPositionYBottom - cursorPositionYTop;
if (cursorWidth == 0)
{
graphics.DrawLine(cursorPen, cursorPositionXLeft, cursorPositionYTop, cursorPositionXLeft, cursorPositionYBottom);
}
else
{
graphics.DrawRectangle(cursorPen, cursorPositionXLeft, cursorPositionYTop, cursorWidth, cursorHeight);
graphics.FillRectangle(cursorBrush, cursorPositionXLeft, cursorPositionYTop, cursorWidth, cursorHeight);
var cursorRectangle = new RectangleF(cursorPositionXLeft, cursorPositionYTop, cursorWidth, cursorHeight);
var oldClip = graphics.Clip;
graphics.Clip = new Region(cursorRectangle);
DrawString(graphics, _inputText, _inputTextFont, cursorTextBrush, inputTextPosition);
graphics.Clip = oldClip;
}
}
else if (!_typingEnabled)
{
// Just draw a semi-transparent rectangle on top to fade the component with the background.
// TODO (caian): This will not work if one decides to add make background semi-transparent as well.
graphics.FillRectangle(_disabledBrush, boxX - _textBoxOutlineWidth, boxY - _textBoxOutlineWidth,
boxWidth + 2* _textBoxOutlineWidth, boxHeight + 2* _textBoxOutlineWidth);
}
}
private void DrawPadButton(System.Drawing.Graphics graphics, PointF point, Image icon, string label, bool pressed, bool enabled)
{
// Use relative positions so we can center the the entire drawing later.
float iconX = 0;
float iconY = 0;
float iconWidth = icon.Width;
float iconHeight = icon.Height;
var labelRectangle = MeasureString(graphics, label, _labelsTextFont);
float labelPositionX = iconWidth + 8 - labelRectangle.X;
float labelPositionY = (iconHeight - labelRectangle.Height) / 2 - labelRectangle.Y - 1;
float fullWidth = labelPositionX + labelRectangle.Width + labelRectangle.X;
float fullHeight = iconHeight;
// Convert all relative positions into absolute.
float originX = (int)(point.X - fullWidth / 2);
float originY = (int)(point.Y - fullHeight / 2);
iconX += originX;
iconY += originY;
var labelPosition = new PointF(labelPositionX + originX, labelPositionY + originY);
graphics.DrawImageUnscaled(icon, (int)iconX, (int)iconY);
DrawString(graphics, label, _labelsTextFont, _textNormalBrush, labelPosition);
GraphicsPath frame = new GraphicsPath();
frame.AddRectangle(new RectangleF(originX - 2 * _padPressedPenWidth, originY - 2 * _padPressedPenWidth,
fullWidth + 4 * _padPressedPenWidth, fullHeight + 4 * _padPressedPenWidth));
if (enabled)
{
if (pressed)
{
graphics.DrawPath(_padPressedPen, frame);
}
}
else
{
// Just draw a semi-transparent rectangle on top to fade the component with the background.
// TODO (caian): This will not work if one decides to add make background semi-transparent as well.
graphics.FillPath(_disabledBrush, frame);
}
}
private void DrawControllerToggle(System.Drawing.Graphics graphics, PointF point, bool enabled)
{
var labelRectangle = MeasureString(graphics, ControllerToggleText, _labelsTextFont);
// Use relative positions so we can center the the entire drawing later.
float keyWidth = _keyModeIcon.Width;
float keyHeight = _keyModeIcon.Height;
float labelPositionX = keyWidth + 8 - labelRectangle.X;
float labelPositionY = -labelRectangle.Y - 1;
float keyX = 0;
float keyY = (int)((labelPositionY + labelRectangle.Height - keyHeight) / 2);
float fullWidth = labelPositionX + labelRectangle.Width;
float fullHeight = Math.Max(labelPositionY + labelRectangle.Height, keyHeight);
// Convert all relative positions into absolute.
float originX = (int)(point.X - fullWidth / 2);
float originY = (int)(point.Y - fullHeight / 2);
keyX += originX;
keyY += originY;
var labelPosition = new PointF(labelPositionX + originX, labelPositionY + originY);
var overlayPosition = new Point((int)keyX, (int)keyY);
graphics.DrawImageUnscaled(_keyModeIcon, overlayPosition);
DrawString(graphics, ControllerToggleText, _labelsTextFont, _textNormalBrush, labelPosition);
}
private bool TryCopyTo(IVirtualMemoryManager destination, ulong position)
{
if (_surface == null)
{
return false;
}
Rectangle lockRectangle = new Rectangle(0, 0, _surface.Width, _surface.Height);
BitmapData surfaceData = _surface.LockBits(lockRectangle, ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
Debug.Assert(surfaceData.Stride == _surfaceInfo.Pitch);
Debug.Assert(surfaceData.Stride * surfaceData.Height == _surfaceInfo.Size);
// Convert the pixel format used in System.Drawing to the one required by a Switch Surface.
int dataLength = surfaceData.Stride * surfaceData.Height;
byte[] data = new byte[dataLength];
Span<uint> dataConvert = MemoryMarshal.Cast<byte, uint>(data);
Marshal.Copy(surfaceData.Scan0, data, 0, dataLength);
for (int i = 0; i < dataConvert.Length; i++)
{
dataConvert[i] = BitOperations.RotateRight(BinaryPrimitives.ReverseEndianness(dataConvert[i]), 8);
}
try
{
destination.Write(position, data);
}
finally
{
_surface.UnlockBits(surfaceData);
}
return true;
}
internal bool DrawTo(RenderingSurfaceInfo surfaceInfo, IVirtualMemoryManager destination, ulong position)
{
lock (_renderLock)
{
if (!_surfaceInfo.Equals(surfaceInfo))
{
_surfaceInfo = surfaceInfo;
RecreateSurface();
RecomputeConstants();
}
Redraw();
return TryCopyTo(destination, position);
}
return _renderer.WriteBufferToMemory(destination, position);
}
public void Dispose()
{
_textBoxBlinkTimedAction.RequestCancel();
_renderAction.RequestCancel();
}
}
}

View file

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

View file

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

View file

@ -144,6 +144,20 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
}), cancelled);
}
public void Reset(Action action)
{
// Create a dedicated cancel token for each task.
var cancelled = new TRef<bool>(false);
Reset(new Thread(() =>
{
while (!Volatile.Read(ref cancelled.Value))
{
action();
}
}), cancelled);
}
private static bool SleepWithSubstep(SleepSubstepData substepData, TRef<bool> cancelled)
{
for (int i = 0; i < substepData.SleepCount; i++)

View file

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

View file

@ -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)]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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();

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<RuntimeIdentifiers>win-x64;osx-x64;linux-x64</RuntimeIdentifiers>
<OutputType>Exe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
@ -24,7 +24,8 @@
<PackageReference Include="Ryujinx.Audio.OpenAL.Dependencies" Version="1.21.0.1" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'osx-x64'" />
<PackageReference Include="OpenTK.Graphics" Version="4.5.0" />
<PackageReference Include="SPB" Version="0.0.3-build15" />
<PackageReference Include="SharpZipLib" Version="1.3.0" />
<PackageReference Include="SharpZipLib" Version="1.3.3" />
<PackageReference Include="Mono.Posix.NETStandard" Version="5.20.1-preview" />
</ItemGroup>
<ItemGroup>

View file

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

View file

@ -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\

View file

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