diff --git a/AGENTS.md b/AGENTS.md index 30ebcf3771..c5ea0eca88 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -69,6 +69,18 @@ small examples to copy from). A module is a `const HLEFunction []` table o build-tested here, so double check them by hand against how an existing neighboring file (e.g. `sceVaudio.cpp`) is listed in each. +## WebSocket debugger + +PPSSPP has a JSON/WebSocket debugger and automation API (connect, read/write memory, set breakpoints, step the CPU, +read GPU state, inject input, tail logs, etc.), served on the same port as Remote ISO sharing at `/debugger` with +subprotocol `debugger.ppsspp.org`. Implementation is in `Core/Debugger/WebSocket.cpp` and +`Core/Debugger/WebSocket/*Subscriber.cpp` (one file per feature area, each documented at the top). Enable it via +Settings > Tools > Developer Tools > "Allow remote debugger", `RemoteDebuggerOnStartup` in the config, or (application +build only, not headless) the `--debugger` command line flag. The bundled web GUI at `/debugger/` comes from the +`assets/debugger` submodule (`unknownbrackets/ppsspp-debugger`, `bundled` branch). For the full protocol reference and +event catalog, see `docs/WebSocketDebugger.md`. A standalone CLI client for talking to it directly lives in +`Tools/wsdbg/`. + ## Quick rebuild on Linux You don't need to do ./b.sh --debug to verify every single little change, instead use this shortcut: diff --git a/docs/WebSocketDebugger.md b/docs/WebSocketDebugger.md new file mode 100644 index 0000000000..a50dbf0d13 --- /dev/null +++ b/docs/WebSocketDebugger.md @@ -0,0 +1,180 @@ +# PPSSPP WebSocket Debugger + +PPSSPP has a JSON/WebSocket-based debugger and automation API, served from the +same HTTP server used for "Remote ISO" disc sharing and file upload. It lets +an external tool (a script, a web page, another editor/IDE) inspect and +control a running emulation session: read/write memory, set breakpoints, +step the CPU, read GE/GPU state, send fake input, tail the log, etc. + +This doc is a local reference; a version of it is planned to also live on the +[PPSSPP website](https://www.ppsspp.org/) (`../ppsspp-site` next to this repo). + +## Where the code lives + +- `Core/WebServer.cpp` / `Core/WebServer.h` - the shared HTTP server (also + used for Remote ISO and file upload). It owns the listening socket and + dispatches `/debugger` requests. +- `Core/Debugger/WebSocket.cpp` - upgrades the HTTP request to a WebSocket and + runs the per-connection event loop (`HandleDebuggerRequest`). +- `Core/Debugger/WebSocket/*.cpp/.h` - one "subscriber" or "broadcaster" per + feature area (CPU, memory, GPU, HLE, input, breakpoints, ...). Each file's + top comment documents its events in detail - this doc gives the overview + and an index into those files. +- `Core/Debugger/WebSocket/WebSocketUtils.h` - shared `DebuggerRequest` + helper (parameter parsing, response/error helpers) and `DebuggerSubscriber` + base class. +- `Common/Net/WebsocketServer.h/.cpp` - the low-level WebSocket framing. + +## Transport + +- Runs on the same port as Remote ISO sharing (`g_Config.iRemoteISOPort`; `0` + means "pick a free port automatically" - the actual bound port is written + back to that config value and logged: `Listening on port N`). +- URL path: `/debugger`. +- WebSocket subprotocol: `debugger.ppsspp.org` (required - a plain HTTP GET + to `/debugger` without a websocket Upgrade just redirects to the bundled + web UI at `/debugger/index.html`). +- Messages are JSON, both directions, always shaped as `{"event": "NAME", ...}`. +- One WebSocket connection = one client; PPSSPP does not limit the number of + simultaneous debugger connections. +- The debugger only actually does anything while `WebServerFlags::DEBUGGER` + is enabled (see "Enabling it" below) - the HTTP server itself may also be + running for other reasons (Remote ISO, upload) without the debugger being + reachable. + +## Message protocol + +Requests you send: +```json +{ "event": "cpu.status" } +``` +Optionally include a `"ticket"` field (any JSON value) - PPSSPP echoes it +back verbatim in the response/error, so you can correlate requests and +responses when firing several at once. `Tools/wsdbg` (see below) assigns an +incrementing integer ticket automatically. + +Responses use the *same* event name as the request: +```json +{ "event": "cpu.status", "ticket": 1, ... } +``` +Responses are not always immediate - some handlers respond asynchronously +(see `DebuggerRequest::Finish()` in `WebSocketUtils.h`/`.cpp`). + +Errors look like this: +```json +{ "event": "error", "message": "...", "level": 2, "ticket": 1 } +``` +`level` is a `LogLevel` (1=NOTICE, 2=ERROR, 3=WARN, 4=INFO, 5=DEBUG, 6=VERBOSE). + +PPSSPP also sends unsolicited ("broadcast") events with no request - see +below. + +By convention, send a `version` event right after connecting (see +`WebSocket/GameSubscriber.cpp`): +```json +{ "event": "version", "name": "my-tool", "version": "1.0" } +``` +PPSSPP responds with its own name/version, and remembers yours (currently +just for internal bookkeeping/future logging). + +## Broadcast (unsolicited) events + +Sent without you asking, whenever the underlying state changes: + +| Event | Sent when | Source | +|---|---|---| +| `log` | A new log line is emitted | `LogBroadcaster.cpp` | +| `game.start` | A game finishes booting | `GameBroadcaster.cpp` | +| `game.quit` | The game is closed/reset | `GameBroadcaster.cpp` | +| `game.pause` / `game.resume` | User opens/leaves the pause menu | `GameBroadcaster.cpp` | +| `cpu.stepping` | CPU enters a stepping/break state | `SteppingBroadcaster.cpp` | +| `cpu.resume` | CPU resumes from stepping | `SteppingBroadcaster.cpp` | +| `input.buttons` | Any emulated button changes state | `InputBroadcaster.cpp` | +| `input.analog` | An analog stick position changes | `InputBroadcaster.cpp` | + +A client can opt out of specific broadcast categories with +`broadcast.config.set` (`{"disallowed": {"logger": true, "game": true, "stepping": true, "input": true}}`), +see `ClientConfigSubscriber.cpp`. `gpu.stats.feed` (see below) works the same +way for periodic GPU stats. + +## Request/response event catalog + +Full details (parameters, response shape) are documented as comments above +each handler in the corresponding `Core/Debugger/WebSocket/*Subscriber.cpp` +file - this is just an index. + +| Category | Events | File | +|---|---|---| +| Game/version | `game.reset`, `game.status`, `version` | `GameSubscriber.cpp` | +| CPU core | `cpu.stepping`, `cpu.resume`, `cpu.status`, `cpu.getAllRegs`, `cpu.getReg`, `cpu.setReg`, `cpu.evaluate` | `CPUCoreSubscriber.cpp` | +| Stepping | `cpu.stepInto`, `cpu.stepOver`, `cpu.stepOut`, `cpu.runUntil`, `cpu.nextHLE` | `SteppingSubscriber.cpp` | +| Breakpoints | `cpu.breakpoint.add/update/remove/list`, `memory.breakpoint.add/update/remove/list` | `BreakpointSubscriber.cpp` | +| Memory read/write | `memory.read_u8/u16/u32`, `memory.read`, `memory.readString`, `memory.write_u8/u16/u32`, `memory.write` | `MemorySubscriber.cpp` | +| Memory info/annotations | `memory.mapping`, `memory.info.config/set/list/search` | `MemoryInfoSubscriber.cpp` | +| Disassembly | `memory.base`, `memory.disasm`, `memory.searchDisasm`, `memory.assemble` | `DisasmSubscriber.cpp` | +| HLE | `hle.thread.list/wake/stop`, `hle.func.list/add/remove/removeRange/rename/scan`, `hle.module.list`, `hle.backtrace` | `HLESubscriber.cpp` | +| GPU stats | `gpu.stats.get`, `gpu.stats.feed` | `GPUStatsSubscriber.cpp` | +| GPU recording | `gpu.record.dump` | `GPURecordSubscriber.cpp` | +| GPU buffers | `gpu.buffer.screenshot`, `gpu.buffer.renderColor/renderDepth/renderStencil`, `gpu.buffer.texture`, `gpu.buffer.clut` | `GPUBufferSubscriber.cpp` | +| Input injection | `input.buttons.send`, `input.buttons.press`, `input.analog.send` | `InputSubscriber.cpp` | +| Replay | `replay.begin/abort/flush/execute/status`, `replay.time.get/set` | `ReplaySubscriber.cpp` | +| Client config | `broadcast.config.get/set` | `ClientConfigSubscriber.cpp` | + +## Enabling it + +- **UI**: Settings > Tools > Developer Tools > "Allow remote debugger" + checkbox (`UI/DeveloperToolsScreen.cpp`). The "Local Server Port" slider on + the Networking screen sets the port (shared with Remote ISO sharing; `0` = + auto-pick). +- **Config**: `RemoteDebuggerOnStartup=true` in `ppsspp.ini` + (`g_Config.bRemoteDebuggerOnStartup`) starts it automatically on launch + (`UI/NativeApp.cpp`). +- **Command line** (application build only): `--debugger` - added in + `Core/CmdLine.cpp`/`.h` as an `Application`-only auto-param, so it forces + `bRemoteDebuggerOnStartup` on for that run without persisting it to the + config file. It's deliberately *not* available in the headless build + (`CmdLineMode::Headless`) - see caveat below. +- **Headless build**: has its own, separate, pre-existing mechanism - + `--debugger=PORT` parsed directly in `headless/Headless.cpp` (not through + `Core/CmdLine.cpp`'s shared parser). It also forces `startBreak = true` so + the CPU halts before running anything. **This is currently known not to + work properly** - treat it as unreliable/needs investigation before + depending on it, which is also why the new `--debugger` flag intentionally + does not apply to `CmdLineMode::Headless`. + +## Discovery + +For LAN auto-discovery (mainly used by the mobile Remote Play/debugger +flow), the server periodically reports its `(local ip, port)` to +`report.ppsspp.org/match/update` (see `RegisterServer()` in +`Core/WebServer.cpp`). Clients can query `report.ppsspp.org/match/list` to +get a list of candidate endpoints on the same network and try connecting to +each in turn. + +## The bundled web-based JS debugger + +`assets/debugger/` is a git submodule +(`https://github.com/unknownbrackets/ppsspp-debugger.git`, `bundled` branch - +see `.gitmodules`) containing a prebuilt React app. PPSSPP serves it directly +at `/debugger/` (`Core/WebServer.cpp`'s `HandleFallback`/`ServeAssetFile`), +so opening `http://:/debugger/` in a browser gets you a full GUI +debugger for free. The actual editable source lives in a different branch of +that same repo (the `bundled` branch only holds the built output that gets +checked in here). + +From reading the minified bundle (`assets/debugger/static/js/main.*.js`), +it connects like this: +- Manual connect: `new WebSocket("ws://ip:port/debugger", "debugger.ppsspp.org")`. +- Auto connect: `fetch("//report.ppsspp.org/match/list")` for a list of + `{ip, p}` candidates (as registered by `RegisterServer()` above), then + tries each with the same WebSocket call until one succeeds. + +## Talking to it yourself + +- `scripts/websocket-test.py` - old minimal Python one-shot script (needs the + `websocket-client` pip package). +- `Tools/wsdbg/` - a small Rust CLI/REPL client for this session's work (see + `Tools/wsdbg/README.md`): connects, does the `version` handshake, and lets + you fire off events by hand or from a one-shot command line, printing + responses and broadcasts as they arrive. Built and smoke-tested against a + live PPSSPP instance while writing this doc.