diff --git a/src/mixxxmainwindow.cpp b/src/mixxxmainwindow.cpp index 0d019430fe8..894341f7fa1 100644 --- a/src/mixxxmainwindow.cpp +++ b/src/mixxxmainwindow.cpp @@ -138,7 +138,7 @@ void MixxxMainWindow::initializeQOpenGL() { if (!CmdlineArgs::Instance().getSafeMode()) { #endif QOpenGLContext context; - context.setFormat(WaveformWidgetFactory::getSurfaceFormat()); + context.setFormat(WaveformWidgetFactory::getSurfaceFormat(m_pCoreServices->getSettings())); if (context.create()) { // This widget and its QOpenGLWindow will be used to query QOpenGL // information (version, driver, etc) in WaveformWidgetFactory. diff --git a/src/waveform/vsyncthread.cpp b/src/waveform/vsyncthread.cpp index 756a4199603..38c789fb320 100644 --- a/src/waveform/vsyncthread.cpp +++ b/src/waveform/vsyncthread.cpp @@ -8,19 +8,23 @@ VSyncThread::VSyncThread(QObject* pParent) : QThread(pParent), m_bDoRendering(true), m_vSyncTypeChanged(false), - m_syncIntervalTimeMicros(33333), // 30 FPS + m_syncIntervalTimeMicros(33333), // 30 FPS m_waitToSwapMicros(0), m_vSyncMode(ST_TIMER), m_syncOk(false), m_droppedFrames(0), m_swapWait(0), m_displayFrameRate(60.0), - m_vSyncPerRendering(1) { + m_vSyncPerRendering(1), + m_pllPhaseOut(0.0), + m_pllDeltaOut(16666.6), // 60 FPS initial delta + m_pllLogging(0.0) { + m_pllTimer.start(); } VSyncThread::~VSyncThread() { m_bDoRendering = false; - m_semaVsyncSlot.release(2); // Two slots + m_semaVsyncSlot.release(m_vSyncMode == ST_PLL ? 1 : 2); // Two slots, one for PLL wait(); //delete m_glw; } @@ -32,57 +36,133 @@ void VSyncThread::run() { m_timer.start(); //qDebug() << "VSyncThread::run()"; + switch (m_vSyncMode) { + case ST_FREE: + runFree(); + break; + case ST_PLL: + runPLL(); + break; + case ST_TIMER: + runTimer(); + break; + default: + assert(false); + break; + } +} + +void VSyncThread::runFree() { + assert(m_vSyncMode == ST_FREE); while (m_bDoRendering) { - if (m_vSyncMode == ST_FREE) { - // for benchmark only! - - // renders the waveform, Possible delayed due to anti tearing - emit vsyncRender(); - m_semaVsyncSlot.acquire(); - - emit vsyncSwap(); // swaps the new waveform to front - m_semaVsyncSlot.acquire(); - - m_sinceLastSwap = m_timer.restart(); - m_waitToSwapMicros = 1000; - usleep(1000); - } else { // if (m_vSyncMode == ST_TIMER) { - emit vsyncRender(); // renders the new waveform. - - // wait until rendering was scheduled. It might be delayed due a - // pending swap (depends one driver vSync settings) - m_semaVsyncSlot.acquire(); - - // qDebug() << "ST_TIMER " << lastMicros << restMicros; - int remainingForSwap = m_waitToSwapMicros - static_cast( - m_timer.elapsed().toIntegerMicros()); - // waiting for interval by sleep - if (remainingForSwap > 100) { - usleep(remainingForSwap); - } + // for benchmark only! - // swaps the new waveform to front in case of gl-wf - emit vsyncSwap(); + // renders the waveform, Possible delayed due to anti tearing + emit vsyncRender(); + m_semaVsyncSlot.acquire(); - // wait until swap occurred. It might be delayed due to driver vSync - // settings. - m_semaVsyncSlot.acquire(); + emit vsyncSwap(); // swaps the new waveform to front + m_semaVsyncSlot.acquire(); - // <- Assume we are VSynced here -> - m_sinceLastSwap = m_timer.restart(); - int lastSwapTime = static_cast(m_sinceLastSwap.toIntegerMicros()); - if (remainingForSwap < 0) { - // Our swapping call was already delayed - // The real swap might happens on the following VSync, depending on driver settings - m_droppedFrames++; // Count as Real Time Error - } - // try to stay in right intervals - m_waitToSwapMicros = m_syncIntervalTimeMicros + - ((m_waitToSwapMicros - lastSwapTime) % m_syncIntervalTimeMicros); + m_sinceLastSwap = m_timer.restart(); + m_waitToSwapMicros = 1000; + usleep(1000); + } +} + +void VSyncThread::runPLL() { + assert(m_vSyncMode == ST_PLL); + qint64 offset = 0; + qint64 nextSwapMicros = 0; + while (m_bDoRendering) { + // Use a phase-locked-loop on the QOpenGLWindow::frameSwapped signal + // to determine when the vsync occurs + + qint64 pllPhaseOut; + qint64 pllDeltaOut; + qint64 now; + + { + std::scoped_lock lock(m_pllMutex); + // last estimated vsync + pllPhaseOut = std::llround(m_pllPhaseOut); + // estimated frame interval + pllDeltaOut = std::llround(m_pllDeltaOut); + now = m_pllTimer.elapsed().toIntegerMicros(); + } + if (pllPhaseOut > nextSwapMicros) { + nextSwapMicros = pllPhaseOut; + } + if (nextSwapMicros == pllPhaseOut) { + nextSwapMicros += pllDeltaOut; + } + + // sleep an integer number of frames extra to approximate the + // selected framerate (eg 10,15,20,30) + const auto skippedFrames = (m_syncIntervalTimeMicros - pllDeltaOut / 2) / pllDeltaOut; + qint64 sleepForSkippedFrames = skippedFrames * pllDeltaOut; + + qint64 sleepUntilSwap = (nextSwapMicros + offset - now) % pllDeltaOut; + if (sleepUntilSwap < 0) { + sleepUntilSwap += pllDeltaOut; + } + usleep(sleepUntilSwap + sleepForSkippedFrames); + + m_sinceLastSwap = m_timer.restart(); + m_waitToSwapMicros = pllDeltaOut + sleepForSkippedFrames; + + // Signal to swap the gl widgets (waveforms, spinnies, vumeters) + // and render them for the next swap + emit vsyncSwapAndRender(); + m_semaVsyncSlot.acquire(); + if (m_sinceLastSwap.toIntegerMicros() > sleepForSkippedFrames + pllDeltaOut * 3 / 2) { + m_droppedFrames++; + // Adjusting the offset on each frame drop ends up at + // an offset with no frame drops + offset = (offset + 2000) % pllDeltaOut; } } } +void VSyncThread::runTimer() { + assert(m_vSyncMode == ST_TIMER); + + while (m_bDoRendering) { + emit vsyncRender(); // renders the new waveform. + + // wait until rendering was scheduled. It might be delayed due a + // pending swap (depends one driver vSync settings) + m_semaVsyncSlot.acquire(); + + // qDebug() << "ST_TIMER " << lastMicros << restMicros; + int remainingForSwap = m_waitToSwapMicros - + static_cast(m_timer.elapsed().toIntegerMicros()); + // waiting for interval by sleep + if (remainingForSwap > 100) { + usleep(remainingForSwap); + } + + // swaps the new waveform to front in case of gl-wf + emit vsyncSwap(); + + // wait until swap occurred. It might be delayed due to driver vSync + // settings. + m_semaVsyncSlot.acquire(); + + // <- Assume we are VSynced here -> + m_sinceLastSwap = m_timer.restart(); + int lastSwapTime = static_cast(m_sinceLastSwap.toIntegerMicros()); + if (remainingForSwap < 0) { + // Our swapping call was already delayed + // The real swap might happens on the following VSync, depending on driver settings + m_droppedFrames++; // Count as Real Time Error + } + // try to stay in right intervals + m_waitToSwapMicros = m_syncIntervalTimeMicros + + ((m_waitToSwapMicros - lastSwapTime) % m_syncIntervalTimeMicros); + } +} + int VSyncThread::elapsed() { return static_cast(m_timer.elapsed().toIntegerMicros()); } @@ -94,6 +174,8 @@ void VSyncThread::setSyncIntervalTimeMicros(int syncTime) { } void VSyncThread::setVSyncType(int type) { + // qDebug() << "setting vsync type" << type; + if (type >= (int)VSyncThread::ST_COUNT) { type = VSyncThread::ST_TIMER; } @@ -102,16 +184,6 @@ void VSyncThread::setVSyncType(int type) { m_vSyncTypeChanged = true; } -int VSyncThread::toNextSyncMicros() { - int rest = m_waitToSwapMicros - static_cast(m_timer.elapsed().toIntegerMicros()); - // int math is fine here, because we do not expect times > 4.2 s - if (rest < 0) { - rest %= m_syncIntervalTimeMicros; - rest += m_syncIntervalTimeMicros; - } - return rest; -} - int VSyncThread::fromTimerToNextSyncMicros(const PerformanceTimer& timer) { int difference = static_cast(m_timer.difference(timer).toIntegerMicros()); // int math is fine here, because we do not expect times > 4.2 s @@ -149,6 +221,9 @@ void VSyncThread::getAvailableVSyncTypes(QList>* pList) { case VSyncThread::ST_FREE: name = tr("Free + 1 ms (for benchmark only)"); break; + case VSyncThread::ST_PLL: + name = tr("frameSwapped-signal driven phase locked loop"); + break; default: break; } @@ -161,3 +236,41 @@ void VSyncThread::getAvailableVSyncTypes(QList>* pList) { mixxx::Duration VSyncThread::sinceLastSwap() const { return m_sinceLastSwap; } + +void VSyncThread::updatePLL() { + std::scoped_lock lock(m_pllMutex); + + // Phase-lock-looped to estimate the vsync based on the + // QOpenGLWindow::frameSwapped signal + + // inspired by https://liquidsdr.org/blog/pll-simple-howto/ + const double alpha = 0.01; // the page above uses 0.05, but a more narrow + // filter seems to work better here + const double beta = 0.5 * alpha * alpha; // increment adjustment factor + + const double pllPhaseIn = m_pllTimer.elapsed().toDoubleMicros(); + + m_pllPhaseOut += m_pllDeltaOut; + + double pllPhaseError = pllPhaseIn - m_pllPhaseOut; + + if (pllPhaseError > 0) { + // when advanced more than a frame, jump to the current frame + m_pllPhaseOut += std::floor(pllPhaseError / m_pllDeltaOut) * m_pllDeltaOut; + pllPhaseError = pllPhaseIn - m_pllPhaseOut; + } + + // apply loop filter and correct output phase and delta + m_pllPhaseOut += alpha * pllPhaseError; // adjust phase + m_pllDeltaOut += beta * pllPhaseError; // adjust delta + + if (pllPhaseIn > m_pllLogging) { + if (m_pllLogging == 0) { + m_pllLogging = pllPhaseIn; + } else { + qDebug() << "phase-locked-loop:" << m_pllPhaseOut << m_pllDeltaOut; + } + // log every 10 seconds + m_pllLogging += 10000000.0; + } +} diff --git a/src/waveform/vsyncthread.h b/src/waveform/vsyncthread.h index 85f00aef365..7b156193421 100644 --- a/src/waveform/vsyncthread.h +++ b/src/waveform/vsyncthread.h @@ -3,6 +3,7 @@ #include #include #include +#include #include "util/performancetimer.h" @@ -17,6 +18,7 @@ class VSyncThread : public QThread { ST_SGI_VIDEO_SYNC, ST_OML_SYNC_CONTROL, ST_FREE, + ST_PLL, ST_COUNT // Dummy Type at last, counting possible types }; @@ -27,7 +29,6 @@ class VSyncThread : public QThread { bool waitForVideoSync(WGLWidget* glw); int elapsed(); - int toNextSyncMicros(); void setSyncIntervalTimeMicros(int usSyncTimer); void setVSyncType(int mode); int droppedFrames(); @@ -41,11 +42,17 @@ class VSyncThread : public QThread { int getSyncIntervalTimeMicros() const { return m_syncIntervalTimeMicros; } + void updatePLL(); signals: + void vsyncSwapAndRender(); void vsyncRender(); void vsyncSwap(); private: + void runFree(); + void runPLL(); + void runTimer(); + bool m_bDoRendering; bool m_vSyncTypeChanged; int m_syncIntervalTimeMicros; @@ -59,4 +66,10 @@ class VSyncThread : public QThread { double m_displayFrameRate; int m_vSyncPerRendering; mixxx::Duration m_sinceLastSwap; + // phase locked loop + std::mutex m_pllMutex; + PerformanceTimer m_pllTimer; + double m_pllPhaseOut; + double m_pllDeltaOut; + double m_pllLogging; }; diff --git a/src/waveform/waveformwidgetfactory.cpp b/src/waveform/waveformwidgetfactory.cpp index 30ededd47ef..17212b2f0db 100644 --- a/src/waveform/waveformwidgetfactory.cpp +++ b/src/waveform/waveformwidgetfactory.cpp @@ -509,21 +509,6 @@ void WaveformWidgetFactory::setEndOfTrackWarningTime(int endTime) { } } -void WaveformWidgetFactory::setVSyncType(int type) { - if (m_config) { - m_config->set(ConfigKey("[Waveform]","VSync"), ConfigValue((int)type)); - } - - m_vSyncType = type; - if (m_vsyncThread) { - m_vsyncThread->setVSyncType(type); - } -} - -int WaveformWidgetFactory::getVSyncType() { - return m_vSyncType; -} - bool WaveformWidgetFactory::setWidgetType(WaveformWidgetType::Type type) { return setWidgetType(type, &m_type); } @@ -702,7 +687,7 @@ void WaveformWidgetFactory::notifyZoomChange(WWaveformViewer* viewer) { } } -void WaveformWidgetFactory::render() { +void WaveformWidgetFactory::renderSelf() { ScopedTimer t("WaveformWidgetFactory::render() %1waveforms", static_cast(m_waveformWidgetHolders.size())); @@ -774,10 +759,14 @@ void WaveformWidgetFactory::render() { m_pGuiTick->process(); //qDebug() << "refresh end" << m_vsyncThread->elapsed(); +} + +void WaveformWidgetFactory::render() { + renderSelf(); m_vsyncThread->vsyncSlotFinished(); } -void WaveformWidgetFactory::swap() { +void WaveformWidgetFactory::swapSelf() { ScopedTimer t("WaveformWidgetFactory::swap() %1waveforms", static_cast(m_waveformWidgetHolders.size())); @@ -812,10 +801,29 @@ void WaveformWidgetFactory::swap() { // If we are using WVuMeter, this does nothing emit swapVuMeters(); } - //qDebug() << "swap end" << m_vsyncThread->elapsed(); +} + +void WaveformWidgetFactory::swap() { + swapSelf(); + m_vsyncThread->vsyncSlotFinished(); +} + +void WaveformWidgetFactory::swapAndRender() { + swapSelf(); + renderSelf(); m_vsyncThread->vsyncSlotFinished(); } +void WaveformWidgetFactory::slotFrameSwapped() { +#ifdef MIXXX_USE_QOPENGL + WGLWidget* widget = SharedGLContext::getWidget(); + // continuously trigger redraws + widget->getOpenGLWindow()->update(); + // update the phase-locked-loop + m_vsyncThread->updatePLL(); +#endif +} + WaveformWidgetType::Type WaveformWidgetFactory::autoChooseWidgetType() const { if (isOpenGlShaderAvailable()) { #ifndef MIXXX_USE_QOPENGL @@ -1123,6 +1131,18 @@ void WaveformWidgetFactory::startVSync(GuiTick* pGuiTick, VisualsManager* pVisua m_vsyncThread->setVSyncType(m_vSyncType); m_vsyncThread->setSyncIntervalTimeMicros(static_cast(1e6 / m_frameRate)); +#ifdef MIXXX_USE_QOPENGL + if (m_vSyncType == VSyncThread::ST_PLL) { + WGLWidget* widget = SharedGLContext::getWidget(); + connect(widget->getOpenGLWindow(), + &QOpenGLWindow::frameSwapped, + this, + &WaveformWidgetFactory::slotFrameSwapped, + Qt::DirectConnection); + widget->show(); + } +#endif + connect(m_vsyncThread, &VSyncThread::vsyncRender, this, @@ -1131,6 +1151,10 @@ void WaveformWidgetFactory::startVSync(GuiTick* pGuiTick, VisualsManager* pVisua &VSyncThread::vsyncSwap, this, &WaveformWidgetFactory::swap); + connect(m_vsyncThread, + &VSyncThread::vsyncSwapAndRender, + this, + &WaveformWidgetFactory::swapAndRender); m_vsyncThread->start(QThread::NormalPriority); } @@ -1186,7 +1210,11 @@ QString WaveformWidgetFactory::buildWidgetDisplayName() const { } // static -QSurfaceFormat WaveformWidgetFactory::getSurfaceFormat() { +QSurfaceFormat WaveformWidgetFactory::getSurfaceFormat(UserSettingsPointer config) { + // The first call should pass the config to set the vsync mode. Subsequent + // calls will use the value as set on the first call. + static const auto vsyncMode = config->getValue(ConfigKey("[Waveform]", "VSync"), 0); + QSurfaceFormat format; // Qt5 requires at least OpenGL 2.1 or OpenGL ES 2.0, default is 2.0 // format.setVersion(2, 1); @@ -1209,13 +1237,14 @@ QSurfaceFormat WaveformWidgetFactory::getSurfaceFormat() { // On OS X, syncing to vsync has good performance FPS-wise and // eliminates tearing. (This is an comment from pre QOpenGLWindow times) format.setSwapInterval(1); + (void)vsyncMode; #else // It seems that on Windows (at least for some AMD drivers), the setting 1 is not // not properly handled. We saw frame rates divided by exact integers, like it should // be with values >1 (see https://github.com/mixxxdj/mixxx/issues/11617) // Reported as https://bugreports.qt.io/browse/QTBUG-114882 // On Linux, horrible FPS were seen with "VSync off" before switching to QOpenGLWindow too - format.setSwapInterval(0); + format.setSwapInterval(vsyncMode == VSyncThread::ST_PLL ? 1 : 0); #endif return format; } diff --git a/src/waveform/waveformwidgetfactory.h b/src/waveform/waveformwidgetfactory.h index 14d12701de6..17054366918 100644 --- a/src/waveform/waveformwidgetfactory.h +++ b/src/waveform/waveformwidgetfactory.h @@ -99,7 +99,7 @@ class WaveformWidgetFactory : public QObject, public Singleton QString buildWidgetDisplayName() const; diff --git a/src/widget/wglwidgetqopengl.cpp b/src/widget/wglwidgetqopengl.cpp index 5d9f7e43107..6e19a5c20c6 100644 --- a/src/widget/wglwidgetqopengl.cpp +++ b/src/widget/wglwidgetqopengl.cpp @@ -93,3 +93,7 @@ void WGLWidget::swapBuffers() { bool WGLWidget::shouldRender() const { return m_pOpenGLWindow && m_pOpenGLWindow->isExposed(); } + +QOpenGLWindow* WGLWidget::getOpenGLWindow() const { + return m_pOpenGLWindow; +} diff --git a/src/widget/wglwidgetqopengl.h b/src/widget/wglwidgetqopengl.h index f4d5a31f2d9..5cd57a4dd11 100644 --- a/src/widget/wglwidgetqopengl.h +++ b/src/widget/wglwidgetqopengl.h @@ -11,6 +11,7 @@ //////////////////////////////// class QPaintDevice; +class QOpenGLWindow; class OpenGLWindow; class TrackDropTarget; @@ -37,6 +38,8 @@ class WGLWidget : public QWidget { void setTrackDropTarget(TrackDropTarget* pTarget); TrackDropTarget* trackDropTarget() const; + QOpenGLWindow* getOpenGLWindow() const; + protected: void showEvent(QShowEvent* event) override; void resizeEvent(QResizeEvent* event) override;