Commit Graph
190 Commits
Author SHA1 Message Date
Henrik RydgårdandClaude Sonnet 5 3edea91c8e Debugger: track hit counts on address breakpoints, expose via cpu.breakpoint.list
BreakPoint (cpu.breakpoint.*) had no hit-count tracking at all, unlike
MemCheck (memory.breakpoint.*), which already tracks numHits. This made it
genuinely hard to tell "this breakpoint is never being reached" apart from
"it's being reached but I'm not seeing the log/pause where I'm looking" -
directly informed by repeatedly hitting exactly that ambiguity while
debugging the VSH boot path this session (see docs/VSHBootInvestigation.md).

Added BreakPoint::numHits, incremented in BreakpointManager::ExecBreakPoint()
whenever a breakpoint's address is hit and any condition passes (matching
MemCheck::Apply()'s existing semantics - counts real triggers, not just
"execution passed through here"). Exposed as a new "hits" field in
cpu.breakpoint.list's response.

Verified live via PPSSPPHeadless + wsdbg: hits reads 0 before the CPU
resumes, 1 after the breakpoint fires once. UnitTest.exe all: 49/49 passed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-14 11:04:32 +02:00
Henrik RydgårdandClaude Sonnet 5 4389f706b9 Debugger: fix a real race in cpu.stepOut/stepOver/runUntil/nextHLE from a delay slot
PrepareResume() used Core_RequestCPUStep(CPUStepType::Into, 1) to step past a
delay slot instruction before deciding whether to add a breakpoint and call
Core_Resume() - but Core_RequestCPUStep() only queues that step for
Core_ProcessStepping() to perform later (on the next iteration of the normal
stepping-mode loop). Every caller (Into's cross-thread branch, Over, Out,
RunUntil, HLE) immediately inspected currentMIPS->pc/inDelaySlot right after
PrepareResume() returned to decide what to do next - reading stale,
pre-step state, since the queued step hadn't run yet.

Worse: those callers then call Core_Resume(), which sets coreState back to
CORE_RUNNING_CPU. Core_ProcessStepping() only processes g_cpuStepCommand
when coreState is CORE_STEPPING_CPU/STEPPING_GE/RUNNING_GE, so once resumed,
the queued step is never processed at all - not just late, silently dropped,
leaving g_cpuStepCommand permanently set until the next Core_Break() resets
it. Any cpu.step*/cpu.runUntil request a client issues in that window (CPU
resumed running, breakpoint not yet hit again) hits
Core_RequestCPUStep()'s "Can't submit two steps in one host frame" guard and
is silently ignored, since none of these call sites check its return value -
this is the "step-out sometimes just doesn't do anything" flakiness reported
against this file.

PrepareResume() is only ever called from within a Core_RunOnCPUThread()
callback, so it's always already running on the CPU thread - safe to
single-step synchronously (currentMIPS->SingleStep(), matching how
Core_PerformCPUStep()'s own CPUStepType::Into case does it) instead of
queuing an async request whose completion every caller then assumes without
verifying.

Verified via UnitTest.exe all (49/49). Attempted to force a live repro via
wsdbg against a delay-slot jal in a demo ELF; wasn't able to reliably
trigger the failure window externally (by the time a client's next command
arrives, the CPU has typically already reached its next breakpoint and
Core_Break() has cleaned up the stale state first) - the race window is
real per the code trace above but appears to be narrow enough that it
mainly shows up under real usage timing (a slow-to-reach next breakpoint,
or a fast follow-up command from a script/UI), not simple synchronous
scripting. The fix is unconditionally more correct regardless: it replaces
a fire-and-forget async request every caller immediately assumed had
already completed with a direct synchronous call that actually has by the
time the next line runs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-14 11:04:32 +02:00
Henrik RydgårdandClaude Sonnet 5 db2d248b4a Rename GPRBreakpoint/gprBreakpoint to RegBreakpoint/regBreakpoint
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
2026-08-13 16:09:51 +02:00
Henrik RydgårdandClaude Sonnet 5 75174af77b 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-13 11:35:29 +02:00
Henrik Rydgård e9a3449ede More MIPSState * plumbing (manual) 2026-08-12 14:02:19 +02:00
Henrik Rydgård 3bd41376da More memory cleanup 2026-08-11 20:12:10 +02:00
Henrik Rydgård a3bf3c7153 Cut down on Claude's excesses 2026-08-11 15:43:48 +02:00
Henrik Rydgård 6929a0d9ae GPUBufferSubscriber: bound stackWidth and validate texture level
gpu.buffer.*'s "uri" output type let a client supply an arbitrary
stackWidth with no upper bound, used as the starting divisor in a loop
that decrements until it evenly divides the buffer's actual (small)
pixel count - a client sending a huge stackWidth (up to ~2 billion)
stalls the connection's handler thread for that many iterations.
Clamp it to the actual pixel count first.

gpu.buffer.texture's level parameter was forwarded as-is (u32) into
GPU_GetCurrentTexture(), which takes a plain int - a client-supplied
value whose u32->int conversion is negative skips backends' "level >=
mip count" bounds check (which only fires for level > 0), reaching
backend texture-copy code with a bogus mip index. Reject it upfront.
2026-08-11 15:40:30 +02:00
Henrik Rydgård 06522e91a0 BreakpointSubscriber: apply the same overflow check to breakpoint removal
WebSocketMemoryBreakpointParams::Parse() (used by add/update) checks
for address + size wrapping around before computing the end address,
but memory.breakpoint.remove computed it inline without that check.
Apply the same check for consistency - a crafted size could otherwise
wrap the computed end below address, causing RemoveMemCheck to operate
on an unintended range.
2026-08-11 15:40:30 +02:00
Henrik Rydgård d8edeb7649 Interpreter: Add correct alignment checks to loads/stores, cleanup 2026-08-11 10:28:48 +02:00
Henrik Rydgård 66c8bfbcb2 Improve semantics 2026-08-10 10:11:24 +02:00
Henrik Rydgård 8cd7e1b2c0 Delete all support for Qt
Our Qt backend has long been left behind and doesn't even support Vulkan
currently. There would be a lot of work to make it viable, and I don't
think anyone is really interested.

ImGui on SDL will soon fulfill the need for a more classic user interface
with a menu bar on Linux, and on Mac we already have a native UI.
2026-08-08 18:18:28 +02:00
Henrik RydgårdandClaude Sonnet 5 03dcfd3931 WebSocket debugger: fail fast on invalid memory ranges, move base64 off the CPU thread
memory.read_u8/u16/u32/read/readString/write_u8/u16/u32/write/search all
validated their address/size parameters (and, for search, the rest of its
param parsing) after already queuing onto the CPU thread. None of that
depends on CPU-thread-owned state, so do it upfront instead and fail fast
without a round trip through the queue for requests we already know are
invalid.

Also, for memory.read and memory.readString, only the raw memory copy
(which needs replacements/emuhacks disabled) now happens on the CPU
thread - the base64 encoding itself happens back on the WebSocket thread
afterward, so a large read no longer blocks the CPU thread's frame pump
for the encoding work too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hqm11k99viLfbJm2MkH4BH
2026-08-08 17:22:12 +02:00
Henrik RydgårdandClaude Sonnet 5 212ed7cdda Debugger: Route HLESubscriber through the CPU thread
Same treatment as the other subscribers: thread list/wake/stop, function and
data symbol list/add/remove/rename/scan, and backtrace now route their
kernel thread, symbol map, and disassembly manager access through
Core_RunOnCPUThread() instead of touching that state directly from the
WebSocket handler thread.

Memory::IsValidRange() checks that only depend on the request's own
address/size params (not on anything CPU-thread-owned) stay outside the
queued callback and fail fast, rather than making a pointless round trip
through the CPU thread for a request already known to be invalid.

hle.func.scan carries the same unbounded-range caveat already noted for
memory.search: no cap on 'size' beyond valid memory range.

Replaced remaining `auto` locals with concrete types.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hqm11k99viLfbJm2MkH4BH
2026-08-08 12:40:18 +02:00
Henrik RydgårdandClaude Sonnet 5 75b9df8384 Debugger: Route MemorySubscriber through the CPU thread, drop the forced-pause dance
Every memory.read*/write*/search endpoint used to call LockMemoryAndCPU(),
which - unless the CPU was already stepping - busy-waited for coreState to
settle, force-paused a running game with Core_Break(), and blocked on
Core_WaitInactive() before touching memory, just to get exclusive access
from the WebSocket handler thread. It also took MIPSComp::jitLock around
saving/restoring emuhack ops for the same reason.

Now the whole body of each handler runs inside Core_RunOnCPUThread(), so
none of that is needed for CPU-thread exclusivity: reads/writes happen
inline on the CPU thread itself, whether the game is running or stepping,
without ever pausing it. Confirmed live that memory reads/search now
complete while coreState stays CORE_RUNNING_CPU throughout - no more
stepping flicker on every debugger memory poll.

Kept Memory::MemoryInitedLock (guards against Memory::Shutdown() racing in
from a different thread, e.g. the UI thread stopping the game - unrelated
to the WebSocket-thread-vs-CPU-thread problem) and MIPSComp::jitLock around
the emuhack save/restore (guards against a UI-triggered CPU core switch,
also a different thread than the one Core_RunOnCPUThread targets).

Same caveats as previous conversions: memory.read for a very large 'size'
now base64-encodes on the CPU thread itself, and memory.search still has no
size cap - both will now block the CPU thread's own frame pump for their
duration on a large enough request. Noted inline, not fixed here.

Replaced remaining `auto` locals with concrete types.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hqm11k99viLfbJm2MkH4BH
2026-08-08 12:15:04 +02:00
Henrik RydgårdandClaude Sonnet 5 c32df3d931 Debugger: Route BreakpointSubscriber, DisasmSubscriber, and most of
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
2026-08-08 11:56:32 +02:00
Henrik RydgårdandClaude Sonnet 5 29825c1e61 Debugger: Route the rest of SteppingSubscriber.cpp's handlers through the CPU thread
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
2026-08-08 11:32:01 +02:00
Henrik RydgårdandClaude Sonnet 5 c6fccefa49 Debugger: Route cpu.stepInto's stepping state through the CPU thread
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
2026-08-08 11:21:46 +02:00
Henrik RydgårdandClaude Sonnet 5 b1f0112cef Debugger: Remove opcode-fusion display and fix cpu step size units
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
2026-08-08 11:16:36 +02:00
Henrik RydgårdandClaude Opus 5 074c8ac523 Add memory.search and hle.data.* to the WebSocket debugger
Reverse-engineering workflows need to (1) find where an unknown value lives
in memory and (2) label what's found, neither of which the debugger API
could do before:

- memory.search (MemorySubscriber.cpp): Cheat-Engine-style scan of a memory
  range for a u8/u16/u32/float value, or a byte pattern with an optional
  wildcard mask.
- hle.data.list/add/remove/rename (HLESubscriber.cpp): manage ST_DATA
  symbols (structs, tables, buffers), mirroring the existing hle.func.*
  commands for functions. Needed a new SymbolMap::RemoveData, since only
  RemoveFunction existed - added following the same pattern.

Verified live against a running PPSSPP instance (game.status, cpu.stepping,
memory.search in u32/bytes/masked-bytes modes, and the full
add/list/rename/remove data-symbol lifecycle) via Tools/wsdbg.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XDNwPPuidmNxQGRJxBuRL6
2026-07-26 21:17:09 +02:00
Henrik Rydgård 5c340ccd09 Remove the concept of scaled/offset depth buffers. 2026-06-10 10:41:30 +02:00
Henrik Rydgård a317890c08 Remove the GPUDebugInterface class
Just a pointless extra layer in the class hiearchy, making it
unnecessarily hard to modify the interface.

Might as well hit GPUCommon directly.
2026-06-02 11:15:08 +02:00
Henrik Rydgård f60e27a9b7 Just some refactoring of the GPUStatistics struct, and more use of StringWriter 2026-05-29 14:40:31 +02:00
Henrik Rydgård 8dad5a5c57 Remove asserts in GPUBufferSubscriber
Fixes #21683
2026-05-19 11:09:53 +02:00
Nemoumbra 1297fdde1f Fix: vector overrun in GPU stats 2026-05-10 05:38:55 +03:00
Henrik Rydgård d6ebfed432 Fix some bugs and warnings from a pass of static analysis 2026-01-29 17:41:54 +01:00
Henrik Rydgård 5f7a937466 Rename ValidSize to ClampValidSizeAt 2025-12-30 20:31:07 +01:00
Henrik Rydgård c20be71c10 Fix the API for MIPSAssembleOpcode to remove a global 2025-11-03 16:08:07 +01:00
Silvris 6d5bf32796 initial tests 2025-09-19 02:40:13 -05:00
Henrik Rydgård 33b87578c5 Revive old alignment utility functions, give them better names. 2025-09-03 20:48:51 +02:00
Henrik Rydgård f2df336f79 Resolve #20746 by checking addresses harshly in MIPSAnalyst
By fixing up badly aligned addresses in HLESubscriber.cpp.

This should help eliminate any bad usage within PPSSPP itself, while
also keeping existing websocket code working.

Additionally, this makes some end addresses exclusive instead of
inclusive, which simplifies address math.
2025-09-03 20:32:19 +02:00
Henrik Rydgård 3b3257ad99 Fix crash in debug builds in gpu.stats.get 2025-08-26 10:24:30 +02:00
Henrik Rydgård a7b36ae0ae Websocket: Request resets asynchronously. sync won't work. 2025-08-26 10:12:43 +02:00
Henrik Rydgård 18ccef1bd4 Just some minor code modernization 2025-08-25 10:45:12 +02:00
Nemoumbra 846c265972 Fixed the docs + better wording 2025-08-10 17:44:38 +03:00
Henrik Rydgård cc3fbb22e4 Change some stat arrays from double to float (to save mem bandwidth) 2025-06-18 11:00:27 +02:00
Henrik Rydgård 088a02bfdb Cleanup a lot of the bootup state management. 2025-03-30 14:02:29 +02:00
Henrik Rydgård c1828f69b9 Add an option to compress .ppsym files when saving or not (was always on before). 2025-03-29 18:17:41 +01:00
Henrik Rydgård 31cf5771f4 Turn the break reason into an enum, fix some minor issues 2025-02-19 16:01:11 -06:00
Henrik Rydgård 4dd3621fa0 Remove some unnecessary <algorithm> includes. 2024-12-18 17:04:27 +01:00
Henrik Rydgård 3e198c53b2 More include cleanup 2024-12-18 13:57:26 +01:00
Henrik Rydgård 96cab3f267 Rework the log manager to be more robust 2024-12-16 12:33:58 +01:00
Henrik Rydgård 638607d29a Refactor: Make GPUCommon own the framedump "recorder". 2024-12-15 12:07:44 +01:00
Henrik Rydgård 597be1c9bc Stop pretending that DisassemblyManager isn't a singleton - it currently is. 2024-12-12 19:25:04 +01:00
Henrik Rydgård 20a17a0e8d Reorganize DebugInterface etc a bit.
KernelThreadDebugInterface no longer has a useless copy of a MIPSDebugInterface.
2024-12-12 18:54:46 +01:00
Henrik Rydgård 83af54950f Move more core-related stuff into Core.cpp/h 2024-12-08 11:54:58 +01:00
Henrik Rydgård ea43e07cce Move some stuff around, rename some stuff 2024-12-05 00:36:48 +01:00
Henrik Rydgård 3ffff831c7 Refactor LogManager (remove GetInstance()). In headless, add option to log to OutputDebugString on Windows. 2024-12-03 21:39:42 +01:00
Henrik Rydgård 96c4a10e8c Add two new core states, rename RUNNING to RUNNING_CPU and similar for stepping. 2024-12-01 21:04:21 +01:00
Henrik Rydgård 7992ff4627 Make CBreakpoints an object 2024-11-25 00:22:53 +01:00