Spec-Zone.ru › Qt 5.9

Qt Quick Scene Graph OpenGL рендерер сцены

Данный документ объясняет, как работает рендерер сцены для OpenGL внутри, чтобы можно было писать код, который использует его оптимальным образом, как с точки зрения производительности, так и с точки зрения возможностей.

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

Примечание: Даже в том случае, если каждый кадр уникален и всё загружается с нуля, стандартный рендерер будет работать хорошо.

Элементы Qt Quick в QML-сцене заполняют дерево экземпляров QSGNode. После создания это дерево — полное описание того, как должен быть рендерен конкретный кадр. Оно не содержит никаких ссылок на элементы Qt Quick и на большинстве платформ будет обрабатываться и рендериться в отдельном потоке. Рендерер — это автономная часть схемы сцены, которая обходит дерево QSGNode и использует геометрию, определённую в QSGGeometryNode, и состояние шейдера, определённое в QSGMaterial, для планирования изменений состояния OpenGL и вызовов рисования.

При необходимости рендерер можно полностью заменить с помощью внутреннего API-интерфейса схемы сцены. Это в основном интересно для поставщиков платформ, которые хотят использовать нестандартные функции оборудования. Для большинства случаев использования стандартного рендерера будет достаточно.

Стандартный рендерер фокусируется на двух основных стратегиях оптимизации рендеринга: объединении вызовов рисования и сохранении геометрии на GPU.

Объединение

Там, где традиционный 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, что может быть довольно дорого, особенно на мобильных и встраиваемых GPU.

Непрозрачные примитивы рендерятся спереди назад с glDepthMask и GL_DEPTH_TEST включёнными. На GPU, которые внутренне выполняют ранние проверки по z, это означает, что фрагментный шейдер не должен выполняться для пикселей или блоков пикселей, которые скрыты. Обратите внимание, что рендерер всё равно должен учитывать эти узлы, и вершинный шейдер всё равно выполняется для каждой вершины в этих примитивах, поэтому, если приложение знает, что что-то полностью скрыто, лучше всего явно скрыть его с помощью Item::visible или Item::opacity.

Примечание: Item::z используется для управления порядком элементов в стеке относительно их коллег. Он не имеет прямой связи с рендерером и буфером глубины OpenGL.

Примитивы с альфа-смешиванием

После отрисовки непрозрачных примитивов рендерер отключит glDepthMask, включит GL_BLEND и отрисует все примитивы с альфа-смешиванием сзади наперёд.

Объединение примитивов с альфа-смешиванием требует от рендерера немного больше усилий, так как элементы, которые перекрываются, должны быть отрисованы в правильном порядке, чтобы альфа-смешивание выглядело правильно. Только на буфере глубины опираться недостаточно. Рендерер выполняет проход по всем примитивам с альфа-смешиванием и анализирует их ограничительный прямоугольник помимо состояния их материала, чтобы понять, какие элементы можно объединить, а какие нет.

В крайнем левом случае синие фоны можно нарисовать одним вызовом, а два текстовых элемента — другим вызовом, так как тексты перекрываются только фоном, перед которым они расположены в стеке. В правом крайнем случае фон «Item 4» перекрывает текст «Item 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]. Изменение этих значений будет в основном интересно для поставщиков платформ.

Корни пакетов

В дополнение к объединению совместимых примитивов в пакеты, стандартный рендерер также пытается минимизировать объём данных, которые необходимо отправлять на GPU для каждого кадра. Стандартный рендерер идентифицирует поддеревья, которые принадлежат вместе, и пытается поместить их в отдельные пакеты. После идентификации пакеты объединяются, загружаются и сохраняются в памяти GPU с использованием объектов буфера вершин.

Узлы преобразования

Каждый элемент Qt Quick вставляет QSGTransformNode в дерево схемы сцены для управления его x, y, масштабом или вращением. Дочерние элементы будут заполнены под этим узлом преобразования. Стандартный рендерер отслеживает состояние узлов преобразования между кадрами и анализирует поддеревья, чтобы определить, является ли узел преобразования хорошим кандидатом на то, чтобы стать корнем набора пакетов. Узел преобразования, который меняется между кадрами и который имеет довольно сложное поддерево, может стать корнем пакета.

QSGGeometryNodes в поддереве корня пакета предварительно преобразуются относительно корня на ЦП. Затем они загружаются и сохраняются на GPU. При изменении преобразования рендерер обновляет только матрицу корня, а не каждый отдельный элемент, что делает прокрутку списков и таблиц очень быстрой. Для последовательных кадров, пока узлы не добавляются и не удаляются, отрисовка списка фактически бесплатная. Когда новый контент входит в поддерево, пакет, который его получает, перестраивается, но это всё равно относительно быстро. Обычно для каждого кадра с добавленными или удалёнными узлами при прокрутке таблицы или списка существует несколько неизменных кадров.

Ещё одним преимуществом идентификации узлов преобразования как корней пакетов является то, что это позволяет рендереру сохранять части дерева, которые не изменились. Например, предположим, что пользовательский интерфейс состоит из списка и строки кнопок. При прокрутке списка и добавлении/удалении элементов списка остальная часть пользовательского интерфейса, строка кнопок, не изменяется и может быть отрисована с помощью геометрии, уже сохранённой на GPU.

Пороговые значения узла и вершины для того, чтобы узел преобразования стал корнем пакета, можно переопределить, используя переменные среды QSG_RENDERER_BATCH_NODE_THRESHOLD=[count] и QSG_RENDERER_BATCH_VERTEX_THRESHOLD=[count]. Переопределение этих флагов будет в основном полезно для поставщиков платформ.

Примечание: Под корнем пакета создаётся один пакет для каждого уникального набора состояния материала и типа геометрии.

Обрезка

При установке Item::clip в значение true, создается QSGClipNode с прямоугольником в своей геометрии. По умолчанию рендерер применяет этот обрез (clip) с помощью цитрингов в OpenGL. Если элемент повернут на угол, не равный 90 градусов, используется буфер трафарета OpenGL. QML Item поддерживает только установку прямоугольника в качестве обрезки, но 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 из них должны быть непрозрачными.
  • По умолчанию рендерер не выполняет обрезку области просмотра и не определяет окклюзии на стороне процессора. Если элемент не должен быть виден, он не должен отображаться. Используйте 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(), что потенциально быстрее.
  • Элементы Image с mipmapping не размещаются в глобальном атласе и не кэшируются.

Если приложение работает медленно, убедитесь, что рендеринг действительно является узким местом. Используйте профайлер! Переменная среды 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 для выделения перерисовок. Этот режим также можно использовать для обнаружения геометрии за пределами области просмотра до некоторой степени. Непрозрачные элементы отрисовываются с зелёным оттенком, а полупрозрачные — с красным. Границы области просмотра отрисовываются синим цветом. Непрозрачное содержимое легче обрабатывается деревом сцены и обычно быстрее отрисовывается.

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

"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.9/qtquick-visualcanvas-scenegraph-renderer.html

Spec-Zone.ru

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