Player distance and nametag settings; far players sent less and more coarsely; profiler addresses

Game
- Player distance (Settings > Interface, "player-distance"): how far
  away another player is still built as a skater, 50 to 1000 m, default
  120. Past it they keep their nametag or dot. All the way up shows
  everyone. A player already shown stays until 15% further out, so one
  skating along the edge is not built and taken down over and over. The
  game ran a full skater for every player on the server wherever they
  were, which is most of a frame on a full one
- Nametags are always ReSkate's own. The "ReSkate nametags" toggle, the
  "Nametag style" button and the nametag-style command are gone; the
  game's own nametags are never shown
- Nametag distance ("nametag-distance"): how far away a name still
  shows before it becomes a dot, 10 to 500 m. Default 120, was a fixed
  150, so by default the name and the skater go at the same distance
- Nametag dots ("nametag-dots"): off hides the dots for far players and
  the ones at the screen's edge for players off screen
- Friends' nametags only ("nametags-friends"): only Steam friends get a
  name or dot; chat bubbles still show for everyone

Server
- A player out of sight of another (half as far again as the low rate
  starts, 255 m by default) is sent to them once a second instead of
  five times. On a full server most players are out of each other's
  sight, and at five a second each they were most of what was sent
- Rotations are rounded before a pose is passed on, more coarsely the
  further away the recipient is: 0.04 degrees within the full rate's
  reach, 0.16 within the half rate's, a third of a degree past that,
  about 5 degrees out of sight. The pose is still an ordinary one. A
  bone that turned by less than a step is the same bytes as before, so
  the difference from the reference leaves it out, and the low bits of
  the ones that did turn pack away. By distance only: a player the
  crowd limit slowed keeps the fine rotations
- A recipient crossing from one precision to another is sent a whole
  state of that player, since a difference from a reference of another
  precision would carry every bone again
- "net" shows the size of the poses sent at each precision

Profiler
- A function in ReSkate.dll is reported by its address when the DLL has
  no symbols. A release build named every one of ours as the nearest of
  three exports, so a report could not say which was slow; the
  release's symbols name the address afterwards
This commit is contained in:
AyeZeeBB committed 2026-10-07 16:43:18 -05:00
1 parent d04920c793
commit 7ee82b2771
22 files changed
+325 -65

No files matched your search

+40 -9
View File
@@ -181,6 +181,13 @@ namespace {
// for a difference from one, however old the reference (net's pose size line), so they are
// kept rare: at every 8 seconds they were a seventh of everything a busy server sent.
constexpr std::uint64_t whole_state_refresh = 60000000;
// How finely a player's rotations are sent, by how far away the recipient is (coarsen_rotations):
// within the full rate's reach, the half rate's, the low rate's, and out of sight. The
// nearest is 0.04 degrees a step, which nothing shows; the low rate's a third of a degree,
// at 170 m and more; out of sight only where the skater is matters. A bone that turns by less
// than a step is not sent at all, and a far skater's fingers and face never turn by more.
constexpr std::array<unsigned, 4> pose_precision{4, 6, 7, 11};
constexpr std::array<const char *, 4> pose_precision_names{"near", "mid", "far", "out of sight"};
constexpr std::array<const char *, 6> traffic_names{"poses", "sound", "voice", "outfits", "objects", "other"};
constexpr std::size_t traffic_kind(PacketKind kind) noexcept {
return kind == PacketKind::pose ? 0 : kind == PacketKind::audio ? 1 : kind == PacketKind::voice ? 2
@@ -239,7 +246,7 @@ void Host::broadcast(const Packet &packet, bool reliable, bool fresh, std::uint6
// Recipients holding the same delta reference share one patch and compression
// (DeltaCache): every update is built first, then all are sent and recorded.
struct Encoded { Packet packet; std::vector<std::uint8_t> raw, wire; DeltaCache deltas; };
std::array<Encoded, 3> encoded;
std::array<Encoded, 12> encoded; // by the interval the pose carries, then by precision
struct Outgoing { Guest *guest; std::uint64_t id; WireUpdate update; PoseDelivery *delivery; std::uint32_t interval; unsigned variant; };
std::vector<Outgoing> outgoing;
outgoing.reserve(guests_.size());
@@ -282,6 +289,9 @@ void Host::broadcast(const Packet &packet, bool reliable, bool fresh, std::uint6
!needs_relay(p.direct_routes, p.route_reported, source->member, now_))
continue;
PoseDelivery *delivery{};
std::uint32_t slot{}; // how often this source's poses go to them, when not every `interval`
bool out_of_sight{};
unsigned precision{};
std::uint32_t interval = multiplayer_pose_interval(config_.tps);
if (packet.kind == PacketKind::pose) {
auto entry = std::find_if(p.pose_delivery.begin(), p.pose_delivery.end(),
@@ -300,8 +310,21 @@ void Host::broadcast(const Packet &packet, bool reliable, bool fresh, std::uint6
}
delivery->by_distance = pose_interval(distance, delivery->by_distance, config_.distances, config_.tps);
interval = crowd_interval(delivery->by_distance, distance, p.crowd);
// Half as far again as the low rate starts: nothing of a skater can be made out
// there, only where they are (their dot, the map).
// (A little nearer to come back into sight than to leave it.)
const auto sight = static_cast<float>(config_.distances.low_rate_start) * (delivery->precision == 3 ? 1.4f : 1.5f);
out_of_sight = config_.distances.valid() && distance > sight * sight;
precision = out_of_sight ? 3U : delivery->by_distance == 200000 ? 2U : delivery->by_distance == 100000 ? 1U : 0U;
} else {
delivery->by_distance = 0;
precision = delivery->precision;
}
// By distance only, which changes seldom: a difference from a reference of another
// precision carries every bone again, so the reference is renewed with it.
if (precision != delivery->precision) {
p.sender.forget(packet.source, PacketKind::pose);
delivery->precision = static_cast<std::uint8_t>(precision);
}
// Standing still for a few seconds: five poses a second carry it to anyone. The
// rate is back at once when they move (the change of rate sends the next pose).
@@ -309,16 +332,21 @@ void Host::broadcast(const Packet &packet, bool reliable, bool fresh, std::uint6
if (delivery->interval_us != interval) delivery->next_source_time = 0;
delivery->interval_us = interval;
if (interval > multiplayer_pose_interval(config_.tps) && packet.time_us < delivery->next_source_time) continue;
// Out of sight, one pose a second is sent (as low-rate poses, a game holds a skater
// it hears nothing of for several seconds). On a full server most players are out
// of each other's sight, and at five a second each they were most of what was sent.
slot = out_of_sight && interval == 200000 ? 1000000U : interval;
}
const auto variant = interval == 200000 ? 2U : interval == 100000 ? 1U : 0U;
const auto variant = (interval == 200000 ? 2U : interval == 100000 ? 1U : 0U) * 4 + precision;
auto &data = encoded[variant];
if (data.raw.empty()) {
data.packet = packet;
data.packet.pose_interval_us = interval;
if (packet.kind == PacketKind::pose) coarsen_rotations(data.packet.pose, pose_precision[precision]);
data.raw = encode(data.packet, true);
data.wire = encode_wire_bytes(data.raw);
}
outgoing.push_back({&p, id, p.sender.prepare(data.packet, data.raw, data.wire, data.deltas), delivery, interval, variant});
outgoing.push_back({&p, id, p.sender.prepare(data.packet, data.raw, data.wire, data.deltas), delivery, slot ? slot : interval, variant});
}
for (auto &out : outgoing) {
// A packet that cannot be built for anyone is its source's fault, never this
@@ -334,8 +362,8 @@ void Host::broadcast(const Packet &packet, bool reliable, bool fresh, std::uint6
counted->total.snapshots += whole;
}
if (sending.kind == PacketKind::pose && !whole) {
++pose_sizes_.sent;
pose_sizes_.sent_bytes += out.update.bytes.size();
++pose_sizes_.sent[out.variant % 4];
pose_sizes_.sent_bytes[out.variant % 4] += out.update.bytes.size();
}
out.guest->sender.sent(sending, std::move(out.update));
}
@@ -1003,11 +1031,14 @@ std::string Host::network_report(std::string_view player, bool console) {
}
text += "\nloop: " + loop_report();
text += "\n" + by_kind(traffic_);
if (pose_sizes_.samples && pose_sizes_.sent) {
if (pose_sizes_.samples) {
const auto each = [&](std::uint64_t sum) { return std::to_string(sum / pose_sizes_.samples) + " B"; };
text += "\npose size: " + std::to_string(pose_sizes_.sent_bytes / pose_sizes_.sent) + " B as sent (whole " + each(pose_sizes_.whole) +
"); it would be " + each(pose_sizes_.second) + " against a reference renewed every second, " + each(pose_sizes_.quarter) +
" every quarter second, " + each(pose_sizes_.last) + " against the pose before (" + std::to_string(pose_sizes_.samples) + " measured)";
text += "\npose size as sent:";
for (std::size_t i = 0; i < pose_sizes_.sent.size(); ++i)
if (pose_sizes_.sent[i])
text += std::string(" ") + pose_precision_names[i] + " " + std::to_string(pose_sizes_.sent_bytes[i] / pose_sizes_.sent[i]) + " B x" +
std::to_string(pose_sizes_.sent[i]);
text += "; as it arrives " + each(pose_sizes_.last) + " against the pose before, whole " + each(pose_sizes_.whole);
}
{
// Who uploads most, and what most of it is: one player's outfit or objects can cost