From f2344be7436a0fd76fcad4207a491309fcd93690 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sun, 18 Oct 2026 11:46:36 -0700 Subject: [PATCH 1/2] debug: allow frame-scheduled input routes on guest pad two --- docs/TCP_COMMANDS.md | 2 +- runtime/CMakeLists.txt | 4 + runtime/src/debug_server.c | 54 ++++++++--- runtime/tests/test_debug_input_route.py | 117 ++++++++++++++++++++++++ 4 files changed, 161 insertions(+), 16 deletions(-) create mode 100644 runtime/tests/test_debug_input_route.py diff --git a/docs/TCP_COMMANDS.md b/docs/TCP_COMMANDS.md index 185ce496..d23beb18 100644 --- a/docs/TCP_COMMANDS.md +++ b/docs/TCP_COMMANDS.md @@ -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` | Start queued segments (`buttons`, `frames`). `port` selects guest pad 1 (default) or 2; only that pad consumes segments, once per vblank. `at_frame` defers the start to that vblank count. 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 leaves it running. Start/status responses include `port`. | | `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 | diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 92208095..61af36f0 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -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 diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 805c6a54..5fb710f4 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -324,6 +324,7 @@ 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; +static unsigned s_input_route_port = 1; /* 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 uint64_t s_input_route_start_frame = 0; @@ -8248,14 +8249,16 @@ 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_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; @@ -8306,17 +8309,27 @@ 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; + } + 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_active = 1; - send_fmt("{\"id\":%d,\"ok\":true,\"steps\":%u,\"start_frame\":%llu}\n", - id, (unsigned)s_input_route_count, + send_fmt("{\"id\":%d,\"ok\":true,\"steps\":%u,\"port\":%u,\"start_frame\":%llu}\n", + id, (unsigned)s_input_route_count, s_input_route_port, (unsigned long long)(s_input_route_start_frame ? s_input_route_start_frame : s_frame_count)); } @@ -8333,9 +8346,9 @@ 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,\"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, (unsigned)s_input_route_index, (unsigned)s_input_route_remaining); } @@ -17118,9 +17131,11 @@ int debug_server_is_connected(void) return s_client != SOCK_INVALID; } -int debug_server_get_input_override(void) +/* 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 (s_input_route_active && s_input_route_index < s_input_route_count && + if (port == s_input_route_port && s_input_route_active && s_input_route_index < s_input_route_count && 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) { @@ -17134,7 +17149,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 +17166,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; diff --git a/runtime/tests/test_debug_input_route.py b/runtime/tests/test_debug_input_route.py new file mode 100644 index 00000000..e26dd6bb --- /dev/null +++ b/runtime/tests/test_debug_input_route.py @@ -0,0 +1,117 @@ +"""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 +#include +#include +#include +#include +#include +#define PSX_BSS +static uint64_t s_frame_count; +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_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")); + 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() From c647d27c0f708b8dcf5c1ba2259c457993682a9e Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sun, 18 Oct 2026 14:27:04 -0700 Subject: [PATCH 2/2] debug: start queued controller input after a native state load --- docs/TCP_COMMANDS.md | 2 +- runtime/src/debug_server.c | 39 +++++++++++++++++++------ runtime/tests/test_debug_input_route.py | 32 ++++++++++++++++++++ 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/docs/TCP_COMMANDS.md b/docs/TCP_COMMANDS.md index d23beb18..99a68e7e 100644 --- a/docs/TCP_COMMANDS.md +++ b/docs/TCP_COMMANDS.md @@ -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`, `port` | Start queued segments (`buttons`, `frames`). `port` selects guest pad 1 (default) or 2; only that pad consumes segments, once per vblank. `at_frame` defers the start to that vblank count. 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 leaves it running. Start/status responses include `port`. | +| `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 | diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 5fb710f4..abae3dff 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -325,8 +325,10 @@ static uint32_t s_input_route_index = 0; static uint32_t s_input_route_remaining = 0; static int s_input_route_active = 0; static unsigned s_input_route_port = 1; -/* 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 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 ---- */ @@ -5041,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; @@ -8251,6 +8254,7 @@ 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; } @@ -8275,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; @@ -8313,6 +8318,11 @@ static void handle_input_route_start(int id, const char *json) 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; @@ -8325,19 +8335,21 @@ static void handle_input_route_start(int id, const char *json) } 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,\"port\":%u,\"start_frame\":%llu}\n", + 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, - (unsigned long long)(s_input_route_start_frame ? s_input_route_start_frame - : s_frame_count)); + 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); } @@ -8346,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," - "\"port\":%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, s_input_route_port, (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); } @@ -17131,12 +17144,20 @@ int debug_server_is_connected(void) return s_client != SOCK_INVALID; } +static void input_route_loaded(void) +{ + 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_frame_count >= s_input_route_start_frame) { + !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++; diff --git a/runtime/tests/test_debug_input_route.py b/runtime/tests/test_debug_input_route.py index e26dd6bb..ca6f2fb7 100644 --- a/runtime/tests/test_debug_input_route.py +++ b/runtime/tests/test_debug_input_route.py @@ -34,6 +34,11 @@ def main(): #include #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; @@ -52,6 +57,7 @@ static int json_get_int(const char *json,const char *key,int fallback){ 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''' @@ -99,6 +105,32 @@ int main(void){ 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; }