Просмотр исходного кода

Add support for inline software keyboard (#1868)

* Add background mode configuration to SoftwareKeyboardApplet

* Add placeholder text generator for Software Keyboard in background mode

* Add stub for GetIndirectLayerImageMap

* Fix default state of DecidedCancel response

* Add GUI text input to Software Keyboard in background mode

* Fix graphical glitch when Inline Software Keyboard appears

* Improve readability of InlineResponses class

* Improve code styling and fix compiler warnings

* Replace ServiceDisplay log class by ServiceVi

* Replace static readonly by const

* Add proper finalization to the keyboard applet in inline mode

* Rename constants to start with uppercase

* Fix inline keyboard not working with some games

* Improve code readability

* Fix code styling
Caian Benedicto 5 лет назад
Родитель
Сommit
e57b140429

+ 18 - 0
Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardRequest.cs

@@ -0,0 +1,18 @@
+namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
+{
+    /// <summary>
+    /// Possible requests to the keyboard when running in inline mode.
+    /// </summary>
+    enum InlineKeyboardRequest : uint
+    {
+        Unknown0                    = 0x0,
+        Finalize                    = 0x4,
+        SetUserWordInfo             = 0x6,
+        SetCustomizeDic             = 0x7,
+        Calc                        = 0xA,
+        SetCustomizedDictionaries   = 0xB,
+        UnsetCustomizedDictionaries = 0xC,
+        UseChangedStringV2          = 0xD,
+        UseMovedCursorV2            = 0xE
+    }
+}

+ 26 - 0
Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardResponse.cs

@@ -0,0 +1,26 @@
+namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
+{
+    /// <summary>
+    /// Possible responses from the keyboard when running in inline mode.
+    /// </summary>
+    enum InlineKeyboardResponse : uint
+    {
+        FinishedInitialize          = 0x0,
+        Default                     = 0x1,
+        ChangedString               = 0x2,
+        MovedCursor                 = 0x3,
+        MovedTab                    = 0x4,
+        DecidedEnter                = 0x5,
+        DecidedCancel               = 0x6,
+        ChangedStringUtf8           = 0x7,
+        MovedCursorUtf8             = 0x8,
+        DecidedEnterUtf8            = 0x9,
+        UnsetCustomizeDic           = 0xA,
+        ReleasedUserWordInfo        = 0xB,
+        UnsetCustomizedDictionaries = 0xC,
+        ChangedStringV2             = 0xD,
+        MovedCursorV2               = 0xE,
+        ChangedStringUtf8V2         = 0xF,
+        MovedCursorUtf8V2           = 0x10
+    }
+}

+ 14 - 0
Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardState.cs

@@ -0,0 +1,14 @@
+namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
+{
+    /// <summary>
+    /// Possible states for the keyboard when running in inline mode.
+    /// </summary>
+    enum InlineKeyboardState : uint
+    {
+        Uninitialized = 0x0,
+        Initializing  = 0x1,
+        Ready         = 0x2,
+        DataAvailable = 0x3,
+        Completed     = 0x4
+    }
+}

+ 295 - 0
Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineResponses.cs

@@ -0,0 +1,295 @@
+using System.IO;
+using System.Text;
+
+namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
+{
+    internal class InlineResponses
+    {
+        private const uint MaxStrLenUTF8 = 0x7D4;
+        private const uint MaxStrLenUTF16 = 0x3EC;
+
+        private static void BeginResponse(InlineKeyboardState state, InlineKeyboardResponse resCode, BinaryWriter writer)
+        {
+            writer.Write((uint)state);
+            writer.Write((uint)resCode);
+        }
+
+        private static uint WriteString(string text, BinaryWriter writer, uint maxSize, Encoding encoding)
+        {
+            // Ensure the text fits in the buffer, but do not straight cut the bytes because
+            // this may corrupt the encoding. Search for a cut in the source string that fits.
+
+            byte[] bytes = null;
+
+            for (int maxStr = text.Length; maxStr >= 0; maxStr--)
+            {
+                // This loop will probably will run only once.
+                bytes = encoding.GetBytes(text.Substring(0, maxStr));
+                if (bytes.Length <= maxSize)
+                {
+                    break;
+                }
+            }
+
+            writer.Write(bytes);
+            writer.Seek((int)maxSize - bytes.Length, SeekOrigin.Current);
+            writer.Write((uint)text.Length); // String size
+
+            return (uint)text.Length; // Return the cursor position at the end of the text
+        }
+
+        private static void WriteStringWithCursor(string text, BinaryWriter writer, uint maxSize, Encoding encoding)
+        {
+            uint cursor = WriteString(text, writer, maxSize, encoding);
+
+            writer.Write(cursor); // Cursor position
+        }
+
+        public static byte[] FinishedInitialize()
+        {
+            uint resSize = 2 * sizeof(uint) + 0x1;
+
+            using (MemoryStream stream = new MemoryStream(new byte[resSize]))
+            using (BinaryWriter writer = new BinaryWriter(stream))
+            {
+                BeginResponse(InlineKeyboardState.Ready, InlineKeyboardResponse.FinishedInitialize, writer);
+                writer.Write((byte)1); // Data (ignored by the program)
+
+                return stream.ToArray();
+            }
+        }
+
+        public static byte[] Default()
+        {
+            uint resSize = 2 * sizeof(uint);
+
+            using (MemoryStream stream = new MemoryStream(new byte[resSize]))
+            using (BinaryWriter writer = new BinaryWriter(stream))
+            {
+                BeginResponse(InlineKeyboardState.Initializing, InlineKeyboardResponse.Default, writer);
+
+                return stream.ToArray();
+            }
+        }
+
+        public static byte[] ChangedString(string text)
+        {
+            uint resSize = 4 * sizeof(uint) + MaxStrLenUTF16;
+
+            using (MemoryStream stream = new MemoryStream(new byte[resSize]))
+            using (BinaryWriter writer = new BinaryWriter(stream))
+            {
+                BeginResponse(InlineKeyboardState.Initializing, InlineKeyboardResponse.ChangedString, writer);
+                WriteStringWithCursor(text, writer, MaxStrLenUTF16, Encoding.Unicode);
+                writer.Write((int)0); // ?
+                writer.Write((int)0); // ?
+
+                return stream.ToArray();
+            }
+        }
+
+        public static byte[] MovedCursor(string text)
+        {
+            uint resSize = 4 * sizeof(uint) + MaxStrLenUTF16;
+
+            using (MemoryStream stream = new MemoryStream(new byte[resSize]))
+            using (BinaryWriter writer = new BinaryWriter(stream))
+            {
+                BeginResponse(InlineKeyboardState.Initializing, InlineKeyboardResponse.MovedCursor, writer);
+                WriteStringWithCursor(text, writer, MaxStrLenUTF16, Encoding.Unicode);
+
+                return stream.ToArray();
+            }
+        }
+
+        public static byte[] MovedTab(string text)
+        {
+            // Should be the same as MovedCursor.
+
+            uint resSize = 4 * sizeof(uint) + MaxStrLenUTF16;
+
+            using (MemoryStream stream = new MemoryStream(new byte[resSize]))
+            using (BinaryWriter writer = new BinaryWriter(stream))
+            {
+                BeginResponse(InlineKeyboardState.Initializing, InlineKeyboardResponse.MovedTab, writer);
+                WriteStringWithCursor(text, writer, MaxStrLenUTF16, Encoding.Unicode);
+
+                return stream.ToArray();
+            }
+        }
+
+        public static byte[] DecidedEnter(string text)
+        {
+            uint resSize = 3 * sizeof(uint) + MaxStrLenUTF16;
+
+            using (MemoryStream stream = new MemoryStream(new byte[resSize]))
+            using (BinaryWriter writer = new BinaryWriter(stream))
+            {
+                BeginResponse(InlineKeyboardState.Completed, InlineKeyboardResponse.DecidedEnter, writer);
+                WriteString(text, writer, MaxStrLenUTF16, Encoding.Unicode);
+
+                return stream.ToArray();
+            }
+        }
+
+        public static byte[] DecidedCancel()
+        {
+            uint resSize = 2 * sizeof(uint);
+
+            using (MemoryStream stream = new MemoryStream(new byte[resSize]))
+            using (BinaryWriter writer = new BinaryWriter(stream))
+            {
+                BeginResponse(InlineKeyboardState.Completed, InlineKeyboardResponse.DecidedCancel, writer);
+
+                return stream.ToArray();
+            }
+        }
+
+        public static byte[] ChangedStringUtf8(string text)
+        {
+            uint resSize = 6 * sizeof(uint) + MaxStrLenUTF8;
+
+            using (MemoryStream stream = new MemoryStream(new byte[resSize]))
+            using (BinaryWriter writer = new BinaryWriter(stream))
+            {
+                BeginResponse(InlineKeyboardState.DataAvailable, InlineKeyboardResponse.ChangedStringUtf8, writer);
+                WriteStringWithCursor(text, writer, MaxStrLenUTF8, Encoding.UTF8);
+                writer.Write((int)0); // ?
+                writer.Write((int)0); // ?
+
+                return stream.ToArray();
+            }
+        }
+
+        public static byte[] MovedCursorUtf8(string text)
+        {
+            uint resSize = 4 * sizeof(uint) + MaxStrLenUTF8;
+
+            using (MemoryStream stream = new MemoryStream(new byte[resSize]))
+            using (BinaryWriter writer = new BinaryWriter(stream))
+            {
+                BeginResponse(InlineKeyboardState.DataAvailable, InlineKeyboardResponse.MovedCursorUtf8, writer);
+                WriteStringWithCursor(text, writer, MaxStrLenUTF8, Encoding.UTF8);
+
+                return stream.ToArray();
+            }
+        }
+
+        public static byte[] DecidedEnterUtf8(string text)
+        {
+            uint resSize = 3 * sizeof(uint) + MaxStrLenUTF8;
+
+            using (MemoryStream stream = new MemoryStream(new byte[resSize]))
+            using (BinaryWriter writer = new BinaryWriter(stream))
+            {
+                BeginResponse(InlineKeyboardState.Completed, InlineKeyboardResponse.DecidedEnterUtf8, writer);
+                WriteString(text, writer, MaxStrLenUTF8, Encoding.UTF8);
+
+                return stream.ToArray();
+            }
+        }
+
+        public static byte[] UnsetCustomizeDic()
+        {
+            uint resSize = 2 * sizeof(uint);
+
+            using (MemoryStream stream = new MemoryStream(new byte[resSize]))
+            using (BinaryWriter writer = new BinaryWriter(stream))
+            {
+                BeginResponse(InlineKeyboardState.Initializing, InlineKeyboardResponse.UnsetCustomizeDic, writer);
+
+                return stream.ToArray();
+            }
+        }
+
+        public static byte[] ReleasedUserWordInfo()
+        {
+            uint resSize = 2 * sizeof(uint);
+
+            using (MemoryStream stream = new MemoryStream(new byte[resSize]))
+            using (BinaryWriter writer = new BinaryWriter(stream))
+            {
+                BeginResponse(InlineKeyboardState.Initializing, InlineKeyboardResponse.ReleasedUserWordInfo, writer);
+
+                return stream.ToArray();
+            }
+        }
+
+        public static byte[] UnsetCustomizedDictionaries()
+        {
+            uint resSize = 2 * sizeof(uint);
+
+            using (MemoryStream stream = new MemoryStream(new byte[resSize]))
+            using (BinaryWriter writer = new BinaryWriter(stream))
+            {
+                BeginResponse(InlineKeyboardState.Initializing, InlineKeyboardResponse.UnsetCustomizedDictionaries, writer);
+
+                return stream.ToArray();
+            }
+        }
+
+        public static byte[] ChangedStringV2(string text)
+        {
+            uint resSize = 6 * sizeof(uint) + MaxStrLenUTF16 + 0x1;
+
+            using (MemoryStream stream = new MemoryStream(new byte[resSize]))
+            using (BinaryWriter writer = new BinaryWriter(stream))
+            {
+                BeginResponse(InlineKeyboardState.DataAvailable, InlineKeyboardResponse.ChangedStringV2, writer);
+                WriteStringWithCursor(text, writer, MaxStrLenUTF16, Encoding.Unicode);
+                writer.Write((int)0); // ?
+                writer.Write((int)0); // ?
+                writer.Write((byte)0); // Flag == 0
+
+                return stream.ToArray();
+            }
+        }
+
+        public static byte[] MovedCursorV2(string text)
+        {
+            uint resSize = 4 * sizeof(uint) + MaxStrLenUTF16 + 0x1;
+
+            using (MemoryStream stream = new MemoryStream(new byte[resSize]))
+            using (BinaryWriter writer = new BinaryWriter(stream))
+            {
+                BeginResponse(InlineKeyboardState.DataAvailable, InlineKeyboardResponse.MovedCursorV2, writer);
+                WriteStringWithCursor(text, writer, MaxStrLenUTF16, Encoding.Unicode);
+                writer.Write((byte)0); // Flag == 0
+
+                return stream.ToArray();
+            }
+        }
+
+        public static byte[] ChangedStringUtf8V2(string text)
+        {
+            uint resSize = 6 * sizeof(uint) + MaxStrLenUTF8 + 0x1;
+
+            using (MemoryStream stream = new MemoryStream(new byte[resSize]))
+            using (BinaryWriter writer = new BinaryWriter(stream))
+            {
+                BeginResponse(InlineKeyboardState.DataAvailable, InlineKeyboardResponse.ChangedStringUtf8V2, writer);
+                WriteStringWithCursor(text, writer, MaxStrLenUTF8, Encoding.UTF8);
+                writer.Write((int)0); // ?
+                writer.Write((int)0); // ?
+                writer.Write((byte)0); // Flag == 0
+
+                return stream.ToArray();
+            }
+        }
+
+        public static byte[] MovedCursorUtf8V2(string text)
+        {
+            uint resSize = 4 * sizeof(uint) + MaxStrLenUTF8 + 0x1;
+
+            using (MemoryStream stream = new MemoryStream(new byte[resSize]))
+            using (BinaryWriter writer = new BinaryWriter(stream))
+            {
+                BeginResponse(InlineKeyboardState.DataAvailable, InlineKeyboardResponse.MovedCursorUtf8V2, writer);
+                WriteStringWithCursor(text, writer, MaxStrLenUTF8, Encoding.UTF8);
+                writer.Write((byte)0); // Flag == 0
+
+                return stream.ToArray();
+            }
+        }
+    }
+}

+ 61 - 0
Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardAppear.cs

@@ -0,0 +1,61 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
+{
+    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+    struct SoftwareKeyboardAppear
+    {
+        private const int OkTextLength = 8;
+
+        // Some games send a Calc without intention of showing the keyboard, a
+        // common trend observed is that this field will be != 0 in such cases.
+        public uint ShouldBeHidden;
+
+        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = OkTextLength + 1)]
+        public string OkText;
+
+        /// <summary>
+        /// The character displayed in the left button of the numeric keyboard.
+        /// This is ignored when Mode is not set to NumbersOnly.
+        /// </summary>
+        public char LeftOptionalSymbolKey;
+
+        /// <summary>
+        /// The character displayed in the right button of the numeric keyboard.
+        /// This is ignored when Mode is not set to NumbersOnly.
+        /// </summary>
+        public char RightOptionalSymbolKey;
+
+        /// <summary>
+        /// When set, predictive typing is enabled making use of the system dictionary,
+        /// and any custom user dictionary.
+        /// </summary>
+        [MarshalAs(UnmanagedType.I1)]
+        public bool PredictionEnabled;
+
+        public byte Empty;
+
+        /// <summary>
+        /// Specifies prohibited characters that cannot be input into the text entry area.
+        /// </summary>
+        public InvalidCharFlags InvalidCharFlag;
+
+        public int Padding1;
+        public int Padding2;
+
+        public byte EnableReturnButton;
+
+        public byte Padding3;
+        public byte Padding4;
+        public byte Padding5;
+
+        public uint CalcArgFlags;
+
+        public uint Padding6;
+        public uint Padding7;
+        public uint Padding8;
+        public uint Padding9;
+        public uint Padding10;
+        public uint Padding11;
+    }
+}

+ 221 - 37
Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs

@@ -5,6 +5,8 @@ using System;
 using System.IO;
 using System.Runtime.InteropServices;
 using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
 
 namespace Ryujinx.HLE.HOS.Applets
 {
@@ -19,10 +21,19 @@ namespace Ryujinx.HLE.HOS.Applets
 
         private SoftwareKeyboardState _state = SoftwareKeyboardState.Uninitialized;
 
+        private bool _isBackground = false;
+
         private AppletSession _normalSession;
         private AppletSession _interactiveSession;
 
-        private SoftwareKeyboardConfig _keyboardConfig;
+        // Configuration for foreground mode
+        private SoftwareKeyboardConfig  _keyboardFgConfig;
+        private SoftwareKeyboardCalc    _keyboardCalc;
+        private SoftwareKeyboardDictSet _keyboardDict;
+
+        // Configuration for background mode
+        private SoftwareKeyboardInitialize _keyboardBgInitialize;
+
         private byte[] _transferMemory;
 
         private string   _textValue = null;
@@ -47,30 +58,46 @@ namespace Ryujinx.HLE.HOS.Applets
             var launchParams   = _normalSession.Pop();
             var keyboardConfig = _normalSession.Pop();
 
-            if (keyboardConfig.Length < Marshal.SizeOf<SoftwareKeyboardConfig>())
+            // TODO: A better way would be handling the background creation properly
+            // in LibraryAppleCreator / Acessor instead of guessing by size.
+            if (keyboardConfig.Length == Marshal.SizeOf<SoftwareKeyboardInitialize>())
             {
-                Logger.Error?.Print(LogClass.ServiceAm, $"SoftwareKeyboardConfig size mismatch. Expected {Marshal.SizeOf<SoftwareKeyboardConfig>():x}. Got {keyboardConfig.Length:x}");
+                _isBackground = true;
+
+                _keyboardBgInitialize = ReadStruct<SoftwareKeyboardInitialize>(keyboardConfig);
+                _state = SoftwareKeyboardState.Uninitialized;
+
+                return ResultCode.Success;
             }
             else
             {
-                _keyboardConfig = ReadStruct<SoftwareKeyboardConfig>(keyboardConfig);
-            }
+                _isBackground = false;
 
-            if (!_normalSession.TryPop(out _transferMemory))
-            {
-                Logger.Error?.Print(LogClass.ServiceAm, "SwKbd Transfer Memory is null");
-            }
+                if (keyboardConfig.Length < Marshal.SizeOf<SoftwareKeyboardConfig>())
+                {
+                    Logger.Error?.Print(LogClass.ServiceAm, $"SoftwareKeyboardConfig size mismatch. Expected {Marshal.SizeOf<SoftwareKeyboardConfig>():x}. Got {keyboardConfig.Length:x}");
+                }
+                else
+                {
+                    _keyboardFgConfig = ReadStruct<SoftwareKeyboardConfig>(keyboardConfig);
+                }
 
-            if (_keyboardConfig.UseUtf8)
-            {
-                _encoding = Encoding.UTF8;
-            }
+                if (!_normalSession.TryPop(out _transferMemory))
+                {
+                    Logger.Error?.Print(LogClass.ServiceAm, "SwKbd Transfer Memory is null");
+                }
 
-            _state = SoftwareKeyboardState.Ready;
+                if (_keyboardFgConfig.UseUtf8)
+                {
+                    _encoding = Encoding.UTF8;
+                }
 
-            Execute();
+                _state = SoftwareKeyboardState.Ready;
 
-            return ResultCode.Success;
+                ExecuteForegroundKeyboard();
+
+                return ResultCode.Success;
+            }
         }
 
         public ResultCode GetResult()
@@ -78,39 +105,39 @@ namespace Ryujinx.HLE.HOS.Applets
             return ResultCode.Success;
         }
 
-        private void Execute()
+        private void ExecuteForegroundKeyboard()
         {
             string initialText = null;
 
             // Initial Text is always encoded as a UTF-16 string in the work buffer (passed as transfer memory)
             // InitialStringOffset points to the memory offset and InitialStringLength is the number of UTF-16 characters
-            if (_transferMemory != null && _keyboardConfig.InitialStringLength > 0)
+            if (_transferMemory != null && _keyboardFgConfig.InitialStringLength > 0)
             {
-                initialText = Encoding.Unicode.GetString(_transferMemory, _keyboardConfig.InitialStringOffset, 2 * _keyboardConfig.InitialStringLength);
+                initialText = Encoding.Unicode.GetString(_transferMemory, _keyboardFgConfig.InitialStringOffset, 2 * _keyboardFgConfig.InitialStringLength);
             }
 
             // If the max string length is 0, we set it to a large default
             // length.
-            if (_keyboardConfig.StringLengthMax == 0)
+            if (_keyboardFgConfig.StringLengthMax == 0)
             {
-                _keyboardConfig.StringLengthMax = 100;
+                _keyboardFgConfig.StringLengthMax = 100;
             }
 
             var args = new SoftwareKeyboardUiArgs
             {
-                HeaderText = _keyboardConfig.HeaderText,
-                SubtitleText = _keyboardConfig.SubtitleText,
-                GuideText = _keyboardConfig.GuideText,
-                SubmitText = (!string.IsNullOrWhiteSpace(_keyboardConfig.SubmitText) ? _keyboardConfig.SubmitText : "OK"),
-                StringLengthMin = _keyboardConfig.StringLengthMin, 
-                StringLengthMax = _keyboardConfig.StringLengthMax,
+                HeaderText = _keyboardFgConfig.HeaderText,
+                SubtitleText = _keyboardFgConfig.SubtitleText,
+                GuideText = _keyboardFgConfig.GuideText,
+                SubmitText = (!string.IsNullOrWhiteSpace(_keyboardFgConfig.SubmitText) ? _keyboardFgConfig.SubmitText : "OK"),
+                StringLengthMin = _keyboardFgConfig.StringLengthMin,
+                StringLengthMax = _keyboardFgConfig.StringLengthMax,
                 InitialText = initialText
             };
 
             // Call the configured GUI handler to get user's input
             if (_device.UiHandler == null)
             {
-                Logger.Warning?.Print(LogClass.Application, $"GUI Handler is not set. Falling back to default");
+                Logger.Warning?.Print(LogClass.Application, "GUI Handler is not set. Falling back to default");
                 _okPressed = true;
             }
             else
@@ -122,22 +149,22 @@ namespace Ryujinx.HLE.HOS.Applets
 
             // If the game requests a string with a minimum length less
             // than our default text, repeat our default text until we meet
-            // the minimum length requirement. 
+            // the minimum length requirement.
             // This should always be done before the text truncation step.
-            while (_textValue.Length < _keyboardConfig.StringLengthMin)
+            while (_textValue.Length < _keyboardFgConfig.StringLengthMin)
             {
                 _textValue = String.Join(" ", _textValue, _textValue);
             }
 
             // If our default text is longer than the allowed length,
             // we truncate it.
-            if (_textValue.Length > _keyboardConfig.StringLengthMax)
+            if (_textValue.Length > _keyboardFgConfig.StringLengthMax)
             {
-                _textValue = _textValue.Substring(0, (int)_keyboardConfig.StringLengthMax);
+                _textValue = _textValue.Substring(0, (int)_keyboardFgConfig.StringLengthMax);
             }
 
             // Does the application want to validate the text itself?
-            if (_keyboardConfig.CheckText)
+            if (_keyboardFgConfig.CheckText)
             {
                 // The application needs to validate the response, so we
                 // submit it to the interactive output buffer, and poll it
@@ -151,7 +178,7 @@ namespace Ryujinx.HLE.HOS.Applets
             {
                 // If the application doesn't need to validate the response,
                 // we push the data to the non-interactive output buffer
-                // and poll it for completion.             
+                // and poll it for completion.
                 _state = SoftwareKeyboardState.Complete;
 
                 _normalSession.Push(BuildResponse(_textValue, false));
@@ -162,16 +189,28 @@ namespace Ryujinx.HLE.HOS.Applets
 
         private void OnInteractiveData(object sender, EventArgs e)
         {
-            // Obtain the validation status response, 
+            // Obtain the validation status response.
             var data = _interactiveSession.Pop();
 
+            if (_isBackground)
+            {
+                OnBackgroundInteractiveData(data);
+            }
+            else
+            {
+                OnForegroundInteractiveData(data);
+            }
+        }
+
+        private void OnForegroundInteractiveData(byte[] data)
+        {
             if (_state == SoftwareKeyboardState.ValidationPending)
             {
                 // TODO(jduncantor):
                 // If application rejects our "attempt", submit another attempt,
                 // and put the applet back in PendingValidation state.
 
-                // For now we assume success, so we push the final result 
+                // For now we assume success, so we push the final result
                 // to the standard output buffer and carry on our merry way.
                 _normalSession.Push(BuildResponse(_textValue, false));
 
@@ -194,6 +233,151 @@ namespace Ryujinx.HLE.HOS.Applets
             }
         }
 
+        private void OnBackgroundInteractiveData(byte[] data)
+        {
+            // WARNING: Only invoke applet state changes after an explicit finalization
+            // request from the game, this is because the inline keyboard is expected to
+            // keep running in the background sending data by itself.
+
+            using (MemoryStream stream = new MemoryStream(data))
+            using (BinaryReader reader = new BinaryReader(stream))
+            {
+                var request = (InlineKeyboardRequest)reader.ReadUInt32();
+
+                long remaining;
+
+                // Always show the keyboard if the state is 'Ready'.
+                bool showKeyboard = _state == SoftwareKeyboardState.Ready;
+
+                switch (request)
+                {
+                    case InlineKeyboardRequest.Unknown0: // Unknown request sent by some games after calc
+                        _interactiveSession.Push(InlineResponses.Default());
+                        break;
+                    case InlineKeyboardRequest.UseChangedStringV2:
+                        // Not used because we only send the entire string after confirmation.
+                        _interactiveSession.Push(InlineResponses.Default());
+                        break;
+                    case InlineKeyboardRequest.UseMovedCursorV2:
+                        // Not used because we only send the entire string after confirmation.
+                        _interactiveSession.Push(InlineResponses.Default());
+                        break;
+                    case InlineKeyboardRequest.SetCustomizeDic:
+                        remaining = stream.Length - stream.Position;
+                        if (remaining != Marshal.SizeOf<SoftwareKeyboardDictSet>())
+                        {
+                            Logger.Error?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard DictSet of {remaining} bytes!");
+                        }
+                        else
+                        {
+                            var keyboardDictData = reader.ReadBytes((int)remaining);
+                            _keyboardDict = ReadStruct<SoftwareKeyboardDictSet>(keyboardDictData);
+                        }
+                        _interactiveSession.Push(InlineResponses.Default());
+                        break;
+                    case InlineKeyboardRequest.Calc:
+                        // Put the keyboard in a Ready state, this will force showing
+                        _state = SoftwareKeyboardState.Ready;
+                        remaining = stream.Length - stream.Position;
+                        if (remaining != Marshal.SizeOf<SoftwareKeyboardCalc>())
+                        {
+                            Logger.Error?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard Calc of {remaining} bytes!");
+                        }
+                        else
+                        {
+                            var keyboardCalcData = reader.ReadBytes((int)remaining);
+                            _keyboardCalc = ReadStruct<SoftwareKeyboardCalc>(keyboardCalcData);
+
+                            if (_keyboardCalc.Utf8Mode == 0x1)
+                            {
+                                _encoding = Encoding.UTF8;
+                            }
+
+                            // Force showing the keyboard regardless of the state, an unwanted
+                            // input dialog may show, but it is better than a soft lock.
+                            if (_keyboardCalc.Appear.ShouldBeHidden == 0)
+                            {
+                                showKeyboard = true;
+                            }
+                        }
+                        // Send an initialization finished signal.
+                        _interactiveSession.Push(InlineResponses.FinishedInitialize());
+                        // Start a task with the GUI handler to get user's input.
+                        new Task(() =>
+                        {
+                            bool submit = true;
+                            string inputText = (!string.IsNullOrWhiteSpace(_keyboardCalc.InputText) ? _keyboardCalc.InputText : DefaultText);
+
+                            // Call the configured GUI handler to get user's input.
+                            if (!showKeyboard)
+                            {
+                                // Submit the default text to avoid soft locking if the keyboard was ignored by
+                                // accident. It's better to change the name than being locked out of the game.
+                                submit = true;
+                                inputText = DefaultText;
+
+                                Logger.Debug?.Print(LogClass.Application, "Received a dummy Calc, keyboard will not be shown");
+                            }
+                            else if (_device.UiHandler == null)
+                            {
+                                Logger.Warning?.Print(LogClass.Application, "GUI Handler is not set. Falling back to default");
+                            }
+                            else
+                            {
+                                var args = new SoftwareKeyboardUiArgs
+                                {
+                                    HeaderText = "", // The inline keyboard lacks these texts
+                                    SubtitleText = "",
+                                    GuideText = "",
+                                    SubmitText = (!string.IsNullOrWhiteSpace(_keyboardCalc.Appear.OkText) ? _keyboardCalc.Appear.OkText : "OK"),
+                                    StringLengthMin = 0,
+                                    StringLengthMax = 100,
+                                    InitialText = inputText
+                                };
+
+                                submit = _device.UiHandler.DisplayInputDialog(args, out inputText);
+                            }
+
+                            if (submit)
+                            {
+                                Logger.Debug?.Print(LogClass.ServiceAm, "Sending keyboard OK...");
+
+                                if (_encoding == Encoding.UTF8)
+                                {
+                                    _interactiveSession.Push(InlineResponses.DecidedEnterUtf8(inputText));
+                                }
+                                else
+                                {
+                                    _interactiveSession.Push(InlineResponses.DecidedEnter(inputText));
+                                }
+                            }
+                            else
+                            {
+                                Logger.Debug?.Print(LogClass.ServiceAm, "Sending keyboard Cancel...");
+                                _interactiveSession.Push(InlineResponses.DecidedCancel());
+                            }
+
+                            // TODO: Why is this necessary? Does the software expect a constant stream of responses?
+                            Thread.Sleep(500);
+
+                            Logger.Debug?.Print(LogClass.ServiceAm, "Resetting state of the keyboard...");
+                            _interactiveSession.Push(InlineResponses.Default());
+                        }).Start();
+                        break;
+                    case InlineKeyboardRequest.Finalize:
+                        // The game wants to close the keyboard applet and will wait for a state change.
+                        _state = SoftwareKeyboardState.Uninitialized;
+                        AppletStateChanged?.Invoke(this, null);
+                        break;
+                    default:
+                        // We shouldn't be able to get here through standard swkbd execution.
+                        Logger.Error?.Print(LogClass.ServiceAm, $"Invalid Software Keyboard request {request} during state {_state}!");
+                        _interactiveSession.Push(InlineResponses.Default());
+                        break;
+                }
+            }
+        }
+
         private byte[] BuildResponse(string text, bool interactive)
         {
             int bufferSize = interactive ? InteractiveBufferSize : StandardBufferSize;
@@ -227,7 +411,7 @@ namespace Ryujinx.HLE.HOS.Applets
             GCHandle handle = GCHandle.Alloc(data, GCHandleType.Pinned);
 
             try
-            {    
+            {
                 return Marshal.PtrToStructure<T>(handle.AddrOfPinnedObject());
             }
             finally

+ 84 - 0
Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCalc.cs

@@ -0,0 +1,84 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
+{
+    /// <summary>
+    /// A structure that defines the configuration options of the software keyboard.
+    /// </summary>
+    [StructLayout(LayoutKind.Sequential, Pack=1, CharSet = CharSet.Unicode)]
+    struct SoftwareKeyboardCalc
+    {
+        private const int InputTextLength = 505;
+
+        public uint Unknown;
+
+        public ushort Size;
+
+        public byte Unknown1;
+        public byte Unknown2;
+
+        public ulong Flags;
+
+        public SoftwareKeyboardInitialize Initialize;
+
+        public float Volume;
+
+        public int CursorPos;
+
+        public SoftwareKeyboardAppear Appear;
+
+        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = InputTextLength + 1)]
+        public string InputText;
+
+        public byte Utf8Mode;
+
+        public byte Unknown3;
+
+        [MarshalAs(UnmanagedType.I1)]
+        public bool BackspaceEnabled;
+
+        public short Unknown4;
+        public byte Unknown5;
+
+        [MarshalAs(UnmanagedType.I1)]
+        public byte KeytopAsFloating;
+
+        [MarshalAs(UnmanagedType.I1)]
+        public byte FooterScalable;
+
+        [MarshalAs(UnmanagedType.I1)]
+        public byte AlphaEnabledInInputMode;
+
+        [MarshalAs(UnmanagedType.I1)]
+        public byte InputModeFadeType;
+
+        [MarshalAs(UnmanagedType.I1)]
+        public byte TouchDisabled;
+
+        [MarshalAs(UnmanagedType.I1)]
+        public byte HardwareKeyboardDisabled;
+
+        public uint Unknown6;
+        public uint Unknown7;
+
+        public float KeytopScale0;
+        public float KeytopScale1;
+        public float KeytopTranslate0;
+        public float KeytopTranslate1;
+        public float KeytopBgAlpha;
+        public float FooterBgAlpha;
+        public float BalloonScale;
+
+        public float Unknown8;
+        public uint Unknown9;
+        public uint Unknown10;
+        public uint Unknown11;
+
+        public byte SeGroup;
+
+        public byte TriggerFlag;
+        public byte Trigger;
+
+        public byte Padding;
+    }
+}

+ 11 - 0
Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardDictSet.cs

@@ -0,0 +1,11 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
+{
+    [StructLayout(LayoutKind.Sequential, Pack = 4)]
+    struct SoftwareKeyboardDictSet
+    {
+        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 28)]
+        public uint[] Entries;
+    }
+}

+ 17 - 0
Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardInitialize.cs

@@ -0,0 +1,17 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
+{
+    /// <summary>
+    /// A structure that indicates the initialization the inline software keyboard.
+    /// </summary>
+    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+    struct SoftwareKeyboardInitialize
+    {
+        public uint Unknown;
+        public byte LibMode;
+        public byte FivePlus;
+        public byte Padding1;
+        public byte Padding2;
+    }
+}

+ 19 - 0
Ryujinx.HLE/HOS/Services/Vi/RootService/IApplicationDisplayService.cs

@@ -1,4 +1,5 @@
 using Ryujinx.Common;
+using Ryujinx.Common.Logging;
 using Ryujinx.Cpu;
 using Ryujinx.HLE.HOS.Ipc;
 using Ryujinx.HLE.HOS.Kernel.Common;
@@ -238,6 +239,24 @@ namespace Ryujinx.HLE.HOS.Services.Vi.RootService
             return null;
         }
 
+        [Command(2450)]
+        // GetIndirectLayerImageMap(s64 width, s64 height, u64 handle, nn::applet::AppletResourceUserId, pid) -> (s64, s64, buffer<bytes, 0x46>)
+        public ResultCode GetIndirectLayerImageMap(ServiceCtx context)
+        {
+            // The size of the layer buffer should be an aligned multiple of width * height
+            // because it was created using GetIndirectLayerImageRequiredMemoryInfo as a guide.
+
+            long layerBuffPosition = context.Request.ReceiveBuff[0].Position;
+            long layerBuffSize     = context.Request.ReceiveBuff[0].Size;
+
+            // Fill the layer with zeros.
+            context.Memory.Fill((ulong)layerBuffPosition, (ulong)layerBuffSize, 0x00);
+
+            Logger.Stub?.PrintStub(LogClass.ServiceVi);
+
+            return ResultCode.Success;
+        }
+
         [Command(2460)]
         // GetIndirectLayerImageRequiredMemoryInfo(u64 width, u64 height) -> (u64 size, u64 alignment)
         public ResultCode GetIndirectLayerImageRequiredMemoryInfo(ServiceCtx context)