From 2484235f80297ae576b0cc27757ea4b7589d3eaa Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sun, 23 Aug 2026 20:42:29 +0200 Subject: [PATCH] feat(core): add debugger with breakpoints and watchpoints --- src/Chip8.Core/Debugger.cs | 215 +++++++++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 src/Chip8.Core/Debugger.cs diff --git a/src/Chip8.Core/Debugger.cs b/src/Chip8.Core/Debugger.cs new file mode 100644 index 0000000..bf91ac3 --- /dev/null +++ b/src/Chip8.Core/Debugger.cs @@ -0,0 +1,215 @@ +using System; +using System.Collections.Generic; + +namespace Chip8.Core; + +public enum StopReason +{ + StepLimit, + Breakpoint, + Watchpoint, + Exited, +} + +public readonly struct RunResult +{ + public RunResult(int steps, StopReason reason, ushort triggeredAddress = 0) + { + this.steps = steps; + this.reason = reason; + this.triggeredAddress = triggeredAddress; + } + + public int steps { get; } + public StopReason reason { get; } + public ushort triggeredAddress { get; } +} + +public sealed class Debugger +{ + readonly HashSet breakpoints = new(); + readonly Dictionary memoryWatches = new(); + readonly Dictionary registerWatches = new(); + + public Debugger(Machine machine) + { + this.machine = machine; + } + + public Machine machine { get; } + + public IReadOnlyCollection breakpointAddresses => breakpoints; + + public IReadOnlyCollection watchedMemory => memoryWatches.Keys; + + public IReadOnlyCollection watchedRegisters => registerWatches.Keys; + + public string? lastWatchDescription { get; private set; } + + public void addBreakpoint(ushort address) => breakpoints.Add(address); + + public void removeBreakpoint(ushort address) => breakpoints.Remove(address); + + public void clearBreakpoints() => breakpoints.Clear(); + + public bool hasBreakpoint(ushort address) => breakpoints.Contains(address); + + public void watchMemory(ushort address) => memoryWatches[address] = machine.memory[address & (machine.memory.Length - 1)]; + + public void unwatchMemory(ushort address) => memoryWatches.Remove(address); + + public void watchRegister(int register) => registerWatches[register & 0xF] = machine.v[register & 0xF]; + + public void unwatchRegister(int register) => registerWatches.Remove(register & 0xF); + + public void clearWatches() + { + memoryWatches.Clear(); + registerWatches.Clear(); + } + + public ushort peekOpcode() => (ushort)((machine.memory[machine.pc] << 8) | machine.memory[machine.pc + 1]); + + public string disassembleNext() + { + ushort opcode = peekOpcode(); + ushort extraWord = Disassembler.isLongLoad(opcode) + ? (ushort)((machine.memory[machine.pc + 2] << 8) | machine.memory[machine.pc + 3]) + : (ushort)0; + return Disassembler.describe(opcode, extraWord); + } + + public IReadOnlyList disassembleWindow(int count) + { + var lines = new List(); + int address = machine.pc; + + for (int taken = 0; taken < count && address + 1 < machine.memory.Length; taken++) + { + ushort opcode = (ushort)((machine.memory[address] << 8) | machine.memory[address + 1]); + if (Disassembler.isLongLoad(opcode) && address + 3 < machine.memory.Length) + { + ushort extra = (ushort)((machine.memory[address + 2] << 8) | machine.memory[address + 3]); + lines.Add(new DecodedInstruction((ushort)address, opcode, extra, 4, Disassembler.describe(opcode, extra))); + address += 4; + } + else + { + lines.Add(new DecodedInstruction((ushort)address, opcode, null, 2, Disassembler.describe(opcode))); + address += 2; + } + } + + return lines; + } + + public void step() => machine.step(); + + public RunResult run(int maxSteps) + { + for (int taken = 0; taken < maxSteps; taken++) + { + if (machine.exited) + { + return new RunResult(taken, StopReason.Exited); + } + if (taken > 0 && breakpoints.Contains(machine.pc)) + { + return new RunResult(taken, StopReason.Breakpoint, machine.pc); + } + + machine.step(); + + if (checkWatches(out ushort hitAddress)) + { + return new RunResult(taken + 1, StopReason.Watchpoint, hitAddress); + } + } + + return new RunResult(maxSteps, machine.exited ? StopReason.Exited : StopReason.StepLimit); + } + + public RunResult runUntil(ushort address, int maxSteps) + { + for (int taken = 0; taken < maxSteps; taken++) + { + if (machine.exited) + { + return new RunResult(taken, StopReason.Exited); + } + machine.step(); + if (machine.pc == address) + { + return new RunResult(taken + 1, StopReason.Breakpoint, address); + } + if (checkWatches(out ushort hitAddress)) + { + return new RunResult(taken + 1, StopReason.Watchpoint, hitAddress); + } + } + + return new RunResult(maxSteps, machine.exited ? StopReason.Exited : StopReason.StepLimit); + } + + public RunResult stepOver(int maxSteps) + { + ushort opcode = peekOpcode(); + if ((opcode & 0xF000) == 0x2000) + { + ushort returnAddress = (ushort)(machine.pc + 2); + return runUntil(returnAddress, maxSteps); + } + + machine.step(); + return new RunResult(1, machine.exited ? StopReason.Exited : StopReason.StepLimit); + } + + bool checkWatches(out ushort hitAddress) + { + hitAddress = 0; + bool triggered = false; + + foreach (ushort address in new List(memoryWatches.Keys)) + { + byte current = machine.memory[address & (machine.memory.Length - 1)]; + if (current != memoryWatches[address]) + { + if (!triggered) + { + hitAddress = address; + lastWatchDescription = "memory 0x" + address.ToString("X4") + " changed to 0x" + current.ToString("X2"); + triggered = true; + } + memoryWatches[address] = current; + } + } + + foreach (int register in new List(registerWatches.Keys)) + { + byte current = machine.v[register]; + if (current != registerWatches[register]) + { + if (!triggered) + { + lastWatchDescription = "register V" + register.ToString("X1") + " changed to 0x" + current.ToString("X2"); + triggered = true; + } + registerWatches[register] = current; + } + } + + return triggered; + } + + public byte[] readMemory(ushort address, int length) + { + var window = new byte[length]; + for (int offset = 0; offset < length; offset++) + { + window[offset] = machine.memory[(address + offset) & (machine.memory.Length - 1)]; + } + return window; + } + + public MachineSnapshot snapshot() => MachineSnapshot.capture(machine); +}