timeline reset

This commit is contained in:
vis2k 2023-03-10 17:51:53 +08:00
parent 80b23f029d
commit 36fcef738c
6 changed files with 139 additions and 37 deletions

View File

@ -50,6 +50,13 @@ public static partial class NetworkClient
[Range(0, 1)]
public static double slowdownSpeed = 0.01f; // 1%
[Header("Snapshot Interpolation: Clamping")]
[Tooltip("If the local timeline is so far behind remote time that catchup would take too long, then we do a hard reset so it won't catch up for a minute or more.")]
public static float resetNegativeThreshold = catchupNegativeThreshold * 5; // needs to be larger than catchup threshold
[Tooltip("If the local timeline is so far ahead remote time that slowdown would take too long, then we do a hard reset so it won't slow down for a minute or more.")]
public static float resetPositiveThreshold = catchupPositiveThreshold * 5; // needs to be larger than catchup threshold
[Tooltip("Catchup/Slowdown is adjusted over n-second exponential moving average.")]
public static int driftEmaDuration = 1; // shouldn't need to modify this, but expose it anyway
@ -155,6 +162,8 @@ public static void OnTimeSnapshot(TimeSnapshot snap)
ref driftEma,
catchupNegativeThreshold,
catchupPositiveThreshold,
resetNegativeThreshold,
resetPositiveThreshold,
ref deliveryTimeEma);
// Debug.Log($"inserted TimeSnapshot remote={snap.remoteTime:F2} local={snap.localTime:F2} total={snapshots.Count}");

View File

@ -90,6 +90,8 @@ public void OnTimeSnapshot(TimeSnapshot snapshot)
ref driftEma,
NetworkClient.catchupNegativeThreshold,
NetworkClient.catchupPositiveThreshold,
NetworkClient.resetNegativeThreshold,
NetworkClient.resetPositiveThreshold,
ref deliveryTimeEma
);
}

View File

@ -61,6 +61,40 @@ public static double Timescale(
return 1;
}
// catchup/slowdown attempts to bring timeline back in sync smoothly.
// however, if timeline is too far behind then we should clamp hard.
// otherwise catchup/slowdown may take 20s or more, minutes, or more.
// at some point, it'll just be way too far behind.
//
// to reproduce, try snapshot interpolation demo and press the button to
// simulate the client timeline at multiple seconds behind. it'll take
// a long time to catch up if the timeline is a long time behind.
//
// returns true if time needs to be clamped.
// drift EMA should also be reset so that catchup/slowdown doesn't
// compute based on old values before the reset.
public static bool TimelineReset(
double drift, // how far we are off from bufferTime
double absoluteResetNegativeThreshold, // in seconds. needs to be larger than catchup thresholds.
double absoluteResetPositiveThreshold // in seconds. needs to be larger than catchup thresholds.
)
{
// if the drift time is too large, it means we are behind more time.
if (drift > absoluteResetPositiveThreshold)
{
return true;
}
// if the drift time is too small, it means we are ahead of time.
if (drift < absoluteResetNegativeThreshold)
{
return true;
}
// don't clamp, all is within acceptable ranges.
return false;
}
// calculate dynamic buffer time adjustment
public static double DynamicAdjustment(
double sendInterval,
@ -120,6 +154,8 @@ public static void InsertAndAdjust<T>(
ref ExponentialMovingAverage driftEma, // for catchup / slowdown
float catchupNegativeThreshold, // in % of sendInteral (careful, we may run out of snapshots)
float catchupPositiveThreshold, // in % of sendInterval
float resetNegativeThreshold, // in % of sendInterval (careful, we may run out of snapshots)
float resetPositiveThreshold, // in % of sendInterval
ref ExponentialMovingAverage deliveryTimeEma) // for dynamic buffer time adjustment
where T : Snapshot
{
@ -205,15 +241,37 @@ public static void InsertAndAdjust<T>(
double drift = driftEma.Value - bufferTime;
// convert relative thresholds to absolute values based on sendInterval
double absoluteNegativeThreshold = sendInterval * catchupNegativeThreshold;
double absolutePositiveThreshold = sendInterval * catchupPositiveThreshold;
double absoluteCatchupNegativeThreshold = sendInterval * catchupNegativeThreshold;
double absoluteCatchupPositiveThreshold = sendInterval * catchupPositiveThreshold;
double absoluteResetNegativeThreshold = sendInterval * resetNegativeThreshold;
double absoluteResetPositiveThreshold = sendInterval * resetPositiveThreshold;
// catchup/slowdown smoothly keeps the timeline in sync.
// however, if we get way too far behind/ahead, then catchup/
// slowdown could take 10s, 30s, minutes, to catch up.
// beyond a certain point, it's better to to reset the timeline.
// it'll cause a noticable visual jump, but it's necessary.
if (TimelineReset(drift, absoluteResetNegativeThreshold, absoluteResetPositiveThreshold))
{
// reset timeline to be behind by 'bufferTime'
localTimeline = latestRemoteTime - bufferTime;
// reset average drift. we just reset, so there is no drift.
driftEma.Reset();
// TODO reset snapshots too? old ones are of no value?
}
// timeline reset could have reset drift.
// recalculate it before adjusting timescale.
drift = driftEma.Value - bufferTime;
// next, set localTimescale to catchup consistently in Update().
// we quantize between default/catchup/slowdown,
// this way we have 'default' speed most of the time(!).
// and only catch up / slow down for a little bit occasionally.
// a consistent multiplier would never be exactly 1.0.
localTimescale = Timescale(drift, catchupSpeed, slowdownSpeed, absoluteNegativeThreshold, absolutePositiveThreshold);
localTimescale = Timescale(drift, catchupSpeed, slowdownSpeed, absoluteCatchupNegativeThreshold, absoluteCatchupPositiveThreshold);
// debug logging
// UnityEngine.Debug.Log($"sendInterval={sendInterval:F3} bufferTime={bufferTime:F3} drift={drift:F3} driftEma={driftEma.Value:F3} timescale={localTimescale:F3} deliveryIntervalEma={deliveryTimeEma.Value:F3}");

View File

@ -51,6 +51,13 @@ public class ClientCube : MonoBehaviour
[Range(0, 1)]
public double slowdownSpeed = 0.01f; // 1%
[Header("Snapshot Interpolation: Clamping")]
[Tooltip("If the local timeline is so far behind remote time that catchup would take too long, then we do a hard reset so it won't catch up for a minute or more.")]
public float resetNegativeThreshold = -5; // needs to be larger than catchup threshold
[Tooltip("If the local timeline is so far ahead remote time that slowdown would take too long, then we do a hard reset so it won't slow down for a minute or more.")]
public float resetPositiveThreshold = 5; // needs to be larger than catchup threshold
[Tooltip("Catchup/Slowdown is adjusted over n-second exponential moving average.")]
public int driftEmaDuration = 1; // shouldn't need to modify this, but expose it anyway
@ -144,6 +151,8 @@ public void OnMessage(Snapshot3D snap)
ref driftEma,
catchupNegativeThreshold,
catchupPositiveThreshold,
resetNegativeThreshold,
resetPositiveThreshold,
ref deliveryTimeEma);
}

View File

@ -176,6 +176,8 @@ MonoBehaviour:
catchupPositiveThreshold: 1
catchupSpeed: 0.009999999776482582
slowdownSpeed: 0.009999999776482582
resetNegativeThreshold: -5
resetPositiveThreshold: 5
driftEmaDuration: 1
dynamicAdjustment: 1
dynamicAdjustmentTolerance: 1

View File

@ -34,8 +34,10 @@ public class SnapshotInterpolationTests
// some defaults
const double catchupSpeed = 0.02;
const double slowdownSpeed = 0.04;
const double negativeThresh = -0.10; // in seconds
const double positiveThresh = 0.10; // in seconds
const double catchupNegativeThresh = -0.10; // in seconds
const double catchupPositiveThresh = 0.10; // in seconds
const double resetNegativeThresh = -1.00; // in seconds
const double resetPositiveThresh = 1.00; // in seconds
[SetUp]
public void SetUp()
@ -72,19 +74,39 @@ public void RemoveRange()
public void Timescale()
{
// no drift: linear time
Assert.That(SnapshotInterpolation.Timescale(0, catchupSpeed, slowdownSpeed, negativeThresh, positiveThresh), Is.EqualTo(1.0));
Assert.That(SnapshotInterpolation.Timescale(0, catchupSpeed, slowdownSpeed, catchupNegativeThresh, catchupPositiveThresh), Is.EqualTo(1.0));
// near negative thresh but not under it: linear time
Assert.That(SnapshotInterpolation.Timescale(-0.09, catchupSpeed, slowdownSpeed, negativeThresh, positiveThresh), Is.EqualTo(1.0));
Assert.That(SnapshotInterpolation.Timescale(-0.09, catchupSpeed, slowdownSpeed, catchupNegativeThresh, catchupPositiveThresh), Is.EqualTo(1.0));
// near positive thresh but not above it: linear time
Assert.That(SnapshotInterpolation.Timescale(0.09, catchupSpeed, slowdownSpeed, negativeThresh, positiveThresh), Is.EqualTo(1.0));
Assert.That(SnapshotInterpolation.Timescale(0.09, catchupSpeed, slowdownSpeed, catchupNegativeThresh, catchupPositiveThresh), Is.EqualTo(1.0));
// below negative thresh: catchup
Assert.That(SnapshotInterpolation.Timescale(-0.11, catchupSpeed, slowdownSpeed, negativeThresh, positiveThresh), Is.EqualTo(0.96));
Assert.That(SnapshotInterpolation.Timescale(-0.11, catchupSpeed, slowdownSpeed, catchupNegativeThresh, catchupPositiveThresh), Is.EqualTo(0.96));
// above positive thresh: slowdown
Assert.That(SnapshotInterpolation.Timescale(0.11, catchupSpeed, slowdownSpeed, negativeThresh, positiveThresh), Is.EqualTo(1.02));
Assert.That(SnapshotInterpolation.Timescale(0.11, catchupSpeed, slowdownSpeed, catchupNegativeThresh, catchupPositiveThresh), Is.EqualTo(1.02));
}
[Test]
public void TimelineReset()
{
// no drift. no reset.
Assert.That(SnapshotInterpolation.TimelineReset(0, resetNegativeThresh, resetPositiveThresh), Is.False);
// near negative thresh but not under it. no reset.
Assert.That(SnapshotInterpolation.TimelineReset(-0.99, resetNegativeThresh, resetPositiveThresh), Is.False);
// near positive thresh but not above it. no reset.
Assert.That(SnapshotInterpolation.TimelineReset(0.99, resetNegativeThresh, resetPositiveThresh), Is.False);
// below negative thresh. reset.
Assert.That(SnapshotInterpolation.TimelineReset(-1.01, resetNegativeThresh, resetPositiveThresh), Is.True);
// above positive thresh. reset.
Assert.That(SnapshotInterpolation.TimelineReset(1.01, resetNegativeThresh, resetPositiveThresh), Is.True);
}
[Test]
@ -149,8 +171,8 @@ public void InsertTwice()
double localTimescale = 0;
// insert twice
SnapshotInterpolation.InsertAndAdjust(buffer, snap, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, snap, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, snap, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, -100, 100, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, snap, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, -100, 100, ref deliveryIntervalEma);
// should only be inserted once
Assert.That(buffer.Count, Is.EqualTo(1));
@ -171,8 +193,8 @@ public void Insert_Sorts()
SimpleSnapshot b = new SimpleSnapshot(3, 0, 43);
// insert in reverse order
SnapshotInterpolation.InsertAndAdjust(buffer, b, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, a, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, b, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, -100, 100, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, a, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, -100, 100, ref deliveryIntervalEma);
// should be in sorted order
Assert.That(buffer.Count, Is.EqualTo(2));
@ -195,11 +217,11 @@ public void Insert_InitializesLocalTimeline()
SimpleSnapshot b = new SimpleSnapshot(3, 0, 43);
// first insertion should initialize the local timeline to remote time
SnapshotInterpolation.InsertAndAdjust(buffer, a, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, a, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, -100, 100, ref deliveryIntervalEma);
Assert.That(localTimeline, Is.EqualTo(2));
// second insertion should not modify the timeline again
SnapshotInterpolation.InsertAndAdjust(buffer, b, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, b, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, -100, 100, ref deliveryIntervalEma);
Assert.That(localTimeline, Is.EqualTo(2));
}
@ -219,9 +241,9 @@ public void Insert_ComputesAverageDrift()
SimpleSnapshot c = new SimpleSnapshot(5, 0, 43);
// insert in order
SnapshotInterpolation.InsertAndAdjust(buffer, a, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, b, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, c, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, a, ref localTimeline, ref localTimescale, 1, 0, 0.01, 0.01, ref driftEma, 0, 0, -100, 100, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, b, ref localTimeline, ref localTimescale, 1, 0, 0.01, 0.01, ref driftEma, 0, 0, -100, 100, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, c, ref localTimeline, ref localTimescale, 1, 0, 0.01, 0.01, ref driftEma, 0, 0, -100, 100, ref deliveryIntervalEma);
// first insertion initializes localTime to '2'.
// so the timediffs to '2' are: 0, 1, 3.
@ -245,9 +267,9 @@ public void Insert_ComputesAverageDrift_Scrambled()
SimpleSnapshot c = new SimpleSnapshot(5, 0, 43);
// insert scrambled (not in order)
SnapshotInterpolation.InsertAndAdjust(buffer, a, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, c, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, b, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, a, ref localTimeline, ref localTimescale, 1, 0, 0.01, 0.01, ref driftEma, 0, 0, -100, 100, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, c, ref localTimeline, ref localTimescale, 1, 0, 0.01, 0.01, ref driftEma, 0, 0, -100, 100, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, b, ref localTimeline, ref localTimescale, 1, 0, 0.01, 0.01, ref driftEma, 0, 0, -100, 100, ref deliveryIntervalEma);
// first insertion initializes localTime to '2'.
// so the timediffs to '2' are: 0, 3, 1.
@ -276,9 +298,9 @@ public void Insert_ComputesAverageDeliveryInterval()
SimpleSnapshot c = new SimpleSnapshot(5, 6, 43);
// insert in order
SnapshotInterpolation.InsertAndAdjust(buffer, a, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, b, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, c, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, a, ref localTimeline, ref localTimescale, 1, 0, 0.01, 0.01, ref driftEma, 0, 0, -100, 100, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, b, ref localTimeline, ref localTimescale, 1, 0, 0.01, 0.01, ref driftEma, 0, 0, -100, 100, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, c, ref localTimeline, ref localTimescale, 1, 0, 0.01, 0.01, ref driftEma, 0, 0, -100, 100, ref deliveryIntervalEma);
// first insertion doesn't compute delivery interval because we need 2 snaps.
// second insertion computes 4-3 = 1
@ -307,9 +329,9 @@ public void Insert_ComputesAverageDeliveryInterval_Scrambled()
SimpleSnapshot c = new SimpleSnapshot(5, 6, 43);
// insert in order
SnapshotInterpolation.InsertAndAdjust(buffer, a, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, c, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, b, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, a, ref localTimeline, ref localTimescale, 1, 0, 0.01, 0.01, ref driftEma, 0, 0, -100, 100, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, c, ref localTimeline, ref localTimescale, 1, 0, 0.01, 0.01, ref driftEma, 0, 0, -100, 100, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, b, ref localTimeline, ref localTimescale, 1, 0, 0.01, 0.01, ref driftEma, 0, 0, -100, 100, ref deliveryIntervalEma);
// first insertion doesn't compute delivery interval because we need 2 snaps.
// second insertion computes 4-3 = 1
@ -334,9 +356,9 @@ public void Sample()
SimpleSnapshot a = new SimpleSnapshot(10, 0, 42);
SimpleSnapshot b = new SimpleSnapshot(20, 0, 43);
SimpleSnapshot c = new SimpleSnapshot(30, 0, 44);
SnapshotInterpolation.InsertAndAdjust(buffer, a, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, b, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, c, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, a, ref localTimeline, ref localTimescale, 1, 0, 0.01, 0.01, ref driftEma, 0, 0, -100, 100, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, b, ref localTimeline, ref localTimescale, 1, 0, 0.01, 0.01, ref driftEma, 0, 0, -100, 100, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, c, ref localTimeline, ref localTimescale, 1, 0, 0.01, 0.01, ref driftEma, 0, 0, -100, 100, ref deliveryIntervalEma);
// sample at a time before the first snapshot
SnapshotInterpolation.Sample(buffer, 9, out int from, out int to, out double t);
@ -372,9 +394,9 @@ public void Step()
SimpleSnapshot b = new SimpleSnapshot(20, 0, 43);
SimpleSnapshot c = new SimpleSnapshot(30, 0, 44);
SnapshotInterpolation.InsertAndAdjust(buffer, a, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, b, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, c, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, a, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, -100, 100, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, b, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, -100, 100, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, c, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, -100, 100, ref deliveryIntervalEma);
// step half way to the next snapshot
SnapshotInterpolation.Step(buffer, 5, ref localTimeline, localTimescale, out SimpleSnapshot fromSnapshot, out SimpleSnapshot toSnapshot, out double t);
@ -397,9 +419,9 @@ public void Step_RemovesOld()
SimpleSnapshot b = new SimpleSnapshot(20, 0, 43);
SimpleSnapshot c = new SimpleSnapshot(30, 0, 44);
SnapshotInterpolation.InsertAndAdjust(buffer, a, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, b, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, c, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, a, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, -100, 100, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, b, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, -100, 100, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, c, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, -100, 100, ref deliveryIntervalEma);
// step 1.5 snapshots worth, so way past the first one
SnapshotInterpolation.Step(buffer, 15, ref localTimeline, localTimescale, out SimpleSnapshot fromSnapshot, out SimpleSnapshot toSnapshot, out double t);