From 13878acdb239eb695094b166cd3905f172a22c83 Mon Sep 17 00:00:00 2001 From: KeatonTheBot Date: Sat, 11 Oct 2025 02:56:13 -0500 Subject: [PATCH 01/17] Avoid lookup of invalid textures if pool did not change (ryubing/ryujinx!113) See merge request ryubing/ryujinx!113 --- src/Ryujinx.Common/Collections/BitMap.cs | 226 ++++++++++++++++++ src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs | 11 + 2 files changed, 237 insertions(+) create mode 100644 src/Ryujinx.Common/Collections/BitMap.cs 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.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); } } From ef9810582a4dee5701ad9abe3de0d276cf72a161 Mon Sep 17 00:00:00 2001 From: LotP <22-lotp@users.noreply.git.ryujinx.app> Date: Sat, 11 Oct 2025 07:47:45 -0500 Subject: [PATCH 02/17] Sync thread name on Schedule (ryubing/ryujinx!157) See merge request ryubing/ryujinx!157 --- src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs | 6 ++++++ src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs | 2 ++ 2 files changed, 8 insertions(+) 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; From 1865be47cf4334a7a5363c766044ffd94e959fb8 Mon Sep 17 00:00:00 2001 From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app> Date: Sat, 11 Oct 2025 10:01:30 -0500 Subject: [PATCH 03/17] gdb: Add `monitor minidump` command (ryubing/ryujinx!158) See merge request ryubing/ryujinx!158 --- src/Ryujinx.HLE/Debugger/Debugger.cs | 39 +++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/Ryujinx.HLE/Debugger/Debugger.cs b/src/Ryujinx.HLE/Debugger/Debugger.cs index 66e42681b..a1eb97c64 100644 --- a/src/Ryujinx.HLE/Debugger/Debugger.cs +++ b/src/Ryujinx.HLE/Debugger/Debugger.cs @@ -1037,12 +1037,13 @@ namespace Ryujinx.HLE.Debugger string response = command.Trim().ToLowerInvariant() switch { - "help" => "backtrace\nbt\nregisters\nreg\nget info\n", + "help" => "backtrace\nbt\nregisters\nreg\nget info\nminidump", "get info" => GetProcessInfo(), "backtrace" => GetStackTrace(), "bt" => GetStackTrace(), "registers" => GetRegisters(), "reg" => GetRegisters(), + "minidump" => GetMinidump(), _ => $"Unknown command: {command}\n" }; @@ -1077,6 +1078,42 @@ namespace Ryujinx.HLE.Debugger return Process.Debugger.GetCpuRegisterPrintout(DebugProcess.GetThread(gThread.Value)); } + private 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(); + } + private string GetProcessInfo() { try From ceec9617efc6cda772c5cc749e78c1331fe7ee36 Mon Sep 17 00:00:00 2001 From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app> Date: Sat, 11 Oct 2025 19:06:14 -0500 Subject: [PATCH 04/17] gdb: Fix the crash that occurs when GDB is connected early (ryubing/ryujinx!159) See merge request ryubing/ryujinx!159 --- src/Ryujinx.HLE/Debugger/Debugger.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Ryujinx.HLE/Debugger/Debugger.cs b/src/Ryujinx.HLE/Debugger/Debugger.cs index a1eb97c64..2c935ca7f 100644 --- a/src/Ryujinx.HLE/Debugger/Debugger.cs +++ b/src/Ryujinx.HLE/Debugger/Debugger.cs @@ -859,7 +859,13 @@ namespace Ryujinx.HLE.Debugger { if (threadId == 0 || threadId == null) { - threadId = GetThreads().First().ThreadUid; + var threads = GetThreads(); + if (threads.Length == 0) + { + ReplyError(); + return; + } + threadId = threads.First().ThreadUid; } if (DebugProcess.GetThread(threadId.Value) == null) @@ -1037,7 +1043,7 @@ namespace Ryujinx.HLE.Debugger string response = command.Trim().ToLowerInvariant() switch { - "help" => "backtrace\nbt\nregisters\nreg\nget info\nminidump", + "help" => "backtrace\nbt\nregisters\nreg\nget info\nminidump\n", "get info" => GetProcessInfo(), "backtrace" => GetStackTrace(), "bt" => GetStackTrace(), @@ -1192,11 +1198,11 @@ 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"); ClientSocket.Close(); From 51584a083b46cd77712c5614ecb10300bb9768ed Mon Sep 17 00:00:00 2001 From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app> Date: Mon, 13 Oct 2025 17:40:15 -0500 Subject: [PATCH 05/17] Flush the error log before exit (ryubing/ryujinx!163) See merge request ryubing/ryujinx!163 --- src/Ryujinx.Common/Logging/Logger.cs | 11 +++++ .../Logging/Targets/AsyncLogTargetWrapper.cs | 41 ++++++++++++++++++- .../HOS/Kernel/Process/KProcess.cs | 4 ++ .../HOS/Kernel/SupervisorCall/Syscall.cs | 3 ++ src/Ryujinx/Program.cs | 3 ++ 5 files changed, 61 insertions(+), 1 deletion(-) 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.HLE/HOS/Kernel/Process/KProcess.cs b/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs index 54311f229..bea1f0ef3 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs @@ -1095,6 +1095,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 +1105,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..005ac1452 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/Syscall.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/Syscall.cs @@ -1893,6 +1893,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/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() From 47559cd3115e243fcb2e66383dd2930c2c22bab4 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Tue, 14 Oct 2025 15:59:02 -0500 Subject: [PATCH 06/17] Revert game list rounding The selected highlight was bugged https://fs.ryujinx.app/40cl4Ih9RiOWLVi7e4lJsw.png --- src/Ryujinx/UI/Views/Misc/ApplicationGridView.axaml | 4 ++-- src/Ryujinx/UI/Views/Misc/ApplicationListView.axaml | 10 ++-------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/Ryujinx/UI/Views/Misc/ApplicationGridView.axaml b/src/Ryujinx/UI/Views/Misc/ApplicationGridView.axaml index 39db76031..b572d04e6 100644 --- a/src/Ryujinx/UI/Views/Misc/ApplicationGridView.axaml +++ b/src/Ryujinx/UI/Views/Misc/ApplicationGridView.axaml @@ -35,7 +35,7 @@ @@ -49,11 +46,9 @@ Padding="10" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" - ClipToBounds="True"> + ClipToBounds="True" + CornerRadius="5"> - - Date: Tue, 14 Oct 2025 16:09:51 -0500 Subject: [PATCH 07/17] chore: move HasPtcCacheFiles & HasShaderCacheFiles into ApplicationData, instead of having the weird static dependency helpers --- .../Systems/AppLibrary/ApplicationData.cs | 28 ++++++++++++++++ .../UI/Controls/ApplicationContextMenu.axaml | 4 +-- .../UI/ViewModels/MainWindowViewModel.cs | 32 ++----------------- 3 files changed, 32 insertions(+), 32 deletions(-) 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}" /> NukePtcCache { get; } = - Commands.CreateConditional(vm => vm?.SelectedApplication != null && - vm.HasPtcCacheFiles(), + Commands.CreateConditional(vm => vm?.SelectedApplication?.HasPtcCacheFiles ?? false, async viewModel => { UserResult result = await ContentDialogHelper.CreateLocalizedConfirmationDialog( @@ -2171,22 +2170,9 @@ namespace Ryujinx.Ava.UI.ViewModels } }); - private bool HasPtcCacheFiles() - { - if (this.SelectedApplication == null) return false; - - DirectoryInfo mainDir = new DirectoryInfo(Path.Combine(AppDataManager.GamesDirPath, - this.SelectedApplication.IdString, "cache", "cpu", "0")); - DirectoryInfo backupDir = new DirectoryInfo(Path.Combine(AppDataManager.GamesDirPath, - this.SelectedApplication.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 static AsyncRelayCommand PurgeShaderCache { get; } = Commands.CreateConditional( - vm => vm?.SelectedApplication != null && vm.HasShaderCacheFiles(), + vm => vm?.SelectedApplication?.HasShaderCacheFiles ?? false, async viewModel => { UserResult result = await ContentDialogHelper.CreateLocalizedConfirmationDialog( @@ -2243,20 +2229,6 @@ namespace Ryujinx.Ava.UI.ViewModels } }); - private bool HasShaderCacheFiles() - { - if (this.SelectedApplication == null) return false; - - DirectoryInfo shaderCacheDir = new(Path.Combine(AppDataManager.GamesDirPath, - this.SelectedApplication.IdString, "cache", "shader")); - - if (!shaderCacheDir.Exists) return false; - - return shaderCacheDir.EnumerateDirectories("*").Any() || - shaderCacheDir.GetFiles("*.toc").Any() || - shaderCacheDir.GetFiles("*.data").Any(); - } - public static RelayCommand OpenPtcDirectory { get; } = Commands.CreateConditional(vm => vm?.SelectedApplication != null, viewModel => From 99126603bae923dac49d729c0613fba483670c22 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Tue, 14 Oct 2025 16:12:11 -0500 Subject: [PATCH 08/17] UI: swap the UI reset checkbox text back to a sentence instead of title cased --- assets/locales.json | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/assets/locales.json b/assets/locales.json index 327c3994e..3a5b45f5f 100644 --- a/assets/locales.json +++ b/assets/locales.json @@ -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": "我想重設我的設定。" + } }, { From 2434c55266587b26a6189dcc2c194a92d5790e0f Mon Sep 17 00:00:00 2001 From: GreemDev Date: Tue, 14 Oct 2025 18:38:56 -0500 Subject: [PATCH 09/17] UI: Updater: Fix "No" opening the changelog and "Show Changelog" doing nothing (aka doing what "No" should be doing) --- src/Ryujinx/UI/Helpers/ContentDialogHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ryujinx/UI/Helpers/ContentDialogHelper.cs b/src/Ryujinx/UI/Helpers/ContentDialogHelper.cs index fd42c6a45..e8730913c 100644 --- a/src/Ryujinx/UI/Helpers/ContentDialogHelper.cs +++ b/src/Ryujinx/UI/Helpers/ContentDialogHelper.cs @@ -354,8 +354,8 @@ namespace Ryujinx.Ava.UI.Helpers primary, secondaryText, LocaleManager.Instance[LocaleKeys.InputDialogYes], - LocaleManager.Instance[LocaleKeys.InputDialogNo], LocaleManager.Instance[LocaleKeys.DialogUpdaterShowChangelogMessage], + LocaleManager.Instance[LocaleKeys.InputDialogNo], (int)Symbol.Help, UserResult.Yes); From 1d409f712721b38327ff80ee0786465952a746d8 Mon Sep 17 00:00:00 2001 From: LotP <22-lotp@users.noreply.git.ryujinx.app> Date: Wed, 15 Oct 2025 15:37:13 -0500 Subject: [PATCH 10/17] 12 GiB heap support (ryubing/ryujinx!166) See merge request ryubing/ryujinx!166 --- src/Ryujinx.HLE/HOS/Kernel/KernelStatic.cs | 1 + src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTableBase.cs | 9 ++++++--- src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs | 4 ++++ src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/Syscall.cs | 3 ++- src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs | 3 ++- 5 files changed, 15 insertions(+), 5 deletions(-) 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..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); 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) From d4107ac05f696827c51ff1d120560ca3536cd8be Mon Sep 17 00:00:00 2001 From: GreemDev Date: Wed, 15 Oct 2025 21:51:13 -0500 Subject: [PATCH 11/17] UI: Add a startup flag to ignore new Amiibo file updates, useful for testing changes you intend on committing to Ryubing/Nfc. Flag is `--local-only-amiibo` --- .../UI/ViewModels/AmiiboWindowViewModel.cs | 54 ++++++++++++++++++- src/Ryujinx/Utilities/CommandLineState.cs | 5 ++ 2 files changed, 57 insertions(+), 2 deletions(-) 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) From 7268acbfb4630d87ccc0ea9ab9ecb1753e1615ea Mon Sep 17 00:00:00 2001 From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app> Date: Thu, 16 Oct 2025 07:49:41 -0500 Subject: [PATCH 12/17] gdb: Do not skip CheckInterrupt when gdb stub is enabled (ryubing/ryujinx!169) See merge request ryubing/ryujinx!169 --- src/ARMeilleure/Instructions/NativeInterface.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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(); From fdbdb05cb583a1604ada310d160791bd945f758e Mon Sep 17 00:00:00 2001 From: GreemDev Date: Thu, 16 Oct 2025 12:23:01 -0500 Subject: [PATCH 13/17] misc: Update Ryujinx.LibHac --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 @@ - + From c33a97f01c631941512140f8dd583ce3029a790f Mon Sep 17 00:00:00 2001 From: GreemDev Date: Thu, 16 Oct 2025 17:32:04 -0500 Subject: [PATCH 14/17] gdb: Cleanup Debugger.cs by moving the GDB command handlers and command processor out of the class and into their own --- src/Ryujinx.HLE/Debugger/Debugger.cs | 1107 +---------------- .../Debugger/Gdb/CommandProcessor.cs | 393 ++++++ src/Ryujinx.HLE/Debugger/Gdb/Commands.cs | 489 ++++++++ src/Ryujinx.HLE/Debugger/Gdb/Registers.cs | 160 +++ .../{GdbXml => Gdb/Xml}/aarch64-core.xml | 0 .../{GdbXml => Gdb/Xml}/aarch64-fpu.xml | 0 .../Debugger/{GdbXml => Gdb/Xml}/arm-core.xml | 0 .../Debugger/{GdbXml => Gdb/Xml}/arm-neon.xml | 0 .../Debugger/{GdbXml => Gdb/Xml}/target32.xml | 0 .../Debugger/{GdbXml => Gdb/Xml}/target64.xml | 0 src/Ryujinx.HLE/Debugger/Helpers.cs | 50 + src/Ryujinx.HLE/Debugger/StringStream.cs | 2 +- src/Ryujinx.HLE/Ryujinx.HLE.csproj | 24 +- 13 files changed, 1150 insertions(+), 1075 deletions(-) create mode 100644 src/Ryujinx.HLE/Debugger/Gdb/CommandProcessor.cs create mode 100644 src/Ryujinx.HLE/Debugger/Gdb/Commands.cs create mode 100644 src/Ryujinx.HLE/Debugger/Gdb/Registers.cs rename src/Ryujinx.HLE/Debugger/{GdbXml => Gdb/Xml}/aarch64-core.xml (100%) rename src/Ryujinx.HLE/Debugger/{GdbXml => Gdb/Xml}/aarch64-fpu.xml (100%) rename src/Ryujinx.HLE/Debugger/{GdbXml => Gdb/Xml}/arm-core.xml (100%) rename src/Ryujinx.HLE/Debugger/{GdbXml => Gdb/Xml}/arm-neon.xml (100%) rename src/Ryujinx.HLE/Debugger/{GdbXml => Gdb/Xml}/target32.xml (100%) rename src/Ryujinx.HLE/Debugger/{GdbXml => Gdb/Xml}/target64.xml (100%) create mode 100644 src/Ryujinx.HLE/Debugger/Helpers.cs diff --git a/src/Ryujinx.HLE/Debugger/Debugger.cs b/src/Ryujinx.HLE/Debugger/Debugger.cs index 2c935ca7f..e03f05b7f 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: @@ -257,834 +106,29 @@ namespace Ryujinx.HLE.Debugger } } - private void ProcessCommand(string cmd) + public string GetStackTrace() { - 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 - { - '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};"); - } - } - } - - private string GetThreadListXml() - { - 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 +164,7 @@ namespace Ryujinx.HLE.Debugger return response.ToString(); } - private string GetProcessInfo() + public string GetProcessInfo() { try { @@ -1130,15 +174,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 +200,7 @@ namespace Ryujinx.HLE.Debugger sb.AppendLine($" 0x{image.BaseAddress:x10} - 0x{endAddress:x10} {name}"); } } - + return sb.ToString(); } catch (Exception e) @@ -1162,22 +210,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); @@ -1202,9 +234,11 @@ namespace Ryujinx.HLE.Debugger { Thread.Sleep(200); } + 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; } @@ -1212,6 +246,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) @@ -1221,7 +256,7 @@ namespace Ryujinx.HLE.Debugger switch (ReadStream.ReadByte()) { case -1: - goto eof; + goto EndOfLoop; case '+': continue; case '-': @@ -1236,7 +271,7 @@ namespace Ryujinx.HLE.Debugger { int x = ReadStream.ReadByte(); if (x == -1) - goto eof; + goto EndOfLoop; if (x == '#') break; cmd += (char)x; @@ -1257,11 +292,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; @@ -1274,58 +309,6 @@ namespace Ryujinx.HLE.Debugger } } - 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); @@ -1358,7 +341,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..19b3b7a2b --- /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, false)) + { + 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/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/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 @@ - - - - - - + + + + + + From 2b159dbca8f8017304f816b5f27d0f109074df07 Mon Sep 17 00:00:00 2001 From: Bluey Enjoyer Date: Fri, 17 Oct 2025 01:43:56 +0100 Subject: [PATCH 15/17] AHHHHHHHHHHHHHH (ryubing/ryujinx!170) See merge request ryubing/ryujinx!170 --- docs/compatibility.csv | 1 + src/Ryujinx.Common/TitleIDs.cs | 1 + 2 files changed, 2 insertions(+) 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/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) From 9aacf9b37ba3c5774b47fd57080237d4df1c59e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hack=E8=8C=B6=E3=82=93?= Date: Thu, 16 Oct 2025 19:45:14 -0500 Subject: [PATCH 16/17] Update Korean translation (ryubing/ryujinx!168) See merge request ryubing/ryujinx!168 --- assets/locales.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/locales.json b/assets/locales.json index 3a5b45f5f..31a3242b8 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)", From 8e941e4a8f5b73878f03fd1a9eca06eaa4defde5 Mon Sep 17 00:00:00 2001 From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app> Date: Thu, 16 Oct 2025 19:53:51 -0500 Subject: [PATCH 17/17] gdb: Cleanup (ryubing/ryujinx!171) See merge request ryubing/ryujinx!171 --- src/Ryujinx.HLE/Debugger/BreakpointManager.cs | 33 ++----------------- src/Ryujinx.HLE/Debugger/Debugger.cs | 5 +++ .../Debugger/Gdb/CommandProcessor.cs | 4 +-- .../Debugger/RegisterInformation.cs | 2 +- 4 files changed, 11 insertions(+), 33 deletions(-) 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 e03f05b7f..cc64a38eb 100644 --- a/src/Ryujinx.HLE/Debugger/Debugger.cs +++ b/src/Ryujinx.HLE/Debugger/Debugger.cs @@ -103,6 +103,10 @@ namespace Ryujinx.HLE.Debugger { Logger.Error?.Print(LogClass.GdbStub, "Error while processing GDB messages", e); } + catch (ObjectDisposedException e) + { + Logger.Error?.Print(LogClass.GdbStub, "Error while processing GDB messages", e); + } } } @@ -304,6 +308,7 @@ namespace Ryujinx.HLE.Debugger WriteStream = null; ClientSocket.Close(); ClientSocket = null; + CommandProcessor = null; BreakpointManager.ClearAll(); } diff --git a/src/Ryujinx.HLE/Debugger/Gdb/CommandProcessor.cs b/src/Ryujinx.HLE/Debugger/Gdb/CommandProcessor.cs index 19b3b7a2b..e9986647d 100644 --- a/src/Ryujinx.HLE/Debugger/Gdb/CommandProcessor.cs +++ b/src/Ryujinx.HLE/Debugger/Gdb/CommandProcessor.cs @@ -310,7 +310,7 @@ namespace Ryujinx.HLE.Debugger.Gdb switch (type) { case "0": // Software breakpoint - if (!Commands.Debugger.BreakpointManager.SetBreakPoint(addr, len, false)) + if (!Commands.Debugger.BreakpointManager.SetBreakPoint(addr, len)) { Commands.ReplyError(); return; @@ -325,7 +325,7 @@ namespace Ryujinx.HLE.Debugger.Gdb Commands.ReplyError(); return; default: - Commands. ReplyError(); + Commands.ReplyError(); return; } } 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();