ghost multiplayer: MTU-safe chunked pose transport + pose smoothing
Phase 3: two robustness fixes for full-model ghosts. - Wire: a full skeleton pose is ~3.1 KB, above the 1500-byte path MTU, so it now travels as up to 3 MTU-safe kTypePoseChunk datagrams (<=1.2 KB each, ~1.2 KB total with headers). The relay forwards each chunk verbatim (re-stamping only the chunk header session id), so its code path is shared with the legacy single-datagram POSE and needs no pose-body parsing. GhostPoseBuildChunks / GhostPoseReassembler (inline in the proto header, standalone-testable) tolerate out-of-order and duplicate chunks, drop incomplete sequences (the next frame resends), discard a partial on a newer seq, and special-case a header-only pose as a single chunk. Receive identity comes from the chunk header (the relay re-stamps it), not the blob, which the relay never touches. - Smoothing: each remote ghost now holds a target skeleton (latest received pose) and a display skeleton chased per render query with exponential catch-up (time constant ~33 ms), so the 15 Hz bone stream renders as smooth motion instead of stepping. New/reshaped skeletons snap instead of blending between layouts. Reassembly state is per-session on the network thread, bounded (64) and cleared on prune/disable. Protocol version bumped to 2.
This commit is contained in:
parent
7b949a312b
commit
cceeac7c4f
3 changed files with 217 additions and 40 deletions
|
|
@ -183,9 +183,16 @@ double g_local_pose_time = 0.0;
|
||||||
// Remote ghosts, keyed by the sender's session id.
|
// Remote ghosts, keyed by the sender's session id.
|
||||||
struct Ghost {
|
struct Ghost {
|
||||||
uint32_t session_id = 0;
|
uint32_t session_id = 0;
|
||||||
float world[16] = {};
|
float world[16] = {}; // latest received entity world (cube fallback)
|
||||||
uint16_t bone_count = 0; // live skeleton rows (0 = no pose)
|
// Target skeleton: the latest complete pose received off the wire.
|
||||||
float bones[kGhostMaxBones * 12] = {};
|
uint16_t target_count = 0;
|
||||||
|
float target_bones[kGhostMaxBones * 12] = {};
|
||||||
|
// Display skeleton: target chased per render query (exponential smoothing),
|
||||||
|
// so 15 Hz bone updates render as smooth motion instead of stepping.
|
||||||
|
uint16_t display_count = 0;
|
||||||
|
float display_bones[kGhostMaxBones * 12] = {};
|
||||||
|
double last_display = 0.0;
|
||||||
|
bool display_snapped = false;
|
||||||
double last_seen = 0.0;
|
double last_seen = 0.0;
|
||||||
};
|
};
|
||||||
std::vector<Ghost> g_ghosts;
|
std::vector<Ghost> g_ghosts;
|
||||||
|
|
@ -226,6 +233,10 @@ bool g_lan_probing = false;
|
||||||
double g_lan_probe_since = 0.0;
|
double g_lan_probe_since = 0.0;
|
||||||
double g_last_ping = 0.0;
|
double g_last_ping = 0.0;
|
||||||
|
|
||||||
|
// Chunk reassembly state per remote session (network thread only; bounded,
|
||||||
|
// ResetSession clears it).
|
||||||
|
std::unordered_map<uint32_t, GhostPoseReassembler> g_pending_poses;
|
||||||
|
|
||||||
// Pose pacing.
|
// Pose pacing.
|
||||||
double g_pose_acc = 0.0;
|
double g_pose_acc = 0.0;
|
||||||
uint32_t g_seq = 0;
|
uint32_t g_seq = 0;
|
||||||
|
|
@ -288,16 +299,36 @@ void SendPose(GhostSock sock, const GhostAddr& to, uint32_t& seq) {
|
||||||
}
|
}
|
||||||
pp.bone_count = pose_rows;
|
pp.bone_count = pose_rows;
|
||||||
pp.flags = pose_rows != 0 ? kPoseFlagsBones : 0;
|
pp.flags = pose_rows != 0 ? kPoseFlagsBones : 0;
|
||||||
PacketBuf p;
|
// MTU-safe chunks (the full pose is ~3 KB and must not IP-fragment).
|
||||||
BuildPacket(p, kTypePose, &pp, sizeof(pp));
|
PacketBuf chunks[kGhostMaxChunks];
|
||||||
GhostSockSendTo(sock, to, &p, PacketSize(p));
|
const uint8_t n = GhostPoseBuildChunks(0, pp.seq, pp, chunks);
|
||||||
|
for (uint8_t i = 0; i < n; ++i) {
|
||||||
|
GhostSockSendTo(sock, to, &chunks[i], PacketSize(chunks[i]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- packet receive --------------------------------------------------------
|
// ---- packet receive --------------------------------------------------------
|
||||||
void HandlePacket(const GhostAddr& from, const PacketBuf& p, double now) {
|
void HandlePacket(const GhostAddr& from, const PacketBuf& p, double now) {
|
||||||
if (p.h.type == kTypePose) {
|
if (p.h.type == kTypePoseChunk) {
|
||||||
PosePayload pp = {};
|
PoseChunkPayload cp = {};
|
||||||
std::memcpy(&pp, p.payload, sizeof(pp));
|
std::memcpy(&cp, p.payload, sizeof(cp));
|
||||||
|
if (cp.session_id == 0 || cp.chunks == 0 || cp.chunks > kGhostMaxChunks) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Reassemble this session's pose stream (network thread only). The
|
||||||
|
// sender identity is the CHUNK HEADER's session id: the relay re-stamps
|
||||||
|
// that (offset 0 of the payload), while the blob's own session field is
|
||||||
|
// a sender-side zero the relay never touches.
|
||||||
|
const uint32_t sender = cp.session_id;
|
||||||
|
if (g_pending_poses.size() > 64) {
|
||||||
|
g_pending_poses.clear(); // churn guard: partials re-send next frame
|
||||||
|
}
|
||||||
|
PosePayload pose = {};
|
||||||
|
GhostPoseReassembler& r = g_pending_poses[sender];
|
||||||
|
if (!r.Push(cp.seq, cp.blob_size, cp.chunk, cp.chunks, cp.data,
|
||||||
|
cp.data_size, &pose)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(g_mu);
|
std::lock_guard<std::mutex> lock(g_mu);
|
||||||
// Determine which relay this came from and skip our own pose echoes
|
// Determine which relay this came from and skip our own pose echoes
|
||||||
|
|
@ -310,42 +341,59 @@ void HandlePacket(const GhostAddr& from, const PacketBuf& p, double now) {
|
||||||
const uint32_t own = from_public ? g_session_public
|
const uint32_t own = from_public ? g_session_public
|
||||||
: from_lan ? g_session_lan
|
: from_lan ? g_session_lan
|
||||||
: 0;
|
: 0;
|
||||||
if (pp.session_id == 0 || (own != 0 && pp.session_id == own)) {
|
if (sender == 0 || (own != 0 && sender == own)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const bool finite =
|
const bool finite =
|
||||||
std::isfinite(pp.pos[0]) && std::isfinite(pp.pos[1]) &&
|
std::isfinite(pose.pos[0]) && std::isfinite(pose.pos[1]) &&
|
||||||
std::isfinite(pp.pos[2]);
|
std::isfinite(pose.pos[2]);
|
||||||
if (!finite) {
|
if (!finite) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const uint16_t rows =
|
const uint16_t rows =
|
||||||
(pp.flags & kPoseFlagsBones) != 0
|
(pose.flags & kPoseFlagsBones) != 0
|
||||||
? std::min<uint16_t>(pp.bone_count, kGhostMaxBones)
|
? std::min<uint16_t>(pose.bone_count, kGhostMaxBones)
|
||||||
: 0;
|
: 0;
|
||||||
auto it = g_ghost_index.find(pp.session_id);
|
auto it = g_ghost_index.find(sender);
|
||||||
if (it != g_ghost_index.end() && it->second < g_ghosts.size()) {
|
if (it != g_ghost_index.end() && it->second < g_ghosts.size()) {
|
||||||
Ghost& gh = g_ghosts[it->second];
|
Ghost& gh = g_ghosts[it->second];
|
||||||
std::memcpy(gh.world, pp.rot, 9 * sizeof(float));
|
std::memcpy(gh.world, pose.rot, 9 * sizeof(float));
|
||||||
std::memcpy(gh.world + 12, pp.pos, 3 * sizeof(float));
|
std::memcpy(gh.world + 12, pose.pos, 3 * sizeof(float));
|
||||||
gh.world[15] = 1.0f;
|
gh.world[15] = 1.0f;
|
||||||
gh.bone_count = rows;
|
gh.target_count = rows;
|
||||||
if (rows != 0) {
|
if (rows != 0) {
|
||||||
std::memcpy(gh.bones, pp.bones, size_t(rows) * 12 * sizeof(float));
|
std::memcpy(gh.target_bones, pose.bones,
|
||||||
|
size_t(rows) * 12 * sizeof(float));
|
||||||
|
}
|
||||||
|
// New skeleton (count changed or first pose): snap the display so we
|
||||||
|
// never blend between two different bone layouts.
|
||||||
|
if (!gh.display_snapped || gh.display_count != rows) {
|
||||||
|
gh.display_count = rows;
|
||||||
|
if (rows != 0) {
|
||||||
|
std::memcpy(gh.display_bones, gh.target_bones,
|
||||||
|
size_t(rows) * 12 * sizeof(float));
|
||||||
|
}
|
||||||
|
gh.display_snapped = true;
|
||||||
|
gh.last_display = 0.0;
|
||||||
}
|
}
|
||||||
gh.last_seen = now;
|
gh.last_seen = now;
|
||||||
} else if (g_ghosts.size() < 16) {
|
} else if (g_ghosts.size() < 16) {
|
||||||
Ghost gh;
|
Ghost gh;
|
||||||
gh.session_id = pp.session_id;
|
gh.session_id = sender;
|
||||||
std::memcpy(gh.world, pp.rot, 9 * sizeof(float));
|
std::memcpy(gh.world, pose.rot, 9 * sizeof(float));
|
||||||
std::memcpy(gh.world + 12, pp.pos, 3 * sizeof(float));
|
std::memcpy(gh.world + 12, pose.pos, 3 * sizeof(float));
|
||||||
gh.world[15] = 1.0f;
|
gh.world[15] = 1.0f;
|
||||||
gh.bone_count = rows;
|
gh.target_count = rows;
|
||||||
|
gh.display_count = rows;
|
||||||
if (rows != 0) {
|
if (rows != 0) {
|
||||||
std::memcpy(gh.bones, pp.bones, size_t(rows) * 12 * sizeof(float));
|
std::memcpy(gh.target_bones, pose.bones,
|
||||||
|
size_t(rows) * 12 * sizeof(float));
|
||||||
|
std::memcpy(gh.display_bones, gh.target_bones,
|
||||||
|
size_t(rows) * 12 * sizeof(float));
|
||||||
}
|
}
|
||||||
|
gh.display_snapped = true;
|
||||||
gh.last_seen = now;
|
gh.last_seen = now;
|
||||||
g_ghost_index[pp.session_id] = g_ghosts.size();
|
g_ghost_index[sender] = g_ghosts.size();
|
||||||
g_ghosts.push_back(gh);
|
g_ghosts.push_back(gh);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -425,6 +473,7 @@ void PruneGhosts(double now) {
|
||||||
for (size_t i = 0; i < g_ghosts.size();) {
|
for (size_t i = 0; i < g_ghosts.size();) {
|
||||||
if (now - g_ghosts[i].last_seen > kGhostStaleSec) {
|
if (now - g_ghosts[i].last_seen > kGhostStaleSec) {
|
||||||
g_ghost_index.erase(g_ghosts[i].session_id);
|
g_ghost_index.erase(g_ghosts[i].session_id);
|
||||||
|
g_pending_poses.erase(g_ghosts[i].session_id);
|
||||||
g_ghosts[i] = g_ghosts.back();
|
g_ghosts[i] = g_ghosts.back();
|
||||||
g_ghosts.pop_back();
|
g_ghosts.pop_back();
|
||||||
// Fix the moved entry's index (only if it wasn't the one we removed).
|
// Fix the moved entry's index (only if it wasn't the one we removed).
|
||||||
|
|
@ -444,6 +493,7 @@ void ResetSession() {
|
||||||
g_roster = 0;
|
g_roster = 0;
|
||||||
g_ghosts.clear();
|
g_ghosts.clear();
|
||||||
g_ghost_index.clear();
|
g_ghost_index.clear();
|
||||||
|
g_pending_poses.clear();
|
||||||
g_session_public = 0;
|
g_session_public = 0;
|
||||||
g_session_lan = 0;
|
g_session_lan = 0;
|
||||||
g_peer_public = Peer{};
|
g_peer_public = Peer{};
|
||||||
|
|
@ -781,13 +831,31 @@ bool GetGhostPose(uint32_t index, float world[16], float color[3],
|
||||||
if (index >= g_ghosts.size()) {
|
if (index >= g_ghosts.size()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const Ghost& gh = g_ghosts[index];
|
Ghost& gh = g_ghosts[index];
|
||||||
std::memcpy(world, gh.world, sizeof(float) * 16);
|
std::memcpy(world, gh.world, sizeof(float) * 16);
|
||||||
GhostColor(gh.session_id, color);
|
GhostColor(gh.session_id, color);
|
||||||
const uint16_t rows = std::min<uint16_t>(gh.bone_count, max_bones);
|
// Chase the display skeleton toward the latest received target: per-frame
|
||||||
|
// exponential catch-up (time-constant ~33 ms) turns the stepped 15 Hz
|
||||||
|
// bone stream into smooth motion. Only once the render thread has moved
|
||||||
|
// the display do we advance it, so two queries in one frame can't double.
|
||||||
|
if (gh.display_count != 0 && gh.display_count == gh.target_count) {
|
||||||
|
const double now = NowSec();
|
||||||
|
double dt = gh.last_display > 0.0 ? now - gh.last_display : 0.0;
|
||||||
|
dt = std::min(dt, 0.1);
|
||||||
|
if (dt > 0.0) {
|
||||||
|
const float alpha = float(1.0 - std::exp(-dt * 30.0));
|
||||||
|
const size_t n = size_t(gh.display_count) * 12;
|
||||||
|
for (size_t i = 0; i < n; ++i) {
|
||||||
|
gh.display_bones[i] +=
|
||||||
|
(gh.target_bones[i] - gh.display_bones[i]) * alpha;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gh.last_display = now;
|
||||||
|
}
|
||||||
|
const uint16_t rows = std::min<uint16_t>(gh.display_count, max_bones);
|
||||||
*bone_count = rows;
|
*bone_count = rows;
|
||||||
if (rows != 0) {
|
if (rows != 0) {
|
||||||
std::memcpy(bones, gh.bones, size_t(rows) * 12 * sizeof(float));
|
std::memcpy(bones, gh.display_bones, size_t(rows) * 12 * sizeof(float));
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,15 @@
|
||||||
// A packet is PacketHeader followed by a fixed per-type payload. PacketBuf
|
// A packet is PacketHeader followed by a fixed per-type payload. PacketBuf
|
||||||
// is sized for the largest payload.
|
// is sized for the largest payload.
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
namespace skate3::native_ghost {
|
namespace skate3::native_ghost {
|
||||||
|
|
||||||
constexpr uint32_t kGhostMagic = 0x54484753u; // "GHST" (LE)
|
constexpr uint32_t kGhostMagic = 0x54484753u; // "GHST" (LE)
|
||||||
constexpr uint8_t kGhostVersion = 1;
|
constexpr uint8_t kGhostVersion = 2;
|
||||||
constexpr uint16_t kGhostDefaultPort = 40001;
|
constexpr uint16_t kGhostDefaultPort = 40001;
|
||||||
constexpr uint32_t kGhostMaxRoomLen = 8;
|
constexpr uint32_t kGhostMaxRoomLen = 8;
|
||||||
constexpr uint32_t kGhostMaxRoster = 32;
|
constexpr uint32_t kGhostMaxRoster = 32;
|
||||||
|
|
@ -39,10 +41,11 @@ constexpr const char* kGhostLobbyRoom = "LOBBY";
|
||||||
enum PacketType : uint8_t {
|
enum PacketType : uint8_t {
|
||||||
kTypeJoin = 1, // client -> relay: { JoinPayload } (empty room = server assigns)
|
kTypeJoin = 1, // client -> relay: { JoinPayload } (empty room = server assigns)
|
||||||
kTypeHello = 2, // relay -> client: { HelloPayload } (session id, room, roster)
|
kTypeHello = 2, // relay -> client: { HelloPayload } (session id, room, roster)
|
||||||
kTypePose = 3, // client -> relay -> other members: { PosePayload }
|
kTypePose = 3, // legacy single-datagram pose (forwarded, unused by clients)
|
||||||
kTypePing = 4, // client -> relay / LAN broadcast probe: { JoinPayload }
|
kTypePing = 4, // client -> relay / LAN broadcast probe: { JoinPayload }
|
||||||
kTypePong = 5, // relay -> client: empty
|
kTypePong = 5, // relay -> client: empty
|
||||||
kTypeLeave = 6, // client -> relay: empty
|
kTypeLeave = 6, // client -> relay: empty
|
||||||
|
kTypePoseChunk = 7, // client -> relay -> other members: { PoseChunkPayload }
|
||||||
};
|
};
|
||||||
|
|
||||||
struct alignas(1) PacketHeader {
|
struct alignas(1) PacketHeader {
|
||||||
|
|
@ -66,11 +69,12 @@ struct alignas(1) HelloPayload {
|
||||||
uint32_t roster[kGhostMaxRoster];
|
uint32_t roster[kGhostMaxRoster];
|
||||||
};
|
};
|
||||||
|
|
||||||
// Full-pose packet (the Phase 2 character-model path): the sender's skater
|
// Full-pose blob (the character-model path): the sender's skater skeleton,
|
||||||
// skeleton, 3 float4 rows per bone (column-vector affine [R | t], model ->
|
// 3 float4 rows per bone (column-vector affine [R | t], model -> world, row
|
||||||
// world, row k = bone k exactly as the scene renderer's bone ring expects).
|
// k = bone k exactly as the scene renderer's bone ring expects). Flags bit 0
|
||||||
// Flags bit 0 = bones[] valid. pos/rot are kept from the entity world for
|
// = bones[] valid. pos/rot are kept from the entity world for the cube
|
||||||
// the cube fallback / diagnostics.
|
// fallback / diagnostics. This struct is the REASSEMBLED content of a
|
||||||
|
// chunked PoseChunk stream; it is no longer sent as a single datagram.
|
||||||
struct alignas(1) PosePayload {
|
struct alignas(1) PosePayload {
|
||||||
uint32_t session_id; // set by the relay on forward (the sender's id)
|
uint32_t session_id; // set by the relay on forward (the sender's id)
|
||||||
uint32_t seq; // sender monotonic sequence
|
uint32_t seq; // sender monotonic sequence
|
||||||
|
|
@ -85,7 +89,81 @@ struct alignas(1) PosePayload {
|
||||||
|
|
||||||
constexpr uint8_t kPoseFlagsBones = 1;
|
constexpr uint8_t kPoseFlagsBones = 1;
|
||||||
|
|
||||||
// Largest payload: PosePayload (32 + 3072 = 3104 bytes).
|
// ---- chunked pose transport -------------------------------------------------
|
||||||
|
// A full PosePayload is ~3.1 KB, far above the 1500-byte path MTU, so it is
|
||||||
|
// split into kGhostMaxChunks MTU-safe datagrams (<= ~1.2 KB each). The relay
|
||||||
|
// forwards each chunk verbatim (it never parses the body), and the receiver
|
||||||
|
// reassembles on the sequence. A lost chunk drops that pose (the next frame
|
||||||
|
// resends); out-of-order chunks are tolerated by the per-chunk offset copy.
|
||||||
|
constexpr uint16_t kGhostPoseHeaderSize =
|
||||||
|
uint16_t(offsetof(PosePayload, bones)); // 68
|
||||||
|
constexpr uint16_t kGhostChunkData = 1200;
|
||||||
|
constexpr uint8_t kGhostMaxChunks = 3;
|
||||||
|
constexpr uint16_t kGhostPoseBlobSize = uint16_t(sizeof(PosePayload)); // 3140
|
||||||
|
|
||||||
|
struct alignas(1) PoseChunkPayload {
|
||||||
|
uint32_t session_id; // offset 0: relay re-stamps the sender id
|
||||||
|
uint32_t seq; // pose sequence this chunk belongs to
|
||||||
|
uint16_t blob_size; // total blob bytes this seq comprises
|
||||||
|
uint16_t data_size; // bytes actually valid in data[]
|
||||||
|
uint8_t chunk; // 0-based chunk index
|
||||||
|
uint8_t chunks; // total chunks for this seq
|
||||||
|
uint8_t data[kGhostChunkData];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Receiver-side reassembly for ONE session (one instance per remote peer,
|
||||||
|
// network thread only). Push() returns true exactly when a complete pose
|
||||||
|
// becomes available and fills *out; a fresh seq discards any partial state.
|
||||||
|
struct GhostPoseReassembler {
|
||||||
|
void Reset() {
|
||||||
|
seq_ = 0;
|
||||||
|
blob_size_ = 0;
|
||||||
|
chunks_ = 0;
|
||||||
|
received_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Push(uint32_t seq, uint16_t blob_size, uint8_t chunk, uint8_t chunks,
|
||||||
|
const uint8_t* data, uint16_t data_size, PosePayload* out) {
|
||||||
|
if (chunk >= chunks || chunks == 0 || chunks > kGhostMaxChunks ||
|
||||||
|
blob_size == 0 || blob_size > kGhostPoseBlobSize) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (seq != seq_) {
|
||||||
|
Reset();
|
||||||
|
seq_ = seq;
|
||||||
|
blob_size_ = blob_size;
|
||||||
|
chunks_ = chunks;
|
||||||
|
} else if (blob_size != blob_size_ || chunks != chunks_) {
|
||||||
|
return false; // inconsistent mid-sequence; drop
|
||||||
|
}
|
||||||
|
const uint32_t bit = 1u << chunk;
|
||||||
|
if ((received_ & bit) != 0) {
|
||||||
|
return false; // duplicate chunk
|
||||||
|
}
|
||||||
|
received_ |= bit;
|
||||||
|
const uint16_t off = uint16_t(chunk) * kGhostChunkData;
|
||||||
|
const uint16_t avail = blob_size_ > off ? blob_size_ - off : 0;
|
||||||
|
const uint16_t n = std::min<uint16_t>(data_size, avail);
|
||||||
|
std::memcpy(blob_ + off, data, n);
|
||||||
|
const uint32_t want = (1u << chunks_) - 1u;
|
||||||
|
if ((received_ & want) != want) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::memset(out, 0, sizeof(*out));
|
||||||
|
std::memcpy(out, blob_, std::min<uint16_t>(blob_size_, uint16_t(sizeof(*out))));
|
||||||
|
Reset();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
uint32_t seq_ = 0;
|
||||||
|
uint16_t blob_size_ = 0;
|
||||||
|
uint8_t chunks_ = 0;
|
||||||
|
uint32_t received_ = 0;
|
||||||
|
uint8_t blob_[kGhostPoseBlobSize] = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Largest payload: PoseChunkPayload (1214 bytes).
|
||||||
constexpr uint16_t kGhostMaxPayload = 3200;
|
constexpr uint16_t kGhostMaxPayload = 3200;
|
||||||
|
|
||||||
struct alignas(1) PacketBuf {
|
struct alignas(1) PacketBuf {
|
||||||
|
|
@ -107,6 +185,35 @@ inline void BuildPacket(PacketBuf& p, uint8_t type, const void* payload,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Builds the chunk packets for a pose. Always writes exactly the returned
|
||||||
|
// chunk count (1 for a header-only pose, up to kGhostMaxChunks for a full
|
||||||
|
// skeleton). The blob is the PosePayload serialized from its first byte.
|
||||||
|
inline uint8_t GhostPoseBuildChunks(uint32_t session_id, uint32_t seq,
|
||||||
|
const PosePayload& pose,
|
||||||
|
PacketBuf out[kGhostMaxChunks]) {
|
||||||
|
const uint16_t blob_size =
|
||||||
|
(pose.flags & kPoseFlagsBones) != 0
|
||||||
|
? uint16_t(sizeof(PosePayload))
|
||||||
|
: kGhostPoseHeaderSize;
|
||||||
|
const uint8_t chunks =
|
||||||
|
uint8_t((blob_size + kGhostChunkData - 1) / kGhostChunkData);
|
||||||
|
const uint8_t* blob = reinterpret_cast<const uint8_t*>(&pose);
|
||||||
|
for (uint8_t c = 0; c < chunks; ++c) {
|
||||||
|
PoseChunkPayload cp = {};
|
||||||
|
cp.session_id = session_id;
|
||||||
|
cp.seq = seq;
|
||||||
|
cp.blob_size = blob_size;
|
||||||
|
cp.chunk = c;
|
||||||
|
cp.chunks = chunks;
|
||||||
|
const uint16_t off = uint16_t(c) * kGhostChunkData;
|
||||||
|
const uint16_t n = std::min<uint16_t>(kGhostChunkData, blob_size - off);
|
||||||
|
cp.data_size = n;
|
||||||
|
std::memcpy(cp.data, blob + off, n);
|
||||||
|
BuildPacket(out[c], kTypePoseChunk, &cp, sizeof(cp));
|
||||||
|
}
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
|
|
||||||
// ---- room code helpers (uppercase alnum, max kGhostMaxRoomLen) -------------
|
// ---- room code helpers (uppercase alnum, max kGhostMaxRoomLen) -------------
|
||||||
|
|
||||||
inline bool RoomValid(const char* room, uint8_t len) {
|
inline bool RoomValid(const char* room, uint8_t len) {
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,8 @@ void GhostRelay::HandlePacket(const GhostAddr& from, const PacketBuf& p,
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case kTypePose: {
|
case kTypePose:
|
||||||
|
case kTypePoseChunk: {
|
||||||
// Clients join before posing; a pose with no room is auto-joined to
|
// Clients join before posing; a pose with no room is auto-joined to
|
||||||
// the lobby so the relay stays resilient to a missed HELLO.
|
// the lobby so the relay stays resilient to a missed HELLO.
|
||||||
if (me->room_len == 0) {
|
if (me->room_len == 0) {
|
||||||
|
|
@ -186,11 +187,12 @@ void GhostRelay::HandlePacket(const GhostAddr& from, const PacketBuf& p,
|
||||||
me->last_seen = now;
|
me->last_seen = now;
|
||||||
// Forward the payload VERBATIM to the other room members, re-stamping
|
// Forward the payload VERBATIM to the other room members, re-stamping
|
||||||
// only the sender session id at payload offset 0 (little-endian). The
|
// only the sender session id at payload offset 0 (little-endian). The
|
||||||
// relay never parses the pose body, so the protocol can grow (e.g. the
|
// relay never parses the pose body (a full skeleton pose is a
|
||||||
// full skeleton palette) without a relay update.
|
// multi-chunk kTypePoseChunk stream; each chunk is forwarded the same
|
||||||
|
// way), so the protocol can grow without a relay update.
|
||||||
PacketBuf out = p;
|
PacketBuf out = p;
|
||||||
out.h.magic = kGhostMagic;
|
out.h.magic = kGhostMagic;
|
||||||
out.h.type = kTypePose;
|
out.h.type = p.h.type;
|
||||||
std::memcpy(out.payload, &me->session_id, sizeof(me->session_id));
|
std::memcpy(out.payload, &me->session_id, sizeof(me->session_id));
|
||||||
const uint16_t size = PacketSize(out);
|
const uint16_t size = PacketSize(out);
|
||||||
for (const Client& c : clients_) {
|
for (const Client& c : clients_) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue