Merge pull request #686 from RetroPortingToolKit/fix/debug-input-route-port2

debug: schedule either controller from a native saved state
This commit is contained in:
Matt Stanley
2026-10-18 16:16:05 -07:00
committed by GitHub
4 changed files with 220 additions and 22 deletions
+1 -1
View File
@@ -125,7 +125,7 @@ Columns: **N** = native, **D** = DuckStation oracle.
| `mmio_clear` | ✓ | | — | Reset the MMIO write ring |
| `gp1_dump` | ✓ | | optional `frame_lo`, `frame_hi`, `count`, `newest` | Dump the dedicated ALWAYS-ON GP1 (0x1F801814 display control) ring — 512K entries ≈ 15 min of gameplay (Tomba writes ~10 GP1/frame), survives the general MMIO ring's eviction. Frame filter is server-side over the full ring. Each entry: val + func/pc/cpu_pc/ra/sp/a0/a1/sr/epc/frame |
| `gcp_ring` | ✓ | | `frame_lo`, `frame_hi`, then `path` or `key` + `top` | Always-on guest-cycle profile ring (512K samples, one every `period` = 4096 guest cycles by default, `PSX_GCP_PERIOD`; about 63 guest seconds). Each sample: cycle, frame, `weight` (sample periods crossed, so idle-skipped waits stay credited to the waiting code), backend `phase`, exception flag, last block leader (`block`, debug-tools builds), last function entered (`fn`), guest `$ra`, native shard in progress, static-dispatch stamp. No args: ring stats. `path`: write the window as CSV. `key` = `fn`/`block`/`ra`/`native`/`disp`: top `top` keys by guest-cycle share in the window |
| `input_route_start` | ✓ | | optional `at_frame` | Start the route queued with `input_route_append` (`buttons`, `frames` per step, counted per pad poll). With `at_frame` the route waits for that vblank count, so it replays on the same guest frames in every run (frame-exact A/B with input) |
| `input_route_start` | ? | | optional `at_frame`, `port`, `after_load` | Start queued segments (`buttons`, `frames`). `port` selects guest pad 1 (default) or 2; only that pad consumes segments, once per vblank. `at_frame` defers to the host debug vblank count, which does not restore with a savestate. `after_load: 1` instead waits for the next successful native load, then starts from that guest boundary; it cannot combine with a positive `at_frame`. Start/status report `waiting_for_load`. A failed load leaves the route waiting; stop or clear to cancel it. One route runs at a time. This overrides guest input, not authoritative netplay host input. Clearing the selected port (or both) stops it; clearing the other port preserves it. |
| `batch_sites` | ✓ | | — | Always-on OpenGL batch-flush attribution: cumulative flushes of non-empty batches by caller source line in `gpu_gl_renderer.c`, as `tex` and `flat` arrays of `[line, count]`. Diff two reads to attribute a window (every flush call site is covered by the `flush_*_batch()` macros) |
| `pc_break` | | ✓² | `addr` | DS execute breakpoint, state captured on hit (via `pc_hit_last`) |
| `pc_unbreak` | | ✓² | `addr` | Remove an execute breakpoint |
+4
View File
@@ -1780,6 +1780,10 @@ if(BUILD_TESTING)
COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_gpu_gp0_history_guard.py
--cc ${CMAKE_C_COMPILER})
add_test(NAME debug_input_route_test
COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_debug_input_route.py
--cc ${CMAKE_C_COMPILER})
add_test(NAME memory_debug_trace_guard_test
COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_memory_debug_trace_guard.py
+66 -21
View File
@@ -324,8 +324,11 @@ static uint32_t s_input_route_count = 0;
static uint32_t s_input_route_index = 0;
static uint32_t s_input_route_remaining = 0;
static int s_input_route_active = 0;
/* input_route_start at_frame: the route waits for this vblank count, so a
* route replays on the same guest frames in every run (0 = start now). */
static unsigned s_input_route_port = 1;
static int s_input_route_wait_load = 0;
static void input_route_loaded(void);
/* at_frame uses host debug history, which is not restored with a snapshot.
* after_load instead waits for a successful native load (0 = start now). */
static uint64_t s_input_route_start_frame = 0;
/* ---- Frontend turbo override ---- */
@@ -5040,6 +5043,7 @@ static void handle_frame(int id, const char *json)
static int s_fp_reset_on_load = 0;
void debug_server_note_savestate_loaded(void)
{
input_route_loaded();
if (!s_fp_reset_on_load) return;
s_fp_reset_on_load = 0;
s_fp = (PsxFrameFingerprint)PSX_FRAME_FINGERPRINT_INIT;
@@ -8248,14 +8252,17 @@ static void handle_pad_status(int id, const char *json)
static void handle_clear_input(int id, const char *json)
{
const int port = json_get_int(json, "port", 0); /* 0 = both */
if (port == 0 || port == (int)s_input_route_port) {
s_input_route_active = 0;
s_input_route_wait_load = 0;
s_input_route_index = 0;
s_input_route_remaining = 0;
}
if (port == 0 || port == 2) {
s_input_override_p2 = -1;
s_input_frames_p2 = 0;
}
if (port == 2) { send_ok(id); return; }
s_input_route_active = 0;
s_input_route_index = 0;
s_input_route_remaining = 0;
s_input_override = -1;
s_input_frames = 0;
s_axis_override = 0;
@@ -8272,6 +8279,7 @@ static void handle_input_route_clear(int id, const char *json)
{
(void)json;
s_input_route_active = 0;
s_input_route_wait_load = 0;
s_input_route_count = 0;
s_input_route_index = 0;
s_input_route_remaining = 0;
@@ -8306,25 +8314,42 @@ static void handle_input_route_start(int id, const char *json)
if (s_input_route_count == 0) {
send_err(id, "input route is empty"); return;
}
s_input_override = -1;
s_input_frames = 0;
s_axis_override = 0;
s_pad_type_override = -1;
const int port = json_get_int(json, "port", 1);
if (port != 1 && port != 2) {
send_err(id, "port must be 1 or 2"); return;
}
const int at = json_get_int(json, "at_frame", 0);
const int after_load = json_get_int(json, "after_load", 0);
if (after_load < 0 || after_load > 1 || (after_load && at > 0)) {
send_err(id, "after_load must be 0 or 1 and cannot combine with at_frame"); return;
}
s_input_route_port = (unsigned)port;
if (port == 2) {
s_input_override_p2 = -1;
s_input_frames_p2 = 0;
} else {
s_input_override = -1;
s_input_frames = 0;
s_axis_override = 0;
s_pad_type_override = -1;
}
s_input_route_index = 0;
s_input_route_remaining = s_input_route[0].frames;
const int at = json_get_int(json, "at_frame", 0);
s_input_route_start_frame = at > 0 && (uint64_t)at > s_frame_count ? (uint64_t)at : 0u;
s_input_route_wait_load = after_load;
s_input_route_active = 1;
send_fmt("{\"id\":%d,\"ok\":true,\"steps\":%u,\"start_frame\":%llu}\n",
id, (unsigned)s_input_route_count,
(unsigned long long)(s_input_route_start_frame ? s_input_route_start_frame
: s_frame_count));
send_fmt("{\"id\":%d,\"ok\":true,\"steps\":%u,\"port\":%u,\"waiting_for_load\":%s,\"start_frame\":%llu}\n",
id, (unsigned)s_input_route_count, s_input_route_port,
after_load ? "true" : "false",
(unsigned long long)(after_load ? 0u : s_input_route_start_frame ? s_input_route_start_frame
: s_frame_count));
}
static void handle_input_route_stop(int id, const char *json)
{
(void)json;
s_input_route_active = 0;
s_input_route_wait_load = 0;
s_input_route_remaining = 0;
send_ok(id);
}
@@ -8333,9 +8358,10 @@ static void handle_input_route_status(int id, const char *json)
{
(void)json;
send_fmt("{\"id\":%d,\"ok\":true,\"active\":%s,\"steps\":%u,"
"\"index\":%u,\"remaining\":%u}\n",
"\"port\":%u,\"waiting_for_load\":%s,\"index\":%u,\"remaining\":%u}\n",
id, s_input_route_active ? "true" : "false",
(unsigned)s_input_route_count, (unsigned)s_input_route_index,
(unsigned)s_input_route_count, s_input_route_port,
s_input_route_wait_load ? "true" : "false", (unsigned)s_input_route_index,
(unsigned)s_input_route_remaining);
}
@@ -17118,10 +17144,20 @@ int debug_server_is_connected(void)
return s_client != SOCK_INVALID;
}
int debug_server_get_input_override(void)
static void input_route_loaded(void)
{
if (s_input_route_active && s_input_route_index < s_input_route_count &&
s_frame_count >= s_input_route_start_frame) {
if (!s_input_route_active || !s_input_route_wait_load || !s_input_route_count) return;
s_input_route_wait_load = 0;
s_input_route_index = 0;
s_input_route_remaining = s_input_route[0].frames;
s_input_route_start_frame = 0;
}
/* One selected guest port consumes the route once per VBlank. Reading the
* other port must not advance it or shorten any segment. */
static int input_route_next(unsigned port)
{
if (port == s_input_route_port && s_input_route_active && s_input_route_index < s_input_route_count &&
!s_input_route_wait_load && s_frame_count >= s_input_route_start_frame) {
int current = (int)s_input_route[s_input_route_index].buttons;
if (s_input_route_remaining > 0 && --s_input_route_remaining == 0) {
s_input_route_index++;
@@ -17134,7 +17170,14 @@ int debug_server_get_input_override(void)
}
return current;
}
int current = s_input_override;
return -1;
}
int debug_server_get_input_override(void)
{
int current = input_route_next(1);
if (current >= 0) return current;
current = s_input_override;
if (s_input_override >= 0 && s_input_frames > 0) {
if (--s_input_frames == 0)
s_input_override = -1;
@@ -17144,7 +17187,9 @@ int debug_server_get_input_override(void)
int debug_server_get_input_override_port2(void)
{
int current = s_input_override_p2;
int current = input_route_next(2);
if (current >= 0) return current;
current = s_input_override_p2;
if (s_input_override_p2 >= 0 && s_input_frames_p2 > 0) {
if (--s_input_frames_p2 == 0)
s_input_override_p2 = -1;
+149
View File
@@ -0,0 +1,149 @@
"""Exercise the actual debug route handlers/getters without SDL or a game.
Extract the unchanged function bodies from debug_server.c; stub only JSON and
responses. Live game qualification must additionally check native pad input.
"""
import argparse
from pathlib import Path
import re
import subprocess
import tempfile
def function(source, name):
found = re.search(r"^(?:static )?(?:int|void) " + name + r"\([^\n]*\)\n\{.*?^\}",
source, re.M | re.S)
if not found:
raise ValueError("Missing source function: " + name)
return found[0]
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--cc', required=True)
args = parser.parse_args()
source = (Path(__file__).resolve().parents[1] / 'src/debug_server.c').read_text()
declarations = source.split('#define INPUT_ROUTE_MAX_STEPS', 1)[1].split(
'static uint64_t s_input_route_start_frame = 0;', 1)[0]
preamble = r'''
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#define PSX_BSS
static uint64_t s_frame_count;
typedef struct { unsigned unused; } PsxFrameFingerprint;
#define PSX_FRAME_FINGERPRINT_INIT {0}
static int s_fp_reset_on_load;
static PsxFrameFingerprint s_fp;
static unsigned s_fp_head,s_fp_total;
static int s_input_override=-1,s_input_override_p2=-1;
static int s_input_frames,s_input_frames_p2,s_axis_override,s_pad_type_override=-1;
static unsigned char s_axis_st[4],s_host_axes[4],s_host_lt,s_host_rt;
static int s_host_layer;
static uint16_t s_host_buttons;
static char response[512];
static void send_ok(int id){(void)id;strcpy(response,"ok");}
static void send_err(int id,const char *text){(void)id;snprintf(response,sizeof response,"ERROR:%s",text);}
static void send_fmt(const char *format,...){va_list v;va_start(v,format);vsnprintf(response,sizeof response,format,v);va_end(v);}
static int json_get_int(const char *json,const char *key,int fallback){
char name[80];snprintf(name,sizeof name,"\"%s\"",key);
const char *p=strstr(json,name);if(!p)return fallback;
p=strchr(p,':');assert(p);return atoi(p+1);
}
'''
body = '\n'.join(function(source, name) for name in (
'handle_clear_input', 'handle_input_route_clear', 'handle_input_route_append',
'handle_input_route_start', 'handle_input_route_stop', 'handle_input_route_status',
'input_route_loaded', 'debug_server_note_savestate_loaded',
'input_route_next', 'debug_server_get_input_override',
'debug_server_get_input_override_port2'))
checks = r'''
static void queue(void){
handle_input_route_clear(1,"{}");
handle_input_route_append(1,"{\"frames\":2,\"buttons\":32767}");
handle_input_route_append(1,"{\"frames\":1,\"buttons\":65535}");
}
int main(void){
queue();s_input_override=123;s_input_override_p2=456;
handle_input_route_start(1,"{\"port\":2,\"at_frame\":10}");
assert(strstr(response,"\"port\":2"));
assert(s_input_override==123&&s_input_override_p2==-1);
s_frame_count=9;
assert(debug_server_get_input_override_port2()==-1);
assert(s_input_route_remaining==2);
s_frame_count=10;
assert(debug_server_get_input_override()==123);
assert(s_input_route_remaining==2);
assert(debug_server_get_input_override_port2()==32767);
assert(s_input_route_remaining==1);
++s_frame_count;
assert(debug_server_get_input_override()==123);
assert(debug_server_get_input_override_port2()==32767);
assert(s_input_route_index==1&&s_input_route_remaining==1);
++s_frame_count;
assert(debug_server_get_input_override_port2()==65535);
assert(!s_input_route_active);
assert(debug_server_get_input_override_port2()==-1);
queue();handle_input_route_start(1,"{\"port\":2}");
handle_clear_input(1,"{\"port\":1}");assert(s_input_route_active);
handle_clear_input(1,"{\"port\":2}");assert(!s_input_route_active);
queue();handle_input_route_start(1,"{}");
assert(s_input_route_port==1);s_input_override_p2=456;
assert(debug_server_get_input_override_port2()==456&&s_input_route_remaining==2);
assert(debug_server_get_input_override()==32767);
handle_clear_input(1,"{\"port\":2}");assert(s_input_route_active);
handle_input_route_status(1,"{}");assert(strstr(response,"\"port\":1"));
handle_input_route_append(1,"{\"frames\":1,\"buttons\":0}");
assert(strstr(response,"ERROR"));
handle_input_route_stop(1,"{}");assert(!s_input_route_active);
handle_input_route_start(1,"{\"port\":3}");
assert(strstr(response,"ERROR")&&!s_input_route_active);
handle_input_route_start(1,"{\"port\":2}");
handle_clear_input(1,"{}");assert(!s_input_route_active);
handle_input_route_clear(1,"{}");handle_input_route_start(1,"{}");
assert(strstr(response,"empty"));
queue();handle_input_route_start(1,"{\"port\":2,\"after_load\":1}");
assert(s_input_route_active&&s_input_route_wait_load);
assert(strstr(response,"\"waiting_for_load\":true"));
for(unsigned n=0;n<20;++n){++s_frame_count;assert(debug_server_get_input_override_port2()==-1);}
assert(s_input_route_remaining==2);
handle_input_route_status(1,"{}");assert(strstr(response,"\"waiting_for_load\":true"));
/* Route admission is independent of optional fingerprint reset. */
debug_server_note_savestate_loaded();assert(!s_input_route_wait_load);
assert(debug_server_get_input_override_port2()==32767&&s_input_route_remaining==1);
debug_server_note_savestate_loaded(); /* An ordinary later load does not restart it. */
assert(s_input_route_remaining==1);
assert(debug_server_get_input_override_port2()==32767);
assert(debug_server_get_input_override_port2()==65535&&!s_input_route_active);
for(unsigned port=1;port<=2;++port){
queue();handle_input_route_start(1,port==1?"{\"after_load\":1}":"{\"port\":2,\"after_load\":1}");
handle_clear_input(1,port==1?"{\"port\":2}":"{\"port\":1}");assert(s_input_route_wait_load);
handle_clear_input(1,port==1?"{\"port\":1}":"{\"port\":2}");assert(!s_input_route_wait_load&&!s_input_route_active);
}
queue();handle_input_route_start(1,"{\"after_load\":1}");
handle_input_route_stop(1,"{}");debug_server_note_savestate_loaded();assert(!s_input_route_active);
queue();handle_input_route_start(1,"{\"after_load\":1}");
handle_input_route_clear(1,"{}");debug_server_note_savestate_loaded();assert(!s_input_route_active);
queue();handle_input_route_start(1,"{\"after_load\":2}");assert(strstr(response,"ERROR")&&!s_input_route_active);
handle_input_route_start(1,"{\"after_load\":1,\"at_frame\":10}");assert(strstr(response,"ERROR")&&!s_input_route_active);
handle_input_route_start(1,"{\"after_load\":1}");s_fp_reset_on_load=1;s_fp_head=4;s_fp_total=5;
debug_server_note_savestate_loaded();assert(!s_input_route_wait_load&&!s_fp_reset_on_load&&!s_fp_head&&!s_fp_total);
puts("debug input route: P2 timing, isolation, deferred start, clearing and legacy P1 pass");
return 0;
}
'''
with tempfile.TemporaryDirectory(prefix='psx-input-route-') as directory:
path = Path(directory) / 'route.c'
path.write_text(preamble + '\n#define INPUT_ROUTE_MAX_STEPS' + declarations +
'static uint64_t s_input_route_start_frame = 0;\n' + body + checks)
executable = Path(directory) / 'route.exe'
subprocess.run([args.cc, '-std=c11', '-Wall', '-Wextra', '-Werror', str(path),
'-o', str(executable)], check=True)
subprocess.run([str(executable)], check=True)
if __name__ == '__main__':
main()