Skip to content

Commit dbd2fcb

Browse files
fix: network transform interpolation perf improvements (#1272)
* AddMeasurement doesn't sort anymore and relies on consumption to keep track of last item to consume. It'll also give up interpolating beyond a certain amount of bursted incoming netvar changes. Fixing endtime vs start time comparison, to make sure we don't have floating point approximation errors * adding doc updating burst test to adapt to new behaviour fixing some algo issue * Better comments * making sure buffer will always be the same capacity Co-authored-by: Matt Walsh <[email protected]>
1 parent 388d775 commit dbd2fcb

File tree

3 files changed

+130
-59
lines changed

3 files changed

+130
-59
lines changed

com.unity.netcode.gameobjects/Components/Interpolator/BufferedLinearInterpolator.cs

Lines changed: 88 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,15 @@ private struct BufferedItem
3737
{
3838
public T Item;
3939
public NetworkTime TimeSent;
40+
41+
public BufferedItem(T item, NetworkTime timeSent)
42+
{
43+
Item = item;
44+
TimeSent = timeSent;
45+
}
4046
}
4147

48+
private const double k_SmallValue = 9.999999439624929E-11; // copied from Vector3's equal operator
4249

4350
/// <summary>
4451
/// Override this if you want configurable buffering, right now using ServerTick's own global buffering
@@ -54,8 +61,36 @@ private struct BufferedItem
5461
private NetworkTime m_EndTimeConsumed;
5562
private NetworkTime m_StartTimeConsumed;
5663

57-
private readonly List<BufferedItem> m_Buffer = new List<BufferedItem>();
58-
private const int k_BufferSizeLimit = 100;
64+
private readonly List<BufferedItem> m_Buffer = new List<BufferedItem>(k_BufferCountLimit);
65+
66+
// Buffer consumption scenarios
67+
// Perfect case consumption
68+
// | 1 | 2 | 3 |
69+
// | 2 | 3 | 4 | consume 1
70+
// | 3 | 4 | 5 | consume 2
71+
// | 4 | 5 | 6 | consume 3
72+
// | 5 | 6 | 7 | consume 4
73+
// jittered case
74+
// | 1 | 2 | 3 |
75+
// | 2 | 3 | | consume 1
76+
// | 3 | | | consume 2
77+
// | 4 | 5 | 6 | consume 3
78+
// | 5 | 6 | 7 | consume 4
79+
// bursted case (assuming max count is 5)
80+
// | 1 | 2 | 3 |
81+
// | 2 | 3 | | consume 1
82+
// | 3 | | | consume 2
83+
// | | | | consume 3
84+
// | | | |
85+
// | 4 | 5 | 6 | 7 | 8 | --> consume all and teleport to last value <8> --> this is the nuclear option, ideally this example would consume 4 and 5
86+
// instead of jumping to 8, but since in OnValueChange we don't yet have an updated server time (updated in pre-update) to know which value
87+
// we should keep and which we should drop, we don't have enough information to do this. Another thing would be to not have the burst in the first place.
88+
89+
// Constant absolute value for max buffer count instead of dynamic time based value. This is in case we have very low tick rates, so
90+
// that we don't have a very small buffer because of this.
91+
private const int k_BufferCountLimit = 100;
92+
private BufferedItem m_LastBufferedItemReceived;
93+
private int m_NbItemsReceivedThisFrame;
5994

6095
private int m_LifetimeConsumedCount;
6196

@@ -66,6 +101,11 @@ internal BufferedLinearInterpolator(NetworkManager manager)
66101
InterpolatorTimeProxy = new InterpolatorTime(manager);
67102
}
68103

104+
internal BufferedLinearInterpolator(IInterpolatorTime proxy)
105+
{
106+
InterpolatorTimeProxy = proxy;
107+
}
108+
69109
public void ResetTo(T targetValue)
70110
{
71111
m_LifetimeConsumedCount = 1;
@@ -87,26 +127,38 @@ private void TryConsumeFromBuffer()
87127
// only consume if we're ready
88128
if (RenderTime >= m_EndTimeConsumed.Time)
89129
{
90-
// buffer is sorted so older (smaller) time values are at the end.
91-
for (int i = m_Buffer.Count - 1; i >= 0; i--) // todo stretch: consume ahead if we see we're missing values
130+
BufferedItem? itemToInterpolateTo = null;
131+
// assumes we're using sequenced messages for netvar syncing
132+
// buffer contains oldest values first, iterating from end to start to remove elements from list while iterating
133+
for (int i = m_Buffer.Count - 1; i >= 0; i--) // todo stretch: consume ahead if we see we're missing values due to packet loss
92134
{
93135
var bufferedValue = m_Buffer[i];
94-
// Consume when ready. This can consume multiple times
136+
// Consume when ready and interpolate to last value we can consume. This can consume multiple values from the buffer
95137
if (bufferedValue.TimeSent.Time <= ServerTimeBeingHandledForBuffering)
96138
{
97-
if (m_LifetimeConsumedCount == 0)
98-
{
99-
m_StartTimeConsumed = bufferedValue.TimeSent;
100-
m_InterpStartValue = bufferedValue.Item;
101-
}
102-
else if (consumedCount == 0)
139+
if (!itemToInterpolateTo.HasValue || bufferedValue.TimeSent.Time > itemToInterpolateTo.Value.TimeSent.Time)
103140
{
104-
m_StartTimeConsumed = m_EndTimeConsumed;
105-
m_InterpStartValue = m_InterpEndValue;
141+
if (m_LifetimeConsumedCount == 0)
142+
{
143+
// if interpolator not initialized, teleport to first value when available
144+
m_StartTimeConsumed = bufferedValue.TimeSent;
145+
m_InterpStartValue = bufferedValue.Item;
146+
}
147+
else if (consumedCount == 0)
148+
{
149+
// Interpolating to new value, end becomes start. We then look in our buffer for a new end.
150+
m_StartTimeConsumed = m_EndTimeConsumed;
151+
m_InterpStartValue = m_InterpEndValue;
152+
}
153+
154+
if (bufferedValue.TimeSent.Time > m_EndTimeConsumed.Time)
155+
{
156+
itemToInterpolateTo = bufferedValue;
157+
m_EndTimeConsumed = bufferedValue.TimeSent;
158+
m_InterpEndValue = bufferedValue.Item;
159+
}
106160
}
107161

108-
m_EndTimeConsumed = bufferedValue.TimeSent;
109-
m_InterpEndValue = bufferedValue.Item;
110162
m_Buffer.RemoveAt(i);
111163
consumedCount++;
112164
m_LifetimeConsumedCount++;
@@ -132,9 +184,9 @@ public T Update(float deltaTime)
132184
if (m_LifetimeConsumedCount >= 1) // shouldn't interpolate between default values, let's wait to receive data first, should only interpolate between real measurements
133185
{
134186
float t = 1.0f;
135-
if (m_EndTimeConsumed.Time != m_StartTimeConsumed.Time)
187+
double range = m_EndTimeConsumed.Time - m_StartTimeConsumed.Time;
188+
if (range > k_SmallValue)
136189
{
137-
double range = m_EndTimeConsumed.Time - m_StartTimeConsumed.Time;
138190
t = (float)((RenderTime - m_StartTimeConsumed.Time) / range);
139191

140192
if (t < 0.0f)
@@ -155,21 +207,31 @@ public T Update(float deltaTime)
155207
m_CurrentInterpValue = Interpolate(m_CurrentInterpValue, target, deltaTime / maxInterpTime); // second interpolate to smooth out extrapolation jumps
156208
}
157209

210+
m_NbItemsReceivedThisFrame = 0;
158211
return m_CurrentInterpValue;
159212
}
160213

161214
public void AddMeasurement(T newMeasurement, NetworkTime sentTime)
162215
{
163-
if (m_Buffer.Count >= k_BufferSizeLimit)
216+
m_NbItemsReceivedThisFrame++;
217+
218+
// This situation can happen after a game is paused. When starting to receive again, the server will have sent a bunch of messages in the meantime
219+
// instead of going through thousands of value updates just to get a big teleport, we're giving up on interpolation and teleporting to the latest value
220+
if (m_NbItemsReceivedThisFrame > k_BufferCountLimit)
164221
{
165-
Debug.LogWarning("Going over buffer size limit while adding new interpolation values, interpolation buffering isn't consuming fast enough, removing oldest value now.");
166-
m_Buffer.RemoveAt(m_Buffer.Count - 1);
222+
if (m_LastBufferedItemReceived.TimeSent.Time < sentTime.Time)
223+
{
224+
m_LastBufferedItemReceived = new BufferedItem(newMeasurement, sentTime);
225+
ResetTo(newMeasurement);
226+
}
227+
228+
return;
167229
}
168230

169231
if (sentTime.Time > m_EndTimeConsumed.Time || m_LifetimeConsumedCount == 0) // treat only if value is newer than the one being interpolated to right now
170232
{
171-
m_Buffer.Add(new BufferedItem { Item = newMeasurement, TimeSent = sentTime });
172-
m_Buffer.Sort((item1, item2) => item2.TimeSent.Time.CompareTo(item1.TimeSent.Time));
233+
m_LastBufferedItemReceived = new BufferedItem(newMeasurement, sentTime);
234+
m_Buffer.Add(m_LastBufferedItemReceived);
173235
}
174236
}
175237

@@ -197,6 +259,10 @@ protected override float Interpolate(float start, float end, float time)
197259
public BufferedLinearInterpolatorFloat(NetworkManager manager) : base(manager)
198260
{
199261
}
262+
263+
public BufferedLinearInterpolatorFloat(IInterpolatorTime proxy) : base(proxy)
264+
{
265+
}
200266
}
201267

202268
internal class BufferedLinearInterpolatorQuaternion : BufferedLinearInterpolator<Quaternion>

com.unity.netcode.gameobjects/Runtime/Timing/NetworkTime.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public struct NetworkTime
5252

5353
/// <summary>
5454
/// Gets the tickrate of the system of this <see cref="NetworkTime"/>.
55+
/// Ticks per second.
5556
/// </summary>
5657
public uint TickRate => m_TickRate;
5758

com.unity.netcode.gameobjects/Tests/Editor/InterpolatorTests.cs

Lines changed: 41 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ private NetworkTime T(float time, uint tickRate = k_MockTickRate)
3030
[Test]
3131
public void TestReset()
3232
{
33-
var interpolator = new BufferedLinearInterpolatorFloat(null);
3433
var timeMock = new MockInterpolatorTime(0, k_MockTickRate);
35-
interpolator.InterpolatorTimeProxy = timeMock;
34+
var interpolator = new BufferedLinearInterpolatorFloat(timeMock);
35+
3636
timeMock.BufferedServerTime = 100f;
3737

3838
interpolator.AddMeasurement(5, T(1.0f));
@@ -51,9 +51,8 @@ public void NormalUsage()
5151
{
5252
// Testing float instead of Vector3. The only difference with Vector3 is the lerp method used.
5353

54-
var interpolator = new BufferedLinearInterpolatorFloat(null);
5554
var mockBufferedTime = new MockInterpolatorTime(0, k_MockTickRate);
56-
interpolator.InterpolatorTimeProxy = mockBufferedTime;
55+
var interpolator = new BufferedLinearInterpolatorFloat(mockBufferedTime);
5756

5857
Assert.That(interpolator.GetInterpolatedValue(), Is.EqualTo(0f));
5958

@@ -97,9 +96,8 @@ public void NormalUsage()
9796
[Test]
9897
public void OutOfOrderShouldStillWork()
9998
{
100-
var interpolator = new BufferedLinearInterpolatorFloat(null);
10199
var mockBufferedTime = new MockInterpolatorTime(0, k_MockTickRate);
102-
interpolator.InterpolatorTimeProxy = mockBufferedTime;
100+
var interpolator = new BufferedLinearInterpolatorFloat(mockBufferedTime);
103101

104102
interpolator.AddMeasurement(0, T(0f));
105103
interpolator.AddMeasurement(2, T(2f));
@@ -127,9 +125,8 @@ public void OutOfOrderShouldStillWork()
127125
[Test]
128126
public void MessageLoss()
129127
{
130-
var interpolator = new BufferedLinearInterpolatorFloat(null);
131128
var mockBufferedTime = new MockInterpolatorTime(0, k_MockTickRate);
132-
interpolator.InterpolatorTimeProxy = mockBufferedTime;
129+
var interpolator = new BufferedLinearInterpolatorFloat(mockBufferedTime);
133130

134131
interpolator.AddMeasurement(1f, T(1f));
135132
interpolator.AddMeasurement(2f, T(2f));
@@ -139,10 +136,22 @@ public void MessageLoss()
139136
// message time=6 was lost
140137
interpolator.AddMeasurement(100f, T(7f)); // high value to produce a misprediction
141138

139+
// first value teleports interpolator
140+
mockBufferedTime.BufferedServerTime = 1f;
141+
interpolator.Update(1f);
142+
Assert.That(interpolator.GetInterpolatedValue(), Is.EqualTo(1f));
143+
144+
// nothing happens, not ready to consume second value yet
145+
mockBufferedTime.BufferedServerTime = 1.5f;
146+
interpolator.Update(0.5f);
147+
Assert.That(interpolator.GetInterpolatedValue(), Is.EqualTo(1f));
148+
149+
// beginning of interpolation, second value consumed, currently at start
142150
mockBufferedTime.BufferedServerTime = 2f;
143-
interpolator.Update(2f);
151+
interpolator.Update(0.5f);
144152
Assert.That(interpolator.GetInterpolatedValue(), Is.EqualTo(1f));
145153

154+
// interpolation starts
146155
mockBufferedTime.BufferedServerTime = 2.5f;
147156
interpolator.Update(0.5f);
148157
Assert.That(interpolator.GetInterpolatedValue(), Is.EqualTo(1.5f));
@@ -194,9 +203,8 @@ public void MessageLoss()
194203
[Test]
195204
public void AddFirstMeasurement()
196205
{
197-
var interpolator = new BufferedLinearInterpolatorFloat(null);
198206
var mockBufferedTime = new MockInterpolatorTime(0, k_MockTickRate);
199-
interpolator.InterpolatorTimeProxy = mockBufferedTime;
207+
var interpolator = new BufferedLinearInterpolatorFloat(mockBufferedTime);
200208

201209
interpolator.AddMeasurement(2f, T(1f));
202210
interpolator.AddMeasurement(3f, T(2f));
@@ -222,9 +230,8 @@ public void AddFirstMeasurement()
222230
[Test]
223231
public void JumpToEachValueIfDeltaTimeTooBig()
224232
{
225-
var interpolator = new BufferedLinearInterpolatorFloat(null);
226233
var mockBufferedTime = new MockInterpolatorTime(0, k_MockTickRate);
227-
interpolator.InterpolatorTimeProxy = mockBufferedTime;
234+
var interpolator = new BufferedLinearInterpolatorFloat(mockBufferedTime);
228235

229236
interpolator.AddMeasurement(2f, T(1f));
230237
interpolator.AddMeasurement(3f, T(2f));
@@ -241,14 +248,14 @@ public void JumpToEachValueIfDeltaTimeTooBig()
241248
[Test]
242249
public void JumpToLastValueFromStart()
243250
{
244-
var interpolator = new BufferedLinearInterpolatorFloat(null);
245251
var mockBufferedTime = new MockInterpolatorTime(0, k_MockTickRate);
246-
interpolator.InterpolatorTimeProxy = mockBufferedTime;
252+
var interpolator = new BufferedLinearInterpolatorFloat(mockBufferedTime);
247253

248254
interpolator.AddMeasurement(1f, T(1f));
249255
interpolator.AddMeasurement(2f, T(2f));
250256
interpolator.AddMeasurement(3f, T(3f));
251257

258+
// big time jump
252259
mockBufferedTime.BufferedServerTime = 10f;
253260
var interpolatedValue = interpolator.Update(10f);
254261
Assert.That(interpolatedValue, Is.EqualTo(3f));
@@ -272,42 +279,32 @@ public void JumpToLastValueFromStart()
272279
[Test]
273280
public void TestBufferSizeLimit()
274281
{
275-
var interpolator = new BufferedLinearInterpolatorFloat(null);
276282
var mockBufferedTime = new MockInterpolatorTime(0, k_MockTickRate);
277-
interpolator.InterpolatorTimeProxy = mockBufferedTime;
283+
var interpolator = new BufferedLinearInterpolatorFloat(mockBufferedTime);
278284

279285
// set first value
280286
interpolator.AddMeasurement(-1f, T(1f));
281287
mockBufferedTime.BufferedServerTime = 1f;
282288
interpolator.Update(1f);
283289

284290
// max + 1
285-
interpolator.AddMeasurement(2, T(2)); // this value should disappear
286-
for (int i = 3; i < 103; i++)
291+
interpolator.AddMeasurement(2, T(2)); // +1, this should trigger a burst and teleport to last value
292+
for (int i = 0; i < 100; i++)
287293
{
288-
interpolator.AddMeasurement(i, T(i));
294+
interpolator.AddMeasurement(i + 3, T(i + 3));
289295
}
290296

291-
// make sure the first value isn't there anymore and that we're already using the second value
292-
// the following shouldn't happen in real life, since the server time should catchup to the real time and not stay behind like this
293-
mockBufferedTime.BufferedServerTime = 2f;
294-
// if there was no buffer limit, we'd still be consuming the first "1" value
295-
var interpolatedValue = interpolator.Update(1f);
296-
// but now, we're still at the initial -1f
297-
Assert.That(interpolatedValue, Is.EqualTo(-1f));
298-
299-
// interpolation continues as expected, interpolating between -1 and 3
300-
mockBufferedTime.BufferedServerTime = 3f;
301-
interpolatedValue = interpolator.Update(1f);
302-
Assert.That(interpolatedValue, Is.EqualTo(1f));
297+
// client was paused for a while, some time has past, we just got a burst of values from the server that teleported us to the last value received
298+
mockBufferedTime.BufferedServerTime = 102;
299+
var interpolatedValue = interpolator.Update(101f);
300+
Assert.That(interpolatedValue, Is.EqualTo(102));
303301
}
304302

305303
[Test]
306304
public void TestUpdatingInterpolatorWithNoData()
307305
{
308-
var interpolator = new BufferedLinearInterpolatorFloat(null);
309306
var mockBufferedTime = new MockInterpolatorTime(0, k_MockTickRate);
310-
interpolator.InterpolatorTimeProxy = mockBufferedTime;
307+
var interpolator = new BufferedLinearInterpolatorFloat(mockBufferedTime);
311308

312309
// invalid case, this is undefined behaviour
313310
Assert.Throws<InvalidOperationException>(() => interpolator.Update(1f));
@@ -316,20 +313,27 @@ public void TestUpdatingInterpolatorWithNoData()
316313
[Test]
317314
public void TestDuplicatedValues()
318315
{
319-
var interpolator = new BufferedLinearInterpolatorFloat(null);
320316
var mockBufferedTime = new MockInterpolatorTime(0, k_MockTickRate);
321-
interpolator.InterpolatorTimeProxy = mockBufferedTime;
317+
var interpolator = new BufferedLinearInterpolatorFloat(mockBufferedTime);
322318

323319
interpolator.AddMeasurement(1f, T(1f));
324320
interpolator.AddMeasurement(2f, T(2f));
325321
interpolator.AddMeasurement(2f, T(2f));
326322

327-
mockBufferedTime.BufferedServerTime = 2f;
323+
// empty interpolator teleports to initial value
324+
mockBufferedTime.BufferedServerTime = 1f;
328325
var interp = interpolator.Update(1f);
329326
Assert.That(interp, Is.EqualTo(1f));
327+
328+
// consume value, start interp, currently at start value
329+
mockBufferedTime.BufferedServerTime = 2f;
330+
interp = interpolator.Update(1f);
331+
Assert.That(interp, Is.EqualTo(1f));
332+
// interp
330333
mockBufferedTime.BufferedServerTime = 2.5f;
331334
interp = interpolator.Update(0.5f);
332335
Assert.That(interp, Is.EqualTo(1.5f));
336+
// reach end
333337
mockBufferedTime.BufferedServerTime = 3f;
334338
interp = interpolator.Update(0.5f);
335339
Assert.That(interp, Is.EqualTo(2f));

0 commit comments

Comments
 (0)