From 595e514f1804ee2da31ff1b24c2facfc120386d1 Mon Sep 17 00:00:00 2001 From: sunshineinabox Date: Sun, 14 Jul 2024 01:16:14 -0700 Subject: [PATCH] Use SkiaSharp for Avalonia in place of ImageSharp (#6269) * Rebased Transformation all at once Use SkiaSharp instead of ImageSharp * Apply suggestions from code review Co-authored-by: Ac_K * Change back unintentionally changed comment --------- Co-authored-by: Ac_K Co-authored-by: Emmanuel Hansen --- src/Ryujinx/AppHost.cs | 53 ++++++++++++------- src/Ryujinx/Ryujinx.csproj | 1 - .../UI/ViewModels/MainWindowViewModel.cs | 15 +++--- .../UserFirmwareAvatarSelectorViewModel.cs | 11 ++-- .../UserFirmwareAvatarSelectorView.axaml.cs | 32 ++++++----- .../UserProfileImageSelectorView.axaml.cs | 18 ++++--- src/Ryujinx/UI/Windows/IconColorPicker.cs | 45 +++++++++------- 7 files changed, 104 insertions(+), 71 deletions(-) diff --git a/src/Ryujinx/AppHost.cs b/src/Ryujinx/AppHost.cs index d405f320..7004908a 100644 --- a/src/Ryujinx/AppHost.cs +++ b/src/Ryujinx/AppHost.cs @@ -40,20 +40,17 @@ using Ryujinx.UI.Common; using Ryujinx.UI.Common.Configuration; using Ryujinx.UI.Common.Helper; using Silk.NET.Vulkan; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats.Png; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; +using SkiaSharp; using SPB.Graphics.Vulkan; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop; using AntiAliasing = Ryujinx.Common.Configuration.AntiAliasing; -using Image = SixLabors.ImageSharp.Image; using InputManager = Ryujinx.Input.HLE.InputManager; using IRenderer = Ryujinx.Graphics.GAL.IRenderer; using Key = Ryujinx.Input.Key; @@ -366,25 +363,33 @@ namespace Ryujinx.Ava return; } - Image image = e.IsBgra ? Image.LoadPixelData(e.Data, e.Width, e.Height) - : Image.LoadPixelData(e.Data, e.Width, e.Height); + var colorType = e.IsBgra ? SKColorType.Bgra8888 : SKColorType.Rgba8888; + using var bitmap = new SKBitmap(new SKImageInfo(e.Width, e.Height, colorType, SKAlphaType.Premul)); - if (e.FlipX) + Marshal.Copy(e.Data, 0, bitmap.GetPixels(), e.Data.Length); + + SKBitmap bitmapToSave = null; + + if (e.FlipX || e.FlipY) { - image.Mutate(x => x.Flip(FlipMode.Horizontal)); + bitmapToSave = new SKBitmap(bitmap.Width, bitmap.Height); + + using var canvas = new SKCanvas(bitmapToSave); + + canvas.Clear(SKColors.Transparent); + + float scaleX = e.FlipX ? -1 : 1; + float scaleY = e.FlipY ? -1 : 1; + + var matrix = SKMatrix.CreateScale(scaleX, scaleY, bitmap.Width / 2f, bitmap.Height / 2f); + + canvas.SetMatrix(matrix); + + canvas.DrawBitmap(bitmap, new SKPoint(e.FlipX ? -bitmap.Width : 0, e.FlipY ? -bitmap.Height : 0)); } - if (e.FlipY) - { - image.Mutate(x => x.Flip(FlipMode.Vertical)); - } - - image.SaveAsPng(path, new PngEncoder - { - ColorType = PngColorType.Rgb, - }); - - image.Dispose(); + SaveBitmapAsPng(bitmapToSave ?? bitmap, path); + bitmapToSave?.Dispose(); Logger.Notice.Print(LogClass.Application, $"Screenshot saved to {path}", "Screenshot"); } @@ -396,6 +401,14 @@ namespace Ryujinx.Ava } } + private void SaveBitmapAsPng(SKBitmap bitmap, string path) + { + using var data = bitmap.Encode(SKEncodedImageFormat.Png, 100); + using var stream = File.OpenWrite(path); + + data.SaveTo(stream); + } + public void Start() { if (OperatingSystem.IsWindows()) diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj index a43f5006..6718b7fc 100644 --- a/src/Ryujinx/Ryujinx.csproj +++ b/src/Ryujinx/Ryujinx.csproj @@ -54,7 +54,6 @@ - diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 549eebf1..b47cc4b7 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -32,7 +32,7 @@ using Ryujinx.UI.App.Common; using Ryujinx.UI.Common; using Ryujinx.UI.Common.Configuration; using Ryujinx.UI.Common.Helper; -using SixLabors.ImageSharp.PixelFormats; +using SkiaSharp; using System; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -40,7 +40,6 @@ using System.Globalization; using System.IO; using System.Threading; using System.Threading.Tasks; -using Image = SixLabors.ImageSharp.Image; using Key = Ryujinx.Input.Key; using MissingKeyException = LibHac.Common.Keys.MissingKeyException; using ShaderCacheLoadingState = Ryujinx.Graphics.Gpu.Shader.ShaderCacheState; @@ -1164,17 +1163,17 @@ namespace Ryujinx.Ava.UI.ViewModels private void PrepareLoadScreen() { using MemoryStream stream = new(SelectedIcon); - using var gameIconBmp = Image.Load(stream); + using var gameIconBmp = SKBitmap.Decode(stream); - var dominantColor = IconColorPicker.GetFilteredColor(gameIconBmp).ToPixel(); + var dominantColor = IconColorPicker.GetFilteredColor(gameIconBmp); const float ColorMultiple = 0.5f; - Color progressFgColor = Color.FromRgb(dominantColor.R, dominantColor.G, dominantColor.B); + Color progressFgColor = Color.FromRgb(dominantColor.Red, dominantColor.Green, dominantColor.Blue); Color progressBgColor = Color.FromRgb( - (byte)(dominantColor.R * ColorMultiple), - (byte)(dominantColor.G * ColorMultiple), - (byte)(dominantColor.B * ColorMultiple)); + (byte)(dominantColor.Red * ColorMultiple), + (byte)(dominantColor.Green * ColorMultiple), + (byte)(dominantColor.Blue * ColorMultiple)); ProgressBarForegroundColor = new SolidColorBrush(progressFgColor); ProgressBarBackgroundColor = new SolidColorBrush(progressBgColor); diff --git a/src/Ryujinx/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs b/src/Ryujinx/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs index 12adfe94..b07bf78b 100644 --- a/src/Ryujinx/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs @@ -9,14 +9,14 @@ using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Ava.UI.Models; using Ryujinx.HLE.FileSystem; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; +using SkiaSharp; using System; using System.Buffers.Binary; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using Color = Avalonia.Media.Color; +using Image = SkiaSharp.SKImage; namespace Ryujinx.Ava.UI.ViewModels { @@ -130,9 +130,12 @@ namespace Ryujinx.Ava.UI.ViewModels stream.Position = 0; - Image avatarImage = Image.LoadPixelData(DecompressYaz0(stream), 256, 256); + Image avatarImage = Image.FromPixelCopy(new SKImageInfo(256, 256, SKColorType.Rgba8888, SKAlphaType.Premul), DecompressYaz0(stream)); - avatarImage.SaveAsPng(streamPng); + using (SKData data = avatarImage.Encode(SKEncodedImageFormat.Png, 100)) + { + data.SaveTo(streamPng); + } _avatarStore.Add(item.FullPath, streamPng.ToArray()); } diff --git a/src/Ryujinx/UI/Views/User/UserFirmwareAvatarSelectorView.axaml.cs b/src/Ryujinx/UI/Views/User/UserFirmwareAvatarSelectorView.axaml.cs index b6376866..064b5e90 100644 --- a/src/Ryujinx/UI/Views/User/UserFirmwareAvatarSelectorView.axaml.cs +++ b/src/Ryujinx/UI/Views/User/UserFirmwareAvatarSelectorView.axaml.cs @@ -6,12 +6,8 @@ using Ryujinx.Ava.UI.Controls; using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.UI.ViewModels; using Ryujinx.HLE.FileSystem; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats.Png; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; +using SkiaSharp; using System.IO; -using Image = SixLabors.ImageSharp.Image; namespace Ryujinx.Ava.UI.Views.User { @@ -70,15 +66,25 @@ namespace Ryujinx.Ava.UI.Views.User { if (ViewModel.SelectedImage != null) { - MemoryStream streamJpg = new(); - Image avatarImage = Image.Load(ViewModel.SelectedImage, new PngDecoder()); + using var streamJpg = new MemoryStream(); + using var bitmap = SKBitmap.Decode(ViewModel.SelectedImage); + using var newBitmap = new SKBitmap(bitmap.Width, bitmap.Height); - avatarImage.Mutate(x => x.BackgroundColor(new Rgba32( - ViewModel.BackgroundColor.R, - ViewModel.BackgroundColor.G, - ViewModel.BackgroundColor.B, - ViewModel.BackgroundColor.A))); - avatarImage.SaveAsJpeg(streamJpg); + using (var canvas = new SKCanvas(newBitmap)) + { + canvas.Clear(new SKColor( + ViewModel.BackgroundColor.R, + ViewModel.BackgroundColor.G, + ViewModel.BackgroundColor.B, + ViewModel.BackgroundColor.A)); + canvas.DrawBitmap(bitmap, 0, 0); + } + + using (var image = SKImage.FromBitmap(newBitmap)) + using (var dataJpeg = image.Encode(SKEncodedImageFormat.Jpeg, 100)) + { + dataJpeg.SaveTo(streamJpg); + } _profile.Image = streamJpg.ToArray(); diff --git a/src/Ryujinx/UI/Views/User/UserProfileImageSelectorView.axaml.cs b/src/Ryujinx/UI/Views/User/UserProfileImageSelectorView.axaml.cs index fabfaa4e..b4f23b5b 100644 --- a/src/Ryujinx/UI/Views/User/UserProfileImageSelectorView.axaml.cs +++ b/src/Ryujinx/UI/Views/User/UserProfileImageSelectorView.axaml.cs @@ -9,11 +9,9 @@ using Ryujinx.Ava.UI.Controls; using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.UI.ViewModels; using Ryujinx.HLE.FileSystem; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Processing; +using SkiaSharp; using System.Collections.Generic; using System.IO; -using Image = SixLabors.ImageSharp.Image; namespace Ryujinx.Ava.UI.Views.User { @@ -102,13 +100,19 @@ namespace Ryujinx.Ava.UI.Views.User private static byte[] ProcessProfileImage(byte[] buffer) { - using Image image = Image.Load(buffer); + using var bitmap = SKBitmap.Decode(buffer); - image.Mutate(x => x.Resize(256, 256)); + var resizedBitmap = bitmap.Resize(new SKImageInfo(256, 256), SKFilterQuality.High); - using MemoryStream streamJpg = new(); + using var streamJpg = new MemoryStream(); - image.SaveAsJpeg(streamJpg); + if (resizedBitmap != null) + { + using var image = SKImage.FromBitmap(resizedBitmap); + using var dataJpeg = image.Encode(SKEncodedImageFormat.Jpeg, 100); + + dataJpeg.SaveTo(streamJpg); + } return streamJpg.ToArray(); } diff --git a/src/Ryujinx/UI/Windows/IconColorPicker.cs b/src/Ryujinx/UI/Windows/IconColorPicker.cs index 72660351..dd6a55d4 100644 --- a/src/Ryujinx/UI/Windows/IconColorPicker.cs +++ b/src/Ryujinx/UI/Windows/IconColorPicker.cs @@ -1,5 +1,4 @@ -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; +using SkiaSharp; using System; using System.Collections.Generic; @@ -36,35 +35,34 @@ namespace Ryujinx.Ava.UI.Windows } } - public static Color GetFilteredColor(Image image) + public static SKColor GetFilteredColor(SKBitmap image) { - var color = GetColor(image).ToPixel(); + var color = GetColor(image); + // We don't want colors that are too dark. // If the color is too dark, make it brighter by reducing the range // and adding a constant color. - int luminosity = GetColorApproximateLuminosity(color.R, color.G, color.B); + int luminosity = GetColorApproximateLuminosity(color.Red, color.Green, color.Blue); if (luminosity < CutOffLuminosity) { - color = Color.FromRgb( - (byte)Math.Min(CutOffLuminosity + color.R, byte.MaxValue), - (byte)Math.Min(CutOffLuminosity + color.G, byte.MaxValue), - (byte)Math.Min(CutOffLuminosity + color.B, byte.MaxValue)); + color = new SKColor( + (byte)Math.Min(CutOffLuminosity + color.Red, byte.MaxValue), + (byte)Math.Min(CutOffLuminosity + color.Green, byte.MaxValue), + (byte)Math.Min(CutOffLuminosity + color.Blue, byte.MaxValue)); } return color; } - public static Color GetColor(Image image) + public static SKColor GetColor(SKBitmap image) { var colors = new PaletteColor[TotalColors]; - var dominantColorBin = new Dictionary(); var buffer = GetBuffer(image); int w = image.Width; - int w8 = w << 8; int h8 = image.Height << 8; @@ -84,9 +82,10 @@ namespace Ryujinx.Ava.UI.Windows { int offset = x + yOffset; - byte cb = buffer[offset].B; - byte cg = buffer[offset].G; - byte cr = buffer[offset].R; + SKColor pixel = buffer[offset]; + byte cr = pixel.Red; + byte cg = pixel.Green; + byte cb = pixel.Blue; var qck = GetQuantizedColorKey(cr, cg, cb); @@ -122,12 +121,22 @@ namespace Ryujinx.Ava.UI.Windows } } - return Color.FromRgb(bestCandidate.R, bestCandidate.G, bestCandidate.B); + return new SKColor(bestCandidate.R, bestCandidate.G, bestCandidate.B); } - public static Bgra32[] GetBuffer(Image image) + public static SKColor[] GetBuffer(SKBitmap image) { - return image.DangerousTryGetSinglePixelMemory(out var data) ? data.ToArray() : Array.Empty(); + var pixels = new SKColor[image.Width * image.Height]; + + for (int y = 0; y < image.Height; y++) + { + for (int x = 0; x < image.Width; x++) + { + pixels[x + y * image.Width] = image.GetPixel(x, y); + } + } + + return pixels; } private static int GetColorScore(Dictionary dominantColorBin, int maxHitCount, PaletteColor color)