Spec-Zone.ru › Qt 5.12

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 имеет значение true, кэширование этого элемента ограничено его дочерними элементами. Когда есть много дочерних элементов, таких как 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 из них должны быть непрозрачными.
  • По умолчанию рендерер не выполняет обрезку по области просмотра и обнаружение объектов на CPU. Если что-то не должно быть видно, его не следует отображать. Используйте 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() вместо использования верхнего прямоугольника. 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, чтобы продемонстрировать влияние обрезки на кэширование.

"Original"

Оригинал

Примечание: Визуализируемые элементы не учитывают обрезку, а порядок рендеринга произволен.

Визуализация пакетов

Установка QSG_VISUALIZE в значение batches визуализирует пакеты в рендерере. Объединенные пакеты отображаются сплошным цветом, а не объединенные пакеты отображаются с диагональной штриховкой. Несколько уникальных цветов означает хорошее кэширование. Не объединенные пакеты — плохо, если они содержат много отдельных узлов.

"batches"

QSG_VISUALIZE=batches

Визуализация обрезки

Установка QSG_VISUALIZE в значение clip рисует красные области поверх сцены, чтобы указать обрезку. Поскольку элементы Qt Quick по умолчанию не обрезаются, обычно обрезка не визуализируется.

QSG_VISUALIZE=clip

Визуализация изменений

Установка QSG_VISUALIZE в значение changes визуализирует изменения в рендерере. Изменения в графе сцены визуализируются мигающей накладкой случайного цвета. Изменения в примитиве визуализируются сплошным цветом, в то время как изменения в предке, такие как изменения матрицы или непрозрачности, визуализируются с узором.

Визуализация лишнего рендеринга

Установка QSG_VISUALIZE в значение overdraw визуализирует лишний рендеринг в рендерере. Визуализируйте все элементы в 3D, чтобы выделить лишний рендеринг. Этот режим также можно использовать для обнаружения геометрии за пределами области просмотра до некоторой степени. Непрозрачные элементы отображаются с зеленоватым оттенком, а полупрозрачные элементы — с красным оттенком. Область ограничительной рамки для области просмотра отображается синим цветом. Непрозрачный контент легче обрабатывается графом сцены и обычно быстрее рендерится.

Обратите внимание, что корневой прямоугольник в приведенном выше коде избыточен, так как окно также белое, поэтому рисование прямоугольника — пустая трата ресурсов в этом случае. Изменение его на элемент может обеспечить небольшой прирост производительности.

"overdraw-1"

"overdraw-2"

QSG_VISUALIZE=overdraw

© The Qt Company Ltd
Licensed under the GNU Free Documentation License, Version 1.3.
https://doc.qt.io/qt-5.12/qtquick-visualcanvas-scenegraph-renderer.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API