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
+
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 @@
+
+