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 2c3b23aec..301c87355 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)",
@@ -6445,26 +6445,27 @@
{
"ID": "SettingsButtonResetConfirm",
"Translations": {
- "ar_SA": "أريد إعادة تعيين إعداداتي",
- "de_DE": "Ich möchte meine Einstellungen zurücksetzen",
- "el_GR": "Θέλω να επαναφέρω τις ρυθμίσεις μου",
- "en_US": "I Want To Reset My Settings",
- "es_ES": "Quiero Restablecer Mi Configuración",
- "fr_FR": "Je Veux Réinitialiser Mes Paramètres",
- "he_IL": "אני רוצה לאפס את ההגדרות שלי",
- "it_IT": "Voglio reimpostare le mie impostazioni",
- "ja_JP": "設定をリセットしたいです",
- "ko_KR": "설정을 초기화하고 싶습니다",
- "no_NO": "Jeg vil tilbakestille innstillingene mine",
- "pl_PL": "Chcę zresetować moje ustawienia",
- "pt_BR": "Quero redefinir minhas configurações",
- "ru_RU": "Я хочу сбросить свои настройки",
- "sv_SE": "Jag vill nollställa mina inställningar",
- "th_TH": "ฉันต้องการรีเซ็ตการตั้งค่าของฉัน",
- "tr_TR": "Ayarlarımı sıfırlamak istiyorum",
- "uk_UA": "Я хочу скинути налаштування",
- "zh_CN": "我要重置我的设置",
- "zh_TW": "我想重設我的設定"
+ "ar_SA": "",
+ "de_DE": "",
+ "el_GR": "",
+ "en_US": "I want to reset my settings.",
+ "es_ES": "Quiero restablecer mi Configuración.",
+ "fr_FR": "Je veux réinitialiser mes paramètres.",
+ "he_IL": "",
+ "it_IT": "Voglio ripristinare le mie impostazioni.",
+ "ja_JP": "",
+ "ko_KR": "설정을 초기화하고 싶습니다.",
+ "no_NO": "Jeg vil tilbakestille innstillingene mine.",
+ "pl_PL": "",
+ "pt_BR": "Quero redefinir minhas configurações.",
+ "ru_RU": "Я хочу сбросить свои настройки.",
+ "sv_SE": "Jag vill nollställa mina inställningar.",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "Я хочу скинути налаштування.",
+ "zh_CN": "我要重置我的设置。",
+ "zh_TW": "我想重設我的設定。"
+
}
},
{
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/Collections/BitMap.cs b/src/Ryujinx.Common/Collections/BitMap.cs
new file mode 100644
index 000000000..2c9211300
--- /dev/null
+++ b/src/Ryujinx.Common/Collections/BitMap.cs
@@ -0,0 +1,226 @@
+namespace Ryujinx.Common.Collections
+{
+ ///
+ /// Represents a collection that can store 1 bit values.
+ ///
+ public struct BitMap
+ {
+ ///
+ /// Size in bits of the integer used internally for the groups of bits.
+ ///
+ public const int IntSize = 64;
+
+ private const int IntShift = 6;
+ private const int IntMask = IntSize - 1;
+
+ private readonly long[] _masks;
+
+ ///
+ /// Gets or sets the value of a bit.
+ ///
+ /// Bit to access
+ /// Bit value
+ public bool this[int bit]
+ {
+ get => IsSet(bit);
+ set
+ {
+ if (value)
+ {
+ Set(bit);
+ }
+ else
+ {
+ Clear(bit);
+ }
+ }
+ }
+
+ ///
+ /// Creates a new bitmap.
+ ///
+ /// Total number of bits
+ public BitMap(int count)
+ {
+ _masks = new long[(count + IntMask) / IntSize];
+ }
+
+ ///
+ /// Checks if any bit is set.
+ ///
+ /// True if any bit is set, false otherwise
+ public bool AnySet()
+ {
+ for (int i = 0; i < _masks.Length; i++)
+ {
+ if (_masks[i] != 0)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Checks if a specific bit is set.
+ ///
+ /// Bit to be checked
+ /// True if set, false otherwise
+ public bool IsSet(int bit)
+ {
+ int wordIndex = bit >> IntShift;
+ int wordBit = bit & IntMask;
+
+ long wordMask = 1L << wordBit;
+
+ return (_masks[wordIndex] & wordMask) != 0;
+ }
+
+ ///
+ /// Checks if any bit inside a given range of bits is set.
+ ///
+ /// Start bit of the range
+ /// End bit of the range (inclusive)
+ /// True if any bit is set, false otherwise
+ public bool IsSet(int start, int end)
+ {
+ if (start == end)
+ {
+ return IsSet(start);
+ }
+
+ int startIndex = start >> IntShift;
+ int startBit = start & IntMask;
+ long startMask = -1L << startBit;
+
+ int endIndex = end >> IntShift;
+ int endBit = end & IntMask;
+ long endMask = (long)(ulong.MaxValue >> (IntMask - endBit));
+
+ if (startIndex == endIndex)
+ {
+ return (_masks[startIndex] & startMask & endMask) != 0;
+ }
+
+ if ((_masks[startIndex] & startMask) != 0)
+ {
+ return true;
+ }
+
+ for (int i = startIndex + 1; i < endIndex; i++)
+ {
+ if (_masks[i] != 0)
+ {
+ return true;
+ }
+ }
+
+ if ((_masks[endIndex] & endMask) != 0)
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Sets the value of a bit to 1.
+ ///
+ /// Bit to be set
+ /// True if the bit was 0 and then changed to 1, false if it was already 1
+ public bool Set(int bit)
+ {
+ int wordIndex = bit >> IntShift;
+ int wordBit = bit & IntMask;
+
+ long wordMask = 1L << wordBit;
+
+ if ((_masks[wordIndex] & wordMask) != 0)
+ {
+ return false;
+ }
+
+ _masks[wordIndex] |= wordMask;
+
+ return true;
+ }
+
+ ///
+ /// Sets a given range of bits to 1.
+ ///
+ /// Start bit of the range
+ /// End bit of the range (inclusive)
+ public void SetRange(int start, int end)
+ {
+ if (start == end)
+ {
+ Set(start);
+ return;
+ }
+
+ int startIndex = start >> IntShift;
+ int startBit = start & IntMask;
+ long startMask = -1L << startBit;
+
+ int endIndex = end >> IntShift;
+ int endBit = end & IntMask;
+ long endMask = (long)(ulong.MaxValue >> (IntMask - endBit));
+
+ if (startIndex == endIndex)
+ {
+ _masks[startIndex] |= startMask & endMask;
+ }
+ else
+ {
+ _masks[startIndex] |= startMask;
+
+ for (int i = startIndex + 1; i < endIndex; i++)
+ {
+ _masks[i] |= -1;
+ }
+
+ _masks[endIndex] |= endMask;
+ }
+ }
+
+ ///
+ /// Sets a given bit to 0.
+ ///
+ /// Bit to be cleared
+ public void Clear(int bit)
+ {
+ int wordIndex = bit >> IntShift;
+ int wordBit = bit & IntMask;
+
+ long wordMask = 1L << wordBit;
+
+ _masks[wordIndex] &= ~wordMask;
+ }
+
+ ///
+ /// Sets all bits to 0.
+ ///
+ public void Clear()
+ {
+ for (int i = 0; i < _masks.Length; i++)
+ {
+ _masks[i] = 0;
+ }
+ }
+
+ ///
+ /// Sets one or more groups of bits to 0.
+ /// See for how many bits are inside each group.
+ ///
+ /// Start index of the group
+ /// End index of the group (inclusive)
+ public void ClearInt(int start, int end)
+ {
+ for (int i = start; i <= end; i++)
+ {
+ _masks[i] = 0;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Common/Logging/Logger.cs b/src/Ryujinx.Common/Logging/Logger.cs
index e6f68599a..0d8bd3ac3 100644
--- a/src/Ryujinx.Common/Logging/Logger.cs
+++ b/src/Ryujinx.Common/Logging/Logger.cs
@@ -187,6 +187,17 @@ namespace Ryujinx.Common.Logging
}
}
+ public static void Flush()
+ {
+ foreach (ILogTarget target in _logTargets)
+ {
+ if (target is AsyncLogTargetWrapper asyncTarget)
+ {
+ asyncTarget.Flush();
+ }
+ }
+ }
+
public static void Shutdown()
{
Updated = null;
diff --git a/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs b/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs
index 1fcfea4da..34f52d6ab 100644
--- a/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs
+++ b/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs
@@ -27,6 +27,17 @@ namespace Ryujinx.Common.Logging.Targets
private readonly int _overflowTimeout;
+ private sealed class FlushEventArgs : LogEventArgs
+ {
+ public readonly ManualResetEventSlim SignalEvent;
+
+ public FlushEventArgs(ManualResetEventSlim signalEvent)
+ : base(LogLevel.Notice, TimeSpan.Zero, string.Empty, string.Empty)
+ {
+ SignalEvent = signalEvent;
+ }
+ }
+
string ILogTarget.Name => _target.Name;
public AsyncLogTargetWrapper(ILogTarget target, int queueLimit = -1, AsyncLogTargetOverflowAction overflowAction = AsyncLogTargetOverflowAction.Block)
@@ -41,7 +52,15 @@ namespace Ryujinx.Common.Logging.Targets
{
try
{
- _target.Log(this, _messageQueue.Take());
+ LogEventArgs item = _messageQueue.Take();
+
+ if (item is FlushEventArgs flush)
+ {
+ flush.SignalEvent.Set();
+ continue;
+ }
+
+ _target.Log(this, item);
}
catch (InvalidOperationException)
{
@@ -68,6 +87,26 @@ namespace Ryujinx.Common.Logging.Targets
}
}
+ public void Flush()
+ {
+ if (_messageQueue.Count == 0 || _messageQueue.IsAddingCompleted)
+ {
+ return;
+ }
+
+ using var signal = new ManualResetEventSlim(false);
+ try
+ {
+ _messageQueue.Add(new FlushEventArgs(signal));
+ }
+ catch (InvalidOperationException)
+ {
+ return;
+ }
+
+ signal.Wait();
+ }
+
public void Dispose()
{
GC.SuppressFinalize(this);
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.Graphics.Gpu/Image/TexturePool.cs b/src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs
index fd1609c23..c2a503840 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs
@@ -1,3 +1,4 @@
+using Ryujinx.Common.Collections;
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Gpu.Memory;
@@ -72,6 +73,7 @@ namespace Ryujinx.Graphics.Gpu.Image
}
private readonly GpuChannel _channel;
+ private readonly BitMap _invalidMap;
private readonly ConcurrentQueue _dereferenceQueue = new();
private TextureDescriptor _defaultDescriptor;
@@ -166,6 +168,7 @@ namespace Ryujinx.Graphics.Gpu.Image
{
_channel = channel;
_aliasLists = new Dictionary();
+ _invalidMap = new BitMap(maximumId + 1);
}
///
@@ -182,6 +185,11 @@ namespace Ryujinx.Graphics.Gpu.Image
if (texture == null)
{
+ if (_invalidMap.IsSet(id))
+ {
+ return ref descriptor;
+ }
+
texture = PhysicalMemory.TextureCache.FindShortCache(descriptor);
if (texture == null)
@@ -198,6 +206,7 @@ namespace Ryujinx.Graphics.Gpu.Image
// If this happens, then the texture address is invalid, we can't add it to the cache.
if (texture == null)
{
+ _invalidMap.Set(id);
return ref descriptor;
}
}
@@ -515,6 +524,8 @@ namespace Ryujinx.Graphics.Gpu.Image
RemoveAliasList(texture);
}
}
+
+ _invalidMap.Clear(id);
}
}
diff --git a/src/Ryujinx.HLE/Debugger/BreakpointManager.cs b/src/Ryujinx.HLE/Debugger/BreakpointManager.cs
index bf462a781..c660b298d 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;
}
}
@@ -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,30 +121,6 @@ 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)
diff --git a/src/Ryujinx.HLE/Debugger/Debugger.cs b/src/Ryujinx.HLE/Debugger/Debugger.cs
index 66e42681b..cc64a38eb 100644
--- a/src/Ryujinx.HLE/Debugger/Debugger.cs
+++ b/src/Ryujinx.HLE/Debugger/Debugger.cs
@@ -1,13 +1,9 @@
-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;
@@ -15,6 +11,7 @@ using System.Net.Sockets;
using System.Text;
using System.Threading;
using IExecutionContext = Ryujinx.Cpu.IExecutionContext;
+using static Ryujinx.HLE.Debugger.Helpers;
namespace Ryujinx.HLE.Debugger
{
@@ -28,18 +25,18 @@ namespace Ryujinx.HLE.Debugger
private Socket ClientSocket = null;
private NetworkStream ReadStream = null;
private NetworkStream WriteStream = null;
- private BlockingCollection Messages = new BlockingCollection(1);
+ private BlockingCollection Messages = new(1);
private Thread DebuggerThread;
private Thread MessageHandlerThread;
private bool _shuttingDown = false;
- private ManualResetEventSlim _breakHandlerEvent = new ManualResetEventSlim(false);
+ private ManualResetEventSlim _breakHandlerEvent = new(false);
- private ulong? cThread;
- private ulong? gThread;
+ private GdbCommandProcessor CommandProcessor = null;
- private BreakpointManager BreakpointManager;
+ internal ulong? CThread;
+ internal ulong? GThread;
- private string previousThreadListXml = "";
+ internal BreakpointManager BreakpointManager;
public Debugger(Switch device, ushort port)
{
@@ -57,172 +54,24 @@ namespace Ryujinx.HLE.Debugger
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 IsProcessAarch32 => DebugProcess.GetThread(GThread.Value).Context.IsAarch32;
private void MessageHandlerMain()
{
while (!_shuttingDown)
{
IMessage msg = Messages.Take();
- try {
+ try
+ {
switch (msg)
{
case BreakInMessage:
Logger.Notice.Print(LogClass.GdbStub, "Break-in requested");
- CommandInterrupt();
+ CommandProcessor.Commands.CommandInterrupt();
break;
case SendNackMessage:
@@ -232,14 +81,14 @@ namespace Ryujinx.HLE.Debugger
case CommandMessage { Command: var cmd }:
Logger.Debug?.Print(LogClass.GdbStub, $"Received Command: {cmd}");
WriteStream.WriteByte((byte)'+');
- ProcessCommand(cmd);
+ 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.Commands.Reply($"T05thread:{ctx.ThreadUid:x};");
break;
case KillMessage:
@@ -254,830 +103,72 @@ 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)
- {
- threadId = GetThreads().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\n",
- "get info" => GetProcessInfo(),
- "backtrace" => GetStackTrace(),
- "bt" => GetStackTrace(),
- "registers" => GetRegisters(),
- "reg" => GetRegisters(),
- _ => $"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 GetProcessInfo()
+ public string GetMinidump()
+ {
+ var response = new StringBuilder();
+ response.AppendLine("=== Begin Minidump ===\n");
+ response.AppendLine(GetProcessInfo());
+
+ foreach (var thread in GetThreads())
+ {
+ response.AppendLine($"=== Thread {thread.ThreadUid} ===");
+ try
+ {
+ string stackTrace = Process.Debugger.GetGuestStackTrace(thread);
+ response.AppendLine(stackTrace);
+ }
+ catch (Exception e)
+ {
+ response.AppendLine($"[Error getting stack trace: {e.Message}]");
+ }
+
+ try
+ {
+ string registers = Process.Debugger.GetCpuRegisterPrintout(thread);
+ response.AppendLine(registers);
+ }
+ catch (Exception e)
+ {
+ response.AppendLine($"[Error getting registers: {e.Message}]");
+ }
+ }
+
+ response.AppendLine("=== End Minidump ===");
+
+ Logger.Info?.Print(LogClass.GdbStub, response.ToString());
+ return response.ToString();
+ }
+
+ public string GetProcessInfo()
{
try
{
@@ -1087,15 +178,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)
@@ -1109,7 +204,7 @@ namespace Ryujinx.HLE.Debugger
sb.AppendLine($" 0x{image.BaseAddress:x10} - 0x{endAddress:x10} {name}");
}
}
-
+
return sb.ToString();
}
catch (Exception e)
@@ -1119,22 +214,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);
@@ -1155,13 +234,15 @@ namespace Ryujinx.HLE.Debugger
// If the user connects before the application is running, wait for the application to start.
int retries = 10;
- while (DebugProcess == null && retries-- > 0)
+ while ((DebugProcess == null || GetThreads().Length == 0) && retries-- > 0)
{
Thread.Sleep(200);
}
- if (DebugProcess == null)
+
+ if (DebugProcess == null || GetThreads().Length == 0)
{
- Logger.Warning?.Print(LogClass.GdbStub, "Application is not running, cannot accept GDB client connection");
+ Logger.Warning?.Print(LogClass.GdbStub,
+ "Application is not running, cannot accept GDB client connection");
ClientSocket.Close();
continue;
}
@@ -1169,6 +250,7 @@ namespace Ryujinx.HLE.Debugger
ClientSocket.NoDelay = true;
ReadStream = new NetworkStream(ClientSocket, System.IO.FileAccess.Read);
WriteStream = new NetworkStream(ClientSocket, System.IO.FileAccess.Write);
+ CommandProcessor = new GdbCommandProcessor(ListenerSocket, ClientSocket, ReadStream, WriteStream, this);
Logger.Notice.Print(LogClass.GdbStub, "GDB client connected");
while (true)
@@ -1178,7 +260,7 @@ namespace Ryujinx.HLE.Debugger
switch (ReadStream.ReadByte())
{
case -1:
- goto eof;
+ goto EndOfLoop;
case '+':
continue;
case '-':
@@ -1193,7 +275,7 @@ namespace Ryujinx.HLE.Debugger
{
int x = ReadStream.ReadByte();
if (x == -1)
- goto eof;
+ goto EndOfLoop;
if (x == '#')
break;
cmd += (char)x;
@@ -1214,11 +296,11 @@ namespace Ryujinx.HLE.Debugger
}
catch (IOException)
{
- goto eof;
+ goto EndOfLoop;
}
}
- eof:
+ EndOfLoop:
Logger.Notice.Print(LogClass.GdbStub, "GDB client lost connection");
ReadStream.Close();
ReadStream = null;
@@ -1226,63 +308,12 @@ namespace Ryujinx.HLE.Debugger
WriteStream = null;
ClientSocket.Close();
ClientSocket = null;
+ CommandProcessor = 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);
@@ -1315,7 +346,7 @@ namespace Ryujinx.HLE.Debugger
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..e9986647d
--- /dev/null
+++ b/src/Ryujinx.HLE/Debugger/Gdb/CommandProcessor.cs
@@ -0,0 +1,393 @@
+using Ryujinx.Common;
+using Ryujinx.Common.Logging;
+using System.Linq;
+using System.Net.Sockets;
+using System.Text;
+
+namespace Ryujinx.HLE.Debugger.Gdb
+{
+ class GdbCommandProcessor
+ {
+ public readonly GdbCommands Commands;
+
+ public GdbCommandProcessor(TcpListener listenerSocket, Socket clientSocket, NetworkStream readStream, NetworkStream writeStream, Debugger debugger)
+ {
+ Commands = new GdbCommands(listenerSocket, clientSocket, readStream, writeStream, debugger);
+ }
+
+ private string previousThreadListXml = "";
+
+ public void Process(string cmd)
+ {
+ StringStream ss = new(cmd);
+
+ switch (ss.ReadChar())
+ {
+ case '!':
+ if (!ss.IsEmpty())
+ {
+ goto unknownCommand;
+ }
+
+ // Enable extended mode
+ Commands.ReplyOK();
+ break;
+ case '?':
+ if (!ss.IsEmpty())
+ {
+ goto unknownCommand;
+ }
+
+ Commands.CommandQuery();
+ break;
+ case 'c':
+ Commands.CommandContinue(ss.IsEmpty() ? null : ss.ReadRemainingAsHex());
+ break;
+ case 'D':
+ if (!ss.IsEmpty())
+ {
+ goto unknownCommand;
+ }
+
+ Commands.CommandDetach();
+ break;
+ case 'g':
+ if (!ss.IsEmpty())
+ {
+ goto unknownCommand;
+ }
+
+ Commands.CommandReadRegisters();
+ break;
+ case 'G':
+ Commands.CommandWriteRegisters(ss);
+ break;
+ case 'H':
+ {
+ char op = ss.ReadChar();
+ ulong? threadId = ss.ReadRemainingAsThreadUid();
+ Commands.CommandSetThread(op, threadId);
+ break;
+ }
+ case 'k':
+ Logger.Notice.Print(LogClass.GdbStub, "Kill request received, detach instead");
+ Commands.Reply("");
+ Commands.CommandDetach();
+ break;
+ case 'm':
+ {
+ ulong addr = ss.ReadUntilAsHex(',');
+ ulong len = ss.ReadRemainingAsHex();
+ Commands.CommandReadMemory(addr, len);
+ break;
+ }
+ case 'M':
+ {
+ ulong addr = ss.ReadUntilAsHex(',');
+ ulong len = ss.ReadUntilAsHex(':');
+ Commands.CommandWriteMemory(addr, len, ss);
+ break;
+ }
+ case 'p':
+ {
+ ulong gdbRegId = ss.ReadRemainingAsHex();
+ Commands.CommandReadRegister((int)gdbRegId);
+ break;
+ }
+ case 'P':
+ {
+ ulong gdbRegId = ss.ReadUntilAsHex('=');
+ Commands.CommandWriteRegister((int)gdbRegId, ss);
+ break;
+ }
+ case 'q':
+ if (ss.ConsumeRemaining("GDBServerVersion"))
+ {
+ Commands.Reply($"name:Ryujinx;version:{ReleaseInformation.Version};");
+ break;
+ }
+
+ if (ss.ConsumeRemaining("HostInfo"))
+ {
+ if (Commands.Debugger.IsProcessAarch32)
+ {
+ Commands.Reply(
+ $"triple:{Helpers.ToHex("arm-unknown-linux-android")};endian:little;ptrsize:4;hostname:{Helpers.ToHex("Ryujinx")};");
+ }
+ else
+ {
+ Commands.Reply(
+ $"triple:{Helpers.ToHex("aarch64-unknown-linux-android")};endian:little;ptrsize:8;hostname:{Helpers.ToHex("Ryujinx")};");
+ }
+
+ break;
+ }
+
+ if (ss.ConsumeRemaining("ProcessInfo"))
+ {
+ if (Commands.Debugger.IsProcessAarch32)
+ {
+ Commands.Reply(
+ $"pid:1;cputype:12;cpusubtype:0;triple:{Helpers.ToHex("arm-unknown-linux-android")};ostype:unknown;vendor:none;endian:little;ptrsize:4;");
+ }
+ else
+ {
+ Commands.Reply(
+ $"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"))
+ {
+ Commands.Reply("PacketSize=10000;qXfer:features:read+;qXfer:threads:read+;vContSupported+");
+ break;
+ }
+
+ if (ss.ConsumePrefix("Rcmd,"))
+ {
+ string hexCommand = ss.ReadRemaining();
+ Commands.HandleQRcmdCommand(hexCommand);
+ break;
+ }
+
+ if (ss.ConsumeRemaining("fThreadInfo"))
+ {
+ Commands. Reply($"m{string.Join(",", Commands.Debugger.DebugProcess.GetThreadUids().Select(x => $"{x:x}"))}");
+ break;
+ }
+
+ if (ss.ConsumeRemaining("sThreadInfo"))
+ {
+ Commands.Reply("l");
+ break;
+ }
+
+ if (ss.ConsumePrefix("ThreadExtraInfo,"))
+ {
+ ulong? threadId = ss.ReadRemainingAsThreadUid();
+ if (threadId == null)
+ {
+ Commands.ReplyError();
+ break;
+ }
+
+ Commands.Reply(Helpers.ToHex(
+ Commands.Debugger.DebugProcess.IsThreadPaused(
+ Commands.Debugger.DebugProcess.GetThread(threadId.Value))
+ ? "Paused"
+ : "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)
+ {
+ Commands.Reply("l");
+ break;
+ }
+
+ if (len >= (ulong)data.Length - offset)
+ {
+ Commands.Reply("l" + Helpers.ToBinaryFormat(data.Substring((int)offset)));
+ break;
+ }
+ else
+ {
+ Commands.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 = Commands.Debugger.IsProcessAarch32 ? "target32.xml" : "target64.xml";
+ }
+
+ string data;
+ if (RegisterInformation.Features.TryGetValue(feature, out data))
+ {
+ if (offset >= (ulong)data.Length)
+ {
+ Commands.Reply("l");
+ break;
+ }
+
+ if (len >= (ulong)data.Length - offset)
+ {
+ Commands.Reply("l" + Helpers.ToBinaryFormat(data.Substring((int)offset)));
+ break;
+ }
+ else
+ {
+ Commands.Reply("m" + Helpers.ToBinaryFormat(data.Substring((int)offset, (int)len)));
+ break;
+ }
+ }
+ else
+ {
+ Commands.Reply("E00"); // Invalid annex
+ break;
+ }
+ }
+
+ goto unknownCommand;
+ case 'Q':
+ goto unknownCommand;
+ case 's':
+ Commands.CommandStep(ss.IsEmpty() ? null : ss.ReadRemainingAsHex());
+ break;
+ case 'T':
+ {
+ ulong? threadId = ss.ReadRemainingAsThreadUid();
+ Commands.CommandIsAlive(threadId);
+ break;
+ }
+ case 'v':
+ if (ss.ConsumePrefix("Cont"))
+ {
+ if (ss.ConsumeRemaining("?"))
+ {
+ Commands.Reply("vCont;c;C;s;S");
+ break;
+ }
+
+ if (ss.ConsumePrefix(";"))
+ {
+ Commands.HandleVContCommand(ss);
+ break;
+ }
+
+ goto unknownCommand;
+ }
+
+ if (ss.ConsumeRemaining("MustReplyEmpty"))
+ {
+ Commands.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}");
+ Commands.ReplyError();
+ return;
+ }
+
+ switch (type)
+ {
+ case "0": // Software breakpoint
+ if (!Commands.Debugger.BreakpointManager.SetBreakPoint(addr, len))
+ {
+ Commands.ReplyError();
+ return;
+ }
+
+ Commands.ReplyOK();
+ return;
+ case "1": // Hardware breakpoint
+ case "2": // Write watchpoint
+ case "3": // Read watchpoint
+ case "4": // Access watchpoint
+ Commands.ReplyError();
+ return;
+ default:
+ Commands.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}");
+ Commands.ReplyError();
+ return;
+ }
+
+ switch (type)
+ {
+ case "0": // Software breakpoint
+ if (!Commands.Debugger.BreakpointManager.ClearBreakPoint(addr, len))
+ {
+ Commands.ReplyError();
+ return;
+ }
+
+ Commands.ReplyOK();
+ return;
+ case "1": // Hardware breakpoint
+ case "2": // Write watchpoint
+ case "3": // Read watchpoint
+ case "4": // Access watchpoint
+ Commands.ReplyError();
+ return;
+ default:
+ Commands.ReplyError();
+ return;
+ }
+ }
+ default:
+ unknownCommand:
+ Logger.Notice.Print(LogClass.GdbStub, $"Unknown command: {cmd}");
+ Commands.Reply("");
+ break;
+ }
+ }
+
+ private string GetThreadListXml()
+ {
+ var sb = new StringBuilder();
+ sb.Append("\n");
+
+ foreach (var thread in Commands.Debugger.GetThreads())
+ {
+ string threadName = System.Security.SecurityElement.Escape(thread.GetThreadName());
+ sb.Append(
+ $"{(Commands.Debugger.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..6c0a258a0
--- /dev/null
+++ b/src/Ryujinx.HLE/Debugger/Gdb/Commands.cs
@@ -0,0 +1,489 @@
+using Ryujinx.Common.Logging;
+using Ryujinx.Memory;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Sockets;
+using System.Text;
+
+namespace Ryujinx.HLE.Debugger.Gdb
+{
+ class GdbCommands
+ {
+ const int GdbRegisterCount64 = 68;
+ const int GdbRegisterCount32 = 66;
+
+ public readonly Debugger Debugger;
+
+ private readonly TcpListener _listenerSocket;
+ private readonly Socket _clientSocket;
+ private readonly NetworkStream _readStream;
+ private 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 Reply(string cmd)
+ {
+ Logger.Debug?.Print(LogClass.GdbStub, $"Reply: {cmd}");
+ _writeStream.Write(Encoding.ASCII.GetBytes($"${cmd}#{Helpers.CalculateChecksum(cmd):x2}"));
+ }
+
+ public void ReplyOK() => Reply("OK");
+
+ public void ReplyError() => Reply("E01");
+
+ internal void CommandQuery()
+ {
+ // GDB is performing initial contact. Stop everything.
+ Debugger.DebugProcess.DebugStop();
+ Debugger.GThread = Debugger.CThread = Debugger.DebugProcess.GetThreadUids().First();
+ Reply($"T05thread:{Debugger.CThread:x};");
+ }
+
+ internal void CommandInterrupt()
+ {
+ // 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();
+ }
+
+ Reply($"T02thread:{Debugger.GThread:x};");
+ }
+
+ internal void CommandContinue(ulong? newPc)
+ {
+ if (newPc.HasValue)
+ {
+ if (Debugger.CThread == null)
+ {
+ ReplyError();
+ return;
+ }
+
+ Debugger.DebugProcess.GetThread(Debugger.CThread.Value).Context.DebugPc = newPc.Value;
+ }
+
+ Debugger.DebugProcess.DebugContinue();
+ }
+
+ internal void CommandDetach()
+ {
+ Debugger.BreakpointManager.ClearAll();
+ CommandContinue(null);
+ }
+
+ internal void CommandReadRegisters()
+ {
+ if (Debugger.GThread == null)
+ {
+ ReplyError();
+ return;
+ }
+
+ var ctx = Debugger.DebugProcess.GetThread(Debugger.GThread.Value).Context;
+ string registers = "";
+ if (Debugger.IsProcessAarch32)
+ {
+ 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);
+ }
+ }
+
+ Reply(registers);
+ }
+
+ internal void CommandWriteRegisters(StringStream ss)
+ {
+ if (Debugger.GThread == null)
+ {
+ ReplyError();
+ return;
+ }
+
+ var ctx = Debugger.DebugProcess.GetThread(Debugger.GThread.Value).Context;
+ if (Debugger.IsProcessAarch32)
+ {
+ for (int i = 0; i < GdbRegisterCount32; i++)
+ {
+ if (!GdbRegisters.Write32(ctx, i, ss))
+ {
+ ReplyError();
+ return;
+ }
+ }
+ }
+ else
+ {
+ for (int i = 0; i < GdbRegisterCount64; i++)
+ {
+ if (!GdbRegisters.Write64(ctx, i, ss))
+ {
+ ReplyError();
+ return;
+ }
+ }
+ }
+
+ if (ss.IsEmpty())
+ {
+ ReplyOK();
+ }
+ else
+ {
+ ReplyError();
+ }
+ }
+
+ internal void CommandSetThread(char op, ulong? threadId)
+ {
+ if (threadId is 0 or null)
+ {
+ var threads = Debugger.GetThreads();
+ if (threads.Length == 0)
+ {
+ ReplyError();
+ return;
+ }
+
+ threadId = threads.First().ThreadUid;
+ }
+
+ if (Debugger.DebugProcess.GetThread(threadId.Value) == null)
+ {
+ ReplyError();
+ return;
+ }
+
+ switch (op)
+ {
+ case 'c':
+ Debugger.CThread = threadId;
+ ReplyOK();
+ return;
+ case 'g':
+ Debugger.GThread = threadId;
+ ReplyOK();
+ return;
+ default:
+ ReplyError();
+ return;
+ }
+ }
+
+ internal void CommandReadMemory(ulong addr, ulong len)
+ {
+ try
+ {
+ var data = new byte[len];
+ Debugger.DebugProcess.CpuMemory.Read(addr, data);
+ 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}");
+ ReplyError();
+ }
+ }
+
+ internal 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);
+ }
+
+ Debugger.DebugProcess.CpuMemory.Write(addr, data);
+ Debugger.DebugProcess.InvalidateCacheRegion(addr, len);
+ ReplyOK();
+ }
+ catch (InvalidMemoryRegionException)
+ {
+ ReplyError();
+ }
+ }
+
+ internal void CommandReadRegister(int gdbRegId)
+ {
+ if (Debugger.GThread == null)
+ {
+ ReplyError();
+ return;
+ }
+
+ var ctx = Debugger.DebugProcess.GetThread(Debugger.GThread.Value).Context;
+ string result;
+ if (Debugger.IsProcessAarch32)
+ {
+ result = GdbRegisters.Read32(ctx, gdbRegId);
+ if (result != null)
+ {
+ Reply(result);
+ }
+ else
+ {
+ ReplyError();
+ }
+ }
+ else
+ {
+ result = GdbRegisters.Read64(ctx, gdbRegId);
+ if (result != null)
+ {
+ Reply(result);
+ }
+ else
+ {
+ ReplyError();
+ }
+ }
+ }
+
+ internal void CommandWriteRegister(int gdbRegId, StringStream ss)
+ {
+ if (Debugger.GThread == null)
+ {
+ ReplyError();
+ return;
+ }
+
+ var ctx = Debugger.DebugProcess.GetThread(Debugger.GThread.Value).Context;
+ if (Debugger.IsProcessAarch32)
+ {
+ if (GdbRegisters.Write32(ctx, gdbRegId, ss) && ss.IsEmpty())
+ {
+ ReplyOK();
+ }
+ else
+ {
+ ReplyError();
+ }
+ }
+ else
+ {
+ if (GdbRegisters.Write64(ctx, gdbRegId, ss) && ss.IsEmpty())
+ {
+ ReplyOK();
+ }
+ else
+ {
+ ReplyError();
+ }
+ }
+ }
+
+ internal void CommandStep(ulong? newPc)
+ {
+ if (Debugger.CThread == null)
+ {
+ ReplyError();
+ return;
+ }
+
+ var thread = Debugger.DebugProcess.GetThread(Debugger.CThread.Value);
+
+ if (newPc.HasValue)
+ {
+ thread.Context.DebugPc = newPc.Value;
+ }
+
+ if (!Debugger.DebugProcess.DebugStep(thread))
+ {
+ ReplyError();
+ }
+ else
+ {
+ Debugger.GThread = Debugger.CThread = thread.ThreadUid;
+ Reply($"T05thread:{thread.ThreadUid:x};");
+ }
+ }
+
+ internal void CommandIsAlive(ulong? threadId)
+ {
+ if (Debugger.GetThreads().Any(x => x.ThreadUid == threadId))
+ {
+ ReplyOK();
+ }
+ else
+ {
+ Reply("E00");
+ }
+ }
+
+ enum VContAction
+ {
+ None,
+ Continue,
+ Stop,
+ Step
+ }
+
+ record VContPendingAction(VContAction Action, ushort? Signal = null);
+
+ internal void HandleVContCommand(StringStream ss)
+ {
+ string[] rawActions = ss.ReadRemaining().Split(';', StringSplitOptions.RemoveEmptyEntries);
+
+ var threadActionMap = new Dictionary();
+ foreach (var 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--)
+ {
+ var rawAction = rawActions[i];
+ var stream = new StringStream(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);
+ }
+
+ 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 = 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 (var (threadUid, action) in threadActionMap)
+ {
+ if (action.Action == VContAction.Continue)
+ {
+ Debugger.DebugProcess.DebugContinue(Debugger.DebugProcess.GetThread(threadUid));
+ }
+ }
+ }
+
+ if (hasError)
+ {
+ ReplyError();
+ }
+ else
+ {
+ ReplyOK();
+ }
+
+ foreach (var (threadUid, action) in threadActionMap)
+ {
+ if (action.Action == VContAction.Step)
+ {
+ Debugger.GThread = Debugger.CThread = threadUid;
+ Reply($"T05thread:{threadUid:x};");
+ }
+ }
+ }
+
+ internal void HandleQRcmdCommand(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"
+ };
+
+ Reply(Helpers.ToHex(response));
+ }
+ catch (Exception e)
+ {
+ Logger.Error?.Print(LogClass.GdbStub, $"Error processing Rcmd: {e.Message}");
+ 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..de2f6c25d
--- /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..a2b802525
--- /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..b43899271 100644
--- a/src/Ryujinx.HLE/Debugger/RegisterInformation.cs
+++ b/src/Ryujinx.HLE/Debugger/RegisterInformation.cs
@@ -17,7 +17,7 @@ namespace Ryujinx.HLE.Debugger
private static string GetEmbeddedResourceContent(string resourceName)
{
- Stream stream = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("Ryujinx.HLE.Debugger.GdbXml." + resourceName);
+ Stream stream = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("Ryujinx.HLE.Debugger.Gdb.Xml." + resourceName);
StreamReader reader = new StreamReader(stream);
string result = reader.ReadToEnd();
reader.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 54311f229..b823d14f7 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);
@@ -1095,6 +1099,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
Logger.Error?.Print(LogClass.Cpu, $"Invalid memory access at virtual address 0x{va:X16}.");
+ Logger.Flush();
+
return false;
}
@@ -1103,6 +1109,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
KernelStatic.GetCurrentThread().PrintGuestStackTrace();
KernelStatic.GetCurrentThread()?.PrintGuestRegisterPrintout();
+ Logger.Flush();
+
throw new UndefinedInstructionException(address, opCode);
}
diff --git a/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/Syscall.cs b/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/Syscall.cs
index c67220617..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;
@@ -1893,6 +1894,9 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall
return;
}
+ Logger.Error?.Print(LogClass.KernelSvc, "The guest program broke execution!");
+ Logger.Flush();
+
// TODO: Debug events.
currentThread.Owner.TerminateCurrentProcess();
diff --git a/src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs b/src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs
index 93bf7e00a..54440ab5f 100644
--- a/src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs
+++ b/src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs
@@ -293,6 +293,12 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
KThread currentThread = KernelStatic.GetCurrentThread();
KThread selectedThread = _state.SelectedThread;
+ if (!currentThread.IsThreadNamed && currentThread.GetThreadName() != "")
+ {
+ currentThread.HostThread.Name = $"<{currentThread.GetThreadName()}>";
+ currentThread.IsThreadNamed = true;
+ }
+
// If the thread is already scheduled and running on the core, we have nothing to do.
if (currentThread == selectedThread)
{
diff --git a/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs b/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs
index 6e0dd906f..058cc3202 100644
--- a/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs
+++ b/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs
@@ -53,6 +53,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
public ulong AffinityMask { get; set; }
public ulong ThreadUid { get; private set; }
+
+ public bool IsThreadNamed { get; set; }
private long _totalTimeRunning;
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/Program.cs b/src/Ryujinx/Program.cs
index db9ae21eb..ecf6779dc 100644
--- a/src/Ryujinx/Program.cs
+++ b/src/Ryujinx/Program.cs
@@ -351,7 +351,10 @@ namespace Ryujinx.Ava
if (isTerminating)
+ {
+ Logger.Flush();
Exit();
+ }
}
internal static void Exit()
diff --git a/src/Ryujinx/Systems/AppLibrary/ApplicationData.cs b/src/Ryujinx/Systems/AppLibrary/ApplicationData.cs
index 5656d6e73..c10ec69bd 100644
--- a/src/Ryujinx/Systems/AppLibrary/ApplicationData.cs
+++ b/src/Ryujinx/Systems/AppLibrary/ApplicationData.cs
@@ -11,11 +11,13 @@ using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Systems.PlayReport;
using Ryujinx.Ava.Utilities;
+using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.Loaders.Processes.Extensions;
using System;
using System.IO;
+using System.Linq;
using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Systems.AppLibrary
@@ -84,6 +86,32 @@ namespace Ryujinx.Ava.Systems.AppLibrary
public LocaleKeys? PlayabilityStatus => Compatibility.Convert(x => x.Status).OrElse(null);
+ public bool HasPtcCacheFiles
+ {
+ get
+ {
+ DirectoryInfo mainDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, IdString, "cache", "cpu", "0"));
+ DirectoryInfo backupDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, IdString, "cache", "cpu", "1"));
+
+ return (mainDir.Exists && (mainDir.EnumerateFiles("*.cache").Any() || mainDir.EnumerateFiles("*.info").Any())) ||
+ (backupDir.Exists && (backupDir.EnumerateFiles("*.cache").Any() || backupDir.EnumerateFiles("*.info").Any()));
+ }
+ }
+
+ public bool HasShaderCacheFiles
+ {
+ get
+ {
+ DirectoryInfo shaderCacheDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, IdString, "cache", "shader"));
+
+ if (!shaderCacheDir.Exists) return false;
+
+ return shaderCacheDir.EnumerateDirectories("*").Any() ||
+ shaderCacheDir.GetFiles("*.toc").Length != 0 ||
+ shaderCacheDir.GetFiles("*.data").Length != 0;
+ }
+ }
+
public string LocalizedStatusTooltip =>
Compatibility.Convert(x =>
#pragma warning disable CS8509 // It is exhaustive for all possible values this can contain.
diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml
index 909154540..9f2999a4c 100755
--- a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml
+++ b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml
@@ -120,13 +120,13 @@
CommandParameter="{Binding}"
Header="{ext:Locale GameListContextMenuCacheManagementNukePptc}"
Icon="{ext:Icon fa-solid fa-trash-can}"
- IsEnabled="{Binding HasPtcCacheFiles}" />
+ IsEnabled="{Binding SelectedApplication.HasPtcCacheFiles, FallbackValue=False}" />
+ IsEnabled="{Binding SelectedApplication.HasShaderCacheFiles, FallbackValue=False}" />