Adr*_* Jr 7 optimization qt qgraphicsview qgraphicsitem qgraphicsscene
我正在开发一个使用Qt 5.6.2的CAD应用程序,它需要在廉价的计算机上运行,同时它需要处理同一场景中的数千个项目.因此,我必须进行大量实验才能获得最佳性能.
我决定创建这篇文章来帮助其他人和我自己,只要其他人也提供更多优化技巧.
我的文本仍在进行中,如果我发现更好的技术(或者说我说的话非常愚蠢),我可能会更新它.
Adr*_* Jr 22
禁用场景交互
事件处理由QGraphicsView引擎的大部分CPU使用负责.在每次鼠标移动时,视图会向场景询问鼠标下的项目,该项目调用QGraphicsItem :: shape()方法来检测交叉点.即使是残疾人物品也会发生.因此,如果您不需要场景与鼠标事件交互,则可以设置QGraphicsView :: setIntenteractive(false).在我的情况下,我的工具中有两种模式(测量和移动/旋转),其中场景基本上是静态的,所有编辑操作都由QGraphicsView执行.通过这样做,我能够将帧速率提高30%,遗憾的是,ViewportAnchor :: AnchorUnderMouse停止工作(修复它的一个方法是重新启用交互并覆盖QGraphicsView :: mouseMoveEvent(QMouseEvent e)以人工按下其中一个鼠标按钮使用基于e)的新QMouseEvent.
重用您的QPainterPaths
在QGraphicsItem对象中缓存QPainterPaths.构建和填充它可能非常慢.在我的情况下,仅仅因为我将6000点的点云转换为具有多个矩形的QPainterPath,所以读取文件需要6秒钟.你不会想要多次这样做.
简化你的QGraphicsItem :: shape()
即使未启用该项,也会在鼠标事件期间多次调用此方法.尽量使其尽可能高效.有时,即使缓存QPainterPath也是不够的,因为由场景执行的路径交叉算法对于复杂的形状来说可能非常慢.在我的情况下,我返回一个大约6000个矩形的形状,它很慢.在对点云进行下采样之后,我能够将矩形的数量减少到大约1000,这显着改善了性能,但仍然不理想,因为即使项目被禁用,仍然会调用shape().因此,我决定维护原始的QGraphicsItem:shape()(返回边界框矩形),并在启用项目时返回更复杂的缓存形状.它在移动鼠标时提高了帧速率近40%,但我仍然认为这是一个黑客,如果我想出一个更好的解决方案,它会更新这篇文章.尽管如此,在我的测试中,只要我保持其边界框不受影响,我就没有任何问题.如果不是这种情况,则必须调用prepareGeometryChange(),然后在其他位置更新边界框和形状缓存.
测试两者:Raster和OpenGL引擎
我期待OpenGL总是比光栅更好,如果您想要的只是为了减少CPU使用率,这可能是正确的.但是,如果您只想增加每秒的帧数,特别是在便宜/旧计算机上,那么值得尝试测试栅格(默认的QGraphicsView视口).在我的测试中,新的QOpenGLWidget比旧的QGLWidget略快,但FPS的数量比使用Raster慢近20%.当然,它可以是特定于应用程序的,结果可能会有所不同,具体取决于您呈现的内容.
将FullViewportUpdate与OpenGL一起使用,并且更喜欢使用栅格的其他部分视口更新方法(虽然需要更严格的边界矩形维护项目).
尝试禁用/启用VSync以查看哪个更适合您:QSurfaceFormat :: defaultFormat().setSwapInterval(0或1).启用可以降低帧速率,禁用可能会导致"撕裂". https://www.khronos.org/opengl/wiki/Swap_Interval
缓存复杂QGraphicsItems
如果您的QGraphicsItem :: paint操作过于复杂且相同类型大部分是静态的,请尝试启用缓存.如果不对项目或ItemCoordinateCache应用转换(如旋转),请使用DeviceCoordinateCache.避免经常调用QGraphicsItem :: update(),或者甚至比没有缓存时更慢.如果你需要更改项目中的某些内容,有两个选项:在子项中绘制它,或使用QGraphicsView :: drawForeground().
组相似的QPainter绘图操作
首选drawLines多次调用drawLine; 赞成drawPoint的drawPoints.使用QVarLengthArray(使用堆栈,因此可以更快)或QVector(使用堆)作为容器.避免经常更换画笔(我怀疑使用OpenGL时更重要).此外,QPoint可以更快,并且比QPointF更小.
喜欢使用化妆品线绘图,避免透明度和抗锯齿
抗锯齿可以被禁用,特别是如果您正在绘制的是水平,垂直或45度线(它们实际上看起来更好)或者您正在使用"视网膜"显示.
搜索热点
在令人惊讶的地方可能会出现瓶颈.使用分析器或其他方法,如经过时间计时器,qDebug或FPS计数器(我把它放在我的QGraphicsView :: drawForeground中)来帮助定位它们.不要让你的代码难看,试图优化你不确定它们是否是热点的东西.FPS计数器的示例(尽量将其保持在25以上):
MyGraphicsView:: MyGraphicsView(){
...
timer = new QTimer(this);
connect(timer, SIGNAL(timeout()), this, SLOT(oneSecTimeout()));
timer->setInterval(1000);
timer->start();
}
void MyGraphicsView::oneSecTimeout()
{
frameRate=(frameRate+numFrames)/2;
qInfo() << frameRate;
numFrames=0;
}
Run Code Online (Sandbox Code Playgroud)
http://doc.qt.io/qt-4.8/qelapsedtimer.html
避免深层复制
在迭代QT容器时,使用foreach(const auto&item,items),const_iterator或items.at(i)而不是items [i],以避免分离.使用const运算符并尽可能多地调用const方法.总是尝试初始化(reserve())您的矢量/数组,并估计其实际大小. https://www.slideshare.net/qtbynokia/optimizing-performance-in-qtbased-applications/37-Implicit_data_sharing_in_Qt
场景索引
对于具有少量项目和/或动态场景(具有动画)的场景,以及具有许多(主要是静态)项目的场景的BspTreeIndex,支持NoIndex.使用QGraphicsScene :: itemAt()方法时,BspTreeIndex允许快速搜索.
不同缩放级别的不同绘制算法
与Qt 40000芯片示例中一样,您无需使用相同的详细绘图算法来绘制在屏幕上看起来非常小的内容.您可以为此任务使用2个不同的QPainterPath缓存对象,或者在我的情况下,使用2个不同的点云向量(一个带有原始向量的简化子集,另一个带有补码).因此,根据缩放级别,我绘制一个或两个.另一个选项是根据缩放级别对点云进行随机播放并仅绘制向量的n个第一个元素.最后一项技术单独将帧速率从5帧提高到15帧/秒(在我最初有100万点的场景中).在QGraphicsItem :: painter()中使用类似于:
const qreal lod = option->levelOfDetailFromTransform(painter->worldTransform());
const int n = qMin(pointCloud.size(), pointCloud.size() * lod/0.08);
painter->drawPoints(pointCloud.constData(), n);
Run Code Online (Sandbox Code Playgroud)
超大你的QGraphicsScene :: sceneRect()
如果您不断增加场景矩形的大小,重建索引可能会在短时间内冻结您的应用程序.为避免这种情况,您可以设置固定大小或添加和删除临时矩形以强制场景增加到更大的初始大小:
auto marginRect = addRect(sceneRect().adjusted(-25000, -25000, 25000, 25000));
sceneRect(); // hack to force update of scene bounding box
delete marginRect;
Run Code Online (Sandbox Code Playgroud)
禁用Scroolbars
如果在粉碎场景时视图闪烁,禁用scroolbars可以修复它:
setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOff );
setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOff );
Run Code Online (Sandbox Code Playgroud)
使用分组将鼠标控制的转换应用于多个项目
使用QGraphicsScene :: createItemGroup()进行分组可避免在转换期间多次调用QGraphicsItem :: itemChange.只有在创建和销毁组时才会调用它.
比较多个Qt版本
我还没有足够的时间去研究它,但至少在我目前的项目中,Qt 5.6.2(在Mac OS上)比Qt 5.8快得多.
我的应用程序虽然不完全是 CAD 程序,但与 CAD 类似,因为它允许用户在空间中构建各种项目的“蓝图”,并且允许用户添加任意数量的项目,并且某些用户可以添加任意数量的项目。 ' 设计可能会变得非常拥挤和复杂,同时出现数百或数千个项目。
视图中的大多数项目或多或少都是静态的(即,仅当用户单击/拖动它们时,它们才会移动或改变外观,但这种情况很少见)。但场景中通常也有一些前景项目不断动画并以 20 fps 的速度移动。
为了避免必须定期重新渲染复杂的静态元素,QGraphicsView每当它们中的任何一个发生更改,或者每当更改的缩放/平移/大小设置时,我都会将所有静态元素预渲染到 的后台缓存中QGraphicsView,并且排除它们作为正常前景视图重绘过程的一部分进行渲染。
这样,当有移动元素以QGraphicsView20 fps 左右运行时,所有数量众多且复杂的静态对象都QGraphicsScene::drawBackground()通过一次调用来绘制(通过 中的代码),而drawPixmap()不必通过算法重新渲染每个对象单独项目。然后可以按照通常的方式在顶部绘制始终移动的元素。
实现这一点涉及到调用setOptimizationFlag(IndirectPainting)和,并且每当任何静态项的任何方面发生变化时也调用它们(以便缓存的背景图像将尽快重新渲染)。setCacheMode(CacheBackground)QGraphicsViewresetCachedContent()
唯一棘手的部分是让所有“背景”在的回调QGraphicsItems内渲染,并且不在通常的回调内渲染(通常比 更频繁地调用)。QGraphicsScenedrawBackground()QGraphicsScene::drawItems()QGraphicsScene::drawBackground()
QGraphicsScene在我的压力测试中,相对于“普通” /方法,这将我的程序的稳态 CPU 使用率减少了大约 50% QGraphicsView(如果我通过调用使用 OpenGL,则减少了大约 80% setViewport(new QOpenGLWidget)QGraphicsView。
唯一的缺点(除了增加代码复杂性之外)是这种方法依赖于使用QGraphicsView::setOptimizationFlag(IndirectPainting)和QGraphicsView::drawItems(),这两种方法或多或少都已被 Qt 弃用,因此这种方法可能无法继续适用于未来的 Qt 版本。(不过它至少可以在 Qt 5.10.1 上运行;这是我尝试过的最新 Qt 版本)
一些说明性代码:
void MyGraphicsScene :: drawBackground(QPainter * p, const QRectF & r)
{
if (_isInBackgroundUpdate == false) // anti-infinite-recursion guard
{
QGraphicsScene::drawBackground(p, r);
const QRectF rect = sceneRect();
p->fillRect(rect, backgroundBrush().color());
// Render the scene's static objects as pixels
// into the QGraphicsView's view-background-cache
this->_isInBackgroundUpdate = true; // anti-infinite-recursion guard
render(p, sceneRect());
this->_isInBackgroundUpdate = false;
}
}
// overridden to draw only the items appropriate to our current
// mode (foreground items OR background items but not both!)
void MyGraphicsScene :: drawItems(QPainter *painter, int numItems, QGraphicsItem *items[], const QStyleOptionGraphicsItem options[], QWidget *widget)
{
// Go through the items-list and only keep items that we are supposed to be
// drawing in this pass (either foreground or background, depending)
int count = 0;
for (int i=0; i<numItems; i++)
{
const bool isItemBackgroundItem = (_backgroundItemsTable.find(items[i]) != _backgroundItemsTable.end());
if (isItemBackgroundItem == this->_isInBackgroundUpdates) items[count++] = items[i];
}
QGraphicsScene::drawItems(painter, count, items, options, widget);
}
Run Code Online (Sandbox Code Playgroud)