From c5ba9bead0dccc0a0972e6d6593467545fc2bc17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 12 Oct 2026 09:57:36 +0200 Subject: [PATCH] docs: How to find and hash a game function for ReplaceTables Co-Authored-By: Claude Opus 5.5 (1M context) --- AGENTS.md | 2 +- docs/debugging.md | 59 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 4ea446a975..aab3a1fe60 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,7 +13,7 @@ for it: |---|---| | [docs/building.md](docs/building.md) | Build commands for every target (VS/MSBuild, CMake, UWP, legacy Android NDK, libretro), unit tests, pspautotests | | [docs/android-device-testing.md](docs/android-device-testing.md) | Running a build on a real Android device or a Quest over adb: installing the build you think you are, forcing activity recreates, measuring retained activities, VR | -| [docs/debugging.md](docs/debugging.md) | Driving the WebSocket debugger and PPSSPPHeadless from a script, measuring a commercial game with headless, comparing binaries' speed (`Tools/headless_bench.py`), breakpoint reliability per CPU backend, debugging a game that works on hardware | +| [docs/debugging.md](docs/debugging.md) | Driving the WebSocket debugger and PPSSPPHeadless from a script, measuring a commercial game with headless, comparing binaries' speed (`Tools/headless_bench.py`), breakpoint reliability per CPU backend, debugging a game that works on hardware, finding and hashing a game function (like its own `memcpy`) for `ReplaceTables` when hardware backends miss CPU writes to VRAM | | [docs/DebuggerThreading.md](docs/DebuggerThreading.md) | `Core_RunOnCPUThread` / `g_frameMutex` / shutdown-lock rules - required reading before touching debugger code | | [docs/HLEModules.md](docs/HLEModules.md) | Adding an HLE module or function, and the seven build files a new source file goes in | | [docs/translations.md](docs/translations.md) | Translating UI strings with Tools/langtool | diff --git a/docs/debugging.md b/docs/debugging.md index 53742585fc..15310fec19 100644 --- a/docs/debugging.md +++ b/docs/debugging.md @@ -314,3 +314,62 @@ relocations, and immediately showed that the first fix attempt scored worse than Reach for that before trying to reason a fix out of a disassembly. A few dozen lines of Python over the ELF beats re-running the game. +## Finding a game function to replace by hash + +The tell is a game that's right on the software renderer and stale on the hardware backends, usually a video +that sticks on its first frame while the audio plays on. The game is writing pixels into VRAM with the CPU, and +the hardware backends only see that when it goes through something we hook. Games ship their own `memcpy`, +`memset` and so on, and we recognise known ones by a hash of their code: the `hardcodedHashes` table in +`Core/MIPS/MIPSAnalyst.cpp` names them, and `Core/HLE/ReplaceTables.cpp` swaps in a replacement +(`Replace_memcpy` hands VRAM copies to `gpu->PerformMemoryCopy`). SSX On Tour's videos were fixed this way. + +Finding the function: + +1. **Get to the right moment.** Confirm the thing is actually happening before breaking on it - SSX's + videos only start 11.5s in, and a screenshot from earlier just shows the paper background. `memory.read` of + the decode buffer at a few `cpu.runUntilTime` points tells you when it starts changing. +2. **Break on the write**, under the interpreter (`-i`), since JIT memory breakpoints only work for constant + addresses: `memory.breakpoint.add address= size=4 write=true enabled=true`, then + `cpu.resume`. Pick a pixel in the middle of the frame, not the first one, which clears also hit. +3. **`hle.backtrace`** gives the function's entry address, and `hle.func.list` its size. Addresses come back + as decimal in JSON; convert carefully (`145311076` is `08a94564`, not `08ab1564`). +4. **`memory.disasm` the whole function** and check that it really is what you're going to call it: argument + order (`a0` dest, `a1` src, `a2` size for memcpy), and no side effects the replacement wouldn't have. A + replacement that doesn't match the original is a much worse bug than the one it fixes. + +Steps 2 and 3 as one script (the SSX run, so 12s in and a pixel at 240,136 of each 8888 stride-512 +framebuffer). Parameters take hex as well as decimal; replies are always decimal: + +``` +cpu.runUntilTime us=12000000 +memory.breakpoint.add address=0x040443c0 size=4 write=true enabled=true +memory.breakpoint.add address=0x040cc3c0 size=4 write=true enabled=true +cpu.resume +hle.backtrace +:quit +``` + +```bash +Tools/wsdbg/target/release/wsdbg.exe --sync --compact --quiet --launch \ + Windows/x64/Release/PPSSPPHeadless.exe GAME.iso --memstick=memstick -i --graphics=vulkan \ + --timeout-wall=240 < script.txt 2>/dev/null | grep -v '^<- log' +``` + +The first `hle.backtrace` frame is the writer: `entry` is the function's start, `code` the store that hit. +Once a replacement is in place, the same script shows it working: the hit's `source` reads `GPUMemcpy/...`, +its `pc` is the function's entry, and the backtrace starts at the caller. +Follow up with `memory.disasm address= count=64` in the same session (it needs the code loaded). + +Getting the hash: `HashFunctions()` in `MIPSAnalyst.cpp` takes CityHash64 over the function's instructions +with their immediates masked out, so it doesn't depend on where the function was loaded. The fastest way to +read it is a temporary line there, then revert it: + +```cpp +if (f.start == 0x08a94500) printf("TMPHASH %016llx size %d\n", (unsigned long long)f.hash, (int)f.size); +``` + +Add the entry to `hardcodedHashes` in hash order, with a comment naming the game. Then check that it took +effect: with `--log --loglevel=4` the loader prints `Replaced memcpy at 08a94500 with hash 3966eef670a93781`. +Check other regions of the game too. Since the hash doesn't depend on address, they usually match the same +entry (SSX's Korean release has the function at `08a94200`). +