ClockDelta.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. # ClockDelta provides the ability to use clock synchronization for
  2. # distributed objects
  3. from pandac.PandaModules import *
  4. from direct.directnotify import DirectNotifyGlobal
  5. from direct.showbase import DirectObject
  6. import math
  7. # The following two parameters, NetworkTimeBits and
  8. # NetworkTimePrecision, define the number of bits required to store a
  9. # network time, and the number of ticks per second it represents,
  10. # respectively. The tradeoff is the longest period of elapsed time we
  11. # can measure, vs. the precision with which we can measure it.
  12. # 16 and 100 give us precision to 1/100th of a second, with a range of
  13. # +/- 5 minutes in a 16-bit integer. These are eminently tweakable,
  14. # but the parameter types in toon.dc must match the number of bits
  15. # specified here (i.e. int16 if NetworkTimeBits is 16; int32 if
  16. # NetworkTimeBits is 32).
  17. NetworkTimeBits = 16
  18. NetworkTimePrecision = 100.0
  19. # These values are derived from the above.
  20. NetworkTimeMask = (1 << NetworkTimeBits) - 1
  21. NetworkTimeTopBits = 32 - NetworkTimeBits
  22. MaxTimeDelta = (NetworkTimeMask / 2.0) / NetworkTimePrecision
  23. # This is the maximum number of seconds by which we expect our clock
  24. # (or the server's clock) to drift over an hour.
  25. ClockDriftPerHour = 1.0 # Is this generous enough?
  26. # And the above, scaled into a per-second value.
  27. ClockDriftPerSecond = ClockDriftPerHour / 3600.0
  28. # How many seconds to insist on waiting before accepting a second
  29. # resync request from another client.
  30. P2PResyncDelay = 10.0
  31. class ClockDelta(DirectObject.DirectObject):
  32. """
  33. The ClockDelta object converts between universal ("network") time,
  34. which is used for all network traffic, and local time (e.g. as
  35. returned by getFrameTime() or getRealTime()), which is used for
  36. everything else.
  37. """
  38. notify = DirectNotifyGlobal.directNotify.newCategory('ClockDelta')
  39. def __init__(self):
  40. self.globalClock = ClockObject.getGlobalClock()
  41. # self.delta is the relative delta from our clock to the
  42. # server's clock.
  43. self.delta = 0
  44. # self.uncertainty represents the number of seconds plus or
  45. # minus in which we are confident our delta matches the
  46. # server's actual time. The initial value, None, represents
  47. # infinity--we have no idea.
  48. self.uncertainty = None
  49. # self.lastResync is the time at which self.uncertainty
  50. # was measured. It is important to remember because our
  51. # uncertainty increases over time (due to relative clock
  52. # drift).
  53. self.lastResync = 0.0
  54. self.accept("resetClock", self.__resetClock)
  55. def getDelta(self):
  56. return self.delta
  57. def getUncertainty(self):
  58. # Returns our current uncertainty with our clock measurement,
  59. # as a number of seconds plus or minus. Returns None,
  60. # representing infinite uncertainty, if we have never received
  61. # a time measurement.
  62. if self.uncertainty == None:
  63. return None
  64. now = self.globalClock.getRealTime()
  65. elapsed = now - self.lastResync
  66. return self.uncertainty + elapsed * ClockDriftPerSecond
  67. def getLastResync(self):
  68. # Returns the local time at which we last resynchronized the
  69. # clock delta.
  70. return self.lastResync
  71. def __resetClock(self, timeDelta):
  72. """
  73. this is called when the global clock gets adjusted
  74. timeDelta is equal to the amount of time, in seconds,
  75. that has been added to the global clock
  76. """
  77. assert(self.notify.debug("adjusting timebase by %f seconds" % timeDelta))
  78. # adjust our timebase by the same amount
  79. self.delta += timeDelta
  80. def clear(self):
  81. """
  82. Throws away any previous synchronization information.
  83. """
  84. self.delta = 0
  85. self.uncertainty = None
  86. self.lastResync = 0.0
  87. def resynchronize(self, localTime, networkTime, newUncertainty,
  88. trustNew = 1):
  89. """resynchronize(self, float localTime, int32 networkTime,
  90. float newUncertainty)
  91. Accepts a new networkTime value, which is understood to
  92. represent the same moment as localTime, plus or minus
  93. uncertainty seconds. Improves our current notion of the time
  94. delta accordingly.
  95. """
  96. newDelta = (float(localTime) -
  97. (float(networkTime) / NetworkTimePrecision))
  98. self.newDelta(localTime, newDelta, newUncertainty)
  99. def peerToPeerResync(self, avId, timestamp, serverTime, uncertainty):
  100. """
  101. Accepts an AI time and uncertainty value from another client,
  102. along with a local timestamp value of the message from this
  103. client which prompted the other client to send us its delta
  104. information.
  105. The return value is true if the other client's measurement was
  106. reasonably close to our own, or false if the other client's
  107. time estimate was wildly divergent from our own; the return
  108. value is negative if the test was not even considered (because
  109. it happened too soon after another recent request).
  110. """
  111. now = self.globalClock.getRealTime()
  112. if now - self.lastResync < P2PResyncDelay:
  113. # We can't process this request; it came in on the heels
  114. # of some other request, and our local timestamp may have
  115. # been resynced since then: ergo, the timestamp in this
  116. # request is meaningless.
  117. assert(self.notify.debug("Ignoring request for resync from %s within %.3f s." % (avId, now - self.lastResync)))
  118. return -1
  119. # The timestamp value will be a timestamp that we sent out
  120. # previously, echoed back to us. Therefore we can confidently
  121. # convert it back into our local time, even though we suspect
  122. # our clock delta might be off.
  123. local = self.networkToLocalTime(timestamp, now)
  124. elapsed = now - local
  125. delta = (local + now) / 2.0 - serverTime
  126. gotSync = 0
  127. if elapsed <= 0 or elapsed > P2PResyncDelay:
  128. # The elapsed time must be positive (the local timestamp
  129. # must be in the past), and shouldn't be more than
  130. # P2PResyncDelay. If it does not meet these requirements,
  131. # it must be very old indeed, or someone is playing tricks
  132. # on us.
  133. self.notify.info("Ignoring old request for resync from %s." % (avId))
  134. else:
  135. # Now the other client has told us his delta and uncertainty
  136. # information, which was generated somewhere in the range
  137. # [-elapsed, 0] seconds ago. That means our complete window
  138. # is wider by that amount.
  139. self.notify.info("Got sync +/- %.3f s, elapsed %.3f s, from %s." % (uncertainty, elapsed, avId))
  140. delta -= elapsed / 2.0
  141. uncertainty += elapsed / 2.0
  142. gotSync = self.newDelta(local, delta, uncertainty, trustNew = 0)
  143. return gotSync
  144. def newDelta(self, localTime, newDelta, newUncertainty,
  145. trustNew = 1):
  146. """
  147. Accepts a new delta and uncertainty pair, understood to
  148. represent time as of localTime. Improves our current notion
  149. of the time delta accordingly. The return value is true if
  150. the new measurement was used, false if it was discarded.
  151. """
  152. oldUncertainty = self.getUncertainty()
  153. if oldUncertainty != None:
  154. assert(self.notify.debug('previous delta at %.3f s, +/- %.3f s.' % (self.delta, oldUncertainty)))
  155. assert(self.notify.debug('new delta at %.3f s, +/- %.3f s.' % (newDelta, newUncertainty)))
  156. # Our previous measurement was self.delta +/- oldUncertainty;
  157. # our new measurement is newDelta +/- newUncertainty. Take
  158. # the intersection of both.
  159. oldLow = self.delta - oldUncertainty
  160. oldHigh = self.delta + oldUncertainty
  161. newLow = newDelta - newUncertainty
  162. newHigh = newDelta + newUncertainty
  163. low = max(oldLow, newLow)
  164. high = min(oldHigh, newHigh)
  165. # If there is no intersection, whoops! Either the old
  166. # measurement or the new measurement is completely wrong.
  167. if low > high:
  168. if not trustNew:
  169. self.notify.info('discarding new delta.')
  170. return 0
  171. self.notify.info('discarding previous delta.')
  172. else:
  173. newDelta = (low + high) / 2.0
  174. newUncertainty = (high - low) / 2.0
  175. assert(self.notify.debug('intersection at %.3f s, +/- %.3f s.' % (newDelta, newUncertainty)))
  176. self.delta = newDelta
  177. self.uncertainty = newUncertainty
  178. self.lastResync = localTime
  179. return 1
  180. ### Primary interface functions ###
  181. def networkToLocalTime(self, networkTime, now = None, bits = 16,
  182. ticksPerSec=NetworkTimePrecision):
  183. """networkToLocalTime(self, int networkTime)
  184. Converts the indicated networkTime to the corresponding
  185. localTime value. The time is assumed to be within +/- 5
  186. minutes of the current local time given in now, or
  187. getRealTime() if now is not specified.
  188. """
  189. if now == None:
  190. now = self.globalClock.getRealTime()
  191. # Are we in non-real-time mode (i.e. filming a movie)? Just return now
  192. if self.globalClock.getMode() == ClockObject.MNonRealTime:
  193. return now
  194. # First, determine what network time we have for 'now'.
  195. ntime = int(math.floor(((now - self.delta) * ticksPerSec) + 0.5))
  196. # The signed difference between these is the number of ticks
  197. # by which the network time differs from 'now'.
  198. if bits == 16:
  199. diff = self.__signExtend(networkTime - ntime)
  200. else:
  201. # Assume the bits is either 16 or 32. If it's 32, no need
  202. # to sign-extend. 32 bits gives us about 227 days of
  203. # continuous timestamp.
  204. diff = networkTime - ntime
  205. return now + float(diff) / ticksPerSec
  206. def localToNetworkTime(self, localTime, bits = 16,
  207. ticksPerSec=NetworkTimePrecision):
  208. """localToNetworkTime(self, float localTime)
  209. Converts the indicated localTime to the corresponding
  210. networkTime value.
  211. """
  212. ntime = int(math.floor(((localTime - self.delta) * ticksPerSec) + 0.5))
  213. if bits == 16:
  214. return self.__signExtend(ntime)
  215. else:
  216. # Assume the bits is either 16 or 32. If it's 32, no need
  217. # to sign-extend. 32 bits gives us about 227 days of
  218. # continuous timestamp.
  219. return ntime
  220. ### Convenience functions ###
  221. def getRealNetworkTime(self, bits=16,
  222. ticksPerSec=NetworkTimePrecision):
  223. """getRealNetworkTime(self)
  224. Returns the current getRealTime() expressed as a network time.
  225. """
  226. return self.localToNetworkTime(self.globalClock.getRealTime(),
  227. bits=bits,
  228. ticksPerSec=ticksPerSec)
  229. def getFrameNetworkTime(self, bits=16,
  230. ticksPerSec=NetworkTimePrecision):
  231. """getFrameNetworkTime(self)
  232. Returns the current getFrameTime() expressed as a network time.
  233. """
  234. return self.localToNetworkTime(self.globalClock.getFrameTime(),
  235. bits=bits,
  236. ticksPerSec=ticksPerSec)
  237. def localElapsedTime(self, networkTime, bits=16,
  238. ticksPerSec=NetworkTimePrecision):
  239. """localElapsedTime(self, int networkTime)
  240. Returns the amount of time elapsed (in seconds) on the client
  241. since the server message was sent. Negative values are
  242. clamped to zero.
  243. """
  244. now = self.globalClock.getFrameTime()
  245. dt = now - self.networkToLocalTime(networkTime, now, bits=bits,
  246. ticksPerSec=ticksPerSec)
  247. return max(dt, 0.0)
  248. ### Private functions ###
  249. def __signExtend(self, networkTime):
  250. """__signExtend(self, int networkTime)
  251. Preserves the lower NetworkTimeBits of the networkTime value,
  252. and extends the sign bit all the way up.
  253. """
  254. return ((networkTime & NetworkTimeMask) << NetworkTimeTopBits) >> NetworkTimeTopBits
  255. globalClockDelta = ClockDelta()