Correct GameLoop Frame timing

Hello people!

So, I've come back to work on my game engine a little bit more. And this is only possible by Casey providing the best content to learn from I ever came across. Without Handmade Hero this hobby wouldn't be anything I could really spent time on because of the lack of good documentation. Especially for game engine related stuff as mostly anybody uses Unity or whatever.

However there is one part, I can't really figure out how to get right and its -- TIMING.

From my understanding and from anybody elses too a basic game loop seems to look like the following pseudocode:

1
2
3
4
5
6
7
8
9
int main()
{
 while(GameIsRunning)
 {
  getInput();
  updateGameAndRender();
  waitForFrameRate();
 }
}


And this is basically how I am doing it too. For the future I would like to play around with some basic physics simulation. And to be able to I have to get timing right. As I've seen in more complex Systems and also in Handmade Hero the estimated frame time is passed to the game itself so that the game can advance it's physics and world simulation based on the time spent until the next frame is displayed.

I really want to understand this and get this right since it bothers me a lot!

As I am using MacOS X I use mach for timing and OpenGL to "blit" the RenderBuffer to the screen. Back when I was starting with this project I didn't quite understand that using mach_wait_until(NStoSleep) makes no sense when vSync is enabled. And I was always wondering why my animations where always kind of stuttery but I couldn't really tell if they actually were.
Knowing that I commented mach_wait_until(NStoSleep) and suddenly I got super smooth animation movement. Even though my FPS were between 58-62 but this could be caused by calculation and rounding problems I guess.

So how would I ever be able to create an !!halfway!! percice physics simulation when my frame timing code results in quite jittery animations?

I get that vSync is used to prevent tearing as it swaps the Buffers at the exact same time when the Screen refreshes. But as I understood when vSync is enabled the program waits at [GlobalGLContext flushBuffer] until the swap happened. So I gess that the OpenGL timing is more percice than mine.

So my Questions are:
1. Am I doing something wrong with my timing code or is this behaviour expected when vSync is disabled?
Moreover if this behaviour is expected, how are smooth animations with 30fps achieved since vSync syncs to 60fps?
2. Or can I vSync to 30fps somehow?
3. Is there a way to get a more percice timing, as my timing seems to vary a lot even when run without XCode. There has to be a way to get even smoother animations without vSync enabled right?
4. And how is timing handled right now in Handmade Hero since BEGIN_BLOCK(FramerateWait) until END_BLOCK(FramerateWait) is completely "commented out" through if 0? Only through vSync?
5. I am not multiplying character movement with the expected frame time as I was not sure if my timing was correct anyways. Could this produce smooth animations without vSync?


Here is all the timing code thats present in my current build:
This code produces smooth animations as vSync is enabled and mach_wait_until(now + (SleepNS)); is not used. But when I disable vSync and use mach_wait_until animations get jittery which makes me think my timing is corrupted. It wouldn't make sense to base physics on corrupted timing...
I deleted everything that has nothing to do with timing to increase readability.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
global_variable uint64 NanosecondsElapsed;
global_variable mach_timebase_info_data_t sTimebaseInfo;

int main(int argc, const char * argv[])
{
    
    running = true;
    
    @autoreleasepool
    {
        
#define MonitorRefreshHz 60;
#define GameUpdateHz (MonitorRefreshHz / 60);
        real32 TargetSecondsPerFrame = (1.0f / 60);
        uint64 LastCounter = mach_absolute_time();
        GameData.LastCounter = LastCounter;
        
        float SecondsElapsed = 0;
        
        if ( sTimebaseInfo.denom == 0 )
        {
            (void) mach_timebase_info(&sTimebaseInfo);
        }

        while (running)
        {
	    OSXProcessPendingMessages(&GameData);            

	    [GlobalGLContext makeCurrentContext];
            
            OSXProcessFrameAndRunGameLogic(&GameData, ContentViewFrame,
                                           MouseInWindowFlag, MouseLocationInView,
                                           MouseButtonMask);
            
            uint64 WorkCounter = mach_absolute_time();
            double WorkSecondsElapsed = ((WorkCounter - LastCounter) * (sTimebaseInfo.numer / sTimebaseInfo.denom)) / 1e9 ;
            double SecondsElapsedForFrame = WorkSecondsElapsed;
            if(SecondsElapsedForFrame < TargetSecondsPerFrame)
            {
                double SleepS = (TargetSecondsPerFrame - SecondsElapsedForFrame);
                double SleepNS = SleepS * 1e9;
                
                if(SleepNS > 0)
                {
                    uint64 now = mach_absolute_time();
                    //mach_wait_until(now + (SleepNS));
                }
            }
            
            [GlobalGLContext flushBuffer];

            
            uint64 EndCounter = mach_absolute_time();
            uint64 CyclesElapsed = EndCounter - LastCounter;
            NanosecondsElapsed = CyclesElapsed * sTimebaseInfo.numer / sTimebaseInfo.denom;
            SecondsElapsed = NanosecondsElapsed / 1e9;
            float FPS = ( float )( 1e9 / (float)NanosecondsElapsed );
            printf("CyclesElapsed: %llu, TimeElapsed: %f, FPS: %f\n", CyclesElapsed, SecondsElapsed, FPS);
            
            LastCounter = EndCounter;
            
            GameData.LastCounter = EndCounter;
            

        }
    } // @autoreleasepool
    return (0);
}



If I got something wrong please correct me. Also feel free to point my to any resources.
I would really appreciate your help!

Thank you very much, and have a nice day!

Edited by Adrian on Reason: Initial post
1. Small fluctuations are expected without vsync. If you want 30fps you can vsync to every second frame. Or better - use delta time to adjust all your motion.

2. Depends on graphics API, but with D3D or OpenGL you definitely can (no idea about Metal).

3. You get smoother animations by usuing delta time for all your motions.

4. Yes, it is handled only though vsync. But Casey mentioned that he will add code to do timing and sleep if necessary. Because you cannot rely only on vsync - user can forcefully turn it off in GPU driver settings.

5. Yes, you want frame time in your motions.

For simple games maybe you can get away without using frame time in calculation motion and physics. But in more physics oriented games it is very important. For real physics simulations you actually want to fix your physics time step. Without fixing it the variance in frame times can lead to various artifacts like box falling through floor or similar.

On basic level physics (including character motion) simulation should look like this:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
int main()
{
 float time = 0;
 float physicsUpdateDelta = 1.f / 60.f; // a constant, adjust for precision you want
 while(GameIsRunning)
 {
  float delta = getDeltaFromPreviousFrame();
  getInput();
  time += delta;
  while (time >= physicsUpdateDelta)
  {
    physicsUpdate(physicsUpdateDelta);
    time -= physicsUpdateDelta;
  }
  updateGameAndRender();
  waitForFrameRate();
 }
}


There are various modifications on this to achieve better precision when delta is not multiple of physicsUpdateDelta or similar. Here's more details: https://gafferongames.com/post/fix_your_timestep/

Don't use vsync as synchronization for your update time step. Different monitors have different vsync's. Sure majority is 60Hz, but what if user has 120Hz, what if 144Hz? If you don't use frame time in calculation motion/physics then your game will run 2x or more faster on these machines. You don't want that. And what if user has GSync or FreeSync monitor - these can do arbitrary "vsyncs", game should run as fast as it can and monitor will handle "sync-ing".

This is a problem that many old DOS games had. They were made for 8086 CPU which run at 4.77MHz, but later when 386 or 486 CPUs came out with much faster MHz then many games were not playable - everything moved super fast because game updated fixed movement per frame. Turbo button reduced clock rate down to smaller number so games were playable: https://en.wikipedia.org/wiki/Turbo_button If game update would have used timer to calculate movement, this would not be a problem.

Use vsync only as feature to limit rendered frame count per second. Either to avoid rendering more than monitor is capable of. Or to limit CPU usage to save battery (important on laptops).

Edited by Mārtiņš Možeiko on
I am most likely wrong, but should it not be mach_wait_until(now + (SleepNS / (sTimebaseInfo.numer / sTimebaseInfo.denom))); since now is in mach_time ( https://developer.apple.com/library/archive/qa/qa1398/_index.html ) and SleepNS is in nanoseconds.


Based on mmozeiko reply, If you don't separate physics update, Can I also recommend having a max value for the frame delta, so if a person can't hit a certain FPS target, they wouldn't warping around but slow down instead, as it was something I had issues with when working on Mokoi.

Edited by Luke Salisbury on Reason: typos
@mmozeiko: Alright thank you! That information already helped me a lot.

@lukesalis: It probably should be. But in my case, the mach timebase sTimebaseInfo.numer / sTimebaseInfo.denom; always equal 1/1...and this results in 1. Could be different on other machines but mine stays the same as mach counts in nanoseconds and one cpu cycle takes one nanosecond. If I'm not completely mistaken...

So to get things right VSYNC + manual (mach on os x) timing is the way to go.

I already advanced a little bit and implemented audio using core audio. I opened a question on stackoverflow regarding audio because as expected...it doesn't work out the way I wanted it to work out.

I already got an answer which helps me a lot though it says that the best way to achieve smooth non glitchy audio would be to put all the audio rendering into the core audio render callback. But since this is not how casey does it, I would like to know how it can be done so that the game renders the audio and writes it to the buffer. This would keep the seperation of platform and game intact which I prefer.

Here is the link to the question. All the code is up there too, maybe you got some ideas.
Realtime sine tone generation with Core Audio

Edited by Adrian on