Qt Quick Scene Graph OpenGL рендерер
В данном документе объясняется, как внутренне работает рендерер сцены для OpenGL, чтобы вы могли писать код, использующий его оптимальным образом, как с точки зрения производительности, так и с точки зрения функций.
Вам не нужно понимать внутренние механизмы рендерера, чтобы обеспечить хорошую производительность. Однако это может быть полезно при интеграции со схемой сцены или для выяснения причин, по которым невозможно извлечь максимальную эффективность из графического процессора.
Примечание: Даже в том случае, если каждый кадр уникален, и всё загружается с нуля, стандартный рендерер будет работать хорошо.
Элементы Qt Quick в сцене QML заполняют дерево экземпляров QSGNode. После создания это дерево представляет собой полное описание того, как должен быть отрисован определенный кадр. Оно не содержит никаких ссылок на элементы Qt Quick и на большинстве платформ обрабатывается и отображается в отдельном потоке. Рендерер – это автономная часть схемы сцены, которая обходит дерево QSGNode и использует геометрию, определённую в QSGGeometryNode, и состояние шейдера, определённое в QSGMaterial, для планирования изменений состояния OpenGL и вызовов отрисовки.
При необходимости рендерер можно полностью заменить, используя внутренний API заднего плана схемы сцены. Это в основном интересно поставщикам платформ, которые хотят воспользоваться функциями нестандартного оборудования. Для большинства случаев использования стандартного рендерера будет достаточно.
Стандартный рендерер фокусируется на двух основных стратегиях оптимизации отрисовки: группировка вызовов отрисовки и сохранение геометрии на графическом процессоре.
Группировка
Если традичный API 2D, такой как 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. На графических процессорах, которые внутренне выполняют раннее проверку z, это означает, что фрагментный шейдер не нужно запускать для пикселей или блоков пикселей, которые закрыты. Имейте в виду, что рендереру всё равно нужно учитывать эти узлы, и вершинный шейдер всё равно запускается для каждой вершины этих примитивов, поэтому, если приложение знает, что что-то полностью скрыто, лучше всего явно скрыть его с помощью Item::visible или Item::opacity.
Примечание: Item::z используется для управления порядком элементов относительно их братьев. Он не имеет прямой связи с рендерером и буфером глубины OpenGL.
Примитивы с альфа-смешиванием
После отрисовки непрозрачных примитивов рендерер отключит glDepthMask, включит GL_BLEND и отрисует все примитивы с альфа-смешиванием в порядке от сзади вперёд.
Группировка примитивов с альфа-смешиванием требует от рендерера немного больше усилий, так как элементы, которые перекрываются, должны быть отрисованы в правильном порядке, чтобы альфа-смешивание выглядело правильно. Только полагаться на буфер глубины недостаточно. Рендерер выполняет проход по всем примитивам с альфа-смешиванием и рассматривает их прямоугольник ограничивающей рамки в дополнение к их состоянию материала, чтобы определить, какие элементы можно объединить в группу, а какие нет.
В левом случае голубые фоны можно отрисовать одним вызовом, а два элемента текста – другим вызовом, так как тексты перекрывают только фон, который они стоят перед ним. В правом случае фон «Элемента 4» перекрывает текст «Элемента 3», поэтому в этом случае каждый фон и текст должны быть отрисованы с помощью отдельных вызовов.
С точки зрения z, альфа-примитивы чередуются с непрозрачными узлами и могут активировать раннюю проверку 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 с прямоугольником в его геометрии. По умолчанию рендерер будет применять этот обрез (clip) с помощью функции scissoring в OpenGL. Если элемент повернут на угол, отличном от 90 градусов, используется буфер OpenGL для маскировки (stencil buffer). Qt Quick Item поддерживает только установку прямоугольника в качестве клипа через QML, но API графа сцены и по умолчанию рендерер могут использовать любую форму для обрезки.
При применении обрезки к поддереву, это поддерево должно быть отрисовано с уникальным состоянием OpenGL. Это означает, что когда Item::clip имеет значение true, кэширование (batching) этого элемента ограничено его дочерними элементами. Если у элемента много дочерних элементов, таких как ListView или GridView, или сложные дочерние элементы, такие как TextArea, это нормально. Однако следует с осторожностью использовать обрезку на небольших элементах, так как это предотвращает кэширование. Это включает в себя метки кнопок, поля ввода или делегаты списков и ячейки таблиц.
Буферы вершин
Каждый пакет (batch) использует буфер вершин (VBO) для хранения своих данных на графическом процессоре (GPU). Этот буфер вершин сохраняется между кадрами и обновляется, когда изменяется часть графа сцены, которую он представляет.
По умолчанию рендерер загружает данные в VBO, используя GL_STATIC_DRAW. Можно выбрать другую стратегию загрузки, установив переменную среды QSG_RENDERER_BUFFER_STRATEGY=[strategy]. Допустимые значения stream и dynamic. Изменение этого значения в основном полезно для поставщиков платформ.
Антиалиасинг
Граф сцены поддерживает два типа антиалиасинга. По умолчанию примитивы, такие как прямоугольники и изображения, будут антиалиасированы путем добавления дополнительных вершин по краю примитивов, так что края плавно переходят в прозрачность. Мы называем этот метод антиалиасинг вершин. Если пользователь запрашивает контекст OpenGL с многократной выборкой (multisampled), установив QSurfaceFormat с количеством выборок (samples) большим, чем 0 с помощью QQuickWindow::setFormat(), граф сцены будет отдавать предпочтение антиалиасингу на основе многократной выборки (MSAA). Два метода повлияют на то, как происходит внутренняя отрисовка, и имеют разные ограничения.
Также можно переопределить используемый метод антиалиасинга, установив переменную среды QSG_ANTIALIASING_METHOD в значение vertex или msaa.
Антиалиасинг вершин может создавать швы между краями смежных примитивов, даже когда два края математически одинаковы. Антиалиасинг с многократной выборкой этого не делает.
Антиалиасинг вершин
Антиалиасинг вершин можно включать и выключать для каждого элемента, используя свойство Item::antialiasing. Он будет работать независимо от того, поддерживает ли это подлежащее оборудование, и обеспечивает более качественный антиалиасинг, как для обычно отрисовываемых примитивов, так и для примитивов, захваченных в буферы кадров, например, с помощью типа ShaderEffectSource.
Недостатком использования антиалиасинга вершин является то, что каждый примитив с включенным антиалиасингом должен быть смешан (blended). С точки зрения кэширования (batching), это означает, что рендерер должен больше работать, чтобы определить, можно ли кэшировать примитив или нет, и из-за перекрытий с другими элементами в сцене это также может привести к меньшему кэшированию, что может повлиять на производительность.
На устройствах с низкой производительностью смешивание (blending) также может быть довольно дорогостоящим, поэтому для изображения или скругленного прямоугольника, который покрывает большую часть экрана, количество смешивания, необходимого для внутренней части этих примитивов, может привести к значительной потере производительности, так как весь примитив должен быть смешан.
Антиалиасинг с многократной выборкой
Антиалиасинг с многократной выборкой — это аппаратная функция, в которой оборудование вычисляет значение покрытия (coverage) для каждого пикселя в примитиве. Некоторые аппаратные средства могут производить многократную выборку с очень низкой стоимостью, в то время как другим аппаратным средствам может потребоваться больше памяти и больше циклов графического процессора для отрисовки кадра.
Используя антиалиасинг с многократной выборкой, многие примитивы, такие как скругленные прямоугольники и элементы изображений, могут быть антиалиасированы и оставаться *непрозрачными* в графе сцены. Это означает, что рендереру проще создавать пакеты и он может полагаться на раннее z-тестирование, чтобы избежать повторного отрисовывания (overdraw).
Когда используется антиалиасинг с многократной выборкой, для контента, отрисовываемого в буферы кадров, необходимы дополнительные расширения для поддержки многократной выборки буферов кадров. Обычно GL_EXT_framebuffer_multisample и GL_EXT_framebuffer_blit. Большинство настольных чипов имеют эти расширения, но они менее распространены в встроенных чипах. Когда многократная выборка буферов кадров недоступна в аппаратном обеспечении, контент, отрисованный в буферы кадров, не будет антиалиасирован, включая контент типа ShaderEffectSource.
Производительность
Как уже говорилось вначале, понимание тонкостей рендерера не требуется для достижения хорошей производительности. Он разработан для оптимизации общих случаев использования и будет демонстрировать хорошую производительность практически во всех ситуациях.
- Хорошая производительность обеспечивается эффективным кэшированием (batching), с минимальным количеством геометрии, которая загружается снова и снова. Установив переменную среды
QSG_RENDERER_DEBUG=render, рендерер будет выводить статистику о том, насколько хорошо проходит кэширование (batching), сколько пакетов используется, какие пакеты сохраняются, а какие непрозрачны и не сохраняются. При стремлении к оптимальной производительности загрузка должна выполняться только при необходимости, число пакетов должно быть меньше 10, и по крайней мере 3-4 из них должны быть непрозрачными. - По умолчанию рендерер не выполняет обрезку области просмотра (viewport clipping) и обнаружение окклюзии на стороне процессора (CPU). Если что-то не должно быть видно, оно не должно отображаться. Используйте
Item::visible: falseдля элементов, которые не должны отображаться. Основная причина, по которой такая логика не добавлена, заключается в том, что это добавляет дополнительную стоимость, которая также повлияет на приложения, которые позаботились о правильном поведении. - Убедитесь, что используется атлас текстур. Элементы Image и BorderImage будут использовать его, если изображение не слишком большое. Для текстур, созданных в C++, передайте QQuickWindow::TextureCanUseAtlas при вызове QQuickWindow::createTexture(). Установив переменную среды
QSG_ATLAS_OVERLAY, все атлас-текстуры будут окрашены в цвет, чтобы их легко было идентифицировать в приложении. - По возможности используйте непрозрачные примитивы. Непрозрачные примитивы обрабатываются рендерером быстрее и быстрее отрисовываются на GPU. Например, файлы PNG часто имеют альфа-канал, даже если каждый пиксель полностью непрозрачен. JPG-файлы всегда непрозрачны. При предоставлении изображений в QQuickImageProvider или создании изображений с помощью QQuickWindow::createTextureFromImage(), сделайте формат изображения QImage::Format_RGB32, если это возможно.
- Будьте внимательны к тому, что перекрывающиеся составные элементы, как на иллюстрации выше, не могут быть кэшированы.
- Обрезка нарушает кэширование (batching). Никогда не используйте ее на уровне отдельного элемента, внутри ячеек таблиц, делегатов элементов или аналогичных элементов. Вместо обрезки текста используйте обрезку (eliding). Вместо обрезки изображения создайте QQuickImageProvider, который возвращает обрезанное изображение.
- Кэширование (batching) работает только с 16-битными индексами. Все встроенные элементы используют 16-битные индексы, но пользовательская геометрия может использовать также 32-битные индексы.
- Некоторые флаги материалов предотвращают кэширование (batching), самым ограничительным из них является QSGMaterial::RequiresFullMatrix, который предотвращает все кэширование.
- Приложения с одноцветным фоном должны устанавливать его с помощью QQuickWindow::setColor() вместо использования элемента Rectangle на верхнем уровне. QQuickWindow::setColor() будет использоваться в вызове
glClear(), что потенциально быстрее. - Изображения с MIP-масштабированием не размещаются в глобальном атласе и не будут кэшироваться.
- Ошибка в драйвере OpenGL, связанная с обратным считыванием буфера кадров (FBO), может повредить отрисованные глифы. Если вы установите переменную среды
QML_USE_GLYPHCACHE_WORKAROUND, Qt сохраняет дополнительную копию глифа в оперативной памяти. Это означает, что производительность немного ниже при отрисовке глифов, которые еще не были отрисованы, так как Qt обращается к дополнительной копии через процессор. Это также означает, что кеш глифов будет использовать в два раза больше памяти. Качество не пострадает от этого.
Если приложение работает плохо, убедитесь, что рендеринг действительно является узким местом. Используйте профайлер! Переменная среды 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, чтобы проиллюстрировать влияние обрезки на кэширование (batching).
Оригинал
Примечание: Визуализируемые элементы не учитывают обрезку, а порядок отрисовки произвольный.
Визуализация пакетов
Установка QSG_VISUALIZE на batches визуализирует пакеты (batches) в рендерере. Объединенные пакеты рисуются сплошным цветом, а не объединенные — диагональной штриховкой. Несколько уникальных цветов означает хорошее кэширование (batching). Необъединенные пакеты являются плохими, если они содержат много отдельных узлов.
QSG_VISUALIZE=batches
Визуализация обрезки
Установка QSG_VISUALIZE на clip рисует красные области поверх сцены, чтобы указать обрезку. Поскольку элементы Qt Quick по умолчанию не обрезаются, обычно обрезка не визуализируется.
QSG_VISUALIZE=clip
Визуализация изменений
Установка QSG_VISUALIZE на changes визуализирует изменения в рендерере. Изменения в графе сцены визуализируются мерцающей накладкой случайного цвета. Изменения в примитиве визуализируются сплошным цветом, а изменения в предке, такие как изменения матрицы или непрозрачности, визуализируются рисунком.
Визуализация повторной отрисовки
Установка QSG_VISUALIZE на overdraw визуализирует повторную отрисовку (overdraw) в рендерере. Визуализирует все элементы в 3D, чтобы выделить повторные отрисовки. Этот режим также можно использовать для обнаружения геометрии за пределами области просмотра (viewport) в некоторой степени. Непрозрачные элементы отрисовываются с зеленоватым оттенком, а полупрозрачные — с красным. Границы области просмотра (viewport) отрисовываются синим цветом. Непрозрачный контент легче обрабатывается графом сцены и обычно быстрее отрисовывается.
Обратите внимание, что корневой прямоугольник в коде выше избыточен, так как окно также белое, поэтому рисование прямоугольника — пустая трата ресурсов в данном случае. Изменение его на элемент Item может дать небольшой прирост производительности.
QSG_VISUALIZE=overdraw
© The Qt Company Ltd
Licensed under the GNU Free Documentation License, Version 1.3.
https://doc.qt.io/qt-5.13/qtquick-visualcanvas-scenegraph-renderer.html