Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
n_clock_sync.c
Go to the documentation of this file.
1/*
2 * Nilorea Library
3 * Copyright (C) 2005-2026 Castagnier Mickael
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14 * implied. See the License for the specific language governing
15 * permissions and limitations under the License.
16 *
17 * SPDX-License-Identifier: Apache-2.0
18 */
19
29#include "nilorea/n_log.h"
30#include "nilorea/n_common.h"
31#include <string.h>
32#include <stdlib.h>
33
40static int cmp_double(const void* a, const void* b) {
41 double da = *(const double*)a;
42 double db = *(const double*)b;
43 if (da < db) return -1;
44 if (da > db) return 1;
45 return 0;
46}
47
49static double median_of(const double* samples, int count) {
50 if (count <= 0) return 0.0;
51
52 double tmp[N_CLOCK_SYNC_SAMPLE_COUNT];
53 int n = (count < N_CLOCK_SYNC_SAMPLE_COUNT) ? count : N_CLOCK_SYNC_SAMPLE_COUNT;
54 memcpy(tmp, samples, (size_t)n * sizeof(double));
55 qsort(tmp, (size_t)n, sizeof(double), cmp_double);
56
57 if (n % 2 == 1) {
58 return tmp[n / 2];
59 }
60 return (tmp[n / 2 - 1] + tmp[n / 2]) / 2.0;
61}
62
64 N_CLOCK_SYNC* cs = NULL;
65 Malloc(cs, N_CLOCK_SYNC, 1);
66 __n_assert(cs, return NULL);
67
68 memset(cs, 0, sizeof(N_CLOCK_SYNC));
69 cs->sample_index = 0;
70 cs->sample_count = 0;
71 cs->estimated_offset = 0.0;
72 cs->estimated_rtt = 0.0;
73 /* Set last_sync_time far enough in the past so first should_send returns TRUE */
75
76 return cs;
77}
78
80 __n_assert(cs, return);
81 __n_assert(*cs, return);
82 Free(*cs);
83 *cs = NULL;
84}
85
90static double clock_sync_effective_offset(const N_CLOCK_SYNC* cs, double local_now) {
91 if (cs->offset_prev == cs->estimated_offset) return cs->estimated_offset;
92 double dt = local_now - cs->offset_change_time;
93 if (dt >= N_CLOCK_SYNC_SLEW_SECONDS) return cs->estimated_offset;
94 if (dt <= 0.0) return cs->offset_prev;
95 double u = dt / N_CLOCK_SYNC_SLEW_SECONDS;
96 u = u * u * (3.0 - 2.0 * u); /* smoothstep: C1-continuous rate */
97 return cs->offset_prev + (cs->estimated_offset - cs->offset_prev) * u;
98}
99
113static void clock_sync_store_sample(N_CLOCK_SYNC* cs, double offset, double rtt, double local_now) {
114 cs->offset_samples[cs->sample_index] = offset;
115 cs->rtt_samples[cs->sample_index] = rtt;
118 cs->sample_count++;
119 }
120
121 int best = 0;
122 for (int i = 1; i < cs->sample_count; i++) {
123 if (cs->rtt_samples[i] < cs->rtt_samples[best]) best = i;
124 }
125 double new_target = cs->offset_samples[best];
126 if (new_target != cs->estimated_offset) {
127 /* re-anchor the slew at the CURRENT effective offset so a
128 * target change mid-slew stays continuous */
129 cs->offset_prev = clock_sync_effective_offset(cs, local_now);
130 cs->offset_change_time = local_now;
131 cs->estimated_offset = new_target;
132 }
134}
135
136int n_clock_sync_process_response(N_CLOCK_SYNC* cs, double client_send_time, double server_time, double local_now) {
137 __n_assert(cs, return FALSE);
138
139 double rtt = local_now - client_send_time;
140 if (rtt < 0.0) {
141 n_log(LOG_ERR, "n_clock_sync: negative RTT (%.4f), ignoring sample", rtt);
142 return FALSE;
143 }
144 if (rtt > N_CLOCK_SYNC_MAX_RTT) {
145 n_log(LOG_WARNING, "n_clock_sync: absurd RTT (%.4f > %.1f), ignoring sample", rtt, (double)N_CLOCK_SYNC_MAX_RTT);
146 return FALSE;
147 }
148
149 if (cs->synthetic) {
150 /* First REAL response: flush the synthetic login seed wholesale.
151 * Its conservative synthetic rtt can be lower than anything a
152 * high-RTT link ever produces, which would pin the estimate on
153 * the seed's one-shot (transit-skewed) offset forever. */
154 cs->sample_index = 0;
155 cs->sample_count = 0;
156 cs->synthetic = 0;
157 }
158
159 double one_way = rtt / 2.0;
160 double offset = server_time + one_way - local_now;
161
162 clock_sync_store_sample(cs, offset, rtt, local_now);
163
164 n_log(LOG_DEBUG, "n_clock_sync: sample %d, offset=%.4f rtt=%.4f (best offset=%.4f median rtt=%.4f)",
165 cs->sample_count, offset, rtt, cs->estimated_offset, cs->estimated_rtt);
166
167 return TRUE;
168}
169
170void n_clock_sync_seed(N_CLOCK_SYNC* cs, double offset, double rtt) {
171 __n_assert(cs, return);
172 if (rtt < 0.0) rtt = 0.0;
173 /* Reset to the synthetic sample and adopt immediately — at login
174 * there is no meaningful previous estimate to slew from. */
175 cs->sample_index = 0;
176 cs->sample_count = 0;
177 clock_sync_store_sample(cs, offset, rtt, 0.0);
179 cs->offset_change_time = 0.0;
180 cs->synthetic = 1;
181 n_log(LOG_DEBUG, "n_clock_sync: seeded offset=%.4f (synthetic rtt=%.4f)", offset, rtt);
182}
183
184double n_clock_sync_server_time(const N_CLOCK_SYNC* cs, double local_now) {
185 __n_assert(cs, return local_now);
186 return local_now + clock_sync_effective_offset(cs, local_now);
187}
188
189int n_clock_sync_should_send(const N_CLOCK_SYNC* cs, double local_now) {
190 __n_assert(cs, return FALSE);
191 /* Burst until warm: a synthetic seed does not count as a real sample. */
192 int real_samples = cs->synthetic ? 0 : cs->sample_count;
193 double interval = (real_samples < N_CLOCK_SYNC_BURST_SAMPLES)
196 return (local_now - cs->last_sync_time) >= interval;
197}
198
199void n_clock_sync_mark_sent(N_CLOCK_SYNC* cs, double local_now) {
200 __n_assert(cs, return);
201 cs->last_sync_time = local_now;
202}
203
int sample_index
current write position in circular buffer
double last_sync_time
local time of last sync request sent
double estimated_offset
add to local time to get estimated server time (raw best-RTT target — n_clock_sync_server_time applie...
double estimated_rtt
current estimated round-trip time
double offset_change_time
local time at which estimated_offset last changed
int synthetic
TRUE while the buffer holds only a synthetic n_clock_sync_seed sample — flushed wholesale by the firs...
int sample_count
number of samples collected so far
double offset_prev
effective offset at the moment estimated_offset last changed — the slew blends offset_prev → estimate...
double rtt_samples[11]
circular buffer of RTT values
double offset_samples[11]
circular buffer of offset estimates
#define N_CLOCK_SYNC_SAMPLE_COUNT
number of samples for the median filter
N_CLOCK_SYNC * n_clock_sync_new(void)
allocate and initialize a new clock sync estimator
#define N_CLOCK_SYNC_INTERVAL
default interval between sync requests in seconds
static int cmp_double(const void *a, const void *b)
comparison function for qsort on doubles
int n_clock_sync_should_send(const N_CLOCK_SYNC *cs, double local_now)
check if it's time to send a new sync request (returns TRUE/FALSE).
double n_clock_sync_server_time(const N_CLOCK_SYNC *cs, double local_now)
get estimated server time given a local time value.
void n_clock_sync_seed(N_CLOCK_SYNC *cs, double offset, double rtt)
Inject a synthetic (offset, rtt) sample, e.g.
static double clock_sync_effective_offset(const N_CLOCK_SYNC *cs, double local_now)
effective (slewed) offset at local_now — pure function of the stored state so the const getter needs ...
#define N_CLOCK_SYNC_BURST_SAMPLES
number of REAL samples below which the burst interval applies
#define N_CLOCK_SYNC_SLEW_SECONDS
seconds over which n_clock_sync_server_time slews from the previous offset estimate to a new one (smo...
void n_clock_sync_delete(N_CLOCK_SYNC **cs)
free a clock sync estimator
void n_clock_sync_mark_sent(N_CLOCK_SYNC *cs, double local_now)
mark that a sync request was just sent
int n_clock_sync_process_response(N_CLOCK_SYNC *cs, double client_send_time, double server_time, double local_now)
record a sync response: client_send_time is the local time the request was sent, server_time is the s...
#define N_CLOCK_SYNC_BURST_INTERVAL
accelerated interval used until N_CLOCK_SYNC_BURST_SAMPLES real responses have been collected — a fre...
#define N_CLOCK_SYNC_MAX_RTT
responses with an RTT above this are rejected outright (retransmit storms / suspend-resume artefacts ...
static double median_of(const double *samples, int count)
compute the median of an array of doubles
static void clock_sync_store_sample(N_CLOCK_SYNC *cs, double offset, double rtt, double local_now)
store one (offset, rtt) sample and recompute the estimates.
clock synchronization estimator
#define Malloc(__ptr, __struct, __size)
Malloc Handler to get errors and set to 0.
Definition n_common.h:204
#define __n_assert(__ptr, __ret)
macro to assert things
Definition n_common.h:279
#define Free(__ptr)
Free Handler to get errors.
Definition n_common.h:263
#define n_log(__LEVEL__,...)
Logging function wrapper to get line and func.
Definition n_log.h:89
#define LOG_DEBUG
debug-level messages
Definition n_log.h:84
#define LOG_ERR
error conditions
Definition n_log.h:76
#define LOG_WARNING
warning conditions
Definition n_log.h:78
Clock synchronization estimator for networked games.
Common headers and low-level functions & define.
Generic log system.