Precision timing callback in Qt C++ doesn't give expected results

452 Views Asked by At

I'm trying to implement high resolution timing for a Game Boy emulator. A 16-17 millisecond timer is sufficient to get the emulation at roughly the right speed, but it eventually loses sync with precision emulations like BGB.

I originally used QElapsedTimer in a while loop. This gave the expected results and kept sync with BGB, but it feels really sloppy and eats up as much CPU time as it possibly can because of the constantly-running while loop. It also keeps the program resident in Task Manager after closing. I tried implementing it using a one millisecond QTimer that tests the QElapsedTimer before executing the next frame. Despite the reduced resolution, I figured that the timing would average out to the correct speed due to checking the QElapsedTimer. This is what I currently have:

void Platform::start() {
    nanoSecondsPerFrame = 1000000000 / system->getRefreshRate();
    speedRegulationTimer->start();
    emulationUpdateTimer->start(1);
}


void Platform::executionLoop() {
    qint64 timeDelay;

    if (frameLocked == true)
        timeDelay = nanoSecondsPerFrame;
    else
        timeDelay = 0;

    if (speedRegulationTimer->nsecsElapsed() >= timeDelay) {
        speedRegulationTimer->restart();
        // Execute the cycles of the emulated system for one frame.
        system->setControllerInputs(buttonInputs);
        system->executeCycles();
        if (system->getIsRunning() == false) {
            this->stop();
            errorMessage = QString::fromStdString(system->getSystemError());
        }
        //timeDelay = speedRegulationTimer->nsecsElapsed();
        FPS++;
    }
}

nanoSecondsPerFrame calculates to 16742005 for the 59.73 Hz refresh rate. speedRegulationTimer is the QElapsedTimer. emulationUpdateTimer is a QTimer set to Qt:PreciseTimer and is connected to executionLoop. The emulation does run, but at about 50-51 FPS instead of the expected 59-60 FPS. This is definitely due to the timing because running it without timing restraints results in an exponentially higher frame rate. Either there's an obvious oversight in my code or the timers aren't working like I expect. If anyone sees an obvious problem or could offer some advice on this, I'd appreciate it.

3

There are 3 best solutions below

6
Jeremy Friesner On

I'd suggest using QElapsedTimer to keep track of when your next frame should be executed (ideally) and then dynamically computing a QTimer::singleShot() call's msec argument based on that, so that your timing loop automatically compensates for the time it takes for the GameBoy code to run; that way you can avoid the "drifts away from sync" problem that you mentioned. Something like this:

// Warning:  uncompiled/untested code, may contain errors
class Platform : public QObject
{
Q_OBJECT;

public:
   Platform() {/* empty */}

   void Start()
   {
      _nanosecondsPerFrame = 1000000000 / system->getRefreshRate();
      _clock.start(); 
      _nextSignalTime = _clock.elapsed();
      ScheduleNextSignal();
   }

private slots:
   void ExecuteFrame()
   {
      // called 59.73 times per second, on average
      [... do GameBoy calls here...]

      ScheduleNextSignal();
   }

private:
   void ScheduleNextSignal()
   {
      _nextSignalTime += _nanosecondsPerFrame;
      QTimer::singleShot(NanosToMillis(_nextSignalTime-_clock.elapsed()), Qt::PreciseTimer, this, SLOT(ExecuteFrame()));
   }

   int NanosToMillis(qint64 nanos) const
   {
      const quint64 _halfAMillisecondInNanos = 500 * 1000;  // so that we'll round to the nearest millisecond rather than always rounding down
      return (int) ((nanos+_halfAMillisecondInNanos)/(1000*1000));
   }

   QElapsedTimer _clock;
   quint64 _nextSignalTime;
   quint64 _nanosecondsPerFrame;
};
0
Benjamin Crew On

I'm adding my own answer based on Jeremy Friesner's suggestion. The 50 FPS issue was caused by another QTimer with a similar timing overlapping with the one used to regulate the emulation updates. I didn't realize that QTimers with nearly the same timeouts could throw timing off by that much, but apparently they can. This is my variation on Jeremy's suggestion if anyone is interested:

void Platform::start() {
    nanoSecondsPerFrame = 1000000000 / system->getRefreshRate();
    milliSecondsPerFrame = (double)nanoSecondsPerFrame / 1000000;
    speedRegulationTimer->start();
    executionLoop();
}


void Platform::executionLoop() {
    qint8 timeDelay;

    if (frameLocked == true)
        timeDelay = round(milliSecondsPerFrame - (speedRegulationTimer->nsecsElapsed() / nanoSecondsPerFrame));
    else
        timeDelay = 1;

    if (timeDelay <= 0)
        timeDelay = 1;

    speedRegulationTimer->restart();
    QTimer::singleShot(timeDelay, Qt::PreciseTimer, this, SLOT(executionLoop()));

    system->setControllerInputs(buttonInputs);
    system->executeCycles();
    if (system->getIsRunning() == false) {
        this->stop();
        errorMessage = QString::fromStdString(system->getSystemError());
    }
    emit screenUpdate();
    FPS++;
}

If the function takes longer than it should to be called, it reduces the number of milliseconds until the next call. Using this implementation, the difference in speed with BGB is practically imperceptible with little CPU time wasted.

3
malek.khlif On

You can use a QTimer with the type Qt::PreciseTimer