Графический конвейер HAR

На этой странице подробно описан полный графический конвейер высокодоступного рендерера (HAR), отслеживающий поток данных от проектного документа Figma до конечных пикселей, отображаемых на экране.

Обзор

Конвейер преобразует высокоуровневые определения пользовательского интерфейса в низкоуровневые графические команды и эффективно отображает их на аппаратных дисплеях. Конвейер разработан для критически важных с точки зрения безопасности автомобильных приложений, с акцентом на детерминированное рендеринг, эффективное управление состоянием и надежное взаимодействие с графическими подсистемами платформы, такими как Direct Rendering Manager (DRM) и Generic Buffer Management (GBM).

Процесс можно разделить на четыре основных этапа:

  1. Предварительная отрисовка: обработка графа сцены, применение настроек и разрешение компоновки.
  2. Генерация команд: Преобразование графа разрешенной сцены в список отображаемых данных, не зависящий от используемой серверной части.
  3. Рендеринг: Выполнение команд отрисовки с использованием графического движка Impeller.
  4. Презентация: Управление буферами кадров и синхронизация с аппаратным обеспечением дисплея.

HAR Graphics Flow

Рисунок 1. Графическая схема HAR.

Этап 1: Предварительная отрисовка

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

1.1 Основа DesignCompose

Конвейер обработки изображений HAR построен на основе экосистемы DesignCompose.

  • Источник: Пользовательский интерфейс разработан в Figma и экспортирован с помощью плагина DesignCompose.
  • Определение: Результатом является экземпляр класса DesignComposeDefinition , сериализованное представление проекта (узлы, стили, варианты).
  • Привязка данных: В модели пользовательского интерфейса приложения используются процедурные макросы (например, #[Design(node = "#speed")] ) для явной привязки полей структуры Rust к конкретным именованным узлам в документе Figma. Это позволяет состоянию приложения автоматически управлять свойствами визуальных элементов.

Ключевыми компонентами этого фундамента являются:

  • Редуктор: выступает в качестве центрального цикла обработки событий, обрабатывая действия и обновляя текущее состояние. Фреймворк предоставляет DefaultReducer , но при необходимости может быть предоставлена ​​собственная реализация редуктора.
  • Presenter: Связывает текущее состояние с моделью пользовательского интерфейса. Трейт Presenter определяется библиотекой фреймворка harry , а эталонная реализация ( UIModelPresenter ) предоставляется в библиотеке harry-app-core .
  • Модель пользовательского интерфейса: генерирует настройки на основе текущего состояния. Код модели пользовательского интерфейса генерируется с помощью макроса DesignDocument предоставляемого крейтом derive_customizations . Структура UIModel в крейте harry-app-core предоставляет пример этого.
  • Squoosh: Предоставляет структуру данных SquooshView и репозиторий вариантов, используемых для отрисовки пользовательского интерфейса в соответствии с дизайном. Сериализованный документ дизайна загружается библиотекой dc_bundle из DesignCompose и преобразуется в дерево структур SquooshView для повышения эффективности во время выполнения.

1.2 Редукторная петля

Конвейер обработки данных управляется действиями. В фреймворке указан перечисляемый тип Actions , который определяет внутренние действия, используемые самим фреймворком, а также вариант CustomAction , позволяющий пользователям определять дополнительные действия, специфичные для приложения (например, UpdateVehicleSpeed ​​или ButtonPress ).

Фреймворк также предоставляет трейт StateAction , который упрощает реализацию действий, влияющих на состояние приложения и, при необходимости, генерирующих побочные эффекты, которые затем передаются обратно в приложение из редуктора для обработки. Подробный пример этого можно найти в перечислении CustomActions в крейте harry-app-core .

Это базовая схема цикла редуктора:

  • Обработка действий: Reducer получает действие и обновляет текущее состояние. Это исходные данные, такие как текущая скорость или какие индикаторы (предупреждающие лампы) активны. Это также может вызывать побочные эффекты (например, звуковой сигнал, воспроизводимый при мигании индикатора ремня безопасности).
  • Презентация: Presenter отображает новое состояние в UIModel . UIModel — это модель представления, содержащая данные, специально отформатированные для пользовательского интерфейса (например, форматирование скорости "120" в строку "65 миль/час").
  • Генерация настроек: вызывается метод apply модели пользовательского интерфейса для генерации набора экземпляров RenderCustomization . Это явные инструкции по изменению дизайна Figma (например, "Установить текст узла #speed на '65 миль/час'").
  • UpdatePolicy для оптимизации: После каждого прохода предварительной отрисовки возвращается значение UpdatePolicy , указывающее, когда потребуется следующее обновление отрисовки. Если нет ожидающих изменений состояния и не запущены анимации, UpdatePolicy сигнализирует о том, что дальнейшие обновления немедленно не требуются. В таких случаях Reducer прекращает генерацию новых списков отображения, предотвращая ненужные циклы отрисовки и экономя ресурсы до тех пор, пока новое действие или событие не вызовет изменение.

1.3 Инициализация процесса загрузки и репозитория представлений

Процесс начинается с экземпляра DesignComposeDefinition . Это проектный документ Figma, сериализованный DesignCompose в структуру протокола буферизации.

  • Первоначальная загрузка: При запуске основной проект (указанный его корневым узлом) преобразуется из DesignComposeDefinition в начальное дерево SquooshView . Это одноразовый процесс.

  • Репозиторий: SquooshVariantRepository управляет многократно используемыми вариантами компонентов и первоначально загружаемыми представлениями.

  • Ленивая загрузка: Чтобы минимизировать время запуска и использование памяти, дополнительные представления (не входящие в исходное дерево корневых узлов) загружаются из документа лениво только тогда, когда на них явно ссылается и они необходимы для логики рендеринга (например, при настройке списка).

1.4. Проход для настройки

Для применения динамического состояния приложения выполняется обход дерева SquooshView :

  • Замена вариантов: Экземпляры компонентов заменяются на определенные варианты (например, значок, представляющий текущий режим движения, меняется со спортивного на экономичный) на основе логики, определяемой временем выполнения.

  • Расширение списка: В Figma один элемент шаблона заменяется динамическим списком дочерних элементов. Для этих дочерних элементов генерируются новые уникальные идентификаторы для обеспечения стабильной идентификации анимаций.

  • Переопределение текста и стиля: текстовое содержимое (например, значение скорости) и стили (например, прозрачность, цвет) обновляются на основе текущего состояния.

1.5 Переменное разрешение

Токены дизайна и переменные, определенные в Figma или локально в приложении, обрабатываются.

  • Привязка: Свойства SquooshView , ссылающиеся на переменные (например, цвета или размеры), заменяются их конкретными значениями для текущего кадра.

1.6 Расчет компоновки

  • Динамическая компоновка: DynamicLayout вычисляет конечное положение и размер (границы) каждого узла в дереве SquooshView .

  • Макет текста: TextHelper использует реализацию трейта LayoutHelper для вычисления метрик текста, переноса и формирования. Это помогает убедиться, что текст правильно располагается в пределах заданных ограничений перед отрисовкой.

1.7 Циферблаты и датчики

Это специализированный этап для автомобильных пользовательских интерфейсов.

  • MeterData : Если узел содержит данные счетчика (определенные в Figma), его геометрия динамически изменяется в зависимости от meter_value (например, скорости транспортного средства).
    • Дуги: Угол развертки регулируется.
    • Вращения: Преобразование вращения рассчитывается на основе начального и конечного углов.
    • Индикаторы выполнения: ширина или высота прямоугольника масштабируются.
    • Векторы прогресса: Длина траектории вектора корректируется.

1.8 Анимация

  • Сравнение: Текущий SquooshView сравнивается с previous_squoosh_view из PreRenderCache .

  • Интерполяция: Если свойства изменились, Squoosh создает интерполяторы для плавного изменения значений (например, прозрачности или преобразования) во времени.

Этап 2: Генерация команд

После того, как дерево SquooshView полностью сформировано и анимировано, оно преобразуется в линейную последовательность команд рисования.

Ключевым компонентом этого этапа является библиотека DisplayList :

  • generate_dl : Эта функция рекурсивно обходит дерево SquooshView .

  • Перевод:

    • Фигуры и контуры: Преобразованы в DisplayListEntry с соответствующим вариантом DisplayListAppearance (например, Rect или Path ).
    • Текст: Преобразовано с помощью TextHelper в текстовые графические элементы.
    • Преобразования и отсечения: преобразуются в пары PushTransform3D и PopTransform3D или PushClipRegion и PopClipRegion для управления стеком состояний отрисовки.
    • Маскирование: преобразовано в пары PushMaskLayer и PopMaskLayer для корректного создания и смешивания слоев.

В итоге получается экземпляр класса Vec<DisplayListEntry> , описывающий, что нужно нарисовать, независимо от способа рисования.

2.1 Передача управления петлеобразователю

После генерации DisplayList , Reducer оборачивает его в экземпляр ViewDescriptor и отправляет по каналу Rust MPSC ( LooperMessage ) потоку Looper. Looper отвечает за этапы рендеринга и отображения, что предотвращает блокировку графического конвейера потоком Reducer.

Этап 3: Рендеринг

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

HAR использует Impeller, движок рендеринга, первоначально разработанный для Flutter. Impeller предназначен для решения проблемы сбоев частоты кадров, возникающих из-за компиляции шейдеров, путем предварительной компиляции небольшого, эффективного набора шейдеров во время сборки. Такой подход в сочетании с эффективной пакетной обработкой и высокооптимизированным бэкендом обеспечивает:

  • Детерминированная производительность: практически исключает ошибки компиляции шейдеров во время выполнения.
  • Быстрый запуск: снижает накладные расходы на инициализацию.
  • Небольшой размер: обеспечивает компактный двоичный формат.

Для более подробного ознакомления с архитектурой Impeller посмотрите видео [Представляем Impeller — новый движок рендеринга Flutter][impeller-video]. Хотя в видео обсуждается Flutter, эти основные преимущества напрямую расширяют возможности автомобильного стека HAR.

Ключевыми компонентами этапа рендеринга являются:

  • ImpellerRenderer : Преобразует список отображаемых данных из фазы предварительной отрисовки в команды отрисовки Impeller.

  • API Impeller для Rust: Обёртка для библиотеки Impeller, предназначенная для использования в Rust (крейты impeller и impeller-rs-bindgen ).

  • TypographyContext : Управляет регистрацией шрифта и изменением формы текста.

импеллер-видео

3.1 Инициализация и управление поверхностью

  • Создание контекста: Рендерер инициализирует экземпляр impeller::Context с бэкендом OpenGL ES, передавая функцию обратного вызова для разрешения указателей на функции OpenGL ES из контекста GL платформы.

  • Обернутая поверхность FBO: Вместо создания собственного окна Impeller выполняет рендеринг в существующий объект кадрового буфера OpenGL (FBO), предоставленный Phase 4. Это делается путем вызова Surface::create_wrapped_fbo .

3.2 Управление ресурсами

  • Изображения: Поддерживаются стандартные форматы и текстуры, сжатые по KTX2. Они загружаются в текстуры графического процессора и управляются внутренней структурой Resources .

  • Шрифты: для отображения текста загружаются и регистрируются шрифты TrueType и OpenType в объекте TypographyContext .

  • Внешние изображения: Специализированная обработка внешних текстур (например, видеопотоков с камер и внешних 3D-рендеров) включает привязку экземпляров EGLImage или внешних текстур OpenGL к объектам Impeller Texture для рендеринга без копирования.

3.3 Проход рендеринга

Цикл render создает экземпляр Impeller DisplayList (не путать с Vec<DisplayListEntry> , сгенерированным на этапе предварительного рендеринга) с помощью DisplayListBuilder :

  1. Очищает буфер и применяет глобальные преобразования для масштабирования DPI и поворота дисплея.

  2. Выполняет итерацию по элементам входного DisplayListEntry :

    • Состояние: save() и restore() используются для добавления и удаления преобразований и областей обрезки.
    • Примитивы: Rect и RoundedRect отрисовываются с использованием стандартных операций рисования.
    • Пути: Создаются и отображаются сложные векторные пути (включая динамические экземпляры Arc ).
    • Текст: Text и StyledText отображаются с использованием TypographyContext .
    • Изображения: Стандартные и внешние изображения отрисовываются с помощью draw_texture_rect .
  3. Отправляет созданный список отображаемых объектов Impeller на поверхность с помощью surface.draw_display_list() , генерируя соответствующие команды GL.

  4. Вызывает swap_buffers() для базового контекста, чтобы запустить фазу 4.

Этап 4: Презентация

На заключительном этапе осуществляется взаимодействие с аппаратным обеспечением дисплея для отображения отрендеренного кадра. HAR использует надежный прямой путь рендеринга в программно-определяемой системе управления транспортными средствами (SDV) на базе Android Automotive OS (AAOS).

Ключевым компонентом этого этапа является HarDirectRenderingContext (из крейта har-gl-context ).

4.1 Архитектура

В слое представления используется подход с двойной буферизацией и целевым объектом отрисовки за пределами экрана:

  1. Буфер отрисовки: Внеэкранный FBO, через который Impeller выполняет рендеринг сцены.

  2. Буфер разрешения (опционально): Дополнительный буфер, необязательный для поддержки многосэмплового сглаживания (MSAA).

    • Эта функция может быть включена при необходимости в зависимости от базовой реализации или конфигурации OpenGL ES. В таких случаях она служит промежуточным целевым объектом для разрешения буфера многосэмплированной отрисовки перед копированием (передачей битового блока) в буфер рендеринга.
  3. Буфер рендеринга: универсальный буфер, поддерживаемый объектом GBM, который соответствует буферу резервного копирования в типичной цепочке обмена графическими файлами.

  4. Передний буфер: буфер GBM, содержимое которого выводится на дисплей.

4.2 Цепочка обмена

При вызове функции swap_buffers , HAR выполняет следующие действия:

  1. Выполняет копирование содержимого буфера отрисовки в буфер рендеринга (с промежуточным копированием в буфер разрешения, если это необходимо для реализации).

  2. Вызывает функцию glFlush() для контекста GL и создает экземпляр EGL_SYNC_NATIVE_FENCE_ANDROID для отслеживания завершения работы графического процессора.

  3. Создает атомарный запрос DRM для переключения буфера рендеринга на экран. Этот запрос содержит файловый дескриптор (называемый входным дескриптором) буфера рендеринга, предотвращающий его отображение контроллером дисплея до завершения отрисовки графическим процессором.

  4. Одновременно запрашивает у DRM новый буфер (называемый исходящим буфером), чтобы сигнализировать о том, что предыдущий буфер (передний буфер для предыдущего кадра) больше не отображается на экране.

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

  6. Сохраняет новый выходной буфер в контексте, чтобы HAR мог дождаться его сигнала в начале процесса swap_buffers в следующем кадре. Это предотвращает отрисовку графическим процессором буфера, который все еще отображается.

4.3 Настройка прямого режима

HAR взаимодействует напрямую с ядром, используя подсистемы DRM и настройки режима ядра (KMS) для конфигурации разрешения дисплея AAOS SDV, минуя взаимодействие с оконными менеджерами, такими как SurfaceFlinger (в определенных конфигурациях), что позволяет осуществлять эксклюзивное и высокоприоритетное управление аппаратным обеспечением дисплея.

4.4 Внешнее отображение

HAR поддерживает делегирование отрисовки определенных элементов пользовательского интерфейса (идентифицируемых тегами в Figma) внешним процессам или потокам. Это полезно для интеграции сложных 3D-сцен (например, визуализации автомобиля из таких движков, как Kanzi или Unity) или другого контента, требующего выделенного контекста OpenGL.

4.4.1 Ключевые компоненты

  • HarExternalRenderContext : Специальный контекст EGL для внешнего сервиса, предназначенный для работы вне экрана.
  • SurfacePool : управляет набором буферов LocalSurface ( Texture плюс EGLImage ) для двойной или тройной буферизации.
  • SharedSurfaceExternalImage : потокобезопасная обертка для передачи дескрипторов EGLImage между внешним сервисом и основным рендерером.

4.4.2 Рабочий процесс

Рабочий процесс осуществляется в следующей последовательности:

  1. Внешний сервис запускается и регистрируется в основном цикле обработки изображений, определяя, какие теги Figma (например, #cluster/3d-car ) он отображает.

  2. Сервис ожидает сигналов RenderStart от устройства зацикливания, чтобы синхронизировать рендеринг с сигналом VSYNC дисплея.

  3. Вне экрана сервис отображает свой контент в буфер кадров, предоставляемый SurfacePool .

  4. Сервис вызывает swap_buffers для своего контекста, которая выполняет ротацию пула и делает завершенный кадр доступным в качестве экземпляра SharedSurface .

  5. SharedSurface оборачивается в ExternalImage и передается по каналу Rust MPSC на устройство зацикливания.

  6. Основной рендерер Impeller (фаза 3) получает внешнее изображение. Вместо копирования пиксельных данных он напрямую привязывает базовое EGLImage к текстуре и отрисовывает его как часть основной сцены, обеспечивая композицию без копирования.

4.5 Платформы разработки и тестирования (har-platform-linux)

В целях разработки и тестирования приложения HAR могут быть ориентированы на стандартные среды рабочего стола Linux и безмониторные системы. Эти платформы реализованы в библиотеке crates/reference/platforms/har-platform-linux .

В отличие от производственной платформы AAOS SDV, эти платформы не используют подсистему direct-rendering har-gl-context для вывода на экран. Вместо этого они полагаются на стандартные библиотеки Rust OpenGL:

  • Оконный режим: использует winit для управления окнами и циклов событий, а glutin для создания контекстов OpenGL ES и интеграции с оконной системой.

  • Безголовый режим: Использует библиотеку har-gl-context для создания контекста внеэкранного буфера с использованием стандартного отображения EGL. Это позволяет выполнять рендеринг во внеэкранный буфер без необходимости видимого окна или прямого доступа к аппаратному обеспечению дисплея, что в основном используется для автоматизированного тестирования или обработки данных на бэкэнде.