0

I'm using C++ with raylib to create a game. I'm running Windows x64 compiling with MinGW in Eclipse.

For the game loop, the normal tutorials give you something like this:

int main()
{
    InitWindow(800, 600, ("Window Title").c_str());
    SetWindowState(FLAG_VSYNC_HINT);
    
    while (!WindowShouldClose())
    {
        Update(); // your function to do game update logic
        
        BeginDrawing();
        
        ClearBackground((Color){0, 0, 0, 255});
        
        Draw(); // your function to do game drawing
        
        EndDrawing();
    }
}

However, this is bad practice. So it sets the VSYNC flag to make sure the drawing only happens at the same rate as the monitor refresh rate. That's fine but if you insert your update logic into the same loop, then your update rate is locked to the draw rate.

This is foolish because different users have different refresh rates. Mines uses 60 Hz, in Europe they use 50 Hz, and lots of people use 120, 144, 240, or any custom refresh rate they want.

So that means the game will run at very different speeds. Not what I want.

What I want instead is the update logic to always be 60 FPS (actually 16 ms intervals so it's 62.5 FPS), while the draw logic runs on the VSYNC loop.

So here's what I tried next:

std::thread ULT(scr_run_updateloop); // spawn a new thread that calls scr_run_updateloop() immediately

while (!WindowShouldClose())
{
    // no more Update() call in the draw loop!
    
    BeginDrawing();
        
    ClearBackground((Color){0, 0, 0, 255});
        
    Draw(); // your function to do game drawing
        
    EndDrawing();
}

and the function called by the thread...

void scr_run_updateloop()
{
    auto time_current = chrono::steady_clock::now();
    auto time_last = time_current;
    uint64_t time_d;
    
    while (true)
    {
        time_current = chrono::steady_clock::now();
        time_d = duration_cast<milliseconds>(time_current - time_last).count();

        if (time_d >= 16) // 16 ms interval is 62.5 FPS
        {
            Update();
            time_last = time_current;
        }
    }
}

This code works. However, that thread is hogging CPU like crazy. My task manager says 25% and I'm on a 4-core system.

What can I do? I've read about sleep and thread::sleep_for but the problem is the sleep time is not guaranteed and can be off by many milliseconds because the OS could be busy with whatever.

Note: I don't need the thread to sync with the main thread at all. The Update loop can be completely separate from the Draw Loop, AFAIK they will never need to share a locked resource.

I've also read about a completely alternate approach where your time between Updates is NOT fixed, but uses deltaTime. Then you interpolate positions, speeds and other stuff to make sure things happen at the same overall speed. That seems extraordinarily overcomplicated and unnecessary. If you can VSYNC to a draw refresh rate than I don't see why you can't set up a custom loop to call once every fixed interval. There must be a way to do this, but how?

DrZ214
  • 486
  • 5
  • 19
  • If your update logic isn’t tied your refresh rate, how will you avoid subtle glitches that occur every so often when two frames are rendered without any position-update occurring between them? – Jeremy Friesner May 22 '23 at 01:56
  • @JeremyFriesner Can you be more specific? If the user is not pressing any keys, the player character will not move and no draw changes will occur. I don't understand what kind of glitch you are talking about. – DrZ214 May 22 '23 at 02:04
  • 2
    if you want the animation to be smooth, any moving object(s) need to move on every frame. If, OTOH, there is occasionally a frame where the moving object(s) are redrawn in the same positions they were at in the previous frame, or where the object(s) moved twice as far as they were moved in the other frames, the user would perceive that as their motion not being "smooth". So if you want smooth animation, it's important for exactly one update to occur between every pair of successive frame-draws. (How far you move things during that update is up to you, as long as it's consistent) – Jeremy Friesner May 22 '23 at 02:21
  • @JeremyFriesner Yeah i see what you're saying now. So for example the refresh rate is 60 Hz but the update loop is 62.5 Hz, so every second there are 2 or 3 extra jumps (for an object moving at speed 1 pxl per update). It's hard to say how big of a problem this will be, might be very game dependent on how many objects and their typical speeds, if they are fast or slow, etc. I should experiment with this but nevertheless it would be nice if my simple update thread is not hogging all CPU when active. – DrZ214 May 22 '23 at 02:39
  • Your update thread should block until it is time to do the next update, to avoid spinning the cpu. The best time to do each update is probably right after drawing completes, so you could have it block on a condition variable that Draw() signals just before it returns… or just don’t use a separate thread; instead call Update() directly from the drawing thread (just before or after Draw()). – Jeremy Friesner May 22 '23 at 02:44
  • 2
    Good article: https://gafferongames.com/post/fix_your_timestep/. Good C++ implementation using modern chrono: https://stackoverflow.com/a/59446610/576911 – Howard Hinnant May 22 '23 at 03:00

1 Answers1

2

Your CPU load is high because your loop will be spinning really fast while waiting for enough time to pass to meet the condition for calling Update. To bring the CPU usage down, instead of having your thread spin, you want it to sleep, that is, to do nothing while it waits.

But there's no need to insert a call to special sleep function into your loop, as you already have a facility that allows you to sleep: Your Draw() function! With Vsync enabled, this function will naturally sleep until the monitor is ready to display the next frame.

The only thing you need to take care of now is make sure that your update function takes this sleep time into account. So instead of updating once every loop, you will want to inspect how much time has passed since the last update and then update your state accordingly.

auto time_current = std::chrono::steady_clock::now();
auto time_last = time_current;
auto const update_interval = std::chrono::milliseconds{16};

while (true)
{
    time_current = std::chrono::steady_clock::now();
    auto time_d = time_current - time_last;

    while (time_d >= update_interval) // perform one update for every interval passed
    {
        Update();
        time_d -= update_interval;
        time_last += update_interval;
    }

    Draw();     // this is where the sleep happens
}
ComicSansMS
  • 51,484
  • 14
  • 155
  • 166
  • Did not work for me. The CPU was still blazing away, and the drawing was actually very glitchy flickering text. Note that Draw() is my own function i wrote myself. So instead of it, i tried BeginDraw(); followed immediately by EndDraw(); which are raylib functions. This brought the CPU down to 1 or 2% but now the actual screen doesn't display anything except for momentary flashes once every 2 seconds or so. – DrZ214 May 22 '23 at 04:41
  • 1
    If I understand the answer correctly, the `Draw();` is intended to be the snippet `BeginDrawing(); ClearBackground((Color){0, 0, 0, 255}); Draw(); EndDrawing();` from your question @DrZ214 – Patrick Roberts May 22 '23 at 04:43
  • @DrZ214 Yes, as Patrick pointed out, `Draw()` here is meant to be 'the entirety of your screen drawing functionality including the vsync`. As I don't know your code base, I will leave it to you to figure out how to implement this properly for your graphics framework. – ComicSansMS May 22 '23 at 04:46
  • @PatrickRoberts Okay i tried substituting those 4 lines of codes for "Draw()" however the behavior is the same as when i tried BeginDraw and EndDraw by themselves. I think raylib does not like it when 2 threads try to initiate drawing at the same time. – DrZ214 May 22 '23 at 04:50
  • @DrZ214 the answer is single-threaded, there are not supposed to be 2 threads running... – Patrick Roberts May 22 '23 at 04:52
  • @PatrickRoberts I see, modified it and ran it with the full draw stuff in that loop. And it works. However, as far as I can see this totally defeats the purpose. This is equivalent to putting Update() in the same draw loop as in the first code example of the OP, and Update and Draw will happen at the exact same rate. If Draw() is truly blocking until VSYNC, and Update is trying to run faster (62.5x per second vs 60x per second), then it will be blocked and slowed to be 60x per second just like the Draw Loop. – DrZ214 May 22 '23 at 05:04
  • @DrZ214 I didn't write this answer, I'm simply trying to be helpful, but since you pinged me, I feel obliged to point out the answer is very much **not** logically equivalent to your first code example. Note that `Update()` may be called more than once using the code in the answer, when the update interval skews far enough from the refresh rate. – Patrick Roberts May 22 '23 at 05:10
  • 1
    @DrZ214 There's two concerns here: 1. How to decouple the update and the draw interval and 2. how to parallelise update and draw. This function tells you how to do 1. and gives a reasonable baseline for 2. Update and draw are completely independent here, even though they are part of the same loop. You can have 0-n updates and 1 draw in each loop iteration. If you need more parallelism for computing the update, that is possible based on this loop, but useful (!) parallelization of work requires significantly more effort than what you attempted in your sample code. – ComicSansMS May 22 '23 at 05:36