Show room server ping in the game overlay

This commit is contained in:
BlackBearReloaded committed 2026-10-10 20:34:56 -04:00
1 parent df75b71568
commit 8123fe3e86
8 files changed
+111 -31

No files matched your search

+6
View File
@@ -46,6 +46,12 @@ launcher before changing rooms. If the room connection fails during play, the im
requests the normal return-to-menu path. This recovery passed on `.30` from a Mario Kart
8 Deluxe 3.0.5 wireless lobby; recovery during an active race remains unqualified.
The game overlay adds **P42** for a room-server round-trip time of 42 milliseconds while
connected (display capped at 9999). It uses ENet's existing measurement, with no extra ping
traffic or changes to ENet. This measures the server path, not latency to another player.
Packet retransmission follows ENet's existing reliable transport. A full disconnect returns
to the launcher; seamless mid-game reconnection is not supported.
## Private server example
Use an Eden `eden-room` executable whose revision is recorded for the test. Initial
+15
View File
@@ -14,6 +14,21 @@ shadowing it. The network compiler launcher now hashes the generated room-member
packet headers as extra cache inputs. Warm-cache shadowing and subsequent header-change
checks pass; the post-build header audit remains mandatory.
The rebased native build and mandatory checks passed, including the header audit. Packet
and real-room ASan/UBSan tests passed after rebuilding host libraries for the new Eden
settings layout; the test now reads Boost's version from Eden's dependency manifest.
## Server ping overlay
The OpenGL and Vulkan game overlays append `P42` for a 42 ms room-server round trip.
The Eden room receive thread copies ENet's existing measurement into an atomic field;
the renderer never reads the live ENet peer. Leaving or losing the room removes the
reading. ENet source, retransmission, timeouts and wire behavior are unchanged.
The native build and mandatory checks, formatting, bounded values, disconnect clearing,
real-room sanitizer tests and both shader compilations passed. The glyph capacity is 28 and the Vulkan push constants
remain within the guaranteed 128 bytes. Console display validation is still pending.
## Latest hardware result
The operator subsequently confirmed creating a Mario Kart lobby while connected to the
+10 -4
View File
@@ -16,6 +16,7 @@
#include <cstring>
#include <time.h>
#include "hud.h"
#include "network/network.h"
#include "loading_scene_glsl.h"
#include "preferences.h"
#include "diagnostics.h"
@@ -40,6 +41,10 @@ namespace Eden {
void PresentVulkanLoading(VideoCore::RendererBase& renderer);
#endif
namespace {
int RoomPingMilliseconds() {
const auto member = Network::GetRoomMember().lock();
return member ? member->GetPingMilliseconds() : -1;
}
// What the loading screen's time carries for reduced motion (loading_scene.glsl): 1000 when the
// player turned it on in Settings > Accessibility, read once.
double LoadingCalm() {
@@ -309,7 +314,7 @@ public:
speed_percent = stats.emulation_speed * 100.0;
last_stats = now;
}
const auto text = FormatHudText(clock, speed_percent, "OGL");
const auto text = FormatHudText(clock, speed_percent, "OGL", RoomPingMilliseconds());
if (hud_enabled.load(std::memory_order_relaxed)) DrawHud(text.data(), false);
Check(eglSwapBuffers(display, surface), "eglSwapBuffers");
}
@@ -407,11 +412,11 @@ private:
const char* sources[]{
"#version 330 core\nvoid main(){vec2 p=vec2((gl_VertexID<<1)&2,gl_VertexID&2);"
"gl_Position=vec4(p*2.0-1.0,0.0,1.0);}",
"#version 330 core\nuniform uint glyphs[24]; uniform ivec2 origin; uniform int startup;"
"#version 330 core\nuniform uint glyphs[28]; uniform ivec2 origin; uniform int startup;"
"uniform float cell; out vec4 color;"
"void main(){ivec2 p=ivec2(gl_FragCoord.xy)-origin; bool ink=false;"
"if(p.x>=0&&p.y>=0){ivec2 c=ivec2(vec2(p)/cell);int i=c.x/4;int x=c.x%4;"
"if(i<24&&x<3&&c.y<5) ink=((glyphs[i]>>uint(c.y*3+2-x))&1u)!=0u;}"
"if(i<28&&x<3&&c.y<5) ink=((glyphs[i]>>uint(c.y*3+2-x))&1u)!=0u;}"
"color=startup!=0?(ink?vec4(0.7216,0.9490,0.0471,1.0):vec4(0.0196,0.0392,0.0039,1.0))"
":(ink?vec4(0.9,0.95,1.0,1.0):vec4(0.025,0.04,0.075,0.5));}"};
for (unsigned i = 0; i < 2; ++i) {
@@ -640,7 +645,8 @@ void GraphicsWindow::OnFrameDisplayed() {
}
vulkan_hud =
MakeHudSnapshot(vulkan_hud_clock, vulkan_hud_output_clock, vulkan_hud_speed,
Eden::Display::output_millihertz.load(std::memory_order_relaxed) / 1000.0);
Eden::Display::output_millihertz.load(std::memory_order_relaxed) / 1000.0,
RoomPingMilliseconds());
#endif
++frame_total;
if (frame_sample_start < 0) {
+28 -19
View File
@@ -78,8 +78,8 @@ inline uint32_t HudGlyph(char c) {
default: return 0;
}
}
inline std::array<uint32_t, 24> HudText(std::string_view text) {
std::array<uint32_t, 24> glyphs{};
inline std::array<uint32_t, 28> HudText(std::string_view text) {
std::array<uint32_t, 28> glyphs{};
for (size_t i = 0; i < text.size() && i < glyphs.size(); ++i)
glyphs[i] = HudGlyph(text[i]);
return glyphs;
@@ -88,10 +88,11 @@ inline std::array<uint32_t, 24> HudText(std::string_view text) {
// they are scaled to the picture, and `width` then carries the picture's height to the shader
// (vulkan_hud_draw.inc).
struct HudSnapshot {
std::array<uint32_t, 24> glyphs{};
std::array<uint32_t, 28> glyphs{};
uint32_t width{};
uint32_t x{28}, y{30}, loading{};
};
static_assert(sizeof(HudSnapshot) == 128); // Vulkan guarantees at least 128 push-constant bytes.
// What a game's start is doing, for the loading screen (loading_scene.glsl draws it: the steps
// one under the other and a progress bar). main.cpp says which step the start is at; the bar's
// share of each step is an estimate, except while shaders are built, where it is counted. Within
@@ -153,7 +154,8 @@ inline std::array<uint32_t, 24> State() {
// picture, set where it is drawn (vulkan_hud_draw.inc); the glyphs' words carry the state.
inline HudSnapshot MakeLoadingSnapshot(double seconds) {
HudSnapshot snapshot{};
snapshot.glyphs = Loading::State();
const auto state = Loading::State();
std::copy(state.begin(), state.end(), snapshot.glyphs.begin());
snapshot.width = 1920;
snapshot.x = 1920;
snapshot.y = 1080;
@@ -178,31 +180,38 @@ struct LoadingPace {
// the frames it adds do not go through OnFrameDisplayed(), which runs once per guest frame), and
// never reads above `refresh`, the output's own rate: a display shows no more than that, however
// many frames it is handed. refresh 0: not known, no limit.
inline std::array<char, 25> FormatHudText(const HudClock& clock, const HudClock& output,
double speed, const char* backend, double refresh = 0) {
std::array<char, 25> text{};
inline std::array<char, 29> FormatHudText(const HudClock& clock, const HudClock& output,
double speed, const char* backend, double refresh = 0,
int ping_ms = -1) {
std::array<char, 29> text{};
const size_t limit = ping_ms < 0 ? text.size() : text.size() - 6;
if (clock.fps < 0) {
std::snprintf(text.data(), text.size(), "%s F-- S-- W--", backend);
return text;
std::snprintf(text.data(), limit, "%s F-- S-- W--", backend);
} else {
const double shown = output.fps < 0 ? clock.fps : refresh > 0 ? std::min(output.fps, refresh) : output.fps;
if (shown < clock.fps + 1.5)
std::snprintf(text.data(), limit, "%s F%.0f S%.0f W%.0f", backend, clock.fps, speed, clock.worst_ms);
else
std::snprintf(text.data(), limit, "%s N%.0f F%.0f S%.0f W%.0f", backend, clock.fps, shown, speed,
clock.worst_ms);
}
if (ping_ms >= 0) {
const auto length = std::string_view{text.data()}.size();
std::snprintf(text.data() + length, text.size() - length, " P%d", std::min(ping_ms, 9999));
}
const double shown = output.fps < 0 ? clock.fps : refresh > 0 ? std::min(output.fps, refresh) : output.fps;
if (shown < clock.fps + 1.5)
std::snprintf(text.data(), text.size(), "%s F%.0f S%.0f W%.0f", backend, clock.fps, speed, clock.worst_ms);
else
std::snprintf(text.data(), text.size(), "%s N%.0f F%.0f S%.0f W%.0f", backend, clock.fps, shown, speed,
clock.worst_ms);
return text;
}
inline HudSnapshot MakeHudSnapshot(const HudClock& clock, const HudClock& output, double speed,
double refresh = 0) {
const auto text = FormatHudText(clock, output, speed, "VLK", refresh);
double refresh = 0, int ping_ms = -1) {
const auto text = FormatHudText(clock, output, speed, "VLK", refresh, ping_ms);
const std::string_view value{text.data()};
return {HudText(value), static_cast<uint32_t>(value.size() * 16 + 24)};
}
// The OpenGL renderer has no frame generation: what it presents is the guest's own rate, so both
// numbers come from one clock. Callers that cannot have an output clock use these.
inline std::array<char, 25> FormatHudText(const HudClock& clock, double speed, const char* backend) {
return FormatHudText(clock, clock, speed, backend);
inline std::array<char, 29> FormatHudText(const HudClock& clock, double speed, const char* backend,
int ping_ms = -1) {
return FormatHudText(clock, clock, speed, backend, 0, ping_ms);
}
inline HudSnapshot MakeHudSnapshot(const HudClock& clock, double speed) {
return MakeHudSnapshot(clock, clock, speed);
+23 -1
View File
@@ -63,6 +63,8 @@ file(WRITE "${MULTIPLAYER_OUTPUT}/packet.cpp.in" "${packet_source}")
configure_file("${MULTIPLAYER_OUTPUT}/packet.cpp.in" "${MULTIPLAYER_OUTPUT}/packet.cpp" COPYONLY)
file(READ "${MULTIPLAYER_SOURCE}/src/network/room_member.h" member_header)
multiplayer_replace(member_header " State GetState() const;"
" State GetState() const;\n int GetPingMilliseconds() const;")
multiplayer_replace(member_header "#include <functional>" "#include <functional>\n#include <stop_token>")
multiplayer_replace(member_header "const std::string& password = \"\", const std::string& token = \"\");"
"const std::string& password = \"\", const std::string& token = \"\",\n std::stop_token stop = {});")
@@ -70,8 +72,28 @@ file(WRITE "${MULTIPLAYER_OUTPUT}/network/room_member.h.in" "${member_header}")
configure_file("${MULTIPLAYER_OUTPUT}/network/room_member.h.in"
"${MULTIPLAYER_OUTPUT}/network/room_member.h" COPYONLY)
file(READ "${MULTIPLAYER_SOURCE}/src/network/room_member.cpp" member_source)
multiplayer_replace(member_source " ENetHost* client = nullptr;"
" std::atomic<int> ping_ms{-1};\n ENetHost* client = nullptr;")
multiplayer_replace(member_source " if (state != new_state) {"
" if (new_state != State::Joined && new_state != State::Moderator) ping_ms = -1;\n if (state != new_state) {")
multiplayer_replace(member_source " enet_host_flush(client);"
[=[ // Read ENet's measurement only on the thread that owns the peer.
if (server && (state == State::Joined || state == State::Moderator))
ping_ms = static_cast<int>(std::min(server->roundTripTime, enet_uint32{9999}));
enet_host_flush(client);]=])
multiplayer_replace(member_source " Disconnect();"
" ping_ms = -1;\n Disconnect();")
string(APPEND member_source [=[
namespace Network {
int RoomMember::GetPingMilliseconds() const {
const auto state = GetState();
return state == State::Joined || state == State::Moderator ?
room_member_impl->ping_ms.load() : -1;
}
}
]=])
multiplayer_replace(member_source "#include <atomic>"
"#include <atomic>\n#include <chrono>\n#include <cerrno>\n#include <cstdio>\n#include \"diagnostics.h\"\n#include \"multiplayer_validation.h\"\n#include \"multiplayer_send.h\"")
"#include <atomic>\n#include <algorithm>\n#include <chrono>\n#include <cerrno>\n#include <cstdio>\n#include \"diagnostics.h\"\n#include \"multiplayer_validation.h\"\n#include \"multiplayer_send.h\"")
multiplayer_replace(member_source [=[ case ENET_EVENT_TYPE_RECEIVE:
switch (event.packet->data[0]) {]=] [=[ case ENET_EVENT_TYPE_RECEIVE:
if (!Eden::Multiplayer::ValidRoomPacket(
+4
View File
@@ -332,6 +332,8 @@ void CheckController(u16 port) {
assert(snapshot.room == "Controller test" && snapshot.members == std::vector<std::string>{"PlayerOne"});
auto member = Network::GetRoomMember().lock();
assert(member);
Wait([&] { return member->GetPingMilliseconds() >= 0; });
assert(member->GetPingMilliseconds() <= 9999);
const auto previous_interface = Settings::values.network_interface.GetValue();
Settings::values.network_interface.SetValue("missing-host-adapter-for-room-test");
const auto room_interface = Network::GetSelectedNetworkInterface();
@@ -402,6 +404,7 @@ void CheckController(u16 port) {
client.Leave();
Wait([&] { return client.GetSnapshot().phase == Phase::Idle; });
assert(!Network::GetSelectedNetworkInterface());
assert(member->GetPingMilliseconds() == -1);
Settings::values.network_interface.SetValue(previous_interface);
assert(client.GetSnapshot().members.empty());
assert(client.BeginGame());
@@ -414,6 +417,7 @@ void CheckController(u16 port) {
Wait([&] { return client.GetSnapshot().phase == Phase::Failed; });
assert(client.GetSnapshot().error == "The connection to the room was lost.");
assert(client.GameConnectionLost() && guest_socket_mode == 1);
assert(member->GetPingMilliseconds() == -1);
assert(!client.Connect(connection));
client.EndGame();
connection.host = "127.0.0.1";
+8 -5
View File
@@ -1,27 +1,30 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#version 450
#extension GL_GOOGLE_include_directive : enable
layout(push_constant) uniform Text { uint glyphs[24]; uint width; uint x; uint y; uint loading; } text;
layout(push_constant) uniform Text { uint glyphs[28]; uint width; uint x; uint y; uint loading; } text;
layout(location = 0) out vec4 color;
#include "loading_text.glsl"
#include "loading_scene.glsl"
void main() {
// Vulkan push-constant arrays require dynamically uniform indices. Load
// each word at a constant index before the per-pixel local-array lookup.
uint glyphs[24] = uint[24](
uint glyphs[28] = uint[28](
text.glyphs[0], text.glyphs[1], text.glyphs[2], text.glyphs[3],
text.glyphs[4], text.glyphs[5], text.glyphs[6], text.glyphs[7],
text.glyphs[8], text.glyphs[9], text.glyphs[10], text.glyphs[11],
text.glyphs[12], text.glyphs[13], text.glyphs[14], text.glyphs[15],
text.glyphs[16], text.glyphs[17], text.glyphs[18], text.glyphs[19],
text.glyphs[20], text.glyphs[21], text.glyphs[22], text.glyphs[23]);
text.glyphs[20], text.glyphs[21], text.glyphs[22], text.glyphs[23],
text.glyphs[24], text.glyphs[25], text.glyphs[26], text.glyphs[27]);
if (text.loading != 0u) {
// The loading screen: x and y carry the picture's size, loading the milliseconds since
// it began (plus one), the glyphs' words what the start is doing (hud.h). Vulkan counts
// rows from the top.
vec2 size = vec2(float(text.x), float(text.y));
uint state[24];
for (int i = 0; i < 24; ++i) state[i] = glyphs[i];
color = vec4(loading_scene(vec2(gl_FragCoord.x, size.y - gl_FragCoord.y), size,
float(text.loading - 1u) * 0.001, glyphs), 1.0);
float(text.loading - 1u) * 0.001, state), 1.0);
return;
}
// The overlay: x and y are where its text starts, width the picture's height. The text is
@@ -33,7 +36,7 @@ void main() {
ivec2 cell = ivec2(vec2(p) / cell_size);
int i = cell.x / 4;
int x = cell.x % 4;
if (i < 24 && x < 3 && cell.y < 5)
if (i < 28 && x < 3 && cell.y < 5)
ink = ((glyphs[i] >> uint((4 - cell.y) * 3 + 2 - x)) & 1u) != 0u;
}
color = ink ? vec4(0.9, 0.95, 1.0, 1.0) : vec4(0.025, 0.04, 0.075, 0.5);
+17 -2
View File
@@ -77,8 +77,23 @@ int main() {
assert(Eden::MakeHudSnapshot(clock, 0).glyphs == Eden::HudText("VLK F-- S-- W--"));
clock.fps = 1000000;
clock.worst_ms = 1000000;
assert(Eden::MakeHudSnapshot(clock, 1000000).width <= 24 * 16 + 24);
static_assert(sizeof(Eden::HudSnapshot) == 112);
assert(Eden::MakeHudSnapshot(clock, 1000000).width <= 28 * 16 + 24);
static_assert(sizeof(Eden::HudSnapshot) == 128);
clock.fps = 60;
clock.worst_ms = 17;
for (const char* backend : {"OGL", "VLK"}) {
const auto online = Eden::FormatHudText(clock, 100, backend, 42);
assert(std::string_view(online.data()).ends_with(" W17 P42"));
assert(std::string_view(Eden::FormatHudText(clock, 100, backend, 0).data()).ends_with(" P0"));
assert(std::string_view(Eden::FormatHudText(clock, 100, backend, -1).data()).ends_with(" W17"));
}
const auto online = Eden::MakeHudSnapshot(guest, shown, 100, 119.88, 9999);
assert(online.glyphs == Eden::HudText("VLK N60 F120 S100 W17 P9999"));
assert(online.width == std::string_view("VLK N60 F120 S100 W17 P9999").size() * 16 + 24);
clock.fps = clock.worst_ms = 1000000;
const auto bounded = Eden::FormatHudText(clock, 1000000, "VLK", 1000000);
assert(std::string_view(bounded.data()).size() <= 28);
assert(std::string_view(bounded.data()).ends_with(" P9999"));
// The loading scene is drawn from the time alone: milliseconds since it began, plus one.
assert(Eden::MakeLoadingSnapshot(0).loading == 1 && Eden::MakeLoadingSnapshot(0).width != 0);
assert(Eden::MakeLoadingSnapshot(2.5).loading == 2501);