mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
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:
@@ -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 |
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user