📜 ⬆️ ⬇️

Timebug part 2: interesting solutions from EA Black Box

Hi, Habr! In my previous article, I talked about an interesting bug in an old toy, clearly demonstrated the accumulation of rounding error and just shared my experience in reverse engineering. I was hoping that this could put an end, but I was very wrong. Therefore, under the cut, I will tell the continuation of the story about the beast called Timebug, about 60 frames per second and about very interesting decisions when developing games.



Prehistory


While writing the previous part of this epic around the incorrectly calculated times of the tracks, I involuntarily tried to touch on as many games as possible. It was too lazy to do extra work, so I was looking for symptoms in all the NFS games that I had at that time. Underground 2 came under the distribution, but I did not find the initial symptoms there:


')
As can be seen from the picture (it is clickable, by the way), IGT is calculated in another way, which is clearly tied to int, and therefore there should not have been an accumulation of error. I happily announced this in our community and planned to forget about it already, but no: Ewil manually counted some videos and again discovered the time difference. We decided that so far we have no time to deal with this and I concentrated on the problem that was already known then, but now I was able to set aside time for myself and take up this particular game.

Symptoms


I studied the behavior of the global timer and found that it was “dumped” with each restart of the race, with each exit in the menu, and in general at any convenient time. This was not part of my plans, because the connection with the already known problem was completely lost, and this timer simply should not break. Out of desperation, I recorded the passage of 10 laps and counted the time with my hands. To my surprise, the times of the circle there were 100% accurate.

Interesting Facts
In fact, another timer was also detected, which also counted the time during the float, but it turned out to be a little useless. By changing it, I did not achieve any visible results.
For some reason, this int timer is not reset to 0, but set to 4000.


The only thing left was to disassemble and watch what was wrong there. Without going into details, I'll show the pseudo-code of the procedure that considers this unfortunate IGT:

if ( g_fFrameLength != 0.0 ) { float v0 = g_fFrameDiff + g_fFrameLength; int v1 = FltToDword(v0); g_dwUnknown0 += v1; g_dwUnknown1 = v1; g_dwUnknown2 = g_dwUnknown0; g_fFrameDiff = v0 - v1 * 0.016666668; g_dwIGT += FltToDword(g_fFrameLength * 4000.0 + 0.5); LODWORD(g_fFrameLength) = 0; ++g_dwFrameCount; g_fIGT = (double)g_dwIGT * 0.00025000001; // Divides IGT by 4000 to get time in seconds } 

Firstly:

 g_dwIGT += FltToDword(g_fFrameLength * 4000.0 + 0.5); 



Initially, this code seemed completely meaningless to me. Why multiply this by 4000, and then add another half?

In fact, this is very tricky magic. 4000 is just a constant that came up with someone from the developers ... But +0.5 is such an interesting way of rounding up according to the laws of mathematics. Add a half to 4.7 and when chopping up to an int get 5, and when adding and rounding 4.3 we get 4, as wanted. The method is not the most accurate, but it probably works faster. Personally, I will take note.

And now, dear readers, I want to play a game with you. Look at the full pseudocode above and try to find an error there. If you get tired or you are just not interested, go to the next part.

Mistake


Line
g_fFrameDiff = v0 - (double) v1 * 0.016666668;

Error under the spoiler, so you do not accidentally peep. I will explain a little: 0.01 (6) is 1/60 of a second. All the code above seems to be an attempt to count and compensate for the engine jams, but they did not take into account the fact that not everyone plays at 60fps. This is where the most interesting result came out, when in my video all circles coincided with reality, but Ewil doesn’t. He plays with the vertical synchronization turned off, and the game is blocked to a maximum of 120 fps and, accordingly, the code for his computer did not work correctly. I slightly modified the code above and brought it into a human form:

 if ( g_fFrameLength != 0.0 ) { float tmpDiff = g_fFrameDiff + g_fFrameLength; int diffTime = FltToDword(v0); g_dwUnknown0 += diffTime; // Some unknown vars g_dwUnknown1 = diffTime; g_dwUnknown2 = g_dwUnknown0; g_fFrameDiff = tmpDiff - diffTime * 1.0/60; g_dwIGT += FltToDword(g_fFrameLength * 4000 + 0.5); g_fFrameLength = 0; ++g_dwFrameCount; g_fIGT = (float)g_dwIGT / 4000; // Divides IGT by 4000 to get time in seconds } 

Here you can see that when calculating the lag, the actual frame time is initially used, and later on, the hard-times 60 fps are used. SUSPICIOUS !
I turn off vsync and get 120 frames per second. I’m going to record a video and get about 0.3 second lap difference. Bingo!

It remains for the small, patch hardcore 60fps hardcore 120fps. To do this, we look at the assembler code and find the address at which this magic constant is located: 0x007875BC.

Spoiler
In fact, it is known that this constant of type float / double will be loaded on the FPU. FPU can not load from the register, so that it was destined to be somewhere in the memory. It’s good that she wasn’t on the stack, otherwise I wouldn’t get off so easily. I would have to make changes to the game code itself, which I did not want to do.

This time I didn’t write any special programs, but I just changed the value of this variable to the necessary ones in the Cheat Engine. After that, I once again recorded 10 laps and counted the time - IGT and RTA finally matched.

In fact, they did not match 100%. But for the most part, due to the fact that the video recording dragged me a frame rate very strongly, because of which the game stopped adequately calculating the difference of times. But in general, the difference was in the region of 0.02 seconds.

I looked around a bit more in the code for what those variables affect that were so zealously calculated in the time counting procedure. I found not very much, but g_fDiffTime is used somewhere in the engine next to g_fFrameTime. Most likely, my assumption about the compensation of scams turned out to be true. But who knows these developers?

Afterword


I do not even know how many times I come across a game that assumes 60 frames per second. This is a very bad style of writing games, and I strongly recommend that you, the readers, take into account the difference in iron. Especially if you are an indie developer. And very especially if you are developing on a PC. For consoles to achieve different capacities of iron will not work, but on the PC because of this, problems constantly pop up. And there are also monitors with 120 / 144Hz refresh rate, and even more. And g-sync has already arrived.

But NFS are ports from consoles, so solutions often have a purely console approach: suppose that the FPS does not rise above 60 (30, 25, any number), and many solutions sharpen precisely for this number of frames per second. Alas, it has become more apparent in the new parts of the series.

This time the article was not so voluminous, although there is a lot of research space here. I hope there is still something interesting in these games, which can be talked about.

Source: https://habr.com/ru/post/335432/


All Articles