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.