Commit Graph
54 Commits
Author SHA1 Message Date
Henrik RydgårdandClaude Opus 5 daa18fc25a ImDebugger: fix stale symbol list after a game is reloaded
The disasm window cached the flattened symbol list and only rebuilt it when one
of three menu items said so. Nothing marked it dirty when a game booted or
exited, and a new SymbolMap is allocated per boot, so the list kept showing the
previous game's functions.

Give SymbolMap a version counter that every mutator bumps, and let the window
compare against it instead. The counter is process-wide rather than per-map, so
a fresh map can't hand out a version a cached copy already holds.

Also re-find the selected symbol by address after a rebuild (the index means
something else afterwards), and drop the unused symbol cache members in
ImMemWindow.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfY7iFJEjmRXf1XGrTs4MF
2026-08-27 10:43:08 +02:00
Henrik RydgårdandClaude Opus 5 82dd963df6 ImDebugger: sort the symbol list by name
The symbol map hands symbols over in address order, which is fine for the
disassembly but meaningless to browse - CrossCraft has 3734 functions and
scrolling for one is just a wall of text.

Sorted where you suggested, in the match list that already backs the filter:
it's rebuilt only when the filter text or the symbol map changes, so this costs
nothing per frame, and sorting the indices rather than symCache_ itself leaves
selectedSymbol_ pointing at the cache, so a selection survives filtering and the
Edit Symbol box above the list keeps working.

Case-insensitive, with ties broken by address so the order can't wobble between
rebuilds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-18 14:26:09 +02:00
Henrik RydgårdandClaude Opus 5 3f0c2da5a9 Debugger: surface source line info in six more places
Following on from the DWARF line table: the lookup was only reachable from
hle.backtrace, the breakpoint hit object and the ImGui disassembly status bar.
Now also in

- the ImDebugger call stack (new Source column),
- the Win32 call stack (new Source column),
- the Win32 disassembly status bar, matching the ImGui one,
- the ImDisasmView right-click menu, which showed a bare address as its heading
  and now leads with "mesh.zig:163 (08841f98)" when there's a line for it,
- breakpoint log lines - a log-only breakpoint's entire output is those lines,
  and "BKP PC=08841f98 mesh.zig:163" reads a great deal better than an address
  when you're scanning a few thousand of them,
- crash stack traces, via FormatStackTrace, which is what the crash screen and
  crash reporting both use.

That last one is where it earns its keep, and it needed the invalid-jump path to
produce a stack trace at all - it was the one exec exception that didn't. It's
also the one that most deserves it: the address it jumped to tells you nothing,
the callers tell you everything. Execution has already moved to the bad address
by the time it's noticed, so a walk from pc finds no function to start from;
WalkCurrentStack takes an explicit starting pc now, and falling back to ra
recovers the chain. Reproducing the original CrossCraft bug:

  CPU Jump: Invalid jump to ae870000 from PC ae870000(invalid) RA 08841f98
  MIPS call stack:
  rendering.mesh.Mesh(PspVertex).draw at mesh.zig:163 (08841c30+368, ...)
  state.MenuState.draw at MenuState.zig:821 (0883ab90+414, ...)
  engine.Engine.stepFrameInternal at State.zig:40 (08820f74+5164, ...)
  utils.module._module_main_thread at engine.zig:468 (088272c4+2fb8, ...)

Fixed a pre-existing double-report while in there: every case in
Core_ExecException sent its message and then fell through to an unconditional
send of the same message, so each exec exception was logged twice. The message
is built in the switch and sent once at the end now.

pspautotests 314/314, UnitTest 55/55.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-18 13:42:14 +02:00
Henrik RydgårdandClaude Opus 5 c5e7c4890a Debugger: decode DWARF line info, and show source locations
Homebrew commonly ships its unstripped ELF next to the EBOOT, which is already
how the symbol loader turns z_un_08841f98 into a function name. That same ELF
carries a DWARF .debug_line section, so the addresses can be mapped to source
files and lines too - and a backtrace stops being four hex numbers:

  08841f98  move sp,fp          mesh.zig:163
  0883afa4  li v0,0x0           MenuState.zig:821
  088260d8  andi at,v0,0xFFFF   State.zig:40
  0882a27c  andi at,v0,0xFFFF   engine.zig:468

Surfaced in three places: per frame in hle.backtrace, in the "hit" object that
cpu.breakpoint.hit and cpu.stepping share, and appended to the disassembly
window's status bar. The breakpoint case keys on the pc rather than the address,
since for a memory breakpoint the useful source location is the instruction that
did the access, not the data it touched.

Storage is a plain sorted table of absolute addresses per module. SymbolMap
keeps module-relative addresses because its .ppsym files are meant to be
reloaded by a different game that pulls in the same module; none of this is ever
written anywhere - it's regenerated from the ELF each boot - so there'd be
nothing for relative addresses to buy. Each module owns its own rows and file
names outright and is keyed the way SymbolMap::UnloadModule is, so unloading one
module drops its lines and nobody else's.

The subtle part is end-of-sequence markers. Without them a lookup for an address
in a gap - a compilation unit built without debug info - confidently reports the
last line of an unrelated file. A prototype run over one test binary
mis-attributed 70 of its 349 functions that way, so sequence ends are recorded
as rows with line 0 and a lookup landing on one reports nothing instead.

DWARF 2 through 4 are decoded (psp-gcc emits 2, Zig 4). Version 5 re-encoded the
file table, so those units are skipped with a warning rather than mis-parsed -
nothing targeting the PSP produces it today.

Scope, since it's narrower than it sounds: PRX conversion strips every .debug
section. I checked all 437 pspautotests .prx and CrossCraft's own app.prx -
none have any. Of 24 installed homebrew EBOOTs, zero carry debug info; CrossCraft
only does because it ships app.elf separately. So this helps someone developing
homebrew, and does nothing at all for a commercial game.

Costs about 1.2 MB for a large Zig binary (98383 rows, 438 files) and nothing
for anything without debug info. Follows bAutoSaveLoadSymbols like the symbols
do.

pspautotests 314/314, UnitTest 55/55, CoreUWP builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-18 13:42:14 +02:00
Henrik RydgårdandClaude Opus 5 ebd2b4757f ImDebugger: prefill the assemble box with the instruction that's there
Assembling started from an empty box, so replacing an instruction meant reading
it off the screen and retyping it, and tweaking one operand meant typing the
whole thing. It now opens with the current instruction already in it, selected,
so typing replaces it and editing is just editing.

Disassembled without symbol substitution for this, unlike what the view itself
shows: a branch displayed as a function name doesn't assemble back, and the
whole point of the prefill is that it's valid input.

Selection is only applied on this path. onChar() also seeds the box - with the
character the user just typed over an instruction - and there the text is the
start of what they're writing, not something to overwrite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-18 13:42:14 +02:00
Henrik RydgårdandClaude Opus 5 a0fbf32469 ImDebugger: add a filter box above the symbol list
A game with symbols loaded puts a few thousand functions in that list, and the
only way to reach one was to scroll. Typing part of a name now narrows it,
case-insensitively.

The filter produces a list of indices into the symbol cache rather than a
filtered copy of it, so selectedSymbol_ keeps meaning the same thing whether or
not a filter is active - the Edit Symbol box above the list needs it to index
the cache. Rebuilt when the filter text changes or the symbol map reloads, not
per frame, and the list clipper works off the match list so a filter that
matches everything costs no more to draw than before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-18 13:42:13 +02:00
Henrik RydgårdandClaude Opus 5 d8a1808b3d ImDebugger: add "Run to here, next frame", gated on the flip count
Prototype of the frame-gated run-to-cursor idea. "Run to here" stops at the
first hit, which isn't what you want for an address hit many times per frame -
you end up stepping through the rest of the current frame to reach the state
you actually care about.

Built on machinery that was already there rather than a new stepping mode: the
one-shot breakpoint behind run-to-cursor already takes a condition (step-into
uses it to pin a step to one thread), and a hit that fails the condition leaves
it armed for the next one. So "the next frame" is just a condition that isn't
true yet - here "flipcount > <now>".

Counting presented frames rather than vblanks matters for a game that doesn't
render at the full refresh rate: at 30fps there are two vblanks per frame, so a
vblank-based condition would let you through halfway into the frame you were
trying to skip. The flip side is that the counter only advances when the
framebuffer actually changed, so if the game has stopped drawing - or is wedged
in the loop you're trying to debug - this never trips and the core keeps
running.

Both counters are exposed to the expression parser, next to
threadid/moduleid/usec/ticks, so they're usable in ordinary breakpoint
conditions and cpu.evaluate too, not just from this menu item: "flipcount" for
presented frames and "vcount" for the PSP's own vblank counter, which is what
sceDisplayGetVcount returns and is the one a game's own timing is written
against.

Verified with a headless session: across a second of emulated time flipcount
went 120 -> 172 and vcount 119 -> 172 (a game rendering every vblank, so they
track).

pspautotests 314/314, UnitTest 55/55.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-18 10:59:08 +02:00
Henrik RydgårdandClaude Opus 5 665cd9808a ImDebugger: bring back assembleOpcode, with an ImGui popup for the input
The disassembly view's assembler has been commented out since the ImGui
debugger was written, because it asked for the opcode with InputBox_GetString,
which is Win32-only. Replaced with a small popup built from ImGui, so it works
everywhere the ImGui debugger does.

Splitting it in two is what the popup costs: assembleOpcode() only records the
address and seed text and raises a flag, since ImGui popups have to be opened
and drawn inside the frame that owns them, and applyAssembly() does the work
when the input is submitted. The flag is consumed in PopupMenu(), next to the
existing rename-function popup, which had already established the pattern. The
new state lives in ImDisasmView.

Behaviour follows the Win32 version, including "register=expression" assigning
a register rather than assembling, and falling through to the assembler when
the left side isn't a register name. Two differences, both deliberate:

- Errors appear inside the popup instead of a modal message box, and the popup
  stays open with the text still in it, so a typo can be corrected instead of
  retyped.
- No Core_RunOnCPUThread() around the register write or the assemble. The Win32
  debugger needs it because its dialogs are pumped by the WinMain message loop,
  a genuinely different thread; UI/ImDebugger always runs on the same thread as
  Core_RunLoopUntil(), so it can touch this state directly (see AGENTS.md).

Also updated for the current APIs while it was dead: MipsAssembleOpcode() takes
an out-parameter for the error now rather than MIPSAsm::GetAssembleError(), and
expression evaluation goes through initExpression()/parseExpression().

The core has to be stepping, checked both when the popup is requested and again
on submit - the popup is modeless, so the core can be resumed while it's open.

The keyboard shortcut (A) is re-enabled along with the context menu item.
onChar(), which seeds the popup with the character typed over an instruction,
already called this and needed no change - it still has no caller of its own,
which is a separate pre-existing gap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-18 10:59:08 +02:00
Henrik RydgårdandClaude Opus 5 35a91b757a Move the temporary breakpoint out of the user's breakpoint list
step-over, step-out and run-until plant a one-shot breakpoint at the address
they want execution to return to. Keeping it in breakPoints_ alongside the
user's own meant the two kept colliding:

- Adding a log-only user breakpoint at the same address hijacked the temporary
  one. AddBreakPoint() didn't match across temp-ness so both existed, and then
  ChangeBreakPoint() looked up "the first enabled breakpoint at this address" -
  a log-only breakpoint isn't enabled, so the temporary one won and had its
  action overwritten to log-only. It lost PAUSE and the step never came back.
- RemoveBreakPoint() erased up to two entries per address to catch an
  overlapping temporary one, so deleting either deleted both - including the
  interpreter's cleanup path in CheckExecBreakpoints() taking the user's
  breakpoint with it.
- ExecBreakPoint() handled one breakpoint per address, so with both at the same
  address only one of them did anything: the step completed but the user's log
  line never printed.
- Nothing dropped it when something *else* stopped us first, so an interrupted
  step left a breakpoint armed at an address nobody was waiting for anymore,
  which later fired as a phantom stop.

It's a single TempBreakPoint member now, invisible to the breakpoint lists and
untouched by user edits. One is enough: step over/out and cross-thread step into
all require the CPU to already be stepping and resume it immediately, so only
one can be in flight, and run-until now replaces rather than stacking (two
pending run-untils had no coherent meaning, and the loser stayed armed).

Behavior follows what other debuggers do. Both breakpoints at an address are
evaluated independently and their actions combine, so a log-only breakpoint
logs without stopping and still lets the step finish. Core_Break() drops the
temporary breakpoint on any stop, whatever the reason - the same way gdb deletes
its step-resume breakpoint and lldb discards the thread plan.

Two things to be careful of, both covered by the new TempBreakpoints test:
HasBreakPoints() has to account for it, or the interpreter's checked run loop
and the JIT skip breakpoint checking entirely and a step with no user
breakpoints set never returns; and IsAddressBreakPoint() (user-facing, for the
lists and disassembly markers) is now separate from NeedsBreakCheckAt() (what
the JIT frontends and interpreter ask), since only the latter should see it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-17 00:29:26 +02:00
Henrik Rydgård 14405c08cf Remove the concept of stepSize from the debugger 2026-08-16 16:59:54 +02:00
Henrik Rydgård 9f90512ef6 Make instruction cache invalidation (for us, jit cache invalidation) clearer 2026-08-16 16:59:52 +02:00
Henrik Rydgård f5bd302694 Improve DescribeAddress, show the description of the currently selected line in disassembly 2026-08-14 14:38:33 +02:00
Henrik Rydgård 0596ee97f6 More memory access cleanup 2026-08-11 20:14:01 +02:00
Henrik Rydgård 4f8859d0ec Centralize a disassembly utility function between the debuggers 2026-08-11 09:08:52 +02:00
Henrik Rydgård 2be4d995f2 More Read_U32 cleanup 2026-08-10 11:23:23 +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ård 330a638281 Add ability to copy function hash from the disassembly view
See #4179
2026-02-09 19:26:43 +01:00
Henrik Rydgård c20be71c10 Fix the API for MIPSAssembleOpcode to remove a global 2025-11-03 16:08:07 +01:00
Henrik Rydgård 1c49ad7b29 Support screen rotation on iOS (unrestricted) 2025-10-22 15:13:21 +02:00
Henrik Rydgård 5751e202a5 Remove redundant .c_str() 2025-08-31 13:37:43 +02:00
Henrik Rydgård bc688d83b1 ImDebugger: Replace the Skim button by adding hold-shift functionality to "Into"
That is, you can hold shift to repeatedly click the button.
2025-06-12 10:23:00 +02:00
Henrik Rydgård ff37cbe184 Minor memory safety fixes 2025-05-14 09:39:09 +02:00
Henrik Rydgård a97ea04827 ImGui: Add "repeatshift" buttons that auto-repeat if you hold shift. 2025-04-09 11:58:31 +02:00
Henrik Rydgård ad17ec5fc8 Minor reorganization in ImDebugger 2025-04-09 11:58:31 +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 23954f5403 Implement the disassembly mode too 2025-01-09 13:35:29 +01:00
Henrik Rydgård 08d251b5bc Add a memory dump window 2025-01-09 13:14:13 +01:00
Henrik Rydgård 3e198c53b2 More include cleanup 2024-12-18 13:57:26 +01:00
Henrik Rydgård 3cc7d6ef7a ImDebugger: Assorted UI improvements 2024-12-13 22:06:56 +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 c85266359f More memory view work 2024-12-12 17:47:37 +01:00
Henrik Rydgård 39ffe92e0a LR->RA rename, fixes 2024-12-12 17:47:37 +01:00
Henrik Rydgård 11e858f0f1 Add input to memview. Use step counters to control updates. 2024-12-12 17:47:37 +01:00
Henrik Rydgård 20c19f96e0 More memview work 2024-12-12 17:47:37 +01:00
Henrik Rydgård 637d15434e Minor code cleanup. Add Goto LR button 2024-12-10 22:56:03 +01:00
Henrik Rydgård 474e7acf54 Hook up the new (bare-bones) ImGui debug window to GPU stepping, fix stepping. 2024-12-05 00:51:59 +01:00
Henrik Rydgård 7f13bc1a34 Add new log category for texture replacements 2024-12-01 14:17:01 +01:00
Henrik Rydgård f665878b3c Fix crash with mismatched pushfont/popfont 2024-11-28 09:50:29 +01:00
Henrik Rydgård 1eadea2589 Show the filesystem types and origins in the list (except ISO) 2024-11-26 00:36:58 +01:00
Henrik Rydgård e95d9b15b4 Add kernel object viewer, reimpl "Run to here" 2024-11-26 00:13:37 +01:00
Henrik Rydgård 28f1a7d45d Slightly better right-click behavior in disasm view 2024-11-25 23:52:31 +01:00
Henrik Rydgård 9801f4c810 Add "Rename function" functionality. Fix some input and stepping bugs. 2024-11-25 10:18:56 +01:00
Henrik Rydgård 7992ff4627 Make CBreakpoints an object 2024-11-25 00:22:53 +01:00
Henrik Rydgård e31636caec Still use a fixed-width font for the disassembly (might change this) 2024-11-25 00:03:06 +01:00
Henrik Rydgård 2a1cda05b0 ImGui: Fix DPI scale, disable unneeded logging 2024-11-24 23:41:27 +01:00
Henrik Rydgård e66516eb50 ImDebugger: Add ability to rename function symbols 2024-11-23 23:23:52 +01:00
Henrik Rydgård 6b11ebfb9f ImDebugger: Fix a bunch of keyboard shortcuts 2024-11-23 23:23:52 +01:00
Henrik Rydgård c5eb600044 Add search functionality to disassembler 2024-11-23 23:23:52 +01:00
Henrik Rydgård 194b2ce076 Ge Debugger: Add a framebuffer listing, along with some plumbing 2024-11-23 23:23:48 +01:00
Henrik Rydgård 80474fd0f0 Some cosmetic fixes to the ImGui debugger 2024-11-07 20:11:23 +01:00