Add docs/WebSocketDebugger.md covering the transport, message protocol, broadcast/request event catalog, how to enable it, LAN discovery, and how the bundled JS web debugger (assets/debugger submodule) connects to it. Point AGENTS.md at it so future sessions know it exists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XDNwPPuidmNxQGRJxBuRL6
9.2 KiB
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 (../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/debuggerrequests.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- sharedDebuggerRequesthelper (parameter parsing, response/error helpers) andDebuggerSubscriberbase class.Common/Net/WebsocketServer.h/.cpp- the low-level WebSocket framing.
Transport
- Runs on the same port as Remote ISO sharing (
g_Config.iRemoteISOPort;0means "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/debuggerwithout 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::DEBUGGERis 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:
{ "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:
{ "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:
{ "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):
{ "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=trueinppsspp.ini(g_Config.bRemoteDebuggerOnStartup) starts it automatically on launch (UI/NativeApp.cpp). - Command line (application build only):
--debugger- added inCore/CmdLine.cpp/.has anApplication-only auto-param, so it forcesbRemoteDebuggerOnStartupon 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=PORTparsed directly inheadless/Headless.cpp(not throughCore/CmdLine.cpp's shared parser). It also forcesstartBreak = trueso 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--debuggerflag intentionally does not apply toCmdLineMode::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://<ip>:<port>/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 byRegisterServer()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 thewebsocket-clientpip package).Tools/wsdbg/- a small Rust CLI/REPL client for this session's work (seeTools/wsdbg/README.md): connects, does theversionhandshake, 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.