utorak, 13. rujna 2016.

New game loop

I had fun time learn how operating systems do (and don't) handle timing, what's guarantied, what's not and how and how achieve results by messing whole OS. On the one hand I'm disappointed there is no guarantee for having timing resolution below 64 Hz (ticks per second) and that there is no cross platform way of getting the resolution. But on the other hand I've observed 1000 Hz resolution on all machines I have access to.

Let's back off a little bit. Game loop is part of game logic which juggles when input events (mouse, keyboard, ...) are collected and processed and when a image frames are drawn. Typically it repeats following steps: check stop condition, process input, update game state, draw frame, sleep until next frame is needed. In previous Stareater version there was no explicit game loop, it was part of WinForm's internal "message pump" which in turn is too an endless loop that waits for a message, delegates it to an appropriate handler and checks if it got "quit" message. For signaling when to draw a next frame there was a timer component which was periodically sending "timer tick" messages. It was serviceable but not a "proper" solution. Timer's timing and my implementation of it's handler were sort of a "do it when you can".

When I started adding more game loop features, namely adjustable FPS limit, a mode without FPS limit and "battery mode" where game loop is bit more mindful about power consumption, it turned out I have to do a bit of research and rework the stuff. When it comes to timing precision there are two options, busy spin (a loop which repeatedly checks the clock) and thread sleep which is more concerned with letting other threads breath then timing. Timer is basically same qualitatively as thread sleep with a difference in how a thread is notified to wake up. So it's not surprising that most game engines, including the one supplied with OpenTK (graphics library used in the project), use busy spin approach. Busy spin is good when you have a lot of animations and movement in general but one has to keep in mind that such method uses all available CPU time which drains more power, heats CPU and makes cooler fans spin while not doing constructive work during the wait period. Since Stareater may or may not have a lot of animation, I made support for use both timing methods.

There was even bigger change to game loop, it was moved to a separate thread. I looked around the internet what is the best way to implement the game loop and there was no clear best practice. One approach (repeated from basic game dev tutorials) was to have ordinary loop which has extra step in cycle to let message pump process accumulated events. Another approach was vague reference to rendering in background thread. I've decided to marry those two concepts and suffer the consequences. The biggest reason I did that is to separate WinForms stuff from OpenGL stuff. This way window can handle it self at it's own pace and frames can be rendered in parallel. No need to send redraw request through who knows how many layers of bureaucracy. Many programmers fear the multithreading but it turned out to not be that bad. Sure I had my share of deadlocks (threads blocking each other) and race conditions (parallel tasks messing with eachothers data) and had to learn some new stuff but it was worth it. I finally had a chance to use thread join operation and discovered wonders of atomic operations and it didn't take long to fix bugs. In fact I'd like to share a few stories:

  • First one was an experiment. In theory when a process quits all of it's threads are killed so I tried to make game loop without explicit end condition. It turned out that exiting an application GUI left background thread working alone. Maybe it was the case when running from Visual Studio or Sharpdevelop, I didn't investigate further because I needed stop condition anyway. Game loop had to have some cleanup logic and it has to stop before GUI components get cleaned up.
  • What happens when GUI and main thread hosting it quit before game loop thread? Errors and exceptions because OpenGL is being used after the application had it deinitialized (disposed the context). Cure for that bug was use thread join so that main thread waits for background thread to finish.
  • More code you have guarded by synchronization mechanisms means deadlocks are more likely. When a combat happens, main thread informs game loop to switch to space battle "scene". Game loop switches scenes and calls scene initialization inside a guarded block and battle scene initialization "invokes" a piece of code on main thread. This code hides drop-down menus which triggers resize of draw area. Main thread tries to inform game loop of resize event but has to wait because loop is in guarded section so no thread can advance. Fix for this one was to make lighter signalization mechanism. Instead of guarding whole blocks of code I've reduced guards to only cover receiving signals (not handling) and simplified signals themselves (used atomic test and set instead of "monitor"). In retrospect asynchronous invocation would fix an issue too but simplifying code has further reaching benefits.


Enough with technical stuff, here is how settings menu looks like now. You can select or enter frame rate frequency limit or choose unlimited mode which will draw frames as fast as possible. Options below regulate whether to use precise but power consuming timing method or potentially imprecise but much less wasteful method. A little technical detail, power mode detection (whether a computer runs on battery or is plugged in) doesn't work in Mono so Linux and Mac won't have a benefit of game automatically switching modes. Players on those platforms will have to use "always" and "never" options which force the usage a particular method.

This is just a first phase of reworking graphics engine. It went better then expected. I know I'm repeating for the hundredth time but multithreading issues are nasty beast and I'm glad there wasn't so many of them. Next phase is moving to newer OpenGL API and improving performance. I've run profiler and noticed there are very bad performance issues. There is a lot of work left to be done.

Nema komentara:

Objavi komentar