Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions docs/PORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,8 +255,9 @@ bare binary, the replays and the goldens are unchanged. The ops:
- `status`: frame, stage, file, map, paused, pace, pad owner. `pause` / `resume`: hold the game at the next vsync
boundary (the pump's pause, shared with the window's pause key). `step frames`: exactly N vsyncs, then pause.
- `wait`: run until a host or PS1 read, the stage or the map equals a value, or a timeout in frames; leaves the game
paused. `pad buttons frames release [sync]`: the channel owns pad 1 (`psyq_pad_set` each vsync) until `pad_free`;
a script keeps precedence; the window keeps pad 2.
paused. `pad buttons frames release [sync] [port]`: the channel owns that port (0 by default, 1 the second pad,
connected while owned; `psyq_pad_set` each vsync) until `pad_free`; a script keeps precedence; the window keeps the
ports the channel does not own.
- `peek` / `poke`: raw host memory, any range `/proc/self/maps` says is mapped (a bad address never faults the game).
`peek_ps1` / `poke_ps1`: a PS1 address, through the adapter's state map first (`game_state_host`, 1/2/4 bytes: the
host objects behind PS1 addresses, an overlay's globals in the slots among them), else the arena (from the first
Expand Down Expand Up @@ -583,8 +584,8 @@ main thread and reports its registers and stack. The `.dmp` opens in WinDbg or V
stack overflow gets the text (the filter runs on what the guard page leaves) and perhaps no dump.

## Known limitations
- **Pads:** digital pads on port 0 and, from the window's input only, port 1; no analog mode, no rumble (`PadSetAct` is
accepted and ignored), no multitap. The script and the debug channel drive port 0 only.
- **Pads:** digital pads on port 0 and port 1 (the window's `input.player2`, a script's `pad2`, the debug channel's
`port`); no analog mode, no rumble (`PadSetAct` is accepted and ignored), no multitap.
- **No reset key** in the window (the console reset exists only as a script step and a debug-channel op).
- **Timing stand-ins:** the CD seek times (3 ticks + 1 per 8192 sectors, at most 40) and `StGetNext`'s 5000 polls
per vsync are estimates, not measurements.
Expand Down
5 changes: 5 additions & 0 deletions docs/RUNTIME.md
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,11 @@ by the stack's CI: it needs a game, its disc and its pinned emulator.
such a checkpoint, or lacks them for one with an image, does not belong to the script: the emulator runner and the
port test report it as a failure, and do so for an expected file as well. Dump files are numbered by the
checkpoint's position (`cp02_x.bin` is the second checkpoint), no-image ones included.
- **The script's second pad:** `"pad2": true` at the script's top level puts a digital pad in port 1 from the first
frame (nothing pressed; without it port 1 is empty on both runners: run.lua sets PCSX-Redux's pad 2 `Connected`
either way), and a `press` step with `"port": 1` holds its buttons there (`{"type": "press", "port": 1, "buttons":
["CROSS"]}`); the other steps, and a `press` without `port`, drive port 0. A press on port 1 without `pad2` is
refused when the script loads. The records' `inputs` trace port 1 from its first frame (`"port": 1` entries).
- **The script's `write_mem` step** writes PS1 memory on both runners, instantly (the next step runs in the same
frame): `{"type": "write_mem", "addr": "0x80010000", "value": 1}` (`size` 1, 2 or 4, default 4, little-endian;
`"mask": "0x2"` sets only the mask's bits from `value` and keeps the others), or `{"type": "write_mem", "addr": ...,
Expand Down
3 changes: 2 additions & 1 deletion include/psxstack/port_harness.h
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,8 @@ void port_mods_data_start(int active);
* (step, wait, a synced pad), then the socket's requests; a reset it was asked for is requested at its end.
* port_debug_poll_paused: the requests only (pump.c's pause loop, every poll). port_debug_resumed: after the pause
* loop (a reset asked for while paused runs here). port_debug_close: from port_exit (the socket file unlinked).
* port_debug_pad_owned: the channel owns the pad (a pad op until pad_free): input.c sends nothing to the pad. */
* port_debug_pad_owned: the ports the channel owns, bit p for port p (a pad op on it until pad_free): input.c sends
* nothing to those. */
extern int port_debug_active;
extern int port_debug_pad_owned;
void port_debug_open(const char *path);
Expand Down
136 changes: 84 additions & 52 deletions runtime/debug.c
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
* completes; the client sends nothing else meanwhile.
* - Ops (args -> result):
* status: -> frame, stage, file, map, slot1_word0, paused (0/1), pace, rate, window (0/1), script (0/1),
* pad_owner ("debug"/"script"/"window"/"none"), pad (u16: the buttons last set).
* pad_owner ("debug"/"script"/"window"/"none"), pad (u16: the buttons last set); the same for port 1, the second
* pad: pad2_owner, pad2, and pad2_connected (0/1).
* pause: -> frame. Holds the game at the next vsync boundary (right after this frame when handled from
* port_frame). Idempotent. resume: -> frame; idempotent.
* step: frames (>= 1, default 1) -> frame. Runs exactly `frames` vsyncs then pauses; deferred.
Expand All @@ -39,11 +40,14 @@
* poke_ps1: addr, data -> len; the same mapping, written.
* pad: buttons (u16, PS1 bit order: SELECT=0, L3, R3, START, UP, RIGHT, DOWN, LEFT, L2, R2, L1, R1, TRIANGLE,
* CIRCLE, CROSS, SQUARE=15; active high), frames (>= 0; 0 = hold until the next pad op), release (>= 0, default
* 0: frames of 0 after `frames`), sync (bool, default false) -> frame. The channel owns the pad from the first
* pad op until pad_free (window input no longer reaches it; a script keeps precedence: "script owns the pad");
* each vsync it calls psyq_pad_set(0, 1, value). With sync the answer waits until frames + release vsyncs ran,
* and a game that was paused when the op arrived is paused again then (a driven run stays at a known frame).
* pad_free: releases the pad to the window (or none).
* 0: frames of 0 after `frames`), sync (bool, default false), port (0, the default, or 1: the second pad) ->
* frame. The channel owns the port from its first pad op until pad_free (window input no longer reaches it, and
* still drives the other port; a script keeps precedence: "script owns the pad"); each vsync it calls
* psyq_pad_set(port, 1, value): port 1 has a pad in it while the channel owns it. With sync the answer waits
* until frames + release vsyncs ran, and a game that was paused when the op arrived is paused again then (a
* driven run stays at a known frame).
* pad_free: port (optional) -> releases that port, or every port the channel owns, to the window (or none: port
* 1 then has nothing in it).
* wait: one of addr (host) / ps1 (PS1 address) / ps1_stage / ps1_map, with size (1/2/4), signed (bool), value,
* timeout (frames, default 600) -> frame, hit (0/1). Runs until the read equals value (checked once before
* running, then after each vsync) or timeout frames passed; deferred; leaves the game paused. ps1_stage and
Expand All @@ -64,7 +68,7 @@
* quit: status (default 0); answers, then port_exit(status, "debug quit").
* - Pause semantics: `paused` means the pump holds the game between vsyncs in its pause loop; the pause key and the
* channel share that state. While paused the channel keeps answering peek/poke/screenshot/status/hash; step runs
* frames from there. A client that disconnects drops its pending op, frees the pad and resumes the game, so a dead
* frames from there. A client that disconnects drops its pending op, frees the pads and resumes the game, so a dead
* tool never leaves the game held. */
#ifdef _WIN32
/* The Windows build has no channel yet (a Unix socket, poll and /proc/self/maps are POSIX): --debug is refused; the
Expand Down Expand Up @@ -155,13 +159,18 @@ typedef struct DebugWait {
} DebugWait;
static DebugWait debug_wait;

/* The pad: `buttons` for `hold` more vsyncs (or forever), then 0 for `release` vsyncs, then 0. Applied once per
* frame (debug_pad_applied_frame), at the op itself and at every later port_debug_frame. */
static u16 debug_pad_buttons;
static long debug_pad_hold, debug_pad_release;
static int debug_pad_forever;
static int debug_pad_repause; /* the sync pad op found the game paused: pause it again when done */
static long debug_pad_applied_frame = -1;
/* A port's pad: `buttons` for `hold` more vsyncs (or forever), then 0 for `release` vsyncs, then 0. Applied once per
* frame (applied_frame), at the op itself and at every later port_debug_frame, while port_debug_pad_owned has the
* port's bit. */
typedef struct DebugPad {
u16 buttons;
long hold, release;
int forever;
long applied_frame;
} DebugPad;
static DebugPad debug_pads[PORT_PADS] = { { 0, 0, 0, 0, -1 }, { 0, 0, 0, 0, -1 } };
static int debug_pad_sync_port; /* the port of the pending sync pad op */
static int debug_pad_repause; /* the sync pad op found the game paused: pause it again when done */

static int debug_reset_pending, debug_reset_repause; /* a reset asked for; it was asked for while paused */
static int debug_load_pending, debug_load_repause; /* a load_state asked for; it was asked for while paused */
Expand All @@ -170,17 +179,24 @@ static char debug_save_path[4096]; /* DEBUG_SAVE's file */

/* ---- The socket */

/* The channel lets go of a port it owns: port 0 back to a pad with nothing pressed, port 1 to nothing in it, until its
* next owner (the window) sets it. */
static void debug_pad_free_port(int port) {
if (port_debug_pad_owned & (1 << port)) {
port_debug_pad_owned &= ~(1 << port);
psyq_pad_set(port, port == 0, 0);
port_framelog_input(port, port == 0, 0);
}
}

static void debug_disconnect(const char *why) {
port_log("debug: client disconnected (%s)", why);
close(debug_client_fd);
debug_client_fd = -1;
debug_line_n = 0;
debug_pending = DEBUG_NONE; /* its answer has no one to go to */
if (port_debug_pad_owned) {
port_debug_pad_owned = 0;
psyq_pad_set(0, 1, 0);
port_framelog_input(0, 1, 0);
}
debug_pad_free_port(0);
debug_pad_free_port(1);
port_pump_resume_request(); /* a dead tool never leaves the game held */
}

Expand Down Expand Up @@ -463,48 +479,53 @@ static int debug_wait_hit(void) {

/* ---- The pad */

static void debug_pad_apply(void) {
static void debug_pad_apply(int port) {
DebugPad *d = &debug_pads[port];
u16 v = 0;
if (debug_pad_forever) {
v = debug_pad_buttons;
} else if (debug_pad_hold > 0) {
v = debug_pad_buttons;
debug_pad_hold--;
} else if (debug_pad_release > 0) {
debug_pad_release--;
if (d->forever) {
v = d->buttons;
} else if (d->hold > 0) {
v = d->buttons;
d->hold--;
} else if (d->release > 0) {
d->release--;
}
psyq_pad_set(0, 1, v);
port_framelog_input(0, 1, v);
debug_pad_applied_frame = port_frames;
psyq_pad_set(port, 1, v);
port_framelog_input(port, 1, v);
d->applied_frame = port_frames;
}

/* Once per vsync: a sync whose frames + release vsyncs all ran is answered, then the pad gets this vsync's value
* (unless the op itself applied it at this frame: a pad op handled at frame F sets the pad the game reads during
* F + 1). */
static void debug_pad_frame(void) {
const DebugPad *sync = &debug_pads[debug_pad_sync_port];
int p;
if (!port_debug_pad_owned) {
return;
}
if (debug_pending == DEBUG_PAD_SYNC && !debug_pad_forever && debug_pad_hold == 0 && debug_pad_release == 0) {
if (debug_pending == DEBUG_PAD_SYNC && !sync->forever && sync->hold == 0 && sync->release == 0) {
debug_pending = DEBUG_NONE;
if (debug_pad_repause) {
port_pump_pause_request(); /* a press from the paused game leaves it paused: the tool's next call finds
* the frame it was answered at, not thousands of unthrottled frames later */
}
debug_ok(&debug_pending_req, "\"frame\": %ld", port_frames);
}
if (debug_pad_applied_frame != port_frames) {
debug_pad_apply();
for (p = 0; p < PORT_PADS; p++) {
if ((port_debug_pad_owned & (1 << p)) && debug_pads[p].applied_frame != port_frames) {
debug_pad_apply(p);
}
}
}

/* ---- The ops */

static const char *debug_pad_owner(void) {
static const char *debug_pad_owner(int port) {
if (port_script_active) {
return "script";
}
if (port_debug_pad_owned) {
if (port_debug_pad_owned & (1 << port)) {
return "debug";
}
return port_window ? "window" : "none";
Expand All @@ -513,10 +534,12 @@ static const char *debug_pad_owner(void) {
static void debug_op_status(const DebugReq *req) {
debug_ok(req,
"\"frame\": %ld, \"stage\": %d, \"file\": %d, \"map\": %u, \"slot1_word0\": %u, \"paused\": %d, "
"\"pace\": %ld, \"rate\": %ld, \"window\": %d, \"script\": %d, \"pad_owner\": \"%s\", \"pad\": %u",
"\"pace\": %ld, \"rate\": %ld, \"window\": %d, \"script\": %d, \"pad_owner\": \"%s\", \"pad\": %u, "
"\"pad2_owner\": \"%s\", \"pad2\": %u, \"pad2_connected\": %d",
port_frames, game_state_stage(), game_state_file(), (u32)game_state_map(), port_state_slot1_word0(),
port_pump_paused(), port_pace_get(), port_rate, port_window != 0, port_script_active != 0,
debug_pad_owner(), port_framelog_last_input(0));
debug_pad_owner(0), port_framelog_last_input(0), debug_pad_owner(1), port_framelog_last_input(1),
psyq_pad_present(1));
}

static void debug_op_step(const DebugReq *req, const PortJson *obj) {
Expand Down Expand Up @@ -572,25 +595,29 @@ static void debug_op_poke(const DebugReq *req, const PortJson *obj, int ps1) {
}

static void debug_op_pad(const DebugReq *req, const PortJson *obj) {
long long buttons, frames, release;
long long buttons, frames, release, port;
DebugPad *d;
int sync;
if (port_script_active) {
debug_error(req, "script owns the pad");
return;
}
if (!debug_int_req(req, obj, "buttons", 0, 0xFFFF, &buttons) ||
!debug_int_opt(req, obj, "frames", 0, 0, 0x7FFFFFFF, &frames) ||
!debug_int_opt(req, obj, "release", 0, 0, 0x7FFFFFFF, &release) || !debug_bool_opt(req, obj, "sync", &sync)) {
!debug_int_opt(req, obj, "release", 0, 0, 0x7FFFFFFF, &release) || !debug_bool_opt(req, obj, "sync", &sync) ||
!debug_int_opt(req, obj, "port", 0, 0, PORT_PADS - 1, &port)) {
return;
}
port_debug_pad_owned = 1;
debug_pad_buttons = (u16)buttons;
debug_pad_forever = frames == 0;
debug_pad_hold = (long)frames;
debug_pad_release = (long)release;
debug_pad_apply();
if (sync && !debug_pad_forever) {
port_debug_pad_owned |= 1 << port;
d = &debug_pads[port];
d->buttons = (u16)buttons;
d->forever = frames == 0;
d->hold = (long)frames;
d->release = (long)release;
debug_pad_apply((int)port);
if (sync && !d->forever) {
debug_pending = DEBUG_PAD_SYNC;
debug_pad_sync_port = (int)port;
debug_pending_req = *req;
debug_pad_repause = port_pump_paused();
port_pump_resume_request();
Expand All @@ -599,11 +626,16 @@ static void debug_op_pad(const DebugReq *req, const PortJson *obj) {
debug_ok(req, "\"frame\": %ld", port_frames);
}

static void debug_op_pad_free(const DebugReq *req) {
if (port_debug_pad_owned) {
port_debug_pad_owned = 0;
psyq_pad_set(0, 1, 0);
port_framelog_input(0, 1, 0);
static void debug_op_pad_free(const DebugReq *req, const PortJson *obj) {
long long port;
if (!debug_int_opt(req, obj, "port", -1, 0, PORT_PADS - 1, &port)) {
return;
}
if (port < 0 || port == 0) {
debug_pad_free_port(0);
}
if (port < 0 || port == 1) {
debug_pad_free_port(1);
}
debug_ok(req, NULL);
}
Expand Down Expand Up @@ -859,7 +891,7 @@ static void debug_handle(const char *line, size_t n) {
} else if (strcmp(op->string, "pad") == 0) {
debug_op_pad(&req, root);
} else if (strcmp(op->string, "pad_free") == 0) {
debug_op_pad_free(&req);
debug_op_pad_free(&req, root);
} else if (strcmp(op->string, "wait") == 0) {
debug_op_wait(&req, root);
} else if (strcmp(op->string, "screenshot") == 0) {
Expand Down
5 changes: 4 additions & 1 deletion runtime/input.c
Original file line number Diff line number Diff line change
Expand Up @@ -1397,8 +1397,11 @@ void port_input_frame(void) {
input_poll();
if (!port_script_active) {
int p;
for (p = port_debug_pad_owned ? 1 : 0; p < PORT_PADS; p++) { /* the debug channel drives port 0 */
for (p = 0; p < PORT_PADS; p++) {
int connected = input_connected(p);
if (port_debug_pad_owned & (1 << p)) {
continue; /* the debug channel drives that port */
}
u16 bits = connected ? input_buttons(p) : 0;
psyq_pad_set(p, connected, bits);
port_framelog_input(p, connected, bits);
Expand Down
Loading
Loading