#1 - quicr module

This commit is contained in:
Martin Slachta
2026-07-22 17:34:44 +02:00
parent a04f0dc262
commit e6dd954ded
149 changed files with 3997 additions and 2126 deletions
+4
View File
@@ -10,6 +10,7 @@ file(GLOB FILES
src/draw/*.cpp
src/draw/RenderPasses/*.cpp
src/debug/*.cpp
src/debug/metrics/*.cpp
src/debug/tools/*.cpp
)
@@ -20,11 +21,14 @@ target_link_libraries(${PROJECT_NAME}
PUBLIC
towards
tw::network
tw::metrics
tw::quicr
loft::common
loft::base
loft_window
loft::render_graph
tw::protocol
tw::message_protocol
tw::serialization
tw::gui
imgui::imgui
@@ -0,0 +1,159 @@
#pragma once
#include "metrics/MetricSeries.hpp"
#include <chrono>
#include <cstdint>
namespace tw::dbg {
/**
* Per-second history of what the client sends, receives and waits for.
*
* Traffic arrives as running totals, one reading per tick: sample() keeps the
* change since the previous reading, so a bucket sums to the traffic of that
* second and its extremes are the quietest and busiest tick within it.
* Durations are recorded as they are measured.
*/
class NetworkMetrics {
public:
using Interval = std::chrono::seconds;
using Series = metrics::MetricSeries<Interval>;
/** How many seconds of history are kept. */
static constexpr size_t DEFAULT_HISTORY = 300;
/** Running totals as of one tick. */
struct Totals {
uint64_t bytes_sent = 0;
uint64_t bytes_received = 0;
uint64_t messages_sent = 0;
uint64_t messages_received = 0;
};
private:
Series m_bytes_out;
Series m_bytes_in;
Series m_messages_out;
Series m_messages_in;
Series m_response_ms;
Series m_update_ms;
Series m_rollbacks;
Series m_correction_distance;
Series m_ack_lag_frames;
Series m_replayed_frames;
Totals m_previous;
bool m_has_previous = false;
static uint64_t delta(uint64_t current, uint64_t previous) {
return current > previous ? current - previous : 0;
}
static double to_millis(std::chrono::nanoseconds elapsed) {
return std::chrono::duration<double, std::milli>(elapsed).count();
}
public:
explicit NetworkMetrics(size_t history_in_seconds = DEFAULT_HISTORY) :
m_bytes_out(history_in_seconds),
m_bytes_in(history_in_seconds),
m_messages_out(history_in_seconds),
m_messages_in(history_in_seconds),
m_response_ms(history_in_seconds),
m_update_ms(history_in_seconds),
m_rollbacks(history_in_seconds),
m_correction_distance(history_in_seconds),
m_ack_lag_frames(history_in_seconds),
m_replayed_frames(history_in_seconds)
{ }
/**
* Records how much `totals` grew since the previous call. The first call
* only remembers where the counters started.
*/
void sample(const Totals& totals) {
if(m_has_previous) {
m_bytes_out.push((double)delta(totals.bytes_sent, m_previous.bytes_sent));
m_bytes_in.push((double)delta(totals.bytes_received, m_previous.bytes_received));
m_messages_out.push((double)delta(totals.messages_sent, m_previous.messages_sent));
m_messages_in.push((double)delta(totals.messages_received, m_previous.messages_received));
}
m_previous = totals;
m_has_previous = true;
}
/** Time between sending an input and seeing the answer to it. */
void record_response_time(std::chrono::nanoseconds elapsed) {
m_response_ms.push(to_millis(elapsed));
}
/** Time one tick spent moving messages in and out, handlers included. */
void record_update_time(std::chrono::nanoseconds elapsed) {
m_update_ms.push(to_millis(elapsed));
}
const Series& bytes_out() const {
return m_bytes_out;
}
const Series& bytes_in() const {
return m_bytes_in;
}
const Series& messages_out() const {
return m_messages_out;
}
const Series& messages_in() const {
return m_messages_in;
}
const Series& response_ms() const {
return m_response_ms;
}
const Series& update_ms() const {
return m_update_ms;
}
/** Records a rollback event (one sample per rollback). */
void record_rollback() {
m_rollbacks.push(1.0);
}
/** Records the distance in meters of a position correction. */
void record_correction_distance(double meters) {
m_correction_distance.push(meters);
}
/** Records how many frames behind the ack is trailing the current frame. */
void record_ack_lag(uint32_t frames) {
m_ack_lag_frames.push((double)frames);
}
/** Records how many frames were replayed during a rollback. */
void record_replayed_frames(uint32_t frames) {
m_replayed_frames.push((double)frames);
}
const Series& rollbacks() const {
return m_rollbacks;
}
const Series& correction_distance() const {
return m_correction_distance;
}
const Series& ack_lag_frames() const {
return m_ack_lag_frames;
}
const Series& replayed_frames() const {
return m_replayed_frames;
}
};
}
+88 -74
View File
@@ -2,95 +2,109 @@
#include <imgui.h>
#include <implot.h>
#include <implot_internal.h>
#include "metrics/BucketMetric.hpp"
#include "metrics/MetricSeries.hpp"
#include <algorithm>
#include <chrono>
#include <string>
#include <vector>
namespace tw::dbg::tools {
template<typename T, typename Interval, typename Op = net::SumOp<T>>
/**
* Plots one series against the seconds behind now, ending at the last second
* that has fully elapsed.
*
* The line follows whichever statistic the series is read with. Reading an
* average also shades the quietest and busiest value of each second behind it;
* a total has no such range to show, since its buckets already hold every value
* of that second added together.
*/
class MetricWidget {
std::string m_name;
net::BucketMetric<T, Interval, Op>& m_metric;
using Self = MetricWidget<T, Interval, Op>;
struct {
T constraint_from;
T constraint_to;
T from;
T to;
} y_axis;
bool m_is_scrolling = true;
public:
MetricWidget(
const std::string& name,
net::BucketMetric<T, Interval, Op>& metric
) :
m_name(name),
m_metric(metric)
{
y_axis = {
.constraint_from = 0,
.constraint_to = 500,
.from = 0,
.to = 250
};
using Series = metrics::MetricSeries<std::chrono::seconds>;
private:
std::string m_name;
std::string m_unit;
const Series* m_series;
metrics::MetricField m_field;
/**
* The second in progress is left out: it only holds the part of itself
* that has elapsed, so drawing it makes the newest point drop and climb
* back once a second.
*/
static constexpr size_t SKIP_IN_PROGRESS = 1;
std::vector<double> m_ages;
std::vector<double> m_values;
std::vector<double> m_lows;
std::vector<double> m_highs;
bool has_range() const {
return m_field == metrics::MetricField::Avg;
}
Self& set_x_axis_limits_contraints(T from, T to) {
ImPlot::SetupAxisLimits(ImAxis_X1, from, to);
return *this;
}
Self& set_y_axis_limits(double from, double to) {
return *this;
}
Self& enable_scrolling() {
m_is_scrolling = true;
}
Self& disable_scrolling() {
m_is_scrolling = true;
}
void draw() {
ImGui::PushID(m_name.c_str());
auto head = m_metric.get_head();
auto head_timeline = m_metric.get_head_timeline();
auto tail = m_metric.get_tail();
auto tail_timeline = m_metric.get_tail_timeline();
static float m_metric_history = 10.0f;
ImGui::Checkbox("Is Scrolling", &m_is_scrolling);
if(m_is_scrolling) {
ImGui::SliderFloat("History", &m_metric_history,1,30,"%.1f s");
/** Describes what is drawn, so the numbers always match the line. */
void draw_summary() const {
if(m_values.empty()) {
ImGui::TextUnformatted("no samples yet");
return;
}
ImGui::Text("Min: %i", m_metric.min());
ImGui::Text("Max: %i", m_metric.max());
auto [low, high] = std::minmax_element(m_values.begin(), m_values.end());
if(ImPlot::BeginPlot(m_name.c_str())) {
auto from = tail_timeline.empty() ? *(head_timeline.end() - 1) : *(tail_timeline.end() - 1);
double total = 0.0;
for(double value : m_values) {
total += value;
}
ImPlot::SetupAxes("Time", m_name.c_str(), ImPlotAxisFlags_None, ImPlotAxisFlags_None);
if(m_is_scrolling) {
ImPlot::SetupAxisLimits(ImAxis_X1, from - m_metric_history, from, ImGuiCond_Always);
ImGui::Text("min %.1f %s avg %.1f %s max %.1f %s",
*low, m_unit.c_str(),
total / (double)m_values.size(), m_unit.c_str(),
*high, m_unit.c_str());
}
public:
MetricWidget(std::string name, std::string unit, const Series& series, metrics::MetricField field) :
m_name(std::move(name)),
m_unit(std::move(unit)),
m_series(&series),
m_field(field)
{ }
/** Draws the last `history_in_seconds` seconds of the series. */
void draw(size_t history_in_seconds) {
ImGui::PushID(m_name.c_str());
m_series->linearize(m_ages, m_values, m_field, history_in_seconds, SKIP_IN_PROGRESS);
if(has_range()) {
m_series->linearize(m_ages, m_lows, metrics::MetricField::Min,
history_in_seconds, SKIP_IN_PROGRESS);
m_series->linearize(m_ages, m_highs, metrics::MetricField::Max,
history_in_seconds, SKIP_IN_PROGRESS);
}
draw_summary();
if(ImPlot::BeginPlot(m_name.c_str(), ImVec2(-1.0f, 150.0f))) {
ImPlot::SetupAxes("seconds ago", m_unit.c_str(),
ImPlotAxisFlags_None, ImPlotAxisFlags_AutoFit);
ImPlot::SetupAxisLimits(ImAxis_X1, -(double)history_in_seconds, 0.0, ImGuiCond_Always);
const int count = (int)m_values.size();
if(has_range() && count > 0) {
ImPlot::PlotShaded("range", m_ages.data(), m_lows.data(), m_highs.data(), count);
}
ImPlot::SetupAxisLimits(ImAxis_Y1, 0, m_metric.max() * 2, ImGuiCond_Always);
// ImPlot::SetupAxisLimitsConstraints(ImAxis_Y1, 0, 10000);
if(count > 0) {
ImPlot::PlotLine(m_name.c_str(), m_ages.data(), m_values.data(), count);
}
ImPlot::PlotLine(m_name.c_str(), head_timeline.data(), head.data(), head.size());
ImPlot::PlotLine(m_name.c_str(), tail_timeline.data(), tail.data(), tail.size());
ImPlot::EndPlot();
}
@@ -1,59 +1,68 @@
#pragma once
#include "metrics/BucketMetric.hpp"
#include "metrics/NetworkStatsLogger.hpp"
#include "debug/metrics/NetworkMetrics.hpp"
#include "debug/tools/MetricWidget.hpp"
#include <implot.h>
#include <implot_internal.h>
#include <imgui.h>
namespace tw::dbg::tools {
/**
* Panel over everything the client measured about its traffic.
*
* Traffic is shown as the total of each second, since that is the rate the
* connection actually carried. Durations are shown as the average of each
* second, with the range behind them.
*/
class NetworkStatsGui {
private:
// MetricWidget<uint32_t, std::chrono::seconds, net::AverageOp<uint32_t>> m_ping_widget;
// MetricWidget<uint32_t, std::chrono::seconds> m_outgoing_widget;
// MetricWidget<uint32_t, std::chrono::seconds> m_incoming_widget;
MetricWidget m_response;
MetricWidget m_update;
MetricWidget m_bytes_in;
MetricWidget m_bytes_out;
MetricWidget m_messages_in;
MetricWidget m_messages_out;
MetricWidget m_rollbacks;
MetricWidget m_correction_distance;
MetricWidget m_ack_lag_frames;
MetricWidget m_replayed_frames;
int m_history_in_seconds = 30;
public:
// NetworkStatsGui() :
// m_ping_widget("Ping", net::NetworkStatsLogger::instance()->ping()),
// m_outgoing_widget("Outgoing", net::NetworkStatsLogger::instance()->outgoing()),
// m_incoming_widget("Incoming", net::NetworkStatsLogger::instance()->incoming())
// {
// }
explicit NetworkStatsGui(const NetworkMetrics& metrics) :
m_response("Response", "ms", metrics.response_ms(), metrics::MetricField::Avg),
m_update("Network update", "ms", metrics.update_ms(), metrics::MetricField::Avg),
m_bytes_in("Bytes in", "B/s", metrics.bytes_in(), metrics::MetricField::Sum),
m_bytes_out("Bytes out", "B/s", metrics.bytes_out(), metrics::MetricField::Sum),
m_messages_in("Messages in", "1/s", metrics.messages_in(), metrics::MetricField::Sum),
m_messages_out("Messages out", "1/s", metrics.messages_out(), metrics::MetricField::Sum),
m_rollbacks("Rollbacks", "1/s", metrics.rollbacks(), metrics::MetricField::Sum),
m_correction_distance("Correction distance", "m", metrics.correction_distance(), metrics::MetricField::Avg),
m_ack_lag_frames("Ack lag", "frames", metrics.ack_lag_frames(), metrics::MetricField::Avg),
m_replayed_frames("Replayed frames", "frames", metrics.replayed_frames(), metrics::MetricField::Avg)
{ }
void draw() {
// auto* instance = net::NetworkStatsLogger::instance();
// auto& ping = instance->ping();
ImGui::Begin("Network Stats");
/* auto head = ping.get_head();
auto head_timeline = ping.get_head_timeline();
ImGui::SliderInt("History", &m_history_in_seconds, 5, 300, "%d s");
auto tail = ping.get_tail();
auto tail_timeline = ping.get_tail_timeline();
const size_t history = (size_t)m_history_in_seconds;
static float ping_history = 10.0f;
ImGui::SliderFloat("Ping History", &ping_history,1,30,"%.1f s");
m_response.draw(history);
m_update.draw(history);
m_bytes_in.draw(history);
m_bytes_out.draw(history);
m_messages_in.draw(history);
m_messages_out.draw(history);
if(ImPlot::BeginPlot("Ping")) {
auto from = tail_timeline.empty() ? *(head_timeline.end() - 1) : *(tail_timeline.end() - 1);
ImPlot::SetupAxes("FrameIdx","FPS", ImPlotAxisFlags_None, ImPlotAxisFlags_None);
ImPlot::SetupAxisLimits(ImAxis_X1, from - ping_history, from, ImGuiCond_Always);
ImPlot::SetupAxisLimits(ImAxis_Y1, 0, 120);
ImPlot::SetupAxisLimitsConstraints(ImAxis_Y1, 0, 10000);
ImPlot::PlotLine("Ping", head_timeline.data(), head.data(), head.size());
ImPlot::PlotLine("Ping", tail_timeline.data(), tail.data(), tail.size());
ImPlot::EndPlot();
} */
// m_ping_widget.draw();
// m_outgoing_widget.draw();
// m_incoming_widget.draw();
ImGui::Separator();
ImGui::TextUnformatted("Prediction");
m_rollbacks.draw(history);
m_correction_distance.draw(history);
m_ack_lag_frames.draw(history);
m_replayed_frames.draw(history);
ImGui::End();
}
@@ -1,57 +0,0 @@
#pragma once
#include <format>
#include <vector>
#include "imgui.h"
#include "metrics/NetworkStatsLogger.hpp"
namespace tw::dbg::tools {
class PacketBacklogGui {
private:
std::vector<uint32_t> m_buckets;
uint32_t m_last_backlog_idx;
public:
void draw() {
// auto* instance = net::NetworkStatsLogger::instance();
// size_t size = instance->get_size();
// if(ImGui::BeginTable("Network Packets", 5)) {
// ImGui::TableSetupColumn("Message Type");
// ImGui::TableSetupColumn("Time");
// ImGui::TableSetupColumn("Is From Us");
// ImGui::TableSetupColumn("Target");
// ImGui::TableSetupColumn("Size");
// for(int32_t i = size-1; i >= 0; i--) {
// auto& item = instance->get_item(i);
// ImGui::PushID(item.timepoint.time_since_epoch().count());
// ImGui::TableNextRow();
// ImGui::TableNextColumn();
// ImGui::Text("%i", item.message_type);
// ImGui::TableNextColumn();
// ImGui::Text(std::format("{}", item.timepoint.time_since_epoch()).c_str());
// ImGui::TableNextColumn();
// ImGui::Checkbox("is_sent_from_us", &item.is_sent_by_us);
// ImGui::TableNextColumn();
// ImGui::Text(item.target.to_string().c_str());
// ImGui::TableNextColumn();
// ImGui::Text("%ld", item.buffer.size());
// ImGui::PopID();
// }
// ImGui::EndTable();
// }
}
};
}
@@ -2,7 +2,6 @@
#include "Address.hpp"
#include "ByteBuffer.hpp"
#include "packets/Packet.hpp"
#include <cstring>
namespace tw::dbg::tools {
@@ -0,0 +1,122 @@
#include "PlayerReconciler.hpp"
#include "world/JoltPhysicsWorld.hpp"
#include "world/CharacterBody.hpp"
#include "world/Transform.hpp"
#include <spdlog/spdlog.h>
#include <glm/glm.hpp>
#include <glm/gtx/norm.hpp>
namespace tw::net {
PlayerReconciler::PlayerReconciler(JoltPhysicsWorld* physics)
: m_physics(physics), m_last_reconciled_ack(0), m_rollback_count(0),
m_last_correction_distance(0.0f), m_last_replayed_frames(0), m_last_ack_frame(0)
{
for (auto& record : m_records) {
record.frame = 0;
record.valid = false;
record.input = glm::vec3(0.0f);
record.predicted_position = glm::vec3(0.0f);
}
}
void PlayerReconciler::record_input(uint32_t frame, glm::vec3 input) {
size_t idx = frame % RING_SIZE;
m_records[idx].frame = frame;
m_records[idx].valid = true;
m_records[idx].input = input;
}
void PlayerReconciler::record_prediction(uint32_t frame, glm::vec3 position) {
size_t idx = frame % RING_SIZE;
if (m_records[idx].frame == frame && m_records[idx].valid) {
m_records[idx].predicted_position = position;
}
}
bool PlayerReconciler::reconcile(uint32_t ack_frame, glm::vec3 authoritative_position,
entt::entity player, entt::registry* registry,
uint32_t current_frame)
{
if (ack_frame == 0 || ack_frame <= m_last_reconciled_ack || ack_frame >= current_frame) {
return false;
}
m_last_reconciled_ack = ack_frame;
m_last_ack_frame = ack_frame;
Record& record = m_records[ack_frame % RING_SIZE];
const bool has_prediction = record.valid && record.frame == ack_frame;
// With a prediction to compare against, an answer that already matches costs
// nothing further. This is the case almost every frame.
if (has_prediction) {
float distance = glm::distance(record.predicted_position, authoritative_position);
m_last_correction_distance = distance;
if (distance < kPositionEpsilon) {
return false;
}
}
// Restoring the frame the answer describes keeps everything the simulation
// derived from it, so only the character has to be moved. Without a stored
// frame there is nothing to restore and the answer is taken as it stands.
const bool restored = has_prediction && m_physics->rollback(ack_frame);
place_character(player, registry, authoritative_position, !restored);
replay_from(ack_frame, player, registry, current_frame);
m_last_replayed_frames = current_frame - 1 - ack_frame;
m_rollback_count++;
spdlog::debug("Corrected at frame {}: distance {}, replayed {}, restored {}",
ack_frame, m_last_correction_distance, m_last_replayed_frames, restored);
return true;
}
void PlayerReconciler::place_character(entt::entity player, entt::registry* registry,
glm::vec3 position, bool clear_velocity) {
CharacterBody* body = registry->try_get<CharacterBody>(player);
if (!body) {
return;
}
body->m_character->SetPosition(JPH::RVec3(position.x, position.y, position.z));
if (clear_velocity) {
body->m_character->SetLinearVelocity(JPH::Vec3::sZero());
body->m_desired_velocity = JPH::Vec3::sZero();
}
}
void PlayerReconciler::replay_from(uint32_t from_frame, entt::entity player,
entt::registry* registry, uint32_t current_frame) {
for (uint32_t f = from_frame + 1; f < current_frame; ++f) {
m_physics->step(f, tw::JoltPhysicsWorld::FIXED_DELTA_TIME, true);
Transform* transform = registry->try_get<Transform>(player);
if (!transform) {
continue;
}
// Frames the ring never saw still need an entry, or the answer to them
// arrives with nothing to compare against and forces another correction.
Record& replayed = m_records[f % RING_SIZE];
replayed.frame = f;
replayed.valid = true;
replayed.predicted_position = transform->position();
}
}
void PlayerReconciler::reset_at(uint32_t frame) {
m_last_reconciled_ack = frame;
for (auto& record : m_records) {
record.valid = false;
}
}
}
@@ -0,0 +1,72 @@
#pragma once
#include <cstdint>
#include <array>
#include <glm/glm.hpp>
#include <entt/entt.hpp>
namespace tw {
class JoltPhysicsWorld;
}
namespace tw::net {
class PlayerReconciler {
private:
struct Record {
uint32_t frame;
bool valid;
glm::vec3 input;
glm::vec3 predicted_position;
};
static constexpr size_t RING_SIZE = 64;
static constexpr float kPositionEpsilon = 0.05f;
tw::JoltPhysicsWorld* m_physics;
std::array<Record, RING_SIZE> m_records;
uint32_t m_last_reconciled_ack = 0;
uint64_t m_rollback_count = 0;
float m_last_correction_distance = 0.0f;
uint32_t m_last_replayed_frames = 0;
uint32_t m_last_ack_frame = 0;
public:
PlayerReconciler(tw::JoltPhysicsWorld* physics);
void record_input(uint32_t frame, glm::vec3 input);
void record_prediction(uint32_t frame, glm::vec3 position);
bool reconcile(uint32_t ack_frame, glm::vec3 authoritative_position,
entt::entity player, entt::registry* registry, uint32_t current_frame);
private:
/**
* Re-simulates `from_frame + 1` up to the newest frame, refreshing the stored
* prediction for each. The inputs come from the character itself, so this
* works even for frames this ring never recorded.
*/
void replay_from(uint32_t from_frame, entt::entity player,
entt::registry* registry, uint32_t current_frame);
/** Places the character at `position` without disturbing the stored frames. */
void place_character(entt::entity player, entt::registry* registry,
glm::vec3 position, bool clear_velocity);
public:
/**
* Drops every stored frame and treats `frame` as already answered. Used when
* the player is placed outright, where nothing recorded before the placement
* describes where it now is.
*/
void reset_at(uint32_t frame);
uint64_t rollback_count() const { return m_rollback_count; }
float last_correction_distance() const { return m_last_correction_distance; }
uint32_t last_replayed_frames() const { return m_last_replayed_frames; }
uint32_t last_ack_frame() const { return m_last_ack_frame; }
};
}
+3 -5
View File
@@ -3,7 +3,6 @@
#include "Address.hpp"
#include "SDLWindow.h"
#include "debug/tools/NetworkStatsGui.hpp"
#include "debug/tools/PacketBacklogGui.hpp"
#include "entt/entity/fwd.hpp"
#include "io/InputState.hpp"
#include "debug/tools/EntityManagerGui.hpp"
@@ -52,7 +51,8 @@ Runtime::Runtime(int argc, char** argv) :
m_physics_world(&m_world),
m_world_renderer("towards", m_window.get(), &m_world, &m_files),
m_input_manager(m_window.get()),
m_world_controller(&m_input_manager, &m_world, &m_physics_world, &m_world_renderer, { "127.0.0.1", get_port_from_args(argc, argv) }),
m_network_metrics(),
m_world_controller(&m_input_manager, &m_world, &m_physics_world, &m_world_renderer, { "127.0.0.1", get_port_from_args(argc, argv) }, &m_network_metrics),
m_lockstep(60)
{
}
@@ -95,8 +95,7 @@ Runtime::Runtime(int argc, char** argv) :
void Runtime::run() {
dbg::tools::EntityManagerGui entity_manager(&m_world);
dbg::tools::PerformanceStatsGui perf_stats(m_lockstep);
dbg::tools::PacketBacklogGui packet_backlog;
dbg::tools::NetworkStatsGui network_stats;
dbg::tools::NetworkStatsGui network_stats(m_network_metrics);
ImPlot::CreateContext();
m_is_running = true;
@@ -147,7 +146,6 @@ void Runtime::run() {
m_world_controller.update(m_lockstep.delta_time());
perf_stats.draw();
packet_backlog.draw();
network_stats.draw();
tw::dbg::ComponentGui<tw::io::InputManager>().draw(&m_input_manager);
+3
View File
@@ -1,5 +1,6 @@
#pragma once
#include "debug/metrics/NetworkMetrics.hpp"
#include "draw/WorldRenderer.hpp"
#include "io/InputState.hpp"
#include "runtime/LockStep.hpp"
@@ -28,6 +29,8 @@ private:
tw::io::InputManager m_input_manager;
tw::dbg::NetworkMetrics m_network_metrics;
tw::ClientWorldController m_world_controller;
tw::LockStep m_lockstep;
+1 -2
View File
@@ -14,8 +14,7 @@ struct CameraData {
CameraData(glm::mat4 projection, Transform view) :
projection(projection),
view(view)
{
}
{ }
};
class Camera {
@@ -12,9 +12,7 @@
#include "PlayerMove.pb.h"
#include "entt/entity/entity.hpp"
#include "messenger/MessageHandler.hpp"
#include "messenger/Messenger.hpp"
#include "TcpStream.hpp"
#include "entt/entity/fwd.hpp"
#include "messages/PlayerMoveMessage.hpp"
#include "metrics/HistoryBuffer.hpp"
#include "world/CharacterBody.hpp"
@@ -22,6 +20,7 @@
#include "world/JoltPhysicsWorld.hpp"
#include "world/WorldEntity.hpp"
#include "tw/serial/WorldStateWriter.hpp"
#include "network/EntityInterpolation.hpp"
namespace tw {
@@ -83,16 +82,26 @@ ClientWorldController::create_entity(const std::string& name, glm::vec3 position
return entity;
}
net::TcpStream create_stream(tw::net::Address address) {
auto stream = net::TcpStream::connect(address);
if(!stream.has_value()) {
spdlog::error("Failed to connect to server");
static std::unique_ptr<msg::MessageEndpoint> create_endpoint() {
auto endpoint_r = msg::MessageEndpoint::create();
if(!endpoint_r) {
spdlog::error("Failed to create the endpoint: {}", endpoint_r.error().message());
throw std::runtime_error("Failed to create the endpoint");
}
return std::move(endpoint_r.value());
}
static msg::MessageConnection* connect_to_server(msg::MessageEndpoint* endpoint, net::Address address) {
auto server_r = endpoint->connect(address.ip_string(), address.port());
if(!server_r) {
spdlog::error("Failed to connect to server: {}", server_r.error().message());
throw std::runtime_error("Failed to connect to server");
}
stream.value().set_non_blocking();
spdlog::info("Connected to server at {}", address.to_string());
return std::move(stream.value());
return server_r.value();
}
std::optional<entt::entity> ClientWorldController::map_from_server_entity(int id) {
@@ -134,33 +143,49 @@ ClientWorldController::ClientWorldController(
World* world,
JoltPhysicsWorld* physics_world,
drw::WorldRenderer* world_renderer,
tw::net::Address address
tw::net::Address address,
dbg::NetworkMetrics* network_metrics
) :
m_input_manager(inputs),
m_world(world),
m_physics_world(physics_world),
m_world_renderer(world_renderer),
m_player_entity(create_player_entity(world, physics_world, world_renderer)),
// m_player_entity(/* create_player_entity(world, physics_world, world_renderer) */),
m_player_controller(&world_renderer->camera(), glm::vec3()),
m_messenger{address},
m_endpoint(create_endpoint()),
m_server(connect_to_server(m_endpoint.get(), address)),
m_messages(m_endpoint.get()),
m_network_metrics(network_metrics),
m_tick_step(20),
m_is_connected(false),
m_input_send_times(INPUT_SEND_TIME_COUNT),
m_position_history_exporter("/home/martin/output.csv"),
m_entity_interpolator(&m_world->registry(), m_player_entity, 300)
m_entity_interpolator(&m_world->registry(), (entt::entity)0, 300),
m_reconciler(physics_world)
{
m_messenger->set_handler<mmo::LoginResponse>(
[&](mmo::LoginResponse* mesg) {
m_messages.set_handler<mmo::LoginResponse>(
[this](msg::PeerId, const mmo::LoginResponse& mesg) {
if(!m_is_connected) {
spdlog::info("Joined the game!");
spdlog::info("Logged in!");
}
});
m_messenger->set_raw_handler(Message<mmo::WorldStateMessage>::value,
[&](std::span<std::byte> data) -> tl::expected<void, net::NetworkError> {
m_messages.set_handler<mmo::SetControlledEntity>(
[this](msg::PeerId, const mmo::SetControlledEntity& mesg) {
spdlog::info("Setting controlled entity from server id {}", mesg.entity_id());
m_controlled_server_id = mesg.entity_id();
try_bind_player_entity();
});
m_endpoint->set_handler(Message<mmo::WorldStateMessage>::value,
[this](msg::PeerId, std::span<const std::byte> data) {
serial::WorldStateReader reader(data);
auto header = reader.read_header();
measure_response_time(header.frame_idx);
while(reader.has_spawn()) {
auto spawn = reader.read_spawn();
auto entity = create_entity("test", glm::vec3());
@@ -168,7 +193,11 @@ ClientWorldController::ClientWorldController(
map_server_entity(spawn, entity);
m_entity_interpolator.register_entity(entity);
if(m_controlled_server_id.has_value() && m_controlled_server_id.value() == spawn) {
try_bind_player_entity();
} else {
m_entity_interpolator.register_entity(entity);
}
}
while(reader.has_entity()) {
@@ -182,7 +211,57 @@ ClientWorldController::ClientWorldController(
}
glm::vec3 p = {entity_r.position.x, entity_r.position.y, entity_r.position.z};
m_entity_interpolator.add_position_for_entity(entity.value(), p);
if(m_player_entity.has_value() && entity.value() == m_player_entity.value()) {
// The entity was created before its position was known, so the
// body sits at the origin until the server places it. There is
// no predicted history to reconcile against yet.
if(!m_player_position_initialized) {
m_player_position_initialized = true;
snap_player_to(entity.value(), p);
continue;
}
glm::vec3 position_before = glm::vec3(0.0f);
Transform* player_transform = m_world->registry().try_get<Transform>(entity.value());
if(player_transform) {
position_before = player_transform->position();
}
bool reconcile_happened = m_reconciler.reconcile(header.frame_idx, p, entity.value(), &m_world->registry(), m_frame_idx);
if(reconcile_happened) {
// Where the replay actually ended up, which is ahead of the
// acked position by the frames that were re-simulated.
glm::vec3 position_after = player_transform
? player_transform->position()
: p;
glm::vec3 correction_delta = position_before - position_after;
float correction_magnitude = glm::length(correction_delta);
if(correction_magnitude > 5.0f) {
correction_delta = glm::normalize(correction_delta) * 5.0f;
}
m_visual_error += correction_delta;
m_render_curr_position = position_after;
m_render_prev_position = position_after;
m_tick_accumulator = 0.0;
m_network_metrics->record_rollback();
m_network_metrics->record_correction_distance(m_reconciler.last_correction_distance());
m_network_metrics->record_replayed_frames(m_reconciler.last_replayed_frames());
}
if(header.frame_idx != 0) {
uint32_t ack_lag = 0;
if(m_frame_idx >= header.frame_idx) {
ack_lag = m_frame_idx - header.frame_idx;
}
m_network_metrics->record_ack_lag(ack_lag);
}
} else {
m_entity_interpolator.add_position_for_entity(entity.value(), p);
}
EntityPositionHistory* history = m_world->registry().try_get<EntityPositionHistory>(entity.value());
@@ -193,33 +272,111 @@ ClientWorldController::ClientWorldController(
}
// apply_entity_positions();
return {};
});
m_messenger->set_handler<mmo::EntitySpawnMessage>(
[&](mmo::EntitySpawnMessage* mesg) {
auto entity = create_entity(mesg->name(), glm::vec3());
m_messages.set_handler<mmo::EntitySpawnMessage>(
[this](msg::PeerId, const mmo::EntitySpawnMessage& mesg) {
auto entity = create_entity(mesg.name(), glm::vec3());
map_server_entity(mesg->entity_id(), entity);
map_server_entity(mesg.entity_id(), entity);
m_entity_interpolator.register_entity(entity);
if(m_controlled_server_id.has_value() && m_controlled_server_id.value() == mesg.entity_id()) {
try_bind_player_entity();
} else {
m_entity_interpolator.register_entity(entity);
}
});
}
ClientWorldController::~ClientWorldController() {
}
void ClientWorldController::try_bind_player_entity() {
if(!m_controlled_server_id.has_value()) {
return;
}
auto local_entity = map_from_server_entity(m_controlled_server_id.value());
if(!local_entity.has_value()) {
return;
}
if(m_player_entity.has_value() && m_player_entity.value() == local_entity.value()) {
return;
}
entt::entity entity = local_entity.value();
spdlog::info("Binding player entity");
m_player_entity = entity;
Transform* transform = m_world->registry().try_get<Transform>(entity);
glm::vec3 position = transform ? transform->position() : glm::vec3(0.0f);
m_world->registry().emplace<CharacterController>(entity, 20.0f);
m_world->registry().emplace<CharacterBody>(entity, m_physics_world->create_character(
new JPH::BoxShape(JPH::Vec3Arg(0.5f, 0.5f, 0.5f)),
position
));
if(m_world->registry().all_of<net::EntityPositionInterpolation>(entity)) {
m_world->registry().remove<net::EntityPositionInterpolation>(entity);
}
}
void ClientWorldController::snap_player_to(entt::entity entity, glm::vec3 position) {
CharacterBody* body = m_world->registry().try_get<CharacterBody>(entity);
if(body) {
body->m_character->SetPosition(JPH::RVec3(position.x, position.y, position.z));
body->m_character->SetLinearVelocity(JPH::Vec3::sZero());
body->m_desired_velocity = JPH::Vec3::sZero();
}
Transform* transform = m_world->registry().try_get<Transform>(entity);
if(transform) {
transform->set_position(position);
}
m_render_prev_position = position;
m_render_curr_position = position;
m_tick_accumulator = 0.0;
m_visual_error = glm::vec3(0.0f);
// Frames simulated before the player was placed describe a position it never
// actually had, so answers to them must not be reconciled against.
m_reconciler.reset_at(m_frame_idx);
}
void ClientWorldController::export_entity_history() {
}
void ClientWorldController::measure_response_time(uint32_t frame_idx) {
// Snapshots carry frame zero until the server has an input to answer, and
// repeat the same frame whenever no newer one arrived in between.
if(frame_idx == 0 || frame_idx <= m_last_measured_frame) {
return;
}
// Anything the send times no longer cover, including a frame we never sent,
// which underflows into a large distance.
if(m_frame_idx - frame_idx >= INPUT_SEND_TIME_COUNT) {
return;
}
m_last_measured_frame = frame_idx;
auto sent_at = m_input_send_times[frame_idx % INPUT_SEND_TIME_COUNT];
m_network_metrics->record_response_time(Clock::now() - sent_at);
}
void ClientWorldController::update(double delta_time) {
m_player_controller.update(m_input_manager, delta_time);
ImGui::Begin("Player Controller");
if(m_player_entity.has_value()) {
ImGui::Text("Player entity ID: %d", (uint32_t)m_player_entity.value());
}
ImGui::Text("Player entity ID: %d", (uint32_t)m_player_entity);
ImGui::Text("Player count: %ld", m_entity_mapping.size());
for(auto mapping : m_entity_mapping) {
ImGui::Text("%d -> %d", (uint32_t)mapping.first, mapping.second);
@@ -228,34 +385,57 @@ void ClientWorldController::update(double delta_time) {
ImGui::End();
if(m_tick_step.update()) {
m_messenger->update();
auto network_start = Clock::now();
m_endpoint->update();
m_network_metrics->record_update_time(Clock::now() - network_start);
m_network_metrics->sample({
.bytes_sent = m_endpoint->bytes_sent(),
.bytes_received = m_endpoint->bytes_received(),
.messages_sent = m_endpoint->messages_sent(),
.messages_received = m_endpoint->messages_received()
});
if(!m_is_connected && false) {
return;
} else {
glm::vec3 input = m_player_controller.input();
// CharacterController& character = m_world->registry().get<CharacterController>(m_player_entity);
// character.set_input(m_frame_idx, m_player_controller.input());
if(m_player_entity.has_value()) {
CharacterController* controller = m_world->registry().try_get<CharacterController>(m_player_entity.value());
if(controller) {
controller->set_input(m_frame_idx, input);
m_reconciler.record_input(m_frame_idx, input);
}
}
m_physics_world->step(m_frame_idx, JoltPhysicsWorld::FIXED_DELTA_TIME, true);
if(m_player_entity.has_value()) {
Transform* player_transform = m_world->registry().try_get<Transform>(m_player_entity.value());
if(player_transform) {
glm::vec3 true_position = player_transform->position();
m_reconciler.record_prediction(m_frame_idx, true_position);
m_render_prev_position = m_render_curr_position;
m_render_curr_position = true_position;
m_tick_accumulator = 0.0;
}
}
mmo::PlayerMoveMessage player_move_message = {};
player_move_message.set_frame_idx(m_frame_idx);
mmo::PlayerInput* player_input = new mmo::PlayerInput();
player_input->set_x(m_player_controller.input().x);
player_input->set_y(m_player_controller.input().y);
player_input->set_z(m_player_controller.input().z);
player_input->set_x(input.x);
player_input->set_y(input.y);
player_input->set_z(input.z);
player_move_message.set_allocated_input(player_input);
auto r = m_messenger->send(player_move_message);
auto r = m_messages.send(m_server, player_move_message, false);
// CharacterBody& ts = m_world->registry().get<CharacterBody>(m_player_entity);
// auto position = ts.m_character->GetPosition();
// character.position_history().set(m_frame_idx, glm::vec3(position[0], position[1], position[2]));
// EntityInterpolation& interpolation = m_world->registry().get<EntityInterpolation>(m_player_entity);
// interpolation.push(std::chrono::steady_clock::now(), glm::vec3(position[0], position[1], position[2]));
m_input_send_times[m_frame_idx % INPUT_SEND_TIME_COUNT] = Clock::now();
// m_player_controller.set_target(glm::vec3(position[0], position[1], position[2]));
//
export_entity_history();
m_frame_idx++;
@@ -268,9 +448,27 @@ void ClientWorldController::update(double delta_time) {
}
}
m_physics_world->step(m_frame_idx, delta_time);
m_entity_interpolator.update();
if(m_player_entity.has_value()) {
Transform* player_transform = m_world->registry().try_get<Transform>(m_player_entity.value());
if(player_transform) {
m_visual_error *= std::exp(-delta_time * kVisualErrorDecayRate);
if(glm::length(m_visual_error) < 0.001f) {
m_visual_error = glm::vec3(0.0f);
}
m_tick_accumulator += delta_time;
float alpha = glm::clamp(
static_cast<float>(m_tick_accumulator / JoltPhysicsWorld::FIXED_DELTA_TIME),
0.0f, 1.0f
);
glm::vec3 smoothed_position = glm::mix(m_render_prev_position, m_render_curr_position, alpha) + m_visual_error;
player_transform->set_position(smoothed_position);
m_player_controller.set_target(smoothed_position);
}
}
}
}
@@ -4,8 +4,9 @@
#include <glm/gtx/io.hpp>
#include "Address.hpp"
#include "TcpStream.hpp"
#include "messenger/MessageHandler.hpp"
#include "ProtobufMessages.hpp"
#include "debug/metrics/NetworkMetrics.hpp"
#include "message_protocol/MessageEndpoint.hpp"
#include "entt/entity/fwd.hpp"
#include "io/InputState.hpp"
#include "metrics/HistoryBufferExporter.hpp"
@@ -15,6 +16,7 @@
#include "draw/WorldRenderer.hpp"
#include "world/ThirdPersonPlayerController.hpp"
#include "network/EntityPositionInterpolator.hpp"
#include "network/PlayerReconciler.hpp"
namespace tw {
@@ -35,10 +37,15 @@ class ClientWorldController {
drw::WorldRenderer* m_world_renderer;
JoltPhysicsWorld* m_physics_world;
entt::entity m_player_entity;
std::optional<entt::entity> m_player_entity;
std::optional<uint32_t> m_controlled_server_id;
ThirdPersonPlayerController m_player_controller;
std::optional<tw::net::MessageHandler> m_messenger;
std::unique_ptr<msg::MessageEndpoint> m_endpoint;
msg::MessageConnection* m_server;
ProtobufMessages m_messages;
dbg::NetworkMetrics* m_network_metrics;
LockStep m_tick_step;
@@ -52,10 +59,59 @@ class ClientWorldController {
std::optional<drw::Mesh> m_mesh;
using Clock = std::chrono::steady_clock;
void try_bind_player_entity();
/**
* Places the player at an authoritative position outright, clearing the
* predicted state that led there. Used for the first position the server
* sends, which the local simulation has no history to reconcile against.
*/
void snap_player_to(entt::entity entity, glm::vec3 position);
/**
* Whether the server has placed the player at least once. Entities are
* created before their position arrives, so the body starts at the origin
* and has to be moved once the first position shows up.
*/
bool m_player_position_initialized = false;
/**
* When each input was sent, indexed by its frame. Holds the most recent
* INPUT_SEND_TIME_COUNT frames; an answer that takes longer than that goes
* unmeasured.
*/
static constexpr size_t INPUT_SEND_TIME_COUNT = 256;
std::vector<Clock::time_point> m_input_send_times;
uint32_t m_last_measured_frame = 0;
/**
* Records how long the answer to `frame_idx` took to arrive, ignoring
* frames that were already measured or are too old to still have a send
* time.
*/
void measure_response_time(uint32_t frame_idx);
HistoryBufferExporter<long, glm::vec3> m_position_history_exporter;
net::EntityPositionInterpolator m_entity_interpolator;
net::PlayerReconciler m_reconciler;
/**
* Visual smoothing for render-rate interpolation between 20 Hz ticks.
*/
glm::vec3 m_render_prev_position{0.0f};
glm::vec3 m_render_curr_position{0.0f};
double m_tick_accumulator = 0.0;
/**
* Visual error from reconciliation corrections, decays over time.
*/
glm::vec3 m_visual_error{0.0f};
static constexpr double kVisualErrorDecayRate = 12.0;
/**
* Mapping from the server entity_id to local entity_id
* Server might have the same entity under different name
@@ -65,8 +121,6 @@ class ClientWorldController {
entt::entity create_entity(const std::string& name, glm::vec3 position);
net::MessageHandler create_messenger();
std::optional<entt::entity> map_from_server_entity(int id);
void map_server_entity(int server_id, entt::entity local_id);
@@ -86,7 +140,8 @@ public:
World* world,
JoltPhysicsWorld* physics_world,
drw::WorldRenderer* world_renderer,
tw::net::Address address
tw::net::Address address,
dbg::NetworkMetrics* network_metrics
);
~ClientWorldController();