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.
This commit is contained in:
MiTHRAL 2026-08-28 02:14:19 -04:00
parent bf3635d7ad
commit 01fe706c13
3 changed files with 43 additions and 1 deletions

View file

@ -32,6 +32,10 @@ namespace system {
class XSocket; 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). // Header of every relay datagram ("GHST" magic, little-endian type + size).
// Must match kGhostMagic / PacketHeader in skate3_native_ghost_proto.h. // Must match kGhostMagic / PacketHeader in skate3_native_ghost_proto.h.
struct XNetRelayHeader { struct XNetRelayHeader {
@ -69,6 +73,7 @@ struct XNetRelaySessionReply {
uint8_t count; uint8_t count;
XNetRelaySessionEntry list[32]; XNetRelaySessionEntry list[32];
}; };
#pragma pack(pop)
// One discovered session (a member of our room). // One discovered session (a member of our room).
struct XNetSession { struct XNetSession {
@ -143,6 +148,9 @@ class XNetTransport {
// header stays platform-independent. // header stays platform-independent.
uint8_t relay_addr_[16] = {}; uint8_t relay_addr_[16] = {};
bool relay_resolved_ = false; 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_; std::thread thread_;
bool running_ = false; bool running_ = false;

View file

@ -11,6 +11,7 @@
#include <thread> #include <thread>
#include <rex/platform.h> #include <rex/platform.h>
#include <rex/logging.h>
#include <rex/system/kernel_state.h> #include <rex/system/kernel_state.h>
#include <rex/system/xsocket.h> #include <rex/system/xsocket.h>
#include <rex/system/xam/user_profile.h> #include <rex/system/xam/user_profile.h>
@ -29,6 +30,15 @@
namespace rex { namespace rex {
namespace system { 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 { namespace {
// Virtual address range: 10.80.0.0/16 (host order prefix 0x0A500000). // Virtual address range: 10.80.0.0/16 (host order prefix 0x0A500000).
@ -156,14 +166,19 @@ void XNetTransport::Reconnect() {
} }
sockaddr_in relay_addr = {}; sockaddr_in relay_addr = {};
if (!Resolve(host_, port_, &relay_addr)) { if (!Resolve(host_, port_, &relay_addr)) {
REXLOG_WARN("xnet transport: cannot resolve relay '{}:{}'", host_, port_);
return; return;
} }
std::memcpy(relay_addr_, &relay_addr, sizeof(relay_addr)); std::memcpy(relay_addr_, &relay_addr, sizeof(relay_addr));
relay_resolved_ = true; relay_resolved_ = true;
sock_ = SocketCreate(); sock_ = SocketCreate();
if (sock_ == -1) { if (sock_ == -1) {
REXLOG_WARN("xnet transport: cannot open relay socket");
return; return;
} }
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. // Join the room so the relay routes our game traffic among its members.
XNetRelayJoin jp = {}; XNetRelayJoin jp = {};
jp.version = kVersion; jp.version = kVersion;
@ -184,6 +199,12 @@ void XNetTransport::Reconnect() {
std::memcpy(pkt + 7, &jp, sizeof(jp)); std::memcpy(pkt + 7, &jp, sizeof(jp));
SocketSendTo(sock_, &relay_addr, pkt, sizeof(pkt)); 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; running_ = true;
thread_ = std::thread(&XNetTransport::Run, this); thread_ = std::thread(&XNetTransport::Run, this);
} }
@ -271,6 +292,14 @@ void XNetTransport::Run() {
if (!running_) { if (!running_) {
return; 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)]; uint8_t buf[7 + sizeof(XNetRelayGame)];
sockaddr_in from = {}; sockaddr_in from = {};
@ -307,6 +336,9 @@ void XNetTransport::HandlePacket(const uint8_t* data, int size) {
// session_id != 0 means accepted (0 = refused). // session_id != 0 means accepted (0 = refused).
active_ = payload[0] != 0; active_ = payload[0] != 0;
joined_ = true; joined_ = true;
REXLOG_INFO("xnet transport: relay {} (vip {}.{}.{}.{})", active_ ? "joined" : "refused",
(LocalVirtualIp() >> 24) & 0xFF, (LocalVirtualIp() >> 16) & 0xFF,
(LocalVirtualIp() >> 8) & 0xFF, LocalVirtualIp() & 0xFF);
break; break;
} }
case kGame: { case kGame: {
@ -350,6 +382,8 @@ void XNetTransport::HandlePacket(const uint8_t* data, int size) {
std::lock_guard<std::mutex> lock(mu_); std::lock_guard<std::mutex> lock(mu_);
sessions_ = std::move(fresh); sessions_ = std::move(fresh);
++reply_seq_; ++reply_seq_;
REXLOG_INFO("xnet transport: session reply with {} session(s) in room '{}'", count,
room_.c_str());
break; break;
} }
default: default:

2
thirdparty/imgui vendored

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