5

I have to render a list of lines (>400000) from a model in a QGraphicsScene. Since this operation is long, I try to do the render operation in another thread.

Note : all my classes are template because I render different types of models with different view transformation for each type

First, I create the QGraphicsView from the main thread :

template<>
GCode3DView<Core::PrintingModel>::GCode3DView(int width, int height, QWidget* parent)
    : QGraphicsView(parent),
      m_scene(new GCode3DScene<Core::PrintingModel>(this)),
      m_result(),
      m_handler(),
      m_yRatio(2.0/3)
{
    int w = width;
    int h = height;
    setScene(m_scene);

    QTransform rotation;
    rotation.rotate(30, Qt::XAxis);
    setTransform(rotation);

    setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    setTransformationAnchor(QGraphicsView::NoAnchor);
    setCacheMode(QGraphicsView::CacheNone);
    setInteractive(false);

    m_scene->setSceneSize(w,h);
    m_scene->setSceneRect(*sceneRect*);
}

When the model (the list of lines) is ready, I call this function from the main thread :

template<class T>
void GCode3DView<T>::setSceneModel(const T *model)
{
    m_result = QPixmap();
    m_scene->setModel(model); //Creates QGraphicsItems if not enough, disables the one that will be unused (see the function below if needed)
    QtConcurrent::run(this,&GCode3DView<T>::saveImage, model);
}

Before the call to a concurrent run, I believe that every item of the scene belong to the main thread. The saveImage is then called from a concurrent thread, doing this :

template<class T>
void GCode3DView<T>::saveImage(){
    QRect rect = viewport()->rect();

    //Create pixmap, fill it, and create QPainter;
    QPixmap pixmap = QPixmap(size()*4);
    pixmap.fill(Utils::GENERAL_BACKGROUND);
    QPainter painter(&pixmap);

    //Render scene in image
    render(&painter, pixmap.rect(), rect); //---> The error occurs here
    //Copy result
    m_result=pixmap.copy();

    m_handler.emitImageReadySignal();
}

My scene is rendered in an image, that I display in the GUI Main thread. Most of the time this works, but sometimes I can see an error :

QObject::killTimer: Timers cannot be stopped from another thread

Which seems to come at the beginning of the render function, before painting the QGraphicsItems. What happens next is that even thought the rendering is finish the CPU usage is still 8%, which means that I still have a thread running and doing nothing. I don't understand why this is happening. I followed this thread and it seems that all my objects belongs to the same thread. How should I do to get a proper render in a separated concurrent thread ?

Thanks in advance.

EDIT Trying to be more precise, it seems that the render function tries to stop a timer running in another thread, which fails and throws the error. I guess that that timer not being killed causes a part of the rendering to continue even thought the render is finished. The GUI is still usable and does not froze, I guess that the thread still running could be the concurrent one used to render.

To be more precise about my QGraphicScene, here's its code : template

class GCode3DScene : public QGraphicsScene
{
    friend class LayerItem<T>;
public:
    GCode3DScene(GCode3DView<T>* parentView = nullptr, QObject* parent = nullptr);
    ~GCode3DScene();

    ///Sets the model, creates LayerItems if needed and hides the unused one
    void setModel(const T* model);
    ///Adds a layer to m_items and to the scene
    void addLayer();
    ///Removes every layer item and the grid
    void clearScene();
    ///Sets the scene size
    void setSceneSize(int width, int height);

protected:
    ///Computes the total progression on the rendering of the scene
    void updateNumberOfRenderedPoints(int nOfRendered);

private:
    int                     m_sceneWidth;   ///Scene's width
    int                     m_sceneHeight; ///Scene's Height
    QList<LayerItem<T>*>    m_items; ///Items to display inside the scene
    LayerItem<T>*           m_grid; ///Grid to use as background
    const T*                m_model; ///Model to display
    double                  m_renderingProgression; ///Progression of the rendering
    GCode3DView<T>*         m_parentView; ///Pointer to parent
};

and the setModel function :

template <class T>
void GCode3DScene<T>::setModel(const T *model)
{
    m_model = model;
    m_renderingProgression = 0;

    //Create grid if not existant
    if(!m_grid){
        m_grid = new LayerItem<T>(this,-1,m_sceneWidth,m_sceneHeight);
        addItem(m_grid);
    }
    m_grid->setModel(model);

    //Get number of layers
    int numberOfLayer = model ? model->getNumberOfLayers() : 0;
    if(numberOfLayer>=1000)
        numberOfLayer=1000;
    //Add Layers if there is not enough
    while(m_items.size()<numberOfLayer)
        addLayer();
    //Hide unused layers if there are too much
    for(int i = 0; i < m_items.size(); ++i){
        m_items.at(i)->setVisible(i<numberOfLayer);
        m_items.at(i)->setModel(i<numberOfLayer?model:nullptr);
    }
}

LayerItem::paint is just some geometric computation and calls to painter->drawLines

If needed, here is the code for GCode3DView :

template<class T>
class GCode3DView : public QGraphicsView
{
    friend GCode3DScene<T>;
public:
    GCode3DView(int width, int height, QWidget *parent = nullptr);
    ~GCode3DView();

    ///Sets the model to display to the scene and ask for render
    void setSceneModel(const T* model);
    ///Renders scene into an image
    void saveImage();

    ...

private:
    GCode3DScene<T>*    m_scene;    ///Scene to display the model
    QPixmap             m_result;   ///Render result

};
ElevenJune
  • 423
  • 1
  • 4
  • 21
  • I don't see anything obviously wrong with the code you've posted. I've done some similar graphics rendering in a separate thread successfully, so I know the concept is fine. Your code samples don't include any timers at all, so it's difficult to say where that's coming from. If you have any timers elsewhere in your code, try putting debug output on both sides of any timer starts or stops to see where this is coming from. You may have created a timer in one thread but are starting/stopping it in another. – goug Jun 10 '21 at 23:52
  • goug : Could it be possible that the timers come from Qt's render function ? I had some similar problem with QNetworkAccessManager when deleting it wrongly, the same error would show and it was coming from inside the destructor of that class – ElevenJune Jun 14 '21 at 08:01
  • The presented amount of code looks like it's still a bit too much and not enough at the same time. Try to make a minimal, verifiable example. That might also help you with better identifying the problem. – NoDataDumpNoContribution Jun 18 '21 at 06:21
  • Afaik, graphics stuff in Qt (and in most frameworks) are not intended to be used from multiple threads. Even using completely independent QGraphicsView doing completely unrelated stuff into separate pixmaps, it could still be a problem if you have any hardware acceleration enabled, as common graphics APIs (eg: OpenGL) are not thread-safe. *[disclaimer: this is a general remark, it might not apply in this specific context, but you might want to double-check]* – spectras Jun 18 '21 at 07:08
  • spectras, thank you for your answer. I'm starting to realize what you say. Do you know if there is a way to do that or should it be always done in the main Thread ? – ElevenJune Jun 18 '21 at 07:12
  • I recommend you that using OpenGL to render them instead of QGraphicsView. because in this way everything comes in GPU and your performance increased a lot. – Parisa.H.R Jun 18 '21 at 17:40
  • 1
    This may be a long shot but it looks like you are painting on a QPixmap on a non-gui thread which according to https://doc.qt.io/qt-5/threads-modules.html is not supported. Have you tried using a QImage instead? – linuxfever Jun 22 '21 at 17:41
  • Also, your qtconcurrent::run essentially results in your qgraphicsview (which is-a qwidget) being used in a non-gui thread. Are you sure that's allowed? Couldn't find anything in the docs. – linuxfever Jun 23 '21 at 08:14
  • linuxfever : Thanks for your comments. I tried the QImage but got the same problem, but it was worth the shot. For the qtconcurrent::run it may not be allowed but I can't find documentation about this either. I found this thread and they don't seem to have the same problem : https://stackoverflow.com/questions/50246310/qobjectkilltimer-timers-cannot-be-stopped-from-another-thread – ElevenJune Jun 28 '21 at 10:05
  • Parisa, I also thought about it but for now the software runs on a Raspberry Pi 4, I don't think I can get better performance using openGL but it will be worth a shot when switching to a more powerful computer. – ElevenJune Jun 28 '21 at 10:07

0 Answers0