Memory no longer holds the hooks, so the disassembly fetches the op the CPU runs, which is the
CallRepl pseudo-op at a hooked address. That lookup can come from the Win32 debugger's UI thread,
so the CPU thread now changes the hook map under a lock (and reads it without, being the only
writer).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The JITs used to find a block by overwriting its first instruction with an "emuhack" holding
the offset into the code space, and replacement functions installed their own emuhack the same
way. Anything that read code (savestates, the debugger, memory dumps, cheats, the function
hasher, games reading their own code) had to translate them back.
Blocks now live in a block shadow: one u32 per guest instruction, indexed by the PC masked with
BLOCK_SHADOW_MASK (1GB on 64-bit, folding the kernel and uncached mirrors; 256MB on 32-bit, iOS
and Switch, where address space is short), with the low bits cleared so a misaligned PC can't
read across two entries. Only the ranges under memory views are committed, so a bad PC still
faults in the dispatcher, and the shadow is reserved before the memory map so it can't land in
a gap of the 4GB window. Entries hold full 32-bit offsets, lifting the 16MB limit. The
dispatchers on every backend, and the IR interpreter's loop, look blocks up there.
A hooked instruction without a compiled block gets the JIT's hook value in its entry: the offset
of the dispatcher's compile path (1 for the interpreters, the IR arena keeping that offset
empty), so dispatching there compiles a block, and the frontends substitute the hook. Compiling
replaces the entry with the block's, and destroying the block puts the hook value back. The hook
value is rewritten when the JIT changes or regenerates its dispatcher.
Hooks live in a map keyed by the masked address, and are dropped when the game overwrites the
hooked instruction, or when an icache invalidation finds the function's hash (immediates masked,
as when matching it) changed. Those checks run when queued invalidations are processed, since
cheats and the debugger queue them before writing. Installing and removing hooks invalidates
deferred, as it happens in HLE calls that can run inside a block. The cache instruction goes
through MIPSState, so it drops changed hooks too.
With clean memory, the save/clear/restore around savestates, memory reads and dumps goes away,
as do the source-range invalidations in the memcpy replacements, sceDmac and the cheats.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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
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
Potentially, a watch or break condition could crash if it was unaligned
between mirrors. This might happen if it's not the condition you wanted,
especially. Play it safe.
Now that it uses a lookup, this is even more dangerous. But, the maps
could be reordered while it's trying to print the pointer and cause that
data to become invalid.
This should be safe from race conditions.