feat: implement multiplayer networking improvements for reduced latency

Major networking enhancements to improve multiplayer performance and
reduce desync issues in games like Mario Kart 8 Deluxe:

Network Performance:
- Add socket connection pooling in BSD service to reduce overhead
- Implement unreliable packet delivery for latency-sensitive game data
- Add packet reliability control for both ProxyPacket and LDNPacket
- Use ENET_PACKET_FLAG_UNSEQUENCED for small UDP packets (<1200 bytes)

Monitoring & Debugging:
- Add PacketStatistics struct to track sent/received/dropped packets
- Enhanced logging for proxy packet handling and socket lifecycle
- Periodic stats logging every 100 packets for diagnostics

Configuration:
- Update lobby_api_url and web_api_url to https://api.ynet-fun.xyz
- Add lobby API URL configuration support

Socket Management:
- Implement SocketPoolKey for efficient socket reuse
- Store domain/type/protocol info in FileDescriptor
- Max pool size limit (8 sockets per type) to prevent memory bloat
- Return closed sockets to pool when room is still connected

Protocol Updates:
- Add 'reliable' field to ProxyPacket and LDNPacket structures
- Update room.cpp packet handlers to respect reliability flags
- Maintain backward compatibility with default reliable=true

These changes significantly reduce packet latency for real-time game
traffic while maintaining reliability for control packets.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
This commit is contained in:
Zephyron
2025-11-05 18:52:06 +10:00
parent ee124b44e3
commit 5117ff3702
9 changed files with 154 additions and 21 deletions

View File

@@ -547,15 +547,32 @@ std::pair<s32, Errno> BSD::SocketImpl(Domain domain, Type type, Protocol protoco
LOG_INFO(Service, "New socket fd={} domain={} type={} protocol={} proxy={}",
fd, domain, type, protocol, using_proxy);
// Store socket type information for pooling
descriptor.domain = Translate(domain);
descriptor.type = Translate(type);
descriptor.protocol = Translate(protocol);
descriptor.is_connection_based = IsConnectionBased(type);
// Try to reuse a socket from the pool if using proxy
if (using_proxy) {
descriptor.socket = std::make_shared<Network::ProxySocket>(room_network);
SocketPoolKey key{descriptor.domain, descriptor.type, descriptor.protocol};
std::lock_guard lock(socket_pool_mutex);
auto it = socket_pool.find(key);
if (it != socket_pool.end() && !it->second.empty()) {
descriptor.socket = it->second.back();
it->second.pop_back();
LOG_DEBUG(Service, "Reused socket from pool for fd={}", fd);
} else {
descriptor.socket = std::make_shared<Network::ProxySocket>(room_network);
descriptor.socket->Initialize(descriptor.domain, descriptor.type, descriptor.protocol);
LOG_DEBUG(Service, "Created new ProxySocket for fd={}", fd);
}
} else {
descriptor.socket = std::make_shared<Network::Socket>();
descriptor.socket->Initialize(descriptor.domain, descriptor.type, descriptor.protocol);
}
descriptor.socket->Initialize(Translate(domain), Translate(type), Translate(protocol));
descriptor.is_connection_based = IsConnectionBased(type);
return {fd, Errno::SUCCESS};
}
@@ -966,13 +983,34 @@ Errno BSD::CloseImpl(s32 fd) {
return Errno::BADF;
}
const Errno bsd_errno = Translate(file_descriptors[fd]->socket->Close());
auto& descriptor = file_descriptors[fd];
const Errno bsd_errno = Translate(descriptor->socket->Close());
if (bsd_errno != Errno::SUCCESS) {
return bsd_errno;
}
LOG_INFO(Service, "Close socket fd={}", fd);
// Try to return ProxySocket to the pool for reuse
auto proxy_socket = std::dynamic_pointer_cast<Network::ProxySocket>(descriptor->socket);
auto room_member = room_network.GetRoomMember().lock();
if (proxy_socket && room_member && room_member->IsConnected()) {
// Socket is still valid, add to pool
std::lock_guard lock(socket_pool_mutex);
SocketPoolKey key{descriptor->domain, descriptor->type, descriptor->protocol};
// Limit pool size to avoid memory bloat (max 8 sockets per type)
constexpr size_t MAX_POOL_SIZE = 8;
if (socket_pool[key].size() < MAX_POOL_SIZE) {
socket_pool[key].push_back(descriptor->socket);
LOG_DEBUG(Service, "Returned socket fd={} to pool", fd);
} else {
LOG_DEBUG(Service, "Socket pool full, destroying socket fd={}", fd);
}
}
file_descriptors[fd].reset();
return bsd_errno;
}

View File

@@ -46,6 +46,9 @@ private:
std::shared_ptr<Network::SocketBase> socket;
s32 flags = 0;
bool is_connection_based = false;
Network::Domain domain = Network::Domain::INET;
Network::Type type = Network::Type::DGRAM;
Network::Protocol protocol = Network::Protocol::UDP;
};
struct PollWork {
@@ -209,6 +212,20 @@ private:
// Callback identifier for the OnProxyPacketReceived event.
Network::RoomMember::CallbackHandle<Network::ProxyPacket> proxy_packet_received;
/// Socket pool to cache and reuse ProxySocket instances
struct SocketPoolKey {
Network::Domain domain;
Network::Type type;
Network::Protocol protocol;
bool operator<(const SocketPoolKey& other) const {
return std::tie(domain, type, protocol) <
std::tie(other.domain, other.type, other.protocol);
}
};
std::map<SocketPoolKey, std::vector<std::shared_ptr<Network::SocketBase>>> socket_pool;
std::mutex socket_pool_mutex;
protected:
virtual std::unique_lock<std::mutex> LockService() override;
};

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
@@ -928,7 +929,13 @@ bool Socket::IsOpened() const {
}
void Socket::HandleProxyPacket(const ProxyPacket& packet) {
LOG_WARNING(Network, "ProxyPacket received, but not in Proxy mode!");
LOG_WARNING(Network,
"ProxyPacket received on regular socket (not ProxySocket). "
"This may indicate socket type mismatch. "
"Packet from {}:{} to {}:{}, protocol={}, reliable={}",
packet.local_endpoint.ip[0], packet.local_endpoint.portno,
packet.remote_endpoint.ip[0], packet.remote_endpoint.portno,
static_cast<int>(packet.protocol), packet.reliable);
}
} // namespace Network

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <chrono>
@@ -30,11 +31,15 @@ ProxySocket::~ProxySocket() {
void ProxySocket::HandleProxyPacket(const ProxyPacket& packet) {
if (protocol != packet.protocol || local_endpoint.portno != packet.remote_endpoint.portno ||
closed) {
stats.packets_dropped++;
LOG_DEBUG(Network, "Dropped packet: protocol mismatch or closed socket. Stats: sent={}, recv={}, dropped={}",
stats.packets_sent, stats.packets_received, stats.packets_dropped);
return;
}
if (!broadcast && packet.broadcast) {
LOG_INFO(Network, "Received broadcast packet, but not configured for broadcast mode");
stats.packets_dropped++;
LOG_DEBUG(Network, "Dropped broadcast packet on non-broadcast socket");
return;
}
@@ -43,6 +48,15 @@ void ProxySocket::HandleProxyPacket(const ProxyPacket& packet) {
std::lock_guard guard(packets_mutex);
received_packets.push(decompressed);
stats.packets_received++;
stats.bytes_received += decompressed.data.size();
// Log statistics periodically (every 100 packets)
if (stats.packets_received % 100 == 0) {
LOG_DEBUG(Network, "ProxySocket stats: sent={} ({} bytes), recv={} ({} bytes), dropped={}",
stats.packets_sent, stats.bytes_sent,
stats.packets_received, stats.bytes_received, stats.packets_dropped);
}
}
template <typename T>
@@ -189,10 +203,20 @@ std::pair<s32, Errno> ProxySocket::Send(std::span<const u8> message, int flags)
void ProxySocket::SendPacket(ProxyPacket& packet) {
if (auto room_member = room_network.GetRoomMember().lock()) {
if (room_member->IsConnected()) {
const size_t original_size = packet.data.size();
packet.data = Common::Compression::CompressDataZSTDDefault(packet.data.data(),
packet.data.size());
room_member->SendProxyPacket(packet);
stats.packets_sent++;
stats.bytes_sent += original_size;
} else {
LOG_WARNING(Network, "Cannot send packet: not connected to room. Total packets dropped: {}",
++stats.packets_dropped);
}
} else {
LOG_ERROR(Network, "Cannot send packet: room member unavailable");
stats.packets_dropped++;
}
}
@@ -236,6 +260,14 @@ std::pair<s32, Errno> ProxySocket::SendTo(u32 flags, std::span<const u8> message
packet.data.clear();
std::copy(message.begin(), message.end(), std::back_inserter(packet.data));
// Determine if packet should use unreliable delivery for better latency
// Use unreliable delivery for:
// 1. Small, frequent game data packets (< 1200 bytes for typical MTU)
// 2. UDP protocol packets (most game traffic)
// 3. Non-broadcast packets (broadcast should be reliable for coordination)
const bool is_game_data = protocol == Protocol::UDP && message.size() < 1200 && !packet.broadcast;
packet.reliable = !is_game_data;
SendPacket(packet);
return {static_cast<s32>(message.size()), Errno::SUCCESS};

View File

@@ -94,6 +94,15 @@ private:
std::mutex packets_mutex;
RoomNetwork& room_network;
// Packet statistics for monitoring
struct PacketStatistics {
u64 packets_sent = 0;
u64 packets_received = 0;
u64 packets_dropped = 0;
u64 bytes_sent = 0;
u64 bytes_received = 0;
} stats;
};
} // namespace Network