Qt Quick Scene Graph OpenGL рендерер
В данном документе объясняется, как внутренне работает рендерер сцены для OpenGL, чтобы вы могли писать код, использующий его оптимальным образом, как с точки зрения производительности, так и функциональности.
Для достижения хорошей производительности не требуется понимать внутреннее устройство рендерера. Однако это может помочь при интеграции с графическим деревом сцены или при выяснении, почему невозможно извлечь максимальную эффективность из графического чипа.
Примечание: Даже в том случае, когда каждый кадр уникален и все загружается с нуля, стандартный рендерер будет работать хорошо.
Элементы Qt Quick в сцене QML заполняют дерево экземпляров QSGNode. После создания это дерево представляет собой полное описание того, как должен быть отрисован определённый кадр. Оно не содержит никаких ссылок обратно на элементы Qt Quick и на большинстве платформ будет обрабатываться и отображаться в отдельном потоке. Рендерер — это самодостаточная часть графического дерева сцены, которая проходит по дереву QSGNode и использует геометрию, определённую в QSGGeometryNode, и состояние шейдера, определённое в QSGMaterial, для планирования изменений состояния OpenGL и вызовов отрисовки.
При необходимости рендерер можно полностью заменить с помощью внутреннего API графического дерева сцены. Это в основном интересно поставщикам платформ, которые хотят использовать нестандартные аппаратные возможности. Для большинства случаев использования стандартного рендерера будет достаточно.
Стандартный рендерер фокусируется на двух основных стратегиях оптимизации рендеринга: объединении вызовов отрисовки и сохранении геометрии на графическом процессоре.
Объединение
В то время как традиционный 2D API, такой как QPainter, Cairo или Context2D, написан для обработки тысяч отдельных вызовов отрисовки в каждом кадре, OpenGL — это чистый API аппаратного ускорения и работает лучше, когда количество вызовов отрисовки очень низкое, а изменения состояния минимальны. Рассмотрим следующий случай использования:
Самый простой способ нарисовать этот список — ячейка за ячейкой. Сначала рисуется фон. Это прямоугольник определённого цвета. В терминах OpenGL это означает выбор программы шейдера для заливки твёрдым цветом, установку цвета заливки, установку матрицы преобразования, содержащей смещения по x и y, и затем, например, glDrawArrays для отрисовки двух треугольников, образующих прямоугольник. Далее рисуется значок. В терминах OpenGL это означает выбор программы шейдера для отрисовки текстур, выбор активной текстуры для использования, установку матрицы преобразования, включение альфа-смешивания и затем, например, glDrawArrays для отрисовки двух треугольников, образующих границу значка. Текст и разделительная линия между ячейками следуют аналогичной схеме. И этот процесс повторяется для каждой ячейки в списке, поэтому для более длинного списка накладные расходы, налагаемые на изменения состояния OpenGL и вызовы отрисовки, полностью перевешивают преимущества использования API с аппаратным ускорением.
Когда каждый примитив большой, эти накладные расходы незначительны, но в типичном пользовательском интерфейсе много небольших элементов, которые суммируются в значительные накладные расходы.
Стандартный рендерер графического дерева работает в этих пределах и будет пытаться объединить отдельные примитивы в группы, сохраняя при этом точный визуальный результат. Результатом являются меньшее количество изменений состояния OpenGL и минимальное количество вызовов отрисовки, что приводит к оптимальной производительности.
Непрозрачные примитивы
Рендерер разделяет непрозрачные примитивы и примитивы, требующие альфа-смешивания. Используя буфер глубины OpenGL и присваивая каждому примитиву уникальную позицию z, рендерер может свободно переупорядочивать непрозрачные примитивы без учёта их расположения на экране и перекрывания друг другом. Анализируя состояние материала каждого примитива, рендерер создаёт группы непрозрачных примитивов. Из набора элементов Qt Quick к ним относятся элементы Rectangle с непрозрачными цветами и полностью непрозрачными изображениями, такими как JPEG или BMP.
Ещё одним преимуществом использования непрозрачных примитивов является то, что для них не требуется включение GL_BLEND, что может быть дорогостоящим, особенно на мобильных и встраиваемых графических процессорах.
Непрозрачные примитивы отрисовываются спереди назад с включёнными glDepthMask и GL_DEPTH_TEST. На графических процессорах, которые внутренне выполняют ранние проверки глубины, это означает, что фрагментный шейдер не должен выполняться для пикселей или блоков пикселей, которые скрыты. Следует помнить, что рендереру всё ещё необходимо учитывать эти узлы, и вершинный шейдер всё ещё выполняется для каждой вершины в этих примитивах, поэтому, если приложение знает, что что-то полностью скрыто, лучше всего явно скрыть его, используя Item::visible или Item::opacity.
Примечание: Item::z используется для управления порядком элементов относительно их братьев. Он не имеет прямой связи с рендерером и буфером глубины OpenGL.
Примитивы с альфа-смешиванием
После отрисовки непрозрачных примитивов рендерер отключит glDepthMask, включит GL_BLEND и отрисует все примитивы с альфа-смешиванием сзади наперёд.
Объединение примитивов с альфа-смешиванием требует от рендерера больше усилий, так как элементы, которые перекрываются, должны отрисовываться в правильном порядке, чтобы альфа-смешивание выглядело корректно. Только на буфер глубины полагаться недостаточно. Рендерер проходит по всем примитивам с альфа-смешиванием и анализирует их прямоугольник обводки, а также состояние материала, чтобы определить, какие элементы можно объединять, а какие нет.
В крайнем левом случае голубые фоны могут быть нарисованы одним вызовом, а два текстовых элемента — другим, так как тексты перекрывают только фон, перед которым они расположены. В крайнем правом случае фон «Элемента 4» перекрывает текст «Элемента 3», поэтому в этом случае каждый из фонов и текстов должен нарисовать отдельный вызов.
С точки зрения 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, а также типы C++, такие как QSGSimpleTextureNode и пользовательские QSGGeometryNodes, использующие текстуры.
Примечание: Крупные текстуры не попадают в атлас текстур.
Текстуры, основанные на атласе, создаются путём передачи QQuickWindow::TextureCanUseAtlas в QQuickWindow::createTextureFromImage().
Примечание: Текстуры, основанные на атласе, не имеют координат текстуры в диапазоне от 0 до 1. Используйте QSGTexture::normalizedTextureSubRect() для получения координат текстуры атласа.
Графическое дерево использует эвристику, чтобы определить, каким должен быть размер атласа и какой размерный порог должен быть для попадания в атлас. При необходимости можно переопределить их, используя переменные среды QSG_ATLAS_WIDTH=[width], QSG_ATLAS_HEIGHT=[height] и QSG_ATLAS_SIZE_LIMIT=[size]. Изменение этих значений в основном будет интересно поставщикам платформ.
Корни объединения
В дополнение к объединению совместимых примитивов в группы, стандартный рендерер также пытается свести к минимуму количество данных, которые необходимо отправлять на графический процессор для каждого кадра. Стандартный рендерер определяет поддеревья, которые принадлежат вместе, и пытается поместить их в отдельные группы. После определения групп они объединяются, загружаются и сохраняются в памяти графического процессора с использованием объектов буфера вершин.
Узлы преобразования
Каждый элемент Qt Quick вставляет QSGTransformNode в дерево сцены для управления его значениями x, y, масштабом или поворотом. Дочерние элементы будут помещены под этот узел преобразования. Стандартный рендерер отслеживает состояние узлов преобразования между кадрами и анализирует поддеревья, чтобы определить, является ли узел преобразования хорошим кандидатом на роль корня набора групп. Узел преобразования, который изменяется между кадрами и у которого есть достаточно сложное поддерево, может стать корнем группы.
QSGGeometryNodes в поддереве корня группы предварительно преобразуются относительно корня на ЦП. Затем они загружаются и сохраняются на графическом процессоре. При изменении преобразования рендерер должен обновить только матрицу корня, а не каждый отдельный элемент, что делает прокрутку списков и таблиц очень быстрой. Для последовательных кадров, пока элементы не добавляются и не удаляются, отрисовка списка фактически бесплатна. При появлении нового содержимого в поддереве группа, которая его получает, перестраивается, но это всё ещё относительно быстро. Обычно есть несколько неизменных кадров для каждого кадра с добавленными или удалёнными узлами при прокрутке таблицы или списка.
Ещё одним преимуществом идентификации узлов преобразования в качестве корней групп является то, что это позволяет рендереру сохранять части дерева, которые не изменились. Например, скажем, пользовательский интерфейс состоит из списка и строки кнопок. При прокрутке списка и добавлении/удалении элементов, остальная часть интерфейса, строка кнопок, не изменяется и может быть нарисована с помощью геометрии, уже сохранённой на графическом процессоре.
Порог узла и вершин для того, чтобы узел преобразования стал корнем группы, может быть переопределён с помощью переменных среды QSG_RENDERER_BATCH_NODE_THRESHOLD=[count] и QSG_RENDERER_BATCH_VERTEX_THRESHOLD=[count]. Переопределение этих флагов будет полезно в основном для поставщиков платформ.
Примечание: Под корнем группы создаётся одна группа для каждого уникального набора состояний материала и типа геометрии.
Обрезка
При установке Item::clip в true создаётся QSGClipNode с прямоугольником в его геометрии. Стандартный рендерер применит эту обрезку, используя обрезку в OpenGL. Если элемент повернут под углом, не равным 90 градусам, используется буфер трафарета OpenGL. Элемент Qt Quick поддерживает только установку прямоугольника в качестве обрезки через QML, но API графического дерева сцены и стандартный рендерер могут использовать любую форму для обрезки.
При применении обрезки к поддереву, это поддерево необходимо отобразить с уникальным состоянием OpenGL. Это означает, что при Item::clip значение истинно, кэширование этого элемента ограничено его дочерними элементами. Когда есть много дочерних элементов, например, ListView или GridView, или сложные дочерние элементы, такие как TextArea, это нормально. Тем не менее, следует использовать обрезку для меньших элементов с осторожностью, так как это предотвращает кэширование. Это включает в себя метку кнопки, текстовое поле или делегат списка и ячейки таблицы.
Буферы вершин
Каждый пакет использует объект буфера вершин (VBO) для хранения данных на графическом процессоре. Этот буфер вершин сохраняется между кадрами и обновляется, когда изменяется часть графа сцены, которую он представляет.
По умолчанию рендерер загрузит данные в VBO с использованием GL_STATIC_DRAW. Можно выбрать другую стратегию загрузки, установив переменную среды QSG_RENDERER_BUFFER_STRATEGY=[strategy]. Допустимые значения stream и dynamic. Изменение этого значения в основном полезно для поставщиков платформ.
Сглаживание
Граф сцены поддерживает два типа сглаживания. По умолчанию такие примитивы, как прямоугольники и изображения, будут сглажены путём добавления дополнительных вершин по краю примитивов, так что края плавно переходят в прозрачность. Мы называем этот метод сглаживание вершин. Если пользователь запрашивает контекст OpenGL с многократной выборкой, установив QSurfaceFormat с количеством выборок больше, чем 0 с помощью QQuickWindow::setFormat(), граф сцены отдаст предпочтение сглаживанию на основе многократной выборки (MSAA). Два метода повлияют на то, как происходит рендеринг внутри, и будут иметь разные ограничения.
Также можно переопределить используемый метод сглаживания, установив переменную среды QSG_ANTIALIASING_METHOD на vertex или msaa.
Сглаживание вершин может создавать швы между краями смежных примитивов, даже если два края математически одинаковы. Сглаживание с многократной выборкой этого не делает.
Сглаживание вершин
Сглаживание вершин можно включать и выключать на основе каждого элемента, используя свойство Item::antialiasing. Это будет работать независимо от того, какую поддержку предоставляет базовое оборудование, и обеспечит более качественное сглаживание, как для обычных примитивов, так и для примитивов, захваченных в буфер кадров, например, с помощью типа ShaderEffectSource.
Недостатком использования сглаживания вершин является то, что каждый примитив с включенным сглаживанием должен быть смешан. С точки зрения кэширования, это означает, что рендереру нужно больше работы, чтобы определить, может ли примитив быть кэширован или нет, и из-за перекрытий с другими элементами сцены это также может привести к меньшему кэшированию, что может повлиять на производительность.
На оборудовании низкого класса смешивание также может быть довольно дорогостоящим, поэтому для изображения или скругленного прямоугольника, который покрывает большую часть экрана, количество смешивания, необходимого для внутренней части этих примитивов, может привести к значительной потере производительности, так как весь примитив должен быть смешан.
Сглаживание с многократной выборкой
Сглаживание с многократной выборкой — это функция оборудования, в которой оборудование вычисляет значение охвата для каждого пикселя в примитиве. Некоторые устройства могут выполнять многократную выборку с очень низкой стоимостью, а другие устройства могут потребовать больше памяти и больше циклов графического процессора для рендеринга кадра.
Используя сглаживание с многократной выборкой, многие примитивы, такие как скругленные прямоугольники и элементы изображения, могут быть сглажены и по-прежнему оставаться непрозрачными в графе сцены. Это означает, что рендерер имеет более простую задачу при создании пакетов и может полагаться на раннее z, чтобы избежать избыточного рисования.
При использовании сглаживания с многократной выборкой контент, отрисованный в буферы кадров, требует дополнительных расширений для поддержки многократной выборки буферов кадров. Обычно GL_EXT_framebuffer_multisample и GL_EXT_framebuffer_blit. Большинство настольных чипов имеют эти расширения, но они менее распространены в встроенных чипах. Когда многократная выборка буфера кадров недоступна в оборудовании, контент, отрисованный в буферы кадров, не будет сглажен, включая содержимое ShaderEffectSource.
Производительность
Как было сказано в начале, понимание тонких деталей рендерера не требуется для обеспечения хорошей производительности. Он разработан для оптимизации для распространённых случаев использования и будет работать довольно хорошо практически во всех обстоятельствах.
- Хорошая производительность обеспечивается эффективным кэшированием, с минимальным количеством геометрии, загружаемой снова и снова. Установив переменную среды
QSG_RENDERER_DEBUG=render, рендерер выведет статистику о том, насколько хорошо проходит кэширование, сколько используется пакетов, какие пакеты сохраняются, а какие непрозрачны и нет. При стремлении к оптимальной производительности загрузки должны происходить только тогда, когда это действительно необходимо, пакеты должны быть меньше 10, и по крайней мере 3-4 из них должны быть непрозрачными. - По умолчанию рендерер не выполняет обрезку области просмотра и не выполняет обнаружение скрытия на стороне процессора. Если что-то не должно быть видно, это не должно отображаться. Используйте
Item::visible: falseдля элементов, которые не должны отображаться. Основная причина, по которой такая логика не добавляется, заключается в том, что это добавляет дополнительные затраты, которые также повредят приложениям, которые заботились о хорошем поведении. - Убедитесь, что используется атлас текстур. Элементы Image и BorderImage будут использовать его, если изображение не слишком большое. Для текстур, созданных в C++, передайте QQuickWindow::TextureCanUseAtlas при вызове QQuickWindow::createTexture(). Установив переменную среды
QSG_ATLAS_OVERLAY, все текстуры атласа будут окрашены, чтобы их легко было идентифицировать в приложении. - По возможности используйте непрозрачные примитивы. Непрозрачные примитивы обрабатываются рендерером и выводятся на графический процессор быстрее. Например, файлы PNG часто имеют альфа-канал, даже если каждый пиксель полностью непрозрачен. Файлы JPG всегда непрозрачны. При предоставлении изображений в QQuickImageProvider или создании изображений с помощью QQuickWindow::createTextureFromImage(), если возможно, используйте формат изображения QImage::Format_RGB32.
- Следите за тем, что перекрывающиеся составные элементы, как на иллюстрации выше, не могут быть кэшированы.
- Обрезка нарушает кэширование. Никогда не используйте её на основе каждого элемента, внутри ячеек таблицы, делегатов элементов или подобного. Вместо обрезки текста используйте обрезку. Вместо обрезки изображения создайте QQuickImageProvider, который возвращает обрезное изображение.
- Кэширование работает только для 16-битных индексов. Все встроенные элементы используют 16-битные индексы, но пользовательская геометрия может использовать также 32-битные индексы.
- Некоторые флаги материалов препятствуют кэшированию, наиболее ограничительным из которых является QSGMaterial::RequiresFullMatrix, который предотвращает любое кэширование.
- Приложения с одноцветным фоном должны устанавливать его с помощью QQuickWindow::setColor() вместо использования элемента Rectangle верхнего уровня. QQuickWindow::setColor() будет использоваться в вызове
glClear(), что потенциально быстрее. - Элементы изображения с MIP-масштабированием не размещаются во глобальном атласе и не кэшируются.
Если приложение работает плохо, убедитесь, что рендеринг действительно является узким местом. Используйте профайлер! Переменная среды 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 для иллюстрации эффектов обрезки на кэширование.
Оригинал
Примечание: Визуализируемые элементы не учитывают обрезку, а порядок рендеринга произвольный.
Визуализация пакетов
Установив QSG_VISUALIZE на batches, визуализируются пакеты в рендерере. Объединённые пакеты отображаются твёрдым цветом, а не объединённые — диагональным узором. Малое количество уникальных цветов означает хорошее кэширование. Необъединённые пакеты плохи, если они содержат много отдельных узлов.
QSG_VISUALIZE=batches
Визуализация обрезки
Установив QSG_VISUALIZE на clip, отрисовываются красные области поверх сцены, чтобы указать обрезку. Поскольку элементы Qt Quick по умолчанию не имеют обрезки, обычно не отображается никакой обрезки.
QSG_VISUALIZE=clip
Визуализация изменений
Установив QSG_VISUALIZE на changes, визуализируются изменения в рендерере. Изменения в графе сцены визуализируются мигающим наложением случайного цвета. Изменения в примитиве визуализируются сплошным цветом, а изменения в предке, такие как изменения матрицы или непрозрачности, визуализируются узором.
Визуализация избыточного рисования
Установив QSG_VISUALIZE на overdraw, визуализируется избыточное рисование в рендерере. Визуализируются все элементы в 3D, чтобы выделить избыточное рисование. Этот режим также можно использовать для обнаружения геометрии за пределами области просмотра в некоторой степени. Непрозрачные элементы отображаются с зелёным оттенком, а полупрозрачные — с красным. Граница области просмотра отображается синим цветом. Непрозрачный контент легче обрабатывать графу сцены и обычно быстрее отображается.
Обратите внимание, что прямоугольник корня в коде выше избыточен, так как окно также белое, поэтому рисование прямоугольника — пустая трата ресурсов в этом случае. Изменение его на элемент может немного улучшить производительность.
QSG_VISUALIZE=overdraw
© The Qt Company Ltd
Licensed under the GNU Free Documentation License, Version 1.3.
https://doc.qt.io/archives/qt-5.11/qtquick-visualcanvas-scenegraph-renderer.html