Skip to content

Commit 576df88

Browse files
sp00ktoberclaude
andauthored
feat(ppsspp): patched PPSSPP v1.20.4, its build, and the headless container (#4)
* feat(ppsspp): patched PPSSPP v1.20.4, its build, and the headless container Five patches against v1.20.4 (GPL-2.0-or-later): savestate commands that answer when done, the ARM JIT VFPU memcheck offset, and backports of the log to every connection, the stepping reason and game.speed.get/set. ppsspp/build.sh builds it on macOS or Linux; the Docker image runs the same script. ppsspp-debug takes stop reasons and cpu.breakpoint.hit where PPSSPP sends them, speaks game.speed.get/set, accepts a new stop as the answer to resume(), and DockerEmulator captures the container's display. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ci: no image build; anyone can build it locally Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 0a4b5c0 commit 576df88

30 files changed

Lines changed: 1509 additions & 124 deletions

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ __pycache__/
55
.ruff_cache/
66
build/
77
dist/
8+
.env
89
*.gen.h
910
*.gen.lua
1011

‎.pre-commit-config.yaml‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ repos:
88
- id: check-toml
99
- id: check-yaml
1010
- id: end-of-file-fixer
11+
exclude: \.patch$ # a blank line of context is one space
1112
- id: trailing-whitespace
13+
exclude: \.patch$ # a blank line of context is one space
1214

1315
- repo: https://github.com/astral-sh/ruff-pre-commit
1416
rev: v0.16.9
@@ -17,6 +19,11 @@ repos:
1719
args: [--fix]
1820
- id: ruff-format
1921

22+
- repo: https://github.com/shellcheck-py/shellcheck-py
23+
rev: v0.11.0.1
24+
hooks:
25+
- id: shellcheck
26+
2027
- repo: https://github.com/fsfe/reuse-tool
2128
rev: v6.2.0
2229
hooks:

‎LICENSES/GPL-2.0-or-later.txt‎

Lines changed: 117 additions & 0 deletions
Large diffs are not rendered by default.

‎README.md‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ release ULES01213), and for porting monsters to it from Monster Hunter Portable
1818
| `packages/` | Python libraries: the debugger client, the file formats, the game (`mhfu`, which also holds the address map every language is generated from), the porter |
1919
| `framework/` | The PRX plugin and its Lua host |
2020
| `apps/` | The studio (monster and map editor), the HUD, the Blender extension |
21+
| `ppsspp/` | PPSSPP patched for automation, its build, and a headless container to run it in |
2122
| `examples/` | Example mods |
2223
| `docs/` | Guides and format references |
2324

@@ -33,13 +34,14 @@ uv run pytest
3334

3435
Tests that read game files take them from `MHFU_DATA` and `MHP3RD_DATA`, each a directory of
3536
extracted `DATA.BIN` files, and skip when the variable is unset; tests against a running PPSSPP
36-
take `PPSSPP_BINARY` and `PPSSPP_GAME` the same way. Nothing from the games is committed here:
37-
no ISO, no extracted files, no memory dumps.
37+
take `PPSSPP_BINARY` and `PPSSPP_GAME` the same way (`ppsspp/build.sh` builds the patched one).
38+
Nothing from the games is committed here: no ISO, no extracted files, no memory dumps.
3839

3940
## Licence
4041

4142
MIT, except the file-format library and everything that bundles it, which are GPL-3.0-or-later
42-
because they contain code derived from [mhff](https://github.com/svanheulen/mhff). Every file's
43-
licence is machine-readable ([REUSE](https://reuse.software/); check with `reuse lint`).
43+
because they contain code derived from [mhff](https://github.com/svanheulen/mhff), and the
44+
PPSSPP patches, which are GPL-2.0-or-later like PPSSPP. Every file's licence is machine-readable
45+
([REUSE](https://reuse.software/); check with `reuse lint`).
4446

4547
Not affiliated with or endorsed by Capcom. Monster Hunter is a trademark of Capcom Co., Ltd.

‎REUSE.toml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,9 @@ version = 1
55
path = "**"
66
SPDX-FileCopyrightText = "2026 sp00ktober"
77
SPDX-License-Identifier = "MIT"
8+
9+
# patches to PPSSPP carry its licence; the last matching annotation wins
10+
[[annotations]]
11+
path = "ppsspp/patches/**"
12+
SPDX-FileCopyrightText = ["2026 sp00ktober", "PPSSPP Project"]
13+
SPDX-License-Identifier = "GPL-2.0-or-later"

‎packages/ppsspp-debug/README.md‎

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ PPSSPP opens the debugger at startup only with `RemoteDebuggerOnStartup = True`
3636
| PPSSPP (1.20.4) | The client |
3737
|---|---|
3838
| Sends its log only to the newest debugger connection, and stops it for all when any connection closes | Log streams get a connection of their own, opened when the stream starts |
39-
| Does not say why the CPU stopped | Matches a breakpoint stop by its pc; counts as a watchpoint hit any stop this client did not ask for |
40-
| Answers `cpu.stepping` and `cpu.resume` with a broadcast, not a reply; pausing a paused CPU answers nothing | `pause()` and `resume()` wait for the broadcast, and are fine to call twice |
39+
| Does not say why the CPU stopped | Matches a breakpoint stop by its pc, and counts as a watchpoint hit any stop this client did not ask for; uses the reason where PPSSPP gives one |
40+
| Reports a hit of a breakpoint that does not stop only as a log line | Parses it; takes `cpu.breakpoint.hit` events instead from a PPSSPP that sends them |
41+
| Answers `cpu.stepping` and `cpu.resume` with a broadcast, not a reply; pausing a paused CPU answers nothing | `pause()` and `resume()` wait for the broadcast, and are fine to call twice; `resume()` also takes a new stop as its answer |
4142
| Stops and restarts the CPU for every memory request while the game runs (about 10 ms each) | `paused()` makes each request about 0.3 ms; one `read()` of a whole struct costs one stop |
4243
| Keeps breakpoints armed after the script that set them has died | `breakpoint()` and `watchpoint()` remove theirs when the block ends, however it ends, and let the game run if a hit arrived that nobody took |
4344
| Reports errors as events, and an unknown event as an error | Raises `DebuggerError`, or `Unsupported` for an event this PPSSPP does not have |
@@ -58,14 +59,16 @@ with LocalEmulator("/path/to/PPSSPPSDL", "/path/to/game.iso") as emu:
5859
ppsspp.wait_for_game()
5960
```
6061

61-
`DockerEmulator(container)` drives PPSSPP in the modkit's headless container through its
62-
`ppsspp-ctl`, and relaunches it when it dies right after reporting ready. A `state=` launch
63-
skips the cold boot, so PRX plugins do not load.
62+
`DockerEmulator()` drives PPSSPP in the modkit's headless container
63+
([`ppsspp/`](../../ppsspp)) through its `ppsspp-ctl`, relaunches it when it dies right after
64+
reporting ready, and captures its display with `screenshot()`. A `state=` launch skips the cold
65+
boot, so PRX plugins do not load.
6466

6567
## Patched PPSSPP
6668

67-
`save_state()`, `load_state()` and `speed()` need the PPSSPP patches that ship with the modkit;
68-
stock PPSSPP raises `Unsupported`.
69+
The modkit's PPSSPP build ([`ppsspp/`](../../ppsspp)) fixes the first two rows of the table.
70+
`save_state()` and `load_state()` need it; `speed()` and `set_speed()` need it or a PPSSPP newer
71+
than 1.20.4. Stock 1.20.4 raises `Unsupported` for them.
6972

7073
`screenshot()` depends on the GPU backend: PPSSPP 1.20.4 with Vulkan answers "Could not
7174
download output" whether the game runs or is stopped.
@@ -75,9 +78,15 @@ download output" whether the game runs or is stopped.
7578
`ppsspp_debug.testing.FakePPSSPP` is a debugger server with PPSSPP's quirks, for testing code
7679
that uses this client without an emulator.
7780

81+
`FakePPSSPP(patched=True)` behaves like the modkit's build, `FakePPSSPP(hit_events=True)` like a
82+
PPSSPP that sends `cpu.breakpoint.hit`.
83+
7884
The live tests run against a real PPSSPP when `PPSSPP_BINARY` and `PPSSPP_GAME` are set, and
7985
print what they measure with `-s`:
8086

8187
```bash
8288
PPSSPP_BINARY=/path/to/PPSSPPSDL PPSSPP_GAME=/path/to/game.iso uv run pytest packages/ppsspp-debug -s
8389
```
90+
91+
`PPSSPP_CONTAINER=ppsspp` in place of `PPSSPP_BINARY` runs them in the modkit's container, with
92+
`PPSSPP_GAME` a path inside it.

‎packages/ppsspp-debug/src/ppsspp_debug/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
RawEvent,
3030
Resumed,
3131
Screenshot,
32+
Speed,
3233
Stepping,
3334
Watchpoint,
3435
parse_hit,
@@ -55,6 +56,7 @@
5556
"RawEvent",
5657
"Resumed",
5758
"Screenshot",
59+
"Speed",
5860
"Stepping",
5961
"Stream",
6062
"SyncStream",

0 commit comments

Comments
 (0)