Qt Quick 场景图默认渲染器

本文档解释了默认场景图渲染器在内部是如何工作的,以便人们可以编写代码,以最佳的方式使用它,无论是在性能还是功能方面。

不需要理解渲染器的内部工作原理也能获得良好的性能。然而,当与场景图集成或试图找出为什么无法从图形芯片中挤出最大效率时,了解这些内部工作原理可能会有所帮助。

注意

即使在每一帧都是唯一的并且所有内容都是从零开始上传的情况下,默认渲染器也会表现良好。

QML场景中的Qt Quick项目填充了一个QSGNode实例的树。一旦创建,这棵树就是如何渲染某一帧的完整描述。它完全不包含任何对Qt Quick项目的引用,并且在大多数平台上会在一个单独的线程中处理和渲染。渲染器是场景图的一个独立部分,它遍历QSGNode树,并使用在QSGGeometryNode中定义的几何形状和在QSGMaterial中定义的着色器状态来更新图形状态并生成绘制调用。

如果需要,可以使用内部场景图后端API完全替换渲染器。这对于希望利用非标准硬件特性的平台供应商来说非常有趣。对于大多数用例,默认渲染器将足够。

默认渲染器专注于两种主要策略来优化渲染:绘制调用的批处理,以及在GPU上保留几何体。

批处理

而传统的2D API,如QPainter、Cairo或Context2D,是为了处理每帧数千个单独的绘制调用而编写的,OpenGL和其他硬件加速的API在绘制调用数量非常少且状态变化保持在最低限度时表现最佳。

注意

虽然以下部分以OpenGL为例,但相同的概念也适用于其他图形API。

考虑以下用例:

../_images/visualcanvas_list.png

绘制此列表的最简单方法是逐个单元格进行。首先,绘制背景。这是一个特定颜色的矩形。在OpenGL术语中,这意味着选择一个着色器程序来进行纯色填充,设置填充颜色,设置包含x和y偏移的变换矩阵,然后使用例如glDrawArrays来绘制组成矩形的两个三角形。接下来绘制图标。在OpenGL术语中,这意味着选择一个着色器程序来绘制纹理,选择要使用的活动纹理,设置变换矩阵,启用alpha混合,然后使用例如glDrawArrays来绘制组成图标边界矩形的两个三角形。单元格之间的文本和分隔线遵循类似的模式。对于列表中的每个单元格,此过程都会重复,因此对于较长的列表,OpenGL状态更改和绘制调用所带来的开销完全超过了使用硬件加速API可能带来的好处。

当每个原语很大时,这种开销可以忽略不计,但在典型的用户界面中,有许多小项目会累积成相当大的开销。

默认的场景图渲染器在这些限制下工作,并会尝试将各个图元合并成批次,同时保持完全相同的视觉效果。结果是减少了OpenGL状态更改和最小化的绘制调用,从而实现最佳性能。

不透明原语

渲染器区分不透明图元和需要alpha混合的图元。通过使用OpenGL的Z缓冲区并为每个图元分配唯一的z位置,渲染器可以自由地重新排序不透明图元,而无需考虑它们在屏幕上的位置以及它们与其他元素的重叠情况。通过查看每个图元的材质状态,渲染器将创建不透明批次。在Qt Quick核心项目集中,这包括具有不透明颜色的矩形项目和完全不透明的图像,例如JPEG或BMP。

使用不透明原语的另一个好处是,不透明原语不需要启用GL_BLEND,这可能会非常耗费资源,尤其是在移动和嵌入式GPU上。

不透明原语在启用glDepthMaskGL_DEPTH_TEST的情况下以从前到后的方式渲染。在内部进行早期z检查的GPU上,这意味着片段着色器不需要为被遮挡的像素或像素块运行。请注意,渲染器仍然需要考虑这些节点,并且顶点着色器仍然会为这些原语中的每个顶点运行,因此如果应用程序知道某些内容完全被遮挡,最好的做法是使用Item::visibleItem::opacity显式隐藏它。

注意

Item::z 用于控制一个项目相对于其兄弟项目的堆叠顺序。它与渲染器和OpenGL的Z缓冲区没有直接关系。

Alpha 混合图元

一旦绘制了不透明的图元,渲染器将禁用glDepthMask,启用GL_BLEND,并以从后到前的方式渲染所有带有透明混合的图元。

在渲染器中,对带有alpha混合的图元进行批处理需要更多的努力,因为重叠的元素需要以正确的顺序渲染,以使alpha混合看起来正确。仅依赖Z缓冲区是不够的。渲染器会对所有带有alpha混合的图元进行一次遍历,并查看它们的边界矩形以及材质状态,以确定哪些元素可以批处理,哪些不能。

../_images/visualcanvas_overlap.png

在最左边的情况下,蓝色背景可以一次性绘制,两个文本元素可以另一次绘制,因为文本只重叠在它们前面的背景上。在最右边的情况下,“Item 4”的背景与“Item 3”的文本重叠,因此在这种情况下,每个背景和文本都需要单独绘制。

在Z-wise方面,alpha原语与不透明节点交错排列,并且在可用时可能会触发early-z,但再次强调,将Item::visible设置为false总是更快。

与3D图元混合

场景图可以支持伪3D和适当的3D图元。例如,可以使用ShaderEffect实现“页面卷曲”效果,或者使用QSGGeometry和自定义材质实现凹凸映射的环面。在这样做时,需要考虑默认渲染器已经使用了深度缓冲区。

渲染器修改了从QSGMaterialShader::vertexShader()返回的顶点着色器,并在应用了模型视图和投影矩阵后压缩顶点的z值,然后在z轴上添加一个小的平移以将其定位到正确的z位置。

压缩假设z值在0到1的范围内。

纹理图集

活动纹理是一个独特的OpenGL状态,这意味着使用不同OpenGL纹理的多个图元无法批量处理。因此,Qt Quick场景图允许多个QSGTexture实例被分配为较大纹理的较小子区域;即纹理图集。

纹理图集的最大好处是多个QSGTexture实例现在引用相同的OpenGL纹理实例。这使得批处理带纹理的绘制调用也成为可能,例如图像项、BorderImage项、ShaderEffect项以及C++类型,如QSGSimpleTextureNode和使用纹理的自定义QSGGeometryNodes。

注意

大纹理不会进入纹理图集。

基于图集的纹理是通过将TextureCanUseAtlas传递给createTextureFromImage()来创建的。

注意

基于图集的纹理没有从0到1的纹理坐标。使用normalizedTextureSubRect()来获取图集纹理坐标。

场景图使用启发式方法来确定图集应该有多大以及进入图集的大小阈值是多少。如果需要不同的值,可以使用环境变量QSG_ATLAS_WIDTH=[width]QSG_ATLAS_HEIGHT=[height]QSG_ATLAS_SIZE_LIMIT=[size]来覆盖它们。更改这些值主要对平台供应商感兴趣。

批量根

除了将兼容的图元合并成批次外,默认渲染器还尝试最小化每帧需要发送到GPU的数据量。默认渲染器识别属于一起的子树,并尝试将这些子树放入单独的批次中。一旦批次被识别,它们就会被合并、上传并存储在GPU内存中,使用顶点缓冲对象。

转换节点

每个Qt Quick Item都会在场景图树中插入一个QSGTransformNode来管理其x、y、缩放或旋转。子项将填充在此变换节点下。默认渲染器跟踪帧之间变换节点的状态,并会查看子树以决定变换节点是否适合成为一组批次的根。在帧之间发生变化且具有相当复杂子树的变换节点可以成为批次的根。

在批处理根节点的子树中的QSGGeometryNodes是相对于根节点在CPU上预先变换的。然后它们被上传并保留在GPU上。当变换发生变化时,渲染器只需要更新根节点的矩阵,而不是每个单独的项目,这使得列表和网格滚动非常快。对于连续的帧,只要没有添加或删除节点,渲染列表实际上是免费的。当新内容进入子树时,获取它的批处理会被重建,但这仍然相对较快。在网格或列表中平移时,通常每帧有几个不变的帧,每帧有添加或删除的节点。

将变换节点识别为批处理根的另一个好处是,它允许渲染器保留未更改的树的部分。例如,假设一个用户界面由一个列表和一个按钮行组成。当列表正在滚动并且委托正在添加和删除时,用户界面的其余部分,即按钮行,是未更改的,并且可以使用已经存储在GPU上的几何图形进行绘制。

可以使用环境变量QSG_RENDERER_BATCH_NODE_THRESHOLD=[count]QSG_RENDERER_BATCH_VERTEX_THRESHOLD=[count]来覆盖转换节点成为批处理根节点的节点和顶点阈值。覆盖这些标志对于平台供应商来说将非常有用。

注意

在批次根目录下,为每个独特的材料状态和几何类型集创建一个批次。

裁剪

当将Item::clip设置为true时,它将在其几何体中创建一个带有矩形的QSGClipNode。默认渲染器将通过使用OpenGL中的剪裁来应用此剪裁。如果项目旋转了非90度的角度,则使用OpenGL的模板缓冲区。Qt Quick Item仅支持通过QML设置矩形作为剪裁,但场景图API和默认渲染器可以使用任何形状进行剪裁。

当对子树应用裁剪时,该子树需要使用唯一的OpenGL状态进行渲染。这意味着当Item::clip为true时,该项目的批处理仅限于其子项。当有许多子项时,如ListViewGridView,或者复杂的子项,如TextArea,这是可以的。然而,应该谨慎地对较小的项目使用裁剪,因为它会阻止批处理。这包括按钮标签、文本字段或列表委托和表格单元格。通常可以通过安排UI来避免裁剪Flickable(或项目视图),使不透明项目覆盖Flickable周围的区域,并依赖窗口边缘来裁剪其他所有内容。

Item::clip设置为true也会设置ItemIsViewport标志;带有ItemObservesViewport标志的子项可能会使用视口进行粗略的预裁剪步骤:例如,Text会省略完全在视口之外的文本行。省略场景图节点或限制vertices是一种优化,可以通过在C++中设置flags来实现,而不是在QML中设置Item::clip

在自定义项目中实现updatePaintNode()时,如果它可以在一个大的几何区域上渲染很多细节,你应该考虑是否有效地将图形限制在视口中;如果是这样,你可以设置ItemObservesViewport标志,并从clipRect()读取当前暴露的区域。一个后果是updatePaintNode()将被更频繁地调用(通常每当内容在视口中移动时每帧调用一次)。

顶点缓冲区

每个批次使用顶点缓冲对象(VBO)在GPU上存储其数据。这个顶点缓冲在帧之间保留,并在其表示的场景图部分发生变化时更新。

默认情况下,渲染器将使用GL_STATIC_DRAW将数据上传到VBO中。可以通过设置环境变量QSG_RENDERER_BUFFER_STRATEGY=[strategy]来选择不同的上传策略。有效值为streamdynamic。更改此值主要对平台供应商有用。

抗锯齿

场景图支持两种类型的抗锯齿。默认情况下,通过在基元的边缘添加更多顶点以使边缘渐变为透明,矩形和图像等基元将被抗锯齿处理。我们称这种方法为顶点抗锯齿。如果用户请求一个多重采样的OpenGL上下文,通过使用QQuickWindow::setFormat()设置一个采样数大于0的QSurfaceFormat,场景图将优先使用基于多重采样的抗锯齿(MSAA)。这两种技术将影响渲染的内部方式,并具有不同的限制。

也可以通过设置环境变量QSG_ANTIALIASING_METHODvertexmsaa来覆盖使用的抗锯齿方法。

顶点抗锯齿可能会在相邻图元的边缘之间产生接缝,即使这两个边缘在数学上是相同的。多重采样抗锯齿则不会。

顶点抗锯齿

可以使用Item::antialiasing属性在每个项目的基础上启用和禁用顶点抗锯齿。无论底层硬件支持什么,它都会工作,并且为正常渲染的图元以及捕获到帧缓冲对象中的图元(例如使用ShaderEffectSource类型)产生更高质量的抗锯齿效果。

使用顶点抗锯齿的缺点是,每个启用了抗锯齿的图元都必须进行混合。就批处理而言,这意味着渲染器需要做更多的工作来确定图元是否可以批处理,并且由于与场景中其他元素的重叠,也可能导致批处理减少,从而可能影响性能。

在低端硬件上,混合操作也可能非常昂贵,因此对于一个覆盖大部分屏幕的图像或圆角矩形,这些图元内部所需的混合量可能会导致显著的性能损失,因为整个图元都必须进行混合。

多重采样抗锯齿

多重采样抗锯齿是一种硬件特性,硬件会为图元中的每个像素计算一个覆盖值。一些硬件可以以非常低的成本进行多重采样,而其他硬件可能需要更多的内存和更多的GPU周期来渲染一帧。

使用多重采样抗锯齿技术,许多图元,如圆角矩形和图像元素,可以在场景图中进行抗锯齿处理,同时仍然保持不透明。这意味着渲染器在创建批次时工作更轻松,并且可以依赖早期深度测试来避免过度绘制。

当使用多重采样抗锯齿时,渲染到帧缓冲对象中的内容需要额外的扩展来支持帧缓冲的多重采样。通常是GL_EXT_framebuffer_multisampleGL_EXT_framebuffer_blit。大多数桌面芯片都有这些扩展,但在嵌入式芯片中较少见。当硬件不支持帧缓冲多重采样时,渲染到帧缓冲对象中的内容将不会进行抗锯齿处理,包括ShaderEffectSource的内容。

性能

正如开头所述,要获得良好的性能,不需要了解渲染器的细节。它是为优化常见用例而编写的,几乎在任何情况下都能表现得很好。

  • 良好的性能来自于有效的批处理,尽可能减少几何体的重复上传。通过设置环境变量 QSG_RENDERER_DEBUG=render,渲染器将输出批处理的统计信息,包括批处理的效果、使用的批处理数量、哪些批处理被保留以及哪些是不透明的。在追求最佳性能时,上传应仅在真正需要时进行,批处理数量应少于10个,并且其中至少3-4个应是不透明的。

  • 默认的渲染器不会进行任何CPU端的视口裁剪或遮挡检测。如果某些内容不应该被看到,它就不应该被显示。对于不应该绘制的项目,使用Item::visible: false。不添加此类逻辑的主要原因是它会增加额外的成本,这也会对那些表现良好的应用程序造成伤害。

  • 确保使用纹理图集。除非图像太大,否则Image和BorderImage项将使用它。对于在C++中创建的纹理,在调用QQuickWindow::createTexture()时传递TextureCanUseAtlas。通过设置环境变量QSG_ATLAS_OVERLAY,所有图集纹理将被着色,以便在应用程序中轻松识别。

  • 尽可能使用不透明的原语。不透明的原语在渲染器中处理速度更快,在GPU上绘制速度也更快。例如,PNG文件通常会有alpha通道,即使每个像素都是完全不透明的。JPG文件总是不透明的。当向QQuickImageProvider提供图像或使用createTextureFromImage()创建图像时,尽可能让图像具有QImage::Format_RGB32格式。

  • 请注意,如上图所示的重叠复合项无法进行批处理。

  • 裁剪会破坏批处理。切勿在表格单元格、项目委托或类似内容中逐项使用。不要裁剪文本,而是使用省略。不要裁剪图像,而是创建一个QQuickImageProvider,返回裁剪后的图像。

  • 批处理仅适用于16位索引。所有内置项目都使用16位索引,但自定义几何体也可以自由使用32位索引。

  • 一些材质标志会阻止批处理,最具限制性的是RequiresFullMatrix,它会阻止所有批处理。

  • 具有单色背景的应用程序应使用setColor()来设置背景,而不是使用顶层的Rectangle项目。setColor()将在调用glClear()时使用,这可能会更快。

  • Mipmapped 图像项不会放置在全局图集中,也不会进行批处理。

  • 与帧缓冲对象(FBO)读取相关的OpenGL驱动程序中的一个错误可能会损坏渲染的字形。如果您设置了QML_USE_GLYPHCACHE_WORKAROUND环境变量,Qt会在RAM中保留字形的额外副本。这意味着在绘制之前未绘制过的字形时,性能会稍微降低,因为Qt通过CPU访问额外的副本。这也意味着字形缓存将使用两倍的内存。质量不受此影响。

如果应用程序表现不佳,请确保渲染确实是瓶颈。使用性能分析器!环境变量 QSG_RENDER_TIMING=1 将输出一些有用的时间参数,这些参数可以帮助精确定位问题所在。

可视化

为了可视化场景图默认渲染器的各个方面,可以将QSG_VISUALIZE环境变量设置为下面每个部分中详细描述的值之一。我们使用以下QML代码提供了一些变量输出的示例:

import QtQuick 2.2

Rectangle {
    width: 200
    height: 140

    ListView {
        id: clippedList
        x: 20
        y: 20
        width: 70
        height: 100
        clip: true
        model: ["Item A", "Item B", "Item C", "Item D"]

        delegate: Rectangle {
            color: "lightblue"
            width: parent.width
            height: 25

            Text {
                text: modelData
                anchors.fill: parent
                horizontalAlignment: Text.AlignHCenter
                verticalAlignment: Text.AlignVCenter
            }
        }
    }

    ListView {
        id: clippedDelegateList
        x: clippedList.x + clippedList.width + 20
        y: 20
        width: 70
        height: 100
        clip: true
        model: ["Item A", "Item B", "Item C", "Item D"]

        delegate: Rectangle {
            color: "lightblue"
            width: parent.width
            height: 25
            clip: true

            Text {
                text: modelData
                anchors.fill: parent
                horizontalAlignment: Text.AlignHCenter
                verticalAlignment: Text.AlignVCenter
            }
        }
    }
}

对于左侧的ListView,我们将其clip属性设置为true。对于右侧的ListView,我们还将每个委托的clip属性设置为true,以说明裁剪对批处理的影响。

../_images/visualize-original.png

原文

注意

可视化的元素不受裁剪限制,渲染顺序是任意的。

可视化批次

QSG_VISUALIZE设置为batches可以在渲染器中可视化批次。合并的批次用纯色绘制,未合并的批次用对角线图案绘制。较少的独特颜色意味着良好的批处理。如果未合并的批次包含许多单独的节点,则是不好的。

../_images/visualize-batches.png

QSG_VISUALIZE=batches

可视化裁剪

QSG_VISUALIZE设置为clip会在场景顶部绘制红色区域以指示裁剪。由于Qt Quick项目默认不裁剪,因此通常不会显示裁剪。

../_images/visualize-clip.png

QSG_VISUALIZE=clip

可视化变化

QSG_VISUALIZE设置为changes可以可视化渲染器中的变化。场景图中的变化会以随机颜色的闪烁覆盖层进行可视化。图元上的变化会以纯色进行可视化,而祖先节点(如矩阵或透明度)的变化则会以图案进行可视化。

可视化过度绘制

QSG_VISUALIZE设置为overdraw可以在渲染器中可视化过度绘制。将所有项目在3D中可视化以突出显示过度绘制。此模式还可以在一定程度上检测视口外的几何体。不透明项目以绿色渲染,而半透明项目以红色渲染。视口的边界框以蓝色渲染。不透明内容对于场景图来说更容易处理,并且通常渲染速度更快。

请注意,上面代码中的根矩形是多余的,因为窗口也是白色的,所以在这种情况下绘制矩形是浪费资源。将其更改为Item可以略微提高性能。

../_images/visualize-overdraw-1.png ../_images/visualize-overdraw-2.png

QSG_VISUALIZE=overdraw

通过Qt渲染硬件接口进行渲染

从Qt 6.0开始,默认的适配总是通过Qt GUI模块提供的图形抽象层Qt Rendering Hardware Interface (RHI)进行渲染。这意味着,与Qt 5不同,场景图不会直接调用OpenGL。相反,它通过使用RHI API记录资源和绘制命令,然后将命令流转换为OpenGL、Vulkan、Metal或Direct 3D调用。着色器处理也通过编写一次着色器代码,编译为SPIR-V,然后转换为适合各种图形API的语言来统一。

为了控制行为,可以使用以下环境变量:

环境变量

可能的值

描述

QSG_RHI_BACKEND

vulkan, metal, opengl, d3d11, d3d12

请求特定的RHI后端。默认情况下,目标图形API是根据平台选择的,除非被此变量或等效的C++ API覆盖。当前默认值为Windows上的Direct3D 11,macOS上的Metal,其他平台上的OpenGL。

QSG_INFO

1

与基于OpenGL的渲染路径类似,设置此选项可以在初始化Qt Quick场景图时打印系统信息。这对于故障排除非常有用。

QSG_RHI_DEBUG_LAYER

1

在适用的情况下(Vulkan, Direct3D),启用图形API实现的调试或验证层(如果可用),无论是在图形设备上还是在实例对象上。对于macOS上的Metal,请设置环境变量METAL_DEVICE_WRAPPER_TYPE=1

QSG_RHI_PREFER_SOFTWARE_RENDERER

1

请求选择使用基于软件的光栅化的适配器或物理设备。仅当底层API支持枚举适配器(例如,Direct3D或Vulkan)时适用,否则忽略。

希望始终使用单一给定图形API运行的应用程序,也可以通过C++请求此功能。例如,在main()函数早期,在构造任何QQuickWindow之前,进行以下调用将强制使用Vulkan(否则将失败):

QQuickWindow::setGraphicsApi(QSGRendererInterface::Vulkan);

请参阅 GraphicsApi。枚举值 OpenGLVulkanMetalDirect3D11Direct3D12 的效果等同于将 QSG_RHI_BACKEND 设置为相应的字符串键运行。

所有QRhi后端将选择系统默认的GPU适配器或物理设备,除非被QSG_RHI_PREFER_SOFTWARE_RENDERER或特定于后端的变量(例如QT_D3D_ADAPTER_INDEXQT_VK_PHYSICAL_DEVICE_INDEX)覆盖。目前没有提供进一步的适配器配置选项。

从Qt 6.5开始,一些以前仅作为环境变量暴露的设置现在可以通过C++ API在QQuickGraphicsConfiguration中使用。例如,设置QSG_RHI_DEBUG_LAYER和调用setDebugLayer(true)是等效的。