A breakpoint hit reached a WebSocket client as two fields on cpu.stepping: a
reason string and one address. Everything else the hit site knew was formatted
into a log line and dropped.
What was missing per kind:
- exec: hit count, condition, symbol.
- memory: the address actually accessed, read vs write, size, and who did it.
The address that reached the client was the *start of the watched range*, so a
client watching 4KB learned only that something in it was touched.
- register: which register. Entirely - the event carried pc and nothing else.
There's now a BreakpointHit captured where the hit happens and carried through
Core_Break() on the stepping reason, rendered as a "hit" object on cpu.stepping.
It's absent rather than empty when the break wasn't a breakpoint (a pause, a
savestate load, an exception), so presence is the test. relatedAddress keeps
reporting the range start for compatibility; hit.address is the accurate one.
The formatter is shared with the new event below, so the two can't drift.
And a new cpu.breakpoint.hit broadcast fires on *every* hit whose condition
passes, whether or not it stops the CPU. That's the part that makes log-only
breakpoints usable for automation: until now their only trace was a line in the
log stream, so a client couldn't count hits, or react to one, without scraping
text. Same "hit" object, plus a sequence number.
Volume needed handling, since a log-only breakpoint in a hot loop produces
events far faster than a connection drains them - measured 13719 hits in three
seconds of one homebrew's draw function. The per-connection queue is capped and
drops rather than growing without bound, and the sequence number is what makes
that honest: a gap tells a client exactly how many it missed. Clients that don't
want the traffic at all can disallow the new "breakpoint" broadcast category.
Building the hit record is skipped entirely when no debugger is connected, which
is one relaxed atomic load on that path.
Verified against a running game, all three kinds. The memory case shows why the
address/range split matters - accessed address 200540160 against a watched range
starting at 200941120, with source "ThreadFillStack" identifying the HLE call
responsible.
libretro gets stubs: it builds Core.cpp and Breakpoints.cpp but not
Core/Debugger/WebSocket.cpp.
pspautotests 314/314, UnitTest 55/55.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
It reported BreakReason::DebugBreak, the same reason a user hitting pause
produces, so a client that asked to run until a point in emulated time couldn't
tell its deadline landing from someone stopping the core by hand - the one
piece of information the cpu.stepping event exists to convey.
Reports "cpu.runUntilTime" now, matching the request that armed it.
No client depended on the old string: nothing outside Core.cpp names
"cpu.debugbreak", and wsdbg's resume handling keys off the event, not the
reason.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
It stopped being about memory when CPU_Shutdown started holding it across the
whole teardown - it's what keeps kernel objects, the symbol map and the memory
map from being freed while another thread reads them. The old name invited the
reading that it locks memory *access*, which it has never done.
Memory::Reinit() now holds it across both halves rather than relying on
Memory::Shutdown()'s own acquire: between Shutdown() and Init() there is no
memory map at all, and a reader could slip into that gap.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
The struct and its API only handle GPR indices today, but the naming
should stay general since this is expected to grow to cover other
register files too (e.g. FPU registers like $f10). Pure rename - no
behavior change:
- Core/Debugger/Breakpoints.{h,cpp}: RegBreakpoint struct, all
BreakpointManager Add/Remove/Change/Get/Exec/Has/Find*RegBreakpoint*
methods, regBreakpoints_/regBreakpointMask_ members.
- Core/Core.{h,cpp}: BreakReason::RegBreakpoint, "cpu.regBreakpoint"
break-reason string.
- Core/Debugger/WebSocket/BreakpointSubscriber.{h,cpp}: WebSocket
events cpu.gprBreakpoint.* -> cpu.regBreakpoint.*, matching
Add/Update/Remove/List handlers and params struct.
- Core/MIPS/MIPSTables.cpp: local variable names in the interpreter's
per-instruction breakpoint check.
- docs/WebSocketDebugger.md updated to match.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
Core_RunLoopUntil() is only reached while a game is actually loaded and
running (via EmuScreen). Anything calling Core_RunOnCPUThread() while at
the main menu with no game loaded would hang forever waiting for a queue
that was never drained. Call Core_ProcessCPUQueue() directly from
NativeFrame(), just before screenManager->render(), so it always runs;
Core_RunLoopUntil() still also drains it for the tight-spin-while-stepping
case once a game is running.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hqm11k99viLfbJm2MkH4BH
Debugger windows (register list, disassembly view, memory view, breakpoint/
thread/module/stack lists, watch list) read CPU-thread-owned state directly
from the GUI thread's WM_PAINT/list-fill handlers, racing against the CPU
thread. Routing every read through Core_RunOnCPUThread would be too slow for
something invoked continuously on paint/list-refresh.
Add g_frameMutex (Core.h/Core.cpp), held by NativeFrame() only across the
span where it actually touches that state (running the CPU, processing
breakpoints, running the ImGui debugger) - not across input handling or the
present/frame-pacing waits. Debugger windows now hold the same mutex while
reading, giving synchronized reads without the round-trip cost of queuing
to the CPU thread.
CtrlRegisterList::onPaint() goes back to always reading live values (now
safe under the lock) and grays them out by color alone while the core is
running, rather than the earlier snapshot-caching approach.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hqm11k99viLfbJm2MkH4BH
Extends the cpu.stepInto treatment to cpu.stepOver, cpu.stepOut, cpu.runUntil,
and cpu.nextHLE: each now routes its breakpoint/stepping manipulation through
Core_RunOnCPUThread() instead of touching it directly from the WebSocket
handler thread. cpu.runUntil didn't have an explicit "must be stepping"
guard to begin with; since the CPU-thread queue is now drained unconditionally
at the top of every Core_RunLoopUntil() iteration (not just while stepping),
queuing from it is safe regardless of current core state.
Also corrects a stale comment on Core_RunOnCPUThread() left over from before
the drain point moved from Core_ProcessStepping() to the top of
Core_RunLoopUntil() - it's not limited to the stepping/paused case.
Replaced remaining `auto` locals in this file with concrete types.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hqm11k99viLfbJm2MkH4BH
The WebSocket debugger's cpu.stepInto handler ran entirely on the WebSocket
handler thread, directly manipulating breakpoints and stepping state (via
Core_RequestCPUStep, g_breakpoints.SetSkipFirst, etc.) that's otherwise only
ever touched from the CPU thread (the one that calls Core_RunLoopUntil, and
thus indirectly NativeFrame).
Adds Core_RunOnCPUThread() - queues a function to run on the CPU thread and
blocks the caller until it's done. The queue is drained at the top of
Core_RunLoopUntil()'s loop, so it's reached continuously (in a tight spin)
while the CPU is stepping/paused, and at least once per call even while fully
running.
cpu.stepInto is the first consumer: once the CPU is already stepping, the
breakpoint/stepping manipulation is now routed through Core_RunOnCPUThread
instead of happening directly on the WebSocket thread. The "not currently
stepping" path still calls Core_Break() directly from the WebSocket thread,
since it's already documented free-threaded and is what makes the CPU thread
start reaching the queue-drain point in the first place.
More WebSocket debugger commands can be converted the same way going forward.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hqm11k99viLfbJm2MkH4BH
DisassemblyManager used to fuse lui+addiu/load/store into single pseudo-
instructions ("li", fused loads/stores) for display. This only applied to a
handful of opcodes, complicated DisassemblyManager, and was the root cause of
a stepping bug: Core_PerformCPUStep's Into/Over cases treated stepSize as a
byte count, while the WebSocket cpu.stepInto handler computed it as an
instruction count (needed to step over a whole fused macro in one go) - so a
plain, non-fused stepInto silently executed zero instructions.
Removed the fusion logic entirely (DisassemblyMacro, DISTYPE_MACRO) - every
disassembly line is now exactly one 4-byte instruction. With that,
"how many instructions does this line span" is always 1, so the
getInstructionSizeAt() byte-size queries in the legacy Windows and ImGui
debuggers are gone too; step requests just pass 1. Core_RequestCPUStep's
stepSize is now consistently in instructions everywhere.
Also fixes the PPSSPPHeadless build, broken since 0ed1f3e added
OpenWebDebugger() (which calls System_LaunchUrl) without a headless stub.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hqm11k99viLfbJm2MkH4BH