mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
tooling: unify dump/screenshot surface, bound TCP sends, fix beetle input + big-dump truncation
- debug_server.c: dump_ram = alias of read_ram (one request, one response, up to full 2 MB); screenshot = screenshot_file (24-bit BMP, optional "path", default psx_screenshot.bmp); vram_peek cap 64->128; read_ram hex encode via nibble table (was 2M snprintf calls stalling the main thread); send_all_blocking bounded (1 MB chunks, SO_SNDTIMEO 2s, heartbeats the starvation watchdog per chunk) and drops the client on send failure instead of letting the watchdog kill the runtime; handle_quit tags exit origin. - beetle_debug_server.c: read_ram cap 64 KB->2 MB via heap+send_raw; dump_ram/screenshot aliases; screenshot w/h->width/height parity; set_input now holds until clear_input (frames param optional) matching psx-runtime semantics — the old default expired after 1 frame and the off-by-one in the getter cleared the override before it was ever read, so scripted beetle navigation silently did nothing; wtrace_dump response heap-built + send_raw (per-entry send_fmt dropped chunks on multi-MB responses, producing broken JSON). - crash_trace: new psx_crash_trace_set_exit_origin(); report gains exit_origin (tcp_quit / sdl_window_close / unknown). - debug_client.py: O(n) string-aware brace parser (old per-byte loop was O(n^2) — minutes on a 2 MB response, which starved the server's send and got the runtime killed); generic key=value passthrough for any server command without a bespoke CLI mapping. - TCP_COMMANDS.md updated for all of the above. Verified: 2 MB read_ram round-trip 0.75 s; scripted beetle navigation reaches the memcard file-select; wtrace_dump returns valid JSON at 21 MB. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+10
-5
@@ -17,10 +17,16 @@ The `debug_client.py` CLI can target either, or `compare` two at once to diff st
|
||||
|
||||
```bash
|
||||
python tools/debug_client.py <cmd> [args] # native (port 4370)
|
||||
python tools/debug_client.py --port 4380 <cmd> # psx-beetle
|
||||
python tools/debug_client.py --ds <cmd> [args] # duckstation (port 4371)
|
||||
python tools/debug_client.py compare <cmd> # run on both, diff results
|
||||
```
|
||||
|
||||
Commands without a bespoke CLI mapping pass through generically: extra
|
||||
args of the form `key=value` become JSON fields (ints when numeric, else
|
||||
strings), so every server command is reachable, e.g.
|
||||
`debug_client.py --port 4370 gpu_frame_dump frame=14528 count=65536`.
|
||||
|
||||
---
|
||||
|
||||
## Command inventory
|
||||
@@ -31,9 +37,8 @@ Columns: **N** = native, **D** = DuckStation oracle.
|
||||
|---|---|---|---|---|
|
||||
| `ping` / `frame` | ✓ | ✓ | — | Heartbeat + current frame number |
|
||||
| `get_registers` (`regs`) | ✓ | ✓ | — | All 32 GPRs + PC + HI + LO (native also: COP0 SR/Cause/EPC, I_STAT, I_MASK) |
|
||||
| `read_ram` | ✓ | ✓ | `addr`, `len` | Read bytes from PS1 address space as hex string |
|
||||
| `read_ram` | ✓ | ✓ | `addr`, `len` | Read bytes from PS1 address space as hex string — up to the full 2 MB in ONE response line. `dump_ram` is an alias (the old chunked multi-line variant is gone: it broke the one-request/one-response protocol and wedged the server) |
|
||||
| `write_ram` | ✓ | ✓ | `addr`, `hex` | Write bytes to PS1 address space |
|
||||
| `dump_ram` | ✓ | ✓ | `addr`, `len` | Chunked RAM read (up to 64 KB) |
|
||||
| `read_scratch` | | ✓ | `addr`, `len` | Read PS1 scratchpad (0x1F800000 region) |
|
||||
| `read_vram` / `vram_peek` | ✓¹ | ✓ | `x`, `y`, `w`, `h` | Read 16-bit VRAM pixels (max 128×128) |
|
||||
| `gpu_state` | ✓ | ✓ | — | Display area, display depth, draw offset, GPUSTAT, clip rect, xfer state |
|
||||
@@ -47,7 +52,7 @@ Columns: **N** = native, **D** = DuckStation oracle.
|
||||
| `cdrom_sector_history_clear` | ✓ | | — | Reset the CD-ROM sector history ring |
|
||||
| `watch` | ✓ | ✓ | `addr` | Set byte-level memory watchpoint (fires per-frame on change) |
|
||||
| `unwatch` | ✓ | ✓ | `addr` | Remove memory watchpoint |
|
||||
| `set_input` | ✓ | ✓ | `buttons` | Override pad1 buttons (PS1 inverted bitmask, 0 = pressed) |
|
||||
| `set_input` | ✓ | ✓ | `buttons`, optional `frames` | Override pad1 buttons (PS1 inverted bitmask, 0 = pressed). Holds until `clear_input` on both backends; pass `frames=N` (beetle) to auto-release after N frames |
|
||||
| `clear_input` | ✓ | ✓ | — | Remove input override |
|
||||
| `turbo` | ✓ | | `enabled` | Enable/disable TCP-controlled frontend turbo for fast-forward validation |
|
||||
| `turbo_state` | ✓ | | — | Query TCP-controlled turbo state |
|
||||
@@ -61,11 +66,11 @@ Columns: **N** = native, **D** = DuckStation oracle.
|
||||
| `frame_timeseries` | ✓ | ✓ | `start`, `end` | Compact timeseries, max 200 frames |
|
||||
| `set_snapshot` | ✓ | ✓ | `slot`, `addr`, `size` | Configure per-frame RAM snapshot region (slots 0-3) |
|
||||
| `get_snapshots` | ✓ | ✓ | — | Show snapshot config |
|
||||
| `screenshot` | ✓ | ✓ | `path` | Save display to PNG/file; native row dumps report `rgb555` or `rgb888` to match display depth |
|
||||
| `screenshot` | ✓ | ✓ | `path` (optional) | Write a 24-bit BMP of the current display to `path` (default `psx_screenshot.bmp` in the runtime cwd); single metadata response `{path,width,height}`. `screenshot_file` is an alias; the old inline-hex-row `screenshot` is gone (it streamed h+1 response lines per request and poisoned the connection) |
|
||||
| `first_failure` | ✓ | | — | Find first divergence point between runs (native-side tracking) |
|
||||
| `read_frame_ram` | ✓ | | `addr`, `len`, `frame` | Read RAM **as of a specific frame** (from ring buffer) |
|
||||
| `wtrace_range` | ✓ | | `lo`, `hi` | Set RAM-write trace range (ring of 1024 writes with RA) |
|
||||
| `wtrace_dump` | ✓ | | — | Dump RAM-write trace entries as JSON |
|
||||
| `wtrace_dump` | ✓ | beetle | optional `addr_lo`, `addr_hi`, `count`, `newest` | Dump RAM-write trace entries as JSON. The address filter is applied server-side over the FULL ring before the emit cap — always pass it when hunting a specific buffer, otherwise you only see the oldest `count` entries of the whole ring |
|
||||
| `wtrace_clear` | ✓ | | — | Reset the trace ring |
|
||||
| `pc_break` | | ✓² | `addr` | DS execute breakpoint, state captured on hit (via `pc_hit_last`) |
|
||||
| `pc_unbreak` | | ✓² | `addr` | Remove an execute breakpoint |
|
||||
|
||||
@@ -16,6 +16,11 @@ void psx_crash_trace_install_handlers(void);
|
||||
* NULL. Writes to psx_last_run_report.json (overwrite). */
|
||||
void psx_crash_trace_dump(const char *reason, void *seh_info);
|
||||
|
||||
/* Tag a deliberate exit() so the atexit report can attribute it
|
||||
* (e.g. "tcp_quit", "sdl_window_close"). An "atexit" report whose
|
||||
* exit_origin is still "unknown" means an untagged exit fired. */
|
||||
void psx_crash_trace_set_exit_origin(const char *origin);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -230,23 +230,31 @@ static void h_read_ram(int id, const char *json) {
|
||||
uint32_t addr = hex_to_u32(addr_s);
|
||||
int len = json_get_int(json, "len", 16);
|
||||
if (len < 1) len = 1;
|
||||
if (len > 65536) len = 65536;
|
||||
/* Match the native server: whole 2 MB RAM in one response if asked. */
|
||||
if (len > 0x200000) len = 0x200000;
|
||||
|
||||
uint8_t *buf = (uint8_t*)malloc(len);
|
||||
if (!buf) { send_err(id, "alloc"); return; }
|
||||
for (int i = 0; i < len; i++) buf[i] = beetle_read_byte((addr + i) & 0x1FFFFFFFu);
|
||||
|
||||
char *hex = (char*)malloc(len * 2 + 1);
|
||||
if (!hex) { free(buf); send_err(id, "alloc"); return; }
|
||||
static const char H[] = "0123456789ABCDEF";
|
||||
/* Build the whole response line on the heap: send_fmt's 128 KB static
|
||||
* buffer would silently truncate a large read into broken JSON. */
|
||||
size_t env = 96;
|
||||
char *out = (char*)malloc((size_t)len * 2 + env);
|
||||
if (!out) { free(buf); send_err(id, "alloc"); return; }
|
||||
int hdr = snprintf(out, env,
|
||||
"{\"id\":%d,\"ok\":true,\"addr\":\"0x%08X\",\"len\":%d,\"hex\":\"",
|
||||
id, addr, len);
|
||||
char *hex = out + hdr;
|
||||
static const char H[] = "0123456789abcdef";
|
||||
for (int i = 0; i < len; i++) {
|
||||
hex[i*2] = H[(buf[i] >> 4) & 0xF];
|
||||
hex[i*2+1] = H[buf[i] & 0xF];
|
||||
}
|
||||
hex[len*2] = 0;
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"addr\":\"0x%08X\",\"len\":%d,\"hex\":\"%s\"}\n",
|
||||
id, addr, len, hex);
|
||||
free(hex); free(buf);
|
||||
char *tail = hex + (size_t)len * 2;
|
||||
memcpy(tail, "\"}\n", 3);
|
||||
send_raw(out, (int)(hdr + (size_t)len * 2 + 3));
|
||||
free(out); free(buf);
|
||||
}
|
||||
|
||||
static void h_press(int id, const char *json) {
|
||||
@@ -264,7 +272,8 @@ static void h_set_input(int id, const char *json) {
|
||||
send_err(id, "missing buttons"); return;
|
||||
}
|
||||
s_input_override = (int)hex_to_u32(val);
|
||||
s_input_frames = json_get_int(json, "frames", 1);
|
||||
/* 0 = hold until clear_input — matches psx-runtime semantics. */
|
||||
s_input_frames = json_get_int(json, "frames", 0);
|
||||
send_ok(id);
|
||||
}
|
||||
|
||||
@@ -328,7 +337,7 @@ static void h_screenshot_file(int id, const char *json) {
|
||||
}
|
||||
free(row);
|
||||
fclose(f);
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"path\":\"%s\",\"w\":%u,\"h\":%u}\n",
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"path\":\"%s\",\"width\":%u,\"height\":%u}\n",
|
||||
id, path, w, h);
|
||||
}
|
||||
|
||||
@@ -548,18 +557,30 @@ static void h_wtrace_dump(int id, const char *json) {
|
||||
int cap = beetle_wtrace_capacity();
|
||||
uint32_t avail = (total < (uint64_t)cap) ? (uint32_t)total : (uint32_t)cap;
|
||||
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"total\":%llu,\"available\":%u,\"entries\":[",
|
||||
/* Heap-build the whole response and send via send_raw — the per-entry
|
||||
* send_fmt path drops chunks on multi-MB responses (same failure mode
|
||||
* as the old read_ram truncation). ~400 bytes/entry worst case. */
|
||||
size_t out_cap = 256 + (size_t)got * 400u;
|
||||
char *out = (char *)malloc(out_cap);
|
||||
if (!out) {
|
||||
free(seqs); free(addrs); free(vals); free(pcs); free(ras); free(sps);
|
||||
free(v0s); free(v1s); free(a0s); free(a1s); free(a2s); free(a3s);
|
||||
free(t0s); free(t1s); free(frames); free(slots); free(sizes);
|
||||
send_err(id, "alloc"); return;
|
||||
}
|
||||
size_t pos = (size_t)snprintf(out, out_cap,
|
||||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"available\":%u,\"entries\":[",
|
||||
id, (unsigned long long)total, avail);
|
||||
uint32_t emitted = 0;
|
||||
for (uint32_t i = 0; i < got; i++) {
|
||||
for (uint32_t i = 0; i < got && pos < out_cap - 512; i++) {
|
||||
uint32_t phys = addrs[i] & 0x1FFFFFFFu;
|
||||
if (phys < flo || phys >= fhi) continue;
|
||||
if (emitted > 0) send_fmt(",");
|
||||
/* Field names match runtime's handle_wtrace_dump exactly: addr,
|
||||
* new, ra, pc, cpu_pc, sp, v0/v1/a0..a3/t0/t1, frame, w.
|
||||
* Beetle-only extras: slot. Runtime-only fields (old, func) are
|
||||
* absent — tools must use .get(). */
|
||||
send_fmt("{\"seq\":%llu,\"addr\":\"0x%08X\","
|
||||
pos += (size_t)snprintf(out + pos, out_cap - pos,
|
||||
"%s{\"seq\":%llu,\"addr\":\"0x%08X\","
|
||||
"\"new\":\"0x%08X\",\"ra\":\"0x%08X\","
|
||||
"\"pc\":\"0x%08X\",\"cpu_pc\":\"0x%08X\",\"sp\":\"0x%08X\","
|
||||
"\"v0\":\"0x%08X\",\"v1\":\"0x%08X\","
|
||||
@@ -567,6 +588,7 @@ static void h_wtrace_dump(int id, const char *json) {
|
||||
"\"a2\":\"0x%08X\",\"a3\":\"0x%08X\","
|
||||
"\"t0\":\"0x%08X\",\"t1\":\"0x%08X\","
|
||||
"\"frame\":%u,\"w\":%u,\"slot\":%u}",
|
||||
(emitted == 0) ? "" : ",",
|
||||
(unsigned long long)seqs[i], addrs[i],
|
||||
vals[i], ras[i],
|
||||
pcs[i], pcs[i], sps[i],
|
||||
@@ -576,7 +598,9 @@ static void h_wtrace_dump(int id, const char *json) {
|
||||
frames[i], (unsigned)sizes[i], (unsigned)slots[i]);
|
||||
emitted++;
|
||||
}
|
||||
send_fmt("],\"emitted\":%u}\n", emitted);
|
||||
pos += (size_t)snprintf(out + pos, out_cap - pos, "],\"emitted\":%u}\n", emitted);
|
||||
send_raw(out, (int)pos);
|
||||
free(out);
|
||||
|
||||
free(seqs); free(addrs); free(vals); free(pcs); free(ras); free(sps);
|
||||
free(v0s); free(v1s); free(a0s); free(a1s); free(a2s); free(a3s);
|
||||
@@ -1054,11 +1078,13 @@ typedef struct { const char *name; cmd_handler handler; } CmdEntry;
|
||||
static const CmdEntry CMDS[] = {
|
||||
{ "ping", h_ping },
|
||||
{ "read_ram", h_read_ram },
|
||||
{ "dump_ram", h_read_ram }, /* alias, parity with native */
|
||||
{ "press", h_press },
|
||||
{ "set_input", h_set_input },
|
||||
{ "clear_input", h_clear_input },
|
||||
{ "pad_status", h_pad_status },
|
||||
{ "screenshot_file", h_screenshot_file },
|
||||
{ "screenshot", h_screenshot_file },
|
||||
{ "screenshot_file", h_screenshot_file }, /* alias */
|
||||
{ "sio_trace_reset", h_sio_trace_reset },
|
||||
{ "sio_trace", h_sio_trace },
|
||||
{ "sio_write_window", h_sio_write_window },
|
||||
@@ -1221,8 +1247,11 @@ void beetle_debug_server_shutdown(void) {
|
||||
}
|
||||
|
||||
int beetle_debug_server_get_input_override(void) {
|
||||
int cur = s_input_override;
|
||||
/* s_input_frames == 0 means hold until clear_input (psx-runtime
|
||||
* semantics). A finite count delivers exactly that many frames. */
|
||||
if (s_input_override >= 0 && s_input_frames > 0) {
|
||||
if (--s_input_frames == 0) s_input_override = -1;
|
||||
}
|
||||
return s_input_override;
|
||||
return cur;
|
||||
}
|
||||
|
||||
@@ -90,6 +90,18 @@ static void hex32(char *out, uint32_t v) {
|
||||
snprintf(out, 16, "\"0x%08X\"", v);
|
||||
}
|
||||
|
||||
/* ── Exit origin ─────────────────────────────────────────────────────── */
|
||||
|
||||
/* Deliberate exit() callers tag themselves here so an "atexit" report can
|
||||
* distinguish a TCP quit from an SDL window close from an unexplained
|
||||
* exit. "unknown" in a report means NOBODY tagged — main returned or an
|
||||
* untagged exit() fired; that is a finding, not noise. */
|
||||
static const char *s_exit_origin = "unknown";
|
||||
|
||||
void psx_crash_trace_set_exit_origin(const char *origin) {
|
||||
if (origin) s_exit_origin = origin;
|
||||
}
|
||||
|
||||
/* ── Main entry ──────────────────────────────────────────────────────── */
|
||||
|
||||
void psx_crash_trace_dump(const char *reason, void *seh_info) {
|
||||
@@ -108,11 +120,13 @@ void psx_crash_trace_dump(const char *reason, void *seh_info) {
|
||||
append_fmt(buf, sizeof(buf), &pos,
|
||||
"{\n"
|
||||
" \"reason\": \"%s\",\n"
|
||||
" \"exit_origin\": \"%s\",\n"
|
||||
" \"timestamp\": \"%s\",\n"
|
||||
" \"frame\": %llu,\n"
|
||||
" \"last_func_addr\": \"0x%08X\",\n"
|
||||
" \"last_store_pc\": \"0x%08X\",\n",
|
||||
reason ? reason : "(unknown)",
|
||||
s_exit_origin,
|
||||
ts,
|
||||
(unsigned long long)s_frame_count,
|
||||
g_debug_current_func_addr,
|
||||
|
||||
+70
-81
@@ -24,6 +24,7 @@
|
||||
#include "dirty_ram_interp.h"
|
||||
#include "card_read_summary.h"
|
||||
#include "card_data_writes.h"
|
||||
#include "crash_trace.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -3277,22 +3278,42 @@ static uint32_t hex_to_u32(const char *s)
|
||||
|
||||
/* ---- Send helpers ---- */
|
||||
|
||||
static void send_all_blocking(sock_t sock, const char *data, size_t len)
|
||||
/* Bounded blocking send. Returns 0 on success, -1 on failure/timeout.
|
||||
*
|
||||
* The server is pumped on the main thread, so an unbounded send to a
|
||||
* client that has stopped draining stalls the emulator until the
|
||||
* starvation watchdog (4 s) kills the whole process. Each chunk gets a
|
||||
* 2 s send timeout and a watchdog heartbeat, so a healthy-but-slow
|
||||
* client can take as long as it likes (progress = heartbeats), while a
|
||||
* wedged client costs at most one timeout and then loses its
|
||||
* connection — never the runtime. */
|
||||
static int send_all_blocking(sock_t sock, const char *data, size_t len)
|
||||
{
|
||||
int ok = 0;
|
||||
#ifdef _WIN32
|
||||
u_long mode = 0;
|
||||
ioctlsocket(sock, FIONBIO, &mode);
|
||||
DWORD tmo = 2000;
|
||||
setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (const char *)&tmo, sizeof(tmo));
|
||||
#else
|
||||
int flags = fcntl(sock, F_GETFL, 0);
|
||||
fcntl(sock, F_SETFL, flags & ~O_NONBLOCK);
|
||||
struct timeval tmo = { 2, 0 };
|
||||
setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tmo, sizeof(tmo));
|
||||
#endif
|
||||
/* send() takes int on Win32; chunk so we can transmit > 2 GB payloads. */
|
||||
size_t sent = 0;
|
||||
while (sent < len) {
|
||||
size_t want = len - sent;
|
||||
if (want > (1u << 30)) want = (1u << 30);
|
||||
if (want > (1u << 20)) want = (1u << 20);
|
||||
int n = send(sock, data + sent, (int)want, 0);
|
||||
if (n > 0) { sent += (size_t)n; continue; }
|
||||
if (n > 0) {
|
||||
sent += (size_t)n;
|
||||
/* Legitimate debug traffic in flight — not starvation. */
|
||||
extern void starvation_watchdog_heartbeat(void);
|
||||
starvation_watchdog_heartbeat();
|
||||
continue;
|
||||
}
|
||||
ok = -1;
|
||||
break;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
@@ -3301,14 +3322,20 @@ static void send_all_blocking(sock_t sock, const char *data, size_t len)
|
||||
#else
|
||||
fcntl(sock, F_SETFL, flags);
|
||||
#endif
|
||||
return ok;
|
||||
}
|
||||
|
||||
void debug_server_send_line(const char *json)
|
||||
{
|
||||
if (s_client == SOCK_INVALID) return;
|
||||
size_t len = strlen(json);
|
||||
send_all_blocking(s_client, json, len);
|
||||
send_all_blocking(s_client, "\n", 1);
|
||||
if (send_all_blocking(s_client, json, len) != 0 ||
|
||||
send_all_blocking(s_client, "\n", 1) != 0) {
|
||||
/* Client stopped draining — drop the connection, keep the
|
||||
* runtime. The next poll() accepts a fresh client. */
|
||||
sock_close(s_client);
|
||||
s_client = SOCK_INVALID;
|
||||
}
|
||||
}
|
||||
|
||||
void debug_server_send_fmt(const char *fmt, ...)
|
||||
@@ -3434,37 +3461,26 @@ static void handle_read_ram(int id, const char *json)
|
||||
"{\"id\":%d,\"ok\":true,\"addr\":\"0x%08X\",\"len\":%d,\"hex\":\"",
|
||||
id, addr, len);
|
||||
char *hex = out + hdr;
|
||||
for (int i = 0; i < len; i++)
|
||||
snprintf(hex + (size_t)i * 2, 3, "%02x", psx_read_byte(addr + (uint32_t)i));
|
||||
/* Nibble-table encode: snprintf per byte costs seconds for a 2 MB
|
||||
* read, which stalls the main-thread-pumped server (and the SDL
|
||||
* event loop) long enough to look like a wedge. */
|
||||
static const char H[] = "0123456789abcdef";
|
||||
for (int i = 0; i < len; i++) {
|
||||
uint8_t b = psx_read_byte(addr + (uint32_t)i);
|
||||
hex[(size_t)i * 2] = H[b >> 4];
|
||||
hex[(size_t)i * 2 + 1] = H[b & 0xF];
|
||||
}
|
||||
char *tail = hex + (size_t)len * 2;
|
||||
memcpy(tail, "\"}", 3);
|
||||
debug_server_send_line(out);
|
||||
free(out);
|
||||
}
|
||||
|
||||
static void handle_dump_ram(int id, const char *json)
|
||||
{
|
||||
char addr_str[32];
|
||||
if (!json_get_str(json, "addr", addr_str, sizeof(addr_str))) {
|
||||
send_err(id, "missing addr"); return;
|
||||
}
|
||||
uint32_t addr = hex_to_u32(addr_str);
|
||||
int len = json_get_int(json, "len", 256);
|
||||
if (len < 1) len = 1;
|
||||
if (len > 4096) len = 4096;
|
||||
|
||||
int offset = 0;
|
||||
while (offset < len) {
|
||||
int chunk = len - offset;
|
||||
if (chunk > 256) chunk = 256;
|
||||
char hex[513];
|
||||
for (int i = 0; i < chunk; i++)
|
||||
snprintf(hex + i * 2, 3, "%02x", psx_read_byte(addr + offset + i));
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"addr\":\"0x%08X\",\"offset\":%d,\"len\":%d,\"hex\":\"%s\"}",
|
||||
id, addr + offset, offset, chunk, hex);
|
||||
offset += chunk;
|
||||
}
|
||||
}
|
||||
/* "dump_ram" is an alias of "read_ram". The old implementation answered a
|
||||
* single request with one response line per 256-byte chunk; any client that
|
||||
* follows the one-request/one-response protocol left the extra lines unread,
|
||||
* the socket send buffer filled, the main-thread-pumped server blocked, and
|
||||
* the freeze watchdog killed the process. One request, one response. */
|
||||
|
||||
static void handle_write_ram(int id, const char *json)
|
||||
{
|
||||
@@ -5481,9 +5497,14 @@ static void handle_get_snapshots(int id, const char *json)
|
||||
s_snapshot_addrs[3], s_snapshot_active[3]);
|
||||
}
|
||||
|
||||
/* Unified screenshot: writes a 24-bit BMP of the current display to "path"
|
||||
* (default psx_screenshot.bmp in the runtime cwd) and answers with a single
|
||||
* metadata line. Registered as both "screenshot" and "screenshot_file";
|
||||
* the old "screenshot" inline-hex-row variant streamed h+1 response lines
|
||||
* per request, which violated the one-request/one-response protocol and
|
||||
* poisoned every client connection that used it. */
|
||||
static void handle_screenshot_file(int id, const char *json)
|
||||
{
|
||||
(void)json;
|
||||
GpuDisplayInfo di;
|
||||
gpu_get_display_info(&di);
|
||||
if (di.disabled || di.width == 0 || di.height == 0) {
|
||||
@@ -5493,7 +5514,10 @@ static void handle_screenshot_file(int id, const char *json)
|
||||
uint32_t w = di.width; if (w > 640) w = 640;
|
||||
uint32_t h = di.height; if (h > 512) h = 512;
|
||||
|
||||
const char *path = "psx_screenshot.bmp";
|
||||
char path[512];
|
||||
if (!json_get_str(json, "path", path, sizeof(path)))
|
||||
strncpy(path, "psx_screenshot.bmp", sizeof(path) - 1);
|
||||
path[sizeof(path) - 1] = '\0';
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) { send_err(id, "cannot open file"); return; }
|
||||
|
||||
@@ -5541,65 +5565,29 @@ static void handle_screenshot_file(int id, const char *json)
|
||||
id, path, w, h);
|
||||
}
|
||||
|
||||
static void handle_screenshot(int id, const char *json)
|
||||
{
|
||||
(void)json;
|
||||
/* Read display area from GPU and encode as display-format hex rows. */
|
||||
GpuDisplayInfo di;
|
||||
gpu_get_display_info(&di);
|
||||
|
||||
if (di.disabled || di.width == 0 || di.height == 0) {
|
||||
send_err(id, "display disabled"); return;
|
||||
}
|
||||
|
||||
uint32_t w = di.width;
|
||||
uint32_t h = di.height;
|
||||
if (w > 640) w = 640;
|
||||
if (h > 512) h = 512;
|
||||
|
||||
/* Send metadata first */
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"width\":%u,\"height\":%u,\"format\":\"%s\"}",
|
||||
id, w, h, di.depth24 ? "rgb888" : "rgb555");
|
||||
|
||||
/* Send rows as hex lines */
|
||||
uint32_t chars_per_pixel = di.depth24 ? 6u : 4u;
|
||||
char *hex = (char *)malloc((size_t)w * chars_per_pixel + 32u);
|
||||
if (!hex) return;
|
||||
|
||||
for (uint32_t y = 0; y < h; y++) {
|
||||
for (uint32_t x = 0; x < w; x++) {
|
||||
if (di.depth24) {
|
||||
uint8_t r, g, b;
|
||||
gpu_display_pixel_rgb(&di, x, y, &r, &g, &b);
|
||||
snprintf(hex + x * 6, 7, "%02x%02x%02x", r, g, b);
|
||||
} else {
|
||||
uint16_t pixel = gpu_vram_peek((int)(di.display_x + x), (int)(di.display_y + y));
|
||||
snprintf(hex + x * 4, 5, "%04x", pixel);
|
||||
}
|
||||
}
|
||||
send_fmt("{\"row\":%u,\"hex\":\"%s\"}", y, hex);
|
||||
}
|
||||
free(hex);
|
||||
}
|
||||
|
||||
static void handle_vram_peek(int id, const char *json)
|
||||
{
|
||||
int x = json_get_int(json, "x", 0);
|
||||
int y = json_get_int(json, "y", 0);
|
||||
int w = json_get_int(json, "w", 8);
|
||||
int h = json_get_int(json, "h", 1);
|
||||
if (w > 64) w = 64;
|
||||
if (h > 64) h = 64;
|
||||
char hex[64*64*4+1];
|
||||
if (w < 1) w = 1;
|
||||
if (h < 1) h = 1;
|
||||
if (w > 128) w = 128;
|
||||
if (h > 128) h = 128;
|
||||
size_t hex_len = (size_t)w * h * 4 + 1;
|
||||
char *hex = (char *)malloc(hex_len);
|
||||
if (!hex) { send_err(id, "alloc failed"); return; }
|
||||
int pos = 0;
|
||||
for (int row = 0; row < h; row++) {
|
||||
for (int col = 0; col < w; col++) {
|
||||
uint16_t p = gpu_vram_peek(x + col, y + row);
|
||||
pos += snprintf(hex + pos, sizeof(hex) - pos, "%04x", p);
|
||||
pos += snprintf(hex + pos, hex_len - pos, "%04x", p);
|
||||
}
|
||||
}
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"x\":%d,\"y\":%d,\"w\":%d,\"h\":%d,\"hex\":\"%s\"}",
|
||||
id, x, y, w, h, hex);
|
||||
free(hex);
|
||||
}
|
||||
|
||||
/* ---- Write trace: hook + handlers (Tier 1 reverse debugger) ---- */
|
||||
@@ -6957,6 +6945,7 @@ static void handle_quit(int id, const char *json)
|
||||
{
|
||||
(void)json;
|
||||
send_ok(id);
|
||||
psx_crash_trace_set_exit_origin("tcp_quit");
|
||||
debug_server_shutdown();
|
||||
exit(0);
|
||||
}
|
||||
@@ -8113,7 +8102,7 @@ static const CmdEntry s_commands[] = {
|
||||
{ "frame", handle_frame },
|
||||
{ "get_registers", handle_get_registers },
|
||||
{ "read_ram", handle_read_ram },
|
||||
{ "dump_ram", handle_dump_ram },
|
||||
{ "dump_ram", handle_read_ram }, /* alias: one request, one response */
|
||||
{ "write_ram", handle_write_ram },
|
||||
{ "gpu_state", handle_gpu_state },
|
||||
{ "mem_words", handle_mem_words },
|
||||
@@ -8245,8 +8234,8 @@ static const CmdEntry s_commands[] = {
|
||||
{ "read_frame_ram", handle_read_frame_ram },
|
||||
{ "set_snapshot", handle_set_snapshot },
|
||||
{ "get_snapshots", handle_get_snapshots },
|
||||
{ "screenshot", handle_screenshot },
|
||||
{ "screenshot_file", handle_screenshot_file },
|
||||
{ "screenshot", handle_screenshot_file },
|
||||
{ "screenshot_file", handle_screenshot_file }, /* alias */
|
||||
{ "gpu_opcodes", handle_gpu_opcodes },
|
||||
{ "gpu_ring_stats", handle_gpu_ring_stats },
|
||||
{ "gpu_frame_dump", handle_gpu_frame_dump },
|
||||
|
||||
@@ -965,6 +965,7 @@ static void sdl_vblank_present(void) {
|
||||
SDL_Event ev;
|
||||
while (SDL_PollEvent(&ev)) {
|
||||
if (ev.type == SDL_QUIT) {
|
||||
psx_crash_trace_set_exit_origin("sdl_window_close");
|
||||
shutdown_runtime();
|
||||
std::exit(0);
|
||||
} else if (ev.type == SDL_CONTROLLERDEVICEADDED) {
|
||||
|
||||
+55
-23
@@ -59,6 +59,7 @@ REPL commands:
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import socket
|
||||
import sys
|
||||
import argparse
|
||||
@@ -89,35 +90,54 @@ def connect(host=DEFAULT_HOST, port=NATIVE_PORT, timeout=10.0):
|
||||
def send_cmd(sock, cmd_dict):
|
||||
line = json.dumps(cmd_dict) + "\n"
|
||||
sock.sendall(line.encode())
|
||||
buf = b""
|
||||
# Server may emit one large JSON object across many TCP chunks, with raw
|
||||
# newlines inside arrays. Track brace/bracket balance (string-aware) and
|
||||
# parse once balance returns to zero.
|
||||
depth_curly = 0
|
||||
depth_sq = 0
|
||||
# One complete JSON object = one response. Track brace balance
|
||||
# (string-aware) with a persistent state machine and fast-skips so a
|
||||
# multi-megabyte response costs O(n), not O(n^2) — the old per-byte
|
||||
# loop took minutes on a 2 MB read_ram, the server's blocking send
|
||||
# backed up, and the runtime's starvation watchdog killed the process.
|
||||
buf = bytearray()
|
||||
pos = 0
|
||||
depth = 0
|
||||
in_str = False
|
||||
esc = False
|
||||
started = False
|
||||
while True:
|
||||
chunk = sock.recv(65536)
|
||||
chunk = sock.recv(1 << 20)
|
||||
if not chunk:
|
||||
break
|
||||
for b in chunk:
|
||||
buf += bytes([b])
|
||||
c = chr(b)
|
||||
buf.extend(chunk)
|
||||
n = len(buf)
|
||||
while pos < n:
|
||||
if in_str:
|
||||
if esc: esc = False
|
||||
elif c == "\\": esc = True
|
||||
elif c == '"': in_str = False
|
||||
if esc:
|
||||
esc = False
|
||||
pos += 1
|
||||
continue
|
||||
# Fast-skip to the next quote or backslash (C-speed find;
|
||||
# hex payloads are one giant string with neither).
|
||||
q = buf.find(b'"', pos)
|
||||
bs = buf.find(b"\\", pos)
|
||||
if q == -1 and bs == -1:
|
||||
pos = n
|
||||
break
|
||||
if bs != -1 and (q == -1 or bs < q):
|
||||
esc = True
|
||||
pos = bs + 1
|
||||
continue
|
||||
in_str = False
|
||||
pos = q + 1
|
||||
continue
|
||||
if c == '"': in_str = True
|
||||
elif c == "{": depth_curly += 1; started = True
|
||||
elif c == "}": depth_curly -= 1
|
||||
elif c == "[": depth_sq += 1
|
||||
elif c == "]": depth_sq -= 1
|
||||
if started and depth_curly == 0 and depth_sq == 0:
|
||||
# Message complete.
|
||||
return json.loads(buf.decode().strip())
|
||||
c = buf[pos]
|
||||
if c == 0x22: # '"'
|
||||
in_str = True
|
||||
elif c == 0x7B: # '{'
|
||||
depth += 1
|
||||
started = True
|
||||
elif c == 0x7D: # '}'
|
||||
depth -= 1
|
||||
if started and depth == 0:
|
||||
return json.loads(buf[:pos + 1].decode())
|
||||
pos += 1
|
||||
return json.loads(buf.decode().strip())
|
||||
|
||||
|
||||
@@ -378,8 +398,20 @@ def build_cmd(args):
|
||||
d["pc_hi"] = args[3]
|
||||
return d, pretty_json
|
||||
else:
|
||||
# Pass through as raw command
|
||||
return {"cmd": cmd}, pretty_json
|
||||
# Pass through as raw command. Extra args of the form key=value
|
||||
# become JSON fields (ints when numeric, else strings), so every
|
||||
# server command is reachable without a bespoke CLI mapping.
|
||||
d = {"cmd": cmd}
|
||||
for a in args[1:]:
|
||||
key, sep, val = a.partition("=")
|
||||
if not sep:
|
||||
return None, lambda _, a=a: (
|
||||
f"Unrecognized arg '{a}' for raw command; use key=value")
|
||||
if re.fullmatch(r"-?\d+", val):
|
||||
d[key] = int(val)
|
||||
else:
|
||||
d[key] = val
|
||||
return d, pretty_json
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user