From 75174af77b7eabfa8dd4f87e90fffbfcdc6a8a1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 13 Aug 2026 09:52:26 +0200 Subject: [PATCH] Add GPR write breakpoints (break when a register is written, anywhere) New debugging primitive: break whenever any instruction writes to a given general-purpose register (0-31), regardless of which address executes the write. Requested for continuing the reboot.bin trace, where the actual blocker is "what sets $s3 to this bad value", not "what happens at a specific address" - existing address/memory breakpoints can't express that directly. - GPRBreakpoint (Core/Debugger/Breakpoints.h) mirrors the existing BreakPoint/MemCheck shape (result/condition/logFormat/hit count), keyed by register index instead of address/range. - BreakpointManager keeps a u32 bitmask (bit i = register i has an active breakpoint) alongside the GPRBreakpoint vector, so the interpreter loop can test "would this write trip anything" with a single shift+and against a value already cached in a local. - RunUntilDowncountZeroWithChecks (Core/MIPS/MIPSTables.cpp) computes the about-to-be-written register from the current instruction's OUT_RT/OUT_RD/OUT_RA flags (GetGPRWriteTarget()) and checks it against the mask, same convention as the existing memcheck handling right above it (checked before the instruction executes, bails via CORE_STEPPING_CPU without running it if tripped). - New BreakReason::GPRBreakpoint ("cpu.gprBreakpoint") for Core_Break. - WebSocket API: cpu.gprBreakpoint.add/update/remove/list, accepting either a 0-31 'register' index or a case-insensitive 'name' (e.g. "s3"), documented in docs/WebSocketDebugger.md. Interpreter-only for now, deliberately. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9 --- Core/Core.cpp | 1 + Core/Core.h | 1 + Core/Debugger/Breakpoints.cpp | 164 ++++++++++++ Core/Debugger/Breakpoints.h | 60 +++++ .../WebSocket/BreakpointSubscriber.cpp | 243 ++++++++++++++++++ .../Debugger/WebSocket/BreakpointSubscriber.h | 5 + Core/MIPS/MIPSTables.cpp | 26 +- docs/WebSocketDebugger.md | 2 +- 8 files changed, 500 insertions(+), 2 deletions(-) diff --git a/Core/Core.cpp b/Core/Core.cpp index 47349e2d57..bdaf7e9cdf 100644 --- a/Core/Core.cpp +++ b/Core/Core.cpp @@ -213,6 +213,7 @@ const char *BreakReasonToString(BreakReason reason) { case BreakReason::SavestateCrash: return "savestate.crash"; case BreakReason::MemoryBreakpoint: return "memory.breakpoint"; case BreakReason::CpuBreakpoint: return "cpu.breakpoint"; + case BreakReason::GPRBreakpoint: return "cpu.gprBreakpoint"; case BreakReason::MemoryAccess: return "memory.access"; // ??? case BreakReason::JitBranchDebug: return "jit.branchdebug"; case BreakReason::RABreak: return "ra.break"; diff --git a/Core/Core.h b/Core/Core.h index 1485875de0..00db0f0d8d 100644 --- a/Core/Core.h +++ b/Core/Core.h @@ -64,6 +64,7 @@ enum class BreakReason { SavestateCrash, MemoryBreakpoint, CpuBreakpoint, + GPRBreakpoint, MemoryAccess, // ??? JitBranchDebug, BreakOnBoot, diff --git a/Core/Debugger/Breakpoints.cpp b/Core/Debugger/Breakpoints.cpp index 34eee8c802..419feb0b95 100644 --- a/Core/Debugger/Breakpoints.cpp +++ b/Core/Debugger/Breakpoints.cpp @@ -94,6 +94,15 @@ size_t BreakpointManager::FindMemCheck(u32 start, u32 end) { return INVALID_MEMCHECK; } +size_t BreakpointManager::FindGPRBreakpoint(int reg) { + for (size_t i = 0; i < gprBreakpoints_.size(); ++i) { + if (gprBreakpoints_[i].reg == reg) + return i; + } + + return INVALID_GPR_BREAKPOINT; +} + bool BreakpointManager::IsAddressBreakPoint(u32 addr) { if (!anyBreakPoints_) @@ -493,6 +502,161 @@ BreakAction BreakpointManager::ExecOpMemCheck(u32 address, u32 pc) { return BREAK_ACTION_IGNORE; } +void BreakpointManager::RecomputeGPRBreakpointMask() { + u32 mask = 0; + for (const auto &bp : gprBreakpoints_) { + if (bp.result != BREAK_ACTION_IGNORE) + mask |= 1u << bp.reg; + } + gprBreakpointMask_ = mask; +} + +int BreakpointManager::AddGPRBreakpoint(int reg) { + size_t bp = FindGPRBreakpoint(reg); + if (bp == INVALID_GPR_BREAKPOINT) { + GPRBreakpoint pt; + pt.reg = reg; + pt.result |= BREAK_ACTION_PAUSE; + + gprBreakpoints_.push_back(pt); + RecomputeGPRBreakpointMask(); + Update(INVALID_ADDRESS); // Not baked into JIT code, no cache invalidation needed. + return (int)gprBreakpoints_.size() - 1; + } else if (!gprBreakpoints_[bp].IsEnabled()) { + gprBreakpoints_[bp].result |= BREAK_ACTION_PAUSE; + gprBreakpoints_[bp].hasCond = false; + RecomputeGPRBreakpointMask(); + Update(INVALID_ADDRESS); + return (int)bp; + } else { + return (int)bp; + } +} + +void BreakpointManager::RemoveGPRBreakpoint(int reg) { + size_t bp = FindGPRBreakpoint(reg); + if (bp != INVALID_GPR_BREAKPOINT) { + gprBreakpoints_.erase(gprBreakpoints_.begin() + bp); + RecomputeGPRBreakpointMask(); + Update(INVALID_ADDRESS); + } +} + +void BreakpointManager::ChangeGPRBreakpoint(int reg, bool status) { + size_t bp = FindGPRBreakpoint(reg); + if (bp != INVALID_GPR_BREAKPOINT) { + if (status) + gprBreakpoints_[bp].result |= BREAK_ACTION_PAUSE; + else + gprBreakpoints_[bp].result = BreakAction(gprBreakpoints_[bp].result & ~BREAK_ACTION_PAUSE); + RecomputeGPRBreakpointMask(); + Update(INVALID_ADDRESS); + } +} + +void BreakpointManager::ChangeGPRBreakpoint(int reg, BreakAction result) { + size_t bp = FindGPRBreakpoint(reg); + if (bp != INVALID_GPR_BREAKPOINT) { + gprBreakpoints_[bp].result = result; + RecomputeGPRBreakpointMask(); + Update(INVALID_ADDRESS); + } +} + +void BreakpointManager::ClearAllGPRBreakpoints() { + if (!gprBreakpoints_.empty()) { + gprBreakpoints_.clear(); + gprBreakpointMask_ = 0; + Update(INVALID_ADDRESS); + } +} + +void BreakpointManager::ChangeGPRBreakpointAddCond(int reg, const BreakPointCond &cond) { + size_t bp = FindGPRBreakpoint(reg); + if (bp != INVALID_GPR_BREAKPOINT) { + gprBreakpoints_[bp].hasCond = true; + gprBreakpoints_[bp].cond = cond; + // No need to update jit for a condition add/remove, they're not baked in. + Update(INVALID_ADDRESS); + } +} + +void BreakpointManager::ChangeGPRBreakpointRemoveCond(int reg) { + size_t bp = FindGPRBreakpoint(reg); + if (bp != INVALID_GPR_BREAKPOINT) { + gprBreakpoints_[bp].hasCond = false; + Update(INVALID_ADDRESS); + } +} + +BreakPointCond *BreakpointManager::GetGPRBreakpointCondition(int reg) { + size_t bp = FindGPRBreakpoint(reg); + if (bp != INVALID_GPR_BREAKPOINT && gprBreakpoints_[bp].hasCond) + return &gprBreakpoints_[bp].cond; + return nullptr; +} + +void BreakpointManager::ChangeGPRBreakpointLogFormat(int reg, const std::string &fmt) { + size_t bp = FindGPRBreakpoint(reg); + if (bp != INVALID_GPR_BREAKPOINT) { + gprBreakpoints_[bp].logFormat = fmt; + Update(INVALID_ADDRESS); + } +} + +bool BreakpointManager::IsGPRBreakpoint(int reg) { + return (gprBreakpointMask_ & (1u << reg)) != 0; +} + +bool BreakpointManager::GetGPRBreakpoint(int reg, GPRBreakpoint *check) { + size_t bp = FindGPRBreakpoint(reg); + if (bp != INVALID_GPR_BREAKPOINT) { + *check = gprBreakpoints_[bp]; + return true; + } + return false; +} + +std::vector BreakpointManager::GetGPRBreakpoints() { + return gprBreakpoints_; +} + +BreakAction BreakpointManager::ExecGPRBreakpoint(int reg, u32 pc) { + // Callers are expected to have already checked GetGPRBreakpointMask() themselves (that's + // the whole point of exposing it - a single shift+and in the hot interpreter loop, skipping + // a function call entirely in the overwhelmingly common no-breakpoint case), but check again + // here too since this is also reachable directly. + if ((gprBreakpointMask_ & (1u << reg)) == 0) + return BREAK_ACTION_IGNORE; + size_t bp = FindGPRBreakpoint(reg); + if (bp == INVALID_GPR_BREAKPOINT) + return BREAK_ACTION_IGNORE; + + GPRBreakpoint &info = gprBreakpoints_[bp]; + if (info.result == BREAK_ACTION_IGNORE) + return BREAK_ACTION_IGNORE; + + if (info.hasCond && !info.cond.Evaluate()) + return BREAK_ACTION_IGNORE; + + ++info.numHits; + + if (info.result & BREAK_ACTION_LOG) { + if (info.logFormat.empty()) { + NOTICE_LOG(Log::JIT, "BKP GPR write r%d, PC=%08x (%s)", reg, pc, g_symbolMap->GetDescription(pc).c_str()); + } else { + std::string formatted; + BreakpointManager::EvaluateLogFormat(currentDebugMIPS, info.logFormat, formatted); + NOTICE_LOG(Log::JIT, "BKP GPR write r%d, PC=%08x: %s", reg, pc, formatted.c_str()); + } + } + if (info.result & BREAK_ACTION_PAUSE) { + Core_Break(BreakReason::GPRBreakpoint, pc); + } + + return info.result; +} + void BreakpointManager::SetSkipFirst(u32 pc) { breakSkipFirstAt_ = pc; breakSkipFirstTicks_ = CoreTiming::GetTicks(currentMIPS); diff --git a/Core/Debugger/Breakpoints.h b/Core/Debugger/Breakpoints.h index c7fea9a0a0..1deee65e68 100644 --- a/Core/Debugger/Breakpoints.h +++ b/Core/Debugger/Breakpoints.h @@ -114,6 +114,30 @@ struct MemCheck { } }; +// A breakpoint that trips whenever a general-purpose register is written to by an +// instruction, regardless of address - identified by register index (0-31), not addr/range. +// Interpreter-only for now (see RunUntilDowncountZeroWithChecks in MIPSTables.cpp) - the JITs +// don't check this at all, so it has no effect unless running with the plain interpreter core. +struct GPRBreakpoint { + int reg = 0; // 0-31, general-purpose register index (matches OUT_RT/OUT_RD/OUT_RA fields). + + BreakAction result = BREAK_ACTION_IGNORE; + std::string logFormat; + + bool hasCond = false; + BreakPointCond cond; + + u32 numHits = 0; + + bool IsEnabled() const { + return (result & BREAK_ACTION_PAUSE) != 0; + } + + bool operator == (const GPRBreakpoint &other) const { + return reg == other.reg; + } +}; + // BreakPoints cannot overlap, only one is allowed per address. // MemChecks can overlap, as long as their ends are different. // WARNING: MemChecks are not always tracked in HLE currently. @@ -121,6 +145,7 @@ class BreakpointManager { public: static const size_t INVALID_BREAKPOINT = -1; static const size_t INVALID_MEMCHECK = -1; + static const size_t INVALID_GPR_BREAKPOINT = -1; bool IsAddressBreakPoint(u32 addr); bool IsAddressBreakPoint(u32 addr, bool* enabled); @@ -158,6 +183,27 @@ public: BreakAction ExecMemCheck(u32 address, bool write, int size, u32 pc, const char *reason); BreakAction ExecOpMemCheck(u32 address, u32 pc); + // GPR write breakpoints - see GPRBreakpoint above. reg is a 0-31 GPR index. + int AddGPRBreakpoint(int reg); // Returns the breakpoint index. + void RemoveGPRBreakpoint(int reg); + void ChangeGPRBreakpoint(int reg, bool enable); + void ChangeGPRBreakpoint(int reg, BreakAction result); + void ClearAllGPRBreakpoints(); + + void ChangeGPRBreakpointAddCond(int reg, const BreakPointCond &cond); + void ChangeGPRBreakpointRemoveCond(int reg); + BreakPointCond *GetGPRBreakpointCondition(int reg); + + void ChangeGPRBreakpointLogFormat(int reg, const std::string &fmt); + + bool IsGPRBreakpoint(int reg); + bool GetGPRBreakpoint(int reg, GPRBreakpoint *bp); + std::vector GetGPRBreakpoints(); + + // Called from the interpreter (RunUntilDowncountZeroWithChecks) right before executing an + // instruction that would write to reg - does not itself execute the instruction. + BreakAction ExecGPRBreakpoint(int reg, u32 pc); + void SetSkipFirst(u32 pc); u32 CheckSkipFirst(); @@ -182,6 +228,15 @@ public: bool HasMemChecks() const { return anyMemChecks_; } + bool HasGPRBreakpoints() const { + return gprBreakpointMask_ != 0; + } + // Bit i set means register i has an active (non-ignored) GPR breakpoint - a cheap way for + // the interpreter's hot per-instruction loop to test "would this write trip anything" with + // a single shift+and, without touching gprBreakpoints_ at all in the common no-match case. + u32 GetGPRBreakpointMask() const { + return gprBreakpointMask_; + } void Frame(); @@ -200,9 +255,12 @@ private: // Finds a memcheck covering (part of) a range, unlike FindMemCheck() above. MemCheck *FindMemCheckInRange(u32 address, int size); void UpdateCachedMemCheckRanges(); + size_t FindGPRBreakpoint(int reg); + void RecomputeGPRBreakpointMask(); std::atomic anyBreakPoints_; std::atomic anyMemChecks_; + std::atomic gprBreakpointMask_; std::vector breakPoints_; u32 breakSkipFirstAt_ = 0; @@ -212,6 +270,8 @@ private: std::vector memCheckRangesRead_; std::vector memCheckRangesWrite_; + std::vector gprBreakpoints_; + bool needsUpdate_ = true; u32 updateAddr_ = 0; diff --git a/Core/Debugger/WebSocket/BreakpointSubscriber.cpp b/Core/Debugger/WebSocket/BreakpointSubscriber.cpp index 928addd1c3..7c48b7e5ec 100644 --- a/Core/Debugger/WebSocket/BreakpointSubscriber.cpp +++ b/Core/Debugger/WebSocket/BreakpointSubscriber.cpp @@ -36,9 +36,42 @@ DebuggerSubscriber *WebSocketBreakpointInit(DebuggerEventHandlerMap &map) { map["memory.breakpoint.remove"] = &WebSocketMemoryBreakpointRemove; map["memory.breakpoint.list"] = &WebSocketMemoryBreakpointList; + map["cpu.gprBreakpoint.add"] = &WebSocketGPRBreakpointAdd; + map["cpu.gprBreakpoint.update"] = &WebSocketGPRBreakpointUpdate; + map["cpu.gprBreakpoint.remove"] = &WebSocketGPRBreakpointRemove; + map["cpu.gprBreakpoint.list"] = &WebSocketGPRBreakpointList; + return nullptr; } +// Resolves a GPR by name (e.g. "s3", case-insensitive) or 0-31 index. Interpreter-only feature - +// see GPRBreakpoint in Breakpoints.h - has no effect while running under a JIT backend. +static bool ParseGPRBreakpointReg(DebuggerRequest &req, int *reg) { + if (req.HasParam("name")) { + std::string name; + if (!req.ParamString("name", &name)) + return false; + for (int i = 0; i < 32; ++i) { + if (!strcasecmp(name.c_str(), MIPSDebugInterface::GetRegName(0, i).c_str())) { + *reg = i; + return true; + } + } + req.Fail(StringFromFormat("Unknown register name: %s", name.c_str())); + return false; + } + + uint32_t regU32; + if (!req.ParamU32("register", ®U32)) + return false; + if (regU32 >= 32) { + req.Fail("Invalid 'register' parameter, must be 0-31"); + return false; + } + *reg = (int)regU32; + return true; +} + struct WebSocketCPUBreakpointParams { uint32_t address = 0; bool hasEnabled = false; @@ -516,3 +549,213 @@ void WebSocketMemoryBreakpointList(DebuggerRequest &req) { json.pop(); }); } + +struct WebSocketGPRBreakpointParams { + int reg = 0; + bool hasEnabled = false; + bool hasLog = false; + bool hasCondition = false; + bool hasLogFormat = false; + + bool enabled; + bool log; + std::string condition; + PostfixExpression compiledCondition; + std::string logFormat; + + bool Parse(DebuggerRequest &req) { + if (!currentDebugMIPS->isAlive()) { + req.Fail("CPU not started"); + return false; + } + + if (!ParseGPRBreakpointReg(req, ®)) + return false; + + hasEnabled = req.HasParam("enabled"); + if (hasEnabled) { + if (!req.ParamBool("enabled", &enabled)) + return false; + } + hasLog = req.HasParam("log"); + if (hasLog) { + if (!req.ParamBool("log", &log)) + return false; + } + hasCondition = req.HasParam("condition"); + if (hasCondition) { + if (!req.ParamString("condition", &condition)) + return false; + if (!initExpression(currentDebugMIPS, condition.c_str(), compiledCondition)) { + req.Fail(StringFromFormat("Could not parse expression syntax: %s", getExpressionError())); + return false; + } + } + hasLogFormat = req.HasParam("logFormat"); + if (hasLogFormat) { + if (!req.ParamString("logFormat", &logFormat)) + return false; + } + + return true; + } + + void Apply() { + if (hasCondition && !condition.empty()) { + BreakPointCond cond; + cond.debug = currentDebugMIPS; + cond.expressionString = condition; + cond.expression = compiledCondition; + g_breakpoints.ChangeGPRBreakpointAddCond(reg, cond); + } else if (hasCondition && condition.empty()) { + g_breakpoints.ChangeGPRBreakpointRemoveCond(reg); + } + + if (hasLogFormat) { + g_breakpoints.ChangeGPRBreakpointLogFormat(reg, logFormat); + } + + if (hasLog && !hasEnabled) { + GPRBreakpoint bp; + if (g_breakpoints.GetGPRBreakpoint(reg, &bp)) + enabled = bp.IsEnabled(); + hasEnabled = true; + } + if (hasLog && hasEnabled) { + BreakAction result = BREAK_ACTION_IGNORE; + if (log) + result |= BREAK_ACTION_LOG; + if (enabled) + result |= BREAK_ACTION_PAUSE; + g_breakpoints.ChangeGPRBreakpoint(reg, result); + } else if (hasEnabled) { + g_breakpoints.ChangeGPRBreakpoint(reg, enabled); + } + } +}; + +// Add a new GPR write breakpoint (cpu.gprBreakpoint.add) +// +// Interpreter-only for now - see GPRBreakpoint in Core/Debugger/Breakpoints.h. Has no effect +// while running under a JIT backend (force the interpreter core, e.g. -i on the command line). +// +// Parameters: +// - register: unsigned integer 0-31 GPR index to break on write to. Ignored if name given. +// - name: string register name (e.g. "s3"), case-insensitive. Takes priority over 'register'. +// - enabled: optional boolean, whether to actually enter stepping when this breakpoint trips. +// - log: optional boolean, whether to log when this breakpoint trips. +// - condition: optional string expression to evaluate - breakpoint does not trip if false. +// - logFormat: optional string to log when breakpoint trips, may include {expression} parts. +// +// Response (same event name) with no extra data. +// +// Note: will replace any GPR breakpoint already set on the same register. +void WebSocketGPRBreakpointAdd(DebuggerRequest &req) { + WebSocketGPRBreakpointParams params; + if (!params.Parse(req)) + return; + + // Route the actual breakpoint manipulation to the CPU thread instead of poking at it directly + // from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h. + Core_RunOnCPUThread([&] { + g_breakpoints.AddGPRBreakpoint(params.reg); + params.Apply(); + }); + req.Respond(); +} + +// Update a GPR write breakpoint (cpu.gprBreakpoint.update) +// +// Parameters: same as cpu.gprBreakpoint.add. +// +// Response (same event name) with no extra data. +void WebSocketGPRBreakpointUpdate(DebuggerRequest &req) { + WebSocketGPRBreakpointParams params; + if (!params.Parse(req)) + return; + + // Route the actual breakpoint manipulation to the CPU thread instead of poking at it directly + // from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h. + bool found = false; + Core_RunOnCPUThread([&] { + GPRBreakpoint bp; + found = g_breakpoints.GetGPRBreakpoint(params.reg, &bp); + if (found) + params.Apply(); + }); + + if (!found) + return req.Fail("Breakpoint not found"); + req.Respond(); +} + +// Remove a GPR write breakpoint (cpu.gprBreakpoint.remove) +// +// Parameters: +// - register: unsigned integer 0-31 GPR index. Ignored if name given. +// - name: string register name (e.g. "s3"), case-insensitive. Takes priority over 'register'. +// +// Response (same event name) with no extra data. +void WebSocketGPRBreakpointRemove(DebuggerRequest &req) { + if (!currentDebugMIPS->isAlive()) { + return req.Fail("CPU not started"); + } + + int reg; + if (!ParseGPRBreakpointReg(req, ®)) + return; + + // Route the actual breakpoint manipulation to the CPU thread instead of poking at it directly + // from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h. + Core_RunOnCPUThread([&] { + g_breakpoints.RemoveGPRBreakpoint(reg); + }); + req.Respond(); +} + +// List all GPR write breakpoints (cpu.gprBreakpoint.list) +// +// No parameters. +// +// Response (same event name): +// - breakpoints: array of objects, each with properties: +// - register: unsigned integer 0-31 GPR index. +// - name: string register name (e.g. "s3"). +// - enabled: boolean, whether to actually enter stepping when this breakpoint trips. +// - log: boolean, whether to log when this breakpoint trips. +// - hits: unsigned integer, number of times this breakpoint has tripped (regardless of +// whether it paused - i.e. even with enabled false, if log is true.) +// - condition: null, or string expression to evaluate - breakpoint does not trip if false. +// - logFormat: null, or string to log when breakpoint trips, may include {expression} parts. +void WebSocketGPRBreakpointList(DebuggerRequest &req) { + if (!currentDebugMIPS->isAlive()) { + return req.Fail("CPU not started"); + } + + // Route the breakpoint reads to the CPU thread instead of poking at them directly from this + // WebSocket handler thread - see Core_RunOnCPUThread() in Core.h. + Core_RunOnCPUThread([&] { + JsonWriter &json = req.Respond(); + json.pushArray("breakpoints"); + std::vector bps = g_breakpoints.GetGPRBreakpoints(); + for (const GPRBreakpoint &bp : bps) { + json.pushDict(); + json.writeInt("register", bp.reg); + json.writeString("name", MIPSDebugInterface::GetRegName(0, bp.reg)); + json.writeBool("enabled", bp.IsEnabled()); + json.writeBool("log", (bp.result & BREAK_ACTION_LOG) != 0); + json.writeUint("hits", bp.numHits); + if (bp.hasCond) + json.writeString("condition", bp.cond.expressionString); + else + json.writeNull("condition"); + if (!bp.logFormat.empty()) + json.writeString("logFormat", bp.logFormat); + else + json.writeNull("logFormat"); + + json.pop(); + } + json.pop(); + }); +} diff --git a/Core/Debugger/WebSocket/BreakpointSubscriber.h b/Core/Debugger/WebSocket/BreakpointSubscriber.h index d0e622cd12..367985b970 100644 --- a/Core/Debugger/WebSocket/BreakpointSubscriber.h +++ b/Core/Debugger/WebSocket/BreakpointSubscriber.h @@ -30,3 +30,8 @@ void WebSocketMemoryBreakpointAdd(DebuggerRequest &req); void WebSocketMemoryBreakpointUpdate(DebuggerRequest &req); void WebSocketMemoryBreakpointRemove(DebuggerRequest &req); void WebSocketMemoryBreakpointList(DebuggerRequest &req); + +void WebSocketGPRBreakpointAdd(DebuggerRequest &req); +void WebSocketGPRBreakpointUpdate(DebuggerRequest &req); +void WebSocketGPRBreakpointRemove(DebuggerRequest &req); +void WebSocketGPRBreakpointList(DebuggerRequest &req); diff --git a/Core/MIPS/MIPSTables.cpp b/Core/MIPS/MIPSTables.cpp index a2cbde1642..f32b2ecb95 100644 --- a/Core/MIPS/MIPSTables.cpp +++ b/Core/MIPS/MIPSTables.cpp @@ -1142,9 +1142,24 @@ static void RunUntilDowncountZeroFast(MIPSState *mips) { } #define _RS(op) ((op>>21) & 0x1F) +// Returns the 0-31 GPR index an instruction is about to write, or -1 if it doesn't write a GPR. +// OUT_RA is always $ra (31) - jal/bltzal/bgezal/etc. have no encoded destination register field, +// unlike jalr (which uses OUT_RD, since its destination is chosen via the rd field). +static inline int GetGPRWriteTarget(const MIPSInstruction *instr, MIPSOpcode op) { + if (instr->flags & OUT_RT) + return (op >> 16) & 0x1F; + if (instr->flags & OUT_RD) + return (op >> 11) & 0x1F; + if (instr->flags & OUT_RA) + return 31; + return -1; +} + static void RunUntilDowncountZeroWithChecks(MIPSState *mips, u64 globalTicks) { bool hasBPs = g_breakpoints.HasBreakPoints(); bool hasMCs = g_breakpoints.HasMemChecks(); + // Bit i set means register i has an active GPR breakpoint - see GetGPRBreakpointMask(). + u32 gprBPMask = g_breakpoints.GetGPRBreakpointMask(); while (mips->downcount >= 0 && coreState == CORE_RUNNING_CPU) { // Don't stop in a delay slot! Well, unless we hit a memcheck in one, of course. do { @@ -1181,6 +1196,15 @@ static void RunUntilDowncountZeroWithChecks(MIPSState *mips, u64 globalTicks) { if (coreState == CORE_STEPPING_CPU) break; } + if (gprBPMask != 0 && (instr->flags & (OUT_RT | OUT_RD | OUT_RA)) != 0 && g_breakpoints.CheckSkipFirst() != mips->pc) { + int gprTarget = GetGPRWriteTarget(instr, op); + if (gprTarget >= 0 && (gprBPMask & (1u << gprTarget)) != 0) { + g_breakpoints.ExecGPRBreakpoint(gprTarget, mips->pc); + // If it tripped, bail without running - same convention as memchecks above. + if (coreState == CORE_STEPPING_CPU) + break; + } + } bool wasInDelaySlot = mips->inDelaySlot; Interpret(mips, instr, op); @@ -1204,7 +1228,7 @@ int MIPSInterpret_RunUntil(MIPSState *mips, u64 globalTicks) { CoreTiming::Advance(mips); uint64_t ticksLeft = globalTicks - CoreTiming::GetTicks(mips); - if (g_breakpoints.HasBreakPoints() || g_breakpoints.HasMemChecks() || ticksLeft <= mips->downcount) { + if (g_breakpoints.HasBreakPoints() || g_breakpoints.HasMemChecks() || g_breakpoints.HasGPRBreakpoints() || ticksLeft <= mips->downcount) { RunUntilDowncountZeroWithChecks(mips, globalTicks); } else { RunUntilDowncountZeroFast(mips); diff --git a/docs/WebSocketDebugger.md b/docs/WebSocketDebugger.md index 202a24eda3..f208c34a87 100644 --- a/docs/WebSocketDebugger.md +++ b/docs/WebSocketDebugger.md @@ -105,7 +105,7 @@ file - this is just an index. | Game/version | `game.reset`, `game.status`, `version` | `GameSubscriber.cpp` | | CPU core | `cpu.stepping`, `cpu.resume`, `cpu.status`, `cpu.getAllRegs`, `cpu.getReg`, `cpu.setReg`, `cpu.evaluate` | `CPUCoreSubscriber.cpp` | | Stepping | `cpu.stepInto`, `cpu.stepOver`, `cpu.stepOut`, `cpu.runUntil`, `cpu.nextHLE` | `SteppingSubscriber.cpp` | -| Breakpoints | `cpu.breakpoint.add/update/remove/list`, `memory.breakpoint.add/update/remove/list` | `BreakpointSubscriber.cpp` | +| Breakpoints | `cpu.breakpoint.add/update/remove/list`, `memory.breakpoint.add/update/remove/list`, `cpu.gprBreakpoint.add/update/remove/list` (break when a GPR is written to, by any instruction anywhere - interpreter-only, no effect under a JIT backend) | `BreakpointSubscriber.cpp` | | Memory read/write | `memory.read_u8/u16/u32`, `memory.read`, `memory.readString`, `memory.write_u8/u16/u32`, `memory.write` | `MemorySubscriber.cpp` | | Memory search | `memory.search` - scan a range for a `u8`/`u16`/`u32`/`float` value or a `bytes` pattern (with an optional wildcard mask), for narrowing down where an unknown value lives (Cheat Engine style) | `MemorySubscriber.cpp` | | Memory info/annotations | `memory.mapping`, `memory.info.config/set/list/search` | `MemoryInfoSubscriber.cpp` |