Compare commits

...

2 commits

Author SHA1 Message Date
MiTHRAL
3ec27d115b netcode-revival: receive timeout on the relay transport socket
Some checks are pending
Lint & Format Check / Format Check (push) Waiting to run
The Run thread blocked in recvfrom with no SO_RCVTIMEO, so when there was no
inbound game traffic the 4s heartbeat never fired (the relay swept the
silent transport client after ~8s) AND StopLocked()'s join() deadlocked
SetRelay on a room change (hanging the ghost network loop). A 200ms receive
timeout wakes the loop regularly: the heartbeat keeps the transport alive
and shutdown/room-change joins complete promptly.
2026-08-28 13:28:58 -04:00
MiTHRAL
01fe706c13 netcode-revival: pack the relay wire structs + heartbeat + transport logging
- #pragma pack(push,1) on the wire structs (the relay's proto is packed; the
  transport's were not, which would inflate sizes and corrupt every datagram)
  plus static_asserts pinning sizes to 7/31/1410/25/801.
- Heartbeat re-JOIN every 4s so the relay never times the transport out.
- REXLOG info for connect/join/session-reply so the flow is observable.
2026-08-28 02:14:19 -04:00
3 changed files with 62 additions and 1 deletions

View file

@ -32,6 +32,10 @@ namespace system {
class XSocket;
// The wire structs below must match the relay's byte-for-byte (the main repo
// packs them with #pragma pack(1); leaving padding here would inflate the
// sizes and silently corrupt every datagram).
#pragma pack(push, 1)
// Header of every relay datagram ("GHST" magic, little-endian type + size).
// Must match kGhostMagic / PacketHeader in skate3_native_ghost_proto.h.
struct XNetRelayHeader {
@ -69,6 +73,7 @@ struct XNetRelaySessionReply {
uint8_t count;
XNetRelaySessionEntry list[32];
};
#pragma pack(pop)
// One discovered session (a member of our room).
struct XNetSession {
@ -143,6 +148,9 @@ class XNetTransport {
// header stays platform-independent.
uint8_t relay_addr_[16] = {};
bool relay_resolved_ = false;
uint8_t join_pkt_[7 + sizeof(XNetRelayJoin)] = {};
uint16_t join_pkt_size_ = 0;
double last_join_ = 0.0; // monotonic seconds (heartbeat to beat the timeout)
std::thread thread_;
bool running_ = false;

View file

@ -11,6 +11,7 @@
#include <thread>
#include <rex/platform.h>
#include <rex/logging.h>
#include <rex/system/kernel_state.h>
#include <rex/system/xsocket.h>
#include <rex/system/xam/user_profile.h>
@ -29,6 +30,15 @@
namespace rex {
namespace system {
// Pin the packed wire sizes to the values the relay expects (the game-side
// proto packs them identically). Catches accidental un-packing or member
// edits before they corrupt the wire.
static_assert(sizeof(XNetRelayHeader) == 7, "relay header must be 7 bytes");
static_assert(sizeof(XNetRelayJoin) == 31, "relay join must be 31 bytes");
static_assert(sizeof(XNetRelayGame) == 1410, "relay game payload must be 1410 bytes");
static_assert(sizeof(XNetRelaySessionEntry) == 25, "session entry must be 25 bytes");
static_assert(sizeof(XNetRelaySessionReply) == 801, "session reply must be 801 bytes");
namespace {
// Virtual address range: 10.80.0.0/16 (host order prefix 0x0A500000).
@ -50,6 +60,23 @@ void SocketClose(int64_t s) {
#endif
}
// The receive loop must wake regularly so the heartbeat fires even with no
// inbound traffic (otherwise the relay sweeps a silent transport client
// after ~8s) and so StopLocked()'s join completes promptly instead of
// deadlocking SetRelay on a room change.
void SocketSetRecvTimeout(int64_t s, int timeout_ms) {
#if REX_PLATFORM_WIN32
DWORD tv = DWORD(timeout_ms);
::setsockopt(SOCKET(s), SOL_SOCKET, SO_RCVTIMEO,
reinterpret_cast<const char*>(&tv), sizeof(tv));
#else
timeval tv = {};
tv.tv_sec = timeout_ms / 1000;
tv.tv_usec = (timeout_ms % 1000) * 1000;
::setsockopt(int(s), SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
#endif
}
bool SocketSendTo(int64_t s, const sockaddr_in* to, const void* buf, int size) {
#if REX_PLATFORM_WIN32
const int n = ::sendto(SOCKET(s), reinterpret_cast<const char*>(buf), size, 0,
@ -156,14 +183,21 @@ void XNetTransport::Reconnect() {
}
sockaddr_in relay_addr = {};
if (!Resolve(host_, port_, &relay_addr)) {
REXLOG_WARN("xnet transport: cannot resolve relay '{}:{}'", host_, port_);
return;
}
std::memcpy(relay_addr_, &relay_addr, sizeof(relay_addr));
relay_resolved_ = true;
sock_ = SocketCreate();
if (sock_ == -1) {
REXLOG_WARN("xnet transport: cannot open relay socket");
return;
}
// Wake the receive loop every 200ms (heartbeat + prompt shutdown).
SocketSetRecvTimeout(sock_, 200);
REXLOG_INFO("xnet transport: connecting to relay '{}:{}' room '{}' (vip {}.{}.{}.{})", host_,
port_, room_.c_str(), (LocalVirtualIp() >> 24) & 0xFF, (LocalVirtualIp() >> 16) & 0xFF,
(LocalVirtualIp() >> 8) & 0xFF, LocalVirtualIp() & 0xFF);
// Join the room so the relay routes our game traffic among its members.
XNetRelayJoin jp = {};
jp.version = kVersion;
@ -184,6 +218,12 @@ void XNetTransport::Reconnect() {
std::memcpy(pkt + 7, &jp, sizeof(jp));
SocketSendTo(sock_, &relay_addr, pkt, sizeof(pkt));
// Keep the JOIN around for the heartbeat (the relay drops silent clients
// after ~8s).
std::memcpy(join_pkt_, pkt, sizeof(pkt));
join_pkt_size_ = uint16_t(sizeof(pkt));
last_join_ = 0.0;
running_ = true;
thread_ = std::thread(&XNetTransport::Run, this);
}
@ -271,6 +311,14 @@ void XNetTransport::Run() {
if (!running_) {
return;
}
// Heartbeat: re-JOIN every ~4s so the relay never times us out.
const double now =
double(std::chrono::steady_clock::now().time_since_epoch().count()) / 1e9;
if (join_pkt_size_ != 0 && now - last_join_ >= 4.0) {
SocketSendTo(sock_, reinterpret_cast<const sockaddr_in*>(relay_addr_), join_pkt_,
join_pkt_size_);
last_join_ = now;
}
}
uint8_t buf[7 + sizeof(XNetRelayGame)];
sockaddr_in from = {};
@ -307,6 +355,9 @@ void XNetTransport::HandlePacket(const uint8_t* data, int size) {
// session_id != 0 means accepted (0 = refused).
active_ = payload[0] != 0;
joined_ = true;
REXLOG_INFO("xnet transport: relay {} (vip {}.{}.{}.{})", active_ ? "joined" : "refused",
(LocalVirtualIp() >> 24) & 0xFF, (LocalVirtualIp() >> 16) & 0xFF,
(LocalVirtualIp() >> 8) & 0xFF, LocalVirtualIp() & 0xFF);
break;
}
case kGame: {
@ -350,6 +401,8 @@ void XNetTransport::HandlePacket(const uint8_t* data, int size) {
std::lock_guard<std::mutex> lock(mu_);
sessions_ = std::move(fresh);
++reply_seq_;
REXLOG_INFO("xnet transport: session reply with {} session(s) in room '{}'", count,
room_.c_str());
break;
}
default:

2
thirdparty/imgui vendored

@ -1 +1 @@
Subproject commit cdda62349d6068e906c2e0ee340d6ec31eedbb9c
Subproject commit 0af8f475d1cf8b98d603702059c0c73deeb81ac2