Qt Quick 场景图默认渲染器

本文档解释了默认场景图渲染器的工作原理,以便用户以最佳的方式编写利用其功能和性能的代码。

即使不了解渲染器的内部结构,也可以获得良好的性能。然而,在集成场景图或确定为什么无法从图形芯片中榨取最大效率时,这可能会有所帮助。

注意:即使在每个帧都独一无二且所有内容都是从零开始上传的情况下,默认渲染器也将表现出良好的性能。

在 QML 场景中,Qt Quick 元素填充了一个 QSGNode 实例的树。一旦创建,这个树就是一个关于特定帧应如何渲染的完整描述。它不包含任何指向 Qt Quick 元素的引用,并且将在大多数平台上以单独的线程进行处理和渲染。渲染器是场景图的一个自包含部分,它遍历 QSGNode 树,并使用定义在 QSGGeometryNode 中的几何形状和定义在 QSGMaterial 中的着色器状态来更新图形状态并生成绘制调用。

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

默认渲染器重点关注两个主要的渲染优化策略:绘制调用的批处理和 GPU 上几何形状的保留。

批处理

传统 2D API,如 QPainter、Cairo 或 Context2D,旨在处理每帧数千个单个绘制调用,而 OpenGL 和其他硬件加速 API 在绘制调用数量很低且状态更改保持到最小的情况下表现最佳。

注意:以下部分中使用的 OpenGL 作为示例,但相同的概念也适用于其他图形 API。

考虑以下用例

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

当每个原始元素较大时,这种开销可以忽略不计,但在典型的UI场景中,有许多小元素相加产生相当大的开销。

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

不透明原始元素

渲染器将不透明原始元素和需要应用半透明的原始元素分开。通过使用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,并以后到前的顺序渲染所有半透明原始元素。

渲染器对半透明原始元素的批处理需要更多的努力,因为重叠的元素需要以正确的顺序渲染,才能使半透明看起来正确。仅依赖Z缓冲区是不够的。渲染器将对所有半透明原始元素执行一次遍历,并会查看它们的边界矩形以及它们的数据材料状态,以确定哪些元素可以批处理,哪些不行。

在左侧的情况中,蓝色背景可以一次性绘制,而两个文本元素可以在另一次调用中绘制,因为文本只与它们所在的前面的背景重叠。在右侧的情况中,“项目4”的背景与“项目3”的文本重叠,因此在这种情况下,背景和文本的绘制都需要使用单独的调用。

在Z轴上,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纹理实例。这使得批处理带纹理的绘制调用成为可能,例如Image项目,BorderImage项目,ShaderEffect项目,以及诸如QSGSimpleTextureNode和自定义QSGGeometryNodes使用纹理的C++类型。

注意:大纹理不会进入纹理图集。

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

注意:基于图集的纹理没有从0到1的范围的纹理坐标。使用QSGTexture::normalizedTextureSubRect()获取图集纹理坐标。

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

Batch Roots

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

变换节点

每个 Qt Quick 项目都会将其 x, y, 缩放或旋转插入到场景图树中的QSGTransformNode 来管理其转换。子项将在该转换节点下填充。默认的渲染器跟踪转换节点在帧之间的状态,并将查看子树以确定转换节点是否是成为一系列批次的根节点的好候选者。在帧之间发生变化并且子树相对复杂的转换节点可以成为批次的根节点。

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

将转换节点识别为批次根的另一个好处是,它允许渲染器保留未改变的树的各个部分。例如,假设一个 UI 由列表和按钮行组成。当列表正在滚动且代理正在添加和删除时,其余的 UI(按钮行)未发生变化,可以使用已经在 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 时,这是可以的。然而,对较小的项目使用裁剪时应谨慎,因为它会阻止批处理。这包括按钮标签、文本字段或列表代理和表格单元格。通常可以通过将不透明的项目排列在 Flickable(或项视图)周围来避免裁剪 Flickable(或项视图),否则则依赖于窗口边缘裁剪其他所有内容。

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

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

顶点缓冲区

每个批处理使用一个顶点缓冲区对象(VBO)来在 GPU 上存储其数据。该顶点缓冲区在帧之间保持,并且当它所代表的场景图的部分发生变化时进行更新。

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

抗锯齿

场景图支持两种类型的抗锯齿。默认情况下,如矩形和图像之类的原语将通过沿原语边缘添加更多顶点来抗锯齿,从而使边缘透明化。我们称之为 顶点抗锯齿。如果用户请求多采样 OpenGL 上下文,可以通过使用 QQuickWindow::setFormat() 并设置包含样本大于 0QSurfaceFormat 来实现基于多采样的抗锯齿 (MSAA)。这两种技术将影响内部渲染的方式,并且有不同的限制。

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

顶点抗锯齿可能在相邻原语边界的缝隙处产生,即使这两个边缘在数学上是相同的。多采样抗锯齿不会。

顶点抗锯齿

可以使用 Item::antialiasing 属性在每项级别上启用或禁用顶点抗锯齿。它将无视底层硬件支持的情况,并能产生更高品质的抗锯齿,对于通常渲染的原语以及捕获到帧缓冲区对象的原语(例如使用 ShaderEffectSource 类型)。

使用顶点抗锯齿的缺点是,每个启用了抗锯齿的原语都需要混合。在批处理方面,这意味着渲染器需要做更多工作来确定原语是否可以批处理,并且由于与其他场景元素的重叠,也可能导致批处理较少,这可能会影响性能。

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

多重采样抗锯齿

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

使用多重采样抗锯齿,许多图元,如圆角矩形和图像元素可以抗锯齿,同时仍然在场景图中保持不可透性。这意味着渲染器创建批次时的工作更简单,可以依赖early-z来避免过度绘制。

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

性能

如开始所述,要获得良好的性能并不需要了解渲染器的细节。它旨在针对常见用例进行优化,并在几乎所有情况下性能都相当好。

  • 良好的性能来自有效的批量处理,尽可能少地重复上传几何形状。通过设置环境变量 QSG_RENDERER_DEBUG=render,渲染器将输出关于批量处理效果、批次数、保留的批次以及不可见的批次等统计信息。当努力优化性能时,上传应该只在真正需要时发生,批次应该少于10个,其中至少3-4个应该是不可见的。
  • 默认的渲染器不做CPU端的视口裁剪也不做遮挡检测。如果某些内容不应该被看到,则不应显示。对于不应绘制的项,请使用 Item::visible: false。不添加此类逻辑的主要原因是它增加了额外的成本,这也可能会损害那些行为良好的应用程序。
  • 确保使用纹理图集。Image 和 BorderImage 项将会使用它,除非图像太大。在调用 QQuickWindow::createTexture() 时,为在C++中创建的纹理传递 QQuickWindow::TextureCanUseAtlas。通过设置环境变量 QSG_ATLAS_OVERLAY,所有图集纹理都将被着色,以便在应用程序中易于识别。
  • 尽可能使用不透明图元。不透明图元在渲染器中处理更快,而且GPU上绘制更快。例如,PNG文件通常会具有alpha通道,尽管每个像素都是完全不透明的。JPG文件始终是透明的。当为QQuickImageProvider提供图像或使用 QQuickWindow::createTextureFromImage 创建图像时,如果可能,请让图像具有 QImage::Format_RGB32
  • 要注意,像上图所示的重叠复合项无法进行批量处理。
  • 裁剪会破坏批量处理。请勿按项基于单个项使用,在表格单元格、项目委托或类似位置使用。不是裁剪文本,而是使用省略。不是裁剪图像,而是创建一个返回裁剪图像的 QQuickImageProvider
  • 批量处理仅适用于16位索引。所有内置项都使用16位索引,但自定义几何可以自由使用32位索引。
  • 某些材质标志会阻止批量处理,其中最限制性的一个是QSGMaterial::RequiresFullMatrix,它阻止所有批量处理。
  • 具有单色背景的应用程序应使用QQuickWindow::setColor()来设置颜色,而不是使用顶级矩形项。《a href="qquickwindow.html#color-prop" translate="no">QQuickWindow::setColor()将用于调用《code translate="no">glClear(),这可能会更快。
  • 纹理映射图像项不会被放置在全局资源集中,也不会进行批量处理。
  • 与帧缓冲对象(FBO)读取回相关的OpenGL驱动程序中的错误可能会损坏渲染的符号。如果您设置了《code translate="no">QML_USE_GLYPHCACHE_WORKAROUND环境变量,Qt将在RAM中保留符号的额外副本。这意味着绘制之前未绘制过的符号时,性能略微降低,因为Qt通过CPU访问额外的副本。这也意味着符号缓存将使用两倍多的内存。这不会影响质量。

如果应用程序性能不佳,请确保渲染确实是瓶颈。使用分析器!环境变量《code translate="no">QSG_RENDER_TIMING=1将输出一些有用的计时参数,这些参数有助于定位问题所在。

可视化

要可视化场景图默认渲染器的各个方面,可以将《code translate="no">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
            }
        }
    }
}

对于左侧的《a href="qml-qtquick-listview.html" translate="no">ListView,我们将它的《a href="qml-qtquick-item.html#clip-prop" translate="no">clip属性设置为《code translate="no">true。对于右侧的《a href="qml-qtquick-listview.html" translate="no">ListView,我们还为每个代理设置了《a href="qml-qtquick-item.html#clip-prop" translate="no">clip属性为《code translate="no">true,以说明剪切对批量处理的影响。

"Original"

原文

注意:可视化的元素不遵守剪切,并且渲染顺序是任意的。

可视化批量

将《code translate="no">QSG_VISUALIZE设置为《code translate="no">batches将可视化渲染器中的批量处理。合并的批量以实色绘制,未合并的批量以对角线线图案绘制。唯一的颜色少意味着良好的批量处理。如果它们包含许多单个节点,则未合并的批量很糟糕。

"batches"

《code translate="no">QSG_VISUALIZE=batches

可视化剪切

将《code translate="no">QSG_VISUALIZE设置为《code translate="no">clip将在场景顶部绘制红色区域以指示剪切。由于Qt Quick项默认不剪切,通常不会进行剪切可视化。

《code translate="no">QSG_VISUALIZE=clip

可视化变化

将《code translate="no">QSG_VISUALIZE设置为《code translate="no">changes将可视化渲染器中的变化。场景图中的变化通过随机颜色的闪烁覆盖可视化。原始的变化以实色可视化,而祖先中的变化,例如矩阵或不透明度的变化,以图案可视化。

可视化过度绘制

QSG_VISUALIZE设置为overdraw将在渲染器中可视化溢绘效果。在三维中可视化所有项目以突出显示溢绘。此模式也可以在一定程度上检测视口外的几何形状。不透明项将以绿色渲染,而半透明项将以红色渲染。视口的边界框以蓝色渲染。不透明内容更容易被场景图处理,并且渲染速度通常更快。

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

"overdraw-1"

"overdraw-2"

QSG_VISUALIZE=overdraw

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

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

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

环境变量可能值描述
QSG_RHI_BACKENDvulkanmetalopengld3d11d3d12请求特定的RHI后端。默认情况下,根据平台选择目标图形API,除非此变量或等效的C++ API覆盖。当前默认是Windows上的Direct3D 11、macOS上的Metal、其他地方上的OpenGL。
QSG_INFO1就像基于OpenGL的渲染路径一样,设置此变量可以在初始化Qt Quick场景图时打印系统信息。这对于故障排除非常有用。
QSG_RHI_DEBUG_LAYER1在适用的情况下(Vulkan、Direct3D),如果图形API实现提供了调试或验证层,则启用这些层,无论是在图形设备还是在实例对象中。对于macOS上的Metal,请设置环境变量METAL_DEVICE_WRAPPER_TYPE=1
QSG_RHI_PREFER_SOFTWARE_RENDERER1请求选择使用软件渲染的基础适配器或物理设备。仅在底层API支持枚举适配器的情况下(例如Direct3D或Vulkan)才适用,否则忽略。

希望始终使用单个给定图形API的应用程序也可以通过C++请求此功能。例如,在main()的早期调用中创建任何QQuickWindow之前,以下调用强制使用Vulkan(否则会失败):

QQuickWindow::setGraphicsApi(QSGRendererInterface::Vulkan);

请参阅QSGRendererInterface::GraphicsApi。枚举值OpenGLVulkanMetalDirect3D11Direct3D12与将QSG_RHI_BACKEND设置为等效字符串键具有相同的效果。

所有QRhi后端都会选择系统默认的GPU适配器或物理设备,除非被QSG_RHI_PREFER_SOFTWARE_RENDERER或特定后端变量(如QT_D3D_ADAPTER_INDEXQT_VK_PHYSICAL_DEVICE_INDEX)覆盖。在此阶段没有提供进一步的适配器配置能力。

从Qt 6.5开始,之前只能作为环境变量公开的一些设置现在可以在QQuickGraphicsConfiguration中以C++ API的形式使用。例如,设置QSG_RHI_DEBUG_LAYER并调用setDebugLayer(true)是等效的。

© 2024 Qt公司有限度假权。其中包含的文档贡献均为各自所有者的版权。提供的文档遵照已由自由软件基金会发布的《GNU自由文档许可协议》第1.3版条款许可。Qt及其相关标志是芬兰和/或世界各地Qt公司有限的商标。所有其他商标均为各自所有者的财产。