summaryrefslogtreecommitdiff
path: root/engine/clockdriftmgr.cpp
blob: 45d725c360e66acb5433c36f46604a15d6c22d65 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: 
//
//=============================================================================//

#include "client.h"
#include "clockdriftmgr.h"
#include "demo.h"
#include "server.h"
#include "enginethreads.h"


ConVar cl_clock_correction( "cl_clock_correction", "1", FCVAR_CHEAT, "Enable/disable clock correction on the client." );

ConVar cl_clockdrift_max_ms( "cl_clockdrift_max_ms", "150", FCVAR_CHEAT, "Maximum number of milliseconds the clock is allowed to drift before the client snaps its clock to the server's." );
ConVar cl_clockdrift_max_ms_threadmode( "cl_clockdrift_max_ms_threadmode", "0", FCVAR_CHEAT, "Maximum number of milliseconds the clock is allowed to drift before the client snaps its clock to the server's." );

ConVar cl_clock_showdebuginfo( "cl_clock_showdebuginfo", "0", FCVAR_CHEAT, "Show debugging info about the clock drift. ");

ConVar cl_clock_correction_force_server_tick( "cl_clock_correction_force_server_tick", "999", FCVAR_CHEAT, "Force clock correction to match the server tick + this offset (-999 disables it)."  );

ConVar cl_clock_correction_adjustment_max_amount( "cl_clock_correction_adjustment_max_amount", "200", FCVAR_CHEAT, 
	"Sets the maximum number of milliseconds per second it is allowed to correct the client clock. "
	"It will only correct this amount if the difference between the client and server clock is equal to or larger than cl_clock_correction_adjustment_max_offset." );

ConVar cl_clock_correction_adjustment_min_offset( "cl_clock_correction_adjustment_min_offset", "10", FCVAR_CHEAT, 
	"If the clock offset is less than this amount (in milliseconds), then no clock correction is applied." );

ConVar cl_clock_correction_adjustment_max_offset( "cl_clock_correction_adjustment_max_offset", "90", FCVAR_CHEAT, 
	"As the clock offset goes from cl_clock_correction_adjustment_min_offset to this value (in milliseconds), "
	"it moves towards applying cl_clock_correction_adjustment_max_amount of adjustment. That way, the response "
	"is small when the offset is small." );



// Given the offset (in milliseconds) of the client clock from the server clock,
// returns how much correction we'd like to apply per second (in seconds).
static float GetClockAdjustmentAmount( float flCurDiffInMS )
{
	flCurDiffInMS = clamp( flCurDiffInMS, cl_clock_correction_adjustment_min_offset.GetFloat(), cl_clock_correction_adjustment_max_offset.GetFloat() );

	float flReturnValue = RemapVal( flCurDiffInMS,
		cl_clock_correction_adjustment_min_offset.GetFloat(), cl_clock_correction_adjustment_max_offset.GetFloat(),
		0, cl_clock_correction_adjustment_max_amount.GetFloat() / 1000.0f );

	return flReturnValue;
}
	 

// -------------------------------------------------------------------------------------------------- /
// CClockDriftMgr implementation.
// -------------------------------------------------------------------------------------------------- /

CClockDriftMgr::CClockDriftMgr()
{
	Clear();
}


void CClockDriftMgr::Clear()
{
	m_nClientTick = 0;
	m_nServerTick = 0;
	m_iCurClockOffset = 0;
	memset( m_ClockOffsets, 0, sizeof( m_ClockOffsets ) );
}


// when running in threaded host mode, the clock drifts by a predictable algorithm
// because the client lags the server by one frame
// so at each update from the network we have lastframeticks-1 pending ticks to execute
// on the client.  If the clock has drifted by exactly that amount, allow it to drift temporarily
// NOTE: When the server gets paused the tick count is still incorrect for a frame
// NOTE: It should be possible to fix this by applying pause before the tick is incremented
// NOTE: or decrementing the client tick after receiving pause
// NOTE: This is due to the fact that currently pause is applied at frame start on the server
// NOTE: and frame end on the client
void CClockDriftMgr::SetServerTick( int nTick )
{
#if !defined( SWDS )
	m_nServerTick = nTick;

	int nMaxDriftTicks = IsEngineThreaded() ? 
		TIME_TO_TICKS( (cl_clockdrift_max_ms_threadmode.GetFloat() / 1000.0) ) :
		TIME_TO_TICKS( (cl_clockdrift_max_ms.GetFloat() / 1000.0) );

	int clientTick = cl.GetClientTickCount() + g_ClientGlobalVariables.simTicksThisFrame - 1;
	if ( cl_clock_correction_force_server_tick.GetInt() == 999 )
	{
		// If this is the first tick from the server, or if we get further than cl_clockdrift_max_ticks off, then 
		// use the old behavior and slam the server's tick into the client tick.
		if ( !IsClockCorrectionEnabled() ||
			 clientTick == 0 || 
			 abs(nTick - clientTick) > nMaxDriftTicks
			)
		{
			cl.SetClientTickCount( nTick - (g_ClientGlobalVariables.simTicksThisFrame - 1) );
			if ( cl.GetClientTickCount() < cl.oldtickcount )
			{
				cl.oldtickcount = cl.GetClientTickCount();
			}
			memset( m_ClockOffsets, 0, sizeof( m_ClockOffsets ) );
		}
	}
	else
	{
		// Used for testing..
		cl.SetClientTickCount( nTick + cl_clock_correction_force_server_tick.GetInt() );
	}

	// adjust the clock offset by the clock with thread mode compensation
	m_ClockOffsets[m_iCurClockOffset] = clientTick - m_nServerTick;
	m_iCurClockOffset = (m_iCurClockOffset + 1) % NUM_CLOCKDRIFT_SAMPLES;
#endif // SWDS
}


float CClockDriftMgr::AdjustFrameTime( float inputFrameTime )
{
	float flAdjustmentThisFrame = 0;
	float flAdjustmentPerSec = 0;
	if ( IsClockCorrectionEnabled() 
#if !defined( _XBOX ) && !defined( SWDS )
		 && !demoplayer->IsPlayingBack()
#endif
		)
	{
		// Get the clock difference in seconds.
		float flCurDiffInSeconds = GetCurrentClockDifference() * host_state.interval_per_tick;
		float flCurDiffInMS = flCurDiffInSeconds * 1000.0f;

		// Is the server ahead or behind us?
		if ( flCurDiffInMS > cl_clock_correction_adjustment_min_offset.GetFloat() )
		{
			flAdjustmentPerSec = -GetClockAdjustmentAmount( flCurDiffInMS );
			flAdjustmentThisFrame = inputFrameTime * flAdjustmentPerSec;
			flAdjustmentThisFrame = max( flAdjustmentThisFrame, -flCurDiffInSeconds );
		}
		else if ( flCurDiffInMS < -cl_clock_correction_adjustment_min_offset.GetFloat() )
		{
			flAdjustmentPerSec = GetClockAdjustmentAmount( -flCurDiffInMS );
			flAdjustmentThisFrame = inputFrameTime * flAdjustmentPerSec;
			flAdjustmentThisFrame = min( flAdjustmentThisFrame, -flCurDiffInSeconds );
		}

		if ( IsEngineThreaded() )
		{
			flAdjustmentThisFrame = -flCurDiffInSeconds;
		}

		AdjustAverageDifferenceBy( flAdjustmentThisFrame );
	}

	ShowDebugInfo( flAdjustmentPerSec );
	return inputFrameTime + flAdjustmentThisFrame;
}


float CClockDriftMgr::GetCurrentClockDifference() const
{
	// Note: this could be optimized a little by updating it each time we add
	// a sample (subtract the old value from the total and add the new one in).
	float total = 0;
	for ( int i=0; i < NUM_CLOCKDRIFT_SAMPLES; i++ )
		total += m_ClockOffsets[i];

	return total / NUM_CLOCKDRIFT_SAMPLES;
}


void CClockDriftMgr::ShowDebugInfo( float flAdjustment )
{
	if ( !cl_clock_showdebuginfo.GetInt() )
		return;

	if ( IsClockCorrectionEnabled() )
	{
		int high=-999, low=999;
		int exactDiff = cl.GetClientTickCount() - m_nServerTick;
		for ( int i=0; i < NUM_CLOCKDRIFT_SAMPLES; i++ )
		{
			high = max( (float)high, m_ClockOffsets[i] );
			low = min( (float)low, m_ClockOffsets[i] );
		}

		Msg( "Clock drift: adjustment (per sec): %.2fms, avg: %.3f, lo: %d, hi: %d, ex: %d\n", flAdjustment*1000.0f, GetCurrentClockDifference(), low, high, exactDiff );
	}
	else
	{
		Msg( "Clock drift disabled.\n" );
	}
}


void CClockDriftMgr::AdjustAverageDifferenceBy( float flAmountInSeconds )
{
	// Don't adjust the average if it's already tiny.
	float c = GetCurrentClockDifference();
	if ( c < 0.05f )
		return;

	float flAmountInTicks = flAmountInSeconds / host_state.interval_per_tick;
	float factor = 1 + flAmountInTicks / c;

	for ( int i=0; i < NUM_CLOCKDRIFT_SAMPLES; i++ )
		m_ClockOffsets[i] *= factor;
	
	Assert( fabs( GetCurrentClockDifference() - (c + flAmountInTicks) ) < 0.001f );
}

extern float NET_GetFakeLag();
extern ConVar net_usesocketsforloopback;

bool CClockDriftMgr::IsClockCorrectionEnabled()
{
#ifdef SWDS
	return false;
#else

	bool bIsMultiplayer = NET_IsMultiplayer();
	// Assume we always want it in multiplayer
	bool bWantsClockDriftMgr = bIsMultiplayer;
	// If we're a multiplayer listen server, we can back off of that if we have zero latency (faked or due to sockets)
	if ( bIsMultiplayer )
	{
		bool bIsListenServer = sv.IsActive();
		bool bLocalConnectionHasZeroLatency = ( NET_GetFakeLag() <= 0.0f ) && !net_usesocketsforloopback.GetBool();

		if ( bIsListenServer && bLocalConnectionHasZeroLatency )
		{
			bWantsClockDriftMgr = false;
		}
	}

	// Only in multi-threaded client/server OR in multi player, but don't use it if we're the listen server w/ no fake lag
	return cl_clock_correction.GetInt() && 
		( IsEngineThreaded() ||	bWantsClockDriftMgr );		
#endif
}