Log how far apart the host and a client move the client's player in a tick

The network test's per-second line (debug.network_test) now ends with
"| drift <ticks> <apart> <mean> <largest>": on the host, the ticks in which
it moved a client's player one tick on from where it took the client's
last prediction, with the client's input of that tick, as the client did,
and how far apart the two moves were. Builds that simulate alike move the
player alike to the last bit, so this is what shows a build that simulates
otherwise; the positions the machines log agree either way, as the netcode
corrects every copy toward the host's. tools/net_compare.py (next commit)
judges two builds by it.

Only counters and the test's log line change: nothing sent between
machines, nothing a game does with them, so the network version stays.
Checked with i686 builds against themselves (0 to 13% of such ticks apart,
up to 42% on a loaded computer) and against one whose players run 0.5% or
2% faster (77 to 93%).

(cherry picked from commit 95b2ddf86d4abc279c4ed462789e0ff7a75a7881)
This commit is contained in:
Paul Hontz authored and MrBruh committed 2026-10-20 00:21:59 +13:00
1 parent 03e28a245e
commit 8e9ee175a9
2 files changed
+95 -3

No files matched your search

+82
View File
@@ -256,6 +256,11 @@ enum
_distributed_unit_part_killer_bit,
};
/* world units: how far apart the host's run of a client's player and the
client's may be, a tick from the same place, and agree (distributed_divergence:
well past the last bits' rounding, well short of a player running 1% faster) */
#define DIVERGENCE_APART_DISTANCE 0.0001f
/* world units: how far a client's own player's unit may be from the host's
before the host takes it no longer, and before the client is put where the
host has it (no further than that: between the two they would disagree for
@@ -707,6 +712,35 @@ static struct
long received;
long corrections;
} distributed_statistics;
/* ... and (the host) how far apart a client's player moved in a tick on the
client and on the host, where the host took the client's last prediction
the tick before, that was of the client's tick before, and the host's tick
ran the player with the client's input of that tick: both machines then
moved the player one tick with the same input from much the same place, so
builds that simulate alike move it alike to the last bit (but where the
player touched something the two have in different places), and a build
that moves a player otherwise moves it otherwise, every tick the player
moves (tools/net_compare.py). The move, not where it ends: the host's
copy is put only halfway to a prediction a little off, and would be as far
off the next for ticks after. Since network_distributed_divergence last
read them: how many such ticks, how many of them further apart than
DIVERGENCE_APART_DISTANCE, and the mean and the largest distance. */
static struct
{
long samples;
long apart;
real total;
real largest;
/* the tick of the client's and of the host's last prediction taken */
long taken_time[MAXIMUM_TRACKED_PLAYERS];
long taken_host_time[MAXIMUM_TRACKED_PLAYERS];
/* where that prediction had the player, and where it left the host's */
real_point3d taken_position[MAXIMUM_TRACKED_PLAYERS];
real_point3d taken_host_position[MAXIMUM_TRACKED_PLAYERS];
/* the client's tick of the latest input the host has of each player
(what the host's tick ran it with) */
long input_time[MAXIMUM_TRACKED_PLAYERS];
} distributed_divergence;
/* ---------- shared (network_distributed.h) */
@@ -726,6 +760,22 @@ void distributed_count_correction(
distributed_statistics.corrections++;
}
void network_distributed_divergence(
long *samples,
long *apart,
real *mean,
real *largest)
{
*samples = distributed_divergence.samples;
*apart = distributed_divergence.apart;
*mean = distributed_divergence.samples ? distributed_divergence.total / (real)distributed_divergence.samples : 0.0f;
*largest = distributed_divergence.largest;
distributed_divergence.samples = 0;
distributed_divergence.apart = 0;
distributed_divergence.total = 0.0f;
distributed_divergence.largest = 0.0f;
}
boolean distributed_real_valid(
real value)
{
@@ -2131,6 +2181,28 @@ static void distributed_take_prediction(
dx = position.x - object->object.position.x;
dy = position.y - object->object.position.y;
dz = position.z - object->object.position.z;
/* (for the tests' reports: a tick on from the last taken, on both, with
the input the client's tick ran: an input's tick is the client's time
before it ran, a prediction's after) */
if (distributed_divergence.taken_host_time[player_index] == now - 1 &&
distributed_divergence.taken_time[player_index] == distributed_predictions[player_index].time - 1 &&
distributed_divergence.input_time[player_index] == distributed_predictions[player_index].time - 1)
{
real_point3d const *client_from = &distributed_divergence.taken_position[player_index];
real_point3d const *host_from = &distributed_divergence.taken_host_position[player_index];
real mx = (position.x - client_from->x) - (object->object.position.x - host_from->x);
real my = (position.y - client_from->y) - (object->object.position.y - host_from->y);
real mz = (position.z - client_from->z) - (object->object.position.z - host_from->z);
real distance = (real)sqrt(mx * mx + my * my + mz * mz);
distributed_divergence.samples++;
if (distance > DIVERGENCE_APART_DISTANCE)
distributed_divergence.apart++;
distributed_divergence.total += distance;
if (distance > distributed_divergence.largest)
distributed_divergence.largest = distance;
}
distributed_divergence.taken_host_time[player_index] = NONE;
if (!(dx * dx + dy * dy + dz * dz <= HOST_ACCEPT_TOLERANCE * HOST_ACCEPT_TOLERANCE))
return;
if (distributed_accepted[player_index].valid)
@@ -2169,6 +2241,10 @@ static void distributed_take_prediction(
{
distributed_predictions[player_index].taken_time = distributed_predictions[player_index].time;
distributed_predictions[player_index].taken_host_time = now;
distributed_divergence.taken_time[player_index] = distributed_predictions[player_index].time;
distributed_divergence.taken_host_time[player_index] = now;
distributed_divergence.taken_position[player_index] = position;
distributed_divergence.taken_host_position[player_index] = object->object.position;
/* (a new anchor: the first, one a second on, or after a jump) */
if (!distributed_accepted[player_index].valid ||
now - distributed_accepted[player_index].host_time >= PREDICTION_ANCHOR_TICKS)
@@ -2507,6 +2583,12 @@ static void distributed_handle_inputs(
network_coop_note_player_structure_bsp(input->player_index, input->structure_bsp_index);
update_server_handle_distributed_input(DATUM_INDEX_NEW(input->player_index, player->identifier), input->tick,
&action, input->control_flags, DISTRIBUTED_INPUT_HISTORY);
/* (for the tests' reports: the newest, as the input taken is) */
if (input->tick > distributed_divergence.input_time[input->player_index] ||
input->tick < distributed_divergence.input_time[input->player_index] - TICKS_PER_SECOND)
{
distributed_divergence.input_time[input->player_index] = input->tick;
}
if (input->host_time != NONE && (host_time == NONE || input->host_time > host_time))
host_time = input->host_time;
}
+13 -3
View File
@@ -23,7 +23,10 @@ Automated system link sessions for testing the netcode without the menus
plays one without the menus) is logged, as below.
Once the game runs, every second each machine logs where every player's
unit is, so the machines' views of the game can be compared.
unit is, so the machines' views of the game can be compared, and the host
how far its own run of each client's player came from the client's
(network_distributed_divergence): tools/net_compare.py compares two builds
so (port/linux/NETCODE.md, "Comparing two builds", has the line's format).
Scripted play for the netcode's parts the bots' wandering does not reach:
debug.network_test_kill (the host kills the last player every so often),
@@ -91,6 +94,7 @@ void platform_log(char const *format, ...);
void damage_kill_object_for_player(long object_index, long player_index);
/* network_distributed.c's */
void network_distributed_statistics(long *sent, long *received, long *corrections);
void network_distributed_divergence(long *samples, long *apart, real *mean, real *largest);
void network_distributed_item_statistics(long *creates, long *deletes, long *failures, long *removed);
void network_damage_statistics(long *sent_reports, long *dealt_reports, long *rejected_reports, long *replayed_events);
/* xinput_sdl.c's */
@@ -407,9 +411,14 @@ static void network_test_log_players(
spawns leaves it where it began) */
long local_player_index = local_player_get_player_index(0);
struct observer_result const *camera = observer_get_camera(0);
long divergence_samples, divergence_apart;
real divergence_mean, divergence_largest;
network_distributed_divergence(&divergence_samples, &divergence_apart, &divergence_mean, &divergence_largest);
platform_log("network test: tick %ld%s | items %ld (+%ld -%ld !%ld x%ld) | %s to %ld | sent %ld received %ld corrected %ld"
" | hits %ld dealt %ld rejected %ld replayed %ld | local %ld camera (%.1f %.1f %.1f) respawn %ld",
" | hits %ld dealt %ld rejected %ld replayed %ld | local %ld camera (%.1f %.1f %.1f) respawn %ld"
" | drift %ld %ld %.6f %.6f",
game_time_get(), line, ground_items, creates, deletes, failures, removed,
game_engine_can_score() ? "playing" : "game over",
game_engine_running() ? (long)game_engine_get_variant()->universal_variant.score_to_win : 0L,
@@ -417,7 +426,8 @@ static void network_test_log_players(
sent_reports, dealt_reports, rejected_reports, replayed_events,
local_player_index == NONE ? -1L : (long)DATUM_INDEX_TO_ABSOLUTE_INDEX(local_player_index),
camera ? camera->position.x : 0.0f, camera ? camera->position.y : 0.0f, camera ? camera->position.z : 0.0f,
local_player_index == NONE ? 0L : (long)player_get(local_player_index)->respawn_timer);
local_player_index == NONE ? 0L : (long)player_get(local_player_index)->respawn_timer,
divergence_samples, divergence_apart, divergence_mean, divergence_largest);
}
}