netplay patch

This commit is contained in:
Alex Vanderveen
2026-07-24 18:26:40 -04:00
parent 2c372195d1
commit 2bba3cdc1e
7 changed files with 379 additions and 52 deletions
+24
View File
@@ -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);
+5
View File
@@ -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,
+107 -25
View File
@@ -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;
}
+211 -20
View File
@@ -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;
+13 -6
View File
@@ -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);
}
+18
View File
@@ -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;
}