diff --git a/lib/recomp-net b/lib/recomp-net index b06b847d..5336f24b 160000 --- a/lib/recomp-net +++ b/lib/recomp-net @@ -1 +1 @@ -Subproject commit b06b847d67f1b45b1b62d30aeb7758ffe70fe7bd +Subproject commit 5336f24b6d31499c95983fb6753d3de91d5913d6 diff --git a/runtime/include/psx_netplay.h b/runtime/include/psx_netplay.h index 3d3e5625..13191f3b 100644 --- a/runtime/include/psx_netplay.h +++ b/runtime/include/psx_netplay.h @@ -108,17 +108,41 @@ int psx_netplay_input_desync(uint32_t *tick, uint32_t *local_hash, uint32_t *re /* 1 if peer sent BYE or went silent for ~timeout_ms (default 1500). */ int psx_netplay_peer_disconnected(uint32_t timeout_ms); +/* + * Ingress / lobby / INPUT retransmit without try_admit. Used while the + * delay-sync starvation latch holds for remote runway refill. + */ +void psx_netplay_pump(void); + /* * Pump + try_admit for the current sim tick. On success, publish has written * SIO and a finish_frame() is owed after the guest completes that tick. * Returns 1 if admitted, 0 if caller must keep polling (linking / wait). * Does NOT advance the session clock. + * + * After sustained admit misses, latches starvation (pump-only) until + * remote_lead >= D for a few frames, then arms a recovery catch-up boost. + * Env: PSX_NET_STARVATION_ENTER_FRAMES, EXIT_FRAMES, EXIT_HR_LEAD. */ int psx_netplay_poll_admit(void); /* Call after the guest finishes the admitted tick (vblank boundary). */ void psx_netplay_finish_frame(void); +/* highest_remote_wire - sim_tick (0 if inactive; can be negative). */ +int psx_netplay_remote_lead(void); +/* Session input delay frames (default 2 when inactive). */ +int psx_netplay_input_delay(void); + +/* + * Extra headroom for post-starvation / behind-peer catch-up + * (min(16, max(0, remote_lead - D, recovery_burst))). + * Host should skip wall-clock pace while this is > 0, then call + * psx_netplay_catchup_consume_frame() once per skipped pace. + */ +int psx_netplay_catchup_budget(void); +void psx_netplay_catchup_consume_frame(void); + /* Park the admit barrier until a peer datagram may be ready (or timeout). */ void psx_netplay_wait_recv(int timeout_ms); diff --git a/runtime/include/sio.h b/runtime/include/sio.h index 49e146f4..3e2f6f45 100644 --- a/runtime/include/sio.h +++ b/runtime/include/sio.h @@ -79,6 +79,11 @@ int sio_get_multitap(void); /* phys_port: 0 = console Port 1, 1 = console Port 2. Default 0. */ void sio_set_multitap_port(int phys_port); int sio_get_multitap_port(void); +/* 1 when multitap is armed and `logical_slot` is a tap pad (not the lone + * pad on the opposite console port). SCPH-1070 taps are treated as plain + * digital controllers (0x41) — DualShock/analog on a tap is not reliable + * across titles, so host input and SIO type requests are forced digital. */ +int sio_pad_on_multitap(int logical_slot); /* Update pad button state. Buttons use PS1 convention: 0=pressed, 1=released. Bit layout: SELECT, L3, R3, START, UP, RIGHT, DOWN, LEFT, diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 41d6e3ee..4d0a1216 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -2191,6 +2191,14 @@ static int effective_player_mode(const PlayerInput& p) { return p.mode; } +/* Pad mode for the SIO seat this sample will drive. Multitap taps are always + * plain digital (0x41) — DualShock on a SCPH-1070 tap is not reliable. */ +static int effective_player_mode_for_sio(const PlayerInput& p, int sio_slot) { + if (sio_pad_on_multitap(sio_slot)) + return (int)PSXRecompV4::PAD_MODE_DIGITAL; + return effective_player_mode(p); +} + /* Open/close SDL handles so they match g_players, and (re)assert each slot's * PSX connection + pad type. Safe to call repeatedly (hotplug, boot). * While delay-sync netplay is active, SIO connection/type are owned by @@ -2202,13 +2210,14 @@ static void refresh_player_devices(void) { if (p.kind != 2) close_player(p); /* keyboard/none: no handle */ else open_player(p, s); if (netplay) continue; - const int mode = effective_player_mode(p); + const int mode = effective_player_mode_for_sio(p, s); sio_set_pad_connected(s, p.kind != 0 ? 1 : 0); sio_set_pad_analog(s, pad_mode_boot_analog(mode), 0x80, 0x80, 0x80, 0x80); /* DIGITAL mode == a plain digital controller that ignores the DualShock * config-mode commands (real SCPH-1080 behaviour); ANALOG/HYBRID == a * config-capable DualShock. A digital pad that wrongly answered 0x43 - * sent Tomba 2's pad driver down the config path -> phantom 0x00 reads. */ + * sent Tomba 2's pad driver down the config path -> phantom 0x00 reads. + * Multitap taps are always digital (see sio_pad_on_multitap). */ sio_set_pad_config_capable(s, mode != PSXRecompV4::PAD_MODE_DIGITAL); } } @@ -2553,9 +2562,12 @@ static int capture_pad_slot(int s, PsxNetPad* out) { * analog axes below. An assigned device keeps its configured mode (a * launcher-selected analog DualShock stays analog, so its input path / SIO * handshake cadence is preserved exactly). Keyboard is always digital. - * A P1 with no assigned device but dev-any-input on presents as HYBRID. */ + * Multitap taps are forced digital. A P1 with no assigned device but + * dev-any-input on presents as HYBRID. */ int mode; - if (p.kind != 0) mode = effective_player_mode(p); + if (sio_pad_on_multitap(s)) + mode = (int)PSXRecompV4::PAD_MODE_DIGITAL; + else if (p.kind != 0) mode = effective_player_mode(p); else if (dev_here) mode = (int)PSXRecompV4::PAD_MODE_HYBRID; else mode = (int)PSXRecompV4::PAD_MODE_DIGITAL; int eff_analog; @@ -2614,8 +2626,9 @@ static int capture_pad_slot(int s, PsxNetPad* out) { /* Netplay-only capture: assigned PlayerInput for this slot only. Never merges * keyboard-all / all-controllers (dev_any_input). Same pad-mode / stick rules - * as capture_pad_slot otherwise. */ -static int capture_pad_slot_exclusive(int s, PsxNetPad* out) { + * as capture_pad_slot otherwise. present_sio_slot is the delay-sync seat this + * blob will publish to (may differ from host card `s` on guests). */ +static int capture_pad_slot_exclusive(int s, PsxNetPad* out, int present_sio_slot) { if (!out) return 0; out->buttons = 0xFFFFu; out->lx = out->ly = out->rx = out->ry = 0x80u; @@ -2627,7 +2640,8 @@ static int capture_pad_slot_exclusive(int s, PsxNetPad* out) { const bool dev_here = false; if (p.kind == 0) return 0; /* no device in this port */ - int mode = effective_player_mode(p); + const int sio_slot = (present_sio_slot >= 0) ? present_sio_slot : s; + int mode = effective_player_mode_for_sio(p, sio_slot); int eff_analog; if (mode == PSXRecompV4::PAD_MODE_DIGITAL) { eff_analog = 0; @@ -2657,6 +2671,8 @@ static int capture_pad_slot_exclusive(int s, PsxNetPad* out) { } static void apply_pad_slot_to_sio(int s, const PsxNetPad& pad) { + if (sio_pad_on_multitap(s)) + sio_set_pad_config_capable(s, 0); sio_set_pad_state_slot(s, pad.buttons); sio_set_pad_sticks(s, pad.lx, pad.ly, pad.rx, pad.ry); sio_request_pad_type(s, pad.analog ? 1 : 0); @@ -2664,22 +2680,42 @@ static void apply_pad_slot_to_sio(int s, const PsxNetPad& pad) { /* Local human pad for delay-sync: sample the host PlayerInput selected for * this peer (see --net-input-player / auto), then recomp-net maps that blob - * onto local_slot (host→sim P1, guest→sim P2). Never writes SIO. Exclusive + * onto local_slot (lobby seat → sim P1/P2/…). Never writes SIO. Exclusive * capture — no keyboard-all / all-controllers merge — so peers hash-agree. */ static void capture_local_human_pad(PsxNetPad* out) { int idx = psx_netplay_input_player(); if (idx < 0 || idx >= PSX_MAX_PLAYERS) idx = 0; - if (!capture_pad_slot_exclusive(idx, out)) { - /* Fallback: if auto picked empty local slot, try P1 (two-machine guest). */ - if (idx != 0 && capture_pad_slot_exclusive(0, out)) { + /* Present as the lobby seat (multitap taps → digital), not the host card. */ + const int seat = psx_netplay_local_slot(); + const int present = (seat >= 0) ? seat : idx; + if (capture_pad_slot_exclusive(idx, out, present)) { + out->connected = 1; + psx_netplay_normalize_pad(out); + return; + } + /* Fallbacks: NETPLAY/P1 card, lobby seat card, then any assigned device. */ + if (idx != 0 && capture_pad_slot_exclusive(0, out, present)) { + out->connected = 1; + psx_netplay_normalize_pad(out); + return; + } + if (seat >= 0 && seat < PSX_MAX_PLAYERS && seat != idx && seat != 0 && + capture_pad_slot_exclusive(seat, out, present)) { + out->connected = 1; + psx_netplay_normalize_pad(out); + return; + } + for (int s = 0; s < PSX_MAX_PLAYERS; ++s) { + if (s == idx || s == 0 || s == seat) continue; + if (capture_pad_slot_exclusive(s, out, present)) { out->connected = 1; psx_netplay_normalize_pad(out); return; } - out->buttons = 0xFFFFu; - out->lx = out->ly = out->rx = out->ry = 0x80u; - out->analog = 1; } + out->buttons = 0xFFFFu; + out->lx = out->ly = out->rx = out->ry = 0x80u; + out->analog = 1; out->connected = 1; psx_netplay_normalize_pad(out); } @@ -3273,6 +3309,12 @@ static void sdl_vblank_present(void) { if (psx_return_to_lobby_requested()) return; netplay_barrier_admit(override_); if (skip_pace_ || psx_return_to_lobby_requested()) return; + /* Post-starvation / behind-peer catch-up: skip wall pace so admits + * can burn down remote tip (mirrors snes_host_catchup_budget). */ + if (psx_netplay_catchup_budget() > 0) { + psx_netplay_catchup_consume_frame(); + return; + } uint64_t perf_start = runtime_perf_section_begin(); frame_pacer_wait(&s_frame_pacer, g_frame_period_ms); runtime_perf_section_end(perf_start, &g_runtime_perf.pacer_ticks); @@ -3293,8 +3335,15 @@ static void sdl_vblank_present(void) { if (g_offline_pad_count >= 3 && fntrace_is_game_started() && !sio_get_multitap()) { sio_set_multitap(1); + /* Tap seats drop to plain digital as soon as the tap is live. */ + for (int s = 0; s < PSX_MAX_PLAYERS; ++s) { + if (!sio_pad_on_multitap(s)) continue; + sio_set_pad_config_capable(s, 0); + sio_set_pad_analog(s, 0, 0x80, 0x80, 0x80, 0x80); + } std::fprintf(stdout, - "psxrecomp: multitap armed (console Port %d)\n", + "psxrecomp: multitap armed (console Port %d; " + "tap pads forced digital)\n", sio_get_multitap_port() + 1); } if (g_headless) @@ -4785,7 +4834,19 @@ namespace { (void)psx_lobby_set_match_caps(&caps); } - int ae_np_input_delay_get(void*) { return g_lnch_lobby_input_delay; } + int ae_np_input_delay_get(void*) { + /* Online guests show host-authoritative match_caps. */ + if (!g_lnch_hosting_lan && !g_lnch_joined_lan) { + const PsxLobbyMatchCaps* caps = psx_lobby_match_caps(); + if (caps && caps->valid) { + int d = caps->input_delay; + if (d < 2) d = 2; + if (d > 20) d = 20; + return d; + } + } + return g_lnch_lobby_input_delay; + } int ae_np_input_delay_set(void*, int delay_frames) { if (delay_frames < 2) delay_frames = 2; if (delay_frames > 20) delay_frames = 20; @@ -4793,7 +4854,14 @@ namespace { ae_np_push_match_caps(nullptr); return 0; } - int ae_np_force_input_relay_get(void*) { return g_lnch_force_input_relay; } + int ae_np_force_input_relay_get(void*) { + if (!g_lnch_hosting_lan && !g_lnch_joined_lan) { + const PsxLobbyMatchCaps* caps = psx_lobby_match_caps(); + if (caps && caps->valid) + return caps->force_input_relay ? 1 : 0; + } + return g_lnch_force_input_relay; + } int ae_np_force_input_relay_set(void*, int force) { g_lnch_force_input_relay = force ? 1 : 0; ae_np_push_match_caps(nullptr); @@ -5803,7 +5871,8 @@ namespace { local_slot = g_lnch_hosting_lan ? state.host_slot : 1; g_lnch_pending_direct_launch.local_slot = local_slot; } - g_lnch_pending_direct_launch.input_player = 0; + /* -1 => resolve at netplay start (prefer NETPLAY/P1 card). */ + g_lnch_pending_direct_launch.input_player = -1; g_lnch_pending_direct_launch.session_id = g_lnch_lan_session_id; g_lnch_pending_direct_launch.input_delay = g_lnch_lobby_input_delay; g_lnch_pending_direct_launch.max_slots = @@ -5877,9 +5946,11 @@ namespace { const PsxLobbyJoinInfo* ji = psx_lobby_join_info(); if (!ji || !ji->ok) return 0; const PsxLobbyMatchCaps* caps = psx_lobby_match_caps(); + if (!caps || !caps->valid) return 0; out->enabled = 1; out->local_slot = ji->local_slot; - out->input_player = 0; + /* -1 => resolve at netplay start (prefer NETPLAY/P1 card). */ + out->input_player = -1; std::snprintf(out->bind_hostport, sizeof(out->bind_hostport), "%s", ji->bind_hostport); std::snprintf(out->peer_hostport, sizeof(out->peer_hostport), "%s", ji->peer_hostport); out->session_id = ji->session_id; @@ -7375,7 +7446,7 @@ session_reboot: /* Dev-any-input keeps P1 connected even with no assigned controller so the * keyboard / any plugged-in controller can drive port 1 standalone. */ const bool dev_p1 = (dev_any_input_enabled() && s == 0); - const int mode = effective_player_mode(g_players[s]); + const int mode = effective_player_mode_for_sio(g_players[s], s); sio_set_pad_connected(s, (g_players[s].kind != 0 || dev_p1) ? 1 : 0); sio_set_pad_analog(s, pad_mode_boot_analog(mode), 0x80, 0x80, 0x80, 0x80); sio_set_pad_config_capable(s, mode != PSXRecompV4::PAD_MODE_DIGITAL); @@ -7718,12 +7789,23 @@ session_reboot: return 1; } /* Resolve which host PlayerInput feeds this peer's net sample. - * Auto: prefer g_players[local_slot] when assigned (same-PC: host - * C40 on P1 + guest keyboard on P2); else player 0 (two-machine). */ + * Auto (-1): always prefer dashboard P1 ("PLAYER N / NETPLAY") — that + * pad is published as lobby local_slot. Seat-card P2/P3… are only used + * when P1 is empty (legacy same-PC layout). Else sole assigned / P1. */ if (net_cfg.input_player < 0 || net_cfg.input_player >= PSX_MAX_PLAYERS) { - const int prefer = net_cfg.local_slot; - if (prefer >= 0 && prefer < PSX_MAX_PLAYERS && g_players[prefer].kind != 0) - net_cfg.input_player = prefer; + const int seat = net_cfg.local_slot; + int sole = -1, n_assigned = 0; + for (int i = 0; i < PSX_MAX_PLAYERS; ++i) { + if (g_players[i].kind == 0) continue; + ++n_assigned; + sole = i; + } + if (g_players[0].kind != 0) + net_cfg.input_player = 0; + else if (seat >= 0 && seat < PSX_MAX_PLAYERS && g_players[seat].kind != 0) + net_cfg.input_player = seat; + else if (n_assigned == 1) + net_cfg.input_player = sole; else net_cfg.input_player = 0; } diff --git a/runtime/src/psx_netplay.c b/runtime/src/psx_netplay.c index d45efb33..28bb58cf 100644 --- a/runtime/src/psx_netplay.c +++ b/runtime/src/psx_netplay.c @@ -118,7 +118,8 @@ static void force_session_pads_connected(int slot_count) sio_set_multitap(0); for (i = 0; i < slot_count; ++i) { sio_connect_pad(i); - sio_set_pad_config_capable(i, 1); + /* Multitap taps are plain digital (sio clamps); lone port pad may be DS. */ + sio_set_pad_config_capable(i, sio_pad_on_multitap(i) ? 0 : 1); } } @@ -132,7 +133,8 @@ void psx_netplay_release_pads(void) for (i = 0; i < n; ++i) { sio_set_pad_state_slot(i, 0xFFFFu); sio_set_pad_sticks(i, 0x80, 0x80, 0x80, 0x80); - sio_request_pad_type(i, 1); + /* Tap slots stay digital; standalone port may request DualShock. */ + sio_request_pad_type(i, sio_pad_on_multitap(i) ? 0 : 1); } } @@ -168,8 +170,13 @@ int psx_netplay_is_host(void) { return 0; } int psx_netplay_request_save(int slot) { (void)slot; return 0; } int psx_netplay_request_load(int slot) { (void)slot; return 0; } int psx_netplay_in_load_barrier(void) { return 0; } +void psx_netplay_pump(void) {} int psx_netplay_poll_admit(void) { return 1; } void psx_netplay_finish_frame(void) {} +int psx_netplay_remote_lead(void) { return 0; } +int psx_netplay_input_delay(void) { return 2; } +int psx_netplay_catchup_budget(void) { return 0; } +void psx_netplay_catchup_consume_frame(void) {} void psx_netplay_wait_recv(int timeout_ms) { (void)timeout_ms; } #else /* PSX_HAS_RECOMP_NET */ @@ -807,11 +814,15 @@ static void decode_pad(const RNetInputSample *in, PsxNetPad *pad) static void apply_pad_slot(int slot, const PsxNetPad *pad) { if (slot < 0 || slot >= g_np.slot_count || slot >= PSX_MAX_PLAYERS || !pad) return; + const int on_tap = sio_pad_on_multitap(slot); sio_set_pad_connected(slot, 1); - sio_set_pad_config_capable(slot, 1); + sio_set_pad_config_capable(slot, on_tap ? 0 : 1); sio_set_pad_state_slot(slot, pad->buttons); - sio_set_pad_sticks(slot, pad->lx, pad->ly, pad->rx, pad->ry); - sio_request_pad_type(slot, pad->analog ? 1 : 0); + if (on_tap) + sio_set_pad_sticks(slot, 0x80, 0x80, 0x80, 0x80); + else + sio_set_pad_sticks(slot, pad->lx, pad->ly, pad->rx, pad->ry); + sio_request_pad_type(slot, (!on_tap && pad->analog) ? 1 : 0); } static void host_sample_local(rnet_u32 tick, RNetInputSample *out, void *ctx) @@ -1070,6 +1081,56 @@ void psx_netplay_bind_guest_saves(void) np_enter_guest_sandbox(); } +/* Delay-sync starvation hold (lockstep-safe; mirrors snes_host_barrier_admit). */ +#define PSX_STARVATION_ENTER_DEFAULT 4 +#define PSX_STARVATION_EXIT_DEFAULT 3 +#define PSX_STARVATION_EXIT_HR_LEAD_DEFAULT 0 +#define PSX_STARVATION_GRACE_TICKS 60 +#define PSX_STARVATION_RECOVERY_BURST 16 +#define PSX_CATCHUP_CAP 16 + +static struct { + int latched; + int enter_run; + int exit_run; + int recovery_amount; + int latch_logged; + int just_cleared; +} g_starv; + +/* Defined below poll_admit; used by the starvation runway check. */ +int psx_netplay_remote_lead(void); +int psx_netplay_input_delay(void); + +static int np_starv_env_int(const char *name, int def) +{ + const char *v = getenv(name); + long n; + char *end; + if (!v || !v[0]) + return def; + n = strtol(v, &end, 10); + if (end == v || *end != '\0' || n < 0 || n > 64) + return def; + return (int)n; +} + +static void np_starv_reset(void) +{ + memset(&g_starv, 0, sizeof(g_starv)); +} + +static int np_starv_runway_ok(void) +{ + int lead = psx_netplay_remote_lead(); + int delay = psx_netplay_input_delay(); + int hr_lead = np_starv_env_int("PSX_NET_STARVATION_EXIT_HR_LEAD", + PSX_STARVATION_EXIT_HR_LEAD_DEFAULT); + if (delay < 0) + delay = 0; + return lead >= delay + hr_lead; +} + void psx_netplay_shutdown(void) { if (g_np.session) { @@ -1079,6 +1140,7 @@ void psx_netplay_shutdown(void) } np_leave_guest_sandbox(); memset(&g_np, 0, sizeof(g_np)); + np_starv_reset(); } int psx_netplay_is_host(void) @@ -1090,9 +1152,6 @@ int psx_netplay_request_save(int slot) { if (!psx_netplay_active() || !rnet_session_is_running(g_np.session)) return 0; - printf("psxrecomp: netplay savestates are disabled\n"); - fflush(stdout); - return 1; if (g_np.local_slot != 0) return 1; /* guest: host-only; ignore */ if (np_xfer_busy() || !g_np.mc_sync_done) @@ -1103,7 +1162,9 @@ int psx_netplay_request_save(int slot) if (!savestate_request_save_protocol(slot)) return 1; /* Coord probe (size=0) does not stall admit — both peers must keep - * running until savestate_poll writes the .pst, then hash-probe stalls. */ + * running until savestate_poll writes the .pst, then hash-probe stalls. + * STATE_* rides the same UDP/relay path as inputs (LAN hub / server + * input relay fan-out). */ if (rnet_session_state_probe(g_np.session, RNET_STATE_OP_SAVE, (rnet_u8)slot, 0, 0) != 0) return 1; g_np.xfer = NP_XFER_SAVE_COORD; @@ -1118,9 +1179,6 @@ int psx_netplay_request_load(int slot) uint32_t size = 0, crc = 0; if (!psx_netplay_active() || !rnet_session_is_running(g_np.session)) return 0; - printf("psxrecomp: netplay savestates are disabled\n"); - fflush(stdout); - return 1; if (g_np.local_slot != 0) return 1; if (np_xfer_busy() || !g_np.mc_sync_done) @@ -1146,23 +1204,51 @@ int psx_netplay_in_load_barrier(void) return (g_np.xfer == NP_XFER_LOAD_APPLYING || g_np.xfer == NP_XFER_LOAD_READY) ? 1 : 0; } -int psx_netplay_poll_admit(void) +static void np_pump_session(void) { - rnet_u32 sim; - if (!psx_netplay_active()) return 1; - rnet_session_pump(g_np.session); np_guest_handle_probe(); np_apply_ready_state(); np_drive_load_barrier(); np_host_drive_xfer(); + if (rnet_session_is_running(g_np.session)) + np_maybe_start_mc_sync(); +} + +void psx_netplay_pump(void) +{ + if (!psx_netplay_active()) + return; + np_pump_session(); +} + +static int np_try_admit_gameplay(void) +{ + rnet_u32 sim = rnet_session_sim_tick(g_np.session); + if (rnet_session_try_admit(g_np.session, sim)) { + g_np.needs_advance = 1; + return 1; + } + force_session_pads_connected(g_np.slot_count); + return 0; +} + +int psx_netplay_poll_admit(void) +{ + rnet_u32 sim; + int enter_need; + int exit_need; + + if (!psx_netplay_active()) return 1; + + np_pump_session(); if (!rnet_session_is_running(g_np.session)) { psx_netplay_release_pads(); + np_starv_reset(); return 0; } - np_maybe_start_mc_sync(); /* Both peers stall until initial memcard hash-agree / transfer finishes. */ if (!g_np.mc_sync_done) return 0; @@ -1200,11 +1286,66 @@ int psx_netplay_poll_admit(void) if (g_np.needs_advance) return 1; sim = rnet_session_sim_tick(g_np.session); - if (rnet_session_try_admit(g_np.session, sim)) { - g_np.needs_advance = 1; + enter_need = np_starv_env_int("PSX_NET_STARVATION_ENTER_FRAMES", + PSX_STARVATION_ENTER_DEFAULT); + exit_need = np_starv_env_int("PSX_NET_STARVATION_EXIT_FRAMES", + PSX_STARVATION_EXIT_DEFAULT); + + /* Startup grace: do not latch before the delay rings warm up. */ + if (sim < (rnet_u32)PSX_STARVATION_GRACE_TICKS) { + g_starv.enter_run = 0; + g_starv.exit_run = 0; + g_starv.latched = 0; + g_starv.just_cleared = 0; + return np_try_admit_gameplay(); + } + + if (g_starv.latched) { + /* Pump already ran; hold try_admit until remote tip refills. */ + if (np_starv_runway_ok()) { + g_starv.exit_run++; + if (g_starv.exit_run >= exit_need) { + g_starv.latched = 0; + g_starv.exit_run = 0; + g_starv.latch_logged = 0; + g_starv.just_cleared = 1; + } else { + return 0; + } + } else { + g_starv.exit_run = 0; + return 0; + } + } + + if (np_try_admit_gameplay()) { + g_starv.enter_run = 0; + if (g_starv.just_cleared) { + g_starv.just_cleared = 0; + g_starv.recovery_amount = PSX_STARVATION_RECOVERY_BURST; + fprintf(stderr, + "psxrecomp: delay_sync_starvation cleared sim=%u lead=%d " + "D=%d — recovery burst %d\n", + (unsigned)psx_netplay_sim_tick(), psx_netplay_remote_lead(), + psx_netplay_input_delay(), PSX_STARVATION_RECOVERY_BURST); + } return 1; } - force_session_pads_connected(g_np.slot_count); + + g_starv.just_cleared = 0; + g_starv.enter_run++; + if (g_starv.enter_run >= enter_need) { + g_starv.latched = 1; + g_starv.enter_run = 0; + if (!g_starv.latch_logged) { + fprintf(stderr, + "psxrecomp: delay_sync_starvation latched sim=%u lead=%d " + "D=%d (enter=%d)\n", + (unsigned)psx_netplay_sim_tick(), psx_netplay_remote_lead(), + psx_netplay_input_delay(), enter_need); + g_starv.latch_logged = 1; + } + } return 0; } @@ -1217,6 +1358,56 @@ void psx_netplay_finish_frame(void) g_np.latched_for_tick = 0; } +int psx_netplay_remote_lead(void) +{ + RNetSessionStats st; + if (!psx_netplay_active()) + return 0; + memset(&st, 0, sizeof(st)); + rnet_session_get_stats(g_np.session, &st); + return st.remote_lead; +} + +int psx_netplay_input_delay(void) +{ + RNetSessionStats st; + if (!psx_netplay_active()) + return 2; + memset(&st, 0, sizeof(st)); + rnet_session_get_stats(g_np.session, &st); + return st.delay > 0 ? (int)st.delay : 2; +} + +int psx_netplay_catchup_budget(void) +{ + int lead; + int delay; + int extra; + int budget; + + if (!psx_netplay_active()) + return 0; + lead = psx_netplay_remote_lead(); + delay = psx_netplay_input_delay(); + if (delay < 0) + delay = 0; + extra = lead - delay; + if (extra < 0) + extra = 0; + budget = extra; + if (g_starv.recovery_amount > budget) + budget = g_starv.recovery_amount; + if (budget > PSX_CATCHUP_CAP) + budget = PSX_CATCHUP_CAP; + return budget; +} + +void psx_netplay_catchup_consume_frame(void) +{ + if (g_starv.recovery_amount > 0) + g_starv.recovery_amount--; +} + void psx_netplay_wait_recv(int timeout_ms) { if (!psx_netplay_active()) return; diff --git a/runtime/src/savestate.c b/runtime/src/savestate.c index b04e4ac0..58ea9436 100644 --- a/runtime/src/savestate.c +++ b/runtime/src/savestate.c @@ -136,9 +136,16 @@ int savestate_write_slot(int slot, const void* data, size_t size) { return 1; } -static int netplay_savestate_blocked(void) { +/* User APIs during netplay: guests cannot initiate; host must use + * psx_netplay_request_* so peers hash-probe and sync over STATE_*. */ +static int netplay_user_blocked(void) { if (!psx_netplay_active()) return 0; - fprintf(stderr, "savestate: disabled during netplay\n"); + if (!psx_netplay_is_host()) { + fprintf(stderr, "savestate: netplay guest cannot save/load (host-only)\n"); + return 1; + } + fprintf(stderr, + "savestate: during netplay use host Shift+F / F (synced path)\n"); return 1; } @@ -171,22 +178,22 @@ static int request_load_inner(int slot) { } int savestate_request_save(int slot) { - if (netplay_savestate_blocked()) return 0; + if (netplay_user_blocked()) return 0; return request_save_inner(slot); } int savestate_request_load(int slot) { - if (netplay_savestate_blocked()) return 0; + if (netplay_user_blocked()) return 0; return request_load_inner(slot); } int savestate_request_save_protocol(int slot) { - if (netplay_savestate_blocked()) return 0; + /* Follow-host sync: guests must write the host-authoritative .pst. */ return request_save_inner(slot); } int savestate_request_load_protocol(int slot) { - if (netplay_savestate_blocked()) return 0; + /* Follow-host sync: guests must apply the host-authoritative .pst. */ return request_load_inner(slot); } diff --git a/runtime/src/sio.c b/runtime/src/sio.c index cd79f833..0a1ea749 100644 --- a/runtime/src/sio.c +++ b/runtime/src/sio.c @@ -768,6 +768,18 @@ int sio_get_multitap_port(void) { #endif } +int sio_pad_on_multitap(int logical_slot) { +#if PSX_MAX_PLAYERS >= 5 + if (!sio_multitap_active()) return 0; + if (logical_slot < 0 || logical_slot >= PSX_MAX_PLAYERS) return 0; + /* Opposite-port lone pad may stay DualShock; every tap slot is digital. */ + return (logical_slot == mtap_standalone_logical()) ? 0 : 1; +#else + (void)logical_slot; + return 0; +#endif +} + void sio_connect_pad(int slot) { if (slot >= 0 && slot < PSX_MAX_PLAYERS) pad_connected |= (uint8_t)(1u << slot); @@ -781,6 +793,7 @@ void sio_set_pad_connected(int slot, int connected) { void sio_set_pad_config_capable(int slot, int capable) { if (slot < 0 || slot >= PSX_MAX_PLAYERS) return; + if (sio_pad_on_multitap(slot)) capable = 0; pad_supports_config[slot] = capable ? 1 : 0; /* A plain digital pad can never be in config mode; clear any stale latch so * the next poll reports the digital id (0x41), not the config id (0xF3). */ @@ -803,6 +816,10 @@ void sio_set_pad_state_slot(int slot, uint16_t buttons) { void sio_set_pad_analog(int slot, int enabled, uint8_t lx, uint8_t ly, uint8_t rx, uint8_t ry) { if (slot < 0 || slot >= PSX_MAX_PLAYERS) return; + if (sio_pad_on_multitap(slot)) { + enabled = 0; + lx = ly = rx = ry = 0x80; + } pad_analog[slot] = enabled ? 1 : 0; pad_type_req[slot] = -1; /* explicit set supersedes any pending request */ pad_stick[slot][0] = lx; pad_stick[slot][1] = ly; @@ -821,6 +838,7 @@ void sio_set_pad_sticks(int slot, uint8_t lx, uint8_t ly, uint8_t rx, uint8_t ry * can never split a poll or a config handshake. A no-op if already that type. */ void sio_request_pad_type(int slot, int analog) { if (slot < 0 || slot >= PSX_MAX_PLAYERS) return; + if (sio_pad_on_multitap(slot)) analog = 0; int want = analog ? 1 : 0; pad_type_req[slot] = (pad_analog[slot] == want) ? -1 : (int8_t)want; }