Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 86 additions & 2 deletions Source/Core/Core/HW/EXI/EXI_DeviceSlippi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1193,6 +1193,9 @@ void CEXISlippi::handleOnlineInputs(u8* payload)
stall_frame_counts[i] = 0;
}

last_interval_time_us = 0;
perf_debt = 0;

// Reset skip variables
frames_to_skip = 0;
is_currently_skipping = false;
Expand All @@ -1211,6 +1214,19 @@ void CEXISlippi::handleOnlineInputs(u8* payload)

if (isDisconnected())
{
// Both clients reach this path when a poor-performance termination fires: the initiating
// side set the reason locally, the other side received it over the disconnect. Show the
// message on both ends rather than only where the debt happened to cross the threshold first.
if (slippi_netplay->GetDisconnectReason() ==
SlippiNetplayClient::SlippiDisconnectReason::POOR_PERFORMANCE)
{
OSD::AddTypedMessage(
OSD::MessageType::PoorPerformanceTermination,
"The match has been terminated due to poor network quality.\nIf you see this message in "
"most of your matches, you probably shouldn't be playing ranked.",
15000, OSD::Color::RED);
}

m_read_queue.push_back(3); // Indicate we disconnected
return;
}
Expand All @@ -1226,13 +1242,78 @@ void CEXISlippi::handleOnlineInputs(u8* payload)
}
else
{
// Consider disconnecting from a match if performance is poor
handlePoorMatchPerformance(frame);

// Send the input for this frame along with everything that has yet to be acked
handleSendInputs(frame, delay, finalized_frame, finalized_frame_checksum, inputs);
}

prepareOpponentInputs(frame, should_skip);
}

void CEXISlippi::handlePoorMatchPerformance(s32 frame)
{
// Only handle poor match performance in ranked. In other modes players can just manually leave.
// Plus ranked is mostly where it matters
if (last_search.mode != SlippiMatchmaking::OnlinePlayMode::RANKED)
return;

u64 interval_frames = 150; // Check every 2.5 seconds
u64 frame_time_us = 16683;

// Skip the first 50 frames of the game and check timing info every interval
if ((frame + (interval_frames - 50)) % interval_frames != 0)
return;

auto cur_time_us = Common::Timer::NowUs();

// Iniitalize the first instance
if (last_interval_time_us == 0)
{
last_interval_time_us = cur_time_us;
return; // We will start processing the next time
}

auto expected_time_us = frame_time_us * interval_frames;
double ratio = static_cast<double>(cur_time_us - last_interval_time_us) /
static_cast<double>(expected_time_us);
last_interval_time_us = cur_time_us;

// Leaky accumulator: each interval adds "debt" proportional to how far over the expected
// duration we ran, and healthy intervals pay it back down. We only terminate once enough debt
// builds up, so a single hitch (local or otherwise) is survivable but sustained degradation
// isn't. The decay keeps this independent of match length: scattered blips drain away before
// they can accumulate to the termination threshold.
s32 terminate_threshold = 30;
s32 debt;
if (ratio >= 1.75)
debt = 15; // Severe
else if (ratio >= 1.50)
debt = 8; // Bad
else if (ratio >= 1.10)
debt = 4; // Mild
else
debt = -1; // Healthy interval, pay down accumulated debt

perf_debt = std::max(0, perf_debt + debt);
INFO_LOG_FMT(SLIPPI_ONLINE, "Modifying performance debt by {}. Currently at: {}/{}", debt,
perf_debt, terminate_threshold);
if (perf_debt >= terminate_threshold)
{
// Tell the server about the poor performance, then drop all remote players with a
// POOR_PERFORMANCE reason. Flipping the connection to DISCONNECTED lets the existing
// disconnect-detection path end the game naturally (next handleOnlineInputs sees
// isDisconnected()), same as a real disconnect. The reason rides the disconnect to the
// peer so both clients surface the OSD message from the shared disconnect path below.
slprs_exi_device_report_match_status(slprs_exi_device_ptr, recent_mm_result.id.c_str(),
"poor_performance", true);
slippi_netplay->ForceDisconnect(SlippiNetplayClient::SlippiDisconnectReason::POOR_PERFORMANCE);
ERROR_LOG_FMT(SLIPPI_ONLINE, "Match terminated due to poor performance. {}/{}", perf_debt,
terminate_threshold);
}
}

bool CEXISlippi::shouldSkipOnlineFrame(s32 frame, s32 finalized_frame)
{
auto status = slippi_netplay->GetSlippiConnectStatus();
Expand Down Expand Up @@ -1421,11 +1502,14 @@ bool CEXISlippi::shouldAdvanceOnlineFrame(s32 frame)

bool is_slow = (offset_us < -t1 && fall_behind_counter > 50) ||
(offset_us < -t2 && fall_far_behind_counter > 15);
if (is_slow && matchmaking->RemotePlayerCount() == 1)
if (is_slow && matchmaking->RemotePlayerCount() == 1 &&
last_search.mode != SlippiMatchmaking::OnlinePlayMode::RANKED)
{
// Only show this in 1v1. With more peers, CalcTimeOffsetUs returns the min across peers,
// which biases negative as the peer count grows and false-positives this warning. The
// message text ("if this appears with most opponents") also only makes sense in 1v1.
// message text ("if this appears with most opponents") also only makes sense in 1v1. Don't
// show in ranked because we have the poor match logic running there which has its own
// message.
OSD::AddTypedMessage(
OSD::MessageType::PerformanceWarning,
"Possible poor match performance detected.\nIf this message appears with most opponents, "
Expand Down
4 changes: 4 additions & 0 deletions Source/Core/Core/HW/EXI/EXI_DeviceSlippi.h
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ class CEXISlippi : public IEXIDevice
void prepareOnlineMatchState();
void setMatchSelections(u8* payload);
bool shouldSkipOnlineFrame(s32 frame, s32 finalized_frame);
void handlePoorMatchPerformance(s32 frame);
bool shouldAdvanceOnlineFrame(s32 frame);
bool opponentRunahead();
void handleLogInRequest();
Expand Down Expand Up @@ -291,6 +292,9 @@ class CEXISlippi : public IEXIDevice
std::vector<u8> gecko_list;

u32 stall_frame_counts[SLIPPI_REMOTE_PLAYER_MAX] = {};
u64 last_interval_time_us = 0;
// Leaky accumulator of poor-performance intervals (see handlePoorMatchPerformance)
s32 perf_debt = 0;

std::vector<u8> m_read_queue;
std::unique_ptr<Slippi::SlippiGame> m_current_game = nullptr;
Expand Down
40 changes: 37 additions & 3 deletions Source/Core/Core/Slippi/SlippiNetplay.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -729,8 +729,14 @@ void SlippiNetplayClient::Disconnect()
{
for (auto peer : conn.second)
{
INFO_LOG_FMT(SLIPPI_ONLINE, "[Netplay] Disconnecting peer {}", peer.first->address.port);
enet_peer_disconnect(peer.first, 0);
// Carry the pending reason (default 0 = unspecified) so an intentional teardown such as a
// poor-performance termination reaches the peer. In 1v1, ForceDisconnect flips the status to
// DISCONNECTED before the network loop's force-kick runs, so this teardown call is the one
// that actually reaches the peer — it must forward the reason rather than send 0.
u32 reason = m_pending_disconnect_reason.load(std::memory_order_acquire);
INFO_LOG_FMT(SLIPPI_ONLINE, "[Netplay] Disconnecting peer {} with reason {}",
peer.first->address.port, reason);
enet_peer_disconnect(peer.first, reason);
}
}

Expand Down Expand Up @@ -1045,7 +1051,8 @@ void SlippiNetplayClient::ThreadFunc()
INFO_LOG_FMT(SLIPPI_ONLINE, "[Netplay] Force-disconnecting ENet peer {}:{} for player {}",
peer_entry.first->address.host, peer_entry.first->address.port,
peer_entry.second.player_idx);
enet_peer_disconnect(peer_entry.first, 0);
enet_peer_disconnect(peer_entry.first,
m_pending_disconnect_reason.load(std::memory_order_acquire));
peer_entry.second.is_disconnected = true;
}
}
Expand Down Expand Up @@ -1107,6 +1114,12 @@ void SlippiNetplayClient::ThreadFunc()
}
}

// Capture any reason the peer encoded in the disconnect data (e.g. poor performance) so the
// EXI thread can surface the same UI we'd show on the initiating side. Only a deliberate
// disconnect sends a non-zero value; organic disconnects send 0.
if (is_connected_client && net_event.data != 0)
m_disconnect_reason.store(net_event.data, std::memory_order_release);

INFO_LOG_FMT(
SLIPPI_ONLINE,
"[Netplay] Disconnect late {}:{}. All peers disconnected: {}. Is connected client: {}",
Expand Down Expand Up @@ -1574,6 +1587,27 @@ void SlippiNetplayClient::ForceDisconnectPlayer(u8 player_idx)
Common::ENet::WakeupThread(m_client);
}

// Force-disconnect every remote player. Delegates to ForceDisconnectPlayer so the liveness
// flip, peer teardown, and network-thread wakeup all reuse the same logic; the final call
// transitions the overall status to DISCONNECTED once no remote players remain active.
void SlippiNetplayClient::ForceDisconnect(SlippiDisconnectReason reason)
{
// Record the reason both for our own UI (we never receive a disconnect event from ourselves)
// and for the network thread to forward to the peer via the enet_peer_disconnect data field.
m_pending_disconnect_reason.store(static_cast<u32>(reason), std::memory_order_release);
m_disconnect_reason.store(static_cast<u32>(reason), std::memory_order_release);

for (u8 i = 0; i < m_remote_player_count; i++)
{
ForceDisconnectPlayer(match_info.remote_player_selections[i].player_idx);
}
}

SlippiNetplayClient::SlippiDisconnectReason SlippiNetplayClient::GetDisconnectReason()
{
return static_cast<SlippiDisconnectReason>(m_disconnect_reason.load(std::memory_order_acquire));
}

// Network-thread only (called from ThreadFunc disconnect handler).
bool SlippiNetplayClient::AreAllPeersDisconnectedForKey(const std::string& key)
{
Expand Down
21 changes: 21 additions & 0 deletions Source/Core/Core/Slippi/SlippiNetplay.h
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,15 @@ class SlippiNetplayClient
NET_CONNECT_STATUS_DISCONNECTED,
};

// Reason carried over the wire alongside an intentional disconnect, via the ENet
// disconnect data field. Values are transmitted as u32; 0 (UNSPECIFIED) is what a
// normal/organic disconnect sends, so any non-zero value is a deliberate reason.
enum class SlippiDisconnectReason : u32
{
UNSPECIFIED = 0,
POOR_PERFORMANCE = 1,
};

bool IsDecider();
bool IsConnectionSelected();
u8 LocalPlayerPort();
Expand All @@ -194,6 +203,8 @@ class SlippiNetplayClient
void DropOldRemoteInputs(int32_t finalized_frame);
std::unordered_map<u8, bool> GetActivePlayerIndices();
void ForceDisconnectPlayer(u8 playerIdx);
void ForceDisconnect(SlippiDisconnectReason reason = SlippiDisconnectReason::UNSPECIFIED);
SlippiDisconnectReason GetDisconnectReason();
SlippiMatchInfo* GetMatchInfo();
SlippiPlayerSelections GetSlippiRemoteChatMessage(bool is_chat_enabled);
u8 GetSlippiRemoteSentChatMessage(bool is_chat_enabled);
Expand Down Expand Up @@ -286,6 +297,16 @@ class SlippiNetplayClient

std::atomic<SlippiConnectStatus> slippi_connect_status{
SlippiConnectStatus::NET_CONNECT_STATUS_UNSET};

// Disconnect reason plumbing (see SlippiDisconnectReason). m_pending_disconnect_reason is set by
// the EXI thread before flipping playerActive and is read by the network thread when it issues
// enet_peer_disconnect so the peer learns why. m_disconnect_reason is the resolved reason for
// this client — set locally on the initiating side, or from the received disconnect data on the
// receiving side — and is read by the EXI thread to drive UI such as the poor-performance OSD.
std::atomic<u32> m_pending_disconnect_reason{
static_cast<u32>(SlippiDisconnectReason::UNSPECIFIED)};
std::atomic<u32> m_disconnect_reason{static_cast<u32>(SlippiDisconnectReason::UNSPECIFIED)};

std::vector<int> failed_connections;
SlippiMatchInfo match_info;

Expand Down
1 change: 1 addition & 0 deletions Source/Core/VideoCommon/OnScreenDisplay.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ enum class MessageType
NetPlayBuffer,
PerformanceWarning,
DesyncWarning,
PoorPerformanceTermination,
// This entry must be kept last so that persistent typed messages are
// displayed before other messages
Typeless,
Expand Down
Loading