Animation looks laggy in QOpenGLWidget - c++

I have no experience in writing a game and this week I'm trying writing a player of a music game's map (finally may become a game?) in QT; met problem and I think I need some help.
I want to show animation in 60 FPS on QOpenGLWidget. It's just some circles move in the widget, and CPU usage is low. But it looks laggy.
I enabled VSync by set the default surface format's swap behavior to doublebuffer/triplebuffer and has an interval of 1 which I think it means 60 FPS.
I implement the paintGL() method and draw the content by QPainter which QT's 2D drawing example does.
The step to compute the positions of each circle is placed outsides the paintGL method, and will run before paintGL is called.
This is the flow of the program runs:
read the script
start a timer
post a event to call "tick" procedure
"tick" procedure runs, and request update the window.
paintGL runs, draw the frame
before exit the paintGL method, a event to call "tick" is posted
I think now it waits for VSync and swap buffer
"tick" is called, go to step 4
the code:
class CgssFumenPlayer : public QOpenGLWidget
{
Q_OBJECT
...
bool Load();
public slots:
void onTick();
protected:
....
void paintGL() override;
QElapsedTimer elapsedTimer;
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QSurfaceFormat fmt;
fmt.setSwapBehavior(QSurfaceFormat::TripleBuffer);
fmt.setSwapInterval(1);
QSurfaceFormat::setDefaultFormat(fmt);
CgssFumenPlayer w;
w.Load();
w.setFixedSize(704, 396);
w.show();
return a.exec();
}
bool CgssFumenPlayer::Load()
{
....
elapsedTimer.start();
QMetaObject::invokeMethod(this, "onTick", Qt::QueuedConnection);
}
void CgssFumenPlayer::onTick()
{
playerContext.currentTime = elapsedTimer.elapsed() / 1000.0;
double f = playerContext.currentTime / (1.0 / 60);
playerContext.currentTime = (int)f * (1.0 / 60);
fumen->Compute(&playerContext);
update();
}
void CgssFumenPlayer::paintGL()
{
QPainter painter;
painter.begin(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.setWindow(0, 0, windowWidth, windowHeight);
painter.fillRect(QRectF(0, 0, windowWidth, windowHeight), QColor().black());
DrawButtons(painter);
DrawIcons(painter, &playerContext);
painter.end();
QMetaObject::invokeMethod(this, "onTick", Qt::QueuedConnection);
}
I tried these ways to get more information:
print current time by qDebug() each time entering the paintGL method.
It seems sometimes frame is dropped; it looks very obvious, and he interval to last time it's called is more than 30ms.
move the mouse in/out the window duration animation. It became laggy in higher possibility.
collect the time cost in compute position, seems only a very short time.
run this program in android, just the same or even more laggy.
game which are much more complex runs fluently on my computer. I think the hardware is fast enough. ( i7-4800M, GTX 765M )
restart the program again and again. it's now fluent (less or no frame-dropping happened), now laggy... I can't find the pattern.
Also, adjust the animation to 30 FPS cause it always looks laggy.
How can I deal with the problem?
(p.s. I hope it can run on android as well)
this is the full source code
https://github.com/sorayuki/CGSSPlayer/releases (cgssplayer.zip, not the source code)
(cgss-fumen.cpp makes no difference in this problem I think)
It can build in QTCreator (5.6) with no other dependency.
(for QT 5.5, it require to add
CONFIG += c++11
into the .pro file)

Related

How to bounce a QWidget around the desktop

I am trying to bounce a QWidget around the screen. This is the code i tried.
class Window : public QMainWindow {
public:
void moveEvent(QMoveEvent* aEvent) override;
};
void Window::moveEvent(QMoveEvent* aEvent) {
QSizeF screenSize = QGuiApplication::primaryScreen()->screenSize();
QRect oldRect = this->geometry();
QRect newRect = oldRect;
QPoint offset;
if (newRect.left() == 0) {
offset.setX(1);
}
else if (newRect.right() == screenSize.width()) {
offset.setX(-1);
}
if (newRect.top() == 0) {
offset.setX(1);
}
else if (newRect.bottom() == screenSize.height()) {
offset.setX(-1);
}
newRect.setTopLeft(newRect.topLeft() + offset);
newRect.setBottomRight(newRect.bottomRight() + offset);
QTimer::singleShot(1, [this, newRect]() {
setGeometry(newRect);
});
}
int main(int argc, char** argv) {
QApplication app{argc, argv};
Window* w = new Window();
w->show();
w->setGeometry(w->geometry());
return app.exec();
}
However, the window does not move around the screen, but somewhat jitters in place. When i move the window with the mouse and let go. It moves sporadically around the desktop, which is also not what i want.
Does anyone know if this is possible? If so, does anyone know the right way to do this?
There are several problems with the posted code, including:
The Window class doesn't have any member-variable to keep track of its current direction of motion. Without keeping that state, it's impossible to correctly calculate the next position along that direction of motion.
Driving the animation from within moveEvent() is a bit tricky, since moveEvent() gets called in response to setGeometry() as well as in response to the user actually moving the window with the mouse; that makes unexpected feedback loops possible, resulting in unexpected behavior.
The code assumes that the screen's usable surface area starts at (0,0) and ends at (screenSize.width(),screenSize.height()), which isn't necessarily a valid assumption. The actual usable area of the screen is a rectangle given by availableGeometry().
When calling setGeometry(), you are setting the new location of the area of the window that the Qt program can actually draw into. However that's only a 99% subset of the actual on-screen area taken up by the window, because the window also includes the non-Qt-controlled regions like the title bar and the window-borders. Those parts need to fit into the availableGeometry() also, otherwise the window won't be positioned quite where you wanted it to be, which can lead to anomalies (like the window getting "stuck" on the top-edge of the screen)
In any case, here's my attempt at rewriting the code to implement a closer-to-correct "bouncing window". Note that it's still a bit glitchy if you try to mouse-drag the window around while the window is also trying to move itself around; ideally the Qt program could detect the mouse-down-event on the title bar and use that to disable its self-animation until after the corresponding mouse-up-event occurs, but AFAICT that isn't possible without resorting to OS-specific hackery, because the window-title-bar-dragging is handled by the OS, not by Qt. Therefore, I'm leaving that logic unimplemented here.
#include <QApplication>
#include <QMainWindow>
#include <QMoveEvent>
#include <QShowEvent>
#include <QScreen>
#include <QTimer>
class Window : public QMainWindow {
public:
Window() : pixelsPerStep(5), moveDelta(pixelsPerStep, pixelsPerStep)
{
updatePosition(); // this will get the QTimer-loop started
}
private:
void updatePosition()
{
const QRect windowFrameRect = frameGeometry(); // our on-screen area including window manager's decorations
const QRect windowRect = geometry(); // our on-screen area including ONLY the Qt-drawable sub-area
// Since setGeometry() sets the area not including the window manager's window-decorations, it
// can end up trying to set the window (including the window-decorations) slightly "out of bounds",
// causing the window to "stick to the top of the screen". To avoid that, we'll adjust (screenRect)
// to be slightly smaller than it really is.
QRect screenRect = QGuiApplication::primaryScreen()->availableGeometry();
screenRect.setTop( screenRect.top() + windowRect.top() - windowFrameRect.top());
screenRect.setBottom( screenRect.bottom() + windowRect.bottom() - windowFrameRect.bottom());
screenRect.setLeft( screenRect.left() + windowRect.left() - windowFrameRect.left());
screenRect.setRight( screenRect.right() + windowRect.right() - windowFrameRect.right());
// Calculate where our window should be positioned next, assuming it continues in a straight line
QRect nextRect = geometry().translated(moveDelta);
// If the window is going to be "off the edge", set it to be exactly on the edge, and reverse our direction
if (nextRect.left() <= screenRect.left()) {nextRect.moveLeft( screenRect.left()); moveDelta.setX( pixelsPerStep);}
if (nextRect.right() >= screenRect.right()) {nextRect.moveRight( screenRect.right()); moveDelta.setX(-pixelsPerStep);}
if (nextRect.top() <= screenRect.top()) {nextRect.moveTop( screenRect.top()); moveDelta.setY( pixelsPerStep);}
if (nextRect.bottom() >= screenRect.bottom()) {nextRect.moveBottom(screenRect.bottom()); moveDelta.setY(-pixelsPerStep);}
setGeometry(nextRect);
QTimer::singleShot(20, [this]() {updatePosition();});
}
const int pixelsPerStep;
QPoint moveDelta; // our current positional-offset-per-step in both X and Y direction
};
int main(int argc, char** argv) {
QApplication app{argc, argv};
Window* w = new Window();
w->show();
return app.exec();
}

Printing QWidget (render) outside of GUI thread

I'm trying to render a QWidget on a QPrinter device without GUI blocking :
My print method looks like this :
void MyClass::print() {
QPrinter *printer = new QPrinter(QPrinter::HighResolution);
printer->setPageSize(QPrinter::A5);
printer->setPageOrientation(QPageLayout::Portrait);
printer->setColorMode(QPrinter::Color);
QPrintDialog *dialog = new QPrintDialog(printer);
if (dialog->exec() == QDialog::Accepted) {
MyWidget *_widget = new MyWidget( /* args */);
QPainter *painter = new QPainter;
painter->begin(printer);
double xscale = printer->pageRect().width() / double(_widget ->width());
double yscale = printer->pageRect().height() / double(_widget ->height());
double scale = qMin(xscale, yscale);
_widget ->setMinimumWidth((printer->pageRect().width() / scale));
_widget ->setMinimumHeight(printer->pageRect().height() / scale);
painter->translate(printer->paperRect().x() + printer->pageRect().width() / 2, printer->paperRect().y() + printer->pageRect().height() / 2);
painter->scale(scale, scale);
painter->translate(-_widget ->width() / 2, -_widget ->height() / 2);
_widget ->render(painter);
painter->end();
}
emit done();
}
With this function i have about 1-2 sec block state so i want to use QThread for this issue But Qt Doc says :
Although QObject is reentrant, the GUI classes, notably QWidget and
all its subclasses, are not reentrant. They can only be used from the
main thread. As noted earlier, QCoreApplication::exec() must also be
called from that thread.
And also :
In practice, the impossibility of using GUI classes in other threads
than the main thread can easily be worked around by putting
time-consuming operations in a separate worker thread and displaying
the results on screen in the main thread when the worker thread is
finished
I've modified Mandelbrot Example but there is nothing to show on screen in my case. my Widget should be rendered (time-consuming operation) and sent to printer that's all.
So do you have anything in mind for my situation ?
If the widget's paintEvent doesn't do much computation, then it'll be very fast to render the widget to a QPicture. A QPicture is just a record of all painter calls. You can then replay them on a printer in a concurrent job.
Alternatively, you can ensure that the widget is not used from the main thread (by staying invisible and not having a parent), and then it's ok to call render from any thread.

Initialise a QGLWidget with a glClearColor

I have a QMainWindow with a QGLWidget in it. I want the widget to display a 'clear' colour of my own choice, instead of the default black screen.
void MyQGLWidget::initializeGL(void) {
glClearColor(0.7f, 0.7f, 0.7f, 1.0f);
}
void MyQGLWidget::paintGL(void) {
qDebug("Painting grey-ness");
glClear(GL_COLOR_BUFFER_BIT);
// ... Do more stuff, but only after a certain event has happened ...
}
This works, and I noticed that the method is called 4 times during the start-up. However, I only want to paint it blank once in the paintGL() method, because this method is being called very often after the start-up, and there actually is small but significant performance loss if the buffers are cleared at every call.
So I changed the code to this:
void MyQGLWidget::paintGL(void) {
static bool screenOnBlank = false;
if (!screenOnBlank) {
qDebug("Painting grey-ness");
glClear(GL_COLOR_BUFFER_BIT);
screenOnBlank = true;
}
// ... Do more stuff, but only after a certain event has happened ...
}
Now the clearing is only done once, but I am left with a black screen, rather that with the grey screen of glClearColor. Why is this? Why does QGLWidget paint my screen black again? And more importantly, how can I make sure the screen is my own default colour, without repainting it blank at every time step?
Short answer:
In most platforms, the state of your backbuffer is undefined after performing a buffer swap. You can find more details here. Hence, you cannot rely on the behaviour that your buffer remains as you left it before the swap operations. Then, to ensure your program is cross-platform, you have no other choice than calling glClear() at each drawing.
In practice:
It is possible that your platform/configuration do guarantee that your buffer is unchanged, or don't guarantee it but it is still the case in practice. If you know you are in those cases after experimenting (see below), but still have your screen turned black, it means that somehow in your code, you did something "wrong" that makes Qt explicitly call glClear() with its own glClearColor().
You can use this code to see if the buffer remains unchanged after swapBuffers(). It is actually the case in my configuration: Qt 4.8 on Linux 64bits:
// -------- main.cpp --------
#include <QApplication>
#include "GLWidget.h"
int main(int argc, char ** argv)
{
QApplication app(argc, argv);
GLWidget * w = new GLWidget();
w->show();
return app.exec();
}
// -------- GLWidget.h --------
#include <QGLWidget>
#include <QTimer>
class GLWidget: public QGLWidget
{
Q_OBJECT
public:
GLWidget() : i(0), N(60)
{
timer.setInterval(16);
connect(&timer, SIGNAL(timeout()),
this, SLOT(animationLoop()));
timer.start();
}
protected:
void initializeGL()
{
glClearColor(1.0, 0.0, 0.0, 1.0);
}
void resizeGL(int w, int h)
{
glViewport(0, 0, (GLint)w, (GLint)h);
}
void paintGL()
{
bool test = false;
if(i<N)
{
if(test)
glClearColor(1.0, 0.0, 0.0, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
}
else
{
if(test)
{
glClearColor(0.0, 1.0, 0.0, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
}
}
}
private slots:
void animationLoop()
{
i++;
if(i>2*N) i=0;
updateGL();
}
private:
int i, N;
QTimer timer;
};
if test == true, then I always call glClearColor() followed by glClear(), but alternating between a red color and a green color every second. I indeed see the color switching back and forth between red and green.
if test == false, then I only call glClearColor() once and for all in initializeGL(). Then in paintGL, I alternate between calling glClear() or not calling it. The screen stays red, i.e. never turns black (or display a unicorn) even when glClear() is not called.
Hence, regarding your problem:
Either your configuration is different than mine (the implementation of swapBuffers is provided by Qt and differs according to the underlying window system)
Or your code is broken.
Simple way to check: compile my code, and see if it still reproduces the issue. If it does, then you are in case 1., and there is nothing you can do about it (can be considered a bug of Qt, of rather an inconsistency, since the correct behaviour is not specified anywhere in the documentation). Otherwise, you are in case 2., and then you should provide more of your code so we could determine where is the issue.

What is the fastest way to get QWidget pixel color under mouse?

I need to get the color of pixel under mouse, inside mouseMoveEvent of a QWidget (Breadboard). Currently I have this code->
void Breadboard::mouseMoveEvent(QMouseEvent *e)
{
QPixmap pixmap = QPixmap::grabWindow(winId());
QRgb color = pixmap.toImage().pixel(e->x(), e->y());
if (QColor(color) == terminalColor)
QMessageBox::information(this, "Ter", "minal");
}
Take a look at (scaled down) screenshot below-
When user moves his mouse on breadboard, the hole should get highlighted with some different color (like in red circle). And when the mouse exits, the previous color (grey) should be restored. So I need to do following steps-
Get color under mouse
According to color, floodfill the hole. (Different holes are distinguished using color)
On mouse out, restore the color. There would be wires going over holes, so I can't update the small rectangle (hole) only.
What is the fastest way of doing this? My attempt to extract color is not working i.e the Message box in my above code never displays. Moreover I doubt if my existing code is fast enough for my purpose. Remember, how fast you will be moving your mouse on breadboard.
Note - I was able to do this using wxWidgets framework. But due to some issues that project got stalled. And I am rewriting it using Qt now.
You are invited to look at code https://github.com/vinayak-garg/dic-sim
The "idiomatic" way of doing this in Qt is completely different from what you're describing. You'd use the Graphics View Framework for this type of thing.
Graphics View provides a surface for managing and interacting with a large number of custom-made 2D graphical items, and a view widget for visualizing the items, with support for zooming and rotation.
You'd define your own QGraphicsItem type for the "cells" in the breadboard that would react to hover enter/leave events by changing their color. The connections between the cells (wires, resistors, whatever) would also have their own graphics item types with the features you need for those.
Here's a quick and dirty example for you. It produces a 50x50 grid of green cells that become red when the mouse is over them.
#include <QtGui>
class MyRect: public QGraphicsRectItem
{
public:
MyRect(qreal x, qreal y, qreal w, qreal h)
: QGraphicsRectItem(x,y,w,h) {
setAcceptHoverEvents(true);
setBrush(Qt::green);
}
protected:
void hoverEnterEvent(QGraphicsSceneHoverEvent *) {
setBrush(Qt::red);
update();
}
void hoverLeaveEvent(QGraphicsSceneHoverEvent *) {
setBrush(Qt::green);
update();
}
};
int main(int argc, char **argv)
{
QApplication app(argc, argv);
QGraphicsScene scene;
for (int i=0; i<50; i++)
for (int j=0; j<50; j++)
scene.addItem(new MyRect(10*i, 10*j, 8, 8));
QGraphicsView view(&scene);
view.show();
return app.exec();
}
You could modify the hover event handlers to talk to your "main window" or "controller" indicating what's currently under the mouse so you can update your caption, legend box or tool palette.
For best speed, render only the portion of the widget you're interested in into a QPaintDevice (like a QPixmap). Try something like this:
void Breadboard::mouseMoveEvent(QMouseEvent *e)
{
// Just 1 pixel.
QPixmap pixmap(1, 1);
// Target coordinates inside the pixmap where drawing should start.
QPoint targetPos(0, 0);
// Source area inside the widget that should be rendered.
QRegion sourceArea( /* use appropriate coordinates from the mouse event */ );
// Render it.
this->render(&pixmap, targetPos, sourceArea, /* look into what flags you need */);
// Do whatever else you need to extract the color from the 1 pixel pixmap.
}
Mat's answer is better if you're willing to refactor your application to use the graphics view API.

Sleep inside a loop that uses paintevent in qt c++

Basically what I wanna do is to draw rectangles for each number in my list. The bigger the number is, the larger the rectangle is.
My problem is when I actually wanna do it, step-by-step, and waiting a few seconds between every drawing. I've looked out for a few solutions but I can't get them to work for this particular case. I saw I could use fflush to release whatever it's in the buffer but I don't know how I can use it for this.
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
painter.setBrush(QBrush(Qt::green, Qt::SolidPattern));
int weight=300/lista.size;
int posx=weight;
for (int i=1; i<=lista.size; i++){
List_node * node = list.get_element_at(i);
int num=node->getValue(); //this returns the value of the node
if (i==3){
painter.setBrush(QBrush(Qt::red, Qt::SolidPattern)); // this line is to draw a rectangle with a different color. Testing purposes.
}
painter.drawRect(posx,400-(num*10),weight,num*10);
sleep(1); //this sleep isn't working correctly.
painter.setBrush(QBrush(Qt::green, Qt::SolidPattern));
posx+=weight;
}
Any help would be really appreciated.
sleep() won't work for this -- it blocks the Qt event loop and keeps Qt from doing its job while it is sleeping.
What you need to do is keep one or more member variables to remember the current state of the image you want to draw, and implement paintEvent() to draw that current single image only. paintEvent() (like every function running in Qt's GUI thread) should always return immediately, and never sleep or block.
Then, to implement the animation part of things, set up a QTimer object to call a slot for you at regular intervals (e.g. once every 1000mS, or however often you like). Implement that slot to adjust your member variables to their next state in the animation-sequence (e.g. rectangle_size++ or whatever) and then call update() on your widget. update() will tell Qt to call paintEvent() again on your widget as soon as possible, so your display will be updated to the next frame very shortly after your slot method returns.
Below is a trivial example of the technique; when run it shows a red rectangle getting larger and smaller:
// begin demo.h
#include <QWidget>
#include <QTimer>
class DemoObj : public QWidget
{
Q_OBJECT
public:
DemoObj();
virtual void paintEvent(QPaintEvent * e);
public slots:
void AdvanceState();
private:
QTimer _timer;
int _rectSize;
int _growthDirection;
};
// begin demo.cpp
#include <QApplication>
#include <QPainter>
#include "demo.h"
DemoObj :: DemoObj() : _rectSize(10), _growthDirection(1)
{
connect(&_timer, SIGNAL(timeout()), this, SLOT(AdvanceState()));
_timer.start(100); // 100 milliseconds delay per frame. You might want to put 2000 here instead
}
void DemoObj :: paintEvent(QPaintEvent * e)
{
QPainter p(this);
p.fillRect(rect(), Qt::white);
QRect r((width()/2)-_rectSize, (height()/2)-_rectSize, (_rectSize*2), (_rectSize*2));
p.fillRect(r, Qt::red);
}
void DemoObj :: AdvanceState()
{
_rectSize += _growthDirection;
if (_rectSize > 50) _growthDirection = -1;
if (_rectSize < 10) _growthDirection = 1;
update();
}
int main(int argc, char ** argv)
{
QApplication app(argc, argv);
DemoObj obj;
obj.resize(150, 150);
obj.show();
return app.exec();
}