Solution to MilliSecs integer wrap?
BlitzMax Forums/BlitzMax Programming/Solution to MilliSecs integer wrap?
| ||
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... |
| ||
I did not take a close look at this, but does it handle the case where Millisecs() is already negative when your program starts? |
| ||
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? |
| ||
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. |
| ||
This millisec-int-wrap issue has caused me problems in Book 1. Thank you for attempting to address it! |
| ||
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. |
| ||
OK, BlitzSupport's explanation made this reply null and void... |
| ||
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. |
| ||
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. |
| ||
And what are you using to measure those Hertz? How do you know when a 10th of a second has gone by? |
| ||
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. |
| ||
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. |
| ||
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. |
| ||
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? |
| ||
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. |
| ||
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? |
| ||
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? |
| ||
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! :( |
| ||
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? |
| ||
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. |
| ||
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!) |
| ||
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. |
| ||
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. |
| ||
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. |
| ||
well that's even cooler then. |