diff --git a/src/ARMeilleure/Instructions/NativeInterface.cs b/src/ARMeilleure/Instructions/NativeInterface.cs index 0cd3754f7..d8b8a02bf 100644 --- a/src/ARMeilleure/Instructions/NativeInterface.cs +++ b/src/ARMeilleure/Instructions/NativeInterface.cs @@ -2,6 +2,8 @@ using ARMeilleure.Memory; using ARMeilleure.State; using ARMeilleure.Translation; using System; +using System.Threading; +using ExecutionContext = ARMeilleure.State.ExecutionContext; namespace ARMeilleure.Instructions { @@ -175,7 +177,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 8fe478e47..aaae7a39a 100644 --- a/src/ARMeilleure/Optimizations.cs +++ b/src/ARMeilleure/Optimizations.cs @@ -9,6 +9,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 ce10a591c..1d78b40db 100644 --- a/src/ARMeilleure/State/ExecutionContext.cs +++ b/src/ARMeilleure/State/ExecutionContext.cs @@ -1,5 +1,8 @@ using ARMeilleure.Memory; using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Threading; namespace ARMeilleure.State { @@ -11,7 +14,7 @@ namespace ARMeilleure.State internal IntPtr NativeContextPtr => _nativeContext.BasePtr; - private bool _interrupted; + internal bool Interrupted { get; private set; } private readonly ICounter _counter; @@ -68,6 +71,8 @@ namespace ARMeilleure.State public bool IsAarch32 { get; set; } + public ulong ThreadUid { get; set; } + internal ExecutionMode ExecutionMode { get @@ -93,14 +98,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) { @@ -108,6 +118,7 @@ namespace ARMeilleure.State _counter = counter; _interruptCallback = interruptCallback; _breakCallback = breakCallback; + _stepCallback = stepCallback; _supervisorCallback = supervisorCallback; _undefinedCallback = undefinedCallback; @@ -130,9 +141,9 @@ namespace ARMeilleure.State internal void CheckInterrupt() { - if (_interrupted) + if (Interrupted) { - _interrupted = false; + Interrupted = false; _interruptCallback?.Invoke(this); } @@ -142,7 +153,18 @@ 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) diff --git a/src/ARMeilleure/Translation/Translator.cs b/src/ARMeilleure/Translation/Translator.cs index 014b12035..df9fa8f01 100644 --- a/src/ARMeilleure/Translation/Translator.cs +++ b/src/ARMeilleure/Translation/Translator.cs @@ -140,7 +140,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); } @@ -196,7 +214,7 @@ namespace ARMeilleure.Translation return nextAddr; } - public ulong Step(State.ExecutionContext context, ulong address) + private ulong Step(State.ExecutionContext context, ulong address) { TranslatedFunction func = Translate(address, context.ExecutionMode, highCq: false, singleStep: true); @@ -249,7 +267,7 @@ namespace ARMeilleure.Translation Stubs, address, highCq, - _ptc.State != PtcState.Disabled, + _ptc.State != PtcState.Disabled && !Optimizations.EnableDebugging, mode: Aarch32Mode.User); Logger.StartPass(PassName.Decoding); @@ -382,9 +400,8 @@ 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; InstEmitFlowHelper.EmitVirtualJump(context, Const(block.Address), isReturn: useReturns); } diff --git a/src/Ryujinx.Common/Logging/LogClass.cs b/src/Ryujinx.Common/Logging/LogClass.cs index 1b404a06a..0662bf0e4 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 fc2b76d15..cc975fb53 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; @@ -155,7 +215,6 @@ namespace Ryujinx.Cpu.AppleHv { throw new Exception($"Unhandled exception from guest kernel with ESR 0x{hvEsr:X} ({hvEc})."); } - address = SynchronousException(memoryManager, ref vcpu); HvApi.hv_vcpu_set_reg(vcpu.Handle, HvReg.PC, address).ThrowOnError(); } @@ -209,6 +268,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 +292,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 bb232940d..4e4b6ef59 100644 --- a/src/Ryujinx.Cpu/AppleHv/HvExecutionContextVcpu.cs +++ b/src/Ryujinx.Cpu/AppleHv/HvExecutionContextVcpu.cs @@ -2,11 +2,10 @@ using ARMeilleure.State; using Ryujinx.Memory; using System; using System.Runtime.InteropServices; -using System.Runtime.Versioning; +using System.Threading; namespace Ryujinx.Cpu.AppleHv { - [SupportedOSPlatform("macos")] class HvExecutionContextVcpu : IHvExecutionContext { private static readonly MemoryBlock _setSimdFpRegFuncMem; @@ -14,6 +13,8 @@ namespace Ryujinx.Cpu.AppleHv private static readonly SetSimdFpReg _setSimdFpReg; private static readonly IntPtr _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 @@ -136,6 +137,7 @@ namespace Ryujinx.Cpu.AppleHv } private readonly ulong _vcpu; + private int _interruptRequested; public HvExecutionContextVcpu(ulong vcpu) { @@ -181,8 +183,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..f30030406 100644 --- a/src/Ryujinx.Cpu/AppleHv/IHvExecutionContext.cs +++ b/src/Ryujinx.Cpu/AppleHv/IHvExecutionContext.cs @@ -15,7 +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 +39,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..d4775f3ed 100644 --- a/src/Ryujinx.Cpu/Jit/JitExecutionContext.cs +++ b/src/Ryujinx.Cpu/Jit/JitExecutionContext.cs @@ -1,5 +1,7 @@ using ARMeilleure.Memory; using ARMeilleure.State; +using System.Threading; +using ExecutionContext = ARMeilleure.State.ExecutionContext; namespace Ryujinx.Cpu.Jit { @@ -53,6 +55,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 +74,7 @@ namespace Ryujinx.Cpu.Jit counter, InterruptHandler, BreakHandler, + StepHandler, SupervisorCallHandler, UndefinedHandler); @@ -93,6 +103,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 +124,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.Gtk3/UI/MainWindow.cs b/src/Ryujinx.Gtk3/UI/MainWindow.cs index b10dfe3f9..bd070f351 100644 --- a/src/Ryujinx.Gtk3/UI/MainWindow.cs +++ b/src/Ryujinx.Gtk3/UI/MainWindow.cs @@ -677,7 +677,10 @@ namespace Ryujinx.UI ConfigurationState.Instance.System.AudioVolume, ConfigurationState.Instance.System.UseHypervisor, ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value, - ConfigurationState.Instance.Multiplayer.Mode); + ConfigurationState.Instance.Multiplayer.Mode, + ConfigurationState.Instance.Debug.EnableGdbStub, + ConfigurationState.Instance.Debug.GdbStubPort, + ConfigurationState.Instance.Debug.DebuggerSuspendOnStart); _emulationContext = new HLE.Switch(configuration); } @@ -790,6 +793,24 @@ namespace Ryujinx.UI shadersDumpWarningDialog.Dispose(); } + + if (ConfigurationState.Instance.Debug.EnableGdbStub.Value) + { + MessageDialog gdbStubWarningDialog = new MessageDialog(this, DialogFlags.Modal, MessageType.Warning, ButtonsType.YesNo, null) + { + Title = "Ryujinx - Warning", + Text = "You have the GDB stub enabled, which is designed to be used by developers only.", + SecondaryText = "For optimal performance, it's recommended to disable the GDB stub. Would you like to disable the GDB stub now?" + }; + + if (gdbStubWarningDialog.Run() == (int)ResponseType.Yes) + { + ConfigurationState.Instance.Debug.EnableGdbStub.Value = false; + SaveConfig(); + } + + gdbStubWarningDialog.Dispose(); + } } private bool LoadApplication(string path, ulong applicationId, bool isFirmwareTitle) @@ -1056,9 +1077,11 @@ namespace Ryujinx.UI RendererWidget.WaitEvent.WaitOne(); RendererWidget.Start(); + _pauseEmulation.Sensitive = false; + _resumeEmulation.Sensitive = false; + UpdateMenuItem.Sensitive = true; _emulationContext.Dispose(); - _deviceExitStatus.Set(); // NOTE: Everything that is here will not be executed when you close the UI. Application.Invoke(delegate @@ -1153,7 +1176,7 @@ namespace Ryujinx.UI RendererWidget.Exit(); // Wait for the other thread to dispose the HLE context before exiting. - _deviceExitStatus.WaitOne(); + _emulationContext.ExitStatus.WaitOne(); RendererWidget.Dispose(); } } @@ -1491,9 +1514,6 @@ namespace Ryujinx.UI UpdateGameMetadata(_emulationContext.Processes.ActiveApplication.ProgramIdText); } - _pauseEmulation.Sensitive = false; - _resumeEmulation.Sensitive = false; - UpdateMenuItem.Sensitive = true; RendererWidget?.Exit(); } diff --git a/src/Ryujinx.Gtk3/UI/RendererWidgetBase.cs b/src/Ryujinx.Gtk3/UI/RendererWidgetBase.cs index 12139e87d..0f2fd244d 100644 --- a/src/Ryujinx.Gtk3/UI/RendererWidgetBase.cs +++ b/src/Ryujinx.Gtk3/UI/RendererWidgetBase.cs @@ -46,7 +46,6 @@ namespace Ryujinx.UI public static event EventHandler StatusUpdatedEvent; - private bool _isActive; private bool _isStopped; private bool _toggleFullscreen; @@ -464,7 +463,7 @@ namespace Ryujinx.UI (Toplevel as MainWindow)?.ActivatePauseMenu(); - while (_isActive) + while (Device.IsActive) { if (_isStopped) { @@ -524,7 +523,7 @@ namespace Ryujinx.UI { _chrono.Restart(); - _isActive = true; + Device.IsActive = true; Gtk.Window parent = Toplevel as Gtk.Window; @@ -578,9 +577,9 @@ namespace Ryujinx.UI _isStopped = true; - if (_isActive) + if (Device.IsActive) { - _isActive = false; + Device.IsActive = false; _exitEvent.WaitOne(); _exitEvent.Dispose(); @@ -589,7 +588,7 @@ namespace Ryujinx.UI private void NvidiaStutterWorkaround() { - while (_isActive) + while (Device.IsActive) { // When NVIDIA Threaded Optimization is on, the driver will snapshot all threads in the system whenever the application creates any new ones. // The ThreadPool has something called a "GateThread" which terminates itself after some inactivity. @@ -608,7 +607,7 @@ namespace Ryujinx.UI public void MainLoop() { - while (_isActive) + while (Device.IsActive) { UpdateFrame(); @@ -621,7 +620,7 @@ namespace Ryujinx.UI private bool UpdateFrame() { - if (!_isActive) + if (!Device.IsActive) { return true; } diff --git a/src/Ryujinx.Gtk3/UI/Windows/SettingsWindow.cs b/src/Ryujinx.Gtk3/UI/Windows/SettingsWindow.cs index dc467c0f2..8cfc763ce 100644 --- a/src/Ryujinx.Gtk3/UI/Windows/SettingsWindow.cs +++ b/src/Ryujinx.Gtk3/UI/Windows/SettingsWindow.cs @@ -117,7 +117,9 @@ namespace Ryujinx.UI.Windows [GUI] ToggleButton _configureController7; [GUI] ToggleButton _configureController8; [GUI] ToggleButton _configureControllerH; - + [GUI] ToggleButton _gdbStubToggle; + [GUI] ToggleButton _suspendOnStartToggle; + [GUI] Adjustment _gdbStubPortSpinAdjustment; #pragma warning restore CS0649, IDE0044 public SettingsWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this(parent, new Builder("Ryujinx.Gtk3.UI.Windows.SettingsWindow.glade"), virtualFileSystem, contentManager) { } @@ -316,6 +318,16 @@ namespace Ryujinx.UI.Windows _custThemeToggle.Click(); } + if (ConfigurationState.Instance.Debug.EnableGdbStub) + { + _gdbStubToggle.Click(); + } + + if (ConfigurationState.Instance.Debug.DebuggerSuspendOnStart) + { + _suspendOnStartToggle.Click(); + } + // Custom EntryCompletion Columns. If added to glade, need to override more signals ListStore tzList = new(typeof(string), typeof(string), typeof(string)); _systemTimeZoneCompletion.Model = tzList; @@ -375,6 +387,8 @@ namespace Ryujinx.UI.Windows _fsLogSpinAdjustment.Value = ConfigurationState.Instance.System.FsGlobalAccessLogMode; _systemTimeOffset = ConfigurationState.Instance.System.SystemTimeOffset; + _gdbStubPortSpinAdjustment.Value = ConfigurationState.Instance.Debug.GdbStubPort; + _gameDirsBox.AppendColumn("", new CellRendererText(), "text", 0); _gameDirsBoxStore = new ListStore(typeof(string)); _gameDirsBox.Model = _gameDirsBoxStore; @@ -659,6 +673,9 @@ namespace Ryujinx.UI.Windows ConfigurationState.Instance.Graphics.ScalingFilter.Value = Enum.Parse(_scalingFilter.ActiveId); ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value = (int)_scalingFilterLevel.Value; ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value = _multiLanSelect.ActiveId; + ConfigurationState.Instance.Debug.EnableGdbStub.Value = _gdbStubToggle.Active; + ConfigurationState.Instance.Debug.GdbStubPort.Value = (ushort)_gdbStubPortSpinAdjustment.Value; + ConfigurationState.Instance.Debug.DebuggerSuspendOnStart.Value = _suspendOnStartToggle.Active; _previousVolumeLevel = ConfigurationState.Instance.System.AudioVolume.Value; diff --git a/src/Ryujinx.Gtk3/UI/Windows/SettingsWindow.glade b/src/Ryujinx.Gtk3/UI/Windows/SettingsWindow.glade index f0dbd6b63..58817f065 100644 --- a/src/Ryujinx.Gtk3/UI/Windows/SettingsWindow.glade +++ b/src/Ryujinx.Gtk3/UI/Windows/SettingsWindow.glade @@ -46,6 +46,12 @@ True True + + 1 + 65535 + 1 + 5 + False Ryujinx - Settings @@ -3146,6 +3152,155 @@ False + + + True + False + 5 + 10 + 5 + vertical + + + True + False + 5 + 5 + vertical + + + True + False + start + 5 + Debug (WARNING: For Developer Use Only) + + + + + + False + True + 0 + + + + + True + False + start + 10 + 10 + vertical + + + Enable GDB Stub + True + True + False + Enables or disables GDB stub (for developer use only) + start + 5 + 5 + True + + + False + True + 0 + + + + + True + False + + + True + False + Specifies which TCP port for the GDB stub to listen on. Possible values are 1-65535. + GDB Stub Port + + + False + True + 5 + 0 + + + + + True + True + Specifies which TCP port for the GDB stub to listen on. Possible values are 1-65535. + 55555 + number + _gdbStubPortSpinAdjustment + True + + + True + True + 1 + + + + + False + True + 5 + 9 + + + + + Suspend application on start + True + True + False + Suspends the application before executing the first instruction, allowing for debugging from the earliest point. + start + 5 + 5 + True + + + False + True + 0 + + + + + True + True + 1 + + + + + False + True + 5 + 0 + + + + + 6 + + + + + True + False + Debug + + + 6 + 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..002fbcb85 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/Debugger.cs @@ -0,0 +1,900 @@ +using ARMeilleure.State; +using Ryujinx.Common; +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Kernel; +using Ryujinx.HLE.HOS.Kernel.Threading; +using Ryujinx.Memory; +using System; +using System.Collections.Concurrent; +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 ulong? cThread; + private ulong? gThread; + + 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(); + } + + private IDebuggableProcess DebugProcess => Device.System.DebugGetApplicationProcess(); + private KThread[] GetThreads() => DebugProcess.GetThreadUids().Select(x => DebugProcess.GetThread(x)).ToArray(); + private 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.ReadLengthAsHex(16); + state.SetX(gdbRegId, value); + return true; + } + case 32: + { + ulong value = ss.ReadLengthAsHex(16); + state.DebugPc = value; + return true; + } + case 33: + { + ulong value = ss.ReadLengthAsHex(8); + state.Pstate = (uint)value; + return true; + } + case >= 34 and <= 65: + { + ulong value0 = ss.ReadLengthAsHex(16); + ulong value1 = ss.ReadLengthAsHex(16); + state.SetV(gdbRegId - 34, new V128(value0, value1)); + return true; + } + case 66: + { + ulong value = ss.ReadLengthAsHex(8); + state.Fpsr = (uint)value; + return true; + } + case 67: + { + ulong value = ss.ReadLengthAsHex(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.ReadLengthAsHex(8); + state.SetX(gdbRegId, value); + return true; + } + case 15: + { + ulong value = ss.ReadLengthAsHex(8); + state.DebugPc = value; + return true; + } + case 16: + { + ulong value = ss.ReadLengthAsHex(8); + state.Pstate = (uint)value; + return true; + } + case >= 17 and <= 32: + { + ulong value0 = ss.ReadLengthAsHex(16); + ulong value1 = ss.ReadLengthAsHex(16); + state.SetV(gdbRegId - 17, new V128(value0, value1)); + return true; + } + case >= 33 and <= 64: + { + ulong value = ss.ReadLengthAsHex(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.ReadLengthAsHex(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(); + switch (msg) + { + case BreakInMessage: + Logger.Notice.Print(LogClass.GdbStub, "Break-in requested"); + CommandQuery(); + 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(); + Reply($"T05thread:{ctx.ThreadUid:x};"); + break; + + case KillMessage: + return; + } + } + } + + 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"); + Reply(""); + Device.IsActive = false; + Device.ExitStatus.WaitOne(); + 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+"); + 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.GetDebugState() == DebugState.Stopped) + { + Reply(ToHex("Stopped")); + } + else + { + Reply(ToHex("Not stopped")); + } + break; + } + + if (ss.ConsumePrefix("Xfer:features:read:")) + { + string feature = ss.ReadUntil(':'); + ulong addr = 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 (addr >= (ulong)data.Length) + { + Reply("l"); + break; + } + + if (len >= (ulong)data.Length - addr) + { + Reply("l" + ToBinaryFormat(data.Substring((int)addr))); + break; + } + else + { + Reply("m" + ToBinaryFormat(data.Substring((int)addr, (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; + } + default: + unknownCommand: + Logger.Notice.Print(LogClass.GdbStub, $"Unknown command: {cmd}"); + Reply(""); + break; + } + } + + void CommandQuery() + { + // GDB is performing initial contact. Stop everything. + DebugProcess.DebugStop(); + gThread = cThread = DebugProcess.GetThreadUids().First(); + Reply($"T05thread:{cThread: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() + { + // TODO: Remove all breakpoints + 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) + { + 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 + { + Reply($"T05thread:{thread.ThreadUid:x};"); + } + } + + private void CommandIsAlive(ulong? threadId) + { + if (GetThreads().Any(x => x.ThreadUid == threadId)) + { + ReplyOK(); + } + else + { + Reply("E00"); + } + } + + 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; + } + 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; + } + } + + private byte CalculateChecksum(string cmd) + { + byte checksum = 0; + foreach (char x in cmd) + { + unchecked + { + checksum += (byte)x; + } + } + + return checksum; + } + + 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(); + } + } + + public void BreakHandler(IExecutionContext ctx, ulong address, int imm) + { + Logger.Notice.Print(LogClass.GdbStub, $"Break hit on thread {ctx.ThreadUid} at pc {address:x016}"); + + Messages.Add(new ThreadBreakMessage(ctx, address, imm)); + DebugProcess.DebugInterruptHandler(ctx); + } + + 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..273a1147f --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/IDebuggableProcess.cs @@ -0,0 +1,19 @@ +using Ryujinx.Cpu; +using Ryujinx.HLE.HOS.Kernel.Threading; +using Ryujinx.Memory; + +namespace Ryujinx.HLE.Debugger +{ + internal interface IDebuggableProcess + { + void DebugStop(); + void DebugContinue(); + bool DebugStep(KThread thread); + KThread GetThread(ulong threadUid); + DebugState GetDebugState(); + 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/HLEConfiguration.cs b/src/Ryujinx.HLE/HLEConfiguration.cs index 955fee4b5..1660ae16a 100644 --- a/src/Ryujinx.HLE/HLEConfiguration.cs +++ b/src/Ryujinx.HLE/HLEConfiguration.cs @@ -169,6 +169,21 @@ namespace Ryujinx.HLE /// public Action RefreshInputConfig { internal get; set; } + /// + /// Enables gdbstub to allow for debugging of the guest . + /// + public bool EnableGdbStub { get; internal set; } + + /// + /// A TCP port to use to expose a gdbstub for a debugger to connect to. + /// + public ushort GdbStubPort { get; internal set; } + + /// + /// Suspend execution when starting an application + /// + public bool DebuggerSuspendOnStart { get; internal set; } + public HLEConfiguration(VirtualFileSystem virtualFileSystem, LibHacHorizonManager libHacHorizonManager, ContentManager contentManager, @@ -194,7 +209,10 @@ namespace Ryujinx.HLE float audioVolume, bool useHypervisor, string multiplayerLanInterfaceId, - MultiplayerMode multiplayerMode) + MultiplayerMode multiplayerMode, + bool enableGdbStub, + ushort gdbStubPort, + bool debuggerSuspendOnStart) { VirtualFileSystem = virtualFileSystem; LibHacHorizonManager = libHacHorizonManager; @@ -222,6 +240,9 @@ namespace Ryujinx.HLE UseHypervisor = useHypervisor; MultiplayerLanInterfaceId = multiplayerLanInterfaceId; MultiplayerMode = multiplayerMode; + EnableGdbStub = enableGdbStub; + GdbStubPort = gdbStubPort; + DebuggerSuspendOnStart = debuggerSuspendOnStart; } } } diff --git a/src/Ryujinx.HLE/HOS/Horizon.cs b/src/Ryujinx.HLE/HOS/Horizon.cs index 64b08e309..4da62d0f0 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; @@ -473,5 +474,13 @@ namespace Ryujinx.HLE.HOS } IsPaused = pause; } + + internal IDebuggableProcess DebugGetApplicationProcess() + { + lock (KernelContext.Processes) + { + return KernelContext.Processes.Values.FirstOrDefault(x => x.IsApplication)?.DebugInterface; + } + } } } diff --git a/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs b/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs index 422f03c64..0d3a7541c 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 = new LinkedList(); Debugger = new HleProcessDebugger(this); + DebugInterface = new DebuggerInterface(this); } public Result InitializeKip( @@ -680,6 +686,12 @@ namespace Ryujinx.HLE.HOS.Kernel.Process SetState(newState); + if (KernelContext.Device.Configuration.DebuggerSuspendOnStart && IsApplication) + { + mainThread.Suspend(ThreadSchedState.ThreadPauseFlag); + debugState = (int)DebugState.Stopped; + } + result = mainThread.Start(); if (result != Result.Success) @@ -728,9 +740,19 @@ namespace Ryujinx.HLE.HOS.Kernel.Process public IExecutionContext CreateExecutionContext() { + ExceptionCallback breakCallback = null; + ExceptionCallbackNoArgs stepCallback = null; + + if (KernelContext.Device.Configuration.EnableGdbStub) + { + breakCallback = KernelContext.Device.Debugger.BreakHandler; + stepCallback = KernelContext.Device.Debugger.StepHandler; + } + return Context?.CreateExecutionContext(new ExceptionCallbacks( InterruptHandler, - null, + breakCallback, + stepCallback, KernelContext.SyscallHandler.SvcCall, UndefinedInstructionHandler)); } @@ -1175,5 +1197,154 @@ 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(); + thread.DebugHalt.WaitOne(); + } + } + + _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 bool DebugStep(KThread target) + { + if (_parent.debugState != (int)DebugState.Stopped) + { + 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(); + + StepBarrier.SignalAndWait(); + + _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(); + StepBarrier.SignalAndWait(); + return true; + } + + public DebugState GetDebugState() + { + return (DebugState)_parent.debugState; + } + + 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 8ef77902c..dce99ec88 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs @@ -296,6 +296,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 835bf5d40..40d366bf4 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs @@ -1,5 +1,6 @@ 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; @@ -114,6 +115,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading private readonly object _activityOperationLock = new(); + internal readonly ManualResetEvent DebugHalt = new(false); + public KThread(KernelContext context) : base(context) { WaitSyncObjects = new KSynchronizationObject[MaxWaitSyncObjects]; @@ -202,8 +205,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 +312,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 +369,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 +1255,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading private void ThreadStart() { _schedulerWaitEvent.WaitOne(); + DebugHalt.Reset(); KernelStatic.SetKernelContext(KernelContext, this); if (_customThreadStart != null) diff --git a/src/Ryujinx.HLE/Ryujinx.HLE.csproj b/src/Ryujinx.HLE/Ryujinx.HLE.csproj index a7bb3cd7f..d6f2aab4e 100644 --- a/src/Ryujinx.HLE/Ryujinx.HLE.csproj +++ b/src/Ryujinx.HLE/Ryujinx.HLE.csproj @@ -32,6 +32,12 @@ + + + + + + @@ -41,6 +47,12 @@ + + + + + + diff --git a/src/Ryujinx.HLE/Switch.cs b/src/Ryujinx.HLE/Switch.cs index 9dfc69892..e1e386181 100644 --- a/src/Ryujinx.HLE/Switch.cs +++ b/src/Ryujinx.HLE/Switch.cs @@ -10,6 +10,7 @@ using Ryujinx.HLE.Loaders.Processes; using Ryujinx.HLE.UI; using Ryujinx.Memory; using System; +using System.Threading; namespace Ryujinx.HLE { @@ -26,11 +27,15 @@ namespace Ryujinx.HLE public Hid Hid { get; } public TamperMachine TamperMachine { get; } public IHostUIHandler UIHandler { get; } + public Debugger.Debugger Debugger { get; } + public ManualResetEvent ExitStatus { get; } public bool EnableDeviceVsync { get; set; } = true; public bool IsFrameAvailable => Gpu.Window.IsFrameAvailable; + public bool IsActive { get; set; } = true; + public Switch(HLEConfiguration configuration) { ArgumentNullException.ThrowIfNull(configuration.GpuRenderer); @@ -49,11 +54,13 @@ namespace Ryujinx.HLE AudioDeviceDriver = new CompatLayerHardwareDeviceDriver(Configuration.AudioDeviceDriver); Memory = new MemoryBlock(Configuration.MemoryConfiguration.ToDramSize(), memoryAllocationFlags); Gpu = new GpuContext(Configuration.GpuRenderer); + Debugger = Configuration.EnableGdbStub ? new Debugger.Debugger(this, Configuration.GdbStubPort) : null; System = new HOS.Horizon(this); Statistics = new PerformanceStatistics(); Hid = new Hid(this, System.HidStorage); Processes = new ProcessLoader(this); TamperMachine = new TamperMachine(); + ExitStatus = new ManualResetEvent(false); System.InitializeServices(); System.State.SetLanguage(Configuration.SystemLanguage); @@ -154,6 +161,8 @@ namespace Ryujinx.HLE AudioDeviceDriver.Dispose(); FileSystem.Dispose(); Memory.Dispose(); + ExitStatus.Set(); + Debugger.Dispose(); } } } diff --git a/src/Ryujinx.Headless.SDL2/Options.cs b/src/Ryujinx.Headless.SDL2/Options.cs index ef8849eea..13737e205 100644 --- a/src/Ryujinx.Headless.SDL2/Options.cs +++ b/src/Ryujinx.Headless.SDL2/Options.cs @@ -225,6 +225,17 @@ namespace Ryujinx.Headless.SDL2 [Option("ignore-missing-services", Required = false, Default = false, HelpText = "Enable ignoring missing services.")] public bool IgnoreMissingServices { 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.Headless.SDL2/Program.cs b/src/Ryujinx.Headless.SDL2/Program.cs index 4ee271203..0011fbe58 100644 --- a/src/Ryujinx.Headless.SDL2/Program.cs +++ b/src/Ryujinx.Headless.SDL2/Program.cs @@ -580,7 +580,10 @@ namespace Ryujinx.Headless.SDL2 options.AudioVolume, options.UseHypervisor ?? true, options.MultiplayerLanInterfaceId, - Common.Configuration.Multiplayer.MultiplayerMode.Disabled); + Common.Configuration.Multiplayer.MultiplayerMode.Disabled, + options.EnableGdbStub, + options.GdbStubPort, + options.DebuggerSuspendOnStart); return new Switch(configuration); } diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs index 8a0be4028..d2338c39f 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs @@ -386,6 +386,21 @@ namespace Ryujinx.UI.Common.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; } + /// /// Loads a configuration file from disk /// diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs index 8420dc5d9..d255eb217 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs @@ -577,6 +577,37 @@ namespace Ryujinx.UI.Common.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.Event += static (sender, e) => LogValueChange(e, nameof(EnableGdbStub)); + GdbStubPort = new ReactiveObject(); + GdbStubPort.Event += static (sender, e) => LogValueChange(e, nameof(GdbStubPort)); + DebuggerSuspendOnStart = new ReactiveObject(); + DebuggerSuspendOnStart.Event += static (sender, e) => LogValueChange(e, nameof(DebuggerSuspendOnStart)); + } + } + /// /// The default configuration instance /// @@ -607,6 +638,11 @@ namespace Ryujinx.UI.Common.Configuration /// public HidSection Hid { get; private set; } + /// + /// The Debug + /// + public DebugSection Debug { get; private set; } + /// /// The Multiplayer section /// @@ -649,6 +685,7 @@ namespace Ryujinx.UI.Common.Configuration System = new SystemSection(); Graphics = new GraphicsSection(); Hid = new HidSection(); + Debug = new DebugSection(); Multiplayer = new MultiplayerSection(); EnableDiscordIntegration = new ReactiveObject(); CheckUpdatesOnStart = new ReactiveObject(); @@ -766,6 +803,9 @@ namespace Ryujinx.UI.Common.Configuration PreferredGpu = Graphics.PreferredGpu, MultiplayerLanInterfaceId = Multiplayer.LanInterfaceId, MultiplayerMode = Multiplayer.Mode, + EnableGdbStub = Debug.EnableGdbStub, + GdbStubPort = Debug.GdbStubPort, + DebuggerSuspendOnStart = Debug.DebuggerSuspendOnStart, }; return configurationFile; @@ -923,6 +963,9 @@ namespace Ryujinx.UI.Common.Configuration }, }, }; + Debug.EnableGdbStub.Value = false; + Debug.GdbStubPort.Value = 55555; + Debug.DebuggerSuspendOnStart.Value = false; } public void Load(ConfigurationFileFormat configurationFileFormat, string configurationFilePath) @@ -1437,6 +1480,16 @@ namespace Ryujinx.UI.Common.Configuration configurationFileUpdated = true; } + if (configurationFileFormat.Version < 48) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 38."); + + configurationFileFormat.EnableGdbStub = false; + configurationFileFormat.GdbStubPort = 55555; + configurationFileFormat.DebuggerSuspendOnStart = false; + + configurationFileUpdated = true; + } if (configurationFileFormat.Version < 48) { @@ -1564,6 +1617,9 @@ namespace Ryujinx.UI.Common.Configuration Hid.EnableMouse.Value = configurationFileFormat.EnableMouse; Hid.Hotkeys.Value = configurationFileFormat.Hotkeys; Hid.InputConfig.Value = configurationFileFormat.InputConfig; + Debug.EnableGdbStub.Value = configurationFileFormat.EnableGdbStub; + Debug.GdbStubPort.Value = configurationFileFormat.GdbStubPort; + Debug.DebuggerSuspendOnStart.Value = configurationFileFormat.DebuggerSuspendOnStart; if (Hid.InputConfig.Value == null) { diff --git a/src/Ryujinx/AppHost.cs b/src/Ryujinx/AppHost.cs index f4bfd1169..df5e315d2 100644 --- a/src/Ryujinx/AppHost.cs +++ b/src/Ryujinx/AppHost.cs @@ -104,7 +104,6 @@ namespace Ryujinx.Ava CursorStates.CursorIsVisible : CursorStates.CursorIsHidden; private bool _isStopped; - private bool _isActive; private bool _renderingStarted; private readonly ManualResetEvent _gpuDoneEvent; @@ -427,8 +426,6 @@ namespace Ryujinx.Ava RendererHost.BoundsChanged += Window_BoundsChanged; - _isActive = true; - _renderingThread.Start(); _viewModel.Volume = ConfigurationState.Instance.System.AudioVolume.Value; @@ -497,7 +494,7 @@ namespace Ryujinx.Ava public void Stop() { - _isActive = false; + Device.IsActive = false; } private void Exit() @@ -510,14 +507,14 @@ namespace Ryujinx.Ava } _isStopped = true; - _isActive = false; + Device.IsActive = false; } public void DisposeContext() { Dispose(); - _isActive = false; + Device.IsActive = false; // NOTE: The render loop is allowed to stay alive until the renderer itself is disposed, as it may handle resource dispose. // We only need to wait for all commands submitted during the main gpu loop to be processed. @@ -872,7 +869,10 @@ namespace Ryujinx.Ava ConfigurationState.Instance.System.AudioVolume, ConfigurationState.Instance.System.UseHypervisor, ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value, - ConfigurationState.Instance.Multiplayer.Mode); + ConfigurationState.Instance.Multiplayer.Mode, + ConfigurationState.Instance.Debug.EnableGdbStub.Value, + ConfigurationState.Instance.Debug.GdbStubPort.Value, + ConfigurationState.Instance.Debug.DebuggerSuspendOnStart.Value); Device = new Switch(configuration); } @@ -948,7 +948,7 @@ namespace Ryujinx.Ava private void MainLoop() { - while (_isActive) + while (Device.IsActive) { UpdateFrame(); @@ -999,7 +999,7 @@ namespace Ryujinx.Ava _renderer.Window.ChangeVSyncMode(Device.EnableDeviceVsync); - while (_isActive) + while (Device.IsActive) { _ticks += _chrono.ElapsedTicks; @@ -1098,7 +1098,7 @@ namespace Ryujinx.Ava private bool UpdateFrame() { - if (!_isActive) + if (!Device.IsActive) { return false; } diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json index b3cab7f5f..a831a9ecc 100644 --- a/src/Ryujinx/Assets/Locales/en_US.json +++ b/src/Ryujinx/Assets/Locales/en_US.json @@ -781,5 +781,12 @@ "MultiplayerMode": "Mode:", "MultiplayerModeTooltip": "Change LDN multiplayer mode.\n\nLdnMitm will modify local wireless/local play functionality in games to function as if it were LAN, allowing for local, same-network connections with other Ryujinx instances and hacked Nintendo Switch consoles that have the ldn_mitm module installed.\n\nMultiplayer requires all players to be on the same game version (i.e. Super Smash Bros. Ultimate v13.0.1 can't connect to v13.0.0).\n\nLeave DISABLED if unsure.", "MultiplayerModeDisabled": "Disabled", - "MultiplayerModeLdnMitm": "ldn_mitm" + "MultiplayerModeLdnMitm": "ldn_mitm", + "SettingsTabDebug": "Debug", + "SettingsTabDebugTitle": "Debug (WARNING: For developer use only)", + "EnableGDBStub": "Enable GDB Stub", + "GDBStubToggleTooltip": "Enables the GDB stub which makes it possible to debug the running application. For development use only!", + "GDBStubPort": "GDB stub port:", + "DebuggerSuspendOnStart": "Suspend application on start", + "DebuggerSuspendOnStartTooltip": "Suspends the application before executing the first instruction, allowing for debugging from the earliest point." } diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs index 70e5fa5c7..a0e4fd455 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -54,6 +54,9 @@ namespace Ryujinx.Ava.UI.ViewModels public event Action SaveSettingsEvent; private int _networkInterfaceIndex; private int _multiplayerModeIndex; + private bool _enableGDBStub; + private ushort _gdbStubPort; + private bool _debuggerSuspendOnStart; public int ResolutionScale { @@ -259,6 +262,36 @@ namespace Ryujinx.Ava.UI.ViewModels } } + 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; @@ -472,7 +505,13 @@ namespace Ryujinx.Ava.UI.ViewModels FsGlobalAccessLogMode = config.System.FsGlobalAccessLogMode; OpenglDebugLevel = (int)config.Logger.GraphicsDebugLevel.Value; + // Multiplayer MultiplayerModeIndex = (int)config.Multiplayer.Mode.Value; + + // Debug + EnableGdbStub = config.Debug.EnableGdbStub.Value; + GDBStubPort = config.Debug.GdbStubPort.Value; + DebuggerSuspendOnStart = config.Debug.DebuggerSuspendOnStart.Value; } public void SaveSettings() @@ -578,9 +617,15 @@ 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; + // Debug + config.Debug.EnableGdbStub.Value = EnableGdbStub; + config.Debug.GdbStubPort.Value = GDBStubPort; + config.Debug.DebuggerSuspendOnStart.Value = DebuggerSuspendOnStart; + config.ToFileFormat().SaveConfig(Program.ConfigurationPath); MainWindow.UpdateGraphicsConfig(); diff --git a/src/Ryujinx/UI/Views/Settings/SettingsDebugView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsDebugView.axaml new file mode 100644 index 000000000..aaa669413 --- /dev/null +++ b/src/Ryujinx/UI/Views/Settings/SettingsDebugView.axaml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + 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 de3c2291a..c6f5d3950 100644 --- a/src/Ryujinx/UI/Windows/SettingsWindow.axaml +++ b/src/Ryujinx/UI/Windows/SettingsWindow.axaml @@ -42,6 +42,7 @@ + +