mirror of
https://github.com/Swordpdf/ProsperoEden.git
synced 2026-10-18 19:59:46 -05:00
Show room server ping in the game overlay
This commit is contained in:
1 parent
df75b71568
commit
8123fe3e86
8 files changed
+111
-31
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user