diff --git a/Directory.Packages.props b/Directory.Packages.props
index 78f9acd59..1389b58a6 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -40,7 +40,7 @@
-
+
diff --git a/assets/locales.json b/assets/locales.json
index bbc30563a..cac65dd80 100644
--- a/assets/locales.json
+++ b/assets/locales.json
@@ -3629,7 +3629,7 @@
"he_IL": "ממשק קלאסי (הפעלה מחדש דרושה)",
"it_IT": "Interfaccia classica (Riavvio necessario)",
"ja_JP": "クラシックインターフェース(再起動必要)",
- "ko_KR": "클래식 인터페이스 (재시작 필요)",
+ "ko_KR": "클래식 인터페이스(다시 시작 필요)",
"no_NO": "Klassisk grensesnitt (Krever omstart)",
"pl_PL": "Klasyczny interfejs (Wymaga restartu)",
"pt_BR": "Interface Clássica (Reinício necessário)",
diff --git a/docs/compatibility.csv b/docs/compatibility.csv
index a75a50f42..2efd1d0d5 100644
--- a/docs/compatibility.csv
+++ b/docs/compatibility.csv
@@ -2280,6 +2280,7 @@
01008F6008C5E000,"Pokémon™ Violet",gpu;nvdec;ldn-works;amd-vendor-bug;mac-bug,ingame,2024-07-30 02:51:48
0100187003A36000,"Pokémon™: Let’s Go, Eevee!",crash;nvdec;online-broken;ldn-broken,ingame,2024-06-01 15:03:04
010003F003A34000,"Pokémon™: Let’s Go, Pikachu!",crash;nvdec;online-broken;ldn-broken,ingame,2024-03-15 07:55:41
+0100F43008C44000,"Pokémon Legends: Z-A",gpu;crash;ldn-broken,ingame,2025-10-16 19:13:00
0100B3F000BE2000,"Pokkén Tournament™ DX",nvdec;ldn-works;opengl-backend-bug;LAN;amd-vendor-bug;intel-vendor-bug,playable,2024-07-18 23:11:08
010030D005AE6000,"Pokkén Tournament™ DX Demo",demo;opengl-backend-bug,playable,2022-08-10 12:03:19
0100A3500B4EC000,"Polandball: Can Into Space",,playable,2020-06-25 15:13:26
diff --git a/src/ARMeilleure/Instructions/NativeInterface.cs b/src/ARMeilleure/Instructions/NativeInterface.cs
index d43e20d83..3c2d9bb33 100644
--- a/src/ARMeilleure/Instructions/NativeInterface.cs
+++ b/src/ARMeilleure/Instructions/NativeInterface.cs
@@ -201,11 +201,7 @@ namespace ARMeilleure.Instructions
ExecutionContext context = GetContext();
- // If debugging, we'll handle interrupts outside
- if (!Optimizations.EnableDebugging)
- {
- context.CheckInterrupt();
- }
+ context.CheckInterrupt();
Statistics.ResumeTimer();
diff --git a/src/Ryujinx.Common/TitleIDs.cs b/src/Ryujinx.Common/TitleIDs.cs
index 793848d6d..f77a2858f 100644
--- a/src/Ryujinx.Common/TitleIDs.cs
+++ b/src/Ryujinx.Common/TitleIDs.cs
@@ -106,6 +106,7 @@ namespace Ryujinx.Common
"0100b3f000be2000", // Pokkén Tournament DX
"0100187003a36000", // Pokémon: Let's Go Eevee!
"010003f003a34000", // Pokémon: Let's Go Pikachu!
+ "0100f43008c44000", // Pokémon Legends: Z-A
//Splatoon Franchise
"0100f8f0000a2000", // Splatoon 2 (EU)
diff --git a/src/Ryujinx.HLE/Debugger/BreakpointManager.cs b/src/Ryujinx.HLE/Debugger/BreakpointManager.cs
index bf462a781..7f45fbf8b 100644
--- a/src/Ryujinx.HLE/Debugger/BreakpointManager.cs
+++ b/src/Ryujinx.HLE/Debugger/BreakpointManager.cs
@@ -11,12 +11,9 @@ namespace Ryujinx.HLE.Debugger
{
public byte[] OriginalData { get; }
- public bool IsStep { get; }
-
- public Breakpoint(byte[] originalData, bool isStep)
+ public Breakpoint(byte[] originalData)
{
OriginalData = originalData;
- IsStep = isStep;
}
}
@@ -28,9 +25,9 @@ namespace Ryujinx.HLE.Debugger
private readonly Debugger _debugger;
private readonly ConcurrentDictionary _breakpoints = new();
- private static readonly byte[] _aarch64BreakInstruction = { 0x00, 0x00, 0x20, 0xD4 }; // BRK #0
- private static readonly byte[] _aarch32BreakInstruction = { 0xFE, 0xDE, 0xFF, 0xE7 }; // TRAP
- private static readonly byte[] _aarch32ThumbBreakInstruction = { 0x80, 0xB6 };
+ private static readonly byte[] _aarch64BreakInstruction = [0x00, 0x00, 0x20, 0xD4]; // BRK #0
+ private static readonly byte[] _aarch32BreakInstruction = [0xFE, 0xDE, 0xFF, 0xE7]; // TRAP
+ private static readonly byte[] _aarch32ThumbBreakInstruction = [0x80, 0xB6];
public BreakpointManager(Debugger debugger)
{
@@ -44,7 +41,7 @@ namespace Ryujinx.HLE.Debugger
/// The length of the instruction to replace.
/// Indicates if this is a single-step breakpoint.
/// True if the breakpoint was set successfully; otherwise, false.
- public bool SetBreakPoint(ulong address, ulong length, bool isStep = false)
+ public bool SetBreakPoint(ulong address, ulong length)
{
if (_breakpoints.ContainsKey(address))
{
@@ -71,7 +68,7 @@ namespace Ryujinx.HLE.Debugger
return false;
}
- var breakpoint = new Breakpoint(originalInstruction, isStep);
+ var breakpoint = new Breakpoint(originalInstruction);
if (_breakpoints.TryAdd(address, breakpoint))
{
Logger.Debug?.Print(LogClass.GdbStub, $"Breakpoint set at 0x{address:X16}");
@@ -124,33 +121,9 @@ namespace Ryujinx.HLE.Debugger
Logger.Debug?.Print(LogClass.GdbStub, "All breakpoints cleared.");
}
- ///
- /// Clears all currently set single-step software breakpoints.
- ///
- public void ClearAllStepBreakpoints()
- {
- var stepBreakpoints = _breakpoints.Where(p => p.Value.IsStep).ToList();
-
- if (stepBreakpoints.Count == 0)
- {
- return;
- }
-
- foreach (var bp in stepBreakpoints)
- {
- if (_breakpoints.TryRemove(bp.Key, out Breakpoint removedBreakpoint))
- {
- WriteMemory(bp.Key, removedBreakpoint.OriginalData);
- }
- }
-
- Logger.Debug?.Print(LogClass.GdbStub, "All step breakpoints cleared.");
- }
-
-
private byte[] GetBreakInstruction(ulong length)
{
- if (_debugger.IsProcessAarch32)
+ if (_debugger.IsProcess32Bit)
{
if (length == 2)
{
diff --git a/src/Ryujinx.HLE/Debugger/Debugger.MainThread.cs b/src/Ryujinx.HLE/Debugger/Debugger.MainThread.cs
new file mode 100644
index 000000000..2e1f40120
--- /dev/null
+++ b/src/Ryujinx.HLE/Debugger/Debugger.MainThread.cs
@@ -0,0 +1,115 @@
+using Ryujinx.Common.Logging;
+using Ryujinx.HLE.Debugger.Gdb;
+using System.IO;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading;
+
+namespace Ryujinx.HLE.Debugger
+{
+ public partial class Debugger
+ {
+ private void DebuggerThreadMain()
+ {
+ IPEndPoint endpoint = new(IPAddress.Any, GdbStubPort);
+ _listenerSocket = new TcpListener(endpoint);
+ _listenerSocket.Start();
+ Logger.Notice.Print(LogClass.GdbStub, $"Currently waiting on {endpoint} for GDB client");
+
+ while (!_shuttingDown)
+ {
+ try
+ {
+ _clientSocket = _listenerSocket.AcceptSocket();
+ }
+ catch (SocketException)
+ {
+ return;
+ }
+
+ // If the user connects before the application is running, wait for the application to start.
+ int retries = 10;
+ while ((DebugProcess == null || GetThreads().Length == 0) && retries-- > 0)
+ {
+ Thread.Sleep(200);
+ }
+
+ if (DebugProcess == null || GetThreads().Length == 0)
+ {
+ Logger.Warning?.Print(LogClass.GdbStub,
+ "Application is not running, cannot accept GDB client connection");
+ _clientSocket.Close();
+ continue;
+ }
+
+ _clientSocket.NoDelay = true;
+ _readStream = new NetworkStream(_clientSocket, System.IO.FileAccess.Read);
+ _writeStream = new NetworkStream(_clientSocket, System.IO.FileAccess.Write);
+ _commands = new GdbCommands(_listenerSocket, _clientSocket, _readStream, _writeStream, this);
+ _commandProcessor = _commands.CreateProcessor();
+
+ Logger.Notice.Print(LogClass.GdbStub, "GDB client connected");
+
+ while (true)
+ {
+ try
+ {
+ switch (_readStream.ReadByte())
+ {
+ case -1:
+ goto EndOfLoop;
+ case '+':
+ continue;
+ case '-':
+ Logger.Notice.Print(LogClass.GdbStub, "NACK received!");
+ continue;
+ case '\x03':
+ _messages.Add(new BreakInMessage());
+ break;
+ case '$':
+ string cmd = "";
+ while (true)
+ {
+ int x = _readStream.ReadByte();
+ if (x == -1)
+ goto EndOfLoop;
+ if (x == '#')
+ break;
+ cmd += (char)x;
+ }
+
+ string checksum = $"{(char)_readStream.ReadByte()}{(char)_readStream.ReadByte()}";
+ if (checksum == $"{Helpers.CalculateChecksum(cmd):x2}")
+ {
+ _messages.Add(new CommandMessage(cmd));
+ }
+ else
+ {
+ _messages.Add(new SendNackMessage());
+ }
+
+ break;
+ }
+ }
+ catch (IOException)
+ {
+ goto EndOfLoop;
+ }
+ }
+
+ EndOfLoop:
+ Logger.Notice.Print(LogClass.GdbStub, "GDB client lost connection");
+ _readStream.Close();
+ _readStream = null;
+ _writeStream.Close();
+ _writeStream = null;
+ _clientSocket.Close();
+ _clientSocket = null;
+ _commandProcessor = null;
+ _commands = null;
+
+ BreakpointManager.ClearAll();
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/Debugger/Debugger.cs b/src/Ryujinx.HLE/Debugger/Debugger.cs
index 2c935ca7f..1c28870b7 100644
--- a/src/Ryujinx.HLE/Debugger/Debugger.cs
+++ b/src/Ryujinx.HLE/Debugger/Debugger.cs
@@ -1,45 +1,44 @@
-using ARMeilleure.State;
-using Ryujinx.Common;
using Ryujinx.Common.Logging;
-using Ryujinx.HLE.HOS.Kernel;
+using Ryujinx.HLE.Debugger.Gdb;
using Ryujinx.HLE.HOS.Kernel.Process;
using Ryujinx.HLE.HOS.Kernel.Threading;
-using Ryujinx.Memory;
using System;
using System.Collections.Concurrent;
-using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using IExecutionContext = Ryujinx.Cpu.IExecutionContext;
+
namespace Ryujinx.HLE.Debugger
{
- public class Debugger : IDisposable
+ public partial class Debugger : IDisposable
{
internal Switch Device { get; private set; }
public ushort GdbStubPort { get; private set; }
- private TcpListener ListenerSocket;
- private Socket ClientSocket = null;
- private NetworkStream ReadStream = null;
- private NetworkStream WriteStream = null;
- private BlockingCollection Messages = new BlockingCollection(1);
- private Thread DebuggerThread;
- private Thread MessageHandlerThread;
- private bool _shuttingDown = false;
- private ManualResetEventSlim _breakHandlerEvent = new ManualResetEventSlim(false);
+ private readonly BlockingCollection _messages = new(1);
+ private readonly Thread _debuggerThread;
+ private readonly Thread _messageHandlerThread;
- private ulong? cThread;
- private ulong? gThread;
+ private TcpListener _listenerSocket;
+ private Socket _clientSocket;
+ private NetworkStream _readStream;
+ private NetworkStream _writeStream;
- private BreakpointManager BreakpointManager;
+ private GdbCommandProcessor _commandProcessor;
+ private GdbCommands _commands;
- private string previousThreadListXml = "";
+ private bool _shuttingDown;
+ private readonly ManualResetEventSlim _breakHandlerEvent = new(false);
+
+ internal ulong? CThread;
+ internal ulong? GThread;
+
+ public readonly BreakpointManager BreakpointManager;
public Debugger(Switch device, ushort port)
{
@@ -48,198 +47,50 @@ namespace Ryujinx.HLE.Debugger
ARMeilleure.Optimizations.EnableDebugging = true;
- DebuggerThread = new Thread(DebuggerThreadMain);
- DebuggerThread.Start();
- MessageHandlerThread = new Thread(MessageHandlerMain);
- MessageHandlerThread.Start();
+ _debuggerThread = new Thread(DebuggerThreadMain);
+ _debuggerThread.Start();
+ _messageHandlerThread = new Thread(MessageHandlerMain);
+ _messageHandlerThread.Start();
BreakpointManager = new BreakpointManager(this);
}
internal KProcess Process => Device.System?.DebugGetApplicationProcess();
internal IDebuggableProcess DebugProcess => Device.System?.DebugGetApplicationProcessDebugInterface();
- private KThread[] GetThreads() => DebugProcess.GetThreadUids().Select(x => DebugProcess.GetThread(x)).ToArray();
- internal bool IsProcessAarch32 => DebugProcess.GetThread(gThread.Value).Context.IsAarch32;
- private KernelContext KernelContext => Device.System.KernelContext;
- const int GdbRegisterCount64 = 68;
- const int GdbRegisterCount32 = 66;
- /* FPCR = FPSR & ~FpcrMask
- All of FPCR's bits are reserved in FPCR and vice versa,
- see ARM's documentation. */
- private const uint FpcrMask = 0xfc1fffff;
+ internal KThread[] GetThreads() =>
+ DebugProcess.GetThreadUids().Select(x => DebugProcess.GetThread(x)).ToArray();
- private string GdbReadRegister64(IExecutionContext state, int gdbRegId)
- {
- switch (gdbRegId)
- {
- case >= 0 and <= 31:
- return ToHex(BitConverter.GetBytes(state.GetX(gdbRegId)));
- case 32:
- return ToHex(BitConverter.GetBytes(state.DebugPc));
- case 33:
- return ToHex(BitConverter.GetBytes(state.Pstate));
- case >= 34 and <= 65:
- return ToHex(state.GetV(gdbRegId - 34).ToArray());
- case 66:
- return ToHex(BitConverter.GetBytes((uint)state.Fpsr));
- case 67:
- return ToHex(BitConverter.GetBytes((uint)state.Fpcr));
- default:
- return null;
- }
- }
-
- private bool GdbWriteRegister64(IExecutionContext state, int gdbRegId, StringStream ss)
- {
- switch (gdbRegId)
- {
- case >= 0 and <= 31:
- {
- ulong value = ss.ReadLengthAsLEHex(16);
- state.SetX(gdbRegId, value);
- return true;
- }
- case 32:
- {
- ulong value = ss.ReadLengthAsLEHex(16);
- state.DebugPc = value;
- return true;
- }
- case 33:
- {
- ulong value = ss.ReadLengthAsLEHex(8);
- state.Pstate = (uint)value;
- return true;
- }
- case >= 34 and <= 65:
- {
- ulong value0 = ss.ReadLengthAsLEHex(16);
- ulong value1 = ss.ReadLengthAsLEHex(16);
- state.SetV(gdbRegId - 34, new V128(value0, value1));
- return true;
- }
- case 66:
- {
- ulong value = ss.ReadLengthAsLEHex(8);
- state.Fpsr = (uint)value;
- return true;
- }
- case 67:
- {
- ulong value = ss.ReadLengthAsLEHex(8);
- state.Fpcr = (uint)value;
- return true;
- }
- default:
- return false;
- }
- }
-
- private string GdbReadRegister32(IExecutionContext state, int gdbRegId)
- {
- switch (gdbRegId)
- {
- case >= 0 and <= 14:
- return ToHex(BitConverter.GetBytes((uint)state.GetX(gdbRegId)));
- case 15:
- return ToHex(BitConverter.GetBytes((uint)state.DebugPc));
- case 16:
- return ToHex(BitConverter.GetBytes((uint)state.Pstate));
- case >= 17 and <= 32:
- return ToHex(state.GetV(gdbRegId - 17).ToArray());
- case >= 33 and <= 64:
- int reg = (gdbRegId - 33);
- int n = reg / 2;
- int shift = reg % 2;
- ulong value = state.GetV(n).Extract(shift);
- return ToHex(BitConverter.GetBytes(value));
- case 65:
- uint fpscr = (uint)state.Fpsr | (uint)state.Fpcr;
- return ToHex(BitConverter.GetBytes(fpscr));
- default:
- return null;
- }
- }
-
- private bool GdbWriteRegister32(IExecutionContext state, int gdbRegId, StringStream ss)
- {
- switch (gdbRegId)
- {
- case >= 0 and <= 14:
- {
- ulong value = ss.ReadLengthAsLEHex(8);
- state.SetX(gdbRegId, value);
- return true;
- }
- case 15:
- {
- ulong value = ss.ReadLengthAsLEHex(8);
- state.DebugPc = value;
- return true;
- }
- case 16:
- {
- ulong value = ss.ReadLengthAsLEHex(8);
- state.Pstate = (uint)value;
- return true;
- }
- case >= 17 and <= 32:
- {
- ulong value0 = ss.ReadLengthAsLEHex(16);
- ulong value1 = ss.ReadLengthAsLEHex(16);
- state.SetV(gdbRegId - 17, new V128(value0, value1));
- return true;
- }
- case >= 33 and <= 64:
- {
- ulong value = ss.ReadLengthAsLEHex(16);
- int regId = (gdbRegId - 33);
- int regNum = regId / 2;
- int shift = regId % 2;
- V128 reg = state.GetV(regNum);
- reg.Insert(shift, value);
- return true;
- }
- case 65:
- {
- ulong value = ss.ReadLengthAsLEHex(8);
- state.Fpsr = (uint)value & FpcrMask;
- state.Fpcr = (uint)value & ~FpcrMask;
- return true;
- }
- default:
- return false;
- }
- }
+ internal bool IsProcess32Bit => DebugProcess.GetThread(GThread.Value).Context.IsAarch32;
private void MessageHandlerMain()
{
while (!_shuttingDown)
{
- IMessage msg = Messages.Take();
- try {
+ IMessage msg = _messages.Take();
+ try
+ {
switch (msg)
{
case BreakInMessage:
Logger.Notice.Print(LogClass.GdbStub, "Break-in requested");
- CommandInterrupt();
+ _commandProcessor.Commands.Interrupt();
break;
case SendNackMessage:
- WriteStream.WriteByte((byte)'-');
+ _writeStream.WriteByte((byte)'-');
break;
case CommandMessage { Command: var cmd }:
Logger.Debug?.Print(LogClass.GdbStub, $"Received Command: {cmd}");
- WriteStream.WriteByte((byte)'+');
- ProcessCommand(cmd);
+ _writeStream.WriteByte((byte)'+');
+ _commandProcessor.Process(cmd);
break;
case ThreadBreakMessage { Context: var ctx }:
DebugProcess.DebugStop();
- gThread = cThread = ctx.ThreadUid;
+ GThread = CThread = ctx.ThreadUid;
_breakHandlerEvent.Set();
- Reply($"T05thread:{ctx.ThreadUid:x};");
+ _commandProcessor.Reply($"T05thread:{ctx.ThreadUid:x};");
break;
case KillMessage:
@@ -254,837 +105,36 @@ namespace Ryujinx.HLE.Debugger
{
Logger.Error?.Print(LogClass.GdbStub, "Error while processing GDB messages", e);
}
- }
- }
-
- private void ProcessCommand(string cmd)
- {
- StringStream ss = new StringStream(cmd);
-
- switch (ss.ReadChar())
- {
- case '!':
- if (!ss.IsEmpty())
- {
- goto unknownCommand;
- }
-
- // Enable extended mode
- ReplyOK();
- break;
- case '?':
- if (!ss.IsEmpty())
- {
- goto unknownCommand;
- }
-
- CommandQuery();
- break;
- case 'c':
- CommandContinue(ss.IsEmpty() ? null : ss.ReadRemainingAsHex());
- break;
- case 'D':
- if (!ss.IsEmpty())
- {
- goto unknownCommand;
- }
-
- CommandDetach();
- break;
- case 'g':
- if (!ss.IsEmpty())
- {
- goto unknownCommand;
- }
-
- CommandReadRegisters();
- break;
- case 'G':
- CommandWriteRegisters(ss);
- break;
- case 'H':
- {
- char op = ss.ReadChar();
- ulong? threadId = ss.ReadRemainingAsThreadUid();
- CommandSetThread(op, threadId);
- break;
- }
- case 'k':
- Logger.Notice.Print(LogClass.GdbStub, "Kill request received, detach instead");
- Reply("");
- CommandDetach();
- break;
- case 'm':
- {
- ulong addr = ss.ReadUntilAsHex(',');
- ulong len = ss.ReadRemainingAsHex();
- CommandReadMemory(addr, len);
- break;
- }
- case 'M':
- {
- ulong addr = ss.ReadUntilAsHex(',');
- ulong len = ss.ReadUntilAsHex(':');
- CommandWriteMemory(addr, len, ss);
- break;
- }
- case 'p':
- {
- ulong gdbRegId = ss.ReadRemainingAsHex();
- CommandReadRegister((int)gdbRegId);
- break;
- }
- case 'P':
- {
- ulong gdbRegId = ss.ReadUntilAsHex('=');
- CommandWriteRegister((int)gdbRegId, ss);
- break;
- }
- case 'q':
- if (ss.ConsumeRemaining("GDBServerVersion"))
- {
- Reply($"name:Ryujinx;version:{ReleaseInformation.Version};");
- break;
- }
-
- if (ss.ConsumeRemaining("HostInfo"))
- {
- if (IsProcessAarch32)
- {
- Reply(
- $"triple:{ToHex("arm-unknown-linux-android")};endian:little;ptrsize:4;hostname:{ToHex("Ryujinx")};");
- }
- else
- {
- Reply(
- $"triple:{ToHex("aarch64-unknown-linux-android")};endian:little;ptrsize:8;hostname:{ToHex("Ryujinx")};");
- }
- break;
- }
-
- if (ss.ConsumeRemaining("ProcessInfo"))
- {
- if (IsProcessAarch32)
- {
- Reply(
- $"pid:1;cputype:12;cpusubtype:0;triple:{ToHex("arm-unknown-linux-android")};ostype:unknown;vendor:none;endian:little;ptrsize:4;");
- }
- else
- {
- Reply(
- $"pid:1;cputype:100000c;cpusubtype:0;triple:{ToHex("aarch64-unknown-linux-android")};ostype:unknown;vendor:none;endian:little;ptrsize:8;");
- }
- break;
- }
-
- if (ss.ConsumePrefix("Supported:") || ss.ConsumeRemaining("Supported"))
- {
- Reply("PacketSize=10000;qXfer:features:read+;qXfer:threads:read+;vContSupported+");
- break;
- }
-
- if (ss.ConsumePrefix("Rcmd,"))
- {
- string hexCommand = ss.ReadRemaining();
- HandleQRcmdCommand(hexCommand);
- break;
- }
-
- if (ss.ConsumeRemaining("fThreadInfo"))
- {
- Reply($"m{string.Join(",", DebugProcess.GetThreadUids().Select(x => $"{x:x}"))}");
- break;
- }
-
- if (ss.ConsumeRemaining("sThreadInfo"))
- {
- Reply("l");
- break;
- }
-
- if (ss.ConsumePrefix("ThreadExtraInfo,"))
- {
- ulong? threadId = ss.ReadRemainingAsThreadUid();
- if (threadId == null)
- {
- ReplyError();
- break;
- }
-
- if (DebugProcess.IsThreadPaused(DebugProcess.GetThread(threadId.Value)))
- {
- Reply(ToHex("Paused"));
- }
- else
- {
- Reply(ToHex("Running"));
- }
- break;
- }
-
- if (ss.ConsumePrefix("Xfer:threads:read:"))
- {
- ss.ReadUntil(':');
- ulong offset = ss.ReadUntilAsHex(',');
- ulong len = ss.ReadRemainingAsHex();
-
- var data = "";
- if (offset > 0)
- {
- data = previousThreadListXml;
- } else
- {
- previousThreadListXml = data = GetThreadListXml();
- }
-
- if (offset >= (ulong)data.Length)
- {
- Reply("l");
- break;
- }
-
- if (len >= (ulong)data.Length - offset)
- {
- Reply("l" + ToBinaryFormat(data.Substring((int)offset)));
- break;
- }
- else
- {
- Reply("m" + ToBinaryFormat(data.Substring((int)offset, (int)len)));
- break;
- }
- }
-
- if (ss.ConsumePrefix("Xfer:features:read:"))
- {
- string feature = ss.ReadUntil(':');
- ulong offset = ss.ReadUntilAsHex(',');
- ulong len = ss.ReadRemainingAsHex();
-
- if (feature == "target.xml")
- {
- feature = IsProcessAarch32 ? "target32.xml" : "target64.xml";
- }
-
- string data;
- if (RegisterInformation.Features.TryGetValue(feature, out data))
- {
- if (offset >= (ulong)data.Length)
- {
- Reply("l");
- break;
- }
-
- if (len >= (ulong)data.Length - offset)
- {
- Reply("l" + ToBinaryFormat(data.Substring((int)offset)));
- break;
- }
- else
- {
- Reply("m" + ToBinaryFormat(data.Substring((int)offset, (int)len)));
- break;
- }
- }
- else
- {
- Reply("E00"); // Invalid annex
- break;
- }
- }
-
- goto unknownCommand;
- case 'Q':
- goto unknownCommand;
- case 's':
- CommandStep(ss.IsEmpty() ? null : ss.ReadRemainingAsHex());
- break;
- case 'T':
- {
- ulong? threadId = ss.ReadRemainingAsThreadUid();
- CommandIsAlive(threadId);
- break;
- }
- case 'v':
- if (ss.ConsumePrefix("Cont"))
- {
- if (ss.ConsumeRemaining("?"))
- {
- Reply("vCont;c;C;s;S");
- break;
- }
-
- if (ss.ConsumePrefix(";"))
- {
- HandleVContCommand(ss);
- break;
- }
-
- goto unknownCommand;
- }
- if (ss.ConsumeRemaining("MustReplyEmpty"))
- {
- Reply("");
- break;
- }
- goto unknownCommand;
- case 'Z':
- {
- string type = ss.ReadUntil(',');
- ulong addr = ss.ReadUntilAsHex(',');
- ulong len = ss.ReadLengthAsHex(1);
- string extra = ss.ReadRemaining();
-
- if (extra.Length > 0)
- {
- Logger.Notice.Print(LogClass.GdbStub, $"Unsupported Z command extra data: {extra}");
- ReplyError();
- return;
- }
-
- switch (type)
- {
- case "0": // Software breakpoint
- if (!BreakpointManager.SetBreakPoint(addr, len, false))
- {
- ReplyError();
- return;
- }
- ReplyOK();
- return;
- case "1": // Hardware breakpoint
- case "2": // Write watchpoint
- case "3": // Read watchpoint
- case "4": // Access watchpoint
- ReplyError();
- return;
- default:
- ReplyError();
- return;
- }
- }
- case 'z':
- {
- string type = ss.ReadUntil(',');
- ss.ConsumePrefix(",");
- ulong addr = ss.ReadUntilAsHex(',');
- ulong len = ss.ReadLengthAsHex(1);
- string extra = ss.ReadRemaining();
-
- if (extra.Length > 0)
- {
- Logger.Notice.Print(LogClass.GdbStub, $"Unsupported z command extra data: {extra}");
- ReplyError();
- return;
- }
-
- switch (type)
- {
- case "0": // Software breakpoint
- if (!BreakpointManager.ClearBreakPoint(addr, len))
- {
- ReplyError();
- return;
- }
- ReplyOK();
- return;
- case "1": // Hardware breakpoint
- case "2": // Write watchpoint
- case "3": // Read watchpoint
- case "4": // Access watchpoint
- ReplyError();
- return;
- default:
- ReplyError();
- return;
- }
- }
- default:
- unknownCommand:
- Logger.Notice.Print(LogClass.GdbStub, $"Unknown command: {cmd}");
- Reply("");
- break;
- }
- }
-
- enum VContAction
- {
- None,
- Continue,
- Stop,
- Step
- }
-
- record VContPendingAction(VContAction Action, ushort? Signal = null);
-
- private void HandleVContCommand(StringStream ss)
- {
- string[] rawActions = ss.ReadRemaining().Split(';', StringSplitOptions.RemoveEmptyEntries);
-
- var threadActionMap = new Dictionary();
- foreach (var thread in GetThreads())
- {
- threadActionMap[thread.ThreadUid] = new VContPendingAction(VContAction.None);
- }
-
- VContAction defaultAction = VContAction.None;
-
- // For each inferior thread, the *leftmost* action with a matching thread-id is applied.
- for (int i = rawActions.Length - 1; i >= 0; i--)
- {
- var rawAction = rawActions[i];
- var stream = new StringStream(rawAction);
-
- char cmd = stream.ReadChar();
- VContAction action = cmd switch
+ catch (ObjectDisposedException e)
{
- 'c' => VContAction.Continue,
- 'C' => VContAction.Continue,
- 's' => VContAction.Step,
- 'S' => VContAction.Step,
- 't' => VContAction.Stop,
- _ => VContAction.None
- };
-
- // Note: We don't support signals yet.
- ushort? signal = null;
- if (cmd == 'C' || cmd == 'S')
- {
- signal = (ushort)stream.ReadLengthAsHex(2);
- }
-
- ulong? threadId = null;
- if (stream.ConsumePrefix(":"))
- {
- threadId = stream.ReadRemainingAsThreadUid();
- }
-
- if (threadId.HasValue)
- {
- if (threadActionMap.ContainsKey(threadId.Value)) {
- threadActionMap[threadId.Value] = new VContPendingAction(action, signal);
- }
- }
- else
- {
- foreach (var row in threadActionMap.ToList())
- {
- threadActionMap[row.Key] = new VContPendingAction(action, signal);
- }
-
- if (action == VContAction.Continue) {
- defaultAction = action;
- } else {
- Logger.Warning?.Print(LogClass.GdbStub, $"Received vCont command with unsupported default action: {rawAction}");
- }
- }
- }
-
- bool hasError = false;
-
- foreach (var (threadUid, action) in threadActionMap)
- {
- if (action.Action == VContAction.Step)
- {
- var thread = DebugProcess.GetThread(threadUid);
- if (!DebugProcess.DebugStep(thread)) {
- hasError = true;
- }
- }
- }
-
- // If we receive "vCont;c", just continue the process.
- // If we receive something like "vCont;c:2e;c:2f" (IDA Pro will send commands like this), continue these threads.
- // For "vCont;s:2f;c", `DebugProcess.DebugStep()` will continue and suspend other threads if needed, so we don't do anything here.
- if (threadActionMap.Values.All(a => a.Action == VContAction.Continue))
- {
- DebugProcess.DebugContinue();
- } else if (defaultAction == VContAction.None) {
- foreach (var (threadUid, action) in threadActionMap)
- {
- if (action.Action == VContAction.Continue)
- {
- DebugProcess.DebugContinue(DebugProcess.GetThread(threadUid));
- }
- }
- }
-
- if (hasError)
- {
- ReplyError();
- }
- else
- {
- ReplyOK();
- }
-
- foreach (var (threadUid, action) in threadActionMap)
- {
- if (action.Action == VContAction.Step)
- {
- gThread = cThread = threadUid;
- Reply($"T05thread:{threadUid:x};");
+ Logger.Error?.Print(LogClass.GdbStub, "Error while processing GDB messages", e);
}
}
}
- private string GetThreadListXml()
+ public string GetStackTrace()
{
- var sb = new StringBuilder();
- sb.Append("\n");
-
- foreach (var thread in GetThreads())
- {
- string threadName = System.Security.SecurityElement.Escape(thread.GetThreadName());
- sb.Append($"{(DebugProcess.IsThreadPaused(thread) ? "Paused" : "Running")}\n");
- }
-
- sb.Append("");
- return sb.ToString();
- }
-
- void CommandQuery()
- {
- // GDB is performing initial contact. Stop everything.
- DebugProcess.DebugStop();
- gThread = cThread = DebugProcess.GetThreadUids().First();
- Reply($"T05thread:{cThread:x};");
- }
-
- void CommandInterrupt()
- {
- // GDB is requesting an interrupt. Stop everything.
- DebugProcess.DebugStop();
- if (gThread == null || !GetThreads().Any(x => x.ThreadUid == gThread.Value))
- {
- gThread = cThread = DebugProcess.GetThreadUids().First();
- }
-
- Reply($"T02thread:{gThread:x};");
- }
-
- void CommandContinue(ulong? newPc)
- {
- if (newPc.HasValue)
- {
- if (cThread == null)
- {
- ReplyError();
- return;
- }
-
- DebugProcess.GetThread(cThread.Value).Context.DebugPc = newPc.Value;
- }
-
- DebugProcess.DebugContinue();
- }
-
- void CommandDetach()
- {
- BreakpointManager.ClearAll();
- CommandContinue(null);
- }
-
- void CommandReadRegisters()
- {
- if (gThread == null)
- {
- ReplyError();
- return;
- }
-
- var ctx = DebugProcess.GetThread(gThread.Value).Context;
- string registers = "";
- if (IsProcessAarch32)
- {
- for (int i = 0; i < GdbRegisterCount32; i++)
- {
- registers += GdbReadRegister32(ctx, i);
- }
- }
- else
- {
- for (int i = 0; i < GdbRegisterCount64; i++)
- {
- registers += GdbReadRegister64(ctx, i);
- }
- }
-
- Reply(registers);
- }
-
- void CommandWriteRegisters(StringStream ss)
- {
- if (gThread == null)
- {
- ReplyError();
- return;
- }
-
- var ctx = DebugProcess.GetThread(gThread.Value).Context;
- if (IsProcessAarch32)
- {
- for (int i = 0; i < GdbRegisterCount32; i++)
- {
- if (!GdbWriteRegister32(ctx, i, ss))
- {
- ReplyError();
- return;
- }
- }
- }
- else
- {
- for (int i = 0; i < GdbRegisterCount64; i++)
- {
- if (!GdbWriteRegister64(ctx, i, ss))
- {
- ReplyError();
- return;
- }
- }
- }
-
- if (ss.IsEmpty())
- {
- ReplyOK();
- }
- else
- {
- ReplyError();
- }
- }
-
- void CommandSetThread(char op, ulong? threadId)
- {
- if (threadId == 0 || threadId == null)
- {
- var threads = GetThreads();
- if (threads.Length == 0)
- {
- ReplyError();
- return;
- }
- threadId = threads.First().ThreadUid;
- }
-
- if (DebugProcess.GetThread(threadId.Value) == null)
- {
- ReplyError();
- return;
- }
-
- switch (op)
- {
- case 'c':
- cThread = threadId;
- ReplyOK();
- return;
- case 'g':
- gThread = threadId;
- ReplyOK();
- return;
- default:
- ReplyError();
- return;
- }
- }
-
- void CommandReadMemory(ulong addr, ulong len)
- {
- try
- {
- var data = new byte[len];
- DebugProcess.CpuMemory.Read(addr, data);
- Reply(ToHex(data));
- }
- catch (InvalidMemoryRegionException)
- {
- // InvalidAccessHandler will show an error message, we log it again to tell user the error is from GDB (which can be ignored)
- // TODO: Do not let InvalidAccessHandler show the error message
- Logger.Notice.Print(LogClass.GdbStub, $"GDB failed to read memory at 0x{addr:X16}");
- ReplyError();
- }
- }
-
- void CommandWriteMemory(ulong addr, ulong len, StringStream ss)
- {
- try
- {
- var data = new byte[len];
- for (ulong i = 0; i < len; i++)
- {
- data[i] = (byte)ss.ReadLengthAsHex(2);
- }
-
- DebugProcess.CpuMemory.Write(addr, data);
- DebugProcess.InvalidateCacheRegion(addr, len);
- ReplyOK();
- }
- catch (InvalidMemoryRegionException)
- {
- ReplyError();
- }
- }
-
- void CommandReadRegister(int gdbRegId)
- {
- if (gThread == null)
- {
- ReplyError();
- return;
- }
-
- var ctx = DebugProcess.GetThread(gThread.Value).Context;
- string result;
- if (IsProcessAarch32)
- {
- result = GdbReadRegister32(ctx, gdbRegId);
- if (result != null)
- {
- Reply(result);
- }
- else
- {
- ReplyError();
- }
- }
- else
- {
- result = GdbReadRegister64(ctx, gdbRegId);
- if (result != null)
- {
- Reply(result);
- }
- else
- {
- ReplyError();
- }
- }
- }
-
- void CommandWriteRegister(int gdbRegId, StringStream ss)
- {
- if (gThread == null)
- {
- ReplyError();
- return;
- }
-
- var ctx = DebugProcess.GetThread(gThread.Value).Context;
- if (IsProcessAarch32)
- {
- if (GdbWriteRegister32(ctx, gdbRegId, ss) && ss.IsEmpty())
- {
- ReplyOK();
- }
- else
- {
- ReplyError();
- }
- }
- else
- {
- if (GdbWriteRegister64(ctx, gdbRegId, ss) && ss.IsEmpty())
- {
- ReplyOK();
- }
- else
- {
- ReplyError();
- }
- }
- }
-
- private void CommandStep(ulong? newPc)
- {
- if (cThread == null)
- {
- ReplyError();
- return;
- }
-
- var thread = DebugProcess.GetThread(cThread.Value);
-
- if (newPc.HasValue)
- {
- thread.Context.DebugPc = newPc.Value;
- }
-
- if (!DebugProcess.DebugStep(thread))
- {
- ReplyError();
- }
- else
- {
- gThread = cThread = thread.ThreadUid;
- Reply($"T05thread:{thread.ThreadUid:x};");
- }
- }
-
- private void CommandIsAlive(ulong? threadId)
- {
- if (GetThreads().Any(x => x.ThreadUid == threadId))
- {
- ReplyOK();
- }
- else
- {
- Reply("E00");
- }
- }
-
- private void HandleQRcmdCommand(string hexCommand)
- {
- try
- {
- string command = FromHex(hexCommand);
- Logger.Debug?.Print(LogClass.GdbStub, $"Received Rcmd: {command}");
-
- string response = command.Trim().ToLowerInvariant() switch
- {
- "help" => "backtrace\nbt\nregisters\nreg\nget info\nminidump\n",
- "get info" => GetProcessInfo(),
- "backtrace" => GetStackTrace(),
- "bt" => GetStackTrace(),
- "registers" => GetRegisters(),
- "reg" => GetRegisters(),
- "minidump" => GetMinidump(),
- _ => $"Unknown command: {command}\n"
- };
-
- Reply(ToHex(response));
- }
- catch (Exception e)
- {
- Logger.Error?.Print(LogClass.GdbStub, $"Error processing Rcmd: {e.Message}");
- ReplyError();
- }
- }
-
- private string GetStackTrace()
- {
- if (gThread == null)
+ if (GThread == null)
return "No thread selected\n";
if (Process == null)
return "No application process found\n";
- return Process.Debugger.GetGuestStackTrace(DebugProcess.GetThread(gThread.Value));
+ return Process.Debugger.GetGuestStackTrace(DebugProcess.GetThread(GThread.Value));
}
- private string GetRegisters()
+ public string GetRegisters()
{
- if (gThread == null)
+ if (GThread == null)
return "No thread selected\n";
if (Process == null)
return "No application process found\n";
- return Process.Debugger.GetCpuRegisterPrintout(DebugProcess.GetThread(gThread.Value));
+ return Process.Debugger.GetCpuRegisterPrintout(DebugProcess.GetThread(GThread.Value));
}
- private string GetMinidump()
+ public string GetMinidump()
{
var response = new StringBuilder();
response.AppendLine("=== Begin Minidump ===\n");
@@ -1120,7 +170,7 @@ namespace Ryujinx.HLE.Debugger
return response.ToString();
}
- private string GetProcessInfo()
+ public string GetProcessInfo()
{
try
{
@@ -1130,15 +180,19 @@ namespace Ryujinx.HLE.Debugger
KProcess kProcess = Process;
var sb = new StringBuilder();
-
+
sb.AppendLine($"Program Id: 0x{kProcess.TitleId:x16}");
sb.AppendLine($"Application: {(kProcess.IsApplication ? 1 : 0)}");
sb.AppendLine("Layout:");
- sb.AppendLine($" Alias: 0x{kProcess.MemoryManager.AliasRegionStart:x10} - 0x{kProcess.MemoryManager.AliasRegionEnd - 1:x10}");
- sb.AppendLine($" Heap: 0x{kProcess.MemoryManager.HeapRegionStart:x10} - 0x{kProcess.MemoryManager.HeapRegionEnd - 1:x10}");
- sb.AppendLine($" Aslr: 0x{kProcess.MemoryManager.AslrRegionStart:x10} - 0x{kProcess.MemoryManager.AslrRegionEnd - 1:x10}");
- sb.AppendLine($" Stack: 0x{kProcess.MemoryManager.StackRegionStart:x10} - 0x{kProcess.MemoryManager.StackRegionEnd - 1:x10}");
-
+ sb.AppendLine(
+ $" Alias: 0x{kProcess.MemoryManager.AliasRegionStart:x10} - 0x{kProcess.MemoryManager.AliasRegionEnd - 1:x10}");
+ sb.AppendLine(
+ $" Heap: 0x{kProcess.MemoryManager.HeapRegionStart:x10} - 0x{kProcess.MemoryManager.HeapRegionEnd - 1:x10}");
+ sb.AppendLine(
+ $" Aslr: 0x{kProcess.MemoryManager.AslrRegionStart:x10} - 0x{kProcess.MemoryManager.AslrRegionEnd - 1:x10}");
+ sb.AppendLine(
+ $" Stack: 0x{kProcess.MemoryManager.StackRegionStart:x10} - 0x{kProcess.MemoryManager.StackRegionEnd - 1:x10}");
+
sb.AppendLine("Modules:");
var debugger = kProcess.Debugger;
if (debugger != null)
@@ -1152,7 +206,7 @@ namespace Ryujinx.HLE.Debugger
sb.AppendLine($" 0x{image.BaseAddress:x10} - 0x{endAddress:x10} {name}");
}
}
-
+
return sb.ToString();
}
catch (Exception e)
@@ -1162,170 +216,6 @@ namespace Ryujinx.HLE.Debugger
}
}
- private void Reply(string cmd)
- {
- Logger.Debug?.Print(LogClass.GdbStub, $"Reply: {cmd}");
- WriteStream.Write(Encoding.ASCII.GetBytes($"${cmd}#{CalculateChecksum(cmd):x2}"));
- }
-
- private void ReplyOK()
- {
- Reply("OK");
- }
-
- private void ReplyError()
- {
- Reply("E01");
- }
-
- private void DebuggerThreadMain()
- {
- var endpoint = new IPEndPoint(IPAddress.Any, GdbStubPort);
- ListenerSocket = new TcpListener(endpoint);
- ListenerSocket.Start();
- Logger.Notice.Print(LogClass.GdbStub, $"Currently waiting on {endpoint} for GDB client");
-
- while (!_shuttingDown)
- {
- try
- {
- ClientSocket = ListenerSocket.AcceptSocket();
- }
- catch (SocketException)
- {
- return;
- }
-
- // If the user connects before the application is running, wait for the application to start.
- int retries = 10;
- while ((DebugProcess == null || GetThreads().Length == 0) && retries-- > 0)
- {
- Thread.Sleep(200);
- }
- if (DebugProcess == null || GetThreads().Length == 0)
- {
- Logger.Warning?.Print(LogClass.GdbStub, "Application is not running, cannot accept GDB client connection");
- ClientSocket.Close();
- continue;
- }
-
- ClientSocket.NoDelay = true;
- ReadStream = new NetworkStream(ClientSocket, System.IO.FileAccess.Read);
- WriteStream = new NetworkStream(ClientSocket, System.IO.FileAccess.Write);
- Logger.Notice.Print(LogClass.GdbStub, "GDB client connected");
-
- while (true)
- {
- try
- {
- switch (ReadStream.ReadByte())
- {
- case -1:
- goto eof;
- case '+':
- continue;
- case '-':
- Logger.Notice.Print(LogClass.GdbStub, "NACK received!");
- continue;
- case '\x03':
- Messages.Add(new BreakInMessage());
- break;
- case '$':
- string cmd = "";
- while (true)
- {
- int x = ReadStream.ReadByte();
- if (x == -1)
- goto eof;
- if (x == '#')
- break;
- cmd += (char)x;
- }
-
- string checksum = $"{(char)ReadStream.ReadByte()}{(char)ReadStream.ReadByte()}";
- if (checksum == $"{CalculateChecksum(cmd):x2}")
- {
- Messages.Add(new CommandMessage(cmd));
- }
- else
- {
- Messages.Add(new SendNackMessage());
- }
-
- break;
- }
- }
- catch (IOException)
- {
- goto eof;
- }
- }
-
- eof:
- Logger.Notice.Print(LogClass.GdbStub, "GDB client lost connection");
- ReadStream.Close();
- ReadStream = null;
- WriteStream.Close();
- WriteStream = null;
- ClientSocket.Close();
- ClientSocket = null;
-
- BreakpointManager.ClearAll();
- }
- }
-
- private byte CalculateChecksum(string cmd)
- {
- byte checksum = 0;
- foreach (char x in cmd)
- {
- unchecked
- {
- checksum += (byte)x;
- }
- }
-
- return checksum;
- }
-
- private string FromHex(string hexString)
- {
- if (string.IsNullOrEmpty(hexString))
- return string.Empty;
-
- byte[] bytes = Convert.FromHexString(hexString);
- return Encoding.ASCII.GetString(bytes);
- }
-
- private string ToHex(byte[] bytes)
- {
- return string.Join("", bytes.Select(x => $"{x:x2}"));
- }
-
- private string ToHex(string str)
- {
- return ToHex(Encoding.ASCII.GetBytes(str));
- }
-
- private string ToBinaryFormat(byte[] bytes)
- {
- return string.Join("", bytes.Select(x =>
- x switch
- {
- (byte)'#' => "}\x03",
- (byte)'$' => "}\x04",
- (byte)'*' => "}\x0a",
- (byte)'}' => "}\x5d",
- _ => Convert.ToChar(x).ToString(),
- }
- ));
- }
-
- private string ToBinaryFormat(string str)
- {
- return ToBinaryFormat(Encoding.ASCII.GetBytes(str));
- }
-
public void Dispose()
{
Dispose(true);
@@ -1337,15 +227,15 @@ namespace Ryujinx.HLE.Debugger
{
_shuttingDown = true;
- ListenerSocket.Stop();
- ClientSocket?.Shutdown(SocketShutdown.Both);
- ClientSocket?.Close();
- ReadStream?.Close();
- WriteStream?.Close();
- DebuggerThread.Join();
- Messages.Add(new KillMessage());
- MessageHandlerThread.Join();
- Messages.Dispose();
+ _listenerSocket.Stop();
+ _clientSocket?.Shutdown(SocketShutdown.Both);
+ _clientSocket?.Close();
+ _readStream?.Close();
+ _writeStream?.Close();
+ _debuggerThread.Join();
+ _messages.Add(new KillMessage());
+ _messageHandlerThread.Join();
+ _messages.Dispose();
_breakHandlerEvent.Dispose();
}
}
@@ -1355,10 +245,10 @@ namespace Ryujinx.HLE.Debugger
DebugProcess.DebugInterruptHandler(ctx);
_breakHandlerEvent.Reset();
- Messages.Add(new ThreadBreakMessage(ctx, address, imm));
+ _messages.Add(new ThreadBreakMessage(ctx, address, imm));
// Messages.Add can block, so we log it after adding the message to make sure user can see the log at the same time GDB receives the break message
Logger.Notice.Print(LogClass.GdbStub, $"Break hit on thread {ctx.ThreadUid} at pc {address:x016}");
- // Wait for the process to stop before returning to avoid BreakHander being called multiple times from the same breakpoint
+ // Wait for the process to stop before returning to avoid BreakHandler being called multiple times from the same breakpoint
_breakHandlerEvent.Wait(5000);
}
diff --git a/src/Ryujinx.HLE/Debugger/Gdb/CommandProcessor.cs b/src/Ryujinx.HLE/Debugger/Gdb/CommandProcessor.cs
new file mode 100644
index 000000000..997b635e4
--- /dev/null
+++ b/src/Ryujinx.HLE/Debugger/Gdb/CommandProcessor.cs
@@ -0,0 +1,407 @@
+using Gommon;
+using Ryujinx.Common;
+using Ryujinx.Common.Logging;
+using Ryujinx.HLE.HOS.Kernel.Threading;
+using System.Linq;
+using System.Text;
+
+namespace Ryujinx.HLE.Debugger.Gdb
+{
+ class GdbCommandProcessor
+ {
+ public readonly GdbCommands Commands;
+
+ private Debugger Debugger => Commands.Debugger;
+ private BreakpointManager BreakpointManager => Commands.Debugger.BreakpointManager;
+ private IDebuggableProcess DebugProcess => Commands.Debugger.DebugProcess;
+
+ public GdbCommandProcessor(GdbCommands commands)
+ {
+ Commands = commands;
+ }
+
+ public void Reply(string cmd)
+ {
+ Logger.Debug?.Print(LogClass.GdbStub, $"Reply: {cmd}");
+ Commands.WriteStream.Write(Encoding.ASCII.GetBytes($"${cmd}#{Helpers.CalculateChecksum(cmd):x2}"));
+ }
+
+ public void ReplyOK() => Reply("OK");
+
+ public void ReplyError() => Reply("E01");
+
+ public void Reply(bool success)
+ {
+ if (success)
+ ReplyOK();
+ else ReplyError();
+ }
+
+ public void Reply(bool success, string cmd)
+ {
+ if (success)
+ Reply(cmd);
+ else ReplyError();
+ }
+
+ private string _previousThreadListXml = string.Empty;
+
+ public void Process(string cmd)
+ {
+ StringStream ss = new(cmd);
+
+ switch (ss.ReadChar())
+ {
+ case '!':
+ if (!ss.IsEmpty())
+ {
+ goto unknownCommand;
+ }
+
+ // Enable extended mode
+ ReplyOK();
+ break;
+ case '?':
+ if (!ss.IsEmpty())
+ {
+ goto unknownCommand;
+ }
+
+ Commands.Query();
+ break;
+ case 'c':
+ Commands.Continue(ss.IsEmpty() ? null : ss.ReadRemainingAsHex());
+ break;
+ case 'D':
+ if (!ss.IsEmpty())
+ {
+ goto unknownCommand;
+ }
+
+ Commands.Detach();
+ break;
+ case 'g':
+ if (!ss.IsEmpty())
+ {
+ goto unknownCommand;
+ }
+
+ Commands.ReadRegisters();
+ break;
+ case 'G':
+ Commands.WriteRegisters(ss);
+ break;
+ case 'H':
+ {
+ char op = ss.ReadChar();
+ ulong? threadId = ss.ReadRemainingAsThreadUid();
+ Commands.SetThread(op, threadId);
+ break;
+ }
+ case 'k':
+ Logger.Notice.Print(LogClass.GdbStub, "Kill request received, detach instead");
+ Reply(string.Empty);
+ Commands.Detach();
+ break;
+ case 'm':
+ {
+ ulong addr = ss.ReadUntilAsHex(',');
+ ulong len = ss.ReadRemainingAsHex();
+ Commands.ReadMemory(addr, len);
+ break;
+ }
+ case 'M':
+ {
+ ulong addr = ss.ReadUntilAsHex(',');
+ ulong len = ss.ReadUntilAsHex(':');
+ Commands.WriteMemory(addr, len, ss);
+ break;
+ }
+ case 'p':
+ {
+ ulong gdbRegId = ss.ReadRemainingAsHex();
+ Commands.ReadRegister((int)gdbRegId);
+ break;
+ }
+ case 'P':
+ {
+ ulong gdbRegId = ss.ReadUntilAsHex('=');
+ Commands.WriteRegister((int)gdbRegId, ss);
+ break;
+ }
+ case 'q':
+ if (ss.ConsumeRemaining("GDBServerVersion"))
+ {
+ Reply($"name:Ryujinx;version:{ReleaseInformation.Version};");
+ break;
+ }
+
+ if (ss.ConsumeRemaining("HostInfo"))
+ {
+ Reply(
+ Debugger.IsProcess32Bit
+ ? $"triple:{Helpers.ToHex("arm-unknown-linux-android")};endian:little;ptrsize:4;hostname:{Helpers.ToHex("Ryujinx")};"
+ : $"triple:{Helpers.ToHex("aarch64-unknown-linux-android")};endian:little;ptrsize:8;hostname:{Helpers.ToHex("Ryujinx")};");
+
+ break;
+ }
+
+ if (ss.ConsumeRemaining("ProcessInfo"))
+ {
+ Reply(
+ Debugger.IsProcess32Bit
+ ? $"pid:1;cputype:12;cpusubtype:0;triple:{Helpers.ToHex("arm-unknown-linux-android")};ostype:unknown;vendor:none;endian:little;ptrsize:4;"
+ : $"pid:1;cputype:100000c;cpusubtype:0;triple:{Helpers.ToHex("aarch64-unknown-linux-android")};ostype:unknown;vendor:none;endian:little;ptrsize:8;");
+
+ break;
+ }
+
+ if (ss.ConsumePrefix("Supported:") || ss.ConsumeRemaining("Supported"))
+ {
+ Reply("PacketSize=10000;qXfer:features:read+;qXfer:threads:read+;vContSupported+");
+ break;
+ }
+
+ if (ss.ConsumePrefix("Rcmd,"))
+ {
+ string hexCommand = ss.ReadRemaining();
+ Commands.Q_Rcmd(hexCommand);
+ break;
+ }
+
+ if (ss.ConsumeRemaining("fThreadInfo"))
+ {
+ Reply(
+ $"m{Debugger.DebugProcess.GetThreadUids().Select(x => $"{x:x}").JoinToString(",")}");
+ break;
+ }
+
+ if (ss.ConsumeRemaining("sThreadInfo"))
+ {
+ Reply("l");
+ break;
+ }
+
+ if (ss.ConsumePrefix("ThreadExtraInfo,"))
+ {
+ ulong? threadId = ss.ReadRemainingAsThreadUid();
+ if (threadId == null)
+ {
+ ReplyError();
+ break;
+ }
+
+ Reply(Helpers.ToHex(
+ DebugProcess.IsThreadPaused(DebugProcess.GetThread(threadId.Value))
+ ? "Paused"
+ : "Running"
+ )
+ );
+
+ break;
+ }
+
+ if (ss.ConsumePrefix("Xfer:threads:read:"))
+ {
+ ss.ReadUntil(':');
+ ulong offset = ss.ReadUntilAsHex(',');
+ ulong len = ss.ReadRemainingAsHex();
+
+ string data;
+ if (offset > 0)
+ {
+ data = _previousThreadListXml;
+ }
+ else
+ {
+ _previousThreadListXml = data = GetThreadListXml();
+ }
+
+ if (offset >= (ulong)data.Length)
+ {
+ Reply("l");
+ break;
+ }
+
+ if (len >= (ulong)data.Length - offset)
+ {
+ Reply("l" + Helpers.ToBinaryFormat(data.Substring((int)offset)));
+ }
+ else
+ {
+ Reply("m" + Helpers.ToBinaryFormat(data.Substring((int)offset, (int)len)));
+ }
+
+ break;
+ }
+
+ if (ss.ConsumePrefix("Xfer:features:read:"))
+ {
+ string feature = ss.ReadUntil(':');
+ ulong offset = ss.ReadUntilAsHex(',');
+ ulong len = ss.ReadRemainingAsHex();
+
+ if (feature == "target.xml")
+ {
+ feature = Debugger.IsProcess32Bit ? "target32.xml" : "target64.xml";
+ }
+
+ if (!RegisterInformation.Features.TryGetValue(feature, out string data))
+ {
+ Reply("E00"); // Invalid annex
+ break;
+ }
+
+ if (offset >= (ulong)data.Length)
+ {
+ Reply("l");
+ break;
+ }
+
+ if (len >= (ulong)data.Length - offset)
+ {
+ Reply("l" + Helpers.ToBinaryFormat(data[(int)offset..]));
+ }
+ else
+ {
+ Reply("m" + Helpers.ToBinaryFormat(data.Substring((int)offset, (int)len)));
+ }
+
+ break;
+ }
+
+ goto unknownCommand;
+ case 'Q':
+ goto unknownCommand;
+ case 's':
+ Commands.Step(ss.IsEmpty() ? null : ss.ReadRemainingAsHex());
+ break;
+ case 'T':
+ {
+ ulong? threadId = ss.ReadRemainingAsThreadUid();
+ Commands.IsAlive(threadId);
+ break;
+ }
+ case 'v':
+ if (ss.ConsumePrefix("Cont"))
+ {
+ if (ss.ConsumeRemaining("?"))
+ {
+ Reply("vCont;c;C;s;S");
+ break;
+ }
+
+ if (ss.ConsumePrefix(";"))
+ {
+ Commands.VCont(ss);
+ break;
+ }
+
+ goto unknownCommand;
+ }
+
+ if (ss.ConsumeRemaining("MustReplyEmpty"))
+ {
+ Reply(string.Empty);
+ break;
+ }
+
+ goto unknownCommand;
+ case 'Z':
+ {
+ string type = ss.ReadUntil(',');
+ ulong addr = ss.ReadUntilAsHex(',');
+ ulong len = ss.ReadLengthAsHex(1);
+ string extra = ss.ReadRemaining();
+
+ if (extra.Length > 0)
+ {
+ Logger.Notice.Print(LogClass.GdbStub, $"Unsupported Z command extra data: {extra}");
+ ReplyError();
+ return;
+ }
+
+ switch (type)
+ {
+ case "0": // Software breakpoint
+ if (!BreakpointManager.SetBreakPoint(addr, len))
+ {
+ ReplyError();
+ return;
+ }
+
+ ReplyOK();
+ return;
+ // ReSharper disable RedundantCaseLabel
+ case "1": // Hardware breakpoint
+ case "2": // Write watchpoint
+ case "3": // Read watchpoint
+ case "4": // Access watchpoint
+ // ReSharper restore RedundantCaseLabel
+ default:
+ ReplyError();
+ return;
+ }
+ }
+ case 'z':
+ {
+ string type = ss.ReadUntil(',');
+ ss.ConsumePrefix(",");
+ ulong addr = ss.ReadUntilAsHex(',');
+ ulong len = ss.ReadLengthAsHex(1);
+ string extra = ss.ReadRemaining();
+
+ if (extra.Length > 0)
+ {
+ Logger.Notice.Print(LogClass.GdbStub, $"Unsupported z command extra data: {extra}");
+ ReplyError();
+ return;
+ }
+
+ switch (type)
+ {
+ case "0": // Software breakpoint
+ if (!BreakpointManager.ClearBreakPoint(addr, len))
+ {
+ ReplyError();
+ return;
+ }
+
+ ReplyOK();
+ return;
+ // ReSharper disable RedundantCaseLabel
+ case "1": // Hardware breakpoint
+ case "2": // Write watchpoint
+ case "3": // Read watchpoint
+ case "4": // Access watchpoint
+ // ReSharper restore RedundantCaseLabel
+ default:
+ ReplyError();
+ return;
+ }
+ }
+ default:
+ unknownCommand:
+ Logger.Notice.Print(LogClass.GdbStub, $"Unknown command: {cmd}");
+ Reply(string.Empty);
+ break;
+ }
+ }
+
+ private string GetThreadListXml()
+ {
+ StringBuilder sb = new();
+ sb.Append("\n");
+
+ foreach (KThread thread in Debugger.GetThreads())
+ {
+ string threadName = System.Security.SecurityElement.Escape(thread.GetThreadName());
+ sb.Append(
+ $"{(DebugProcess.IsThreadPaused(thread) ? "Paused" : "Running")}\n");
+ }
+
+ sb.Append("");
+ return sb.ToString();
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/Debugger/Gdb/Commands.cs b/src/Ryujinx.HLE/Debugger/Gdb/Commands.cs
new file mode 100644
index 000000000..33c7f3675
--- /dev/null
+++ b/src/Ryujinx.HLE/Debugger/Gdb/Commands.cs
@@ -0,0 +1,451 @@
+using Ryujinx.Common.Logging;
+using Ryujinx.Cpu;
+using Ryujinx.HLE.HOS.Kernel.Threading;
+using Ryujinx.Memory;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Sockets;
+
+namespace Ryujinx.HLE.Debugger.Gdb
+{
+ class GdbCommands
+ {
+ const int GdbRegisterCount64 = 68;
+ const int GdbRegisterCount32 = 66;
+
+ public readonly Debugger Debugger;
+
+ public GdbCommandProcessor Processor { get; private set; }
+
+ internal readonly TcpListener ListenerSocket;
+ internal readonly Socket ClientSocket;
+ internal readonly NetworkStream ReadStream;
+ internal readonly NetworkStream WriteStream;
+
+
+ public GdbCommands(TcpListener listenerSocket, Socket clientSocket, NetworkStream readStream,
+ NetworkStream writeStream, Debugger debugger)
+ {
+ ListenerSocket = listenerSocket;
+ ClientSocket = clientSocket;
+ ReadStream = readStream;
+ WriteStream = writeStream;
+ Debugger = debugger;
+ }
+
+ public void SetProcessor(GdbCommandProcessor commandProcessor)
+ {
+ if (Processor != null) return;
+
+ Processor = commandProcessor;
+ }
+
+ public GdbCommandProcessor CreateProcessor()
+ {
+ if (Processor != null)
+ return Processor;
+
+ return Processor = new GdbCommandProcessor(this);
+ }
+
+ internal void Query()
+ {
+ // GDB is performing initial contact. Stop everything.
+ Debugger.DebugProcess.DebugStop();
+ Debugger.GThread = Debugger.CThread = Debugger.DebugProcess.GetThreadUids().First();
+ Processor.Reply($"T05thread:{Debugger.CThread:x};");
+ }
+
+ internal void Interrupt()
+ {
+ // GDB is requesting an interrupt. Stop everything.
+ Debugger.DebugProcess.DebugStop();
+ if (Debugger.GThread == null || Debugger.GetThreads().All(x => x.ThreadUid != Debugger.GThread.Value))
+ {
+ Debugger.GThread = Debugger.CThread = Debugger.DebugProcess.GetThreadUids().First();
+ }
+
+ Processor.Reply($"T02thread:{Debugger.GThread:x};");
+ }
+
+ internal void Continue(ulong? newPc)
+ {
+ if (newPc.HasValue)
+ {
+ if (Debugger.CThread == null)
+ {
+ Processor.ReplyError();
+ return;
+ }
+
+ Debugger.DebugProcess.GetThread(Debugger.CThread.Value).Context.DebugPc = newPc.Value;
+ }
+
+ Debugger.DebugProcess.DebugContinue();
+ }
+
+ internal void Detach()
+ {
+ Debugger.BreakpointManager.ClearAll();
+ Continue(null);
+ }
+
+ internal void ReadRegisters()
+ {
+ if (Debugger.GThread == null)
+ {
+ Processor.ReplyError();
+ return;
+ }
+
+ IExecutionContext ctx = Debugger.DebugProcess.GetThread(Debugger.GThread.Value).Context;
+ string registers = string.Empty;
+ if (Debugger.IsProcess32Bit)
+ {
+ for (int i = 0; i < GdbRegisterCount32; i++)
+ {
+ registers += GdbRegisters.Read32(ctx, i);
+ }
+ }
+ else
+ {
+ for (int i = 0; i < GdbRegisterCount64; i++)
+ {
+ registers += GdbRegisters.Read64(ctx, i);
+ }
+ }
+
+ Processor.Reply(registers);
+ }
+
+ internal void WriteRegisters(StringStream ss)
+ {
+ if (Debugger.GThread == null)
+ {
+ Processor.ReplyError();
+ return;
+ }
+
+ IExecutionContext ctx = Debugger.DebugProcess.GetThread(Debugger.GThread.Value).Context;
+ if (Debugger.IsProcess32Bit)
+ {
+ for (int i = 0; i < GdbRegisterCount32; i++)
+ {
+ if (!GdbRegisters.Write32(ctx, i, ss))
+ {
+ Processor.ReplyError();
+ return;
+ }
+ }
+ }
+ else
+ {
+ for (int i = 0; i < GdbRegisterCount64; i++)
+ {
+ if (!GdbRegisters.Write64(ctx, i, ss))
+ {
+ Processor.ReplyError();
+ return;
+ }
+ }
+ }
+
+ Processor.Reply(ss.IsEmpty());
+ }
+
+ internal void SetThread(char op, ulong? threadId)
+ {
+ if (threadId is 0 or null)
+ {
+ KThread[] threads = Debugger.GetThreads();
+ if (threads.Length == 0)
+ {
+ Processor.ReplyError();
+ return;
+ }
+
+ threadId = threads.First().ThreadUid;
+ }
+
+ if (Debugger.DebugProcess.GetThread(threadId.Value) == null)
+ {
+ Processor.ReplyError();
+ return;
+ }
+
+ switch (op)
+ {
+ case 'c':
+ Debugger.CThread = threadId;
+ Processor.ReplyOK();
+ return;
+ case 'g':
+ Debugger.GThread = threadId;
+ Processor.ReplyOK();
+ return;
+ default:
+ Processor.ReplyError();
+ return;
+ }
+ }
+
+ internal void ReadMemory(ulong addr, ulong len)
+ {
+ try
+ {
+ var data = new byte[len];
+ Debugger.DebugProcess.CpuMemory.Read(addr, data);
+ Processor.Reply(Helpers.ToHex(data));
+ }
+ catch (InvalidMemoryRegionException)
+ {
+ // InvalidAccessHandler will show an error message, we log it again to tell user the error is from GDB (which can be ignored)
+ // TODO: Do not let InvalidAccessHandler show the error message
+ Logger.Notice.Print(LogClass.GdbStub, $"GDB failed to read memory at 0x{addr:X16}");
+ Processor.ReplyError();
+ }
+ }
+
+ internal void WriteMemory(ulong addr, ulong len, StringStream ss)
+ {
+ try
+ {
+ var data = new byte[len];
+ for (ulong i = 0; i < len; i++)
+ {
+ data[i] = (byte)ss.ReadLengthAsHex(2);
+ }
+
+ Debugger.DebugProcess.CpuMemory.Write(addr, data);
+ Debugger.DebugProcess.InvalidateCacheRegion(addr, len);
+ Processor.ReplyOK();
+ }
+ catch (InvalidMemoryRegionException)
+ {
+ Processor.ReplyError();
+ }
+ }
+
+ internal void ReadRegister(int gdbRegId)
+ {
+ if (Debugger.GThread == null)
+ {
+ Processor.ReplyError();
+ return;
+ }
+
+ IExecutionContext ctx = Debugger.DebugProcess.GetThread(Debugger.GThread.Value).Context;
+ string result = Debugger.IsProcess32Bit
+ ? GdbRegisters.Read32(ctx, gdbRegId)
+ : GdbRegisters.Read64(ctx, gdbRegId);
+
+ Processor.Reply(result != null, result);
+ }
+
+ internal void WriteRegister(int gdbRegId, StringStream ss)
+ {
+ if (Debugger.GThread == null)
+ {
+ Processor.ReplyError();
+ return;
+ }
+
+ IExecutionContext ctx = Debugger.DebugProcess.GetThread(Debugger.GThread.Value).Context;
+ if (Debugger.IsProcess32Bit)
+ {
+ Processor.Reply(GdbRegisters.Write32(ctx, gdbRegId, ss) && ss.IsEmpty());
+ }
+ else
+ {
+ Processor.Reply(GdbRegisters.Write64(ctx, gdbRegId, ss) && ss.IsEmpty());
+ }
+ }
+
+ internal void Step(ulong? newPc)
+ {
+ if (Debugger.CThread == null)
+ {
+ Processor.ReplyError();
+ return;
+ }
+
+ KThread thread = Debugger.DebugProcess.GetThread(Debugger.CThread.Value);
+
+ if (newPc.HasValue)
+ {
+ thread.Context.DebugPc = newPc.Value;
+ }
+
+ if (!Debugger.DebugProcess.DebugStep(thread))
+ {
+ Processor.ReplyError();
+ }
+ else
+ {
+ Debugger.GThread = Debugger.CThread = thread.ThreadUid;
+ Processor.Reply($"T05thread:{thread.ThreadUid:x};");
+ }
+ }
+
+ internal void IsAlive(ulong? threadId)
+ {
+ if (Debugger.GetThreads().Any(x => x.ThreadUid == threadId))
+ {
+ Processor.ReplyOK();
+ }
+ else
+ {
+ Processor.Reply("E00");
+ }
+ }
+
+ enum VContAction
+ {
+ None,
+ Continue,
+ Stop,
+ Step
+ }
+
+ record VContPendingAction(VContAction Action/*, ushort? Signal = null*/);
+
+ internal void VCont(StringStream ss)
+ {
+ string[] rawActions = ss.ReadRemaining().Split(';', StringSplitOptions.RemoveEmptyEntries);
+
+ Dictionary threadActionMap = new();
+ foreach (KThread thread in Debugger.GetThreads())
+ {
+ threadActionMap[thread.ThreadUid] = new VContPendingAction(VContAction.None);
+ }
+
+ VContAction defaultAction = VContAction.None;
+
+ // For each inferior thread, the *leftmost* action with a matching thread-id is applied.
+ for (int i = rawActions.Length - 1; i >= 0; i--)
+ {
+ string rawAction = rawActions[i];
+ StringStream stream = new(rawAction);
+
+ char cmd = stream.ReadChar();
+ VContAction action = cmd switch
+ {
+ 'c' or 'C' => VContAction.Continue,
+ 's' or 'S' => VContAction.Step,
+ 't' => VContAction.Stop,
+ _ => VContAction.None
+ };
+
+ // Note: We don't support signals yet.
+ //ushort? signal = null;
+ if (cmd is 'C' or 'S')
+ {
+ /*signal = (ushort)*/stream.ReadLengthAsHex(2);
+ // we still call the read length method even if we have signals commented
+ // since that method advances the underlying string position
+ }
+
+ ulong? threadId = null;
+ if (stream.ConsumePrefix(":"))
+ {
+ threadId = stream.ReadRemainingAsThreadUid();
+ }
+
+ if (threadId.HasValue)
+ {
+ if (threadActionMap.ContainsKey(threadId.Value))
+ {
+ threadActionMap[threadId.Value] = new VContPendingAction(action/*, signal*/);
+ }
+ }
+ else
+ {
+ foreach (ulong thread in threadActionMap.Keys)
+ {
+ threadActionMap[thread] = new VContPendingAction(action/*, signal*/);
+ }
+
+ if (action == VContAction.Continue)
+ {
+ defaultAction = action;
+ }
+ else
+ {
+ Logger.Warning?.Print(LogClass.GdbStub,
+ $"Received vCont command with unsupported default action: {rawAction}");
+ }
+ }
+ }
+
+ bool hasError = false;
+
+ foreach ((ulong threadUid, VContPendingAction action) in threadActionMap)
+ {
+ if (action.Action == VContAction.Step)
+ {
+ KThread thread = Debugger.DebugProcess.GetThread(threadUid);
+ if (!Debugger.DebugProcess.DebugStep(thread))
+ {
+ hasError = true;
+ }
+ }
+ }
+
+ // If we receive "vCont;c", just continue the process.
+ // If we receive something like "vCont;c:2e;c:2f" (IDA Pro will send commands like this), continue these threads.
+ // For "vCont;s:2f;c", `DebugProcess.DebugStep()` will continue and suspend other threads if needed, so we don't do anything here.
+ if (threadActionMap.Values.All(a => a.Action == VContAction.Continue))
+ {
+ Debugger.DebugProcess.DebugContinue();
+ }
+ else if (defaultAction == VContAction.None)
+ {
+ foreach ((ulong threadUid, VContPendingAction action) in threadActionMap)
+ {
+ if (action.Action == VContAction.Continue)
+ {
+ Debugger.DebugProcess.DebugContinue(Debugger.DebugProcess.GetThread(threadUid));
+ }
+ }
+ }
+
+ Processor.Reply(!hasError);
+
+ foreach ((ulong threadUid, VContPendingAction action) in threadActionMap)
+ {
+ if (action.Action == VContAction.Step)
+ {
+ Debugger.GThread = Debugger.CThread = threadUid;
+ Processor.Reply($"T05thread:{threadUid:x};");
+ }
+ }
+ }
+
+ internal void Q_Rcmd(string hexCommand)
+ {
+ try
+ {
+ string command = Helpers.FromHex(hexCommand);
+ Logger.Debug?.Print(LogClass.GdbStub, $"Received Rcmd: {command}");
+
+ string response = command.Trim().ToLowerInvariant() switch
+ {
+ "help" => "backtrace\nbt\nregisters\nreg\nget info\nminidump\n",
+ "get info" => Debugger.GetProcessInfo(),
+ "backtrace" or "bt" => Debugger.GetStackTrace(),
+ "registers" or "reg" => Debugger.GetRegisters(),
+ "minidump" => Debugger.GetMinidump(),
+ _ => $"Unknown command: {command}\n"
+ };
+
+ Processor.Reply(Helpers.ToHex(response));
+ }
+ catch (Exception e)
+ {
+ Logger.Error?.Print(LogClass.GdbStub, $"Error processing Rcmd: {e.Message}");
+ Processor.ReplyError();
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/Debugger/Gdb/Registers.cs b/src/Ryujinx.HLE/Debugger/Gdb/Registers.cs
new file mode 100644
index 000000000..f9ce7b153
--- /dev/null
+++ b/src/Ryujinx.HLE/Debugger/Gdb/Registers.cs
@@ -0,0 +1,160 @@
+using ARMeilleure.State;
+using Ryujinx.Cpu;
+using System;
+
+namespace Ryujinx.HLE.Debugger.Gdb
+{
+ static class GdbRegisters
+ {
+ /*
+ FPCR = FPSR & ~FpcrMask
+ All of FPCR's bits are reserved in FPCR and vice versa,
+ see ARM's documentation.
+ */
+ private const uint FpcrMask = 0xfc1fffff;
+
+ public static string Read64(IExecutionContext state, int gdbRegId)
+ {
+ switch (gdbRegId)
+ {
+ case >= 0 and <= 31:
+ return Helpers.ToHex(BitConverter.GetBytes(state.GetX(gdbRegId)));
+ case 32:
+ return Helpers.ToHex(BitConverter.GetBytes(state.DebugPc));
+ case 33:
+ return Helpers.ToHex(BitConverter.GetBytes(state.Pstate));
+ case >= 34 and <= 65:
+ return Helpers.ToHex(state.GetV(gdbRegId - 34).ToArray());
+ case 66:
+ return Helpers.ToHex(BitConverter.GetBytes((uint)state.Fpsr));
+ case 67:
+ return Helpers.ToHex(BitConverter.GetBytes((uint)state.Fpcr));
+ default:
+ return null;
+ }
+ }
+
+ public static bool Write64(IExecutionContext state, int gdbRegId, StringStream ss)
+ {
+ switch (gdbRegId)
+ {
+ case >= 0 and <= 31:
+ {
+ ulong value = ss.ReadLengthAsLEHex(16);
+ state.SetX(gdbRegId, value);
+ return true;
+ }
+ case 32:
+ {
+ ulong value = ss.ReadLengthAsLEHex(16);
+ state.DebugPc = value;
+ return true;
+ }
+ case 33:
+ {
+ ulong value = ss.ReadLengthAsLEHex(8);
+ state.Pstate = (uint)value;
+ return true;
+ }
+ case >= 34 and <= 65:
+ {
+ ulong value0 = ss.ReadLengthAsLEHex(16);
+ ulong value1 = ss.ReadLengthAsLEHex(16);
+ state.SetV(gdbRegId - 34, new V128(value0, value1));
+ return true;
+ }
+ case 66:
+ {
+ ulong value = ss.ReadLengthAsLEHex(8);
+ state.Fpsr = (uint)value;
+ return true;
+ }
+ case 67:
+ {
+ ulong value = ss.ReadLengthAsLEHex(8);
+ state.Fpcr = (uint)value;
+ return true;
+ }
+ default:
+ return false;
+ }
+ }
+
+ public static string Read32(IExecutionContext state, int gdbRegId)
+ {
+ switch (gdbRegId)
+ {
+ case >= 0 and <= 14:
+ return Helpers.ToHex(BitConverter.GetBytes((uint)state.GetX(gdbRegId)));
+ case 15:
+ return Helpers.ToHex(BitConverter.GetBytes((uint)state.DebugPc));
+ case 16:
+ return Helpers.ToHex(BitConverter.GetBytes((uint)state.Pstate));
+ case >= 17 and <= 32:
+ return Helpers.ToHex(state.GetV(gdbRegId - 17).ToArray());
+ case >= 33 and <= 64:
+ int reg = (gdbRegId - 33);
+ int n = reg / 2;
+ int shift = reg % 2;
+ ulong value = state.GetV(n).Extract(shift);
+ return Helpers.ToHex(BitConverter.GetBytes(value));
+ case 65:
+ uint fpscr = (uint)state.Fpsr | (uint)state.Fpcr;
+ return Helpers.ToHex(BitConverter.GetBytes(fpscr));
+ default:
+ return null;
+ }
+ }
+
+ public static bool Write32(IExecutionContext state, int gdbRegId, StringStream ss)
+ {
+ switch (gdbRegId)
+ {
+ case >= 0 and <= 14:
+ {
+ ulong value = ss.ReadLengthAsLEHex(8);
+ state.SetX(gdbRegId, value);
+ return true;
+ }
+ case 15:
+ {
+ ulong value = ss.ReadLengthAsLEHex(8);
+ state.DebugPc = value;
+ return true;
+ }
+ case 16:
+ {
+ ulong value = ss.ReadLengthAsLEHex(8);
+ state.Pstate = (uint)value;
+ return true;
+ }
+ case >= 17 and <= 32:
+ {
+ ulong value0 = ss.ReadLengthAsLEHex(16);
+ ulong value1 = ss.ReadLengthAsLEHex(16);
+ state.SetV(gdbRegId - 17, new V128(value0, value1));
+ return true;
+ }
+ case >= 33 and <= 64:
+ {
+ ulong value = ss.ReadLengthAsLEHex(16);
+ int regId = (gdbRegId - 33);
+ int regNum = regId / 2;
+ int shift = regId % 2;
+ V128 reg = state.GetV(regNum);
+ reg.Insert(shift, value);
+ return true;
+ }
+ case 65:
+ {
+ ulong value = ss.ReadLengthAsLEHex(8);
+ state.Fpsr = (uint)value & FpcrMask;
+ state.Fpcr = (uint)value & ~FpcrMask;
+ return true;
+ }
+ default:
+ return false;
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/Debugger/GdbXml/aarch64-core.xml b/src/Ryujinx.HLE/Debugger/Gdb/Xml/aarch64-core.xml
similarity index 100%
rename from src/Ryujinx.HLE/Debugger/GdbXml/aarch64-core.xml
rename to src/Ryujinx.HLE/Debugger/Gdb/Xml/aarch64-core.xml
diff --git a/src/Ryujinx.HLE/Debugger/GdbXml/aarch64-fpu.xml b/src/Ryujinx.HLE/Debugger/Gdb/Xml/aarch64-fpu.xml
similarity index 100%
rename from src/Ryujinx.HLE/Debugger/GdbXml/aarch64-fpu.xml
rename to src/Ryujinx.HLE/Debugger/Gdb/Xml/aarch64-fpu.xml
diff --git a/src/Ryujinx.HLE/Debugger/GdbXml/arm-core.xml b/src/Ryujinx.HLE/Debugger/Gdb/Xml/arm-core.xml
similarity index 100%
rename from src/Ryujinx.HLE/Debugger/GdbXml/arm-core.xml
rename to src/Ryujinx.HLE/Debugger/Gdb/Xml/arm-core.xml
diff --git a/src/Ryujinx.HLE/Debugger/GdbXml/arm-neon.xml b/src/Ryujinx.HLE/Debugger/Gdb/Xml/arm-neon.xml
similarity index 100%
rename from src/Ryujinx.HLE/Debugger/GdbXml/arm-neon.xml
rename to src/Ryujinx.HLE/Debugger/Gdb/Xml/arm-neon.xml
diff --git a/src/Ryujinx.HLE/Debugger/GdbXml/target32.xml b/src/Ryujinx.HLE/Debugger/Gdb/Xml/target32.xml
similarity index 100%
rename from src/Ryujinx.HLE/Debugger/GdbXml/target32.xml
rename to src/Ryujinx.HLE/Debugger/Gdb/Xml/target32.xml
diff --git a/src/Ryujinx.HLE/Debugger/GdbXml/target64.xml b/src/Ryujinx.HLE/Debugger/Gdb/Xml/target64.xml
similarity index 100%
rename from src/Ryujinx.HLE/Debugger/GdbXml/target64.xml
rename to src/Ryujinx.HLE/Debugger/Gdb/Xml/target64.xml
diff --git a/src/Ryujinx.HLE/Debugger/Helpers.cs b/src/Ryujinx.HLE/Debugger/Helpers.cs
new file mode 100644
index 000000000..eb243dc5b
--- /dev/null
+++ b/src/Ryujinx.HLE/Debugger/Helpers.cs
@@ -0,0 +1,50 @@
+using Gommon;
+using System;
+using System.Linq;
+using System.Text;
+
+namespace Ryujinx.HLE.Debugger
+{
+ public static class Helpers
+ {
+ public static byte CalculateChecksum(string cmd)
+ {
+ byte checksum = 0;
+ foreach (char x in cmd)
+ {
+ unchecked
+ {
+ checksum += (byte)x;
+ }
+ }
+
+ return checksum;
+ }
+
+ public static string FromHex(string hexString)
+ {
+ if (string.IsNullOrEmpty(hexString))
+ return string.Empty;
+
+ byte[] bytes = Convert.FromHexString(hexString);
+ return Encoding.ASCII.GetString(bytes);
+ }
+
+ public static string ToHex(byte[] bytes) => string.Join("", bytes.Select(x => $"{x:x2}"));
+
+ public static string ToHex(string str) => ToHex(Encoding.ASCII.GetBytes(str));
+
+ public static string ToBinaryFormat(string str) => ToBinaryFormat(Encoding.ASCII.GetBytes(str));
+ public static string ToBinaryFormat(byte[] bytes) =>
+ bytes.Select(x =>
+ x switch
+ {
+ (byte)'#' => "}\x03",
+ (byte)'$' => "}\x04",
+ (byte)'*' => "}\x0a",
+ (byte)'}' => "}\x5d",
+ _ => Convert.ToChar(x).ToString()
+ }
+ ).JoinToString(string.Empty);
+ }
+}
diff --git a/src/Ryujinx.HLE/Debugger/RegisterInformation.cs b/src/Ryujinx.HLE/Debugger/RegisterInformation.cs
index b5fd88ea5..c1c576558 100644
--- a/src/Ryujinx.HLE/Debugger/RegisterInformation.cs
+++ b/src/Ryujinx.HLE/Debugger/RegisterInformation.cs
@@ -17,8 +17,8 @@ namespace Ryujinx.HLE.Debugger
private static string GetEmbeddedResourceContent(string resourceName)
{
- Stream stream = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("Ryujinx.HLE.Debugger.GdbXml." + resourceName);
- StreamReader reader = new StreamReader(stream);
+ Stream stream = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("Ryujinx.HLE.Debugger.Gdb.Xml." + resourceName);
+ StreamReader reader = new(stream);
string result = reader.ReadToEnd();
reader.Dispose();
stream.Dispose();
diff --git a/src/Ryujinx.HLE/Debugger/StringStream.cs b/src/Ryujinx.HLE/Debugger/StringStream.cs
index d8148a9c2..bc422f51f 100644
--- a/src/Ryujinx.HLE/Debugger/StringStream.cs
+++ b/src/Ryujinx.HLE/Debugger/StringStream.cs
@@ -3,7 +3,7 @@ using System.Globalization;
namespace Ryujinx.HLE.Debugger
{
- class StringStream
+ internal class StringStream
{
private readonly string Data;
private int Position;
diff --git a/src/Ryujinx.HLE/HOS/Kernel/KernelStatic.cs b/src/Ryujinx.HLE/HOS/Kernel/KernelStatic.cs
index e05fc8397..e3c5865ae 100644
--- a/src/Ryujinx.HLE/HOS/Kernel/KernelStatic.cs
+++ b/src/Ryujinx.HLE/HOS/Kernel/KernelStatic.cs
@@ -29,6 +29,7 @@ namespace Ryujinx.HLE.HOS.Kernel
capabilities,
context.ResourceLimit,
MemoryRegion.Service,
+ context.Device.Configuration.MemoryConfiguration,
null,
customThreadStart);
diff --git a/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTableBase.cs b/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTableBase.cs
index ee1b4a7be..bc59b0b4d 100644
--- a/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTableBase.cs
+++ b/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTableBase.cs
@@ -102,6 +102,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory
ProcessCreationFlags flags,
bool fromBack,
MemoryRegion memRegion,
+ MemoryConfiguration memConfig,
ulong address,
ulong size,
KMemoryBlockSlabManager slabManager)
@@ -117,6 +118,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory
addrSpaceBase,
addrSpaceSize,
memRegion,
+ memConfig,
address,
size,
slabManager);
@@ -159,6 +161,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory
ulong addrSpaceStart,
ulong addrSpaceEnd,
MemoryRegion memRegion,
+ MemoryConfiguration memConfig,
ulong address,
ulong size,
KMemoryBlockSlabManager slabManager)
@@ -193,7 +196,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory
case ProcessCreationFlags.AddressSpace64BitDeprecated:
aliasRegion.Size = 0x180000000;
- heapRegion.Size = 0x180000000;
+ heapRegion.Size = memConfig == MemoryConfiguration.MemoryConfiguration12GiB ? 0x300000000u : 0x180000000u;
stackRegion.Size = 0;
tlsIoRegion.Size = 0;
CodeRegionStart = 0x8000000;
@@ -223,7 +226,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory
int addressSpaceWidth = (int)ulong.Log2(_reservedAddressSpaceSize);
aliasRegion.Size = 1UL << (addressSpaceWidth - 3);
- heapRegion.Size = 0x180000000;
+ heapRegion.Size = memConfig == MemoryConfiguration.MemoryConfiguration12GiB ? 0x300000000u : 0x180000000u;
stackRegion.Size = 1UL << (addressSpaceWidth - 8);
tlsIoRegion.Size = 1UL << (addressSpaceWidth - 3);
CodeRegionStart = BitUtils.AlignDown(address, RegionAlignment);
@@ -237,7 +240,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory
else
{
aliasRegion.Size = 0x1000000000;
- heapRegion.Size = 0x180000000;
+ heapRegion.Size = memConfig == MemoryConfiguration.MemoryConfiguration12GiB ? 0x300000000u : 0x180000000u;
stackRegion.Size = 0x80000000;
tlsIoRegion.Size = 0x1000000000;
CodeRegionStart = BitUtils.AlignDown(address, RegionAlignment);
diff --git a/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs b/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs
index bea1f0ef3..eb75fb9a1 100644
--- a/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs
+++ b/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs
@@ -124,6 +124,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
KPageList pageList,
KResourceLimit resourceLimit,
MemoryRegion memRegion,
+ MemoryConfiguration memConfig,
IProcessContextFactory contextFactory,
ThreadStart customThreadStart = null)
{
@@ -153,6 +154,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
creationInfo.Flags,
!creationInfo.Flags.HasFlag(ProcessCreationFlags.EnableAslr),
memRegion,
+ memConfig,
codeAddress,
codeSize,
slabManager);
@@ -189,6 +191,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
ReadOnlySpan capabilities,
KResourceLimit resourceLimit,
MemoryRegion memRegion,
+ MemoryConfiguration memConfig,
IProcessContextFactory contextFactory,
ThreadStart customThreadStart = null)
{
@@ -252,6 +255,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
creationInfo.Flags,
!creationInfo.Flags.HasFlag(ProcessCreationFlags.EnableAslr),
memRegion,
+ memConfig,
codeAddress,
codeSize,
slabManager);
@@ -1088,7 +1092,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
MemoryManager = new KPageTable(KernelContext, CpuMemory, Context.AddressSpaceSize);
}
- private bool InvalidAccessHandler(ulong va)
+ private static bool InvalidAccessHandler(ulong va)
{
KernelStatic.GetCurrentThread()?.PrintGuestStackTrace();
KernelStatic.GetCurrentThread()?.PrintGuestRegisterPrintout();
@@ -1100,7 +1104,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
return false;
}
- private void UndefinedInstructionHandler(IExecutionContext context, ulong address, int opCode)
+ private static void UndefinedInstructionHandler(IExecutionContext context, ulong address, int opCode)
{
KernelStatic.GetCurrentThread().PrintGuestStackTrace();
KernelStatic.GetCurrentThread()?.PrintGuestRegisterPrintout();
@@ -1204,16 +1208,16 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
private class DebuggerInterface : IDebuggableProcess
{
- private Barrier StepBarrier;
+ private readonly Barrier _stepBarrier;
private readonly KProcess _parent;
private readonly KernelContext _kernelContext;
- private KThread steppingThread;
+ private KThread _steppingThread;
public DebuggerInterface(KProcess p)
{
_parent = p;
_kernelContext = p.KernelContext;
- StepBarrier = new(2);
+ _stepBarrier = new(2);
}
public void DebugStop()
@@ -1281,7 +1285,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
}
_kernelContext.CriticalSection.Enter();
- steppingThread = target;
+ _steppingThread = target;
bool waiting = target.MutexOwner != null || target.WaitingSync || target.WaitingInArbitration;
target.Context.RequestDebugStep();
if (waiting)
@@ -1301,14 +1305,14 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
_kernelContext.CriticalSection.Leave();
bool stepTimedOut = false;
- if (!StepBarrier.SignalAndWait(TimeSpan.FromMilliseconds(2000)))
+ if (!_stepBarrier.SignalAndWait(TimeSpan.FromMilliseconds(2000)))
{
Logger.Warning?.Print(LogClass.Kernel, $"Failed to step thread {target.ThreadUid} in time.");
stepTimedOut = true;
}
_kernelContext.CriticalSection.Enter();
- steppingThread = null;
+ _steppingThread = null;
if (waiting)
{
lock (_parent._threadingLock)
@@ -1330,7 +1334,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
return false;
}
- StepBarrier.SignalAndWait();
+ _stepBarrier.SignalAndWait();
return true;
}
@@ -1365,12 +1369,12 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
public void DebugInterruptHandler(IExecutionContext ctx)
{
_kernelContext.CriticalSection.Enter();
- bool stepping = steppingThread != null;
+ bool stepping = _steppingThread != null;
_kernelContext.CriticalSection.Leave();
if (stepping)
{
- StepBarrier.SignalAndWait();
- StepBarrier.SignalAndWait();
+ _stepBarrier.SignalAndWait();
+ _stepBarrier.SignalAndWait();
}
_parent.InterruptHandler(ctx);
}
diff --git a/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/Syscall.cs b/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/Syscall.cs
index 005ac1452..260ff8af3 100644
--- a/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/Syscall.cs
+++ b/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/Syscall.cs
@@ -137,6 +137,7 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall
capabilities,
resourceLimit,
memRegion,
+ _context.Device.Configuration.MemoryConfiguration,
contextFactory,
customThreadStart);
@@ -888,7 +889,7 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall
[Svc(1)]
public Result SetHeapSize([PointerSized] out ulong address, [PointerSized] ulong size)
{
- if ((size & 0xfffffffe001fffff) != 0)
+ if ((size & 0xfffffffd001fffff) != 0)
{
address = 0;
diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs
index 5729052e8..7f0c6b3f5 100644
--- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs
+++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs
@@ -189,7 +189,7 @@ namespace Ryujinx.HLE.Loaders.Processes
codeAddress,
codeSize);
- result = process.InitializeKip(creationInfo, kip.Capabilities, pageList, context.ResourceLimit, memoryRegion, processContextFactory);
+ result = process.InitializeKip(creationInfo, kip.Capabilities, pageList, context.ResourceLimit, memoryRegion, context.Device.Configuration.MemoryConfiguration, processContextFactory);
if (result != Result.Success)
{
Logger.Error?.Print(LogClass.Loader, $"Process initialization returned error \"{result}\".");
@@ -389,6 +389,7 @@ namespace Ryujinx.HLE.Loaders.Processes
MemoryMarshal.Cast(npdm.KernelCapabilityData),
resourceLimit,
memoryRegion,
+ context.Device.Configuration.MemoryConfiguration,
processContextFactory);
if (result != Result.Success)
diff --git a/src/Ryujinx.HLE/Ryujinx.HLE.csproj b/src/Ryujinx.HLE/Ryujinx.HLE.csproj
index 1938796e8..7e4c8a9e1 100644
--- a/src/Ryujinx.HLE/Ryujinx.HLE.csproj
+++ b/src/Ryujinx.HLE/Ryujinx.HLE.csproj
@@ -33,12 +33,12 @@
-
-
-
-
-
-
+
+
+
+
+
+
@@ -48,12 +48,12 @@
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/src/Ryujinx/UI/ViewModels/AmiiboWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/AmiiboWindowViewModel.cs
index 51541b615..0ba071475 100644
--- a/src/Ryujinx/UI/ViewModels/AmiiboWindowViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/AmiiboWindowViewModel.cs
@@ -6,6 +6,7 @@ using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Common.Models.Amiibo;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Windows;
+using Ryujinx.Ava.Utilities;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
@@ -250,6 +251,7 @@ namespace Ryujinx.Ava.UI.ViewModels
catch (Exception exception)
{
Logger.Warning?.Print(LogClass.Application, $"Unable to read data from '{_amiiboJsonPath}': {exception}");
+ localIsValid = false;
}
if (!localIsValid || await NeedsUpdate(amiiboJson.LastUpdated))
@@ -280,11 +282,59 @@ namespace Ryujinx.Ava.UI.ViewModels
return amiiboJson;
}
+ private async Task ReadLocalJsonFileAsync()
+ {
+ bool isValid = false;
+ AmiiboJson amiiboJson = new();
+
+ try
+ {
+ try
+ {
+ if (File.Exists(_amiiboJsonPath))
+ {
+ isValid = TryGetAmiiboJson(await File.ReadAllTextAsync(_amiiboJsonPath), out amiiboJson);
+ }
+ }
+ catch (Exception exception)
+ {
+ Logger.Warning?.Print(LogClass.Application, $"Unable to read data from '{_amiiboJsonPath}': {exception}");
+ isValid = false;
+ }
+
+ if (!isValid)
+ {
+ return null;
+ }
+ }
+ catch (Exception exception)
+ {
+ if (!isValid)
+ {
+ Logger.Error?.Print(LogClass.Application, $"Couldn't get valid amiibo data: {exception}");
+
+ // Neither local file is not valid JSON, close window.
+ await ShowInfoDialog();
+ Close();
+ }
+ }
+
+ return amiiboJson;
+ }
+
private async Task LoadContentAsync()
{
- AmiiboJson amiiboJson = await GetMostRecentAmiiboListOrDefaultJson();
+ AmiiboJson? amiiboJson;
- _amiiboList = amiiboJson.Amiibo.OrderBy(amiibo => amiibo.AmiiboSeries).ToList();
+ if (CommandLineState.OnlyLocalAmiibo)
+ amiiboJson = await ReadLocalJsonFileAsync();
+ else
+ amiiboJson = await GetMostRecentAmiiboListOrDefaultJson();
+
+ if (!amiiboJson.HasValue)
+ return;
+
+ _amiiboList = amiiboJson.Value.Amiibo.OrderBy(amiibo => amiibo.AmiiboSeries).ToList();
ParseAmiiboData();
}
diff --git a/src/Ryujinx/Utilities/CommandLineState.cs b/src/Ryujinx/Utilities/CommandLineState.cs
index d7d113ebe..f156792a1 100644
--- a/src/Ryujinx/Utilities/CommandLineState.cs
+++ b/src/Ryujinx/Utilities/CommandLineState.cs
@@ -25,6 +25,7 @@ namespace Ryujinx.Ava.Utilities
public static string LaunchApplicationId { get; private set; }
public static bool StartFullscreenArg { get; private set; }
public static bool HideAvailableUpdates { get; private set; }
+ public static bool OnlyLocalAmiibo { get; private set; }
public static void ParseArguments(string[] args)
{
@@ -130,6 +131,10 @@ namespace Ryujinx.Ava.Utilities
OverridePPTC = args[++i];
break;
+ case "-la":
+ case "--local-only-amiibo":
+ OnlyLocalAmiibo = true;
+ break;
case "-m":
case "--memory-manager-mode":
if (i + 1 >= args.Length)