diff --git a/Ryujinx.Common/Configuration/ConfigurationFileFormat.cs b/Ryujinx.Common/Configuration/ConfigurationFileFormat.cs index 72cc579c78..79993d879f 100644 --- a/Ryujinx.Common/Configuration/ConfigurationFileFormat.cs +++ b/Ryujinx.Common/Configuration/ConfigurationFileFormat.cs @@ -227,9 +227,20 @@ namespace Ryujinx.Configuration /// Loads a configuration file from disk /// /// The path to the JSON configuration file - public static ConfigurationFileFormat Load(string path) + public static bool TryLoad(string path, out ConfigurationFileFormat configurationFileFormat) { - return JsonHelper.DeserializeFromFile(path); + try + { + configurationFileFormat = JsonHelper.DeserializeFromFile(path); + + return true; + } + catch + { + configurationFileFormat = null; + + return false; + } } /// @@ -238,7 +249,8 @@ namespace Ryujinx.Configuration /// The path to the JSON configuration file public void SaveConfig(string path) { - File.WriteAllText(path, JsonHelper.Serialize(this, true)); + using FileStream fileStream = File.Create(path, 4096, FileOptions.WriteThrough); + JsonHelper.Serialize(fileStream, this, true); } } } \ No newline at end of file diff --git a/Ryujinx.Graphics.Nvdec.Vp9/DecodeFrame.cs b/Ryujinx.Graphics.Nvdec.Vp9/DecodeFrame.cs index 9e267376fd..2963f7cf8c 100644 --- a/Ryujinx.Graphics.Nvdec.Vp9/DecodeFrame.cs +++ b/Ryujinx.Graphics.Nvdec.Vp9/DecodeFrame.cs @@ -1,13 +1,14 @@ using Ryujinx.Common.Memory; +using Ryujinx.Graphics.Nvdec.Vp9.Common; +using Ryujinx.Graphics.Nvdec.Vp9.Dsp; +using Ryujinx.Graphics.Nvdec.Vp9.Types; +using Ryujinx.Graphics.Video; using System; using System.Buffers.Binary; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using Ryujinx.Graphics.Nvdec.Vp9.Common; -using Ryujinx.Graphics.Nvdec.Vp9.Dsp; -using Ryujinx.Graphics.Nvdec.Vp9.Types; -using Ryujinx.Graphics.Video; +using System.Threading.Tasks; using Mv = Ryujinx.Graphics.Nvdec.Vp9.Types.Mv; namespace Ryujinx.Graphics.Nvdec.Vp9 @@ -1095,6 +1096,19 @@ namespace Ryujinx.Graphics.Nvdec.Vp9 data = data.Slice(size); } + private static void GetTileBuffers(ref Vp9Common cm, ArrayPtr data, int tileCols, ref Array64 tileBuffers) + { + int c; + + for (c = 0; c < tileCols; ++c) + { + bool isLast = c == tileCols - 1; + ref TileBuffer buf = ref tileBuffers[c]; + buf.Col = c; + GetTileBuffer(isLast, ref cm.Error, ref data, ref buf); + } + } + private static void GetTileBuffers( ref Vp9Common cm, ArrayPtr data, @@ -1181,5 +1195,163 @@ namespace Ryujinx.Graphics.Nvdec.Vp9 // Get last tile data. return cm.TileWorkerData[tileCols * tileRows - 1].BitReader.FindEnd(); } + + private static bool DecodeTileCol(ref TileWorkerData tileData, ref Vp9Common cm, ref Array64 tileBuffers) + { + ref TileInfo tile = ref tileData.Xd.Tile; + int finalCol = (1 << cm.Log2TileCols) - 1; + ArrayPtr bitReaderEnd = ArrayPtr.Null; + + int n = tileData.BufStart; + + tileData.Xd.Corrupted = false; + + do + { + ref TileBuffer buf = ref tileBuffers[n]; + + Debug.Assert(cm.Log2TileRows == 0); + tileData.Dqcoeff = new Array32>(); + tile.Init(ref cm, 0, buf.Col); + SetupTokenDecoder(buf.Data, buf.Size, ref tileData.ErrorInfo, ref tileData.BitReader); + cm.InitMacroBlockD(ref tileData.Xd, new ArrayPtr(ref tileData.Dqcoeff[0][0], 32 * 32)); + tileData.Xd.ErrorInfo = new Ptr(ref tileData.ErrorInfo); + + for (int miRow = tile.MiRowStart; miRow < tile.MiRowEnd; miRow += Constants.MiBlockSize) + { + tileData.Xd.LeftContext = new Array3>(); + tileData.Xd.LeftSegContext = new Array8(); + for (int miCol = tile.MiColStart; miCol < tile.MiColEnd; miCol += Constants.MiBlockSize) + { + DecodePartition(ref tileData, ref cm, miRow, miCol, BlockSize.Block64x64, 4); + } + } + + if (buf.Col == finalCol) + { + bitReaderEnd = tileData.BitReader.FindEnd(); + } + } while (!tileData.Xd.Corrupted && ++n <= tileData.BufEnd); + + tileData.DataEnd = bitReaderEnd; + return !tileData.Xd.Corrupted; + } + + public static unsafe ArrayPtr DecodeTilesMt(ref Vp9Common cm, ArrayPtr data, int maxThreads) + { + ArrayPtr bitReaderEnd = ArrayPtr.Null; + + int tileCols = 1 << cm.Log2TileCols; + int tileRows = 1 << cm.Log2TileRows; + int totalTiles = tileCols * tileRows; + int numWorkers = Math.Min(maxThreads, tileCols); + int n; + + Debug.Assert(tileCols <= (1 << 6)); + Debug.Assert(tileRows == 1); + + cm.AboveContext.ToSpan().Fill(0); + cm.AboveSegContext.ToSpan().Fill(0); + + for (n = 0; n < numWorkers; ++n) + { + ref TileWorkerData tileData = ref cm.TileWorkerData[n + totalTiles]; + + tileData.Xd = cm.Mb; + tileData.Xd.Counts = new Ptr(ref tileData.Counts); + tileData.Counts = new Vp9BackwardUpdates(); + } + + Array64 tileBuffers = new Array64(); + + GetTileBuffers(ref cm, data, tileCols, ref tileBuffers); + + tileBuffers.ToSpan().Slice(0, tileCols).Sort(CompareTileBuffers); + + if (numWorkers == tileCols) + { + TileBuffer largest = tileBuffers[0]; + Span buffers = tileBuffers.ToSpan(); + buffers.Slice(1).CopyTo(buffers.Slice(0, tileBuffers.Length - 1)); + tileBuffers[tileCols - 1] = largest; + } + else + { + int start = 0, end = tileCols - 2; + TileBuffer tmp; + + // Interleave the tiles to distribute the load between threads, assuming a + // larger tile implies it is more difficult to decode. + while (start < end) + { + tmp = tileBuffers[start]; + tileBuffers[start] = tileBuffers[end]; + tileBuffers[end] = tmp; + start += 2; + end -= 2; + } + } + + int baseVal = tileCols / numWorkers; + int remain = tileCols % numWorkers; + int bufStart = 0; + + for (n = 0; n < numWorkers; ++n) + { + int count = baseVal + (remain + n) / numWorkers; + ref TileWorkerData tileData = ref cm.TileWorkerData[n + totalTiles]; + + tileData.BufStart = bufStart; + tileData.BufEnd = bufStart + count - 1; + tileData.DataEnd = data.Slice(data.Length); + bufStart += count; + } + + Ptr cmPtr = new Ptr(ref cm); + + Parallel.For(0, numWorkers, (n) => + { + ref TileWorkerData tileData = ref cmPtr.Value.TileWorkerData[n + totalTiles]; + + if (!DecodeTileCol(ref tileData, ref cmPtr.Value, ref tileBuffers)) + { + cmPtr.Value.Mb.Corrupted = true; + } + }); + + for (; n > 0; --n) + { + if (bitReaderEnd.IsNull) + { + ref TileWorkerData tileData = ref cm.TileWorkerData[n - 1 + totalTiles]; + bitReaderEnd = tileData.DataEnd; + } + } + + for (n = 0; n < numWorkers; ++n) + { + ref TileWorkerData tileData = ref cm.TileWorkerData[n + totalTiles]; + AccumulateFrameCounts(ref cm.Counts.Value, ref tileData.Counts); + } + + Debug.Assert(!bitReaderEnd.IsNull || cm.Mb.Corrupted); + return bitReaderEnd; + } + + private static int CompareTileBuffers(TileBuffer bufA, TileBuffer bufB) + { + return (bufA.Size < bufB.Size ? 1 : 0) - (bufA.Size > bufB.Size ? 1 : 0); + } + + private static void AccumulateFrameCounts(ref Vp9BackwardUpdates accum, ref Vp9BackwardUpdates counts) + { + Span a = MemoryMarshal.Cast(MemoryMarshal.CreateSpan(ref accum, 1)); + Span c = MemoryMarshal.Cast(MemoryMarshal.CreateSpan(ref counts, 1)); + + for (int i = 0; i < a.Length; i++) + { + a[i] += c[i]; + } + } } } diff --git a/Ryujinx.Graphics.Nvdec.Vp9/Decoder.cs b/Ryujinx.Graphics.Nvdec.Vp9/Decoder.cs index ff4221acfc..f82ca76101 100644 --- a/Ryujinx.Graphics.Nvdec.Vp9/Decoder.cs +++ b/Ryujinx.Graphics.Nvdec.Vp9/Decoder.cs @@ -92,7 +92,14 @@ namespace Ryujinx.Graphics.Nvdec.Vp9 cm.Mb.SetupBlockPlanes(1, 1); - cm.AllocTileWorkerData(_allocator, 1 << pictureInfo.Log2TileCols, 1 << pictureInfo.Log2TileRows); + int tileCols = 1 << pictureInfo.Log2TileCols; + int tileRows = 1 << pictureInfo.Log2TileRows; + + // Video usually have only 4 columns, so more threads won't make a difference for those. + // Try to not take all CPU cores for video decoding. + int maxThreads = Math.Min(4, Environment.ProcessorCount / 2); + + cm.AllocTileWorkerData(_allocator, tileCols, tileRows, maxThreads); cm.AllocContextBuffers(_allocator, output.Width, output.Height); cm.InitContextBuffers(); cm.SetupSegmentationDequant(); @@ -104,7 +111,14 @@ namespace Ryujinx.Graphics.Nvdec.Vp9 { try { - DecodeFrame.DecodeTiles(ref cm, new ArrayPtr(dataPtr, bitstream.Length)); + if (maxThreads > 1 && tileRows == 1 && tileCols > 1) + { + DecodeFrame.DecodeTilesMt(ref cm, new ArrayPtr(dataPtr, bitstream.Length), maxThreads); + } + else + { + DecodeFrame.DecodeTiles(ref cm, new ArrayPtr(dataPtr, bitstream.Length)); + } } catch (InternalErrorException) { diff --git a/Ryujinx.Graphics.Nvdec.Vp9/Dsp/InvTxfm.cs b/Ryujinx.Graphics.Nvdec.Vp9/Dsp/InvTxfm.cs index b4ad4344eb..e41a31cac1 100644 --- a/Ryujinx.Graphics.Nvdec.Vp9/Dsp/InvTxfm.cs +++ b/Ryujinx.Graphics.Nvdec.Vp9/Dsp/InvTxfm.cs @@ -87,6 +87,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp return rv; } + [SkipLocalsInit] public static void Iwht4x416Add(ReadOnlySpan input, Span dest, int stride) { /* 4-point reversible, orthonormal inverse Walsh-Hadamard in 3.5 adds, @@ -142,6 +143,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp } } + [SkipLocalsInit] public static void Iwht4x41Add(ReadOnlySpan input, Span dest, int stride) { int i; @@ -209,6 +211,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp output[3] = WrapLow(DctConstRoundShift(s0 + s1 - s3)); } + [SkipLocalsInit] public static void Idct4(ReadOnlySpan input, Span output) { Span step = stackalloc short[4]; @@ -231,6 +234,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp output[3] = WrapLow(step[0] - step[3]); } + [SkipLocalsInit] public static void Idct4x416Add(ReadOnlySpan input, Span dest, int stride) { int i, j; @@ -359,6 +363,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp output[7] = WrapLow(-x1); } + [SkipLocalsInit] public static void Idct8(ReadOnlySpan input, Span output) { Span step1 = stackalloc short[8]; @@ -416,6 +421,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp output[7] = WrapLow(step1[0] - step1[7]); } + [SkipLocalsInit] public static void Idct8x864Add(ReadOnlySpan input, Span dest, int stride) { int i, j; @@ -449,6 +455,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp } } + [SkipLocalsInit] public static void Idct8x812Add(ReadOnlySpan input, Span dest, int stride) { int i, j; @@ -457,6 +464,8 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp Span tempIn = stackalloc int[8]; Span tempOut = stackalloc int[8]; + output.Fill(0); + // First transform rows // Only first 4 row has non-zero coefs for (i = 0; i < 4; ++i) @@ -671,6 +680,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp output[15] = WrapLow(-x1); } + [SkipLocalsInit] public static void Idct16(ReadOnlySpan input, Span output) { Span step1 = stackalloc short[16]; @@ -838,6 +848,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp output[15] = WrapLow(step2[0] - step2[15]); } + [SkipLocalsInit] public static void Idct16x16256Add(ReadOnlySpan input, Span dest, int stride) { int i, j; @@ -870,6 +881,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp } } + [SkipLocalsInit] public static void Idct16x1638Add(ReadOnlySpan input, Span dest, int stride) { int i, j; @@ -878,6 +890,8 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp Span tempIn = stackalloc int[16]; Span tempOut = stackalloc int[16]; + output.Fill(0); + // First transform rows. Since all non-zero dct coefficients are in // upper-left 8x8 area, we only need to calculate first 8 rows here. for (i = 0; i < 8; ++i) @@ -903,6 +917,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp } } + [SkipLocalsInit] public static void Idct16x1610Add(ReadOnlySpan input, Span dest, int stride) { int i, j; @@ -911,6 +926,8 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp Span tempIn = stackalloc int[16]; Span tempOut = stackalloc int[16]; + output.Fill(0); + // First transform rows. Since all non-zero dct coefficients are in // upper-left 4x4 area, we only need to calculate first 4 rows here. for (i = 0; i < 4; ++i) @@ -955,6 +972,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp } } + [SkipLocalsInit] public static void Idct32(ReadOnlySpan input, Span output) { Span step1 = stackalloc short[32]; @@ -1324,6 +1342,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp output[31] = WrapLow(step1[0] - step1[31]); } + [SkipLocalsInit] public static void Idct32x321024Add(ReadOnlySpan input, Span dest, int stride) { int i, j; @@ -1370,6 +1389,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp } } + [SkipLocalsInit] public static void Idct32x32135Add(ReadOnlySpan input, Span dest, int stride) { int i, j; @@ -1378,6 +1398,8 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp Span tempIn = stackalloc int[32]; Span tempOut = stackalloc int[32]; + output.Fill(0); + // Rows // Only upper-left 16x16 has non-zero coeff for (i = 0; i < 16; ++i) @@ -1403,6 +1425,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp } } + [SkipLocalsInit] public static void Idct32x3234Add(ReadOnlySpan input, Span dest, int stride) { int i, j; @@ -1411,6 +1434,8 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp Span tempIn = stackalloc int[32]; Span tempOut = stackalloc int[32]; + output.Fill(0); + // Rows // Only upper-left 8x8 has non-zero coeff for (i = 0; i < 8; ++i) @@ -1456,6 +1481,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp } } + [SkipLocalsInit] public static void HighbdIwht4x416Add(ReadOnlySpan input, Span dest, int stride, int bd) { /* 4-point reversible, orthonormal inverse Walsh-Hadamard in 3.5 adds, @@ -1511,6 +1537,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp } } + [SkipLocalsInit] public static void HighbdIwht4x41Add(ReadOnlySpan input, Span dest, int stride, int bd) { int i; @@ -1584,6 +1611,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp output[3] = HighbdWrapLow(DctConstRoundShift(s0 + s1 - s3), bd); } + [SkipLocalsInit] public static void HighbdIdct4(ReadOnlySpan input, Span output, int bd) { Span step = stackalloc int[4]; @@ -1613,6 +1641,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp output[3] = HighbdWrapLow(step[0] - step[3], bd); } + [SkipLocalsInit] public static void HighbdIdct4x416Add(ReadOnlySpan input, Span dest, int stride, int bd) { int i, j; @@ -1748,6 +1777,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp output[7] = HighbdWrapLow(-x1, bd); } + [SkipLocalsInit] public static void HighbdIdct8(ReadOnlySpan input, Span output, int bd) { Span step1 = stackalloc int[8]; @@ -1803,6 +1833,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp output[7] = HighbdWrapLow(step1[0] - step1[7], bd); } + [SkipLocalsInit] public static void HighbdIdct8x864Add(ReadOnlySpan input, Span dest, int stride, int bd) { int i, j; @@ -1835,6 +1866,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp } } + [SkipLocalsInit] public static void HighbdIdct8x812Add(ReadOnlySpan input, Span dest, int stride, int bd) { int i, j; @@ -1843,6 +1875,8 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp Span tempIn = stackalloc int[8]; Span tempOut = stackalloc int[8]; + output.Fill(0); + // First transform rows // Only first 4 row has non-zero coefs for (i = 0; i < 4; ++i) @@ -2062,6 +2096,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp output[15] = HighbdWrapLow(-x1, bd); } + [SkipLocalsInit] public static void HighbdIdct16(ReadOnlySpan input, Span output, int bd) { Span step1 = stackalloc int[16]; @@ -2236,6 +2271,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp output[15] = HighbdWrapLow(step2[0] - step2[15], bd); } + [SkipLocalsInit] public static void HighbdIdct16x16256Add(ReadOnlySpan input, Span dest, int stride, int bd) { int i, j; @@ -2268,6 +2304,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp } } + [SkipLocalsInit] public static void HighbdIdct16x1638Add(ReadOnlySpan input, Span dest, int stride, int bd) { int i, j; @@ -2276,6 +2313,8 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp Span tempIn = stackalloc int[16]; Span tempOut = stackalloc int[16]; + output.Fill(0); + // First transform rows. Since all non-zero dct coefficients are in // upper-left 8x8 area, we only need to calculate first 8 rows here. for (i = 0; i < 8; ++i) @@ -2303,6 +2342,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp } } + [SkipLocalsInit] public static void HighbdIdct16x1610Add(ReadOnlySpan input, Span dest, int stride, int bd) { int i, j; @@ -2311,6 +2351,8 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp Span tempIn = stackalloc int[16]; Span tempOut = stackalloc int[16]; + output.Fill(0); + // First transform rows. Since all non-zero dct coefficients are in // upper-left 4x4 area, we only need to calculate first 4 rows here. for (i = 0; i < 4; ++i) @@ -2355,6 +2397,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp } } + [SkipLocalsInit] public static void HighbdIdct32(ReadOnlySpan input, Span output, int bd) { Span step1 = stackalloc int[32]; @@ -2539,7 +2582,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp step2[8] = step1[8]; step2[15] = step1[15]; temp1 = -step1[9] * (long)CosPi8_64 + step1[14] * (long)CosPi24_64; - temp2 = step1[9] * (long)CosPi24_64 + step1[14] * (long)CosPi8_64; + temp2 = step1[9] * (long)CosPi24_64 + step1[14] * (long)CosPi8_64; step2[9] = HighbdWrapLow(DctConstRoundShift(temp1), bd); step2[14] = HighbdWrapLow(DctConstRoundShift(temp2), bd); temp1 = -step1[10] * (long)CosPi24_64 - step1[13] * (long)CosPi8_64; @@ -2731,6 +2774,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp output[31] = HighbdWrapLow(step1[0] - step1[31], bd); } + [SkipLocalsInit] public static void HighbdIdct32x321024Add(ReadOnlySpan input, Span dest, int stride, int bd) { int i, j; @@ -2777,6 +2821,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp } } + [SkipLocalsInit] public static void HighbdIdct32x32135Add(ReadOnlySpan input, Span dest, int stride, int bd) { int i, j; @@ -2785,6 +2830,8 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp Span tempIn = stackalloc int[32]; Span tempOut = stackalloc int[32]; + output.Fill(0); + // Rows // Only upper-left 16x16 has non-zero coeff for (i = 0; i < 16; ++i) @@ -2812,6 +2859,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp } } + [SkipLocalsInit] public static void HighbdIdct32x3234Add(ReadOnlySpan input, Span dest, int stride, int bd) { int i, j; @@ -2820,6 +2868,8 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Dsp Span tempIn = stackalloc int[32]; Span tempOut = stackalloc int[32]; + output.Fill(0); + // Rows // Only upper-left 8x8 has non-zero coeff for (i = 0; i < 8; ++i) diff --git a/Ryujinx.Graphics.Nvdec.Vp9/TileBuffer.cs b/Ryujinx.Graphics.Nvdec.Vp9/TileBuffer.cs index 3b60889bf1..c5a25e6bc8 100644 --- a/Ryujinx.Graphics.Nvdec.Vp9/TileBuffer.cs +++ b/Ryujinx.Graphics.Nvdec.Vp9/TileBuffer.cs @@ -4,6 +4,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9 { internal struct TileBuffer { + public int Col; public ArrayPtr Data; public int Size; } diff --git a/Ryujinx.Graphics.Nvdec.Vp9/TileWorkerData.cs b/Ryujinx.Graphics.Nvdec.Vp9/TileWorkerData.cs index 4055727420..333a077a0e 100644 --- a/Ryujinx.Graphics.Nvdec.Vp9/TileWorkerData.cs +++ b/Ryujinx.Graphics.Nvdec.Vp9/TileWorkerData.cs @@ -1,14 +1,20 @@ using Ryujinx.Common.Memory; using Ryujinx.Graphics.Nvdec.Vp9.Dsp; using Ryujinx.Graphics.Nvdec.Vp9.Types; +using Ryujinx.Graphics.Video; namespace Ryujinx.Graphics.Nvdec.Vp9 { internal struct TileWorkerData { + public ArrayPtr DataEnd; + public int BufStart; + public int BufEnd; public Reader BitReader; + public Vp9BackwardUpdates Counts; public MacroBlockD Xd; /* dqcoeff are shared by all the planes. So planes must be decoded serially */ public Array32> Dqcoeff; + public InternalErrorInfo ErrorInfo; } } diff --git a/Ryujinx.Graphics.Nvdec.Vp9/Types/Vp9Common.cs b/Ryujinx.Graphics.Nvdec.Vp9/Types/Vp9Common.cs index 4ca059542d..faadd3498c 100644 --- a/Ryujinx.Graphics.Nvdec.Vp9/Types/Vp9Common.cs +++ b/Ryujinx.Graphics.Nvdec.Vp9/Types/Vp9Common.cs @@ -127,9 +127,9 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Types MBs = MbRows * MbCols; } - public void AllocTileWorkerData(MemoryAllocator allocator, int tileCols, int tileRows) + public void AllocTileWorkerData(MemoryAllocator allocator, int tileCols, int tileRows, int maxThreads) { - TileWorkerData = allocator.Allocate(tileCols * tileRows); + TileWorkerData = allocator.Allocate(tileCols * tileRows + (maxThreads > 1 ? maxThreads : 0)); } public void FreeTileWorkerData(MemoryAllocator allocator) diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardRequest.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardRequest.cs index d0024001e2..b17debfc75 100644 --- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardRequest.cs +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardRequest.cs @@ -1,18 +1,48 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard { /// - /// Possible requests to the keyboard when running in inline mode. + /// Possible requests to the software keyboard when running in inline mode. /// enum InlineKeyboardRequest : uint { - Unknown0 = 0x0, - Finalize = 0x4, - SetUserWordInfo = 0x6, - SetCustomizeDic = 0x7, - Calc = 0xA, - SetCustomizedDictionaries = 0xB, + /// + /// Finalize the keyboard applet. + /// + Finalize = 0x4, + + /// + /// Set user words for text prediction. + /// + SetUserWordInfo = 0x6, + + /// + /// Sets the CustomizeDic data. Can't be used if CustomizedDictionaries is already set. + /// + SetCustomizeDic = 0x7, + + /// + /// Configure the keyboard applet and put it in a state where it is processing input. + /// + Calc = 0xA, + + /// + /// Set custom dictionaries for text prediction. Can't be used if SetCustomizeDic is already set. + /// + SetCustomizedDictionaries = 0xB, + + /// + /// Release custom dictionaries data. + /// UnsetCustomizedDictionaries = 0xC, - UseChangedStringV2 = 0xD, - UseMovedCursorV2 = 0xE + + /// + /// [8.0.0+] Request the keyboard applet to use the ChangedStringV2 response when notifying changes in text data. + /// + UseChangedStringV2 = 0xD, + + /// + /// [8.0.0+] Request the keyboard applet to use the MovedCursorV2 response when notifying changes in cursor position. + /// + UseMovedCursorV2 = 0xE } } diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardResponse.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardResponse.cs index 61908b7bfb..b21db507cb 100644 --- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardResponse.cs +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardResponse.cs @@ -1,26 +1,93 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard { /// - /// Possible responses from the keyboard when running in inline mode. + /// Possible responses from the software keyboard when running in inline mode. /// 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, + /// + /// The software keyboard received a Calc and it is fully initialized. Reply data is ignored by the user-process. + /// + FinishedInitialize = 0x0, + + /// + /// Default response. Official sw has no handling for this besides just closing the storage. + /// + Default = 0x1, + + /// + /// The text data in the software keyboard changed (UTF-16 encoding). + /// + ChangedString = 0x2, + + /// + /// The cursor position in the software keyboard changed (UTF-16 encoding). + /// + MovedCursor = 0x3, + + /// + /// A tab in the software keyboard changed. + /// + MovedTab = 0x4, + + /// + /// The OK key was pressed in the software keyboard, confirming the input text (UTF-16 encoding). + /// + DecidedEnter = 0x5, + + /// + /// The Cancel key was pressed in the software keyboard, cancelling the input. + /// + DecidedCancel = 0x6, + + /// + /// Same as ChangedString, but with UTF-8 encoding. + /// + ChangedStringUtf8 = 0x7, + + /// + /// Same as MovedCursor, but with UTF-8 encoding. + /// + MovedCursorUtf8 = 0x8, + + /// + /// Same as DecidedEnter, but with UTF-8 encoding. + /// + DecidedEnterUtf8 = 0x9, + + /// + /// They software keyboard is releasing the data previously set by a SetCustomizeDic request. + /// + UnsetCustomizeDic = 0xA, + + /// + /// They software keyboard is releasing the data previously set by a SetUserWordInfo request. + /// + ReleasedUserWordInfo = 0xB, + + /// + /// They software keyboard is releasing the data previously set by a SetCustomizedDictionaries request. + /// UnsetCustomizedDictionaries = 0xC, - ChangedStringV2 = 0xD, - MovedCursorV2 = 0xE, - ChangedStringUtf8V2 = 0xF, - MovedCursorUtf8V2 = 0x10 + + /// + /// Same as ChangedString, but with additional fields. + /// + ChangedStringV2 = 0xD, + + /// + /// Same as MovedCursor, but with additional fields. + /// + MovedCursorV2 = 0xE, + + /// + /// Same as ChangedStringUtf8, but with additional fields. + /// + ChangedStringUtf8V2 = 0xF, + + /// + /// Same as MovedCursorUtf8, but with additional fields. + /// + MovedCursorUtf8V2 = 0x10 } } diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardState.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardState.cs index 2940d1614d..024ff2cf4d 100644 --- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardState.cs +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardState.cs @@ -1,14 +1,33 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard { /// - /// Possible states for the keyboard when running in inline mode. + /// Possible states for the software keyboard when running in inline mode. /// enum InlineKeyboardState : uint { + /// + /// The software keyboard has just been created or finalized and is uninitialized. + /// Uninitialized = 0x0, - Initializing = 0x1, - Ready = 0x2, + + /// + /// A Calc was previously received and fulfilled, so the software keyboard is initialized, but is not processing input. + /// + Initialized = 0x1, + + /// + /// A Calc was received and the software keyboard is processing input. + /// + Ready = 0x2, + + /// + /// New text data or cursor position of the software keyboard are available. + /// DataAvailable = 0x3, - Completed = 0x4 + + /// + /// The Calc request was fulfilled with either a text input or a cancel. + /// + Complete = 0x4 } } diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineResponses.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineResponses.cs index 60cc528775..50e77b7421 100644 --- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineResponses.cs +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineResponses.cs @@ -45,41 +45,41 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard writer.Write(cursor); // Cursor position } - public static byte[] FinishedInitialize() + public static byte[] FinishedInitialize(InlineKeyboardState state) { 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); + BeginResponse(state, InlineKeyboardResponse.FinishedInitialize, writer); writer.Write((byte)1); // Data (ignored by the program) return stream.ToArray(); } } - public static byte[] Default() + public static byte[] Default(InlineKeyboardState state) { 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); + BeginResponse(state, InlineKeyboardResponse.Default, writer); return stream.ToArray(); } } - public static byte[] ChangedString(string text) + public static byte[] ChangedString(string text, InlineKeyboardState state) { - uint resSize = 4 * sizeof(uint) + MaxStrLenUTF16; + uint resSize = 6 * sizeof(uint) + MaxStrLenUTF16; using (MemoryStream stream = new MemoryStream(new byte[resSize])) using (BinaryWriter writer = new BinaryWriter(stream)) { - BeginResponse(InlineKeyboardState.Initializing, InlineKeyboardResponse.ChangedString, writer); + BeginResponse(state, InlineKeyboardResponse.ChangedString, writer); WriteStringWithCursor(text, writer, MaxStrLenUTF16, Encoding.Unicode); writer.Write((int)0); // ? writer.Write((int)0); // ? @@ -88,21 +88,21 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard } } - public static byte[] MovedCursor(string text) + public static byte[] MovedCursor(string text, InlineKeyboardState state) { 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); + BeginResponse(state, InlineKeyboardResponse.MovedCursor, writer); WriteStringWithCursor(text, writer, MaxStrLenUTF16, Encoding.Unicode); return stream.ToArray(); } } - public static byte[] MovedTab(string text) + public static byte[] MovedTab(string text, InlineKeyboardState state) { // Should be the same as MovedCursor. @@ -111,48 +111,48 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard using (MemoryStream stream = new MemoryStream(new byte[resSize])) using (BinaryWriter writer = new BinaryWriter(stream)) { - BeginResponse(InlineKeyboardState.Initializing, InlineKeyboardResponse.MovedTab, writer); + BeginResponse(state, InlineKeyboardResponse.MovedTab, writer); WriteStringWithCursor(text, writer, MaxStrLenUTF16, Encoding.Unicode); return stream.ToArray(); } } - public static byte[] DecidedEnter(string text) + public static byte[] DecidedEnter(string text, InlineKeyboardState state) { 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); + BeginResponse(state, InlineKeyboardResponse.DecidedEnter, writer); WriteString(text, writer, MaxStrLenUTF16, Encoding.Unicode); return stream.ToArray(); } } - public static byte[] DecidedCancel() + public static byte[] DecidedCancel(InlineKeyboardState state) { 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); + BeginResponse(state, InlineKeyboardResponse.DecidedCancel, writer); return stream.ToArray(); } } - public static byte[] ChangedStringUtf8(string text) + public static byte[] ChangedStringUtf8(string text, InlineKeyboardState state) { 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); + BeginResponse(state, InlineKeyboardResponse.ChangedStringUtf8, writer); WriteStringWithCursor(text, writer, MaxStrLenUTF8, Encoding.UTF8); writer.Write((int)0); // ? writer.Write((int)0); // ? @@ -161,81 +161,81 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard } } - public static byte[] MovedCursorUtf8(string text) + public static byte[] MovedCursorUtf8(string text, InlineKeyboardState state) { 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); + BeginResponse(state, InlineKeyboardResponse.MovedCursorUtf8, writer); WriteStringWithCursor(text, writer, MaxStrLenUTF8, Encoding.UTF8); return stream.ToArray(); } } - public static byte[] DecidedEnterUtf8(string text) + public static byte[] DecidedEnterUtf8(string text, InlineKeyboardState state) { 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); + BeginResponse(state, InlineKeyboardResponse.DecidedEnterUtf8, writer); WriteString(text, writer, MaxStrLenUTF8, Encoding.UTF8); return stream.ToArray(); } } - public static byte[] UnsetCustomizeDic() + public static byte[] UnsetCustomizeDic(InlineKeyboardState state) { 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); + BeginResponse(state, InlineKeyboardResponse.UnsetCustomizeDic, writer); return stream.ToArray(); } } - public static byte[] ReleasedUserWordInfo() + public static byte[] ReleasedUserWordInfo(InlineKeyboardState state) { 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); + BeginResponse(state, InlineKeyboardResponse.ReleasedUserWordInfo, writer); return stream.ToArray(); } } - public static byte[] UnsetCustomizedDictionaries() + public static byte[] UnsetCustomizedDictionaries(InlineKeyboardState state) { 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); + BeginResponse(state, InlineKeyboardResponse.UnsetCustomizedDictionaries, writer); return stream.ToArray(); } } - public static byte[] ChangedStringV2(string text) + public static byte[] ChangedStringV2(string text, InlineKeyboardState state) { 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); + BeginResponse(state, InlineKeyboardResponse.ChangedStringV2, writer); WriteStringWithCursor(text, writer, MaxStrLenUTF16, Encoding.Unicode); writer.Write((int)0); // ? writer.Write((int)0); // ? @@ -245,14 +245,14 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard } } - public static byte[] MovedCursorV2(string text) + public static byte[] MovedCursorV2(string text, InlineKeyboardState state) { 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); + BeginResponse(state, InlineKeyboardResponse.MovedCursorV2, writer); WriteStringWithCursor(text, writer, MaxStrLenUTF16, Encoding.Unicode); writer.Write((byte)0); // Flag == 0 @@ -260,14 +260,14 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard } } - public static byte[] ChangedStringUtf8V2(string text) + public static byte[] ChangedStringUtf8V2(string text, InlineKeyboardState state) { 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); + BeginResponse(state, InlineKeyboardResponse.ChangedStringUtf8V2, writer); WriteStringWithCursor(text, writer, MaxStrLenUTF8, Encoding.UTF8); writer.Write((int)0); // ? writer.Write((int)0); // ? @@ -277,14 +277,14 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard } } - public static byte[] MovedCursorUtf8V2(string text) + public static byte[] MovedCursorUtf8V2(string text, InlineKeyboardState state) { 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); + BeginResponse(state, InlineKeyboardResponse.MovedCursorUtf8V2, writer); WriteStringWithCursor(text, writer, MaxStrLenUTF8, Encoding.UTF8); writer.Write((byte)0); // Flag == 0 diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardAppear.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardAppear.cs index 23e8bd1fe5..262dd4df80 100644 --- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardAppear.cs +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardAppear.cs @@ -2,33 +2,38 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard { + /// + /// A structure with appearance configurations for the software keyboard when running in inline mode. + /// [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. + /// + /// 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; + /// + /// The string displayed in the Submit button. + /// [MarshalAs(UnmanagedType.ByValTStr, SizeConst = OkTextLength + 1)] public string OkText; /// /// The character displayed in the left button of the numeric keyboard. - /// This is ignored when Mode is not set to NumbersOnly. /// public char LeftOptionalSymbolKey; /// /// The character displayed in the right button of the numeric keyboard. - /// This is ignored when Mode is not set to NumbersOnly. /// public char RightOptionalSymbolKey; /// - /// When set, predictive typing is enabled making use of the system dictionary, - /// and any custom user dictionary. + /// When set, predictive typing is enabled making use of the system dictionary, and any custom user dictionary. /// [MarshalAs(UnmanagedType.I1)] public bool PredictionEnabled; @@ -43,13 +48,24 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard public int Padding1; public int Padding2; - public byte EnableReturnButton; + /// + /// Indicates the return button is enabled in the keyboard. This allows for input with multiple lines. + /// + [MarshalAs(UnmanagedType.I1)] + public bool UseNewLine; + + /// + /// [10.0.0+] If value is 1 or 2, then keytopAsFloating=0 and footerScalable=1 in Calc. + /// + public byte Unknown1; - public byte Padding3; public byte Padding4; public byte Padding5; - public uint CalcArgFlags; + /// + /// Bitmask 0x1000 of the Calc and DirectionalButtonAssignEnabled in bitmask 0x10000000. + /// + public uint CalcFlags; public uint Padding6; public uint Padding7; diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs index 89ed559256..4f43472de4 100644 --- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs @@ -1,4 +1,5 @@ -using Ryujinx.Common.Logging; +using Ryujinx.Common; +using Ryujinx.Common.Logging; using Ryujinx.HLE.HOS.Applets.SoftwareKeyboard; using Ryujinx.HLE.HOS.Services.Am.AppletAE; using System; @@ -14,31 +15,41 @@ namespace Ryujinx.HLE.HOS.Applets { private const string DefaultText = "Ryujinx"; + private const long DebounceTimeMillis = 200; + private const int ResetDelayMillis = 500; + private readonly Switch _device; private const int StandardBufferSize = 0x7D8; private const int InteractiveBufferSize = 0x7D4; + private const int MaxUserWords = 0x1388; - private SoftwareKeyboardState _state = SoftwareKeyboardState.Uninitialized; + private SoftwareKeyboardState _foregroundState = SoftwareKeyboardState.Uninitialized; + private volatile InlineKeyboardState _backgroundState = InlineKeyboardState.Uninitialized; private bool _isBackground = false; + private bool _alreadyShown = false; + private volatile bool _useChangedStringV2 = false; private AppletSession _normalSession; private AppletSession _interactiveSession; - // Configuration for foreground mode - private SoftwareKeyboardConfig _keyboardFgConfig; - private SoftwareKeyboardCalc _keyboardCalc; - private SoftwareKeyboardDictSet _keyboardDict; + // Configuration for foreground mode. + private SoftwareKeyboardConfig _keyboardForegroundConfig; - // Configuration for background mode - private SoftwareKeyboardInitialize _keyboardBgInitialize; + // Configuration for background (inline) mode. + private SoftwareKeyboardInitialize _keyboardBackgroundInitialize; + private SoftwareKeyboardCalc _keyboardBackgroundCalc; + private SoftwareKeyboardCustomizeDic _keyboardBackgroundDic; + private SoftwareKeyboardDictSet _keyboardBackgroundDictSet; + private SoftwareKeyboardUserWord[] _keyboardBackgroundUserWords; private byte[] _transferMemory; - private string _textValue = null; + private string _textValue = ""; private bool _okPressed = false; private Encoding _encoding = Encoding.Unicode; + private long _lastTextSetMillis = 0; public event EventHandler AppletStateChanged; @@ -55,22 +66,27 @@ namespace Ryujinx.HLE.HOS.Applets _interactiveSession.DataAvailable += OnInteractiveData; + _alreadyShown = false; + _useChangedStringV2 = false; + var launchParams = _normalSession.Pop(); var keyboardConfig = _normalSession.Pop(); - // TODO: A better way would be handling the background creation properly - // in LibraryAppleCreator / Acessor instead of guessing by size. if (keyboardConfig.Length == Marshal.SizeOf()) { + // Initialize the keyboard applet in background mode. + _isBackground = true; - _keyboardBgInitialize = ReadStruct(keyboardConfig); - _state = SoftwareKeyboardState.Uninitialized; + _keyboardBackgroundInitialize = ReadStruct(keyboardConfig); + _backgroundState = InlineKeyboardState.Uninitialized; return ResultCode.Success; } else { + // Initialize the keyboard applet in foreground mode. + _isBackground = false; if (keyboardConfig.Length < Marshal.SizeOf()) @@ -79,7 +95,7 @@ namespace Ryujinx.HLE.HOS.Applets } else { - _keyboardFgConfig = ReadStruct(keyboardConfig); + _keyboardForegroundConfig = ReadStruct(keyboardConfig); } if (!_normalSession.TryPop(out _transferMemory)) @@ -87,12 +103,12 @@ namespace Ryujinx.HLE.HOS.Applets Logger.Error?.Print(LogClass.ServiceAm, "SwKbd Transfer Memory is null"); } - if (_keyboardFgConfig.UseUtf8) + if (_keyboardForegroundConfig.UseUtf8) { _encoding = Encoding.UTF8; } - _state = SoftwareKeyboardState.Ready; + _foregroundState = SoftwareKeyboardState.Ready; ExecuteForegroundKeyboard(); @@ -105,32 +121,44 @@ namespace Ryujinx.HLE.HOS.Applets return ResultCode.Success; } + private InlineKeyboardState GetInlineState() + { + return _backgroundState; + } + + private void SetInlineState(InlineKeyboardState state) + { + _backgroundState = state; + } + 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 && _keyboardFgConfig.InitialStringLength > 0) + if (_transferMemory != null && _keyboardForegroundConfig.InitialStringLength > 0) { - initialText = Encoding.Unicode.GetString(_transferMemory, _keyboardFgConfig.InitialStringOffset, 2 * _keyboardFgConfig.InitialStringLength); + initialText = Encoding.Unicode.GetString(_transferMemory, _keyboardForegroundConfig.InitialStringOffset, + 2 * _keyboardForegroundConfig.InitialStringLength); } // If the max string length is 0, we set it to a large default // length. - if (_keyboardFgConfig.StringLengthMax == 0) + if (_keyboardForegroundConfig.StringLengthMax == 0) { - _keyboardFgConfig.StringLengthMax = 100; + _keyboardForegroundConfig.StringLengthMax = 100; } var args = new SoftwareKeyboardUiArgs { - HeaderText = _keyboardFgConfig.HeaderText, - SubtitleText = _keyboardFgConfig.SubtitleText, - GuideText = _keyboardFgConfig.GuideText, - SubmitText = (!string.IsNullOrWhiteSpace(_keyboardFgConfig.SubmitText) ? _keyboardFgConfig.SubmitText : "OK"), - StringLengthMin = _keyboardFgConfig.StringLengthMin, - StringLengthMax = _keyboardFgConfig.StringLengthMax, + HeaderText = _keyboardForegroundConfig.HeaderText, + SubtitleText = _keyboardForegroundConfig.SubtitleText, + GuideText = _keyboardForegroundConfig.GuideText, + SubmitText = (!string.IsNullOrWhiteSpace(_keyboardForegroundConfig.SubmitText) ? + _keyboardForegroundConfig.SubmitText : "OK"), + StringLengthMin = _keyboardForegroundConfig.StringLengthMin, + StringLengthMax = _keyboardForegroundConfig.StringLengthMax, InitialText = initialText }; @@ -151,26 +179,26 @@ namespace Ryujinx.HLE.HOS.Applets // than our default text, repeat our default text until we meet // the minimum length requirement. // This should always be done before the text truncation step. - while (_textValue.Length < _keyboardFgConfig.StringLengthMin) + while (_textValue.Length < _keyboardForegroundConfig.StringLengthMin) { _textValue = String.Join(" ", _textValue, _textValue); } // If our default text is longer than the allowed length, // we truncate it. - if (_textValue.Length > _keyboardFgConfig.StringLengthMax) + if (_textValue.Length > _keyboardForegroundConfig.StringLengthMax) { - _textValue = _textValue.Substring(0, (int)_keyboardFgConfig.StringLengthMax); + _textValue = _textValue.Substring(0, (int)_keyboardForegroundConfig.StringLengthMax); } // Does the application want to validate the text itself? - if (_keyboardFgConfig.CheckText) + if (_keyboardForegroundConfig.CheckText) { // 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; + _foregroundState = SoftwareKeyboardState.ValidationPending; _interactiveSession.Push(BuildResponse(_textValue, true)); } @@ -179,7 +207,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. - _state = SoftwareKeyboardState.Complete; + _foregroundState = SoftwareKeyboardState.Complete; _normalSession.Push(BuildResponse(_textValue, false)); @@ -204,7 +232,7 @@ namespace Ryujinx.HLE.HOS.Applets private void OnForegroundInteractiveData(byte[] data) { - if (_state == SoftwareKeyboardState.ValidationPending) + if (_foregroundState == SoftwareKeyboardState.ValidationPending) { // TODO(jduncantor): // If application rejects our "attempt", submit another attempt, @@ -216,9 +244,9 @@ namespace Ryujinx.HLE.HOS.Applets AppletStateChanged?.Invoke(this, null); - _state = SoftwareKeyboardState.Complete; + _foregroundState = SoftwareKeyboardState.Complete; } - else if(_state == SoftwareKeyboardState.Complete) + else if(_foregroundState == SoftwareKeyboardState.Complete) { // If we have already completed, we push the result text // back on the output buffer and poll the application. @@ -242,142 +270,278 @@ namespace Ryujinx.HLE.HOS.Applets using (MemoryStream stream = new MemoryStream(data)) using (BinaryReader reader = new BinaryReader(stream)) { - var request = (InlineKeyboardRequest)reader.ReadUInt32(); - + InlineKeyboardRequest request = (InlineKeyboardRequest)reader.ReadUInt32(); + InlineKeyboardState state = GetInlineState(); long remaining; - // Always show the keyboard if the state is 'Ready'. - bool showKeyboard = _state == SoftwareKeyboardState.Ready; + Logger.Debug?.Print(LogClass.ServiceAm, $"Keyboard received command {request} in state {state}"); 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()); + _useChangedStringV2 = true; break; case InlineKeyboardRequest.UseMovedCursorV2: - // Not used because we only send the entire string after confirmation. - _interactiveSession.Push(InlineResponses.Default()); + // Not used because we only reply with the final string. + break; + case InlineKeyboardRequest.SetUserWordInfo: + // Read the user word info data. + remaining = stream.Length - stream.Position; + if (remaining < sizeof(int)) + { + Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard User Word Info of {remaining} bytes"); + } + else + { + int wordsCount = reader.ReadInt32(); + int wordSize = Marshal.SizeOf(); + remaining = stream.Length - stream.Position; + + if (wordsCount > MaxUserWords) + { + Logger.Warning?.Print(LogClass.ServiceAm, $"Received {wordsCount} User Words but the maximum is {MaxUserWords}"); + } + else if (wordsCount * wordSize != remaining) + { + Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard User Word Info data of {remaining} bytes for {wordsCount} words"); + } + else + { + _keyboardBackgroundUserWords = new SoftwareKeyboardUserWord[wordsCount]; + + for (int word = 0; word < wordsCount; word++) + { + byte[] wordData = reader.ReadBytes(wordSize); + _keyboardBackgroundUserWords[word] = ReadStruct(wordData); + } + } + } + _interactiveSession.Push(InlineResponses.ReleasedUserWordInfo(state)); break; case InlineKeyboardRequest.SetCustomizeDic: + // Read the custom dic data. + remaining = stream.Length - stream.Position; + if (remaining != Marshal.SizeOf()) + { + Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard Customize Dic of {remaining} bytes"); + } + else + { + var keyboardDicData = reader.ReadBytes((int)remaining); + _keyboardBackgroundDic = ReadStruct(keyboardDicData); + } + _interactiveSession.Push(InlineResponses.UnsetCustomizeDic(state)); + break; + case InlineKeyboardRequest.SetCustomizedDictionaries: + // Read the custom dictionaries data. remaining = stream.Length - stream.Position; if (remaining != Marshal.SizeOf()) { - Logger.Error?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard DictSet of {remaining} bytes!"); + Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard DictSet of {remaining} bytes"); } else { var keyboardDictData = reader.ReadBytes((int)remaining); - _keyboardDict = ReadStruct(keyboardDictData); + _keyboardBackgroundDictSet = ReadStruct(keyboardDictData); } - _interactiveSession.Push(InlineResponses.Default()); + _interactiveSession.Push(InlineResponses.UnsetCustomizedDictionaries(state)); break; case InlineKeyboardRequest.Calc: - // Put the keyboard in a Ready state, this will force showing - _state = SoftwareKeyboardState.Ready; + // The Calc request tells the Applet to enter the main input handling loop, which will end + // with either a text being submitted or a cancel request from the user. + + // NOTE: Some Calc requests happen early in the process and are not meant to be shown. This possibly + // happens because the game has complete control over when the inline keyboard is drawn, but here it + // would cause a dialog to pop in the emulator, which is inconvenient. An algorithm is applied to + // decide whether it is a dummy Calc or not, but regardless of the result, the dummy Calc appears to + // never happen twice, so the keyboard will always show if it has already been shown before. + bool forceShowKeyboard = _alreadyShown; + _alreadyShown = true; + + // Read the Calc data. remaining = stream.Length - stream.Position; if (remaining != Marshal.SizeOf()) { - Logger.Error?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard Calc of {remaining} bytes!"); + Logger.Error?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard Calc of {remaining} bytes"); } else { var keyboardCalcData = reader.ReadBytes((int)remaining); - _keyboardCalc = ReadStruct(keyboardCalcData); + _keyboardBackgroundCalc = ReadStruct(keyboardCalcData); - if (_keyboardCalc.Utf8Mode == 0x1) + // Check if the application expects UTF8 encoding instead of UTF16. + if (_keyboardBackgroundCalc.UseUtf8) { _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) + if (_keyboardBackgroundCalc.Appear.ShouldBeHidden == 0) { - showKeyboard = true; + forceShowKeyboard = true; } } // Send an initialization finished signal. - _interactiveSession.Push(InlineResponses.FinishedInitialize()); + state = InlineKeyboardState.Ready; + SetInlineState(state); + _interactiveSession.Push(InlineResponses.FinishedInitialize(state)); // 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(); + new Task(() => { GetInputTextAndSend(forceShowKeyboard, state); }).Start(); break; case InlineKeyboardRequest.Finalize: - // The game wants to close the keyboard applet and will wait for a state change. - _state = SoftwareKeyboardState.Uninitialized; + // The calling process wants to close the keyboard applet and will wait for a state change. + _backgroundState = InlineKeyboardState.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()); + Logger.Warning?.Print(LogClass.ServiceAm, $"Invalid Software Keyboard request {request} during state {_backgroundState}"); + _interactiveSession.Push(InlineResponses.Default(state)); break; } } } + private void GetInputTextAndSend(bool forceShowKeyboard, InlineKeyboardState oldState) + { + bool submit = true; + + // Use the text specified by the Calc if it is available, otherwise use the default one. + string inputText = (!string.IsNullOrWhiteSpace(_keyboardBackgroundCalc.InputText) ? + _keyboardBackgroundCalc.InputText : DefaultText); + + // Compute the elapsed time for the debouncing algorithm. + long currentMillis = PerformanceCounter.ElapsedMilliseconds; + long inputElapsedMillis = currentMillis - _lastTextSetMillis; + + // Reset the input text before submitting the final result, that's because some games do not expect + // consecutive submissions to abruptly shrink and they will crash if it happens. Changing the string + // before the final submission prevents that. + InlineKeyboardState newState = InlineKeyboardState.DataAvailable; + SetInlineState(newState); + ChangedString("", newState); + + if (inputElapsedMillis < DebounceTimeMillis) + { + // A repeated Calc request has been received without player interaction, after the input has been + // sent. This behavior happens in some games, so instead of showing another dialog, just apply a + // time-based debouncing algorithm and repeat the last submission, either a value or a cancel. + inputText = _textValue; + submit = _textValue != null; + + Logger.Warning?.Print(LogClass.Application, "Debouncing repeated keyboard request"); + } + else if (!forceShowKeyboard) + { + // 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. + 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 + { + // Call the configured GUI handler to get user's input. + var args = new SoftwareKeyboardUiArgs + { + HeaderText = "", // The inline keyboard lacks these texts + SubtitleText = "", + GuideText = "", + SubmitText = (!string.IsNullOrWhiteSpace(_keyboardBackgroundCalc.Appear.OkText) ? + _keyboardBackgroundCalc.Appear.OkText : "OK"), + StringLengthMin = 0, + StringLengthMax = 100, + InitialText = inputText + }; + + submit = _device.UiHandler.DisplayInputDialog(args, out inputText); + inputText = submit ? inputText : null; + } + + // The 'Complete' state indicates the Calc request has been fulfilled by the applet. + newState = InlineKeyboardState.Complete; + + if (submit) + { + Logger.Debug?.Print(LogClass.ServiceAm, "Sending keyboard OK"); + DecidedEnter(inputText, newState); + } + else + { + Logger.Debug?.Print(LogClass.ServiceAm, "Sending keyboard Cancel"); + DecidedCancel(newState); + } + + _interactiveSession.Push(InlineResponses.Default(newState)); + + // The constant calls to PopInteractiveData suggest that the keyboard applet continuously reports + // data back to the application and this can also be time-sensitive. Pushing a state reset right + // after the data has been sent does not work properly and the application will soft-lock. This + // delay gives time for the application to catch up with the data and properly process the state + // reset. + Thread.Sleep(ResetDelayMillis); + + // 'Initialized' is the only known state so far that does not soft-lock the keyboard after use. + newState = InlineKeyboardState.Initialized; + + Logger.Debug?.Print(LogClass.ServiceAm, $"Resetting state of the keyboard to {newState}"); + + SetInlineState(newState); + _interactiveSession.Push(InlineResponses.Default(newState)); + + // Keep the text and the timestamp of the input for the debouncing algorithm. + _textValue = inputText; + _lastTextSetMillis = PerformanceCounter.ElapsedMilliseconds; + } + + private void ChangedString(string text, InlineKeyboardState state) + { + if (_encoding == Encoding.UTF8) + { + if (_useChangedStringV2) + { + _interactiveSession.Push(InlineResponses.ChangedStringUtf8V2(text, state)); + } + else + { + _interactiveSession.Push(InlineResponses.ChangedStringUtf8(text, state)); + } + } + else + { + if (_useChangedStringV2) + { + _interactiveSession.Push(InlineResponses.ChangedStringV2(text, state)); + } + else + { + _interactiveSession.Push(InlineResponses.ChangedString(text, state)); + } + } + } + + private void DecidedEnter(string text, InlineKeyboardState state) + { + if (_encoding == Encoding.UTF8) + { + _interactiveSession.Push(InlineResponses.DecidedEnterUtf8(text, state)); + } + else + { + _interactiveSession.Push(InlineResponses.DecidedEnter(text, state)); + } + } + + private void DecidedCancel(InlineKeyboardState state) + { + _interactiveSession.Push(InlineResponses.DecidedCancel(state)); + } + private byte[] BuildResponse(string text, bool interactive) { int bufferSize = interactive ? InteractiveBufferSize : StandardBufferSize; diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCalc.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCalc.cs index 6213c2e4f0..a80690c3d4 100644 --- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCalc.cs +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCalc.cs @@ -3,7 +3,7 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard { /// - /// A structure that defines the configuration options of the software keyboard. + /// A structure with configuration options of the software keyboard when starting a new input request in inline mode. /// [StructLayout(LayoutKind.Sequential, Pack=1, CharSet = CharSet.Unicode)] struct SoftwareKeyboardCalc @@ -12,28 +12,56 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard public uint Unknown; + /// + /// The size of the Calc struct, as reported by the process communicating with the applet. + /// public ushort Size; public byte Unknown1; public byte Unknown2; + /// + /// Configuration flags. Their purpose is currently unknown. + /// public ulong Flags; + /// + /// The original parameters used when initializing the keyboard applet. + /// public SoftwareKeyboardInitialize Initialize; + /// + /// The audio volume used by the sound effects of the keyboard. + /// public float Volume; + /// + /// The initial position of the text cursor (caret) in the provided input text. + /// public int CursorPos; + /// + /// Appearance configurations for the on-screen keyboard. + /// public SoftwareKeyboardAppear Appear; + /// + /// The initial input text to be used by the software keyboard. + /// [MarshalAs(UnmanagedType.ByValTStr, SizeConst = InputTextLength + 1)] public string InputText; - public byte Utf8Mode; + /// + /// When set, the strings communicated by software keyboard will be encoded as UTF-8 instead of UTF-16. + /// + [MarshalAs(UnmanagedType.I1)] + public bool UseUtf8; public byte Unknown3; + /// + /// [5.0.0+] Enable the backspace key in the software keyboard. + /// [MarshalAs(UnmanagedType.I1)] public bool BackspaceEnabled; @@ -41,32 +69,57 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard public byte Unknown5; [MarshalAs(UnmanagedType.I1)] - public byte KeytopAsFloating; + public bool KeytopAsFloating; [MarshalAs(UnmanagedType.I1)] - public byte FooterScalable; + public bool FooterScalable; [MarshalAs(UnmanagedType.I1)] - public byte AlphaEnabledInInputMode; + public bool AlphaEnabledInInputMode; - [MarshalAs(UnmanagedType.I1)] public byte InputModeFadeType; + /// + /// When set, the software keyboard ignores touch input. + /// [MarshalAs(UnmanagedType.I1)] - public byte TouchDisabled; + public bool TouchDisabled; + /// + /// When set, the software keyboard ignores hardware keyboard commands. + /// [MarshalAs(UnmanagedType.I1)] - public byte HardwareKeyboardDisabled; + public bool HardwareKeyboardDisabled; public uint Unknown6; public uint Unknown7; + /// + /// Default value is 1.0. + /// public float KeytopScale0; + + /// + /// Default value is 1.0. + /// public float KeytopScale1; + public float KeytopTranslate0; public float KeytopTranslate1; + + /// + /// Default value is 1.0. + /// public float KeytopBgAlpha; + + /// + /// Default value is 1.0. + /// public float FooterBgAlpha; + + /// + /// Default value is 1.0. + /// public float BalloonScale; public float Unknown8; @@ -74,9 +127,19 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard public uint Unknown10; public uint Unknown11; + /// + /// [5.0.0+] Enable sound effect. + /// public byte SeGroup; + /// + /// [6.0.0+] Enables the Trigger field when Trigger is non-zero. + /// public byte TriggerFlag; + + /// + /// [6.0.0+] Always set to zero. + /// public byte Trigger; public byte Padding; diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCustomizeDic.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCustomizeDic.cs new file mode 100644 index 0000000000..538fb9274b --- /dev/null +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCustomizeDic.cs @@ -0,0 +1,14 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// + /// A structure used by SetCustomizeDic request to software keyboard. + /// + [StructLayout(LayoutKind.Sequential, Pack = 4)] + struct SoftwareKeyboardCustomizeDic + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 112)] + public byte[] Unknown; + } +} diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardDictSet.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardDictSet.cs index 1abdc15b24..b4ffdb9082 100644 --- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardDictSet.cs +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardDictSet.cs @@ -2,10 +2,33 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard { - [StructLayout(LayoutKind.Sequential, Pack = 4)] + /// + /// A structure with custom dictionary words for the software keyboard. + /// + [StructLayout(LayoutKind.Sequential, Pack = 2)] struct SoftwareKeyboardDictSet { - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 28)] - public uint[] Entries; + /// + /// A 0x1000-byte aligned buffer position. + /// + public ulong BufferPosition; + + /// + /// A 0x1000-byte aligned buffer size. + /// + public uint BufferSize; + + /// + /// Array of word entries in the buffer. + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 24)] + public ulong[] Entries; + + /// + /// Number of used entries in the Entries field. + /// + public ushort TotalEntries; + + public ushort Padding1; } } diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardInitialize.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardInitialize.cs index 28e3df7f59..764d0e38fd 100644 --- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardInitialize.cs +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardInitialize.cs @@ -3,14 +3,23 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard { /// - /// A structure that indicates the initialization the inline software keyboard. + /// A structure that mirrors the parameters used to initialize the keyboard applet. /// [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] struct SoftwareKeyboardInitialize { public uint Unknown; + + /// + /// The applet mode used when launching the swkb. The bits regarding the background vs foreground mode can be wrong. + /// public byte LibMode; + + /// + /// [5.0.0+] Set to 0x1 to indicate a firmware version >= 5.0.0. + /// public byte FivePlus; + public byte Padding1; public byte Padding2; } diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUserWord.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUserWord.cs new file mode 100644 index 0000000000..08f1c3d33a --- /dev/null +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUserWord.cs @@ -0,0 +1,14 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// + /// A structure used by SetUserWordInfo request to the software keyboard. + /// + [StructLayout(LayoutKind.Sequential, Pack = 4)] + struct SoftwareKeyboardUserWord + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 100)] + public byte[] Unknown; + } +} diff --git a/Ryujinx.HLE/HOS/Services/Prepo/IPrepoService.cs b/Ryujinx.HLE/HOS/Services/Prepo/IPrepoService.cs index 283128fe86..5454e9327d 100644 --- a/Ryujinx.HLE/HOS/Services/Prepo/IPrepoService.cs +++ b/Ryujinx.HLE/HOS/Services/Prepo/IPrepoService.cs @@ -5,16 +5,25 @@ using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; using Ryujinx.HLE.HOS.Services.Account.Acc; using Ryujinx.HLE.Utilities; +using System; using System.Text; namespace Ryujinx.HLE.HOS.Services.Prepo { - [Service("prepo:a")] - [Service("prepo:a2")] - [Service("prepo:u")] + [Service("prepo:a", PrepoServicePermissionLevel.Admin)] // 1.0.0-5.1.0 + [Service("prepo:a2", PrepoServicePermissionLevel.Admin)] // 6.0.0+ + [Service("prepo:m", PrepoServicePermissionLevel.Manager)] + [Service("prepo:u", PrepoServicePermissionLevel.User)] + [Service("prepo:s", PrepoServicePermissionLevel.System)] class IPrepoService : IpcService { - public IPrepoService(ServiceCtx context) { } + private PrepoServicePermissionLevel _permission; + private ulong _systemSessionId; + + public IPrepoService(ServiceCtx context, PrepoServicePermissionLevel permission) + { + _permission = permission; + } [Command(10100)] // 1.0.0-5.1.0 [Command(10102)] // 6.0.0-9.2.0 @@ -22,6 +31,11 @@ namespace Ryujinx.HLE.HOS.Services.Prepo // SaveReport(u64, pid, buffer, buffer) public ResultCode SaveReport(ServiceCtx context) { + if (((int)_permission & 1) == 0) + { + return ResultCode.PermissionDenied; + } + // We don't care about the differences since we don't use the play report. return ProcessReport(context, withUserID: false); } @@ -32,6 +46,11 @@ namespace Ryujinx.HLE.HOS.Services.Prepo // SaveReportWithUser(nn::account::Uid, u64, pid, buffer, buffer) public ResultCode SaveReportWithUser(ServiceCtx context) { + if (((int)_permission & 1) == 0) + { + return ResultCode.PermissionDenied; + } + // We don't care about the differences since we don't use the play report. return ProcessReport(context, withUserID: true); } @@ -57,6 +76,29 @@ namespace Ryujinx.HLE.HOS.Services.Prepo return ResultCode.Success; } + [Command(10400)] // 9.0.0+ + // GetSystemSessionId() -> u64 + public ResultCode GetSystemSessionId(ServiceCtx context) + { + if (((int)_permission & 1) == 0) + { + return ResultCode.PermissionDenied; + } + + if (_systemSessionId == 0) + { + byte[] randomBuffer = new byte[8]; + + new Random().NextBytes(randomBuffer); + + _systemSessionId = BitConverter.ToUInt64(randomBuffer, 0); + } + + context.ResponseData.Write(_systemSessionId); + + return ResultCode.Success; + } + private ResultCode ProcessReport(ServiceCtx context, bool withUserID) { UserId userId = withUserID ? context.RequestData.ReadStruct() : new UserId(); diff --git a/Ryujinx.HLE/HOS/Services/Prepo/ResultCode.cs b/Ryujinx.HLE/HOS/Services/Prepo/ResultCode.cs index 1e110ea68d..3199e27055 100644 --- a/Ryujinx.HLE/HOS/Services/Prepo/ResultCode.cs +++ b/Ryujinx.HLE/HOS/Services/Prepo/ResultCode.cs @@ -7,9 +7,9 @@ namespace Ryujinx.HLE.HOS.Services.Prepo Success = 0, - InvalidArgument = (1 << ErrorCodeShift) | ModuleId, - InvalidState = (5 << ErrorCodeShift) | ModuleId, - InvalidBufferSize = (9 << ErrorCodeShift) | ModuleId, - Unknown1 = (90 << ErrorCodeShift) | ModuleId + InvalidArgument = (1 << ErrorCodeShift) | ModuleId, + InvalidState = (5 << ErrorCodeShift) | ModuleId, + InvalidBufferSize = (9 << ErrorCodeShift) | ModuleId, + PermissionDenied = (90 << ErrorCodeShift) | ModuleId } } \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Services/Prepo/Types/PrepoServicePermissionLevel.cs b/Ryujinx.HLE/HOS/Services/Prepo/Types/PrepoServicePermissionLevel.cs new file mode 100644 index 0000000000..624c3fde37 --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Prepo/Types/PrepoServicePermissionLevel.cs @@ -0,0 +1,12 @@ +using System; + +namespace Ryujinx.HLE.HOS.Services.Prepo +{ + enum PrepoServicePermissionLevel + { + Admin = -1, + User = 1, + System = 2, + Manager = 6 + } +} \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Services/Settings/ISystemSettingsServer.cs b/Ryujinx.HLE/HOS/Services/Settings/ISystemSettingsServer.cs index c41e6618a8..d19a534548 100644 --- a/Ryujinx.HLE/HOS/Services/Settings/ISystemSettingsServer.cs +++ b/Ryujinx.HLE/HOS/Services/Settings/ISystemSettingsServer.cs @@ -220,6 +220,18 @@ namespace Ryujinx.HLE.HOS.Services.Settings return ResultCode.Success; } + [Command(60)] + // IsUserSystemClockAutomaticCorrectionEnabled() -> bool + public ResultCode IsUserSystemClockAutomaticCorrectionEnabled(ServiceCtx context) + { + // NOTE: When set to true, is automatically synced with the internet. + context.ResponseData.Write(true); + + Logger.Stub?.PrintStub(LogClass.ServiceSet, "Stubbed"); + + return ResultCode.Success; + } + public byte[] GetFirmwareData(Switch device) { long titleId = 0x0100000000000809; @@ -267,4 +279,4 @@ namespace Ryujinx.HLE.HOS.Services.Settings } } } -} \ No newline at end of file +} diff --git a/Ryujinx/Program.cs b/Ryujinx/Program.cs index 29043bb8ee..c6d3b1bd14 100644 --- a/Ryujinx/Program.cs +++ b/Ryujinx/Program.cs @@ -89,30 +89,32 @@ namespace Ryujinx string localConfigurationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config.json"); string appDataConfigurationPath = Path.Combine(AppDataManager.BaseDirPath, "Config.json"); - // Now load the configuration as the other subsystems are now registered. - if (File.Exists(localConfigurationPath)) + // Now load the configuration as the other subsystems are now registered + ConfigurationPath = File.Exists(localConfigurationPath) + ? localConfigurationPath + : File.Exists(appDataConfigurationPath) + ? appDataConfigurationPath + : null; + + if (ConfigurationPath == null) { - ConfigurationPath = localConfigurationPath; - - ConfigurationFileFormat configurationFileFormat = ConfigurationFileFormat.Load(localConfigurationPath); - - ConfigurationState.Instance.Load(configurationFileFormat, ConfigurationPath); - } - else if (File.Exists(appDataConfigurationPath)) - { - ConfigurationPath = appDataConfigurationPath; - - ConfigurationFileFormat configurationFileFormat = ConfigurationFileFormat.Load(appDataConfigurationPath); - - ConfigurationState.Instance.Load(configurationFileFormat, ConfigurationPath); - } - else - { - // No configuration, we load the default values and save it on disk. + // No configuration, we load the default values and save it to disk ConfigurationPath = appDataConfigurationPath; ConfigurationState.Instance.LoadDefault(); - ConfigurationState.Instance.ToFileFormat().SaveConfig(appDataConfigurationPath); + ConfigurationState.Instance.ToFileFormat().SaveConfig(ConfigurationPath); + } + else + { + if (ConfigurationFileFormat.TryLoad(ConfigurationPath, out ConfigurationFileFormat configurationFileFormat)) + { + ConfigurationState.Instance.Load(configurationFileFormat, ConfigurationPath); + } + else + { + ConfigurationState.Instance.LoadDefault(); + Logger.Warning?.PrintMsg(LogClass.Application, $"Failed to load config! Loading the default config instead.\nFailed config location {ConfigurationPath}"); + } } if (startFullscreenArg) @@ -120,7 +122,7 @@ namespace Ryujinx ConfigurationState.Instance.Ui.StartFullscreen.Value = true; } - // Logging system informations. + // Logging system information. PrintSystemInfo(); // Initialize Gtk. diff --git a/Ryujinx/Ui/MainWindow.cs b/Ryujinx/Ui/MainWindow.cs index 7697376bd5..92a2b4f1ca 100644 --- a/Ryujinx/Ui/MainWindow.cs +++ b/Ryujinx/Ui/MainWindow.cs @@ -157,6 +157,17 @@ namespace Ryujinx.Ui if (ConfigurationState.Instance.Ui.GuiColumns.FileSizeColumn) _fileSizeToggle.Active = true; if (ConfigurationState.Instance.Ui.GuiColumns.PathColumn) _pathToggle.Active = true; + _favToggle.Toggled += Fav_Toggled; + _iconToggle.Toggled += Icon_Toggled; + _appToggle.Toggled += App_Toggled; + _developerToggle.Toggled += Developer_Toggled; + _versionToggle.Toggled += Version_Toggled; + _timePlayedToggle.Toggled += TimePlayed_Toggled; + _lastPlayedToggle.Toggled += LastPlayed_Toggled; + _fileExtToggle.Toggled += FileExt_Toggled; + _fileSizeToggle.Toggled += FileSize_Toggled; + _pathToggle.Toggled += Path_Toggled; + _gameTable.Model = _tableStore = new ListStore( typeof(bool), typeof(Gdk.Pixbuf), @@ -1142,7 +1153,7 @@ namespace Ryujinx.Ui UpdateColumns(); } - private void Title_Toggled(object sender, EventArgs args) + private void App_Toggled(object sender, EventArgs args) { ConfigurationState.Instance.Ui.GuiColumns.AppColumn.Value = _appToggle.Active; diff --git a/Ryujinx/Ui/MainWindow.glade b/Ryujinx/Ui/MainWindow.glade index 6cefe08aa3..688f0e2553 100644 --- a/Ryujinx/Ui/MainWindow.glade +++ b/Ryujinx/Ui/MainWindow.glade @@ -171,7 +171,6 @@ Enable or Disable Favorite Games Column in the game list Enable Favorite Games Column True - @@ -181,7 +180,6 @@ Enable or Disable Icon Column in the game list Enable Icon Column True - @@ -191,7 +189,6 @@ Enable or Disable Title Name/ID Column in the game list Enable Title Name/ID Column True - @@ -201,7 +198,6 @@ Enable or Disable Developer Column in the game list Enable Developer Column True - @@ -211,7 +207,6 @@ Enable or Disable Version Column in the game list Enable Version Column True - @@ -221,7 +216,6 @@ Enable or Disable Time Played Column in the game list Enable Time Played Column True - @@ -231,7 +225,6 @@ Enable or Disable Last Played Column in the game list Enable Last Played Column True - @@ -241,7 +234,6 @@ Enable or Disable file extension column in the game list Enable File Ext Column True - @@ -251,7 +243,6 @@ Enable or Disable File Size Column in the game list Enable File Size Column True - @@ -261,7 +252,6 @@ Enable or Disable Path Column in the game list Enable Path Column True - diff --git a/Ryujinx/Ui/Resources/Controller_JoyConLeft.svg b/Ryujinx/Ui/Resources/Controller_JoyConLeft.svg index c3b82f8a26..e60253510f 100644 --- a/Ryujinx/Ui/Resources/Controller_JoyConLeft.svg +++ b/Ryujinx/Ui/Resources/Controller_JoyConLeft.svg @@ -11,14 +11,15 @@ id="Layer_1" x="0px" y="0px" - viewBox="0 0 1000.8 1000" - style="enable-background:new 0 0 1000.8 1000;" + viewBox="0 0 1000.8 1300" xml:space="preserve" - sodipodi:docname="JoyConLeft.svg" - inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)">image/svg+xml + inkscape:current-layer="g344" + inkscape:document-rotation="0" /> + id="path1502" /> diff --git a/Ryujinx/Ui/Resources/Controller_JoyConPair.svg b/Ryujinx/Ui/Resources/Controller_JoyConPair.svg index cf0f2bb09a..fe6a9f813b 100644 --- a/Ryujinx/Ui/Resources/Controller_JoyConPair.svg +++ b/Ryujinx/Ui/Resources/Controller_JoyConPair.svg @@ -11,15 +11,46 @@ id="Layer_1" x="0px" y="0px" - viewBox="0 0 1000.8 1000" - style="enable-background:new 0 0 1000.8 1000;" + viewBox="0 0 999.99996 1300" xml:space="preserve" - sodipodi:docname="JoyConPair.svg" - inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)">image/svg+xml + inkscape:current-layer="layer1" + inkscape:document-rotation="0" + showguides="true" + inkscape:guide-bbox="true" + scale-x="1.7" /> - - - - - - - - - - - - - - - - - - - - - - @@ -158,16 +93,29 @@ - - + + + + + + + + + + + + + + + + + + + + + + + @@ -182,295 +130,566 @@ - - - - - - - - - - - - - - + id="path1502" /> diff --git a/Ryujinx/Ui/Resources/Controller_JoyConRight.svg b/Ryujinx/Ui/Resources/Controller_JoyConRight.svg index aa64330c9e..309fb37685 100644 --- a/Ryujinx/Ui/Resources/Controller_JoyConRight.svg +++ b/Ryujinx/Ui/Resources/Controller_JoyConRight.svg @@ -11,14 +11,15 @@ id="Layer_1" x="0px" y="0px" - viewBox="0 0 1000.8 1000" - style="enable-background:new 0 0 1000.8 1000;" + viewBox="0 0 1000.8 1300" xml:space="preserve" - sodipodi:docname="JoyConRight.svg" - inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)">image/svg+xml + inkscape:current-layer="g315" + inkscape:document-rotation="0" /> + id="path1532" /> diff --git a/Ryujinx/Ui/Windows/ControllerWindow.cs b/Ryujinx/Ui/Windows/ControllerWindow.cs index a5345b0354..e06c6c6be3 100644 --- a/Ryujinx/Ui/Windows/ControllerWindow.cs +++ b/Ryujinx/Ui/Windows/ControllerWindow.cs @@ -254,9 +254,9 @@ namespace Ryujinx.Ui.Windows _controllerImage.Pixbuf = _controllerType.ActiveId switch { "ProController" => new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.Resources.Controller_ProCon.svg", 400, 400), - "JoyconLeft" => new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.Resources.Controller_JoyConLeft.svg", 400, 400), - "JoyconRight" => new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.Resources.Controller_JoyConRight.svg", 400, 400), - _ => new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.Resources.Controller_JoyConPair.svg", 400, 400), + "JoyconLeft" => new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.Resources.Controller_JoyConLeft.svg", 400, 500), + "JoyconRight" => new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.Resources.Controller_JoyConRight.svg", 400, 500), + _ => new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.Resources.Controller_JoyConPair.svg", 400, 500), }; } diff --git a/global.json b/global.json index 2cb2ac9bd8..d129334e9b 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,6 @@ { "sdk": { - "version": "5.0.100" + "version": "5.0.100", + "rollForward": "latestFeature" } } \ No newline at end of file