Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vsyncthread mode for qopenglwindow frameswapped driven phase locked loop #12469

Merged
merged 5 commits into from
Jan 2, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/mixxxmainwindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
116 changes: 99 additions & 17 deletions src/waveform/vsyncthread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ 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
m_pllTimer.start();
}

VSyncThread::~VSyncThread() {
Expand All @@ -32,8 +35,8 @@ void VSyncThread::run() {
m_timer.start();

//qDebug() << "VSyncThread::run()";
while (m_bDoRendering) {
if (m_vSyncMode == ST_FREE) {
if (m_vSyncMode == ST_FREE) {
while (m_bDoRendering) {
// for benchmark only!

// renders the waveform, Possible delayed due to anti tearing
Expand All @@ -46,16 +49,70 @@ void VSyncThread::run() {
m_sinceLastSwap = m_timer.restart();
m_waitToSwapMicros = 1000;
usleep(1000);
} else { // if (m_vSyncMode == ST_TIMER) {
}
} else if (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();
m_semaVsyncSlot.acquire();
JoergAtGithub marked this conversation as resolved.
Show resolved Hide resolved
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;
}
}
} else { // if (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<int>(
m_timer.elapsed().toIntegerMicros());
int remainingForSwap = m_waitToSwapMicros -
static_cast<int>(m_timer.elapsed().toIntegerMicros());
// waiting for interval by sleep
if (remainingForSwap > 100) {
usleep(remainingForSwap);
Expand Down Expand Up @@ -94,6 +151,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;
}
Expand All @@ -102,16 +161,6 @@ void VSyncThread::setVSyncType(int type) {
m_vSyncTypeChanged = true;
}

int VSyncThread::toNextSyncMicros() {
int rest = m_waitToSwapMicros - static_cast<int>(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<int>(m_timer.difference(timer).toIntegerMicros());
// int math is fine here, because we do not expect times > 4.2 s
Expand Down Expand Up @@ -149,6 +198,9 @@ void VSyncThread::getAvailableVSyncTypes(QList<QPair<int, QString>>* 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;
}
Expand All @@ -161,3 +213,33 @@ void VSyncThread::getAvailableVSyncTypes(QList<QPair<int, QString>>* 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

// qDebug() << "PLL delta" << m_pllDeltaOut;
}
10 changes: 9 additions & 1 deletion src/waveform/vsyncthread.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <QPair>
#include <QSemaphore>
#include <QThread>
#include <mutex>

#include "util/performancetimer.h"

Expand All @@ -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
};

Expand All @@ -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();
Expand All @@ -41,7 +42,9 @@ class VSyncThread : public QThread {
int getSyncIntervalTimeMicros() const {
return m_syncIntervalTimeMicros;
}
void updatePLL();
signals:
void vsyncSwapAndRender();
void vsyncRender();
void vsyncSwap();

Expand All @@ -59,4 +62,9 @@ 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;
};
55 changes: 38 additions & 17 deletions src/waveform/waveformwidgetfactory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -816,6 +801,21 @@ void WaveformWidgetFactory::swap() {
m_vsyncThread->vsyncSlotFinished();
}

void WaveformWidgetFactory::swapAndRender() {
swap();
render();
}

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
Expand Down Expand Up @@ -1123,6 +1123,18 @@ void WaveformWidgetFactory::startVSync(GuiTick* pGuiTick, VisualsManager* pVisua
m_vsyncThread->setVSyncType(m_vSyncType);
m_vsyncThread->setSyncIntervalTimeMicros(static_cast<int>(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,
Expand All @@ -1131,6 +1143,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);
}
Expand Down Expand Up @@ -1186,7 +1202,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);
Expand All @@ -1209,13 +1229,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;
}
6 changes: 3 additions & 3 deletions src/waveform/waveformwidgetfactory.h
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ class WaveformWidgetFactory : public QObject, public Singleton<WaveformWidgetFac
int findHandleIndexFromType(WaveformWidgetType::Type type);

/// Returns the desired surface format for the OpenGLWindow
static QSurfaceFormat getSurfaceFormat();
static QSurfaceFormat getSurfaceFormat(UserSettingsPointer config = nullptr);

protected:
bool setWidgetType(
Expand Down Expand Up @@ -130,8 +130,6 @@ class WaveformWidgetFactory : public QObject, public Singleton<WaveformWidgetFac
void addVuMeter(WVuMeterBase* pWidget);

void startVSync(GuiTick* pGuiTick, VisualsManager* pVisualsManager);
void setVSyncType(int vsType);
int getVSyncType();

void setPlayMarkerPosition(double position);
double getPlayMarkerPosition() const { return m_playMarkerPosition; }
Expand Down Expand Up @@ -160,6 +158,8 @@ class WaveformWidgetFactory : public QObject, public Singleton<WaveformWidgetFac
private slots:
void render();
void swap();
void swapAndRender();
void slotFrameSwapped();

private:
void evaluateWidgets();
Expand Down
4 changes: 4 additions & 0 deletions src/widget/wglwidgetqopengl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,7 @@ void WGLWidget::swapBuffers() {
bool WGLWidget::shouldRender() const {
return m_pOpenGLWindow && m_pOpenGLWindow->isExposed();
}

QOpenGLWindow* WGLWidget::getOpenGLWindow() const {
return m_pOpenGLWindow;
}
3 changes: 3 additions & 0 deletions src/widget/wglwidgetqopengl.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
////////////////////////////////

class QPaintDevice;
class QOpenGLWindow;
class OpenGLWindow;
class TrackDropTarget;

Expand All @@ -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;
Expand Down
Loading