From 595e514f1804ee2da31ff1b24c2facfc120386d1 Mon Sep 17 00:00:00 2001
From: sunshineinabox <aqemail@gmail.com>
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 <Acoustik666@gmail.com>

* Change back unintentionally changed comment

---------

Co-authored-by: Ac_K <Acoustik666@gmail.com>
Co-authored-by: Emmanuel Hansen <emmausssss@gmail.com>
---
 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 d405f32050..7004908a7f 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<Bgra32>(e.Data, e.Width, e.Height)
-                                               : Image.LoadPixelData<Rgba32>(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 a43f50063f..6718b7fcc4 100644
--- a/src/Ryujinx/Ryujinx.csproj
+++ b/src/Ryujinx/Ryujinx.csproj
@@ -54,7 +54,6 @@
     <PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" />
     <PackageReference Include="SPB" />
     <PackageReference Include="SharpZipLib" />
-    <PackageReference Include="SixLabors.ImageSharp" />
   </ItemGroup>
 
   <ItemGroup>
diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs
index 549eebf14d..b47cc4b7c4 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<Bgra32>(stream);
+            using var gameIconBmp = SKBitmap.Decode(stream);
 
-            var dominantColor = IconColorPicker.GetFilteredColor(gameIconBmp).ToPixel<Bgra32>();
+            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 12adfe94bb..b07bf78b94 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<Rgba32>(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 b6376866d1..064b5e908b 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 fabfaa4e80..b4f23b5b86 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 72660351a8..dd6a55d4d6 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<Bgra32> image)
+        public static SKColor GetFilteredColor(SKBitmap image)
         {
-            var color = GetColor(image).ToPixel<Bgra32>();
+            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<Bgra32> image)
+        public static SKColor GetColor(SKBitmap image)
         {
             var colors = new PaletteColor[TotalColors];
-
             var dominantColorBin = new Dictionary<int, int>();
 
             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<Bgra32> image)
+        public static SKColor[] GetBuffer(SKBitmap image)
         {
-            return image.DangerousTryGetSinglePixelMemory(out var data) ? data.ToArray() : Array.Empty<Bgra32>();
+            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<int, int> dominantColorBin, int maxHitCount, PaletteColor color)