Commit Graph
13 Commits
Author SHA1 Message Date
Henrik RydgårdandClaude Opus 5 59eb8a9a81 Report emulated time in cpu.status, and note headless/wsdbg traps in AGENTS.md
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
2026-08-17 18:51:54 +02:00
Henrik RydgårdandClaude Opus 5 05f5668dfe Save symbols outside any module to a per-game file, and only save real names
Module symbols are keyed by module+crc so they're shared by every game that
loads that module. But symbols the user attaches to addresses that aren't in
any module - the heap, the stack, scratchpad, a hardware register, typically
after a memory.search - describe one game's own memory layout and are worthless
to any other game. Those now go to PSP/SYSTEM/SYMBOLS/<gameID>_syms.ppsym.

They're module index 0 ("absolute"), which already round-trips through the
existing per-module code: GetModuleRelativeAddr/GetModuleAbsoluteAddr are
identity for it, so the file format is unchanged, just with absolute addresses.
SaveModuleSymbols only needed to stop requiring a ModuleEntry. Auto-load/save
hang off CPU_Init/CPU_Shutdown rather than module load/unload, gated on the same
bAutoSaveLoadSymbols setting - and deliberately not on SYSPROP_HAS_DEBUGGER,
which only the Windows port reports true for, so LoadSymbolsIfSupported next to
it does nothing at all on headless. hle.game.saveSymbols/loadSymbols expose it.

Four things found while doing it:

- Symbols outside any module were being dropped on the floor. AddFunction/
  AddData/AddLabel take moduleIndex -1 as "work it out", pass it to
  GetModuleIndex(), and store whatever comes back - but that returns -1 when no
  module contains the address, and -1 is never an active module, so the symbol
  never reached the active maps: invisible to every lookup and to any save.
  hle.data.add had spotted this and normalized -1 to 0 locally; nothing else
  did, so e.g. hle.func.add outside a module silently did nothing. Fixed
  centrally in a new ResolveModuleIndex() the three of them share.
  (This only became reachable with the GetModuleIndex() fix in 29a38af37e -
  before that it returned a wrong-but-valid module index instead.)

- The saved files were almost entirely noise. Every function the analyzer finds
  is named z_un_<addr> and every import stub zz_<name>, both regenerated from
  scratch on each load. One real module wrote 13KB - 443 unnamed functions and
  64 stub names - for the four names a human had actually chosen. Worse, on the
  next run those were loaded back as authoritative and would beat the module's
  own symbols to the address. Now only names that aren't regenerated get saved,
  and a module with none writes no file at all (and removes a stale one, so
  deleting a symbol sticks). That module's file went 13020 -> 81 bytes.

- LoadModuleSymbols trusted the addresses in the file. It's meant to be
  hand-edited and can outlive the build it came from, so relative addresses past
  the end of the module are now skipped with a warning instead of landing at
  nonsense addresses.

- AddFunction and AddData both erased the map entry they were updating and then
  read back through the now-dangling iterator to refresh the active copy. Only
  latent: the refresh is guarded on the active copy's module matching the new
  one, which is false exactly when the erase happens. Re-point the iterator at
  the entry's new home instead, so it can't rot if that guard ever changes.
  AddLabel already did the equivalent correctly, via a local copy.

Filename sanitizing goes through SanitizeString with a new FileName restriction
rather than being open-coded; unlike the existing restrictions it substitutes
'_' instead of dropping, so two module names can't collapse onto one file.

Verified end to end on headless with cpu_alu.prx: named a function inside the
module and data/functions in scratchpad and the heap, let it exit, checked both
files, rebooted and confirmed all of it came back at the right addresses.
Unit tests 51/51, pspautotests 314/314.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-17 16:53:48 +02:00
Henrik RydgårdandClaude Sonnet 5 83b1d13c88 Debugger: add kernel object introspection over the WebSocket API
New events, all read-only (never mutate kernel state, no cleanup/sort calls
- see HLEKernelObjectSubscriber.cpp's header comment):

- hle.object.list: every live kernel object of every type at once (uid,
  type, name, one-line quickInfo), with an optional 'type' filter. Uses
  KernelObjectPool::IterateAll(), a new type-agnostic sibling of the
  existing Iterate<T>().
- hle.eventflag.list/info, hle.mutex.list/info, hle.semaphore.list/info,
  hle.msgpipe.list/info, hle.callback.list/info: per-type full detail
  (all Native* status struct fields plus waiting-thread lists), reading
  straight off the classes exposed in the previous commit.

Also adds JsonWriter::DictScope/ArrayScope (Common/Data/Format/JSONWriter.h)
- RAII push/pop for pushDict()/pushArray(), used throughout the new
  handlers. A forgotten or early-returned pop() previously just produced
  silently malformed JSON; with 11 new handlers each writing a handful of
  nested arrays/dicts, that seemed worth fixing at the API level rather
  than trusting every call site to pair things up by hand. Existing
  handlers are untouched - this is purely additive.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-17 16:01:23 +02:00
Henrik RydgårdandClaude Sonnet 5 ce33c964f7 Debugger: add log.channels.list/log.channel.set over the WebSocket API
Lets a client query all log categories (Common/Log.h's Log enum) with their
current level/enabled state, and change a channel's level and/or enabled
state at runtime - same data LogConfigScreen already exposes in the UI, now
reachable from the debugger protocol. Levels are named strings (notice,
error, warning, info, debug, verbose) rather than the 1-6 numbers the
existing passive 'log' event uses (LogBroadcaster.cpp, left unchanged) -
clearer for a config-style API where you're not scanning a stream.

Registered as a new subscriber alongside the others in WebSocket.cpp, and
wired into all the build systems that need a new source file (CMake, the
Windows and UWP vcxprojs, Android.mk - libretro's Makefile.common doesn't
build any WebSocket debugger files so needs no entry).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-17 16:01:23 +02:00
Henrik RydgårdandClaude Sonnet 5 29a38af37e Per-module symbol save/load, module identity via crc, GetModuleIndex fix
SymbolMap:
- Fix GetModuleIndex(): it only checked the end of an active module's range
  (via activeModuleEnds.upper_bound), never the start, so an address sitting
  in the gap before a module was silently misattributed to it. Added
  GetModuleIndexByName() as a companion lookup.
- AddModule() gains an optional crc param, stored per ModuleEntry. Reactivating
  a module by name now also requires the crc to agree when both sides know it,
  so two unrelated binaries that happen to share a name no longer get merged
  into one symbol table (addresses the old TODO at the top of SymbolMap.h).
- AddLabel()/AddFunction() gain an updateName param (default false, preserving
  existing "first writer wins" behavior) so a trusted source - like a loaded
  symbol file - can be allowed to overwrite a name that a lower-confidence
  automatic pass already assigned.
- New SaveModuleSymbols()/LoadModuleSymbols()/GetModuleSymbolsPath(): save or
  restore one module's functions/data/labels to/from a small human-editable
  text file, addressed relative to the module (so the file stays valid however
  the module ends up positioned on a later run). Keyed by
  PSP/SYSTEM/SYMBOLS/<moduleName>_<crc>.ppsym - deliberately by module+crc
  rather than by game, so it's shared by every game/homebrew that loads the
  exact same module. A "# game <id> <title>" comment records who last saved
  it, informational only.

WebSocket debugger: hle.module.saveSymbols/loadSymbols expose the above.

sceKernelModule.cpp: auto-load a module's saved symbols right after it's
registered with the symbol map (both the real ELF-load path and the
savestate-load path), and auto-save on unload (before UnloadModule(), while
its symbols are still active) - gated behind the new bAutoSaveLoadSymbols
config setting (default off), with a matching Developer Tools checkbox and
a --auto-save-load-symbols command-line override for headless use.

Includes some in-progress cleanup already staged: DescribeAddress now calls
g_symbolMap->GetDescription() directly instead of through the now-removed
MIPSDebugInterface::getDescription() wrapper.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-17 16:01:23 +02:00
Henrik Rydgård 9b9083d3e5 Fix for headless port problem by Claude 2026-08-16 23:09:12 +02:00
Henrik RydgårdandClaude Sonnet 5 a502b55fea Debugger: add memory.disasm compact mode and memory.searchDisasm findAll
Two real gaps hit repeatedly while investigating the VSH boot path (see
docs/VSHBootInvestigation.md):

- memory.disasm's response is the full per-field JSON (type, address,
  addressSize, encoding, macroEncoding, backgroundColor, name, params,
  symbol, function, dataSymbol, breakpoint, isCurrentPC, branch,
  relevantData, conditionMet, dataAccess - ~15 fields per line). Reading
  disassembly by hand meant writing a throwaway script each time to reduce
  this down to "ADDR: name params" - and at least once, a bug in one of
  those scripts produced misleading output that wasn't caught immediately.
  Added compact=true: returns "lines" as an array of plain strings
  ("M AAAAAAAA  [symbol: ]name params", M = '>' for current PC, '*'/'o'
  for an enabled/disabled breakpoint) instead, computed once correctly
  here instead of ad hoc every time.

- memory.searchDisasm already existed but only ever returned the first
  match - genuinely limiting for "find every caller of this address"
  call-graph-style queries, which came up directly while trying to trace
  which function builds VSH's GE display list. Added findAll=true: scans
  the whole range and returns every match in a new "addresses" array
  (capped at 1000), instead of stopping at the first. Default behavior
  (address: first match or null) is unchanged for existing callers.

Verified live via PPSSPPHeadless + wsdbg: compact mode against a real
demo ELF's entry point produces clean, correctly-marked text lines;
findAll=true against the same range found all 11 jal instructions instead
of just the first. 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 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 3eee14b02b Clean up the AI-generated documentation manually, AGENTS.md updates 2026-07-27 10:55:54 +02:00
Henrik RydgårdandClaude Opus 5 333ae20092 Update docs for the unified --debugger=PORT flag
Reflect the CmdLine.cpp unification and the headless net::Init() fix in
docs/WebSocketDebugger.md and AGENTS.md - previously these described the
app-only boolean --debugger flag and flagged headless as unreliable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XDNwPPuidmNxQGRJxBuRL6
2026-07-26 21:52:48 +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årdandClaude Opus 5 2fa8efad2b Document the WebSocket debugger interface
Add docs/WebSocketDebugger.md covering the transport, message protocol,
broadcast/request event catalog, how to enable it, LAN discovery, and how
the bundled JS web debugger (assets/debugger submodule) connects to it.
Point AGENTS.md at it so future sessions know it exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XDNwPPuidmNxQGRJxBuRL6
2026-07-26 21:01:28 +02:00