From 57d3296ba4e5c1fc7ca30376c7ca8eb3041ae2f6 Mon Sep 17 00:00:00 2001
From: Mary <me@thog.eu>
Date: Sun, 28 Nov 2021 21:24:17 +0100
Subject: infra: Migrate to .NET 6 (#2829)

* 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>
---
 .../SoftwareKeyboardRendererBase.cs                | 585 +++++++++++++++++++++
 1 file changed, 585 insertions(+)
 create mode 100644 Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRendererBase.cs

(limited to 'Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRendererBase.cs')

diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRendererBase.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRendererBase.cs
new file mode 100644
index 00000000..b059200d
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRendererBase.cs
@@ -0,0 +1,585 @@
+using Ryujinx.HLE.Ui;
+using Ryujinx.Memory;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Processing;
+using SixLabors.ImageSharp.Drawing.Processing;
+using SixLabors.Fonts;
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Numerics;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
+{
+    /// <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;
+            }
+        }
+    }
+}
-- 
cgit v1.2.3-70-g09d2