diff --git a/assets/locales.json b/assets/locales.json index d931017aa..915fb42e5 100644 --- a/assets/locales.json +++ b/assets/locales.json @@ -25021,6 +25021,206 @@ "zh_CN": "动态 Rich Presence", "zh_TW": "動態 Rich Presence" } + }, + { + "ID": "SettingsTabDebug", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Debug", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "SettingsTabDebugTitle", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Debug", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "SettingsTabDebugNote", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "WARNING: For developer use only, will reduce performance", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "SettingsTabDebugEnableGDBStub", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Enable GDB Stub", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "SettingsTabDebugGDBStubToggleTooltip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Enables the GDB stub which makes it possible to debug the running application. For development use only!", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "SettingsTabDebugGDBStubPort", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "GDB Stub Port:", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "SettingsTabDebugSuspendOnStart", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Suspend Application on Start", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "SettingsTabDebugSuspendOnStartTooltip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Suspends the application before executing the first instruction, allowing for debugging from the earliest point.", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } } ] } diff --git a/src/ARMeilleure/Instructions/NativeInterface.cs b/src/ARMeilleure/Instructions/NativeInterface.cs index b4922629b..d43e20d83 100644 --- a/src/ARMeilleure/Instructions/NativeInterface.cs +++ b/src/ARMeilleure/Instructions/NativeInterface.cs @@ -3,6 +3,7 @@ using ARMeilleure.State; using ARMeilleure.Translation; using System; using System.Runtime.InteropServices; +using ExecutionContext = ARMeilleure.State.ExecutionContext; namespace ARMeilleure.Instructions { @@ -200,7 +201,11 @@ namespace ARMeilleure.Instructions ExecutionContext context = GetContext(); - context.CheckInterrupt(); + // If debugging, we'll handle interrupts outside + if (!Optimizations.EnableDebugging) + { + context.CheckInterrupt(); + } Statistics.ResumeTimer(); diff --git a/src/ARMeilleure/Optimizations.cs b/src/ARMeilleure/Optimizations.cs index 18390de31..6dd7befe7 100644 --- a/src/ARMeilleure/Optimizations.cs +++ b/src/ARMeilleure/Optimizations.cs @@ -12,6 +12,7 @@ namespace ARMeilleure public static bool AllowLcqInFunctionTable { get; set; } = true; public static bool UseUnmanagedDispatchLoop { get; set; } = true; + public static bool EnableDebugging { get; set; } = false; public static bool UseAdvSimdIfAvailable { get; set; } = true; public static bool UseArm64AesIfAvailable { get; set; } = true; diff --git a/src/ARMeilleure/State/ExecutionContext.cs b/src/ARMeilleure/State/ExecutionContext.cs index 223e59d79..fa1a4a032 100644 --- a/src/ARMeilleure/State/ExecutionContext.cs +++ b/src/ARMeilleure/State/ExecutionContext.cs @@ -1,4 +1,5 @@ using ARMeilleure.Memory; +using System.Threading; namespace ARMeilleure.State { @@ -10,7 +11,7 @@ namespace ARMeilleure.State internal nint NativeContextPtr => _nativeContext.BasePtr; - private bool _interrupted; + internal bool Interrupted { get; private set; } private readonly ICounter _counter; @@ -65,6 +66,8 @@ namespace ARMeilleure.State public bool IsAarch32 { get; set; } + public ulong ThreadUid { get; set; } + internal ExecutionMode ExecutionMode { get @@ -90,14 +93,19 @@ namespace ARMeilleure.State private readonly ExceptionCallbackNoArgs _interruptCallback; private readonly ExceptionCallback _breakCallback; + private readonly ExceptionCallbackNoArgs _stepCallback; private readonly ExceptionCallback _supervisorCallback; private readonly ExceptionCallback _undefinedCallback; + internal int ShouldStep; + public ulong DebugPc { get; set; } + public ExecutionContext( IJitMemoryAllocator allocator, ICounter counter, ExceptionCallbackNoArgs interruptCallback = null, ExceptionCallback breakCallback = null, + ExceptionCallbackNoArgs stepCallback = null, ExceptionCallback supervisorCallback = null, ExceptionCallback undefinedCallback = null) { @@ -105,6 +113,7 @@ namespace ARMeilleure.State _counter = counter; _interruptCallback = interruptCallback; _breakCallback = breakCallback; + _stepCallback = stepCallback; _supervisorCallback = supervisorCallback; _undefinedCallback = undefinedCallback; @@ -127,9 +136,9 @@ namespace ARMeilleure.State internal void CheckInterrupt() { - if (_interrupted) + if (Interrupted) { - _interrupted = false; + Interrupted = false; _interruptCallback?.Invoke(this); } @@ -139,16 +148,37 @@ namespace ARMeilleure.State public void RequestInterrupt() { - _interrupted = true; + Interrupted = true; + } + + public void StepHandler() + { + _stepCallback?.Invoke(this); + } + + public void RequestDebugStep() + { + Interlocked.Exchange(ref ShouldStep, 1); + RequestInterrupt(); } internal void OnBreak(ulong address, int imm) { + if (Optimizations.EnableDebugging) + { + DebugPc = Pc; + } + _breakCallback?.Invoke(this, address, imm); } internal void OnSupervisorCall(ulong address, int imm) { + if (Optimizations.EnableDebugging) + { + DebugPc = Pc; + } + _supervisorCallback?.Invoke(this, address, imm); } diff --git a/src/ARMeilleure/State/NativeContext.cs b/src/ARMeilleure/State/NativeContext.cs index c90e522a9..25b5e51c3 100644 --- a/src/ARMeilleure/State/NativeContext.cs +++ b/src/ARMeilleure/State/NativeContext.cs @@ -22,6 +22,11 @@ namespace ARMeilleure.State public ulong ExclusiveValueHigh; public int Running; public long Tpidr2El0; + + /// + /// This is only set when Optimizations.EnableDebugging is true. + /// + public ulong CurrentPc; } private static NativeCtxStorage _dummyStorage = new(); @@ -39,6 +44,11 @@ namespace ARMeilleure.State public ulong GetPc() { + if (Optimizations.EnableDebugging) + { + return GetStorage().CurrentPc; + } + // TODO: More precise tracking of PC value. return GetStorage().DispatchAddress; } @@ -268,6 +278,11 @@ namespace ARMeilleure.State return StorageOffset(ref _dummyStorage, ref _dummyStorage.Running); } + public static int GetCurrentPcOffset() + { + return StorageOffset(ref _dummyStorage, ref _dummyStorage.CurrentPc); + } + private static int StorageOffset(ref NativeCtxStorage storage, ref T target) { return (int)Unsafe.ByteOffset(ref Unsafe.As(ref storage), ref target); diff --git a/src/ARMeilleure/Translation/PTC/Ptc.cs b/src/ARMeilleure/Translation/PTC/Ptc.cs index f36d4256d..c69ebcadb 100644 --- a/src/ARMeilleure/Translation/PTC/Ptc.cs +++ b/src/ARMeilleure/Translation/PTC/Ptc.cs @@ -33,7 +33,7 @@ namespace ARMeilleure.Translation.PTC private const string OuterHeaderMagicString = "PTCohd\0\0"; private const string InnerHeaderMagicString = "PTCihd\0\0"; - private const uint InternalVersion = 7008; //! To be incremented manually for each change to the ARMeilleure project. + private const uint InternalVersion = 7009; //! To be incremented manually for each change to the ARMeilleure project. private const string ActualDir = "0"; private const string BackupDir = "1"; @@ -303,6 +303,13 @@ namespace ARMeilleure.Translation.PTC return false; } + if (outerHeader.DebuggerMode != Optimizations.EnableDebugging) + { + InvalidateCompressedStream(compressedStream); + + return false; + } + nint intPtr = nint.Zero; try @@ -479,6 +486,7 @@ namespace ARMeilleure.Translation.PTC MemoryManagerMode = GetMemoryManagerMode(), OSPlatform = GetOSPlatform(), Architecture = (uint)RuntimeInformation.ProcessArchitecture, + DebuggerMode = Optimizations.EnableDebugging, UncompressedStreamSize = (long)Unsafe.SizeOf() + @@ -1068,7 +1076,7 @@ namespace ARMeilleure.Translation.PTC return osPlatform; } - [StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 86*/)] + [StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 87*/)] private struct OuterHeader { public ulong Magic; @@ -1080,6 +1088,7 @@ namespace ARMeilleure.Translation.PTC public byte MemoryManagerMode; public uint OSPlatform; public uint Architecture; + public bool DebuggerMode; public long UncompressedStreamSize; diff --git a/src/ARMeilleure/Translation/Translator.cs b/src/ARMeilleure/Translation/Translator.cs index d8528cfd6..0300d51e1 100644 --- a/src/ARMeilleure/Translation/Translator.cs +++ b/src/ARMeilleure/Translation/Translator.cs @@ -119,7 +119,25 @@ namespace ARMeilleure.Translation NativeInterface.RegisterThread(context, Memory, this); - if (Optimizations.UseUnmanagedDispatchLoop) + if (Optimizations.EnableDebugging) + { + context.DebugPc = address; + do + { + if (Interlocked.CompareExchange(ref context.ShouldStep, 0, 1) == 1) + { + context.DebugPc = Step(context, context.DebugPc); + context.StepHandler(); + } + else + { + context.DebugPc = ExecuteSingle(context, context.DebugPc); + } + context.CheckInterrupt(); + } + while (context.Running && context.DebugPc != 0); + } + else if (Optimizations.UseUnmanagedDispatchLoop) { Stubs.DispatchLoop(context.NativeContextPtr, address); } @@ -175,8 +193,24 @@ namespace ARMeilleure.Translation return nextAddr; } - public ulong Step(State.ExecutionContext context, ulong address) + private ulong Step(State.ExecutionContext context, ulong address) { + try + { + OpCode opCode = Decoder.DecodeOpCode(Memory, address, context.ExecutionMode); + + // For branch instructions during single-stepping, we handle them manually + // func.Execute() will sometimes execute the entire function call, which is not what we want + if (opCode.Instruction.Name is InstName.Bl or InstName.Blr or InstName.Blx or InstName.Br) + { + return ExecuteBranchInstructionForStepping(context, address, opCode); + } + } + catch + { + // ignore + } + TranslatedFunction func = Translate(address, context.ExecutionMode, highCq: false, singleStep: true); address = func.Execute(Stubs.ContextWrapper, context); @@ -186,6 +220,94 @@ namespace ARMeilleure.Translation return address; } + private static ulong ExecuteBranchInstructionForStepping(State.ExecutionContext context, ulong address, OpCode opCode) + { + switch (opCode.Instruction.Name) + { + case InstName.Bl: + if (opCode is IOpCodeBImm opBImm) + { + // Set link register + if (context.ExecutionMode == ExecutionMode.Aarch64) + { + context.SetX(30, address + (ulong)opCode.OpCodeSizeInBytes); // LR = X30 + } + else + { + // For ARM32, need to set the appropriate return address + uint returnAddr = opCode is OpCode32 op32 && op32.IsThumb + ? (uint)address + (uint)opCode.OpCodeSizeInBytes | 1u // Thumb bit set + : (uint)address + (uint)opCode.OpCodeSizeInBytes; + context.SetX(14, returnAddr); // LR = R14 + } + return (ulong)opBImm.Immediate; + } + break; + + case InstName.Blr: + if (opCode is OpCodeBReg opBReg) + { + // Set link register + if (context.ExecutionMode == ExecutionMode.Aarch64) + { + context.SetX(30, address + (ulong)opCode.OpCodeSizeInBytes); // LR = X30 + } + else + { + uint returnAddr = opCode is OpCode32 op32 && op32.IsThumb + ? (uint)address + (uint)opCode.OpCodeSizeInBytes | 1u // Thumb bit set + : (uint)address + (uint)opCode.OpCodeSizeInBytes; + context.SetX(14, returnAddr); // LR = R14 + } + return context.GetX(opBReg.Rn); + } + break; + + case InstName.Blx: + if (opCode is IOpCodeBImm opBlxImm) + { + // Handle mode switching for BLX + if (opCode is OpCode32 op32) + { + uint returnAddr = op32.IsThumb + ? (uint)address + (uint)opCode.OpCodeSizeInBytes | 1u + : (uint)address + (uint)opCode.OpCodeSizeInBytes; + context.SetX(14, returnAddr); + + // BLX switches between ARM and Thumb modes + context.SetPstateFlag(PState.TFlag, !op32.IsThumb); + } + return (ulong)opBlxImm.Immediate; + } + else if (opCode is IOpCode32BReg opBlxReg) + { + if (opCode is OpCode32 op32) + { + uint returnAddr = op32.IsThumb + ? (uint)address + (uint)opCode.OpCodeSizeInBytes | 1u + : (uint)address + (uint)opCode.OpCodeSizeInBytes; + context.SetX(14, returnAddr); + + // For BLX register, the target address determines the mode + ulong targetAddr = context.GetX(opBlxReg.Rm); + context.SetPstateFlag(PState.TFlag, (targetAddr & 1) != 0); + return targetAddr & ~1UL; // Clear the Thumb bit for the actual address + } + } + break; + + case InstName.Br: + if (opCode is OpCodeBReg opBr) + { + // BR doesn't set link register, just branches to the target + return context.GetX(opBr.Rn); + } + break; + } + + throw new InvalidOperationException($"Unhandled branch instruction: {opCode.Instruction.Name}"); + } + internal TranslatedFunction GetOrTranslate(ulong address, ExecutionMode mode) { if (!Functions.TryGetValue(address, out TranslatedFunction func)) @@ -367,9 +489,13 @@ namespace ARMeilleure.Translation if (block.Exit) { - // Left option here as it may be useful if we need to return to managed rather than tail call in - // future. (eg. for debug) - bool useReturns = false; + // Return to managed rather than tail call. + bool useReturns = Optimizations.EnableDebugging; + + if (Optimizations.EnableDebugging) + { + EmitPcUpdate(context, block.Address); + } InstEmitFlowHelper.EmitVirtualJump(context, Const(block.Address), isReturn: useReturns); } @@ -393,6 +519,11 @@ namespace ARMeilleure.Translation } } + if (Optimizations.EnableDebugging) + { + EmitPcUpdate(context, opCode.Address); + } + Operand lblPredicateSkip = default; if (context.IsInIfThenBlock && context.CurrentIfThenBlockCond != Condition.Al) @@ -489,6 +620,14 @@ namespace ARMeilleure.Translation context.MarkLabel(lblExit); } + internal static void EmitPcUpdate(EmitterContext context, ulong address) + { + long currentPcOffs = NativeContext.GetCurrentPcOffset(); + + Operand currentPcAddr = context.Add(context.LoadArgument(OperandType.I64, 0), Const(currentPcOffs)); + context.Store(currentPcAddr, Const(address)); + } + public void InvalidateJitCacheRegion(ulong address, ulong size) { ulong[] overlapAddresses = []; diff --git a/src/Ryujinx.Common/Logging/LogClass.cs b/src/Ryujinx.Common/Logging/LogClass.cs index a4117580e..89f0336dc 100644 --- a/src/Ryujinx.Common/Logging/LogClass.cs +++ b/src/Ryujinx.Common/Logging/LogClass.cs @@ -13,6 +13,7 @@ namespace Ryujinx.Common.Logging Cpu, Emulation, FFmpeg, + GdbStub, Font, Gpu, Hid, diff --git a/src/Ryujinx.Cpu/AppleHv/Arm/ExceptionLevel.cs b/src/Ryujinx.Cpu/AppleHv/Arm/ExceptionLevel.cs new file mode 100644 index 000000000..08114e12a --- /dev/null +++ b/src/Ryujinx.Cpu/AppleHv/Arm/ExceptionLevel.cs @@ -0,0 +1,10 @@ +namespace Ryujinx.Cpu.AppleHv.Arm +{ + enum ExceptionLevel : uint + { + PstateMask = 0xfffffff0, + EL1h = 0b0101, + El1t = 0b0100, + EL0 = 0b0000, + } +} diff --git a/src/Ryujinx.Cpu/AppleHv/HvExecutionContext.cs b/src/Ryujinx.Cpu/AppleHv/HvExecutionContext.cs index 53cea5385..f13662e44 100644 --- a/src/Ryujinx.Cpu/AppleHv/HvExecutionContext.cs +++ b/src/Ryujinx.Cpu/AppleHv/HvExecutionContext.cs @@ -11,7 +11,18 @@ namespace Ryujinx.Cpu.AppleHv class HvExecutionContext : IExecutionContext { /// - public ulong Pc => _impl.ElrEl1; + public ulong Pc + { + get + { + uint currentEl = Pstate & ~((uint)ExceptionLevel.PstateMask); + if (currentEl == (uint)ExceptionLevel.EL1h) + { + return _impl.ElrEl1; + } + return _impl.Pc; + } + } /// public long TpidrEl0 @@ -48,6 +59,9 @@ namespace Ryujinx.Cpu.AppleHv set => _impl.Fpsr = value; } + /// + public ulong ThreadUid { get; set; } + /// public bool IsAarch32 { @@ -67,6 +81,7 @@ namespace Ryujinx.Cpu.AppleHv private readonly ICounter _counter; private readonly IHvExecutionContext _shadowContext; private IHvExecutionContext _impl; + private int _shouldStep; private readonly ExceptionCallbacks _exceptionCallbacks; @@ -103,6 +118,11 @@ namespace Ryujinx.Cpu.AppleHv _exceptionCallbacks.BreakCallback?.Invoke(this, address, imm); } + private void StepHandler() + { + _exceptionCallbacks.StepCallback?.Invoke(this); + } + private void SupervisorCallHandler(ulong address, int imm) { _exceptionCallbacks.SupervisorCallback?.Invoke(this, address, imm); @@ -127,6 +147,30 @@ namespace Ryujinx.Cpu.AppleHv return Interlocked.Exchange(ref _interruptRequested, 0) != 0; } + /// + public void RequestDebugStep() + { + Interlocked.Exchange(ref _shouldStep, 1); + } + + /// + public ulong DebugPc + { + get => Pc; + set + { + uint currentEl = Pstate & ~((uint)ExceptionLevel.PstateMask); + if (currentEl == (uint)ExceptionLevel.EL1h) + { + _impl.ElrEl1 = value; + } + else + { + _impl.Pc = value; + } + } + } + /// public void StopRunning() { @@ -142,6 +186,22 @@ namespace Ryujinx.Cpu.AppleHv while (Running) { + if (Interlocked.CompareExchange(ref _shouldStep, 0, 1) == 1) + { + uint currentEl = Pstate & ~((uint)ExceptionLevel.PstateMask); + if (currentEl == (uint)ExceptionLevel.EL1h) + { + HvApi.hv_vcpu_get_sys_reg(vcpu.Handle, HvSysReg.SPSR_EL1, out ulong spsr).ThrowOnError(); + spsr |= (1 << 21); + HvApi.hv_vcpu_set_sys_reg(vcpu.Handle, HvSysReg.SPSR_EL1, spsr); + } + else + { + Pstate |= (1 << 21); + } + HvApi.hv_vcpu_set_sys_reg(vcpu.Handle, HvSysReg.MDSCR_EL1, 1); + } + HvApi.hv_vcpu_run(vcpu.Handle).ThrowOnError(); HvExitReason reason = vcpu.ExitInfo->Reason; @@ -209,6 +269,20 @@ namespace Ryujinx.Cpu.AppleHv SupervisorCallHandler(elr - 4UL, id); vcpu = RentFromPool(memoryManager.AddressSpace, vcpu); break; + case ExceptionClass.SoftwareStepLowerEl: + HvApi.hv_vcpu_get_sys_reg(vcpuHandle, HvSysReg.SPSR_EL1, out ulong spsr).ThrowOnError(); + spsr &= ~((ulong)(1 << 21)); + HvApi.hv_vcpu_set_sys_reg(vcpuHandle, HvSysReg.SPSR_EL1, spsr).ThrowOnError(); + HvApi.hv_vcpu_set_sys_reg(vcpuHandle, HvSysReg.MDSCR_EL1, 0); + ReturnToPool(vcpu); + StepHandler(); + vcpu = RentFromPool(memoryManager.AddressSpace, vcpu); + break; + case ExceptionClass.BrkAarch64: + ReturnToPool(vcpu); + BreakHandler(elr, (ushort)esr); + vcpu = RentFromPool(memoryManager.AddressSpace, vcpu); + break; default: throw new Exception($"Unhandled guest exception {ec}."); } @@ -219,10 +293,7 @@ namespace Ryujinx.Cpu.AppleHv // TODO: Invalidate only the range that was modified? return HvAddressSpace.KernelRegionTlbiEretAddress; } - else - { - return HvAddressSpace.KernelRegionEretAddress; - } + return HvAddressSpace.KernelRegionEretAddress; } private static void DataAbort(MemoryTracking tracking, ulong vcpu, uint esr) diff --git a/src/Ryujinx.Cpu/AppleHv/HvExecutionContextShadow.cs b/src/Ryujinx.Cpu/AppleHv/HvExecutionContextShadow.cs index 6ce8e1800..4ea5f276d 100644 --- a/src/Ryujinx.Cpu/AppleHv/HvExecutionContextShadow.cs +++ b/src/Ryujinx.Cpu/AppleHv/HvExecutionContextShadow.cs @@ -18,6 +18,8 @@ namespace Ryujinx.Cpu.AppleHv public bool IsAarch32 { get; set; } + public ulong ThreadUid { get; set; } + private readonly ulong[] _x; private readonly V128[] _v; @@ -46,5 +48,14 @@ namespace Ryujinx.Cpu.AppleHv { _v[index] = value; } + + public void RequestInterrupt() + { + } + + public bool GetAndClearInterruptRequested() + { + return false; + } } } diff --git a/src/Ryujinx.Cpu/AppleHv/HvExecutionContextVcpu.cs b/src/Ryujinx.Cpu/AppleHv/HvExecutionContextVcpu.cs index 1949cabdf..9ef03e61e 100644 --- a/src/Ryujinx.Cpu/AppleHv/HvExecutionContextVcpu.cs +++ b/src/Ryujinx.Cpu/AppleHv/HvExecutionContextVcpu.cs @@ -2,6 +2,7 @@ using ARMeilleure.State; using Ryujinx.Memory; using System.Runtime.InteropServices; using System.Runtime.Versioning; +using System.Threading; namespace Ryujinx.Cpu.AppleHv { @@ -13,6 +14,8 @@ namespace Ryujinx.Cpu.AppleHv private static readonly SetSimdFpReg _setSimdFpReg; private static readonly nint _setSimdFpRegNativePtr; + public ulong ThreadUid { get; set; } + static HvExecutionContextVcpu() { // .NET does not support passing vectors by value, so we need to pass a pointer and use a native @@ -135,6 +138,7 @@ namespace Ryujinx.Cpu.AppleHv } private readonly ulong _vcpu; + private int _interruptRequested; public HvExecutionContextVcpu(ulong vcpu) { @@ -180,8 +184,16 @@ namespace Ryujinx.Cpu.AppleHv public void RequestInterrupt() { - ulong vcpu = _vcpu; - HvApi.hv_vcpus_exit(ref vcpu, 1); + if (Interlocked.Exchange(ref _interruptRequested, 1) == 0) + { + ulong vcpu = _vcpu; + HvApi.hv_vcpus_exit(ref vcpu, 1); + } + } + + public bool GetAndClearInterruptRequested() + { + return Interlocked.Exchange(ref _interruptRequested, 0) != 0; } } } diff --git a/src/Ryujinx.Cpu/AppleHv/IHvExecutionContext.cs b/src/Ryujinx.Cpu/AppleHv/IHvExecutionContext.cs index 54b73acc6..134405b5c 100644 --- a/src/Ryujinx.Cpu/AppleHv/IHvExecutionContext.cs +++ b/src/Ryujinx.Cpu/AppleHv/IHvExecutionContext.cs @@ -15,6 +15,7 @@ namespace Ryujinx.Cpu.AppleHv uint Fpcr { get; set; } uint Fpsr { get; set; } + ulong ThreadUid { get; set; } ulong GetX(int index); void SetX(int index, ulong value); @@ -39,5 +40,8 @@ namespace Ryujinx.Cpu.AppleHv SetV(i, context.GetV(i)); } } + + void RequestInterrupt(); + bool GetAndClearInterruptRequested(); } } diff --git a/src/Ryujinx.Cpu/ExceptionCallbacks.cs b/src/Ryujinx.Cpu/ExceptionCallbacks.cs index d9293302b..6e50b4d70 100644 --- a/src/Ryujinx.Cpu/ExceptionCallbacks.cs +++ b/src/Ryujinx.Cpu/ExceptionCallbacks.cs @@ -29,6 +29,11 @@ namespace Ryujinx.Cpu /// public readonly ExceptionCallback BreakCallback; + /// + /// Handler for CPU software interrupts caused by single-stepping. + /// + public readonly ExceptionCallbackNoArgs StepCallback; + /// /// Handler for CPU software interrupts caused by the Arm SVC instruction. /// @@ -47,16 +52,19 @@ namespace Ryujinx.Cpu /// /// Handler for CPU interrupts triggered using /// Handler for CPU software interrupts caused by the Arm BRK instruction + /// Handler for CPU software interrupts caused by single-stepping /// Handler for CPU software interrupts caused by the Arm SVC instruction /// Handler for CPU software interrupts caused by any undefined Arm instruction public ExceptionCallbacks( ExceptionCallbackNoArgs interruptCallback = null, ExceptionCallback breakCallback = null, + ExceptionCallbackNoArgs stepCallback = null, ExceptionCallback supervisorCallback = null, ExceptionCallback undefinedCallback = null) { InterruptCallback = interruptCallback; BreakCallback = breakCallback; + StepCallback = stepCallback; SupervisorCallback = supervisorCallback; UndefinedCallback = undefinedCallback; } diff --git a/src/Ryujinx.Cpu/IExecutionContext.cs b/src/Ryujinx.Cpu/IExecutionContext.cs index c38210800..df0c94278 100644 --- a/src/Ryujinx.Cpu/IExecutionContext.cs +++ b/src/Ryujinx.Cpu/IExecutionContext.cs @@ -1,5 +1,6 @@ using ARMeilleure.State; using System; +using System.Threading; namespace Ryujinx.Cpu { @@ -46,6 +47,11 @@ namespace Ryujinx.Cpu /// bool IsAarch32 { get; set; } + /// + /// Thread UID. + /// + public ulong ThreadUid { get; set; } + /// /// Indicates whenever the CPU is still running code. /// @@ -108,5 +114,23 @@ namespace Ryujinx.Cpu /// If you only need to pause the thread temporarily, use instead. /// void StopRunning(); + + /// + /// Requests the thread to stop running temporarily and call . + /// + /// + /// The thread might not pause immediately. + /// One must not assume that guest code is no longer being executed by the thread after calling this function. + /// After single stepping, the thread should call call . + /// + void RequestDebugStep(); + + /// + /// Current Program Counter (for debugging). + /// + /// + /// PC register for the debugger. Must not be accessed while the thread isn't stopped for debugging. + /// + ulong DebugPc { get; set; } } } diff --git a/src/Ryujinx.Cpu/Jit/JitExecutionContext.cs b/src/Ryujinx.Cpu/Jit/JitExecutionContext.cs index f15486e68..f00acc1d7 100644 --- a/src/Ryujinx.Cpu/Jit/JitExecutionContext.cs +++ b/src/Ryujinx.Cpu/Jit/JitExecutionContext.cs @@ -1,5 +1,6 @@ using ARMeilleure.Memory; using ARMeilleure.State; +using ExecutionContext = ARMeilleure.State.ExecutionContext; namespace Ryujinx.Cpu.Jit { @@ -53,6 +54,13 @@ namespace Ryujinx.Cpu.Jit set => _impl.IsAarch32 = value; } + /// + public ulong ThreadUid + { + get => _impl.ThreadUid; + set => _impl.ThreadUid = value; + } + /// public bool Running => _impl.Running; @@ -65,6 +73,7 @@ namespace Ryujinx.Cpu.Jit counter, InterruptHandler, BreakHandler, + StepHandler, SupervisorCallHandler, UndefinedHandler); @@ -93,6 +102,11 @@ namespace Ryujinx.Cpu.Jit _exceptionCallbacks.BreakCallback?.Invoke(this, address, imm); } + private void StepHandler(ExecutionContext context) + { + _exceptionCallbacks.StepCallback?.Invoke(this); + } + private void SupervisorCallHandler(ExecutionContext context, ulong address, int imm) { _exceptionCallbacks.SupervisorCallback?.Invoke(this, address, imm); @@ -109,6 +123,16 @@ namespace Ryujinx.Cpu.Jit _impl.RequestInterrupt(); } + /// + public void RequestDebugStep() => _impl.RequestDebugStep(); + + /// + public ulong DebugPc + { + get => _impl.DebugPc; + set => _impl.DebugPc = value; + } + /// public void StopRunning() { diff --git a/src/Ryujinx.Cpu/LightningJit/State/ExecutionContext.cs b/src/Ryujinx.Cpu/LightningJit/State/ExecutionContext.cs index a366dcca6..a1ba0002e 100644 --- a/src/Ryujinx.Cpu/LightningJit/State/ExecutionContext.cs +++ b/src/Ryujinx.Cpu/LightningJit/State/ExecutionContext.cs @@ -1,6 +1,8 @@ +using ARMeilleure; using ARMeilleure.Memory; using ARMeilleure.State; using System; +using System.Threading; namespace Ryujinx.Cpu.LightningJit.State { @@ -51,6 +53,8 @@ namespace Ryujinx.Cpu.LightningJit.State } public bool IsAarch32 { get; set; } + + public ulong ThreadUid { get; set; } internal ExecutionMode ExecutionMode { @@ -77,15 +81,20 @@ namespace Ryujinx.Cpu.LightningJit.State private readonly ExceptionCallbackNoArgs _interruptCallback; private readonly ExceptionCallback _breakCallback; + private readonly ExceptionCallbackNoArgs _stepCallback; private readonly ExceptionCallback _supervisorCallback; private readonly ExceptionCallback _undefinedCallback; + internal int ShouldStep; + public ulong DebugPc { get; set; } + public ExecutionContext(IJitMemoryAllocator allocator, ICounter counter, ExceptionCallbacks exceptionCallbacks) { _nativeContext = new NativeContext(allocator); _counter = counter; _interruptCallback = exceptionCallbacks.InterruptCallback; _breakCallback = exceptionCallbacks.BreakCallback; + _stepCallback = exceptionCallbacks.StepCallback; _supervisorCallback = exceptionCallbacks.SupervisorCallback; _undefinedCallback = exceptionCallbacks.UndefinedCallback; @@ -117,6 +126,17 @@ namespace Ryujinx.Cpu.LightningJit.State _interrupted = true; } + public void StepHandler() + { + _stepCallback?.Invoke(this); + } + + public void RequestDebugStep() + { + Interlocked.Exchange(ref ShouldStep, 1); + RequestInterrupt(); + } + internal void OnBreak(ulong address, int imm) { _breakCallback?.Invoke(this, address, imm); diff --git a/src/Ryujinx.HLE/Debugger/BreakpointManager.cs b/src/Ryujinx.HLE/Debugger/BreakpointManager.cs new file mode 100644 index 000000000..bf462a781 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/BreakpointManager.cs @@ -0,0 +1,203 @@ +using Ryujinx.Common; +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Kernel.Threading; +using Ryujinx.Memory; +using System.Collections.Concurrent; +using System.Linq; + +namespace Ryujinx.HLE.Debugger +{ + internal class Breakpoint + { + public byte[] OriginalData { get; } + + public bool IsStep { get; } + + public Breakpoint(byte[] originalData, bool isStep) + { + OriginalData = originalData; + IsStep = isStep; + } + } + + /// + /// Manages software breakpoints for the debugger. + /// + public class BreakpointManager + { + private readonly Debugger _debugger; + private readonly ConcurrentDictionary _breakpoints = new(); + + private static readonly byte[] _aarch64BreakInstruction = { 0x00, 0x00, 0x20, 0xD4 }; // BRK #0 + private static readonly byte[] _aarch32BreakInstruction = { 0xFE, 0xDE, 0xFF, 0xE7 }; // TRAP + private static readonly byte[] _aarch32ThumbBreakInstruction = { 0x80, 0xB6 }; + + public BreakpointManager(Debugger debugger) + { + _debugger = debugger; + } + + /// + /// Sets a software breakpoint at a specified address. + /// + /// The memory address to set the breakpoint at. + /// 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) + { + if (_breakpoints.ContainsKey(address)) + { + return false; + } + + byte[] breakInstruction = GetBreakInstruction(length); + if (breakInstruction == null) + { + Logger.Error?.Print(LogClass.GdbStub, $"Unsupported instruction length for breakpoint: {length}"); + return false; + } + + var originalInstruction = new byte[length]; + if (!ReadMemory(address, originalInstruction)) + { + Logger.Error?.Print(LogClass.GdbStub, $"Failed to read memory at 0x{address:X16} to set breakpoint."); + return false; + } + + if (!WriteMemory(address, breakInstruction)) + { + Logger.Error?.Print(LogClass.GdbStub, $"Failed to write breakpoint at 0x{address:X16}."); + return false; + } + + var breakpoint = new Breakpoint(originalInstruction, isStep); + if (_breakpoints.TryAdd(address, breakpoint)) + { + Logger.Debug?.Print(LogClass.GdbStub, $"Breakpoint set at 0x{address:X16}"); + return true; + } + + Logger.Error?.Print(LogClass.GdbStub, $"Failed to add breakpoint at 0x{address:X16}."); + return false; + } + + /// + /// Clears a software breakpoint at a specified address. + /// + /// The memory address of the breakpoint to clear. + /// The length of the instruction (unused). + /// True if the breakpoint was cleared successfully; otherwise, false. + public bool ClearBreakPoint(ulong address, ulong length) + { + if (_breakpoints.TryGetValue(address, out Breakpoint breakpoint)) + { + if (!WriteMemory(address, breakpoint.OriginalData)) + { + Logger.Error?.Print(LogClass.GdbStub, $"Failed to restore original instruction at 0x{address:X16} to clear breakpoint."); + return false; + } + + _breakpoints.TryRemove(address, out _); + Logger.Debug?.Print(LogClass.GdbStub, $"Breakpoint cleared at 0x{address:X16}"); + return true; + } + + Logger.Warning?.Print(LogClass.GdbStub, $"No breakpoint found at address 0x{address:X16}"); + return false; + } + + /// + /// Clears all currently set software breakpoints. + /// + public void ClearAll() + { + foreach (var bp in _breakpoints) + { + if (!WriteMemory(bp.Key, bp.Value.OriginalData)) + { + Logger.Error?.Print(LogClass.GdbStub, $"Failed to restore original instruction at 0x{bp.Key:X16} while clearing all breakpoints."); + } + + } + _breakpoints.Clear(); + Logger.Debug?.Print(LogClass.GdbStub, "All breakpoints cleared."); + } + + /// + /// Clears all currently set single-step software breakpoints. + /// + public void ClearAllStepBreakpoints() + { + var stepBreakpoints = _breakpoints.Where(p => p.Value.IsStep).ToList(); + + if (stepBreakpoints.Count == 0) + { + return; + } + + foreach (var bp in stepBreakpoints) + { + if (_breakpoints.TryRemove(bp.Key, out Breakpoint removedBreakpoint)) + { + WriteMemory(bp.Key, removedBreakpoint.OriginalData); + } + } + + Logger.Debug?.Print(LogClass.GdbStub, "All step breakpoints cleared."); + } + + + private byte[] GetBreakInstruction(ulong length) + { + if (_debugger.IsProcessAarch32) + { + if (length == 2) + { + return _aarch32ThumbBreakInstruction; + } + + if (length == 4) + { + return _aarch32BreakInstruction; + } + } + else + { + if (length == 4) + { + return _aarch64BreakInstruction; + } + } + + return null; + } + + private bool ReadMemory(ulong address, byte[] data) + { + try + { + _debugger.DebugProcess.CpuMemory.Read(address, data); + return true; + } + catch (InvalidMemoryRegionException) + { + return false; + } + } + + private bool WriteMemory(ulong address, byte[] data) + { + try + { + _debugger.DebugProcess.CpuMemory.Write(address, data); + _debugger.DebugProcess.InvalidateCacheRegion(address, (ulong)data.Length); + return true; + } + catch (InvalidMemoryRegionException) + { + return false; + } + } + } +} diff --git a/src/Ryujinx.HLE/Debugger/DebugState.cs b/src/Ryujinx.HLE/Debugger/DebugState.cs new file mode 100644 index 000000000..d2efa2bff --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/DebugState.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.HLE.Debugger +{ + public enum DebugState + { + Running, + Stopping, + Stopped, + } +} diff --git a/src/Ryujinx.HLE/Debugger/Debugger.cs b/src/Ryujinx.HLE/Debugger/Debugger.cs new file mode 100644 index 000000000..7a626b840 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/Debugger.cs @@ -0,0 +1,1327 @@ +using ARMeilleure.State; +using Ryujinx.Common; +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Kernel; +using Ryujinx.HLE.HOS.Kernel.Process; +using Ryujinx.HLE.HOS.Kernel.Threading; +using Ryujinx.Memory; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using IExecutionContext = Ryujinx.Cpu.IExecutionContext; + +namespace Ryujinx.HLE.Debugger +{ + public class Debugger : IDisposable + { + internal Switch Device { get; private set; } + + public ushort GdbStubPort { get; private set; } + + private TcpListener ListenerSocket; + private Socket ClientSocket = null; + private NetworkStream ReadStream = null; + private NetworkStream WriteStream = null; + private BlockingCollection Messages = new BlockingCollection(1); + private Thread DebuggerThread; + private Thread MessageHandlerThread; + private bool _shuttingDown = false; + private ManualResetEventSlim _breakHandlerEvent = new ManualResetEventSlim(false); + + private ulong? cThread; + private ulong? gThread; + + private BreakpointManager BreakpointManager; + + private string previousThreadListXml = ""; + + public Debugger(Switch device, ushort port) + { + Device = device; + GdbStubPort = port; + + ARMeilleure.Optimizations.EnableDebugging = true; + + DebuggerThread = new Thread(DebuggerThreadMain); + DebuggerThread.Start(); + MessageHandlerThread = new Thread(MessageHandlerMain); + MessageHandlerThread.Start(); + BreakpointManager = new BreakpointManager(this); + } + + internal KProcess Process => Device.System?.DebugGetApplicationProcess(); + internal IDebuggableProcess DebugProcess => Device.System?.DebugGetApplicationProcessDebugInterface(); + private KThread[] GetThreads() => DebugProcess.GetThreadUids().Select(x => DebugProcess.GetThread(x)).ToArray(); + internal bool IsProcessAarch32 => DebugProcess.GetThread(gThread.Value).Context.IsAarch32; + private KernelContext KernelContext => Device.System.KernelContext; + + const int GdbRegisterCount64 = 68; + const int GdbRegisterCount32 = 66; + /* FPCR = FPSR & ~FpcrMask + All of FPCR's bits are reserved in FPCR and vice versa, + see ARM's documentation. */ + private const uint FpcrMask = 0xfc1fffff; + + 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; + } + } + + private void MessageHandlerMain() + { + while (!_shuttingDown) + { + IMessage msg = Messages.Take(); + try { + switch (msg) + { + case BreakInMessage: + Logger.Notice.Print(LogClass.GdbStub, "Break-in requested"); + CommandInterrupt(); + break; + + case SendNackMessage: + WriteStream.WriteByte((byte)'-'); + break; + + case CommandMessage { Command: var cmd }: + Logger.Debug?.Print(LogClass.GdbStub, $"Received Command: {cmd}"); + WriteStream.WriteByte((byte)'+'); + ProcessCommand(cmd); + break; + + case ThreadBreakMessage { Context: var ctx }: + DebugProcess.DebugStop(); + gThread = cThread = ctx.ThreadUid; + _breakHandlerEvent.Set(); + Reply($"T05thread:{ctx.ThreadUid:x};"); + break; + + case KillMessage: + return; + } + } + catch (IOException e) + { + Logger.Error?.Print(LogClass.GdbStub, "Error while processing GDB messages", e); + } + catch (NullReferenceException e) + { + Logger.Error?.Print(LogClass.GdbStub, "Error while processing GDB messages", e); + } + } + } + + private void ProcessCommand(string cmd) + { + StringStream ss = new StringStream(cmd); + + switch (ss.ReadChar()) + { + case '!': + if (!ss.IsEmpty()) + { + goto unknownCommand; + } + + // Enable extended mode + ReplyOK(); + break; + case '?': + if (!ss.IsEmpty()) + { + goto unknownCommand; + } + + CommandQuery(); + break; + case 'c': + CommandContinue(ss.IsEmpty() ? null : ss.ReadRemainingAsHex()); + break; + case 'D': + if (!ss.IsEmpty()) + { + goto unknownCommand; + } + + CommandDetach(); + break; + case 'g': + if (!ss.IsEmpty()) + { + goto unknownCommand; + } + + CommandReadRegisters(); + break; + case 'G': + CommandWriteRegisters(ss); + break; + case 'H': + { + char op = ss.ReadChar(); + ulong? threadId = ss.ReadRemainingAsThreadUid(); + CommandSetThread(op, threadId); + break; + } + case 'k': + Logger.Notice.Print(LogClass.GdbStub, "Kill request received, detach instead"); + Reply(""); + CommandDetach(); + break; + case 'm': + { + ulong addr = ss.ReadUntilAsHex(','); + ulong len = ss.ReadRemainingAsHex(); + CommandReadMemory(addr, len); + break; + } + case 'M': + { + ulong addr = ss.ReadUntilAsHex(','); + ulong len = ss.ReadUntilAsHex(':'); + CommandWriteMemory(addr, len, ss); + break; + } + case 'p': + { + ulong gdbRegId = ss.ReadRemainingAsHex(); + CommandReadRegister((int)gdbRegId); + break; + } + case 'P': + { + ulong gdbRegId = ss.ReadUntilAsHex('='); + CommandWriteRegister((int)gdbRegId, ss); + break; + } + case 'q': + if (ss.ConsumeRemaining("GDBServerVersion")) + { + Reply($"name:Ryujinx;version:{ReleaseInformation.Version};"); + break; + } + + if (ss.ConsumeRemaining("HostInfo")) + { + if (IsProcessAarch32) + { + Reply( + $"triple:{ToHex("arm-unknown-linux-android")};endian:little;ptrsize:4;hostname:{ToHex("Ryujinx")};"); + } + else + { + Reply( + $"triple:{ToHex("aarch64-unknown-linux-android")};endian:little;ptrsize:8;hostname:{ToHex("Ryujinx")};"); + } + break; + } + + if (ss.ConsumeRemaining("ProcessInfo")) + { + if (IsProcessAarch32) + { + Reply( + $"pid:1;cputype:12;cpusubtype:0;triple:{ToHex("arm-unknown-linux-android")};ostype:unknown;vendor:none;endian:little;ptrsize:4;"); + } + else + { + Reply( + $"pid:1;cputype:100000c;cpusubtype:0;triple:{ToHex("aarch64-unknown-linux-android")};ostype:unknown;vendor:none;endian:little;ptrsize:8;"); + } + break; + } + + if (ss.ConsumePrefix("Supported:") || ss.ConsumeRemaining("Supported")) + { + Reply("PacketSize=10000;qXfer:features:read+;qXfer:threads:read+;vContSupported+"); + break; + } + + if (ss.ConsumePrefix("Rcmd,")) + { + string hexCommand = ss.ReadRemaining(); + HandleQRcmdCommand(hexCommand); + break; + } + + if (ss.ConsumeRemaining("fThreadInfo")) + { + Reply($"m{string.Join(",", DebugProcess.GetThreadUids().Select(x => $"{x:x}"))}"); + break; + } + + if (ss.ConsumeRemaining("sThreadInfo")) + { + Reply("l"); + break; + } + + if (ss.ConsumePrefix("ThreadExtraInfo,")) + { + ulong? threadId = ss.ReadRemainingAsThreadUid(); + if (threadId == null) + { + ReplyError(); + break; + } + + if (DebugProcess.IsThreadPaused(DebugProcess.GetThread(threadId.Value))) + { + Reply(ToHex("Paused")); + } + else + { + Reply(ToHex("Running")); + } + break; + } + + if (ss.ConsumePrefix("Xfer:threads:read:")) + { + ss.ReadUntil(':'); + ulong offset = ss.ReadUntilAsHex(','); + ulong len = ss.ReadRemainingAsHex(); + + var data = ""; + if (offset > 0) + { + data = previousThreadListXml; + } else + { + previousThreadListXml = data = GetThreadListXml(); + } + + if (offset >= (ulong)data.Length) + { + Reply("l"); + break; + } + + if (len >= (ulong)data.Length - offset) + { + Reply("l" + ToBinaryFormat(data.Substring((int)offset))); + break; + } + else + { + Reply("m" + ToBinaryFormat(data.Substring((int)offset, (int)len))); + break; + } + } + + if (ss.ConsumePrefix("Xfer:features:read:")) + { + string feature = ss.ReadUntil(':'); + ulong offset = ss.ReadUntilAsHex(','); + ulong len = ss.ReadRemainingAsHex(); + + if (feature == "target.xml") + { + feature = IsProcessAarch32 ? "target32.xml" : "target64.xml"; + } + + string data; + if (RegisterInformation.Features.TryGetValue(feature, out data)) + { + if (offset >= (ulong)data.Length) + { + Reply("l"); + break; + } + + if (len >= (ulong)data.Length - offset) + { + Reply("l" + ToBinaryFormat(data.Substring((int)offset))); + break; + } + else + { + Reply("m" + ToBinaryFormat(data.Substring((int)offset, (int)len))); + break; + } + } + else + { + Reply("E00"); // Invalid annex + break; + } + } + + goto unknownCommand; + case 'Q': + goto unknownCommand; + case 's': + CommandStep(ss.IsEmpty() ? null : ss.ReadRemainingAsHex()); + break; + case 'T': + { + ulong? threadId = ss.ReadRemainingAsThreadUid(); + CommandIsAlive(threadId); + break; + } + case 'v': + if (ss.ConsumePrefix("Cont")) + { + if (ss.ConsumeRemaining("?")) + { + Reply("vCont;c;C;s;S"); + break; + } + + if (ss.ConsumePrefix(";")) + { + HandleVContCommand(ss); + break; + } + + goto unknownCommand; + } + if (ss.ConsumeRemaining("MustReplyEmpty")) + { + Reply(""); + break; + } + goto unknownCommand; + case 'Z': + { + string type = ss.ReadUntil(','); + ulong addr = ss.ReadUntilAsHex(','); + ulong len = ss.ReadLengthAsHex(1); + string extra = ss.ReadRemaining(); + + if (extra.Length > 0) + { + Logger.Notice.Print(LogClass.GdbStub, $"Unsupported Z command extra data: {extra}"); + ReplyError(); + return; + } + + switch (type) + { + case "0": // Software breakpoint + if (!BreakpointManager.SetBreakPoint(addr, len, false)) + { + ReplyError(); + return; + } + ReplyOK(); + return; + case "1": // Hardware breakpoint + case "2": // Write watchpoint + case "3": // Read watchpoint + case "4": // Access watchpoint + ReplyError(); + return; + default: + ReplyError(); + return; + } + } + case 'z': + { + string type = ss.ReadUntil(','); + ss.ConsumePrefix(","); + ulong addr = ss.ReadUntilAsHex(','); + ulong len = ss.ReadLengthAsHex(1); + string extra = ss.ReadRemaining(); + + if (extra.Length > 0) + { + Logger.Notice.Print(LogClass.GdbStub, $"Unsupported z command extra data: {extra}"); + ReplyError(); + return; + } + + switch (type) + { + case "0": // Software breakpoint + if (!BreakpointManager.ClearBreakPoint(addr, len)) + { + ReplyError(); + return; + } + ReplyOK(); + return; + case "1": // Hardware breakpoint + case "2": // Write watchpoint + case "3": // Read watchpoint + case "4": // Access watchpoint + ReplyError(); + return; + default: + ReplyError(); + return; + } + } + default: + unknownCommand: + Logger.Notice.Print(LogClass.GdbStub, $"Unknown command: {cmd}"); + Reply(""); + break; + } + } + + enum VContAction + { + None, + Continue, + Stop, + Step + } + + record VContPendingAction(VContAction Action, ushort? Signal = null); + + private void HandleVContCommand(StringStream ss) + { + string[] rawActions = ss.ReadRemaining().Split(';', StringSplitOptions.RemoveEmptyEntries); + + var threadActionMap = new Dictionary(); + foreach (var thread in GetThreads()) + { + threadActionMap[thread.ThreadUid] = new VContPendingAction(VContAction.None); + } + + VContAction defaultAction = VContAction.None; + + // For each inferior thread, the *leftmost* action with a matching thread-id is applied. + for (int i = rawActions.Length - 1; i >= 0; i--) + { + var rawAction = rawActions[i]; + var stream = new StringStream(rawAction); + + char cmd = stream.ReadChar(); + VContAction action = cmd switch + { + '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) + { + threadId = GetThreads().First().ThreadUid; + } + + if (DebugProcess.GetThread(threadId.Value) == null) + { + ReplyError(); + return; + } + + switch (op) + { + case 'c': + cThread = threadId; + ReplyOK(); + return; + case 'g': + gThread = threadId; + ReplyOK(); + return; + default: + ReplyError(); + return; + } + } + + void CommandReadMemory(ulong addr, ulong len) + { + try + { + var data = new byte[len]; + DebugProcess.CpuMemory.Read(addr, data); + Reply(ToHex(data)); + } + catch (InvalidMemoryRegionException) + { + // InvalidAccessHandler will show an error message, we log it again to tell user the error is from GDB (which can be ignored) + // TODO: Do not let InvalidAccessHandler show the error message + Logger.Notice.Print(LogClass.GdbStub, $"GDB failed to read memory at 0x{addr:X16}"); + ReplyError(); + } + } + + void CommandWriteMemory(ulong addr, ulong len, StringStream ss) + { + try + { + var data = new byte[len]; + for (ulong i = 0; i < len; i++) + { + data[i] = (byte)ss.ReadLengthAsHex(2); + } + + DebugProcess.CpuMemory.Write(addr, data); + DebugProcess.InvalidateCacheRegion(addr, len); + ReplyOK(); + } + catch (InvalidMemoryRegionException) + { + ReplyError(); + } + } + + void CommandReadRegister(int gdbRegId) + { + if (gThread == null) + { + ReplyError(); + return; + } + + var ctx = DebugProcess.GetThread(gThread.Value).Context; + string result; + if (IsProcessAarch32) + { + result = GdbReadRegister32(ctx, gdbRegId); + if (result != null) + { + Reply(result); + } + else + { + ReplyError(); + } + } + else + { + result = GdbReadRegister64(ctx, gdbRegId); + if (result != null) + { + Reply(result); + } + else + { + ReplyError(); + } + } + } + + void CommandWriteRegister(int gdbRegId, StringStream ss) + { + if (gThread == null) + { + ReplyError(); + return; + } + + var ctx = DebugProcess.GetThread(gThread.Value).Context; + if (IsProcessAarch32) + { + if (GdbWriteRegister32(ctx, gdbRegId, ss) && ss.IsEmpty()) + { + ReplyOK(); + } + else + { + ReplyError(); + } + } + else + { + if (GdbWriteRegister64(ctx, gdbRegId, ss) && ss.IsEmpty()) + { + ReplyOK(); + } + else + { + ReplyError(); + } + } + } + + private void CommandStep(ulong? newPc) + { + if (cThread == null) + { + ReplyError(); + return; + } + + var thread = DebugProcess.GetThread(cThread.Value); + + if (newPc.HasValue) + { + thread.Context.DebugPc = newPc.Value; + } + + if (!DebugProcess.DebugStep(thread)) + { + ReplyError(); + } + else + { + gThread = cThread = thread.ThreadUid; + Reply($"T05thread:{thread.ThreadUid:x};"); + } + } + + private void CommandIsAlive(ulong? threadId) + { + if (GetThreads().Any(x => x.ThreadUid == threadId)) + { + ReplyOK(); + } + else + { + Reply("E00"); + } + } + + private void HandleQRcmdCommand(string hexCommand) + { + try + { + string command = FromHex(hexCommand); + Logger.Debug?.Print(LogClass.GdbStub, $"Received Rcmd: {command}"); + + string response = command.Trim().ToLowerInvariant() switch + { + "help" => "backtrace\nbt\nregisters\nreg\nget info\n", + "get info" => GetProcessInfo(), + "backtrace" => GetStackTrace(), + "bt" => GetStackTrace(), + "registers" => GetRegisters(), + "reg" => GetRegisters(), + _ => $"Unknown command: {command}\n" + }; + + Reply(ToHex(response)); + } + catch (Exception e) + { + Logger.Error?.Print(LogClass.GdbStub, $"Error processing Rcmd: {e.Message}"); + ReplyError(); + } + } + + private string GetStackTrace() + { + if (gThread == null) + return "No thread selected\n"; + + if (Process == null) + return "No application process found\n"; + + return Process.Debugger.GetGuestStackTrace(DebugProcess.GetThread(gThread.Value)); + } + + private string GetRegisters() + { + 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)); + } + + private string GetProcessInfo() + { + try + { + if (Process == null) + return "No application process found\n"; + + 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("Modules:"); + var debugger = kProcess.Debugger; + if (debugger != null) + { + var images = debugger.GetLoadedImages(); + for (int i = 0; i < images.Count; i++) + { + var image = images[i]; + ulong endAddress = image.BaseAddress + image.Size - 1; + string name = debugger.GetGuessedNsoNameFromIndex(i); + sb.AppendLine($" 0x{image.BaseAddress:x10} - 0x{endAddress:x10} {name}"); + } + } + + return sb.ToString(); + } + catch (Exception e) + { + Logger.Error?.Print(LogClass.GdbStub, $"Error getting process info: {e.Message}"); + return $"Error getting process info: {e.Message}\n"; + } + } + + private void Reply(string cmd) + { + Logger.Debug?.Print(LogClass.GdbStub, $"Reply: {cmd}"); + WriteStream.Write(Encoding.ASCII.GetBytes($"${cmd}#{CalculateChecksum(cmd):x2}")); + } + + private void ReplyOK() + { + Reply("OK"); + } + + private void ReplyError() + { + Reply("E01"); + } + + private void DebuggerThreadMain() + { + var endpoint = new IPEndPoint(IPAddress.Any, GdbStubPort); + ListenerSocket = new TcpListener(endpoint); + ListenerSocket.Start(); + Logger.Notice.Print(LogClass.GdbStub, $"Currently waiting on {endpoint} for GDB client"); + + while (!_shuttingDown) + { + try + { + ClientSocket = ListenerSocket.AcceptSocket(); + } + catch (SocketException) + { + return; + } + + // If the user connects before the application is running, wait for the application to start. + int retries = 10; + while (DebugProcess == null && retries-- > 0) + { + Thread.Sleep(200); + } + if (DebugProcess == null) + { + Logger.Warning?.Print(LogClass.GdbStub, "Application is not running, cannot accept GDB client connection"); + ClientSocket.Close(); + continue; + } + + ClientSocket.NoDelay = true; + ReadStream = new NetworkStream(ClientSocket, System.IO.FileAccess.Read); + WriteStream = new NetworkStream(ClientSocket, System.IO.FileAccess.Write); + Logger.Notice.Print(LogClass.GdbStub, "GDB client connected"); + + while (true) + { + try + { + switch (ReadStream.ReadByte()) + { + case -1: + goto eof; + case '+': + continue; + case '-': + Logger.Notice.Print(LogClass.GdbStub, "NACK received!"); + continue; + case '\x03': + Messages.Add(new BreakInMessage()); + break; + case '$': + string cmd = ""; + while (true) + { + int x = ReadStream.ReadByte(); + if (x == -1) + goto eof; + if (x == '#') + break; + cmd += (char)x; + } + + string checksum = $"{(char)ReadStream.ReadByte()}{(char)ReadStream.ReadByte()}"; + if (checksum == $"{CalculateChecksum(cmd):x2}") + { + Messages.Add(new CommandMessage(cmd)); + } + else + { + Messages.Add(new SendNackMessage()); + } + + break; + } + } + catch (IOException) + { + goto eof; + } + } + + eof: + Logger.Notice.Print(LogClass.GdbStub, "GDB client lost connection"); + ReadStream.Close(); + ReadStream = null; + WriteStream.Close(); + WriteStream = null; + ClientSocket.Close(); + ClientSocket = null; + + BreakpointManager.ClearAll(); + } + } + + private byte CalculateChecksum(string cmd) + { + byte checksum = 0; + foreach (char x in cmd) + { + unchecked + { + checksum += (byte)x; + } + } + + return checksum; + } + + private string FromHex(string hexString) + { + if (string.IsNullOrEmpty(hexString)) + return string.Empty; + + byte[] bytes = Convert.FromHexString(hexString); + return Encoding.ASCII.GetString(bytes); + } + + private string ToHex(byte[] bytes) + { + return string.Join("", bytes.Select(x => $"{x:x2}")); + } + + private string ToHex(string str) + { + return ToHex(Encoding.ASCII.GetBytes(str)); + } + + private string ToBinaryFormat(byte[] bytes) + { + return string.Join("", bytes.Select(x => + x switch + { + (byte)'#' => "}\x03", + (byte)'$' => "}\x04", + (byte)'*' => "}\x0a", + (byte)'}' => "}\x5d", + _ => Convert.ToChar(x).ToString(), + } + )); + } + + private string ToBinaryFormat(string str) + { + return ToBinaryFormat(Encoding.ASCII.GetBytes(str)); + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _shuttingDown = true; + + ListenerSocket.Stop(); + ClientSocket?.Shutdown(SocketShutdown.Both); + ClientSocket?.Close(); + ReadStream?.Close(); + WriteStream?.Close(); + DebuggerThread.Join(); + Messages.Add(new KillMessage()); + MessageHandlerThread.Join(); + Messages.Dispose(); + _breakHandlerEvent.Dispose(); + } + } + + public void BreakHandler(IExecutionContext ctx, ulong address, int imm) + { + DebugProcess.DebugInterruptHandler(ctx); + + _breakHandlerEvent.Reset(); + 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 + _breakHandlerEvent.Wait(5000); + } + + public void StepHandler(IExecutionContext ctx) + { + DebugProcess.DebugInterruptHandler(ctx); + } + } +} diff --git a/src/Ryujinx.HLE/Debugger/GdbSignal.cs b/src/Ryujinx.HLE/Debugger/GdbSignal.cs new file mode 100644 index 000000000..ee4efbda4 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/GdbSignal.cs @@ -0,0 +1,15 @@ +namespace Ryujinx.HLE.Debugger +{ + enum GdbSignal + { + Zero = 0, + Int = 2, + Quit = 3, + Trap = 5, + Abort = 6, + Alarm = 14, + IO = 23, + XCPU = 24, + Unknown = 143 + } +} diff --git a/src/Ryujinx.HLE/Debugger/GdbXml/aarch64-core.xml b/src/Ryujinx.HLE/Debugger/GdbXml/aarch64-core.xml new file mode 100644 index 000000000..9899a0e4a --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/GdbXml/aarch64-core.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx.HLE/Debugger/GdbXml/aarch64-fpu.xml b/src/Ryujinx.HLE/Debugger/GdbXml/aarch64-fpu.xml new file mode 100644 index 000000000..a09120bc4 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/GdbXml/aarch64-fpu.xml @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx.HLE/Debugger/GdbXml/arm-core.xml b/src/Ryujinx.HLE/Debugger/GdbXml/arm-core.xml new file mode 100644 index 000000000..2307d65f9 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/GdbXml/arm-core.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx.HLE/Debugger/GdbXml/arm-neon.xml b/src/Ryujinx.HLE/Debugger/GdbXml/arm-neon.xml new file mode 100644 index 000000000..d61f6b854 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/GdbXml/arm-neon.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx.HLE/Debugger/GdbXml/target32.xml b/src/Ryujinx.HLE/Debugger/GdbXml/target32.xml new file mode 100644 index 000000000..890679858 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/GdbXml/target32.xml @@ -0,0 +1,14 @@ + + + + + + arm + + + diff --git a/src/Ryujinx.HLE/Debugger/GdbXml/target64.xml b/src/Ryujinx.HLE/Debugger/GdbXml/target64.xml new file mode 100644 index 000000000..cfd5bf780 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/GdbXml/target64.xml @@ -0,0 +1,14 @@ + + + + + + aarch64 + + + diff --git a/src/Ryujinx.HLE/Debugger/IDebuggableProcess.cs b/src/Ryujinx.HLE/Debugger/IDebuggableProcess.cs new file mode 100644 index 000000000..0896f25d2 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/IDebuggableProcess.cs @@ -0,0 +1,21 @@ +using Ryujinx.Cpu; +using Ryujinx.HLE.HOS.Kernel.Threading; +using Ryujinx.Memory; + +namespace Ryujinx.HLE.Debugger +{ + internal interface IDebuggableProcess + { + void DebugStop(); + void DebugContinue(); + void DebugContinue(KThread thread); + bool DebugStep(KThread thread); + KThread GetThread(ulong threadUid); + DebugState GetDebugState(); + bool IsThreadPaused(KThread thread); + ulong[] GetThreadUids(); + public void DebugInterruptHandler(IExecutionContext ctx); + IVirtualMemoryManager CpuMemory { get; } + void InvalidateCacheRegion(ulong address, ulong size); + } +} diff --git a/src/Ryujinx.HLE/Debugger/Message/BreakInMessage.cs b/src/Ryujinx.HLE/Debugger/Message/BreakInMessage.cs new file mode 100644 index 000000000..81d8784ae --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/Message/BreakInMessage.cs @@ -0,0 +1,6 @@ +namespace Ryujinx.HLE.Debugger +{ + struct BreakInMessage : IMessage + { + } +} diff --git a/src/Ryujinx.HLE/Debugger/Message/CommandMessage.cs b/src/Ryujinx.HLE/Debugger/Message/CommandMessage.cs new file mode 100644 index 000000000..ad265d432 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/Message/CommandMessage.cs @@ -0,0 +1,12 @@ +namespace Ryujinx.HLE.Debugger +{ + struct CommandMessage : IMessage + { + public string Command; + + public CommandMessage(string cmd) + { + Command = cmd; + } + } +} diff --git a/src/Ryujinx.HLE/Debugger/Message/IMessage.cs b/src/Ryujinx.HLE/Debugger/Message/IMessage.cs new file mode 100644 index 000000000..4b03183c5 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/Message/IMessage.cs @@ -0,0 +1,6 @@ +namespace Ryujinx.HLE.Debugger +{ + interface IMessage + { + } +} diff --git a/src/Ryujinx.HLE/Debugger/Message/KillMessage.cs b/src/Ryujinx.HLE/Debugger/Message/KillMessage.cs new file mode 100644 index 000000000..43ae0f21e --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/Message/KillMessage.cs @@ -0,0 +1,6 @@ +namespace Ryujinx.HLE.Debugger +{ + struct KillMessage : IMessage + { + } +} diff --git a/src/Ryujinx.HLE/Debugger/Message/SendNackMessage.cs b/src/Ryujinx.HLE/Debugger/Message/SendNackMessage.cs new file mode 100644 index 000000000..ce804c46e --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/Message/SendNackMessage.cs @@ -0,0 +1,6 @@ +namespace Ryujinx.HLE.Debugger +{ + struct SendNackMessage : IMessage + { + } +} diff --git a/src/Ryujinx.HLE/Debugger/Message/ThreadBreakMessage.cs b/src/Ryujinx.HLE/Debugger/Message/ThreadBreakMessage.cs new file mode 100644 index 000000000..027096eeb --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/Message/ThreadBreakMessage.cs @@ -0,0 +1,18 @@ +using IExecutionContext = Ryujinx.Cpu.IExecutionContext; + +namespace Ryujinx.HLE.Debugger +{ + public class ThreadBreakMessage : IMessage + { + public IExecutionContext Context { get; } + public ulong Address { get; } + public int Opcode { get; } + + public ThreadBreakMessage(IExecutionContext context, ulong address, int opcode) + { + Context = context; + Address = address; + Opcode = opcode; + } + } +} diff --git a/src/Ryujinx.HLE/Debugger/RegisterInformation.cs b/src/Ryujinx.HLE/Debugger/RegisterInformation.cs new file mode 100644 index 000000000..b5fd88ea5 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/RegisterInformation.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.IO; + +namespace Ryujinx.HLE.Debugger +{ + class RegisterInformation + { + public static readonly Dictionary Features = new() + { + { "target64.xml", GetEmbeddedResourceContent("target64.xml") }, + { "target32.xml", GetEmbeddedResourceContent("target32.xml") }, + { "aarch64-core.xml", GetEmbeddedResourceContent("aarch64-core.xml") }, + { "aarch64-fpu.xml", GetEmbeddedResourceContent("aarch64-fpu.xml") }, + { "arm-core.xml", GetEmbeddedResourceContent("arm-core.xml") }, + { "arm-neon.xml", GetEmbeddedResourceContent("arm-neon.xml") }, + }; + + private static string GetEmbeddedResourceContent(string resourceName) + { + Stream stream = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("Ryujinx.HLE.Debugger.GdbXml." + resourceName); + StreamReader reader = new StreamReader(stream); + string result = reader.ReadToEnd(); + reader.Dispose(); + stream.Dispose(); + return result; + } + } +} diff --git a/src/Ryujinx.HLE/Debugger/StringStream.cs b/src/Ryujinx.HLE/Debugger/StringStream.cs new file mode 100644 index 000000000..d8148a9c2 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/StringStream.cs @@ -0,0 +1,109 @@ +using System.Diagnostics; +using System.Globalization; + +namespace Ryujinx.HLE.Debugger +{ + class StringStream + { + private readonly string Data; + private int Position; + + public StringStream(string s) + { + Data = s; + } + + public char ReadChar() + { + return Data[Position++]; + } + + public string ReadUntil(char needle) + { + int needlePos = Data.IndexOf(needle, Position); + + if (needlePos == -1) + { + needlePos = Data.Length; + } + + string result = Data.Substring(Position, needlePos - Position); + Position = needlePos + 1; + return result; + } + + public string ReadLength(int len) + { + string result = Data.Substring(Position, len); + Position += len; + return result; + } + + public string ReadRemaining() + { + string result = Data.Substring(Position); + Position = Data.Length; + return result; + } + + public ulong ReadRemainingAsHex() + { + return ulong.Parse(ReadRemaining(), NumberStyles.HexNumber); + } + + public ulong ReadUntilAsHex(char needle) + { + return ulong.Parse(ReadUntil(needle), NumberStyles.HexNumber); + } + + public ulong ReadLengthAsHex(int len) + { + return ulong.Parse(ReadLength(len), NumberStyles.HexNumber); + } + + public ulong ReadLengthAsLEHex(int len) + { + Debug.Assert(len % 2 == 0); + + ulong result = 0; + int pos = 0; + while (pos < len) + { + result += ReadLengthAsHex(2) << (4 * pos); + pos += 2; + } + return result; + } + + public ulong? ReadRemainingAsThreadUid() + { + string s = ReadRemaining(); + return s == "-1" ? null : ulong.Parse(s, NumberStyles.HexNumber); + } + + public bool ConsumePrefix(string prefix) + { + if (Data.Substring(Position).StartsWith(prefix)) + { + Position += prefix.Length; + return true; + } + return false; + } + + public bool ConsumeRemaining(string match) + { + if (Data.Substring(Position) == match) + { + Position += match.Length; + return true; + } + return false; + } + + public bool IsEmpty() + { + return Position >= Data.Length; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/ArmProcessContextFactory.cs b/src/Ryujinx.HLE/HOS/ArmProcessContextFactory.cs index 759780c42..28f7ef25f 100644 --- a/src/Ryujinx.HLE/HOS/ArmProcessContextFactory.cs +++ b/src/Ryujinx.HLE/HOS/ArmProcessContextFactory.cs @@ -69,7 +69,7 @@ namespace Ryujinx.HLE.HOS mode = MemoryManagerMode.SoftwarePageTable; } - ICpuEngine cpuEngine = isArm64Host && (mode == MemoryManagerMode.HostMapped || mode == MemoryManagerMode.HostMappedUnsafe) + ICpuEngine cpuEngine = isArm64Host && (mode == MemoryManagerMode.HostMapped || mode == MemoryManagerMode.HostMappedUnsafe) && !context.Device.Configuration.EnableGdbStub ? new LightningJitEngine(_tickSource) : new JitEngine(_tickSource); diff --git a/src/Ryujinx.HLE/HOS/Horizon.cs b/src/Ryujinx.HLE/HOS/Horizon.cs index 5063b4329..517f8ef16 100644 --- a/src/Ryujinx.HLE/HOS/Horizon.cs +++ b/src/Ryujinx.HLE/HOS/Horizon.cs @@ -5,6 +5,7 @@ using LibHac.Fs.Shim; using LibHac.FsSystem; using LibHac.Tools.FsSystem; using Ryujinx.Cpu; +using Ryujinx.HLE.Debugger; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS.Kernel; using Ryujinx.HLE.HOS.Kernel.Memory; @@ -500,5 +501,21 @@ namespace Ryujinx.HLE.HOS IsPaused = pause; } + + internal IDebuggableProcess DebugGetApplicationProcessDebugInterface() + { + lock (KernelContext.Processes) + { + return KernelContext.Processes.Values.FirstOrDefault(x => x.IsApplication)?.DebugInterface; + } + } + + internal KProcess DebugGetApplicationProcess() + { + lock (KernelContext.Processes) + { + return KernelContext.Processes.Values.FirstOrDefault(x => x.IsApplication); + } + } } } diff --git a/src/Ryujinx.HLE/HOS/Kernel/Process/HleProcessDebugger.cs b/src/Ryujinx.HLE/HOS/Kernel/Process/HleProcessDebugger.cs index c4a9835cc..87da9f7a6 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Process/HleProcessDebugger.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Process/HleProcessDebugger.cs @@ -4,6 +4,7 @@ using Ryujinx.HLE.HOS.Kernel.Memory; using Ryujinx.HLE.HOS.Kernel.Threading; using Ryujinx.HLE.Loaders.Elf; using Ryujinx.Memory; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -17,7 +18,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process private readonly KProcess _owner; - private class Image + public class Image { public ulong BaseAddress { get; } public ulong Size { get; } @@ -54,6 +55,15 @@ namespace Ryujinx.HLE.HOS.Kernel.Process trace.AppendLine($"Process: {_owner.Name}, PID: {_owner.Pid}"); + string ThreadName = thread.GetThreadName(); + + if (!String.IsNullOrEmpty(ThreadName)) + { + trace.AppendLine($"Thread ID: {thread.ThreadUid} ({ThreadName})"); + } else { + trace.AppendLine($"Thread ID: {thread.ThreadUid}"); + } + void AppendTrace(ulong address) { if (AnalyzePointer(out PointerInfo info, address, thread)) @@ -283,7 +293,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process return null; } - private string GetGuessedNsoNameFromIndex(int index) + public string GetGuessedNsoNameFromIndex(int index) { if ((uint)index > 11) { @@ -316,6 +326,16 @@ namespace Ryujinx.HLE.HOS.Kernel.Process } } + public List GetLoadedImages() + { + EnsureLoaded(); + + lock (_images) + { + return [.. _images]; + } + } + private void EnsureLoaded() { if (Interlocked.CompareExchange(ref _loaded, 1, 0) == 0) diff --git a/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs b/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs index 478b4e864..0a57f5bc6 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs @@ -1,6 +1,7 @@ using Ryujinx.Common; using Ryujinx.Common.Logging; using Ryujinx.Cpu; +using Ryujinx.HLE.Debugger; using Ryujinx.HLE.Exceptions; using Ryujinx.HLE.HOS.Kernel.Common; using Ryujinx.HLE.HOS.Kernel.Memory; @@ -11,6 +12,8 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; +using ExceptionCallback = Ryujinx.Cpu.ExceptionCallback; +using ExceptionCallbackNoArgs = Ryujinx.Cpu.ExceptionCallbackNoArgs; namespace Ryujinx.HLE.HOS.Kernel.Process { @@ -89,6 +92,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Process public IVirtualMemoryManager CpuMemory => Context.AddressSpace; public HleProcessDebugger Debugger { get; private set; } + public IDebuggableProcess DebugInterface { get; private set; } + protected int debugState = (int)DebugState.Running; public KProcess(KernelContext context, bool allowCodeMemoryForJit = false) : base(context) { @@ -110,6 +115,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process _threads = []; Debugger = new HleProcessDebugger(this); + DebugInterface = new DebuggerInterface(this); } public Result InitializeKip( @@ -679,6 +685,13 @@ namespace Ryujinx.HLE.HOS.Kernel.Process SetState(newState); + if (KernelContext.Device.Configuration.DebuggerSuspendOnStart && IsApplication) + { + mainThread.Suspend(ThreadSchedState.ThreadPauseFlag); + debugState = (int)DebugState.Stopped; + Logger.Notice.Print(LogClass.Kernel, $"Application is suspended on start for debugging."); + } + result = mainThread.Start(); if (result != Result.Success) @@ -727,9 +740,19 @@ namespace Ryujinx.HLE.HOS.Kernel.Process public IExecutionContext CreateExecutionContext() { + ExceptionCallback breakCallback = null; + ExceptionCallbackNoArgs stepCallback = null; + + if (KernelContext.Device.Configuration.EnableGdbStub && KernelContext.Device.Debugger != null) + { + breakCallback = KernelContext.Device.Debugger.BreakHandler; + stepCallback = KernelContext.Device.Debugger.StepHandler; + } + return Context?.CreateExecutionContext(new ExceptionCallbacks( InterruptHandler, - null, + breakCallback, + stepCallback, KernelContext.SyscallHandler.SvcCall, UndefinedInstructionHandler)); } @@ -1174,5 +1197,186 @@ namespace Ryujinx.HLE.HOS.Kernel.Process { return Capabilities.IsSvcPermitted(svcId); } + + private class DebuggerInterface : IDebuggableProcess + { + private Barrier StepBarrier; + private readonly KProcess _parent; + private readonly KernelContext _kernelContext; + private KThread steppingThread; + + public DebuggerInterface(KProcess p) + { + _parent = p; + _kernelContext = p.KernelContext; + StepBarrier = new(2); + } + + public void DebugStop() + { + if (Interlocked.CompareExchange(ref _parent.debugState, (int)DebugState.Stopping, + (int)DebugState.Running) != (int)DebugState.Running) + { + return; + } + + _kernelContext.CriticalSection.Enter(); + lock (_parent._threadingLock) + { + foreach (KThread thread in _parent._threads) + { + thread.Suspend(ThreadSchedState.ThreadPauseFlag); + thread.Context.RequestInterrupt(); + if (!thread.DebugHalt.WaitOne(TimeSpan.FromMilliseconds(50))) + { + Logger.Warning?.Print(LogClass.Kernel, $"Failed to suspend thread {thread.ThreadUid} in time."); + } + } + } + + _parent.debugState = (int)DebugState.Stopped; + _kernelContext.CriticalSection.Leave(); + } + + public void DebugContinue() + { + if (Interlocked.CompareExchange(ref _parent.debugState, (int)DebugState.Running, + (int)DebugState.Stopped) != (int)DebugState.Stopped) + { + return; + } + + _kernelContext.CriticalSection.Enter(); + lock (_parent._threadingLock) + { + foreach (KThread thread in _parent._threads) + { + thread.Resume(ThreadSchedState.ThreadPauseFlag); + } + } + _kernelContext.CriticalSection.Leave(); + } + + public void DebugContinue(KThread target) + { + Interlocked.Exchange(ref _parent.debugState, (int)DebugState.Running); + + _kernelContext.CriticalSection.Enter(); + lock (_parent._threadingLock) + { + target.Resume(ThreadSchedState.ThreadPauseFlag); + } + _kernelContext.CriticalSection.Leave(); + } + + public bool DebugStep(KThread target) + { + if (!IsThreadPaused(target)) + { + return false; + } + + _kernelContext.CriticalSection.Enter(); + steppingThread = target; + bool waiting = target.MutexOwner != null || target.WaitingSync || target.WaitingInArbitration; + target.Context.RequestDebugStep(); + if (waiting) + { + lock (_parent._threadingLock) + { + foreach (KThread thread in _parent._threads) + { + thread.Resume(ThreadSchedState.ThreadPauseFlag); + } + } + } + else + { + target.Resume(ThreadSchedState.ThreadPauseFlag); + } + _kernelContext.CriticalSection.Leave(); + + bool stepTimedOut = false; + if (!StepBarrier.SignalAndWait(TimeSpan.FromMilliseconds(2000))) + { + Logger.Warning?.Print(LogClass.Kernel, $"Failed to step thread {target.ThreadUid} in time."); + stepTimedOut = true; + } + + _kernelContext.CriticalSection.Enter(); + steppingThread = null; + if (waiting) + { + lock (_parent._threadingLock) + { + foreach (KThread thread in _parent._threads) + { + thread.Suspend(ThreadSchedState.ThreadPauseFlag); + } + } + } + else + { + target.Suspend(ThreadSchedState.ThreadPauseFlag); + } + _kernelContext.CriticalSection.Leave(); + + if (stepTimedOut) + { + return false; + } + + StepBarrier.SignalAndWait(); + return true; + } + + public DebugState GetDebugState() + { + return (DebugState)_parent.debugState; + } + + public bool IsThreadPaused(KThread target) + { + return (target.SchedFlags & ThreadSchedState.ThreadPauseFlag) != 0; + } + + public ulong[] GetThreadUids() + { + lock (_parent._threadingLock) + { + var threads = _parent._threads.Where(x => !x.TerminationRequested).ToArray(); + return threads.Select(x => x.ThreadUid).ToArray(); + } + } + + public KThread GetThread(ulong threadUid) + { + lock (_parent._threadingLock) + { + var threads = _parent._threads.Where(x => !x.TerminationRequested).ToArray(); + return threads.FirstOrDefault(x => x.ThreadUid == threadUid); + } + } + + public void DebugInterruptHandler(IExecutionContext ctx) + { + _kernelContext.CriticalSection.Enter(); + bool stepping = steppingThread != null; + _kernelContext.CriticalSection.Leave(); + if (stepping) + { + StepBarrier.SignalAndWait(); + StepBarrier.SignalAndWait(); + } + _parent.InterruptHandler(ctx); + } + + public IVirtualMemoryManager CpuMemory { get { return _parent.CpuMemory; } } + + public void InvalidateCacheRegion(ulong address, ulong size) + { + _parent.Context.InvalidateCacheRegion(address, size); + } + } } } diff --git a/src/Ryujinx.HLE/HOS/Kernel/Process/ProcessExecutionContext.cs b/src/Ryujinx.HLE/HOS/Kernel/Process/ProcessExecutionContext.cs index b8118fbb4..f0e44c4b7 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Process/ProcessExecutionContext.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Process/ProcessExecutionContext.cs @@ -1,5 +1,6 @@ using ARMeilleure.State; using Ryujinx.Cpu; +using System.Threading; namespace Ryujinx.HLE.HOS.Kernel.Process { @@ -17,10 +18,14 @@ namespace Ryujinx.HLE.HOS.Kernel.Process public bool IsAarch32 { get => false; set { } } + public ulong ThreadUid { get; set; } + public bool Running { get; private set; } = true; private readonly ulong[] _x = new ulong[32]; + public ulong DebugPc { get; set; } + public ulong GetX(int index) => _x[index]; public void SetX(int index, ulong value) => _x[index] = value; @@ -31,6 +36,10 @@ namespace Ryujinx.HLE.HOS.Kernel.Process { } + public void RequestDebugStep() + { + } + public void StopRunning() { Running = false; diff --git a/src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs b/src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs index 7471702c3..54b20ff99 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs @@ -301,6 +301,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading currentThread.SchedulerWaitEvent.Reset(); currentThread.ThreadContext.Unlock(); + currentThread.DebugHalt.Set(); // Wake all the threads that might be waiting until this thread context is unlocked. for (int core = 0; core < CpuCoresCount; core++) diff --git a/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs b/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs index b5a14ad5b..20fb426ba 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs @@ -1,12 +1,15 @@ using Ryujinx.Common.Logging; using Ryujinx.Cpu; +using Ryujinx.HLE.Debugger; using Ryujinx.HLE.HOS.Kernel.Common; using Ryujinx.HLE.HOS.Kernel.Process; using Ryujinx.HLE.HOS.Kernel.SupervisorCall; using Ryujinx.Horizon.Common; +using Ryujinx.Memory; using System; using System.Collections.Generic; using System.Numerics; +using System.Text; using System.Threading; namespace Ryujinx.HLE.HOS.Kernel.Threading @@ -16,6 +19,23 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading private const int TlsUserDisableCountOffset = 0x100; private const int TlsUserInterruptFlagOffset = 0x102; + // Tls -> ThreadType + private const int TlsThreadTypeOffsetAArch64 = 0x1F8; + private const int TlsThreadTypeOffsetAArch32 = 0x1FC; + + // Tls -> ThreadType -> Version + private const int TlsThreadTypeVersionOffsetAArch64 = 0x46; + private const int TlsThreadTypeVersionOffsetAArch32 = 0x26; + + // Tls -> ThreadType (Version 0) -> ThreadNamePointer + private const int TlsThreadTypeVersion0ThreadNamePointerOffsetAArch64 = 0x1A8; + private const int TlsThreadTypeVersion0ThreadNamePointerOffsetAArch32 = 0xE8; + + // Tls -> ThreadType (Version 1) -> ThreadNamePointer + private const int TlsThreadTypeThreadNamePointerOffsetAArch64 = 0x1A0; + private const int TlsThreadTypeThreadNamePointerOffsetAArch32 = 0xE4; + + public const int MaxWaitSyncObjects = 64; private ManualResetEvent _schedulerWaitEvent; @@ -114,6 +134,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading private readonly Lock _activityOperationLock = new(); + internal readonly ManualResetEvent DebugHalt = new(false); + public KThread(KernelContext context) : base(context) { WaitSyncObjects = new KSynchronizationObject[MaxWaitSyncObjects]; @@ -202,8 +224,10 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading } Context.TpidrroEl0 = (long)_tlsAddress; + Context.DebugPc = _entrypoint; ThreadUid = KernelContext.NewThreadUid(); + Context.ThreadUid = ThreadUid; HostThread.Name = customThreadStart != null ? $"HLE.OsThread.{ThreadUid}" : $"HLE.GuestThread.{ThreadUid}"; @@ -307,7 +331,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading { KernelContext.CriticalSection.Enter(); - if (Owner != null && Owner.PinnedThreads[KernelStatic.GetCurrentThread().CurrentCore] == this) + KThread currentThread = KernelStatic.GetCurrentThread(); + + if (Owner != null && currentThread != null && Owner.PinnedThreads[currentThread.CurrentCore] == this) { Owner.UnpinThread(this); } @@ -362,7 +388,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading { ThreadSchedState state = PrepareForTermination(); - if (state != ThreadSchedState.TerminationPending) + if (KernelStatic.GetCurrentThread() == this && state != ThreadSchedState.TerminationPending) { KernelContext.Synchronization.WaitFor(new KSynchronizationObject[] { this }, -1, out _); } @@ -1248,6 +1274,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading private void ThreadStart() { _schedulerWaitEvent.WaitOne(); + DebugHalt.Reset(); KernelStatic.SetKernelContext(KernelContext, this); if (_customThreadStart != null) @@ -1431,5 +1458,84 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading { Owner.CpuMemory.Write(_tlsAddress + TlsUserInterruptFlagOffset, 0); } + + public string GetThreadName() + { + try + { + ulong threadNamePtr = 0; + if (Context.IsAarch32) + { + uint threadTypePtr32 = Owner.CpuMemory.Read(_tlsAddress + TlsThreadTypeOffsetAArch32); + if (threadTypePtr32 == 0) + { + return ""; + } + + ushort version = Owner.CpuMemory.Read(threadTypePtr32 + TlsThreadTypeVersionOffsetAArch32); + switch (version) + { + case 0x0000: + case 0xFFFF: + threadNamePtr = Owner.CpuMemory.Read(threadTypePtr32 + TlsThreadTypeVersion0ThreadNamePointerOffsetAArch32); + break; + case 0x0001: + threadNamePtr = Owner.CpuMemory.Read(threadTypePtr32 + TlsThreadTypeThreadNamePointerOffsetAArch32); + break; + default: + Logger.Warning?.Print(LogClass.Kernel, $"Unknown ThreadType struct version: {version}"); + break; + } + } + else + { + ulong threadTypePtr64 = Owner.CpuMemory.Read(_tlsAddress + TlsThreadTypeOffsetAArch64); + if (threadTypePtr64 == 0) + { + return ""; + } + + ushort version = Owner.CpuMemory.Read(threadTypePtr64 + TlsThreadTypeVersionOffsetAArch64); + switch (version) + { + case 0x0000: + case 0xFFFF: + threadNamePtr = Owner.CpuMemory.Read(threadTypePtr64 + TlsThreadTypeVersion0ThreadNamePointerOffsetAArch64); + break; + case 0x0001: + threadNamePtr = Owner.CpuMemory.Read(threadTypePtr64 + TlsThreadTypeThreadNamePointerOffsetAArch64); + break; + default: + Logger.Warning?.Print(LogClass.Kernel, $"Unknown ThreadType struct version: {version}"); + break; + } + } + + if (threadNamePtr == 0) + { + return ""; + } + + List nameBytes = new(); + for (int i = 0; i < 0x20; i++) + { + byte b = Owner.CpuMemory.Read(threadNamePtr + (ulong)i); + if (b == 0) + { + break; + } + nameBytes.Add(b); + } + return Encoding.UTF8.GetString(nameBytes.ToArray()); + + } catch (InvalidMemoryRegionException) + { + Logger.Warning?.Print(LogClass.Kernel, "Failed to get thread name."); + return ""; + } catch (Exception e) { + Logger.Error?.Print(LogClass.Kernel, $"Error getting thread name: {e.Message}"); + return ""; + } + } } } diff --git a/src/Ryujinx.HLE/HleConfiguration.cs b/src/Ryujinx.HLE/HleConfiguration.cs index 10c2a1f30..e2f95ede7 100644 --- a/src/Ryujinx.HLE/HleConfiguration.cs +++ b/src/Ryujinx.HLE/HleConfiguration.cs @@ -194,6 +194,21 @@ namespace Ryujinx.HLE /// public Action RefreshInputConfig { internal get; set; } + /// + /// Enables gdbstub to allow for debugging of the guest . + /// + public bool EnableGdbStub { internal get; set; } + + /// + /// A TCP port to use to expose a gdbstub for a debugger to connect to. + /// + public ushort GdbStubPort { internal get; set; } + + /// + /// Suspend execution when starting an application + /// + public bool DebuggerSuspendOnStart { internal get; set; } + /// /// The desired hacky workarounds. /// @@ -222,6 +237,9 @@ namespace Ryujinx.HLE bool multiplayerDisableP2p, string multiplayerLdnPassphrase, string multiplayerLdnServer, + bool enableGdbStub, + ushort gdbStubPort, + bool debuggerSuspendOnStart, int customVSyncInterval, EnabledDirtyHack[] dirtyHacks = null) { @@ -248,6 +266,9 @@ namespace Ryujinx.HLE MultiplayerDisableP2p = multiplayerDisableP2p; MultiplayerLdnPassphrase = multiplayerLdnPassphrase; MultiplayerLdnServer = multiplayerLdnServer; + EnableGdbStub = enableGdbStub; + GdbStubPort = gdbStubPort; + DebuggerSuspendOnStart = debuggerSuspendOnStart; Hacks = dirtyHacks ?? []; } diff --git a/src/Ryujinx.HLE/Ryujinx.HLE.csproj b/src/Ryujinx.HLE/Ryujinx.HLE.csproj index 5139d9276..1938796e8 100644 --- a/src/Ryujinx.HLE/Ryujinx.HLE.csproj +++ b/src/Ryujinx.HLE/Ryujinx.HLE.csproj @@ -33,6 +33,12 @@ + + + + + + @@ -42,6 +48,12 @@ + + + + + + diff --git a/src/Ryujinx.HLE/Switch.cs b/src/Ryujinx.HLE/Switch.cs index bdcbe82c7..e1aa8e0e4 100644 --- a/src/Ryujinx.HLE/Switch.cs +++ b/src/Ryujinx.HLE/Switch.cs @@ -14,6 +14,7 @@ using Ryujinx.HLE.Loaders.Processes; using Ryujinx.HLE.UI; using Ryujinx.Memory; using System; +using System.Threading; namespace Ryujinx.HLE { @@ -41,6 +42,7 @@ namespace Ryujinx.HLE public Hid Hid { get; } public TamperMachine TamperMachine { get; } public IHostUIHandler UIHandler { get; } + public Debugger.Debugger Debugger { get; } public int CpuCoresCount = 4; // Switch has a quad-core Tegra X1 SoC @@ -72,6 +74,7 @@ namespace Ryujinx.HLE AudioDeviceDriver = new CompatLayerHardwareDeviceDriver(Configuration.AudioDeviceDriver); Memory = new MemoryBlock(Configuration.MemoryConfiguration.ToDramSize(), memoryAllocationFlags); Gpu = new GpuContext(Configuration.GpuRenderer, DirtyHacks); + Debugger = Configuration.EnableGdbStub ? new Debugger.Debugger(this, Configuration.GdbStubPort) : null; System = new HOS.Horizon(this); Statistics = new PerformanceStatistics(this); Hid = new Hid(this, System.HidStorage); @@ -173,6 +176,7 @@ namespace Ryujinx.HLE AudioDeviceDriver.Dispose(); FileSystem.Dispose(); Memory.Dispose(); + Debugger?.Dispose(); TitleIDs.CurrentApplication.Value = null; Shared = null; diff --git a/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs b/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs index f15d24e8a..dc7b8625e 100644 --- a/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs +++ b/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs @@ -338,6 +338,9 @@ namespace Ryujinx.Headless false, string.Empty, string.Empty, + options.EnableGdbStub, + options.GdbStubPort, + options.DebuggerSuspendOnStart, options.CustomVSyncInterval ) .Configure( diff --git a/src/Ryujinx/Headless/Options.cs b/src/Ryujinx/Headless/Options.cs index 49050005c..876d0c936 100644 --- a/src/Ryujinx/Headless/Options.cs +++ b/src/Ryujinx/Headless/Options.cs @@ -423,6 +423,17 @@ namespace Ryujinx.Headless [Option("skip-user-profiles-manager", Required = false, Default = false, HelpText = "Enable skips the Profiles Manager popup during gameplay. Select the desired profile before starting the game")] public bool SkipUserProfilesManager { get; set; } + // Debug + + [Option("enable-gdb-stub", Required = false, Default = false, HelpText = "Enables the GDB stub so that a developer can attach a debugger to the emulated process.")] + public bool EnableGdbStub { get; set; } + + [Option("gdb-stub-port", Required = false, Default = 55555, HelpText = "Specifies which TCP port the GDB stub listens on.")] + public ushort GdbStubPort { get; set; } + + [Option("suspend-on-start", Required = false, Default = false, HelpText = "Suspend execution when starting an application.")] + public bool DebuggerSuspendOnStart { get; set; } + // Values [Value(0, MetaName = "input", HelpText = "Input to load.", Required = true)] diff --git a/src/Ryujinx/Systems/AppHost.cs b/src/Ryujinx/Systems/AppHost.cs index 1c5f64309..f11280d62 100644 --- a/src/Ryujinx/Systems/AppHost.cs +++ b/src/Ryujinx/Systems/AppHost.cs @@ -218,6 +218,10 @@ namespace Ryujinx.Ava.Systems ConfigurationState.Instance.Multiplayer.LdnServer.Event += UpdateLdnServerState; ConfigurationState.Instance.Multiplayer.DisableP2p.Event += UpdateDisableP2pState; + ConfigurationState.Instance.Debug.EnableGdbStub.Event += UpdateEnableGdbStubState; + ConfigurationState.Instance.Debug.GdbStubPort.Event += UpdateGdbStubPortState; + ConfigurationState.Instance.Debug.DebuggerSuspendOnStart.Event += UpdateDebuggerSuspendOnStartState; + _gpuCancellationTokenSource = new CancellationTokenSource(); _gpuDoneEvent = new ManualResetEvent(false); } @@ -564,6 +568,21 @@ namespace Ryujinx.Ava.Systems Device.Configuration.MultiplayerDisableP2p = e.NewValue; } + private void UpdateEnableGdbStubState(object sender, ReactiveEventArgs e) + { + Device.Configuration.EnableGdbStub = e.NewValue; + } + + private void UpdateGdbStubPortState(object sender, ReactiveEventArgs e) + { + Device.Configuration.GdbStubPort = e.NewValue; + } + + private void UpdateDebuggerSuspendOnStartState(object sender, ReactiveEventArgs e) + { + Device.Configuration.DebuggerSuspendOnStart = e.NewValue; + } + public void Stop() { _isActive = false; diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs index c21383349..26ea73f73 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs @@ -464,6 +464,21 @@ namespace Ryujinx.Ava.Systems.Configuration /// public bool UseHypervisor { get; set; } + /// + /// Enables or disables the GDB stub + /// + public bool EnableGdbStub { get; set; } + + /// + /// Which TCP port should the GDB stub listen on + /// + public ushort GdbStubPort { get; set; } + + /// + /// Suspend execution when starting an application + /// + public bool DebuggerSuspendOnStart { get; set; } + /// /// Show toggles for dirty hacks in the UI. /// diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs index afabdb4e3..58f25b783 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs @@ -156,6 +156,10 @@ namespace Ryujinx.Ava.Systems.Configuration Multiplayer.LdnPassphrase.Value = cff.MultiplayerLdnPassphrase; Multiplayer.LdnServer.Value = cff.LdnServer; + Debug.EnableGdbStub.Value = cff.EnableGdbStub; + Debug.GdbStubPort.Value = cff.GdbStubPort; + Debug.DebuggerSuspendOnStart.Value = cff.DebuggerSuspendOnStart; + { Hacks.ShowDirtyHacks.Value = shouldLoadFromFile ? cff.ShowDirtyHacks : Hacks.ShowDirtyHacks.Value; // Get from global config only diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs index 29a390b26..bc8fdb40a 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs @@ -703,6 +703,37 @@ namespace Ryujinx.Ava.Systems.Configuration } } + /// + /// Debug configuration section + /// + public class DebugSection + { + /// + /// Enables or disables the GDB stub + /// + public ReactiveObject EnableGdbStub { get; private set; } + + /// + /// Which TCP port should the GDB stub listen on + /// + public ReactiveObject GdbStubPort { get; private set; } + + /// + /// Suspend execution when starting an application + /// + public ReactiveObject DebuggerSuspendOnStart { get; private set; } + + public DebugSection() + { + EnableGdbStub = new ReactiveObject(); + EnableGdbStub.LogChangesToValue(nameof(EnableGdbStub)); + GdbStubPort = new ReactiveObject(); + GdbStubPort.LogChangesToValue(nameof(GdbStubPort)); + DebuggerSuspendOnStart = new ReactiveObject(); + DebuggerSuspendOnStart.LogChangesToValue(nameof(DebuggerSuspendOnStart)); + } + } + public class HacksSection { /// @@ -801,6 +832,11 @@ namespace Ryujinx.Ava.Systems.Configuration /// public MultiplayerSection Multiplayer { get; private set; } + /// + /// The Debug + /// + public DebugSection Debug { get; private set; } + /// /// The Dirty Hacks section /// @@ -854,6 +890,7 @@ namespace Ryujinx.Ava.Systems.Configuration Graphics = new GraphicsSection(); Hid = new HidSection(); Multiplayer = new MultiplayerSection(); + Debug = new DebugSection(); Hacks = new HacksSection(); UpdateCheckerType = new ReactiveObject(); FocusLostActionType = new ReactiveObject(); @@ -893,6 +930,9 @@ namespace Ryujinx.Ava.Systems.Configuration Multiplayer.DisableP2p, Multiplayer.LdnPassphrase, Multiplayer.GetLdnServer(), + Debug.EnableGdbStub, + Debug.GdbStubPort, + Debug.DebuggerSuspendOnStart, Graphics.CustomVSyncInterval, Hacks.ShowDirtyHacks ? Hacks.EnabledHacks : null); } diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.cs index 4a565d5d3..185aedf64 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationState.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.cs @@ -147,6 +147,9 @@ namespace Ryujinx.Ava.Systems.Configuration MultiplayerDisableP2p = Multiplayer.DisableP2p, MultiplayerLdnPassphrase = Multiplayer.LdnPassphrase, LdnServer = Multiplayer.LdnServer, + EnableGdbStub = Debug.EnableGdbStub, + GdbStubPort = Debug.GdbStubPort, + DebuggerSuspendOnStart = Debug.DebuggerSuspendOnStart, ShowDirtyHacks = Hacks.ShowDirtyHacks, DirtyHacks = Hacks.EnabledHacks.Select(it => it.Pack()).ToArray(), }; @@ -324,6 +327,9 @@ namespace Ryujinx.Ava.Systems.Configuration }, } ]; + Debug.EnableGdbStub.Value = false; + Debug.GdbStubPort.Value = 55555; + Debug.DebuggerSuspendOnStart.Value = false; } private static GraphicsBackend DefaultGraphicsBackend() diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs index 654eb0c43..54fd951fb 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -71,6 +71,10 @@ namespace Ryujinx.Ava.UI.ViewModels private string _ldnPassphrase; [ObservableProperty] private string _ldnServer; + private bool _enableGDBStub; + private ushort _gdbStubPort; + private bool _debuggerSuspendOnStart; + public SettingsHacksViewModel DirtyHacks { get; } private readonly bool _isGameRunning; @@ -387,6 +391,36 @@ namespace Ryujinx.Ava.UI.ViewModels public bool IsInvalidLdnPassphraseVisible { get; set; } + public bool EnableGdbStub + { + get => _enableGDBStub; + set + { + _enableGDBStub = value; + ConfigurationState.Instance.Debug.EnableGdbStub.Value = _enableGDBStub; + } + } + + public ushort GDBStubPort + { + get => _gdbStubPort; + set + { + _gdbStubPort = value; + ConfigurationState.Instance.Debug.GdbStubPort.Value = _gdbStubPort; + } + } + + public bool DebuggerSuspendOnStart + { + get => _debuggerSuspendOnStart; + set + { + _debuggerSuspendOnStart = value; + ConfigurationState.Instance.Debug.DebuggerSuspendOnStart.Value = _debuggerSuspendOnStart; + } + } + public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this() { _virtualFileSystem = virtualFileSystem; @@ -680,10 +714,16 @@ namespace Ryujinx.Ava.UI.ViewModels FsGlobalAccessLogMode = config.System.FsGlobalAccessLogMode; OpenglDebugLevel = (int)config.Logger.GraphicsDebugLevel.Value; + // Multiplayer MultiplayerModeIndex = (int)config.Multiplayer.Mode.Value; DisableP2P = config.Multiplayer.DisableP2p; LdnPassphrase = config.Multiplayer.LdnPassphrase; LdnServer = config.Multiplayer.LdnServer; + + // Debug + EnableGdbStub = config.Debug.EnableGdbStub.Value; + GDBStubPort = config.Debug.GdbStubPort.Value; + DebuggerSuspendOnStart = config.Debug.DebuggerSuspendOnStart.Value; } public void SaveSettings(bool global = false) @@ -800,12 +840,18 @@ namespace Ryujinx.Ava.UI.ViewModels config.System.FsGlobalAccessLogMode.Value = FsGlobalAccessLogMode; config.Logger.GraphicsDebugLevel.Value = (GraphicsDebugLevel)OpenglDebugLevel; + // Multiplayer config.Multiplayer.LanInterfaceId.Value = _networkInterfaces[NetworkInterfaceList[NetworkInterfaceIndex]]; config.Multiplayer.Mode.Value = (MultiplayerMode)MultiplayerModeIndex; config.Multiplayer.DisableP2p.Value = DisableP2P; config.Multiplayer.LdnPassphrase.Value = LdnPassphrase; config.Multiplayer.LdnServer.Value = LdnServer; + // Debug + config.Debug.EnableGdbStub.Value = EnableGdbStub; + config.Debug.GdbStubPort.Value = GDBStubPort; + config.Debug.DebuggerSuspendOnStart.Value = DebuggerSuspendOnStart; + // Dirty Hacks config.Hacks.Xc2MenuSoftlockFix.Value = DirtyHacks.Xc2MenuSoftlockFix; config.Hacks.DisableNifmIsAnyInternetRequestAccepted.Value = diff --git a/src/Ryujinx/UI/Views/Settings/SettingsDebugView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsDebugView.axaml new file mode 100644 index 000000000..f491dda24 --- /dev/null +++ b/src/Ryujinx/UI/Views/Settings/SettingsDebugView.axaml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Views/Settings/SettingsDebugView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsDebugView.axaml.cs new file mode 100644 index 000000000..14a65b8b2 --- /dev/null +++ b/src/Ryujinx/UI/Views/Settings/SettingsDebugView.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia.Controls; + +namespace Ryujinx.Ava.UI.Views.Settings +{ + public partial class SettingsDebugView : UserControl + { + public SettingsDebugView() + { + InitializeComponent(); + } + } +} + diff --git a/src/Ryujinx/UI/Windows/SettingsWindow.axaml b/src/Ryujinx/UI/Windows/SettingsWindow.axaml index 15d174123..9bfe0a9db 100644 --- a/src/Ryujinx/UI/Windows/SettingsWindow.axaml +++ b/src/Ryujinx/UI/Windows/SettingsWindow.axaml @@ -46,6 +46,7 @@ + +