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>
---
 .../SoftwareKeyboard/SoftwareKeyboardRenderer.cs   | 729 +++------------------
 1 file changed, 88 insertions(+), 641 deletions(-)

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

diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRenderer.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRenderer.cs
index dfd10925..c30ad11b 100644
--- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRenderer.cs
+++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRenderer.cs
@@ -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);
-
-            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);
+            _renderer = new SoftwareKeyboardRendererBase(uiTheme);
 
-            _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);
-
-            }, TextBoxBlinkSleepMilliseconds);
-        }
-
-        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.FromArgb(overrideAlpha.GetValueOrDefault(a), r, g, b);
-        }
-
-        private Image LoadResource(Assembly assembly, string resourcePath, int newWidth, int newHeight)
-        {
-            Stream resourceStream = assembly.GetManifestResourceStream(resourcePath);
-
-            Debug.Assert(resourceStream != null);
-
-            var originalImage = Image.FromStream(resourceStream);
-
-            if (newHeight == 0 || newWidth == 0)
-            {
-                return originalImage;
-            }
-
-            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;
-        }
-
-#pragma warning disable CS8632
-        public void UpdateTextState(string? inputText, int? cursorStart, int? cursorEnd, bool? overwriteMode, bool? typingEnabled)
-#pragma warning restore CS8632
-        {
-            lock (_renderLock)
-            {
-                // 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);
-
-                // Reset the cursor blink.
-                Volatile.Write(ref _textBoxBlinkCounter.Value, 0);
-            }
-        }
-
-        public void UpdateCommandState(bool? acceptPressed, bool? cancelPressed, bool? controllerEnabled)
-        {
-            lock (_renderLock)
-            {
-                // Update the parameters that were provided.
-                _acceptPressed     = acceptPressed.GetValueOrDefault(_acceptPressed);
-                _cancelPressed     = cancelPressed.GetValueOrDefault(_cancelPressed);
-                _controllerEnabled = controllerEnabled.GetValueOrDefault(_controllerEnabled);
-            }
-        }
-
-        private void Redraw()
-        {
-            if (_surface == null)
-            {
-                return;
-            }
-
-            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)
+                lock (stateLock)
                 {
-                    // Just draw a semi-transparent rectangle on top to fade the component with the background.
-                    // TODO (caian): This will not work if one decides to add make background semi-transparent as well.
-                    graphics.FillRectangle(_disabledBrush, messagePositionX, messagePositionY, messageRectangle.Width, messageRectangle.Height);
-                }
-
-                DrawTextBox(graphics);
-
-                float halfWidth = _panelRectangle.Width / 2;
-
-                PointF acceptButtonPosition  = new PointF(halfWidth - 180, 185);
-                PointF cancelButtonPosition  = new PointF(halfWidth      , 185);
-                PointF disableButtonPosition = new PointF(halfWidth + 180, 185);
-
-                DrawPadButton       (graphics, acceptButtonPosition , _padAcceptIcon, AcceptText, _acceptPressed, _controllerEnabled);
-                DrawPadButton       (graphics, cancelButtonPosition , _padCancelIcon, CancelText, _cancelPressed, _controllerEnabled);
-                DrawControllerToggle(graphics, disableButtonPosition, _controllerEnabled);
-            }
-        }
-
-        private void RecreateSurface()
-        {
-            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;
+                    // 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);
 
-            _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;
+                    // Tell the render thread there is something new to render.
+                    Monitor.PulseAll(stateLock);
+                }
+            }, TextBoxBlinkSleepMilliseconds);
         }
 
-        private float CalibrateTextHeight(Font font)
+        private static void StartRenderer(TimedAction timedAction, SoftwareKeyboardRendererBase renderer, SoftwareKeyboardUiState state, object stateLock)
         {
-            // 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);
+            SoftwareKeyboardUiState internalState = new SoftwareKeyboardUiState();
 
-            var surfaceSize = (int)Math.Ceiling(2 * font.Size);
+            bool canCreateSurface = false;
+            bool needsUpdate      = true;
 
-            string calibrationText = "|";
-
-            using (var surface = new Bitmap(surfaceSize, surfaceSize, PixelFormat.Format32bppArgb))
-            using (var graphics = CreateGraphics(surface))
+            timedAction.Reset(() =>
             {
-                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++)
+                lock (stateLock)
                 {
-                    for (int x = 0; x < surfaceData.Stride; x += 4)
+                    if (!Monitor.Wait(stateLock, RendererWaitTimeoutMilliseconds))
                     {
-                        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;
                     }
-                }
-
-                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;
+                    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);
 
-            var inputTextPosition = new PointF(inputTextX, inputTextY);
+                    canCreateSurface = state.SurfaceInfo != null && internalState.SurfaceInfo == null;
 
-            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)
+                    if (canCreateSurface)
                     {
-                        // 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;
-                        }
+                        internalState.SurfaceInfo = state.SurfaceInfo;
                     }
-                    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)
+                if (canCreateSurface)
                 {
-                    graphics.DrawLine(cursorPen, cursorPositionXLeft, cursorPositionYTop, cursorPositionXLeft, cursorPositionYBottom);
+                    renderer.CreateSurface(internalState.SurfaceInfo);
                 }
-                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;
+                if (needsUpdate)
+                {
+                    renderer.DrawMutableElements(internalState);
+                    renderer.CopyImageToBuffer();
+                    needsUpdate = false;
                 }
-            }
-            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)
+        private static bool UpdateStateField<T>(ref T source, ref T destination) where T : IEquatable<T>
         {
-            // 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 (!source.Equals(destination))
             {
-                if (pressed)
-                {
-                    graphics.DrawPath(_padPressedPen, frame);
-                }
+                destination = source;
+                return true;
             }
-            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);
+            return false;
         }
 
-        private bool TryCopyTo(IVirtualMemoryManager destination, ulong position)
+#pragma warning disable CS8632
+        public void UpdateTextState(string? inputText, int? cursorBegin, int? cursorEnd, bool? overwriteMode, bool? typingEnabled)
+#pragma warning restore CS8632
         {
-            if (_surface == null)
+            lock (_stateLock)
             {
-                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);
+                // Update the parameters that were provided.
+                _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);
 
-            Marshal.Copy(surfaceData.Scan0, data, 0, dataLength);
+                // Reset the cursor blink.
+                _state.TextBoxBlinkCounter = 0;
 
-            for (int i = 0; i < dataConvert.Length; i++)
-            {
-                dataConvert[i] = BitOperations.RotateRight(BinaryPrimitives.ReverseEndianness(dataConvert[i]), 8);
+                // Tell the render thread there is something new to render.
+                Monitor.PulseAll(_stateLock);
             }
+        }
 
-            try
-            {
-                destination.Write(position, data);
-            }
-            finally
+        public void UpdateCommandState(bool? acceptPressed, bool? cancelPressed, bool? controllerEnabled)
+        {
+            lock (_stateLock)
             {
-                _surface.UnlockBits(surfaceData);
-            }
+                // Update the parameters that were provided.
+                _state.AcceptPressed     = acceptPressed.GetValueOrDefault(_state.AcceptPressed);
+                _state.CancelPressed     = cancelPressed.GetValueOrDefault(_state.CancelPressed);
+                _state.ControllerEnabled = controllerEnabled.GetValueOrDefault(_state.ControllerEnabled);
 
-            return true;
+                // Tell the render thread there is something new to render.
+                Monitor.PulseAll(_stateLock);
+            }
         }
 
-        internal bool DrawTo(RenderingSurfaceInfo surfaceInfo, IVirtualMemoryManager destination, ulong position)
+        public void SetSurfaceInfo(RenderingSurfaceInfo surfaceInfo)
         {
-            lock (_renderLock)
+            lock (_stateLock)
             {
-                if (!_surfaceInfo.Equals(surfaceInfo))
-                {
-                    _surfaceInfo = surfaceInfo;
-                    RecreateSurface();
-                    RecomputeConstants();
-                }
-
-                Redraw();
+                _state.SurfaceInfo = surfaceInfo;
 
-                return TryCopyTo(destination, position);
+                // Tell the render thread there is something new to render.
+                Monitor.PulseAll(_stateLock);
             }
         }
 
+        internal bool DrawTo(IVirtualMemoryManager destination, ulong position)
+        {
+            return _renderer.WriteBufferToMemory(destination, position);
+        }
+
         public void Dispose()
         {
             _textBoxBlinkTimedAction.RequestCancel();
+            _renderAction.RequestCancel();
         }
     }
 }
-- 
cgit v1.2.3-70-g09d2