mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-01 14:58:14 +00:00
wsdbg: Add :screenshot, and document nested key=value params
:screenshot saves gpu.buffer.screenshot as a PNG without dumping the data URI into the output. The docs claimed nested parameters need a raw JSON line, which gets no ticket; a single-quoted JSON value in key=value form works and keeps it. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
4504092d3d
commit
2a5febd634
3 files changed
+77
-14
No files matched your search
@@ -90,8 +90,10 @@ trips would otherwise hang the script forever, so it gives up after `--sync-time
|
||||
(default 30), reports it, and makes the run exit non-zero.
|
||||
|
||||
Matching is by ticket, always - `--sync` never waits for "whatever message arrives next", which is
|
||||
what used to quietly desynchronise a script. A raw JSON line (the only way to send nested
|
||||
parameters) is sent exactly as written, so it's waited for only if *you* gave it a `ticket`;
|
||||
what used to quietly desynchronise a script. So prefer the `key=value` form for nested parameters
|
||||
too: values are parsed as JSON, and single quotes keep the inner double quotes intact, as in
|
||||
`input.buttons.send buttons='{"cross":true}'`. A raw JSON line is sent exactly as written, so it's
|
||||
waited for only if *you* gave it a `ticket`;
|
||||
without one there is nothing to match and `--sync` moves straight on to the next line. Raw lines
|
||||
are rejected up front, rather than sent and left to fail somewhere downstream, if they aren't valid
|
||||
JSON, aren't an object, have no string `event`, or carry a `ticket` that isn't an integer.
|
||||
@@ -115,6 +117,7 @@ way took minutes per run. These run inside the one session instead:
|
||||
|---|---|
|
||||
| `:sleep <seconds>` | Wall-clock pause. Keeps draining and printing messages while it waits. |
|
||||
| `:wait <event> [timeout]` | Blocks until a message with that event name arrives. Exits non-zero if it never does. |
|
||||
| `:screenshot <file.png>` | Saves `gpu.buffer.screenshot` as a PNG, without printing the image data. Needs the CPU stopped. Use a native path (`C:/...`) on Windows. With headless, use `--graphics=software`: the Vulkan backend has no output image to read back there. |
|
||||
| `:echo <text>` | Prints text, for marking up a script's output. |
|
||||
| `# comment` | Ignored. |
|
||||
|
||||
|
||||
+63
-6
@@ -528,6 +528,8 @@ fn print_help() {
|
||||
println!("wsdbg - connected. Type an event name and optional key=value params, e.g.:");
|
||||
println!(" game.status");
|
||||
println!(" cpu.setReg thread=0 name=4 value=1000");
|
||||
println!("Values are parsed as JSON where possible. Single-quote nested ones, which keeps the ticket:");
|
||||
println!(" input.buttons.send buttons='{{\"cross\":true}}'");
|
||||
println!("Or paste a full JSON message starting with '{{' to send it verbatim.");
|
||||
println!("A numeric 'ticket' is auto-assigned to shorthand commands so you can match up responses.");
|
||||
println!(":help show this message");
|
||||
@@ -535,6 +537,7 @@ fn print_help() {
|
||||
println!(":snapshot <name> <addr> <size> memory.read into a locally-named byte buffer");
|
||||
println!(":snapshots list saved snapshots");
|
||||
println!(":diff <name1> <name2> byte-compare two snapshots");
|
||||
println!(":screenshot <file.png> save gpu.buffer.screenshot to a PNG file");
|
||||
println!(":sleep <seconds> pause, still printing anything that arrives");
|
||||
println!(":wait <event> [timeout] block until that event arrives (e.g. cpu.stepping)");
|
||||
println!(":echo <text> print text, for marking up a script's output");
|
||||
@@ -678,6 +681,7 @@ fn send_and_wait(
|
||||
event: &str,
|
||||
params: &[String],
|
||||
timeout_secs: f64,
|
||||
print_response: bool,
|
||||
) -> Result<serde_json::Value> {
|
||||
let ticket = next_ticket();
|
||||
let json_text = build_event_json(event, params, Some(ticket))?;
|
||||
@@ -688,11 +692,13 @@ fn send_and_wait(
|
||||
while Instant::now() < deadline {
|
||||
match socket.read() {
|
||||
Ok(Message::Text(text)) => {
|
||||
print_incoming(&text);
|
||||
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&text) {
|
||||
if v.get("ticket").and_then(|t| t.as_u64()) == Some(ticket) {
|
||||
return Ok(v);
|
||||
}
|
||||
let v = serde_json::from_str::<serde_json::Value>(&text).ok();
|
||||
let is_ours = v.as_ref().and_then(|v| v.get("ticket")).and_then(|t| t.as_u64()) == Some(ticket);
|
||||
if !is_ours || print_response {
|
||||
print_incoming(&text);
|
||||
}
|
||||
if is_ours {
|
||||
return Ok(v.unwrap());
|
||||
}
|
||||
}
|
||||
Ok(Message::Close(frame)) => return Err(anyhow!("connection closed by PPSSPP: {frame:?}")),
|
||||
@@ -724,7 +730,7 @@ fn cmd_snapshot(socket: &mut WebSocket<TcpStream>, snapshots: &mut Snapshots, ar
|
||||
}
|
||||
let name = args[0];
|
||||
let params = vec![format!("address={}", args[1]), format!("size={}", args[2])];
|
||||
match send_and_wait(socket, "memory.read", ¶ms, timeout_secs) {
|
||||
match send_and_wait(socket, "memory.read", ¶ms, timeout_secs, true) {
|
||||
Ok(resp) => {
|
||||
if resp.get("event").and_then(|e| e.as_str()) == Some("error") {
|
||||
let msg = resp.get("message").and_then(|m| m.as_str()).unwrap_or("unknown error");
|
||||
@@ -751,6 +757,51 @@ fn cmd_snapshot(socket: &mut WebSocket<TcpStream>, snapshots: &mut Snapshots, ar
|
||||
}
|
||||
}
|
||||
|
||||
// :screenshot <file.png> - saves what gpu.buffer.screenshot returns as a PNG file. The response
|
||||
// (a data: URI of the whole image) isn't printed, it would bury everything else in the output.
|
||||
fn cmd_screenshot(socket: &mut WebSocket<TcpStream>, args: &[&str], timeout_secs: f64) -> bool {
|
||||
if args.len() != 1 {
|
||||
eprintln!("! Usage: :screenshot <file.png>");
|
||||
return false;
|
||||
}
|
||||
let path = args[0];
|
||||
let resp = match send_and_wait(socket, "gpu.buffer.screenshot", &["type=uri".to_string()], timeout_secs, false) {
|
||||
Ok(resp) => resp,
|
||||
Err(e) => {
|
||||
eprintln!("! {e}");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
if resp.get("event").and_then(|e| e.as_str()) == Some("error") {
|
||||
let msg = resp.get("message").and_then(|m| m.as_str()).unwrap_or("unknown error");
|
||||
eprintln!("! gpu.buffer.screenshot failed: {msg}");
|
||||
return false;
|
||||
}
|
||||
let uri = resp.get("uri").and_then(|u| u.as_str()).unwrap_or("");
|
||||
let b64 = match uri.split_once(";base64,") {
|
||||
Some((_, b)) => b,
|
||||
None => {
|
||||
eprintln!("! Response had no base64 data: URI");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let bytes = match base64::engine::general_purpose::STANDARD.decode(b64) {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
eprintln!("! Could not decode the image: {e}");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
if let Err(e) = std::fs::write(path, &bytes) {
|
||||
eprintln!("! Could not write {path}: {e}");
|
||||
return false;
|
||||
}
|
||||
let w = resp.get("width").and_then(|w| w.as_u64()).unwrap_or(0);
|
||||
let h = resp.get("height").and_then(|h| h.as_u64()).unwrap_or(0);
|
||||
println!("screenshot saved: {path} ({w}x{h})");
|
||||
true
|
||||
}
|
||||
|
||||
// :snapshots - list what's been captured so far in this session.
|
||||
fn cmd_list_snapshots(snapshots: &Snapshots) {
|
||||
if snapshots.is_empty() {
|
||||
@@ -914,6 +965,12 @@ fn run_repl(mut socket: WebSocket<TcpStream>, sync: bool, sync_timeout: f64) ->
|
||||
cmd_snapshot(&mut socket, &mut snapshots, &args, sync_timeout);
|
||||
}
|
||||
Some(":snapshots") => cmd_list_snapshots(&snapshots),
|
||||
Some(":screenshot") => {
|
||||
let args: Vec<&str> = words.collect();
|
||||
if !cmd_screenshot(&mut socket, &args, sync_timeout) {
|
||||
failed = true;
|
||||
}
|
||||
}
|
||||
Some(":diff") => {
|
||||
let args: Vec<&str> = words.collect();
|
||||
cmd_diff(&snapshots, &args);
|
||||
|
||||
+9
-6
@@ -96,16 +96,19 @@ A working invocation, and the traps around it:
|
||||
- Some events deliberately never respond while the CPU is stepping, so `--sync` will burn its full timeout on them:
|
||||
`gpu.stats.get` and `gpu.stats.feed` (documented - they answer after the next flip), `gpu.record.dump`, and
|
||||
`input.buttons.press` (waits for N frames). Resume the CPU first, or skip them in scripted runs.
|
||||
- Log broadcasts drown scripted output. Send this first:
|
||||
`{"event":"broadcast.config.set","disallowed":{"logger":true,"input":true}}`. Note `wsdbg`'s `key=value` shorthand
|
||||
can't build nested objects - paste raw JSON lines (any line starting with `{` is sent verbatim) for those.
|
||||
- Log broadcasts drown scripted output. Pass `--quiet` to wsdbg, which turns them off.
|
||||
- **Nested parameters work in wsdbg's `key=value` shorthand**: values are parsed as JSON, and single quotes keep
|
||||
the inner double quotes, e.g. `input.buttons.send buttons='{"cross":true}'`. Prefer that over a raw JSON line,
|
||||
which gets no ticket (see below).
|
||||
- Keep wsdbg scripts in files and pipe them in, rather than building JSON inline in a shell command - inline
|
||||
`{"event":...}` in a bash heredoc trips Claude Code's command analyzer ("brace with quote character") and forces a
|
||||
manual approval prompt for every single invocation.
|
||||
- **`--sync` can only match a response to a request that carries a ticket**, and wsdbg only assigns tickets to its
|
||||
`key=value` shorthand. A raw JSON line (needed for nested params) gets no ticket, so `--sync` just waits for the
|
||||
next message and treats whatever broadcast arrives first as the answer, silently desynchronising the rest of the
|
||||
script. Use the shorthand wherever the parameters are flat. Hex works there: `memory.disasm address=0x08804000`.
|
||||
`key=value` shorthand. A raw JSON line without a `ticket` isn't waited for at all, so use the shorthand (nested
|
||||
values included, see above). Hex works there: `memory.disasm address=0x08804000`.
|
||||
- **To see the screen from a script, use wsdbg's `:screenshot <file.png>`** while the CPU is stopped, e.g. after a
|
||||
`cpu.runUntilTime`. With headless, run `--graphics=software` for it: headless Vulkan has no output image to read
|
||||
back and asserts. Give a native path on Windows (`C:/...`, not `/c/...`).
|
||||
- **Headless reports `SYSPROP_HAS_DEBUGGER` as false** (only `Windows/main.cpp` implements it), so anything gated on
|
||||
it does nothing there - `LoadSymbolsIfSupported()` in `Core/System.cpp`, for instance, doesn't load `.ppmap`/`.sym`
|
||||
at all under headless. Gate new debugger-adjacent features on their own config flag, not on that property.
|
||||
|
||||
Reference in new issue
Block a user