From fee33960cdc0a7520a236d757ded4f35ecaab482 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sat, 24 Oct 2026 09:51:59 -0700 Subject: [PATCH] Compose analog routes with port and post-load admission; regenerate BIOS dispatcher --- generated/OpenBIOS.emitter.sha | 2 +- generated/OpenBIOS_dispatch.c | 19 ++++++++ runtime/src/debug_server.c | 15 ++++--- runtime/tests/test_debug_input_route.py | 60 +++++++++++++------------ 4 files changed, 61 insertions(+), 35 deletions(-) diff --git a/generated/OpenBIOS.emitter.sha b/generated/OpenBIOS.emitter.sha index ea2c91b8..2b7c6987 100644 --- a/generated/OpenBIOS.emitter.sha +++ b/generated/OpenBIOS.emitter.sha @@ -1 +1 @@ -19ca6a92f84105422cbcdf584777ddc955cf76d80f574891d090a88b88780eed +e2bf436a77fe1e353ebd47bde9c1a99231b577c4ffd84170295866a3c8133699 diff --git a/generated/OpenBIOS_dispatch.c b/generated/OpenBIOS_dispatch.c index 53da932f..67f5443a 100644 --- a/generated/OpenBIOS_dispatch.c +++ b/generated/OpenBIOS_dispatch.c @@ -36,6 +36,13 @@ extern int (*g_psx_bios_hle_hook)(CPUState* cpu, uint32_t phys); extern uint32_t g_psx_mod_guest_functions; extern int psx_mod_dispatch_guest_function(CPUState*, uint32_t); +/* Game function-override tier (runtime/include/func_override.h): + * hand-written C registered against a guest address. NULL (the + * default) = no overrides, dispatch identical to a build without + * the tier. Returns 1 when the override handled the call; the + * guest resumes at $ra exactly as if the original ran jr $ra. */ +extern int (*g_psx_func_override_hook)(CPUState* cpu, uint32_t phys); + #ifdef PSX_HAS_GAME_DISPATCH extern int psx_game_address_in_text(uint32_t addr); #endif @@ -9111,6 +9118,18 @@ static void psx_dispatch_impl(CPUState* cpu, uint32_t addr, uint32_t stop_addr) /* Byte-guarded A0/B0/C0 call-vector tail stubs. */ if (!found && psx_bios_try_native_call_stub(cpu, addr)) found = 1; + /* Function-override tier: AFTER the BIOS tiers (an override + * can never shadow a kernel service vector), BEFORE every + * game code backend, so one address-keyed hook covers the + * static EXE, runtime-loaded overlays and dirty RAM alike. + * Handled (rc 1) => the override completed against guest + * state; resume at $ra via the trampoline's normal + * return/tail contract. rc 0 => fall through untouched. */ + if (!found && g_psx_func_override_hook && + g_psx_func_override_hook(cpu, addr & 0x1FFFFFFFu)) { + cpu->pc = cpu->gpr[31]; + found = 1; + } #ifdef PSX_HAS_GAME_DISPATCH /* Game EXEs can overlap the BIOS shell copy window at * physical 0x30000-0x5AFFF. If the target belongs to the diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 639712b4..c9e008a4 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -8416,10 +8416,8 @@ static void handle_clear_input(int id, const char *json) } if(port>=3) {send_ok(id);return;} if (port == 0 || port == (int)s_input_route_port) { - s_input_route_active = 0; + input_route_stop(&s_route); s_input_route_wait_load = 0; - s_input_route_index = 0; - s_input_route_remaining = 0; } if (port == 0 || port == 2) { s_pad_connected_override[1] = -1; @@ -8502,10 +8500,10 @@ static void handle_input_route_status(int id, const char *json) uint8_t ax[4] = { 0x80, 0x80, 0x80, 0x80 }; const int axes_live = input_route_frame_axes(&s_route, ax); send_fmt("{\"id\":%d,\"ok\":true,\"active\":%s,\"steps\":%u," - "\"index\":%u,\"remaining\":%u,\"axes_live\":%s," + "\"port\":%u,\"waiting_for_load\":%s,\"index\":%u,\"remaining\":%u,\"axes_live\":%s," "\"axes\":[%u,%u,%u,%u]}\n", id, s_route.active ? "true" : "false", - (unsigned)s_route.count, (unsigned)s_route.index, + (unsigned)s_route.count, s_input_route_port, s_input_route_wait_load?"true":"false", (unsigned)s_route.index, (unsigned)s_route.remaining, axes_live ? "true" : "false", ax[0], ax[1], ax[2], ax[3]); } @@ -17867,6 +17865,13 @@ int debug_server_get_input_override_port2(void) return current; } +int debug_server_get_input_override_extra(unsigned slot) { + if(slot<2 || slot>=8 || !s_extra_active[slot])return -1; + int value=s_extra_buttons[slot]; + if(s_extra_frames[slot]>0 && !--s_extra_frames[slot])s_extra_active[slot]=0; + return value; +} + int debug_server_get_axis_override(unsigned char st[4]) { /* A route step with axes owns the sticks for the poll it drove. */ diff --git a/runtime/tests/test_debug_input_route.py b/runtime/tests/test_debug_input_route.py index 5b832692..f63ee31b 100644 --- a/runtime/tests/test_debug_input_route.py +++ b/runtime/tests/test_debug_input_route.py @@ -11,7 +11,7 @@ import tempfile def function(source, name): - found = re.search(r"^(?:static )?(?:int|void) " + name + r"\([^\n]*\)\n\{.*?^\}", + found = re.search(r"^(?:static )?(?:int|void) " + name + r"\([^\n]*\)\s*\{.*?^\}", source, re.M | re.S) if not found: raise ValueError("Missing source function: " + name) @@ -23,10 +23,9 @@ def main(): 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 "input_route.h" #include #include #include @@ -43,6 +42,9 @@ 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 int s_pad_connected_override[2]={-1,-1}; static int s_extra_active[8],s_extra_frames[8]; +static InputRoute s_route; +static unsigned s_input_route_port=1; +static int s_input_route_wait_load; 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; @@ -60,7 +62,7 @@ static int json_get_int(const char *json,const char *key,int fallback){ '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', + 'input_route_poll_port', 'debug_server_get_input_override', 'debug_server_get_input_override_port2')) checks = r''' static void queue(void){ @@ -75,62 +77,62 @@ int main(void){ 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); + assert(s_route.remaining==2); s_frame_count=10; assert(debug_server_get_input_override()==123); - assert(s_input_route_remaining==2); + assert(s_route.remaining==2); assert(debug_server_get_input_override_port2()==32767); - assert(s_input_route_remaining==1); + assert(s_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); + assert(s_route.index==1&&s_route.remaining==1); ++s_frame_count; assert(debug_server_get_input_override_port2()==65535); - assert(!s_input_route_active); + assert(!s_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); + handle_clear_input(1,"{\"port\":1}");assert(s_route.active); + handle_clear_input(1,"{\"port\":2}");assert(!s_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_port2()==456&&s_route.remaining==2); assert(debug_server_get_input_override()==32767); - handle_clear_input(1,"{\"port\":2}");assert(s_input_route_active); + handle_clear_input(1,"{\"port\":2}");assert(s_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_stop(1,"{}");assert(!s_route.active); handle_input_route_start(1,"{\"port\":3}"); - assert(strstr(response,"ERROR")&&!s_input_route_active); + assert(strstr(response,"ERROR")&&!s_route.active); handle_input_route_start(1,"{\"port\":2}"); - handle_clear_input(1,"{}");assert(!s_input_route_active); + handle_clear_input(1,"{}");assert(!s_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(s_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); + assert(s_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); + assert(debug_server_get_input_override_port2()==32767&&s_route.remaining==1); debug_server_note_savestate_loaded(); /* An ordinary later load does not restart it. */ - assert(s_input_route_remaining==1); + assert(s_route.remaining==1); assert(debug_server_get_input_override_port2()==32767); - assert(debug_server_get_input_override_port2()==65535&&!s_input_route_active); + assert(debug_server_get_input_override_port2()==65535&&!s_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); + handle_clear_input(1,port==1?"{\"port\":1}":"{\"port\":2}");assert(!s_input_route_wait_load&&!s_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); + handle_input_route_stop(1,"{}");debug_server_note_savestate_loaded();assert(!s_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_clear(1,"{}");debug_server_note_savestate_loaded();assert(!s_route.active); + queue();handle_input_route_start(1,"{\"after_load\":2}");assert(strstr(response,"ERROR")&&!s_route.active); + handle_input_route_start(1,"{\"after_load\":1,\"at_frame\":10}");assert(strstr(response,"ERROR")&&!s_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"); @@ -139,11 +141,11 @@ int main(void){ ''' 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) + path.write_text(preamble + body + checks,encoding='utf-8',newline='\n') executable = Path(directory) / 'route.exe' subprocess.run([args.cc, '-std=c11', '-Wall', '-Wextra', '-Werror', str(path), - '-o', str(executable)], check=True) + '-I',str(Path(__file__).resolve().parents[1]/'include'), + str(Path(__file__).resolve().parents[1]/'src/input_route.c'),'-o', str(executable)], check=True) subprocess.run([str(executable)], check=True)