Browse Source

better clock synchronization, including p2p sync

David Rose 22 years ago
parent
commit
aec3aa147c

+ 1 - 1
direct/src/distributed/ClientRepository.py

@@ -59,7 +59,7 @@ class ClientRepository(ConnectionRepository.ConnectionRepository):
         precisely measured and may drift slightly after startup, but
         precisely measured and may drift slightly after startup, but
         it should be accurate plus or minus a couple of seconds.
         it should be accurate plus or minus a couple of seconds.
         """
         """
-        return time.time() + self.cr.getServerDelta()
+        return time.time() + self.serverDelta
 
 
     def parseDcFile(self, dcFileName):
     def parseDcFile(self, dcFileName):
         self.dcFile = DCFile()
         self.dcFile = DCFile()

+ 161 - 15
direct/src/distributed/ClockDelta.py

@@ -24,6 +24,18 @@ NetworkTimePrecision = 100.0
 # These values are derived from the above.
 # These values are derived from the above.
 NetworkTimeMask = (1 << NetworkTimeBits) - 1
 NetworkTimeMask = (1 << NetworkTimeBits) - 1
 NetworkTimeTopBits = 32 - NetworkTimeBits
 NetworkTimeTopBits = 32 - NetworkTimeBits
+MaxTimeDelta = (NetworkTimeMask / 2.0) / NetworkTimePrecision
+
+# This is the maximum number of seconds by which we expect our clock
+# (or the server's clock) to drift over an hour.
+ClockDriftPerHour = 1.0   # Is this generous enough?
+
+# And the above, scaled into a per-second value.
+ClockDriftPerSecond = ClockDriftPerHour / 3600.0
+
+# How many seconds to insist on waiting before accepting a second
+# resync request from another client.
+P2PResyncDelay = 10.0
 
 
 class ClockDelta(DirectObject.DirectObject):
 class ClockDelta(DirectObject.DirectObject):
     """
     """
@@ -38,33 +50,171 @@ class ClockDelta(DirectObject.DirectObject):
     def __init__(self):
     def __init__(self):
         self.globalClock = ClockObject.getGlobalClock()
         self.globalClock = ClockObject.getGlobalClock()
 
 
+        # self.delta is the relative delta from our clock to the
+        # server's clock.
         self.delta = 0
         self.delta = 0
+
+        # self.uncertainty represents the number of seconds plus or
+        # minus in which we are confident our delta matches the
+        # server's actual time.  The initial value, None, represents
+        # infinity--we have no idea.
+        self.uncertainty = None
+
+        # self.lastResync is the time at which self.uncertainty
+        # was measured.  It is important to remember because our
+        # uncertainty increases over time (due to relative clock
+        # drift).
+        self.lastResync = 0.0
+        
         self.accept("resetClock", self.__resetClock)
         self.accept("resetClock", self.__resetClock)
 
 
+    def getDelta(self):
+        return self.delta
+
+    def getUncertainty(self):
+        # Returns our current uncertainty with our clock measurement,
+        # as a number of seconds plus or minus.  Returns None,
+        # representing infinite uncertainty, if we have never received
+        # a time measurement.
+        
+        if self.uncertainty == None:
+            return None
+        
+        now = self.globalClock.getRealTime()
+        elapsed = now - self.lastResync
+        return self.uncertainty + elapsed * ClockDriftPerSecond
+
+    def getLastResync(self):
+        # Returns the local time at which we last resynchronized the
+        # clock delta.
+        return self.lastResync
+    
     def __resetClock(self, timeDelta):
     def __resetClock(self, timeDelta):
         """
         """
         this is called when the global clock gets adjusted
         this is called when the global clock gets adjusted
         timeDelta is equal to the amount of time, in seconds,
         timeDelta is equal to the amount of time, in seconds,
         that has been added to the global clock
         that has been added to the global clock
         """
         """
-        self.notify.debug("adjusting timebase by %f seconds" % timeDelta)
+        assert(self.notify.debug("adjusting timebase by %f seconds" % timeDelta))
         # adjust our timebase by the same amount
         # adjust our timebase by the same amount
         self.delta += timeDelta
         self.delta += timeDelta
 
 
-    def resynchronize(self, localTime, networkTime):
-        """resynchronize(self, float localTime, uint32 networkTime)
-
-        Resets the relative delta so that the indicated networkTime
-        and localTime map to the same instant.  The return value is
-        the amount by which the clock changes, in seconds.
+    def clear(self):
+        """
+        Throws away any previous synchronization information.
+        """
+        self.delta = 0
+        self.uncertainty = None
+        self.lastResync = 0.0
+
+    def resynchronize(self, localTime, networkTime, newUncertainty,
+                      trustNew = 1):
+        """resynchronize(self, float localTime, int32 networkTime,
+                         float newUncertainty)
+
+        Accepts a new networkTime value, which is understood to
+        represent the same moment as localTime, plus or minus
+        uncertainty seconds.  Improves our current notion of the time
+        delta accordingly.
         """
         """
         newDelta = (float(localTime) -
         newDelta = (float(localTime) -
                     (float(networkTime) / NetworkTimePrecision))
                     (float(networkTime) / NetworkTimePrecision))
-        change = newDelta - self.delta
-        self.delta = newDelta
+        self.newDelta(localTime, newDelta, newUncertainty)
 
 
-        return change
+    def peerToPeerResync(self, avId, timestamp, serverTime, uncertainty):
+        """
+        Accepts an AI time and uncertainty value from another client,
+        along with a local timestamp value of the message from this
+        client which prompted the other client to send us its delta
+        information.
+
+        The return value is true if the other client's measurement was
+        reasonably close to our own, or false if the other client's
+        time estimate was wildly divergent from our own; the return
+        value is negative if the test was not even considered (because
+        it happened too soon after another recent request).
+        """
+        
+        now = self.globalClock.getRealTime()
+        if now - self.lastResync < P2PResyncDelay:
+            # We can't process this request; it came in on the heels
+            # of some other request, and our local timestamp may have
+            # been resynced since then: ergo, the timestamp in this
+            # request is meaningless.
+            assert(self.notify.debug("Ignoring request for resync from %s within %.3f s." % (avId, now - self.lastResync)))
+            return -1
+            
+        # The timestamp value will be a timestamp that we sent out
+        # previously, echoed back to us.  Therefore we can confidently
+        # convert it back into our local time, even though we suspect
+        # our clock delta might be off.
+        local = self.networkToLocalTime(timestamp, now)
+        elapsed = now - local
+        delta = (local + now) / 2.0 - serverTime
+
+        gotSync = 0
+        if elapsed <= 0 or elapsed > P2PResyncDelay:
+            # The elapsed time must be positive (the local timestamp
+            # must be in the past), and shouldn't be more than
+            # P2PResyncDelay.  If it does not meet these requirements,
+            # it must be very old indeed, or someone is playing tricks
+            # on us.
+            self.notify.info("Ignoring old request for resync from %s." % (avId))
+        else:
+            # Now the other client has told us his delta and uncertainty
+            # information, which was generated somewhere in the range
+            # [-elapsed, 0] seconds ago.  That means our complete window
+            # is wider by that amount.
+            self.notify.info("Got sync +/- %.3f s, elapsed %.3f s, from %s." % (uncertainty, elapsed, avId))
+            delta -= elapsed / 2.0
+            uncertainty += elapsed / 2.0
+        
+            gotSync = self.newDelta(local, delta, uncertainty, trustNew = 0)
 
 
+        return gotSync
+
+    def newDelta(self, localTime, newDelta, newUncertainty,
+                 trustNew = 1):
+        """
+        Accepts a new delta and uncertainty pair, understood to
+        represent time as of localTime.  Improves our current notion
+        of the time delta accordingly.  The return value is true if
+        the new measurement was used, false if it was discarded.
+        """
+        oldUncertainty = self.getUncertainty()
+        if oldUncertainty != None:
+            assert(self.notify.debug('previous delta at %.3f s, +/- %.3f s.' % (self.delta, oldUncertainty)))
+            assert(self.notify.debug('new delta at %.3f s, +/- %.3f s.' % (newDelta, newUncertainty)))
+            # Our previous measurement was self.delta +/- oldUncertainty;
+            # our new measurement is newDelta +/- newUncertainty.  Take
+            # the intersection of both.
+            
+            oldLow = self.delta - oldUncertainty
+            oldHigh = self.delta + oldUncertainty
+            newLow = newDelta - newUncertainty
+            newHigh = newDelta + newUncertainty
+
+            low = max(oldLow, newLow)
+            high = min(oldHigh, newHigh)
+                      
+            # If there is no intersection, whoops!  Either the old
+            # measurement or the new measurement is completely wrong.
+            if low > high:
+                if not trustNew:
+                    self.notify.info('discarding new delta.')
+                    return 0
+                
+                self.notify.info('discarding previous delta.')
+            else:
+                newDelta = (low + high) / 2.0
+                newUncertainty = (high - low) / 2.0
+                assert(self.notify.debug('intersection at %.3f s, +/- %.3f s.' % (newDelta, newUncertainty)))
+        
+        self.delta = newDelta
+        self.uncertainty = newUncertainty
+        self.lastResync = localTime
+
+        return 1
 
 
     ### Primary interface functions ###
     ### Primary interface functions ###
 
 
@@ -147,11 +297,7 @@ class ClockDelta(DirectObject.DirectObject):
         now = self.globalClock.getFrameTime()
         now = self.globalClock.getFrameTime()
         dt = now - self.networkToLocalTime(networkTime, now, bits=bits)
         dt = now - self.networkToLocalTime(networkTime, now, bits=bits)
 
 
-        if (dt >= 0.0):
-            return dt
-        else:
-            self.notify.debug('negative clock delta: %.3f' % dt)
-            return 0.0
+        return max(dt, 0.0)
 
 
 
 
 
 

+ 46 - 11
direct/src/distributed/DistributedSmoothNode.py

@@ -283,17 +283,20 @@ class DistributedSmoothNode(DistributedNode.DistributedNode):
         
         
         now = globalClock.getFrameTime()
         now = globalClock.getFrameTime()
         local = globalClockDelta.networkToLocalTime(timestamp, now)
         local = globalClockDelta.networkToLocalTime(timestamp, now)
-        chug = globalClock.getRealTime() - now
+        real = globalClock.getRealTime()
+        chug = real - now
 
 
         # Sanity check the timestamp from the other avatar.  It should
         # Sanity check the timestamp from the other avatar.  It should
         # be just slightly in the past, but it might be off by as much
         # be just slightly in the past, but it might be off by as much
         # as this frame's amount of time forward or back.
         # as this frame's amount of time forward or back.
         howFarFuture = local - now
         howFarFuture = local - now
         if howFarFuture - chug >= MaxFuture:
         if howFarFuture - chug >= MaxFuture:
-            # Too far off; resync both of us.
-            if self.cr.timeManager != None:
-                self.cr.timeManager.synchronize("Packets from %d off by %.1f s" % (self.doId, howFarFuture))
-            self.d_suggestResync(self.cr.localToonDoId)
+            # Too far off; advise the other client of our clock information.
+            if globalClockDelta.getUncertainty() != None:
+                self.d_suggestResync(self.cr.localToonDoId, timestamp,
+                                     globalClockDelta.getRealNetworkTime(),
+                                     real - globalClockDelta.getDelta(),
+                                     globalClockDelta.getUncertainty())
         
         
         self.smoother.setTimestamp(local)
         self.smoother.setTimestamp(local)
         self.smoother.markPosition()
         self.smoother.markPosition()
@@ -334,16 +337,48 @@ class DistributedSmoothNode(DistributedNode.DistributedNode):
 
 
     ### Monitor clock sync ###
     ### Monitor clock sync ###
 
 
-    def d_suggestResync(self, avId):
-        self.sendUpdate("suggestResync", [avId])
+    def d_suggestResync(self, avId, timestampA, timestampB, serverTime, uncertainty):
+        self.sendUpdate("suggestResync", [avId, timestampA, timestampB, serverTime, uncertainty])
         
         
-    def suggestResync(self, avId):
+    def suggestResync(self, avId, timestampA, timestampB, serverTime, uncertainty):
         """suggestResync(self, avId)
         """suggestResync(self, avId)
 
 
         This message is sent from one client to another when the other
         This message is sent from one client to another when the other
         client receives a timestamp from this client that is so far
         client receives a timestamp from this client that is so far
         out of date as to suggest that one or both clients needs to
         out of date as to suggest that one or both clients needs to
-        resynchronize with the AI.
+        resynchronize their clock information.
+        """
+        result = \
+               self.peerToPeerResync(avId, timestampA, serverTime, uncertainty)
+        if result >= 0 and \
+           globalClockDelta.getUncertainty() != None:
+            other = self.cr.doId2do.get(avId)
+            if other and hasattr(other, "d_returnResync"):
+                real = globalClock.getRealTime()
+                other.d_returnResync(self.cr.localToonDoId, timestampB,
+                                     real - globalClockDelta.getDelta(),
+                                     globalClockDelta.getUncertainty())
+        
+
+    def d_returnResync(self, avId, timestampB, serverTime, uncertainty):
+        self.sendUpdate("returnResync", [avId, timestampB, serverTime, uncertainty])
+        
+    def returnResync(self, avId, timestampB, serverTime, uncertainty):
+        """returnResync(self, avId)
+
+        A reply sent by a client whom we recently sent suggestResync
+        to, this reports the client's new delta information so we can
+        adjust our clock as well.
         """
         """
-        if self.cr.timeManager != None:
-            self.cr.timeManager.synchronize("suggested by %d" % (avId))
+        self.peerToPeerResync(avId, timestampB, serverTime, uncertainty)
+
+    def peerToPeerResync(self, avId, timestamp, serverTime, uncertainty):
+        gotSync = globalClockDelta.peerToPeerResync(avId, timestamp, serverTime, uncertainty)
+
+        # If we didn't get anything useful from the other client,
+        # maybe our clock is just completely hosed.  Go ask the AI.
+        if not gotSync:
+            if self.cr.timeManager != None:
+                self.cr.timeManager.synchronize("suggested by %d" % (avId))
+
+        return gotSync