am: Initial swkbd implementation

Currently only implements the full screen keyboard, inline keyboard will come later.
This commit is contained in:
jduncanator 2019-11-15 02:41:47 +11:00
parent 79abc6ed93
commit 6a107e6dfb
14 changed files with 476 additions and 35 deletions

View file

@ -12,7 +12,8 @@ namespace Ryujinx.HLE.HOS.Applets
{
_appletMapping = new Dictionary<AppletId, Type>
{
{ AppletId.PlayerSelect, typeof(PlayerSelectApplet) }
{ AppletId.PlayerSelect, typeof(PlayerSelectApplet) },
{ AppletId.SoftwareKeyboard, typeof(SoftwareKeyboardApplet) }
};
}

View file

@ -7,7 +7,9 @@ namespace Ryujinx.HLE.HOS.Applets
{
event EventHandler AppletStateChanged;
ResultCode Start(AppletFifo<byte[]> inData, AppletFifo<byte[]> outData);
ResultCode Start(AppletSession normalSession,
AppletSession interactiveSession);
ResultCode GetResult();
}
}

View file

@ -9,8 +9,8 @@ namespace Ryujinx.HLE.HOS.Applets
{
private Horizon _system;
private AppletFifo<byte[]> _inputData;
private AppletFifo<byte[]> _outputData;
private AppletSession _normalSession;
private AppletSession _interactiveSession;
public event EventHandler AppletStateChanged;
@ -19,14 +19,14 @@ namespace Ryujinx.HLE.HOS.Applets
_system = system;
}
public ResultCode Start(AppletFifo<byte[]> inData, AppletFifo<byte[]> outData)
public ResultCode Start(AppletSession normalSession,
AppletSession interactiveSession)
{
_inputData = inData;
_outputData = outData;
_normalSession = normalSession;
_interactiveSession = interactiveSession;
// TODO(jduncanator): Parse PlayerSelectConfig from input data
_outputData.Push(BuildResponse());
normalSession.Push(BuildResponse());
AppletStateChanged?.Invoke(this, null);
return ResultCode.Success;

View file

@ -0,0 +1,164 @@
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Applets.SoftwareKeyboard;
using Ryujinx.HLE.HOS.Services.Am.AppletAE;
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
namespace Ryujinx.HLE.HOS.Applets
{
internal class SoftwareKeyboardApplet : IApplet
{
private const string DEFAULT_NUMB = "1";
private const string DEFAULT_TEXT = "Ryujinx";
private const int STANDARD_BUFFER_SIZE = 0x7D8;
private const int INTERACTIVE_BUFFER_SIZE = 0x7D4;
private SoftwareKeyboardState _state = SoftwareKeyboardState.Uninitialized;
private AppletSession _normalSession;
private AppletSession _interactiveSession;
private SoftwareKeyboardConfig _keyboardConfig;
private string _textValue = DEFAULT_TEXT;
public event EventHandler AppletStateChanged;
public SoftwareKeyboardApplet(Horizon system) { }
public ResultCode Start(AppletSession normalSession,
AppletSession interactiveSession)
{
_normalSession = normalSession;
_interactiveSession = interactiveSession;
_interactiveSession.DataAvailable += OnInteractiveData;
var launchParams = _normalSession.Pop();
var keyboardConfig = _normalSession.Pop();
var transferMemory = _normalSession.Pop();
_keyboardConfig = ReadStruct<SoftwareKeyboardConfig>(keyboardConfig);
_state = SoftwareKeyboardState.Ready;
Execute();
return ResultCode.Success;
}
public ResultCode GetResult()
{
return ResultCode.Success;
}
private void Execute()
{
if (_keyboardConfig.Type == SoftwareKeyboardType.NumbersOnly)
{
_textValue = DEFAULT_NUMB;
}
if (_textValue.Length > _keyboardConfig.StringLengthMax)
{
_textValue = _textValue.Substring(0, (int)_keyboardConfig.StringLengthMax);
}
if (!_keyboardConfig.CheckText)
{
// 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.
_state = SoftwareKeyboardState.Complete;
_normalSession.Push(BuildResponse(_textValue, false));
AppletStateChanged?.Invoke(this, null);
}
else
{
// The application needs to validate the response, so we
// submit it to the interactive output buffer, and poll it
// for validation. Once validated, the application will submit
// back a validation status, which is handled in OnInteractiveDataPushIn.
_state = SoftwareKeyboardState.ValidationPending;
_interactiveSession.Push(BuildResponse(_textValue, true));
}
}
private void OnInteractiveData(object sender, EventArgs e)
{
if(_state == SoftwareKeyboardState.ValidationPending)
{
// TODO(jduncantor):
// If application rejects our "attempt", submit another attempt,
// and put the applet back in PendingValidation state.
// Obtain the validation status response, for now we assume
// success and carry on our merry way.
_interactiveSession.Pop();
_normalSession.Push(BuildResponse(_textValue, false));
AppletStateChanged?.Invoke(this, null);
_state = SoftwareKeyboardState.Complete;
}
else if(_state == SoftwareKeyboardState.Complete)
{
// If we have already completed, we push the result text
// back on the output buffer and poll the application.
_normalSession.Push(BuildResponse(_textValue, false));
AppletStateChanged?.Invoke(this, null);
}
else
{
throw new InvalidOperationException("Software Keyboard is in an invalid state.");
}
}
private byte[] BuildResponse(string text, bool interactive)
{
int bufferSize = !interactive ? STANDARD_BUFFER_SIZE : INTERACTIVE_BUFFER_SIZE;
using (MemoryStream stream = new MemoryStream(new byte[bufferSize]))
using (BinaryWriter writer = new BinaryWriter(stream))
{
byte[] output = Encoding.Unicode.GetBytes(text);
if (!interactive)
{
// Result Code
writer.Write((uint)0);
}
else
{
// In interactive mode, we write the length of the text
// as a long, rather than a result code.
writer.Write((long)output.Length);
}
writer.Write(output);
return stream.ToArray();
}
}
private static T ReadStruct<T>(byte[] data)
where T : struct
{
GCHandle handle = GCHandle.Alloc(data, GCHandleType.Pinned);
try
{
return Marshal.PtrToStructure<T>(handle.AddrOfPinnedObject());
}
finally
{
handle.Free();
}
}
}
}

View file

@ -0,0 +1,20 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
[StructLayout(LayoutKind.Explicit)]
struct SoftwareKeyboardConfig
{
[FieldOffset(0)]
public SoftwareKeyboardType Type;
[FieldOffset(0x3AC)]
public uint StringLengthMax;
[FieldOffset(0x3B0)]
public uint StringLengthMaxExtended;
[FieldOffset(0x3D0), MarshalAs(UnmanagedType.I1)]
public bool CheckText;
}
}

View file

@ -0,0 +1,10 @@
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
internal enum SoftwareKeyboardState
{
Uninitialized,
Ready,
ValidationPending,
Complete
}
}

View file

@ -0,0 +1,9 @@
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
internal enum SoftwareKeyboardType : uint
{
Default = 0,
NumbersOnly = 1,
LettersOnly = 2
}
}

View file

@ -11,35 +11,50 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Lib
{
private IApplet _applet;
private AppletFifo<byte[]> _inData;
private AppletFifo<byte[]> _outData;
private AppletSession _normalSession;
private AppletSession _interactiveSession;
private KEvent _stateChangedEvent;
private KEvent _normalOutDataEvent;
private KEvent _interactiveOutDataEvent;
public ILibraryAppletAccessor(AppletId appletId, Horizon system)
{
_stateChangedEvent = new KEvent(system);
_normalOutDataEvent = new KEvent(system);
_interactiveOutDataEvent = new KEvent(system);
_applet = AppletManager.Create(appletId, system);
_inData = new AppletFifo<byte[]>();
_outData = new AppletFifo<byte[]>();
_normalSession = new AppletSession();
_interactiveSession = new AppletSession();
_applet.AppletStateChanged += OnAppletStateChanged;
_normalSession.DataAvailable += OnNormalOutData;
_interactiveSession.DataAvailable += OnInteractiveOutData;
Logger.PrintInfo(LogClass.ServiceAm, $"Applet '{appletId}' created.");
}
private void OnAppletStateChanged(object sender, EventArgs e)
{
_stateChangedEvent.ReadableEvent.Signal();
_stateChangedEvent.WritableEvent.Signal();
}
private void OnNormalOutData(object sender, EventArgs e)
{
_normalOutDataEvent.WritableEvent.Signal();
}
private void OnInteractiveOutData(object sender, EventArgs e)
{
_interactiveOutDataEvent.WritableEvent.Signal();
}
[Command(0)]
// GetAppletStateChangedEvent() -> handle<copy>
public ResultCode GetAppletStateChangedEvent(ServiceCtx context)
{
_stateChangedEvent.ReadableEvent.Signal();
if (context.Process.HandleTable.GenerateHandle(_stateChangedEvent.ReadableEvent, out int handle) != KernelResult.Success)
{
throw new InvalidOperationException("Out of handles!");
@ -47,6 +62,8 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Lib
context.Response.HandleDesc = IpcHandleDesc.MakeCopy(handle);
Logger.PrintInfo(LogClass.ServiceAm, $"GetAppletStateChangedEvent called.");
return ResultCode.Success;
}
@ -54,13 +71,18 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Lib
// Start()
public ResultCode Start(ServiceCtx context)
{
return (ResultCode)_applet.Start(_inData, _outData);
Logger.PrintInfo(LogClass.ServiceAm, $"Start called.");
return (ResultCode)_applet.Start(_normalSession.GetConsumer(),
_interactiveSession.GetConsumer());
}
[Command(30)]
// GetResult()
public ResultCode GetResult(ServiceCtx context)
{
Logger.PrintInfo(LogClass.ServiceAm, $"GetResult called.");
return (ResultCode)_applet.GetResult();
}
@ -70,7 +92,9 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Lib
{
IStorage data = GetObject<IStorage>(context, 0);
_inData.Push(data.Data);
_normalSession.Push(data.Data);
Logger.PrintInfo(LogClass.ServiceAm, $"PushInData called.");
return ResultCode.Success;
}
@ -79,10 +103,74 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Lib
// PopOutData() -> object<nn::am::service::IStorage>
public ResultCode PopOutData(ServiceCtx context)
{
byte[] data = _outData.Pop();
byte[] data = _normalSession.Pop();
MakeObject(context, new IStorage(data));
_normalOutDataEvent.WritableEvent.Clear();
Logger.PrintInfo(LogClass.ServiceAm, $"PopOutData called.");
return ResultCode.Success;
}
[Command(103)]
// PushInteractiveInData(object<nn::am::service::IStorage>)
public ResultCode PushInteractiveInData(ServiceCtx context)
{
IStorage data = GetObject<IStorage>(context, 0);
_interactiveSession.Push(data.Data);
Logger.PrintInfo(LogClass.ServiceAm, $"PushInteractiveInData called.");
return ResultCode.Success;
}
[Command(104)]
// PopInteractiveOutData() -> object<nn::am::service::IStorage>
public ResultCode PopInteractiveOutData(ServiceCtx context)
{
byte[] data = _interactiveSession.Pop();
MakeObject(context, new IStorage(data));
Logger.PrintInfo(LogClass.ServiceAm, $"PopInteractiveOutData called.");
_interactiveOutDataEvent.WritableEvent.Clear();
return ResultCode.Success;
}
[Command(105)]
// GetPopOutDataEvent() -> handle<copy>
public ResultCode GetPopOutDataEvent(ServiceCtx context)
{
if (context.Process.HandleTable.GenerateHandle(_normalOutDataEvent.ReadableEvent, out int handle) != KernelResult.Success)
{
throw new InvalidOperationException("Out of handles!");
}
context.Response.HandleDesc = IpcHandleDesc.MakeCopy(handle);
Logger.PrintInfo(LogClass.ServiceAm, $"GetPopOutDataEvent called.");
return ResultCode.Success;
}
[Command(106)]
// GetPopInteractiveOutDataEvent() -> handle<copy>
public ResultCode GetPopInteractiveOutDataEvent(ServiceCtx context)
{
if (context.Process.HandleTable.GenerateHandle(_interactiveOutDataEvent.ReadableEvent, out int handle) != KernelResult.Success)
{
throw new InvalidOperationException("Out of handles!");
}
context.Response.HandleDesc = IpcHandleDesc.MakeCopy(handle);
Logger.PrintInfo(LogClass.ServiceAm, $"GetPopInteractiveOutDataEvent called.");
return ResultCode.Success;
}
}

View file

@ -29,5 +29,19 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Sys
return ResultCode.Success;
}
[Command(11)]
// CreateTransferMemoryStorage(b8, u64, handle<copy>) -> object<nn::am::service::IStorage>
public ResultCode CreateTransferMemoryStorage(ServiceCtx context)
{
bool unknown = context.RequestData.ReadBoolean();
long size = context.RequestData.ReadInt64();
// NOTE: We don't support TransferMemory for now.
MakeObject(context, new IStorage(new byte[size]));
return ResultCode.Success;
}
}
}

View file

@ -2,14 +2,30 @@
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Ryujinx.HLE.HOS.Services.Am.AppletAE
{
internal class AppletFifo<T> : IEnumerable<T>
internal class AppletFifo<T> : IAppletFifo<T>
{
private ConcurrentQueue<T> _dataQueue;
public int Count => _dataQueue.Count;
public event EventHandler DataAvailable;
public bool IsSynchronized
{
get { return ((ICollection)_dataQueue).IsSynchronized; }
}
public object SyncRoot
{
get { return ((ICollection)_dataQueue).SyncRoot; }
}
public int Count
{
get { return _dataQueue.Count; }
}
public AppletFifo()
{
@ -19,6 +35,22 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE
public void Push(T item)
{
_dataQueue.Enqueue(item);
DataAvailable?.Invoke(this, null);
}
public bool TryAdd(T item)
{
try
{
this.Push(item);
return true;
}
catch
{
return false;
}
}
public T Pop()
@ -36,6 +68,11 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE
return _dataQueue.TryDequeue(out result);
}
public bool TryTake(out T item)
{
return this.TryPop(out item);
}
public T Peek()
{
if (_dataQueue.TryPeek(out T result))
@ -66,6 +103,11 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE
_dataQueue.CopyTo(array, arrayIndex);
}
public void CopyTo(Array array, int index)
{
this.CopyTo((T[])array, index);
}
public IEnumerator<T> GetEnumerator()
{
return _dataQueue.GetEnumerator();

View file

@ -0,0 +1,77 @@
using System;
namespace Ryujinx.HLE.HOS.Services.Am.AppletAE
{
internal class AppletSession
{
private IAppletFifo<byte[]> _inputData;
private IAppletFifo<byte[]> _outputData;
public event EventHandler DataAvailable;
public int Length
{
get { return _inputData.Count; }
}
public AppletSession()
: this(new AppletFifo<byte[]>(),
new AppletFifo<byte[]>())
{ }
public AppletSession(
IAppletFifo<byte[]> inputData,
IAppletFifo<byte[]> outputData)
{
_inputData = inputData;
_outputData = outputData;
_inputData.DataAvailable += OnDataAvailable;
}
private void OnDataAvailable(object sender, EventArgs e)
{
DataAvailable?.Invoke(this, null);
}
public void Push(byte[] item)
{
if (!this.TryPush(item))
{
// TODO(jduncanator): Throw a proper exception
throw new InvalidOperationException();
}
}
public bool TryPush(byte[] item)
{
return _outputData.TryAdd(item);
}
public byte[] Pop()
{
if (this.TryPop(out byte[] item))
{
return item;
}
throw new InvalidOperationException("Input data empty.");
}
public bool TryPop(out byte[] item)
{
return _inputData.TryTake(out item);
}
/// <summary>
/// This returns an AppletSession that can be used at the
/// other end of the pipe. Pushing data into this new session
/// will put it in the first session's input buffer, and vice
/// versa.
/// </summary>
public AppletSession GetConsumer()
{
return new AppletSession(this._outputData, this._inputData);
}
}
}

View file

@ -0,0 +1,10 @@
using System;
using System.Collections.Concurrent;
namespace Ryujinx.HLE.HOS.Services.Am.AppletAE
{
interface IAppletFifo<T> : IProducerConsumerCollection<T>
{
event EventHandler DataAvailable;
}
}

View file

@ -24,11 +24,17 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE
// Write(u64, buffer<bytes, 0x21>)
public ResultCode Write(ServiceCtx context)
{
// TODO: Error conditions.
long writePosition = context.RequestData.ReadInt64();
if (writePosition > _storage.Data.Length)
{
return ResultCode.OutOfBounds;
}
(long position, long size) = context.Request.GetBufferType0x21();
size = Math.Min(size, _storage.Data.Length - writePosition);
if (size > 0)
{
long maxSize = _storage.Data.Length - writePosition;
@ -50,23 +56,20 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE
// Read(u64) -> buffer<bytes, 0x22>
public ResultCode Read(ServiceCtx context)
{
// TODO: Error conditions.
long readPosition = context.RequestData.ReadInt64();
if (readPosition > _storage.Data.Length)
{
return ResultCode.OutOfBounds;
}
(long position, long size) = context.Request.GetBufferType0x22();
byte[] data;
size = Math.Min(size, _storage.Data.Length - readPosition);
if (_storage.Data.Length > size)
{
data = new byte[size];
byte[] data = new byte[size];
Buffer.BlockCopy(_storage.Data, 0, data, 0, (int)size);
}
else
{
data = _storage.Data;
}
Buffer.BlockCopy(_storage.Data, (int)readPosition, data, 0, (int)size);
context.Memory.WriteBytes(position, data);

View file

@ -9,6 +9,7 @@ namespace Ryujinx.HLE.HOS.Services.Am
NoMessages = (3 << ErrorCodeShift) | ModuleId,
ObjectInvalid = (500 << ErrorCodeShift) | ModuleId,
OutOfBounds = (503 << ErrorCodeShift) | ModuleId,
CpuBoostModeInvalid = (506 << ErrorCodeShift) | ModuleId
}
}