Solution to MilliSecs integer wrap?

BlitzMax Forums/BlitzMax Programming/Solution to MilliSecs integer wrap?

BlitzSupport(Posted 2009) [#1]
As you may know, MilliSecs () can wrap from the highest integer value possible, 2147483647, to the lowest integer value, -2147483648, at any time, up to a maximum of 49 days, potentially causing weird effects in game timing, and potentially disastrous effects for long-running applications/server processes, etc.

This is my attempt to deal with it -- a timer returning a Long value that starts from zero and counts upwards for around, um, 220 million years, if my calculations are correct, which is not necessarily true.

Important! Any direct comparisons with GameTime's result should use Long values, otherwise you may run into MilliSecs-type wrap effects, missing the point completely! Eg.


' Untested -- don't try to run this as-is!

ResetGameTime ()

ticks:Long = GameTime () ' Store current time in a Long

Repeat

    ' Check one second has elapsed...

    If GameTime () > ticks + 1000:Long ' Make literal value Long

        Print "One second passed..."

        ticks = GameTime () ' Store new time

    EndIf

Until KeyHit (KEY_ESCAPE)

End


I have a vague feeling I haven't thought about it enough and it'll be shown up as an embarrassing failure, so please let me know if I've done something stupid here! If it's deemed to be valid I'll stick it in the Code Archives.

To use it, call ResetGameTime at the start of your code (this is mandatory) and simply use GameTime as a direct replacement for MilliSecs ().

Code and demo...



This code shows a fake MilliSecs () value wrapping around and GameTime dealing with it...



... and, not that it matters, this is how I came up with 220 million years! Please let me know if I've screwed this up...




TaskMaster(Posted 2009) [#2]
I did not take a close look at this, but does it handle the case where Millisecs() is already negative when your program starts?


TaskMaster(Posted 2009) [#3]
Just for simplicity, let's pretend our integer wraps at 500 to make the math easier to visualize.

We have an integer that counts up to 500, then flips to -501, then counts back up. So, the counting looks like this:

0, 1, 2 ... 499, 500, -501, -500, -499 ... -2, -1, 0, 1, 2 ...

Your math is doing this:

Now=Millisecs()
GameTime = GameTime + (Now - Last)
Last=Now

So, if the current time is 500 and you go 1 more tick, you get this:

Now = -501
GameTime = 500 + (-501 - 500)

Which should return 500 + (-1001) = -501, correct? But, we expect 501. Or am I missing something?


Floyd(Posted 2009) [#4]
There is rarely any need for huge timer values. You are usually interested in time intervals and comparing times, e.g. has one second elapsed.

The correct way to compare times is by subtraction.

If time2 >= time1 + 1000 Then do_something   ; wrong if wrap-around occurs
If time2 - time1 >= 1000 Then do_something   ; wrap-around doesn't matter.

Wrap-to-negative affects this only if the interval (time2 - time1) reaches 2^31 milliseconds.

In the "wrap at 500" example the valid numbers are -501 to +500. Numbers that differ by a multiple of 1002 are equal, e.g. -501 = +501. Thus -1001 is really -1001 + 1002 = +1.


Tachyon(Posted 2009) [#5]
This millisec-int-wrap issue has caused me problems in Book 1. Thank you for attempting to address it!


BlitzSupport(Posted 2009) [#6]

I did not take a close look at this, but does it handle the case where Millisecs() is already negative when your program starts?


Yep, seems to -- you can change SetMSecs (MAXI - 250) in the second demo to a negative number to try it.

With regards to the second query, I think this is partly because your 'Now' and 'Last' aren't being integer-wrapped, whereas in my code they're limited to Int values (which I think works out the same as Floyd's explanation in the end) -- try this:


' GameTime + (Now - Last)

' Your test (negative result):
Print 2147483647:Long + (-2147483648:Long - 2147483647:Long)

' What I'm doing (positive result of 2147483648, the next number outside Int range):

Print 2147483647:Long + (-2147483648 - 2147483647) ' Ints!




There is rarely any need for huge timer values. You are usually interested in time intervals and comparing times, e.g. has one second elapsed.


Yep, but the main point for me was not to have to worry about dodgy wrapping effects at all (this whole area causes me intense confusion once I start thinking about it, trivial as it may seem), as well as just being a convenient way to start/reset my ticks from zero.


TaskMaster(Posted 2009) [#7]
OK, BlitzSupport's explanation made this reply null and void...


TaskMaster(Posted 2009) [#8]
I see.

I didn't take into account using the integer wrap to handle the math of the wrapped integer. I have never considered that. hmmm...

So dang confusing.


GfK(Posted 2009) [#9]
My solution to the millisecs problem, is to not use millisecs.

As an alternative, I have a 10Hz timer which I can reset at non-critical points (level changes etc), then use that for all timing operations.

You could make it 100Hz or 1000Hz if you wanted to, its just that 10Hz happens to be right for my current needs.


TaskMaster(Posted 2009) [#10]
And what are you using to measure those Hertz? How do you know when a 10th of a second has gone by?


GfK(Posted 2009) [#11]
And what are you using to measure those Hertz? How do you know when a 10th of a second has gone by?
I'm using beans to count them. Magic ones. When a Hz goes by, I place a bean on my desk. When another Hz goes by, I place another bean, and soforth. After ten beans, I scoop them up and start again. K?

Anyway, as if to avoid being baited into yet another argument with you, unless a player plays the same level continuously without pausing for food, toilet, or sleep, for roughly six and a half years, there is no problem.


TaskMaster(Posted 2009) [#12]
I am not trying to argue? I am trying to figure out how you are counting Hz, if you are not using Millisecs()?!?!

If there is another, easier method of counting time, I would not mind knowing what it is.


GfK(Posted 2009) [#13]
OK. So many people seem to try goading others into fights on here lately I assume everyone's at it. Sorry.

Anyway, it sounds like you haven't done anything much with timers. You don't need to count anything yourself.

When you create a timer, you specify the Hz (ticks per second) at creation time.
myTimer:TTimer = CreateTimer(10)

You can check how many ticks have happened thus:
elapsedTime = myTimer.Ticks()

You can reset the tick counter at any time by one of two methods.

1. Reset the tick counter on the existing timer.
myTimer._ticks = 0

...or...

2. Create a new timer and remove the original one. You can either use the same object handle and let GC pick it up, or manually Null the original timer.

This is far better than using millisecs as you have much tighter control over the whole thing rather than just letting an uncontrollable tick counter run away with itself at 1000 times per second.


ImaginaryHuman(Posted 2009) [#14]
It doesn't need to be this complicated.

Use a Long as an accumulator of 32-bit unsigned integer `time chunks` like you would accumulate time in fixed-rate logic - and convert the integer to unsigned, where an integer value of -2147483648 is really 0, and 2147483647 is really 4294967295.

e.g.

Global MilliSeconds:Long=0:Long              'Initialize before use
Global LastMilliSeconds:Long=MilliSecsLong() 'Initialize before use

Function MilliSecsLong:Long()
   Local Milli:Long=Long(Millisecs())+2147483648:Long           'Convert to 32-bit unsigned
   If Milli<LastMilliSeconds Then MilliSeconds:+4294967296:Long 'Accumulate 2^32
   LastMilliSeconds=Milli
   Return MilliSeconds+Milli
End Function

Print MilliSecsLong()

The only requirement is that you call MilliSecsLong() at least once every 49 days. It doesn't matter if MilliSecs() starts out negative or not because it's always converted to 32-bit unsigned. I did not test this, but it should work, right? Problem solved.

The only difference to MilliSecs() is that the value you get when you enter the application will be 2147483648 higher. Possibly something to keep in mind if you use different combinations of MilliSecs() and MilliSecsLong() in the same program. To make it seem more compatible with MilliSecs() (for up to 49 days) you could subtract 2147483648 from the calculation before/after returning it.

Maybe this can be added as an official function in BlitzMax?


ImaginaryHuman(Posted 2009) [#15]
Here is a proof that it works. All I've done is comment out one line in the function and replace it with one that adds on enough value to cause a wrap-around, a value which is calculated based on the MilliSecs() at startup (the difference between that and the wrap value (2^31)-1.

Global MilliSeconds:Long=0:Long              'Initialize before use
Global LastMilliSeconds:Long=MilliSecsLong() 'Initialize before use

Global startmillis:Int=MilliSecs()           'Temporary for testing purposes

Function MilliSecsLong:Long()
   'Local Milli:Long=Long(MilliSecs())+2147483648:Long           'Convert to 32-bit unsigned
Local Milli:Long=Long(MilliSecs()+($7ffffffc:Long-StartMillis))+2147483648:Long
   If Milli<LastMilliSeconds Then MilliSeconds:+4294967296:Long 'Accumulate 2^32
   LastMilliSeconds=Milli
   Return MilliSeconds+Milli
End Function

For count=1 To 20
   Print "Millisecs: "+(MilliSecs()+Int(($7ffffffc:Long-StartMillis)))
   Print "MillisecsLong: "+MilliSecsLong()
   Delay 1
Next

You will see the `Millisecs` result wrap around after 4 iterations, while the `MilliSecsLong` will just keep on going.


TaskMaster(Posted 2009) [#16]
Ah, a timer. I hadn't even thought about that.

Wouldn't you have to be careful with that in Windows? As there are a limited number of timers for the entire OS? Correct?


ImaginaryHuman(Posted 2009) [#17]
You don't need a timer if you use my above code, but timers are handy for knowing how many times something has happened since a start point - which you could also calculate manually based on MilliSecsLong(). As to the o/s, yes there are a limited number of timers on each platform. However, how many timers with different hz rates do you really need?


BlitzSupport(Posted 2009) [#18]

It doesn't need to be this complicated


OK... but your code appears to do exactly the same as mine, except I skip an 'If' check! I do supply an extra reset function, but that does nothing different to your Global initialisation (ie. I could do that while defining the globals, but want to provide a reset function).

I fail to see the improvement really! :(


TaskMaster(Posted 2009) [#19]
IH, the point I was trying to make is that other program running on your system could also be using the system timers. Isn't that correct? Even if you only need 1 or 2, isn't it possible that there are none available?


ImaginaryHuman(Posted 2009) [#20]
My code just seemed a lot shorter is all. ;-) Perhaps this just confirms then that your code is working. I honestly didn't quite understand what your code was doing at first, but now I can see that it's doing basically the same thing.


BlitzSupport(Posted 2009) [#21]
In fairness, it started out a lot more complicated and I only arrived at this solution through lots of trial and error, plus swearing.

There seems to be no dispute about the logic working, anyway, which was my main concern, so I'll stick it in the Code Archives as an option for anyone who wants it.


... other program running on your system could also be using the system timers.


I seem to recall this was a problem in some situations. Yep, here it is. (Includes Gfk's decision to start using 10 Hz timers!)


GfK(Posted 2009) [#22]
I decided to use 10Hz timers in that instance for Buzzword! That's been on sale for nigh on two years now.

I'm only using one timer now, and have no need for any more. But they're there to be used.


Grey Alien(Posted 2009) [#23]
I just use: Change = Now - LastTime which works fine for negative numbers e.g. -100 - -102 gives a change of 2. The only problem is for the single time when the counter actually wraps, you'll get a dumb value but my timing code will clamp it to a sensible value which may or may not be perceived as a tiny one-off glitch by the user.


TaskMaster(Posted 2009) [#24]
Actually Grey, as Floyd pointed out, the moment of the flip is not a problem either. Because the integer will force the math to work, since the overflow will get flipped from negative to positive anyway do to the fact that it is integer math.


Grey Alien(Posted 2009) [#25]
well that's even cooler then.