如何优化 QGraphicsView 的性能?
How to optimize QGraphicsView's performance?
我正在使用 Qt 5.6.2 开发 CAD 应用程序,它需要在廉价计算机中 运行 同时处理同一场景中的数千个项目。因此,我必须进行大量实验才能获得最佳性能。
我决定创建这个 post 来帮助他人和我自己,只要其他人也提供更多优化技巧。
我的文章仍在进行中,如果我发现更好的技术(或者我说了一些非常愚蠢的话),我可能会更新它。
禁用场景交互
事件处理由 QGraphicsView 引擎的 CPU 使用的很大一部分负责。在每次鼠标移动时,视图都会向场景询问鼠标下的项目,这会调用 QGraphicsItem::shape() 方法来检测交叉点。即使是禁用的物品也会发生这种情况。所以,如果你不需要你的场景与鼠标事件交互,你可以设置QGraphicsView::setIntenteractive(false)。就我而言,我的工具中有两种模式(测量和 move/rotate),其中场景基本上是静态的,所有编辑操作都由 QGraphicsView 执行。通过这样做,我能够将帧速率提高 30%,不幸的是 ViewportAnchor::AnchorUnderMouse 停止工作。
重用您的 QPainterPaths
将 QPainterPaths 缓存在 QGraphicsItem 对象中。构建和填充它可能非常缓慢。在我的例子中,读取文件需要 6 秒,因为我正在将具有 6000 个点的点云转换为具有多个矩形的 QPainterPath。你不会想做不止一次。
此外,在 Qt 5.13 中,现在可以保留 QPainterPaths 的内部向量大小,避免在增长时出现多个副本。
简化您的 QGraphicsItem::shape()
此方法在鼠标事件期间被多次调用,即使该项目未启用。尝试使其尽可能高效。
有时,即使缓存 QPainterPath 也是不够的,因为场景执行的路径相交算法对于复杂形状来说可能非常慢。在我的例子中,我正在 return 绘制一个包含大约 6000 个矩形的形状,速度非常慢。在对点云进行下采样后,我能够将矩形的数量减少到 1000 左右,这显着提高了性能,但仍然不理想,因为即使在禁用项目时仍会调用 shape()。因此,我决定保留原始的 QGraphicsItem:shape()(return 是边界框矩形),并且只在启用项目时 return 更复杂的缓存形状。它在移动鼠标时提高了近 40% 的帧率,但我仍然认为这是一个 hack,如果我想出更好的解决方案,我会更新这个 post。尽管如此,在我的测试中,只要我保持其边界框不变,我就没有任何问题。如果不是这种情况,则必须调用 prepareGeometryChange(),然后更新边界框和别处的形状缓存。
同时测试:Raster 和 OpenGL 引擎
我原以为 OpenGL 总是比光栅更好,如果出于显而易见的原因您只想减少 CPU 的使用,这可能是真的。但是,如果您只想增加每秒的帧数,尤其是在 cheap/old 计算机中,那么也值得尝试测试光栅(默认的 QGraphicsView 视口)。在我的测试中,新的 QOpenGLWidget 比旧的 QGLWidget 稍快,但 FPS 的数量比使用 Raster 慢了近 20%。当然,它可以是特定于应用程序的,结果可能会因渲染内容而异。
将 FullViewportUpdate 与 OpenGL 一起使用,并更喜欢使用光栅的其他部分视口更新方法(尽管需要更严格的边界矩形维护项目)。
尝试 disable/enable 垂直同步,看看哪个更适合您:QSurfaceFormat::defaultFormat().setSwapInterval(0 或 1)。启用会降低帧率,禁用会导致“撕裂”。
https://www.khronos.org/opengl/wiki/Swap_Interval
缓存复杂 QGraphicsItems
如果您的 QGraphicsItem::paint 操作过于复杂并且同一类型大多是静态的,请尝试启用缓存。如果您不对项目应用变换(如旋转),则使用 DeviceCoordinateCache,否则使用 ItemCoordinateCache。避免经常调用 QGraphicsItem::update() ,否则它可能比没有缓存更慢。如果您需要更改项目中的某些内容,有两种选择:将其绘制在子项中,或使用 QGraphicsView::drawForeground().
分组类似QPainter绘图操作
优先使用 drawLines 而不是多次调用 drawLine;比 drawPoint 更喜欢 drawPoints。使用 QVarLengthArray(使用堆栈,因此可以更快)或 QVector(使用堆)作为容器。避免经常更换画笔(我怀疑这在使用 OpenGL 时更重要)。 QPoint 也可以比 QPointF 更快并且更小。
更喜欢使用修饰线绘图,避免透明和抗锯齿
可以禁用抗锯齿功能,尤其是当您绘制的都是水平线、垂直线或 45 度线(实际上这样看起来更好)或者您使用的是“视网膜”显示器时。
搜索热点
瓶颈可能出现在意想不到的地方。使用探查器(在 macOS 中我使用 Instruments/Time Profiler)或其他方法,如耗时计时器、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;
}
void MyGraphicsView::drawForeground(QPainter * painter, const QRectF & rect)
{
numFrames++;
//...
}
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() ) 您的 vectors/arrays 并对其实际大小进行良好估计。
https://www.slideshare.net/qtbynokia/optimizing-performance-in-qtbased-applications/37-Implicit_data_sharing_in_Qt
场景索引
对于项目较少的场景,支持 NoIndex and/or 动态场景(带动画),对于项目多(主要是静态)的场景,支持 BspTreeIndex。 BspTreeIndex 允许在使用 QGraphicsScene::itemAt() 方法时进行快速搜索。
不同缩放级别的不同绘制算法
与 Qt 40000 芯片示例中一样,您不需要使用相同的详细绘图算法来绘制在屏幕上看起来很小的东西。您可以为此任务使用 2 个不同的 QPainterPath 缓存对象,或者像我的情况一样,有 2 个不同的点云向量(一个具有原始向量的简化子集,另一个具有补集)。因此,根据缩放级别,我绘制一个或两个。另一种选择是根据缩放级别打乱点云并仅绘制矢量的前 n 个元素。最后一项技术将我的帧速率从 5fps 增加到 15fps(在我最初有 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);
加大你的 QGraphicsScene::sceneRect()
如果您不断增加场景矩形的大小,重建索引可能会在短时间内冻结您的应用程序。为避免这种情况,您可以设置固定大小或添加和删除临时矩形以强制场景增加到更大的初始大小:
auto marginRect = addRect(sceneRect().adjusted(-25000, -25000, 25000, 25000));
sceneRect(); // hack to force update of scene bounding box
delete marginRect;
禁用滚动条
如果滚动场景时视图闪烁,禁用滚动条可以解决这个问题:
setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOff );
setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOff );
使用分组将鼠标控制的变换应用于多个项目
使用 QGraphicsScene::createItemGroup() 分组可避免在转换过程中多次调用 QGraphicsItem::itemChange。仅在组创建和销毁时调用两次。
比较多个Qt版本
我还没有足够的时间来研究它,但至少在我当前的项目中,Qt 5.6.2(在 Mac OS 上)比 Qt 5.8 快得多。
我的应用程序虽然不完全是 CAD 程序,但它类似于 CAD,因为它允许用户在 space 中构建各种项目的 "blueprint",并且允许用户随心所欲地添加项目,一些用户的设计会变得非常拥挤和复杂,一次出现成百上千个项目。
视图中的大多数项目将或多或少是静态的(即,只有当用户 clicks/drags 在它们上面时,它们才会移动或改变外观,这种情况很少见)。但场景中通常也有一些前景项目不断动画并以 20fps 的速度四处移动。
为了避免必须定期重新渲染复杂的静态元素,我将所有静态元素预渲染到 QGraphicsView
的后台缓存中,只要它们中的任何一个发生变化,或者每当 zoom/pan/size QGraphicsView
的设置更改,并排除它们作为正常前景视图重绘过程的一部分呈现。
这样,当 QGraphicsView
周围有 20fps 的移动元素 运行 时,将绘制所有数量众多且精心设计的静态对象(通过 QGraphicsScene::drawBackground()
) 通过对 drawPixmap()
的一次调用,而不必通过算法单独重新呈现每个项目。然后可以以通常的方式在顶部绘制始终移动的元素。
实现这一点涉及在 QGraphicsView
(s) 上调用 setOptimizationFlag(IndirectPainting)
和 setCacheMode(CacheBackground)
,并在任何静态的任何方面调用它们 resetCachedContent()
-项目更改(以便尽快重新呈现缓存背景图像)。
唯一棘手的部分是让所有 "background" QGraphicsItems
在 QGraphicsScene
的 drawBackground()
回调中呈现,并且不在通常的内部呈现QGraphicsScene::drawItems()
回调(通常比 QGraphicsScene::drawBackground()
调用得更频繁)。
在我的压力测试中,相对于 "vanilla" QGraphicsScene
/QGraphicsView
方法(并且通过如果我通过在 QGraphicsView
).
上调用 setViewport(new QOpenGLWidget
) 使用 OpenGL,则大约 80%
唯一的缺点(除了增加代码的复杂性)是这种方法依赖于使用 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);
}
我正在使用 Qt 5.6.2 开发 CAD 应用程序,它需要在廉价计算机中 运行 同时处理同一场景中的数千个项目。因此,我必须进行大量实验才能获得最佳性能。
我决定创建这个 post 来帮助他人和我自己,只要其他人也提供更多优化技巧。
我的文章仍在进行中,如果我发现更好的技术(或者我说了一些非常愚蠢的话),我可能会更新它。
禁用场景交互
事件处理由 QGraphicsView 引擎的 CPU 使用的很大一部分负责。在每次鼠标移动时,视图都会向场景询问鼠标下的项目,这会调用 QGraphicsItem::shape() 方法来检测交叉点。即使是禁用的物品也会发生这种情况。所以,如果你不需要你的场景与鼠标事件交互,你可以设置QGraphicsView::setIntenteractive(false)。就我而言,我的工具中有两种模式(测量和 move/rotate),其中场景基本上是静态的,所有编辑操作都由 QGraphicsView 执行。通过这样做,我能够将帧速率提高 30%,不幸的是 ViewportAnchor::AnchorUnderMouse 停止工作。
重用您的 QPainterPaths
将 QPainterPaths 缓存在 QGraphicsItem 对象中。构建和填充它可能非常缓慢。在我的例子中,读取文件需要 6 秒,因为我正在将具有 6000 个点的点云转换为具有多个矩形的 QPainterPath。你不会想做不止一次。 此外,在 Qt 5.13 中,现在可以保留 QPainterPaths 的内部向量大小,避免在增长时出现多个副本。
简化您的 QGraphicsItem::shape()
此方法在鼠标事件期间被多次调用,即使该项目未启用。尝试使其尽可能高效。 有时,即使缓存 QPainterPath 也是不够的,因为场景执行的路径相交算法对于复杂形状来说可能非常慢。在我的例子中,我正在 return 绘制一个包含大约 6000 个矩形的形状,速度非常慢。在对点云进行下采样后,我能够将矩形的数量减少到 1000 左右,这显着提高了性能,但仍然不理想,因为即使在禁用项目时仍会调用 shape()。因此,我决定保留原始的 QGraphicsItem:shape()(return 是边界框矩形),并且只在启用项目时 return 更复杂的缓存形状。它在移动鼠标时提高了近 40% 的帧率,但我仍然认为这是一个 hack,如果我想出更好的解决方案,我会更新这个 post。尽管如此,在我的测试中,只要我保持其边界框不变,我就没有任何问题。如果不是这种情况,则必须调用 prepareGeometryChange(),然后更新边界框和别处的形状缓存。
同时测试:Raster 和 OpenGL 引擎
我原以为 OpenGL 总是比光栅更好,如果出于显而易见的原因您只想减少 CPU 的使用,这可能是真的。但是,如果您只想增加每秒的帧数,尤其是在 cheap/old 计算机中,那么也值得尝试测试光栅(默认的 QGraphicsView 视口)。在我的测试中,新的 QOpenGLWidget 比旧的 QGLWidget 稍快,但 FPS 的数量比使用 Raster 慢了近 20%。当然,它可以是特定于应用程序的,结果可能会因渲染内容而异。
将 FullViewportUpdate 与 OpenGL 一起使用,并更喜欢使用光栅的其他部分视口更新方法(尽管需要更严格的边界矩形维护项目)。
尝试 disable/enable 垂直同步,看看哪个更适合您:QSurfaceFormat::defaultFormat().setSwapInterval(0 或 1)。启用会降低帧率,禁用会导致“撕裂”。 https://www.khronos.org/opengl/wiki/Swap_Interval
缓存复杂 QGraphicsItems
如果您的 QGraphicsItem::paint 操作过于复杂并且同一类型大多是静态的,请尝试启用缓存。如果您不对项目应用变换(如旋转),则使用 DeviceCoordinateCache,否则使用 ItemCoordinateCache。避免经常调用 QGraphicsItem::update() ,否则它可能比没有缓存更慢。如果您需要更改项目中的某些内容,有两种选择:将其绘制在子项中,或使用 QGraphicsView::drawForeground().
分组类似QPainter绘图操作
优先使用 drawLines 而不是多次调用 drawLine;比 drawPoint 更喜欢 drawPoints。使用 QVarLengthArray(使用堆栈,因此可以更快)或 QVector(使用堆)作为容器。避免经常更换画笔(我怀疑这在使用 OpenGL 时更重要)。 QPoint 也可以比 QPointF 更快并且更小。
更喜欢使用修饰线绘图,避免透明和抗锯齿
可以禁用抗锯齿功能,尤其是当您绘制的都是水平线、垂直线或 45 度线(实际上这样看起来更好)或者您使用的是“视网膜”显示器时。
搜索热点
瓶颈可能出现在意想不到的地方。使用探查器(在 macOS 中我使用 Instruments/Time Profiler)或其他方法,如耗时计时器、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;
}
void MyGraphicsView::drawForeground(QPainter * painter, const QRectF & rect)
{
numFrames++;
//...
}
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() ) 您的 vectors/arrays 并对其实际大小进行良好估计。 https://www.slideshare.net/qtbynokia/optimizing-performance-in-qtbased-applications/37-Implicit_data_sharing_in_Qt
场景索引
对于项目较少的场景,支持 NoIndex and/or 动态场景(带动画),对于项目多(主要是静态)的场景,支持 BspTreeIndex。 BspTreeIndex 允许在使用 QGraphicsScene::itemAt() 方法时进行快速搜索。
不同缩放级别的不同绘制算法
与 Qt 40000 芯片示例中一样,您不需要使用相同的详细绘图算法来绘制在屏幕上看起来很小的东西。您可以为此任务使用 2 个不同的 QPainterPath 缓存对象,或者像我的情况一样,有 2 个不同的点云向量(一个具有原始向量的简化子集,另一个具有补集)。因此,根据缩放级别,我绘制一个或两个。另一种选择是根据缩放级别打乱点云并仅绘制矢量的前 n 个元素。最后一项技术将我的帧速率从 5fps 增加到 15fps(在我最初有 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);
加大你的 QGraphicsScene::sceneRect()
如果您不断增加场景矩形的大小,重建索引可能会在短时间内冻结您的应用程序。为避免这种情况,您可以设置固定大小或添加和删除临时矩形以强制场景增加到更大的初始大小:
auto marginRect = addRect(sceneRect().adjusted(-25000, -25000, 25000, 25000));
sceneRect(); // hack to force update of scene bounding box
delete marginRect;
禁用滚动条
如果滚动场景时视图闪烁,禁用滚动条可以解决这个问题:
setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOff );
setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOff );
使用分组将鼠标控制的变换应用于多个项目
使用 QGraphicsScene::createItemGroup() 分组可避免在转换过程中多次调用 QGraphicsItem::itemChange。仅在组创建和销毁时调用两次。
比较多个Qt版本
我还没有足够的时间来研究它,但至少在我当前的项目中,Qt 5.6.2(在 Mac OS 上)比 Qt 5.8 快得多。
我的应用程序虽然不完全是 CAD 程序,但它类似于 CAD,因为它允许用户在 space 中构建各种项目的 "blueprint",并且允许用户随心所欲地添加项目,一些用户的设计会变得非常拥挤和复杂,一次出现成百上千个项目。
视图中的大多数项目将或多或少是静态的(即,只有当用户 clicks/drags 在它们上面时,它们才会移动或改变外观,这种情况很少见)。但场景中通常也有一些前景项目不断动画并以 20fps 的速度四处移动。
为了避免必须定期重新渲染复杂的静态元素,我将所有静态元素预渲染到 QGraphicsView
的后台缓存中,只要它们中的任何一个发生变化,或者每当 zoom/pan/size QGraphicsView
的设置更改,并排除它们作为正常前景视图重绘过程的一部分呈现。
这样,当 QGraphicsView
周围有 20fps 的移动元素 运行 时,将绘制所有数量众多且精心设计的静态对象(通过 QGraphicsScene::drawBackground()
) 通过对 drawPixmap()
的一次调用,而不必通过算法单独重新呈现每个项目。然后可以以通常的方式在顶部绘制始终移动的元素。
实现这一点涉及在 QGraphicsView
(s) 上调用 setOptimizationFlag(IndirectPainting)
和 setCacheMode(CacheBackground)
,并在任何静态的任何方面调用它们 resetCachedContent()
-项目更改(以便尽快重新呈现缓存背景图像)。
唯一棘手的部分是让所有 "background" QGraphicsItems
在 QGraphicsScene
的 drawBackground()
回调中呈现,并且不在通常的内部呈现QGraphicsScene::drawItems()
回调(通常比 QGraphicsScene::drawBackground()
调用得更频繁)。
在我的压力测试中,相对于 "vanilla" QGraphicsScene
/QGraphicsView
方法(并且通过如果我通过在 QGraphicsView
).
setViewport(new QOpenGLWidget
) 使用 OpenGL,则大约 80%
唯一的缺点(除了增加代码的复杂性)是这种方法依赖于使用 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);
}