Last week I explained how Source simulates time for game code. In both SourceMod and AMX Mod X, there exists a timer system based off the “game time.” Each active timer has an interval and a next execute time.
The algorithm for a timer, on both systems, is:
IF next_execute < game_time THEN RUN TIMER next_execute = game_time + interval END IF
That is, until someone filed an interesting report. The user created two timers: a 30 second timer, and a 1 second timer that kept repeating. Both timers printed messages each execution. The result looked something like this:
Timer 2: iteration 25 Timer 2: iteration 26 Timer 2: iteration 27 Timer 1: iteration 1 Timer 2: iteration 28 Timer 2: iteration 29 Timer 2: iteration 30
What happened? The two timers weren't syncing up; you would expect both the thirtieth iteration of the second timer and the first iteration of the first timer to happen at the same time. The reason is pretty simple. SourceMod (and AMX Mod X) both guarantee a minimum accuracy of 0.1 seconds. As an optimization, they only process the timer list every 0.1 seconds, using the same algorithm as described above. This guarantees a minimum of 0.1 second accuracy.
However, 0.1 isn't nicely divisible by the tickrate all the time. For example, it takes four 30ms ticks to reach 0.1 seconds, but 4*0.03 = 0.12 seconds. Thus, every time SourceMod was processing timers, it was compounding a small margin of error. For example, below is a progression against a 30ms tick rate.
- t+00.000: Wait until t+00.010
- t+00.003, t+00.006, t+00.009
- t+00.012: Wait until t+00.022
- t+00.015, t+00.018, t+00.021
- t+00.024: Wait until t+00.034
- t+00.027, t+00.030, t+00.033
- t+00.036: Wait until t+00.046
- t+00.048: Wait until t+00.058
- t+00.060: Wait until t+00.070
For a one-shot timer, that's not a problem. But for a repeatable timer, it means there will be no compensation for the steady drift. Continuing that logic, a 1s timer actually executes near at most t+1.08, a full .08s of drift. After 27 iterations, that drift is 27*0.08, or a full 2 seconds!
The correct algorithm would be:
- t+00.000: Wait until t+00.010
- t+00.003, t+00.006, t+00.009
- t+00.012: Wait until t+00.020
- t+00.015, t+00.018
- t+00.021: Wait until t+00.030
- t+00.024, t+00.027
- t+00.030: Wait until t+00.040
- t+00.033, t+00.036, t+00.039
- t+00.042: Wait until t+00.050
- t+00.051: Wait until t+00.060
In other words, the correct code is:
IF next_execute < game_time THEN RUN TIMER next_execute = next_execute + interval END IF
The difference being that basing the next time from the last time, instead of the current time, removes the compounding of the error. PM discovered this little trick, though it seemed strange at first, it's self correcting. It works as long as your desired accuracy is greater than the actual accuracy, and it's cheaper than manually computing a margin of error. Source will never tick at greater than 100ms, so SourceMod's 0.1 second guarantee is safe. Note the actual error itself isn't removed -- timers can still have around 0.02s of inaccuracy total, depending on the tick rate.
As for why we ever wrote this code in the first place -- it probably came straight from AMX Mod X. I am unsure as to why AMX Mod X did it, but perhaps there was a reason long ago.
I think that we did that in AMX Mod X because we coded what first came to our mind (I think I wrote that original code and I wasn’t too experienced back then). I just looked it up and found out that in the original version of the code, OLO used current game time + interval. I must have misinterpreted that when I changed the task system to be more programmer friendly. So I’m afraid that this bug originates in my fault :D
whoah this weblog is excellent i love studying your articles.
Keep up the great work! You already know, many persons are hunting
round for this info, you could help them greatly.
My brother suggested I may like this website. He was totally right.
This post actually made my day. You cann’t believe
just how much time I had spent for this info!
Thanks!