Correct way to synchronize QOpenGLWidget repaint from a worker thread
-
Hi everyone,
I need some help with thread synchronization. After trying a variety of approaches, I'm not getting the desired behavior so I'm reaching out for some ideas. Here is the main idea: I have my main thread, which contains some UI elements, including a QOpenGLWidget. At some point, the user pushes a button, which starts up a second thread that starts a calculation. The calculation is basically a complicated iteration, and at the end of each iteration, I want to refresh the UI, especially the QOpenGLWidget to get a "live" update of the state of the calculation.
The basic implementation I had was that the worker thread emits a signal after each iteration, which gets caught in the main thread, and the receiving slot simply calls the repaint() method on the parent UI element. (The QOpenGLWidget is a child of this). But the result is not a smooth animation. Most of the time, the UI just seems to freeze, and then suddenly tries to "catch up" and renders several updates, and then freezes again. I thought that maybe the rendering takes too long and that most of the repaint events simply get thrown away (can that happen? Or maybe the main UI event loop stalls?) I tried to do some synchronization with various combinations of mutexes, wait conditions, and timers, but to no avail. I just can't seem to get a smooth animation.
So, I'm hoping to get some ideas from the community on how to do proper synchronization for this particular scenario. Let me know if I should add more detail, but I look forward to your ideas, since I am stumped. Thanks! -
Hi everyone,
I need some help with thread synchronization. After trying a variety of approaches, I'm not getting the desired behavior so I'm reaching out for some ideas. Here is the main idea: I have my main thread, which contains some UI elements, including a QOpenGLWidget. At some point, the user pushes a button, which starts up a second thread that starts a calculation. The calculation is basically a complicated iteration, and at the end of each iteration, I want to refresh the UI, especially the QOpenGLWidget to get a "live" update of the state of the calculation.
The basic implementation I had was that the worker thread emits a signal after each iteration, which gets caught in the main thread, and the receiving slot simply calls the repaint() method on the parent UI element. (The QOpenGLWidget is a child of this). But the result is not a smooth animation. Most of the time, the UI just seems to freeze, and then suddenly tries to "catch up" and renders several updates, and then freezes again. I thought that maybe the rendering takes too long and that most of the repaint events simply get thrown away (can that happen? Or maybe the main UI event loop stalls?) I tried to do some synchronization with various combinations of mutexes, wait conditions, and timers, but to no avail. I just can't seem to get a smooth animation.
So, I'm hoping to get some ideas from the community on how to do proper synchronization for this particular scenario. Let me know if I should add more detail, but I look forward to your ideas, since I am stumped. Thanks!@febiodeveloper said in Correct way to synchronize QOpenGLWidget repaint from a worker thread:
n. Most of the time, the UI just seems to freeze, and then suddenly tries to "catch up" and renders several updates,
This does not look correct - looks like you're somehow blocking the ui. Please show some minimal code.
-
@febiodeveloper said in Correct way to synchronize QOpenGLWidget repaint from a worker thread:
n. Most of the time, the UI just seems to freeze, and then suddenly tries to "catch up" and renders several updates,
This does not look correct - looks like you're somehow blocking the ui. Please show some minimal code.
@Christian-Ehrlicher The code is complicated, and I've tried many different ways, but let me share my initial idea (which I know is flawed, but I guess I'm not entirely sure I understand why).
At some point, the user pushes a button, which starts a new thread. At some point during the execution of this thread, some data is processed and then a signal is sent:// this is called from the second "worker" thread void processData() { // some data is prepared here doSomeProcessing(); // inform the main thread that data is ready emit dataReady(); }
The signal is caught in the main thread by the parent UI element that contains the QOpenGLWidget as a child:
void MainUI::on_dataReady() { repaint(); }
My expectation was that this then redraws all UI elements, including the QOpenGLWidget, and then continues. But that does not happen. Instead, I get the "freeze", "catch-up", then "freeze again" for undetermined amounts of time.
I also want to point out that the processData function can be called many times in the time it takes the render the scene in the GL widget. But despite my many attempts using mutexes, wait conditions, etc., to synchronize this, I just can't get the behavior I want, which is that when the signal is emitted from the worker thread, I want this thread to wait, until the UI, including the GL widget is completely updated. Only then can the worker thread continue. I would greatly appreciate any ideas on how to synchronize the two threads to achieve this. I hope this helps to clarify the problem. Thanks! -
@Christian-Ehrlicher The code is complicated, and I've tried many different ways, but let me share my initial idea (which I know is flawed, but I guess I'm not entirely sure I understand why).
At some point, the user pushes a button, which starts a new thread. At some point during the execution of this thread, some data is processed and then a signal is sent:// this is called from the second "worker" thread void processData() { // some data is prepared here doSomeProcessing(); // inform the main thread that data is ready emit dataReady(); }
The signal is caught in the main thread by the parent UI element that contains the QOpenGLWidget as a child:
void MainUI::on_dataReady() { repaint(); }
My expectation was that this then redraws all UI elements, including the QOpenGLWidget, and then continues. But that does not happen. Instead, I get the "freeze", "catch-up", then "freeze again" for undetermined amounts of time.
I also want to point out that the processData function can be called many times in the time it takes the render the scene in the GL widget. But despite my many attempts using mutexes, wait conditions, etc., to synchronize this, I just can't get the behavior I want, which is that when the signal is emitted from the worker thread, I want this thread to wait, until the UI, including the GL widget is completely updated. Only then can the worker thread continue. I would greatly appreciate any ideas on how to synchronize the two threads to achieve this. I hope this helps to clarify the problem. Thanks!Hi,
How are you sharing the data between your thread and UI ?
-
... and is processData() really executed in another thread?
-
@SGaist Both the main thread and worker thread have access to the same object. Access is controlled via mutexes. I'm not getting any deadlocks. I also tried using a QWaitCondition in the worker thread to see if that helps with synchronization, but it did not.
-
... and is processData() really executed in another thread?
@Christian-Ehrlicher Yes, I am positive that the processData() is called from the worker thread.
-
@SGaist Both the main thread and worker thread have access to the same object. Access is controlled via mutexes. I'm not getting any deadlocks. I also tried using a QWaitCondition in the worker thread to see if that helps with synchronization, but it did not.
@febiodeveloper said in Correct way to synchronize QOpenGLWidget repaint from a worker thread:
ccess is controlled via mutexes.
So the ui is blocked when the other thread modifies it because it is waiting for the lock? Why use a thread then at all?
-
@febiodeveloper said in Correct way to synchronize QOpenGLWidget repaint from a worker thread:
ccess is controlled via mutexes.
So the ui is blocked when the other thread modifies it because it is waiting for the lock? Why use a thread then at all?
@Christian-Ehrlicher I've made some progress here. First, regarding your question, I'm using a worker thread, because I want the UI to remain responsive while the worker thread does its calculation. Only when the worker thread has a result, it should inform the main thread so it can update the displayed results.
The progress I made is the realization that the issue seems to be that calling repaint on the parent widget is not causing the OpenGLWidget to be repainted. (The odd behavior I described above was actually the result of a repaint in the mouse move event. So, when I moved my mouse, the GL widget was repainting and showing updated results, but when I stopped moving the mouse or left the GL widget, it would stop updating.) I confirmed this by calling the repaint on the GL widget directly.void MainUI::on_dataReady() { glw->repaint(); // calling repaint on the QOpenGLWidget works }
This works, but since I plan on adding other (non-GL) widgets for displaying additional results, I was hoping that if I call a repaint on the parent widget, all children would be updated as well, but that does not appear to be the case.
So, I guess my new question is whether the described behavior is expected (i.e. calling repaint() on a parent widget, does not necessarily repaint all the children.), and if so, is there a way to force a repaint on all the child widgets?
-
@Christian-Ehrlicher I've made some progress here. First, regarding your question, I'm using a worker thread, because I want the UI to remain responsive while the worker thread does its calculation. Only when the worker thread has a result, it should inform the main thread so it can update the displayed results.
The progress I made is the realization that the issue seems to be that calling repaint on the parent widget is not causing the OpenGLWidget to be repainted. (The odd behavior I described above was actually the result of a repaint in the mouse move event. So, when I moved my mouse, the GL widget was repainting and showing updated results, but when I stopped moving the mouse or left the GL widget, it would stop updating.) I confirmed this by calling the repaint on the GL widget directly.void MainUI::on_dataReady() { glw->repaint(); // calling repaint on the QOpenGLWidget works }
This works, but since I plan on adding other (non-GL) widgets for displaying additional results, I was hoping that if I call a repaint on the parent widget, all children would be updated as well, but that does not appear to be the case.
So, I guess my new question is whether the described behavior is expected (i.e. calling repaint() on a parent widget, does not necessarily repaint all the children.), and if so, is there a way to force a repaint on all the child widgets?
@febiodeveloper I was able to reproduce the issue in a small test application. I can't seem to upload the code, so I have to copy/paste it here. There are two files (GLTest.h and GLTest.c)). This code starts a thread when you push the Start button. The thread then emits a signal every second. The slot that receives it (MainUI::onDataReady()) calls a repaint on itself, but that does not repaint the child QOpenGLWidget. However, calling repaint on the GL widget works as expected. But I don't understand why calling repaint() on the parent (MainUI) does not result in a repaint of the GL widget.
GLTest.h
#pragma once #include <QtCore/QThread> #include <QtWidgets/QBoxLayout> #include <QtWidgets/QPushButton> #include <QtOpenGLWidgets/QOpenGLWidget> #include <stdlib.h> class MyThread : public QThread { Q_OBJECT public: MyThread() {} void run() override { while (true) { sleep(1); emit dataReady(); } } signals: void dataReady(); }; class MyGLWidget : public QOpenGLWidget { public: MyGLWidget(QWidget* parent = nullptr) : QOpenGLWidget(parent) {} void paintGL() override { glClearColor((float)rand() / RAND_MAX, (float)rand() / RAND_MAX, (float)rand() / RAND_MAX, 1.f); glClear(GL_COLOR_BUFFER_BIT); } }; class MainUI : public QWidget { Q_OBJECT private: MyGLWidget* glw; QPushButton* pb; public: MainUI(QWidget* parent) : QWidget(parent) { QVBoxLayout* l = new QVBoxLayout; pb = new QPushButton("Start"); l->addWidget(pb); l->addWidget(glw = new MyGLWidget); setLayout(l); connect(pb, &QPushButton::clicked, this, &MainUI::startThread); } void startThread() { pb->setDisabled(true); MyThread* thread = new MyThread(); connect(thread, &MyThread::dataReady, this, &MainUI::onDataReady); thread->start(); } public slots: void onDataReady() { repaint(); // glw->repaint(); // Calling repaint directly on QOpenGLWidget works } };
and the .cpp file:
#include <QtWidgets/QApplication> #include <QtWidgets/QMainWindow> #include "GLTest.h" int main(int nargs, char** argv) { QApplication app(nargs, argv); QMainWindow w; w.setCentralWidget(new MainUI(&w)); w.show(); return app.exec(); }
-
@febiodeveloper I was able to reproduce the issue in a small test application. I can't seem to upload the code, so I have to copy/paste it here. There are two files (GLTest.h and GLTest.c)). This code starts a thread when you push the Start button. The thread then emits a signal every second. The slot that receives it (MainUI::onDataReady()) calls a repaint on itself, but that does not repaint the child QOpenGLWidget. However, calling repaint on the GL widget works as expected. But I don't understand why calling repaint() on the parent (MainUI) does not result in a repaint of the GL widget.
GLTest.h
#pragma once #include <QtCore/QThread> #include <QtWidgets/QBoxLayout> #include <QtWidgets/QPushButton> #include <QtOpenGLWidgets/QOpenGLWidget> #include <stdlib.h> class MyThread : public QThread { Q_OBJECT public: MyThread() {} void run() override { while (true) { sleep(1); emit dataReady(); } } signals: void dataReady(); }; class MyGLWidget : public QOpenGLWidget { public: MyGLWidget(QWidget* parent = nullptr) : QOpenGLWidget(parent) {} void paintGL() override { glClearColor((float)rand() / RAND_MAX, (float)rand() / RAND_MAX, (float)rand() / RAND_MAX, 1.f); glClear(GL_COLOR_BUFFER_BIT); } }; class MainUI : public QWidget { Q_OBJECT private: MyGLWidget* glw; QPushButton* pb; public: MainUI(QWidget* parent) : QWidget(parent) { QVBoxLayout* l = new QVBoxLayout; pb = new QPushButton("Start"); l->addWidget(pb); l->addWidget(glw = new MyGLWidget); setLayout(l); connect(pb, &QPushButton::clicked, this, &MainUI::startThread); } void startThread() { pb->setDisabled(true); MyThread* thread = new MyThread(); connect(thread, &MyThread::dataReady, this, &MainUI::onDataReady); thread->start(); } public slots: void onDataReady() { repaint(); // glw->repaint(); // Calling repaint directly on QOpenGLWidget works } };
and the .cpp file:
#include <QtWidgets/QApplication> #include <QtWidgets/QMainWindow> #include "GLTest.h" int main(int nargs, char** argv) { QApplication app(nargs, argv); QMainWindow w; w.setCentralWidget(new MainUI(&w)); w.show(); return app.exec(); }
Thx for the reproducer. I would say it's by design but can't find any documentation about why an opengl widget is not updated when the parent received an update(). You may fill a bug report with your testcase to get a clear answer why this is not documented.
-
Thx for the reproducer. I would say it's by design but can't find any documentation about why an opengl widget is not updated when the parent received an update(). You may fill a bug report with your testcase to get a clear answer why this is not documented.
@Christian-Ehrlicher Thanks for looking into this. I will post a bug report.