The tick basis was actually sound - CoreTiming::GetTicks() is continuous across
Advance(), so "ticks unchanged" really does mean "no instruction retired since",
which is exactly the window the suppression needs. The plumbing around it was
the problem:
- ExecRegBreakpoint() applied the skip only to the pause, so stepping off a
log+pause register breakpoint printed it again and counted a second hit. The
check now sits at the top of ExecBreakPoint(), ExecMemCheck(), ExecOpMemCheck()
and ExecRegBreakpoint() instead of being repeated at seven call sites across
the interpreter and four JIT frontends, where one of them had it wrong and
another checked a different address than the rest.
- Address 0 doubled as "nothing to skip" (ClearSkipFirst() existed but was dead
code; the JITs cleared by calling SetSkipFirst(0)), so a breakpoint at 0 would
have been permanently suppressed. There's an explicit valid flag now, and
ClearSkipFirst() is what clears it.
- The marker was set from five places and never cleared when execution stopped,
so one could outlive the resume that armed it. Core_Break() clears it now, and
the two WebSocket subscribers that set it immediately before asking for a
step - which sets it again itself - no longer do.
- SetSkipFirst() now only arms when some breakpoint machinery actually exists,
so a stale marker can't sit around waiting to swallow a breakpoint added later.
CheckSkipFirst() returning an address (compared against pc by each caller) is
replaced by ShouldSkipBreakpoint(addr), which compares against both addr and
currentMIPS->pc - under a JIT those differ, and only some callers knew that.
Covered by the Breakpoints unit test, including that a suppressed breakpoint
neither logs nor counts a hit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
cpu.status only reported raw CPU ticks, which a client can't turn into a time:
the PSP's clock frequency is changeable and games do change it, so the
ticks-per-second ratio isn't fixed over a run. CrossCraft Classic runs at
333MHz, so assuming the default 222MHz reads 9.1s where the truth is 6.3s -
enough to put scripted input injection in the wrong place entirely.
Adds "us" (emulated microseconds) and "clockHz" alongside "ticks".
CoreTiming::GetGlobalTimeUs() can't be used directly for this: it rebases its
own internal counters as a side effect, and cpu.status is deliberately served
straight from the WebSocket thread rather than queued to the CPU thread (it's
meant to be cheap and frequently pollable). So PeekGlobalTimeUs() computes the
same value without the rebasing.
AGENTS.md picks up the things that cost time while driving headless over the
websocket API: --sync silently desynchronises on raw JSON lines because only
wsdbg's key=value shorthand gets a ticket; headless reports HAS_DEBUGGER as
false so anything gated on it silently does nothing there; the memstick is
hardcoded next to the executable; a leftover headless process turns a build
into an LNK1168 that looks like a compile error; wrapping the launcher in
`timeout` kills the emulator along with it, losing the crash you stopped at;
response field names aren't uniform (value vs uintValue); broadcast.config.set
rejects two of the four keys the docs list.
Also writes down the ELF-as-oracle technique that cracked the relocation bug -
when homebrew ships app.elf next to app.prx, the pre-link ELF still has the
symbols and the relocation symbol indices the PRX format discards, so the
loader's output can be checked exhaustively offline instead of by re-running.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
CPUCoreSubscriber through the CPU thread
Continues the pattern started with SteppingSubscriber: route breakpoint,
disassembly, symbol, and register access through Core_RunOnCPUThread()
instead of touching that state directly from the WebSocket handler thread.
Two intentional exceptions, matching the reasoning already used for
cpu.stepInto's "not currently stepping" branch:
- cpu.stepping's Core_Break() call stays unqueued - it's what makes the CPU
thread start reaching the queue drain point in the first place.
- cpu.status stays unqueued - it's meant to be a cheap, frequently-pollable
status check, and its "pc" field is already documented as inaccurate unless
stepping. Matches how SteppingBroadcaster already reads the same state
directly from the WebSocket thread.
Where a handler's response doesn't depend on anything the queued lambda
computed (plain add/remove endpoints), moved req.Respond() back out after
the Core_RunOnCPUThread() call for readability - the JSON building and
socket write happen later in Finish() regardless of where Respond() is
called, so there's no thread-safety difference either way, just clarity
about what actually needs to run on the CPU thread.
memory.searchDisasm carries the same caveat flagged for memory.search: its
scan range has no size cap, so if the CPU is stepping, a very large range
will now block the CPU thread's own frame pump for the scan's duration
rather than running unqueued on the WebSocket thread as before. Not fixed
here - noted in a comment at the call site.
Replaced remaining `auto` locals in these three files with concrete types.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hqm11k99viLfbJm2MkH4BH