mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
netplay patch
This commit is contained in:
+1
-1
Submodule lib/recomp-net updated: b06b847d67...5336f24b6d
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user