QTimers vs Threading: How to achieve maximum performance?
-
Hey all,
I'm trying to do something a little unconventional in Qt, that is, building a game via Qt Quick. I'm taking a deep dive into performance issues with this post, so please bear with me:
Right now I'm trying to create tick function; it ticks every 16 ms, which is equivalent to 60fps. This will ensure that my game runs smoothly on most platforms.
I've tried multiple approaches to this. First I'd have a QTimer handle the 16ms delay
inline void calculateTimeStep(){ deltaTime = runtimeTimer.restart(); //Provides value then restarts the timer (Should be roughly 16ms) QDebug() << deltaTime; emit tick(deltaTime); //signals to update all game objects } void startTimeStep(){ runtimeTimer.start(); //QElapsed timer QTimer* updateTimer = new QTimer(this); updateTimer->setTimerType(Qt::PreciseTimer); //Hopefully narrow down the precision QObject::connect(updateTimer, &QTimer::timeout, this, &TimeStep::calculateTimeStep); updateTimer->start(16); //16ms = 60FPS }
One would think timestepping is as simple as that, but this undermines a flaw within QTimer: it's possible for timeout to occur much earlier than intended; my logs occasionally have 3-5ms in them. It also can be called late, as I see values such as 18-21 in my logs. There's also jitter within the movement in my game objects.
I've struggled with this for about a month or so, until recently, where I learned about threads.
My game is calculation intensive, so the main thread is most likely busy. My second approach was to run the code inside a thread, which maxed out the timer precision, with values ranging from pretty consistent 16-17ms. This is a step up from the uncontrollable 16-21 from earlier. The only issue I have with this circles back around to QTimer calling timeout early; I saw occasional 3s in my logs, adding some noticable jitter to my game objects. Still, this is a massive improvement from before, where the dips were from 3-5.Finally, I had one more idea (which yielded the best results): for max precision, run a while loop and compute the values without a QTimer. Here, I have the TimeStep class inherit from QThread instead of QObject, and for as long as the game loop is active, I'll have it run code in a while loop:
void run ()override{ runtimeTimer.start();//Elapsed TImer running = true; while(running){ //Using nanoseconds for the most accurate measurement if(runtimeTimer.nsecsElapsed() >= 16'666'667) //16'000'000nsecs or 16ms = 60FPS { deltatime = (double)runtimeTimer.nsecsElapsed()/1'000'000; //Convert to ms for convenience runtimeTimer.restart(); qDebug() << deltatime; manager->tick(deltatime); //Instead of emitting a signal and potentially causing overhead, give it to the manager directly } } } }
This method is by far the most taxing on the CPU to my knowledge. It runs perfectly smooth 70% of the time, but at random points in my game, roughly 15-20 seconds in, I'll experience lag phases, and they last for 10-15 seconds. I'm assuming that the while loop is using too much CPU over time. I've set the priority to lowest and even idle, but same issue, just to a lesser degree... How can I acheive smooth FPS without delaying the main thread?
-
Adding to this, I know that the thread isn't causing the jitter now, as it consistently logs 16'666'667 to 16'666'668. This is a rendering issue:
- I get smooth gameplay when the lag phases haven't kicked in using my 3rd approach, but when they finally occur, it isn't visually pleasing.
-The second approach is more consistent overall, but still not smooth: There's noticeable jitter due to the timeout drops but it's far less than the 1st approach. However, the jitter remains throughout the entire experience.
There's downsides to both, but I prefer smoother gameplay, which makes me prefer the 3rd approach overall. I just don't understand how to keep the thread light, while running every 16 seconds down to the ms without jarring hiccups. I've tried using msleep, but it's very course, which doesn't help my issue.
Again, the 2nd approach would be ideal if the timeout never got called so early. Maybe there is a method to prevent that?
-
Posts about the capabilities of QTimer come up every few months. Timer resolution is dependent upon the underlying OS capabilities, and in a time-sharing OS, there will be jitter when small timer intervals are used. The timers should only be used for functions that facilitate smooth user interaction, not critical events with short intervals. I hesitate to mention solid interval limits because it is system dependent, but I dont do anything less than 50ms when I do timers.
Within the confines of a time-sharing OS your approach with threads and busy-waits is more appropriate. You can insert a "small" msleep in the loop to destress the cpu, but again, you're competing with other tasks so you are at the mercy of the cpu scheduler. look into realtime scheduler class as that may help, but understand that class requires you to execute as root, and you can starve other tasks by misusing it.
-
Don't use timers for game tick. They are completely unsynchronized with the display refresh rate and you'll always get stutter. You can't make it smooth that way. Definitely don't use msleep or the like either. That's not what it's for.
Change your approach - don't assume a constant for the time interval, that's not happening either. There's no software or hardware that supports that and no serious game engine does it like that.
Instead do your game tick calculation and request display update at the end, e.g. QWindow::requestUpdate. In your paintEvent do your rendering and start another game tick. Qt is (by default) synchronized for 60Hz output. If you sync it properly using monitor v-sync the game runs as smooth as is physically possible and it only takes as much CPU as it needs and no more (Qt idles out when waiting for display's vertical refresh).
That's the basic version. A more advanced version is where you run separate game and render threads and do a so called pipelining - update Nth frame game state on one thread while rendering N-1 frame on render thread. It requires a bit more setup and is necessary when you have so much calculation that you can't fit it together with rendering in a single 16ms window. For simple cases, where your calculations take less than that the first approach is fine, you don't need separate thread.
In any case don't use QTimer for any of that. Don't try to ask for specific time, that's not gonna happen and if you base your animations on that assumed constant interval they will always be choppy. Even if you manage to tweak it on your machine it will break on another, where the timings are a little off compared to yours. Instead measure how much time has actually passed from last frame (use QElapsedTimer for that, like in your threading approach) and use that as a delta time value that you multiply all your movement, animations, velocities etc. by.