На этой странице подробно описан весь графический конвейер средства визуализации с высокой доступностью (HAR), а также отслеживается поток данных от документа дизайна Figma до конечных пикселей, отображаемых на экране.
Обзор
Конвейер преобразует высокоуровневые определения пользовательского интерфейса в низкоуровневые графические команды и эффективно представляет их на аппаратных дисплеях. Конвейер предназначен для критически важных приложений, связанных с безопасностью автомобиля. Он обеспечивает детерминированный рендеринг, эффективное управление состоянием и надежное взаимодействие с графическими подсистемами платформы, такими как Direct Rendering Manager (DRM) и Generic Buffer Management (GBM).
Конвейер можно разделить на четыре основных этапа:
- Предварительная отрисовка. Обработка графа сцены, применение настроек и определение макета.
- Генерация команд. Преобразование разрешенного графа сцены в список отображения, не зависящий от бэкенда.
- Рендеринг. Выполнение команд рисования с помощью графического движка Impeller.
- Презентация. Управление буферами кадров и синхронизация с аппаратным обеспечением дисплея.
Рисунок 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. Передача в Looper
После создания DisplayList Reducer упаковывает его в экземпляр ViewDescriptor и отправляет по каналу Rust MPSC (LooperMessage) в поток Looper. Looper отвечает за отрисовку и показ, что не позволяет потоку Reducer блокировать графический конвейер.
Этап 3. Отрисовка
Независимый от платформы DisplayList передается в серверную часть рендеринга, где абстрактные команды преобразуются в инструкции графического процессора.
В HAR используется Impeller – движок рендеринга, изначально созданный для Flutter. Impeller решает проблему сбоев частоты кадров из-за компиляции шейдеров, предварительно компилируя небольшой эффективный набор шейдеров во время сборки. Такой подход в сочетании с эффективной пакетной обработкой и оптимизированным бэкендом обеспечивает:
- Детерминированная производительность. Практически исключает сбои компиляции шейдеров во время выполнения.
- Быстрый запуск. Снижает накладные расходы на инициализацию.
- Компактность. Небольшой размер двоичного файла.
Чтобы узнать больше об архитектуре Impeller, посмотрите видео Introducing Impeller - Flutter's new rendering engine (Представляем Impeller – новый движок рендеринга Flutter). Хотя в видео обсуждается Flutter, эти основные преимущества напрямую расширяют возможности автомобильного стека HAR.
Ключевые компоненты этапа отрисовки:
ImpellerRenderer– преобразует список отображения из этапа предварительной отрисовки в команды отрисовки Impeller.Impeller Rust API. Оболочка библиотеки Impeller для использования в Rust (крейты
impellerиimpeller-rs-bindgen).TypographyContext– управляет регистрацией шрифтов и формированием текста.
3.1. Инициализация и управление поверхностями
Создание контекста. Рендерер инициализирует экземпляр
impeller::Contextс бэкендом OpenGL ES, передавая обратный вызов для разрешения указателей функций OpenGL ES из контекста GL платформы.Обернутая поверхность FBO. Вместо создания собственного окна Impeller выполняет рендеринг в существующий объект буфера кадров (FBO) OpenGL, предоставляемый Phase 4. Для этого используется вызов
Surface::create_wrapped_fbo.
3.2. Управление ресурсами
Изображения. Поддерживаются стандартные форматы и сжатые текстуры KTX2. Они загружаются в текстуры графического процессора и управляются внутренней структурой
Resources.Шрифты. Шрифты TrueType и OpenType загружаются и регистрируются в
TypographyContextдля отображения текста.Внешние изображения. Специализированная обработка внешних текстур (например, видеопотоков с камер и внешних 3D-рендереров) включает привязку экземпляров
EGLImageили внешних текстур OpenGL к объектамTextureImpeller для рендеринга без копирования.
3.3. Проход рендеринга
Цикл render создает экземпляр DisplayList Impeller (не путайте с Vec<DisplayListEntry>, созданным на этапе предварительной отрисовки) с помощью DisplayListBuilder:
Очищает буфер и применяет глобальные преобразования для масштабирования DPI и поворота экрана.
Выполняет итерацию по входным элементам
DisplayListEntry:- Состояние.
save()иrestore()используются для добавления и удаления преобразований и областей отсечения. - Примитивы.
RectиRoundedRectотрисовываются с помощью стандартных операций рисования. - Контуры. Создаются и отрисовываются сложные векторные контуры (в том числе динамические экземпляры
Arc). - Текст.
TextиStyledTextотображаются с помощьюTypographyContext. - Изображения. Стандартные и внешние изображения отрисовываются с помощью
draw_texture_rect.
- Состояние.
Отправляет созданный список отображения Impeller на поверхность с помощью
surface.draw_display_list(), генерируя базовые команды GL.Вызывает
swap_buffers()в базовом контексте, чтобы запустить этап 4.
Этап 4. Презентация
На этом этапе происходит взаимодействие с аппаратным обеспечением дисплея, чтобы показать отрисованный кадр. HAR использует надежный путь прямого рендеринга на программно-определяемых автомобилях (SDV) с Android Automotive OS (AAOS).
Ключевым компонентом этого этапа является HarDirectRenderingContext (в контейнере har-gl-context).
4.1. Архитектура
На уровне представления используется подход с двойной буферизацией и целью отрисовки за пределами экрана:
Буфер отрисовки. Закадровый объект фреймбуфера, в котором Impeller отрисовывает сцену.
Буфер разрешения (необязательно). Дополнительный буфер для поддержки множественной выборки со сглаживанием (MSAA).
- Его можно включить, если это необходимо для реализации или конфигурации OpenGL ES. В таких случаях он служит промежуточной целью для разрешения буфера отрисовки с множественной выборкой перед блиттингом (передачей битовых блоков) в буфер рендеринга.
Буфер рендеринга. Общий буфер, поддерживаемый объектом GBM, который соответствует заднему буферу в типичной цепочке смены графики.
Передний буфер. Буфер GBM, который сканируется на дисплей.
4.2. Цепочка буферов
При вызове swap_buffers HAR выполняет следующие действия:
Переносит содержимое буфера отрисовки в буфер рендеринга (при необходимости с промежуточным переносом в буфер разрешения).
Вызывает
glFlush()в контексте GL и создает экземплярEGL_SYNC_NATIVE_FENCE_ANDROIDдля отслеживания завершения работы графического процессора.Создает атомарный запрос DRM для замены буфера рендеринга на экран. Этот запрос содержит дескриптор файла барьера графического процессора (так называемый входной барьер), который не позволяет контроллеру дисплея показывать буфер рендеринга до того, как графический процессор закончит отрисовку.
Одновременно запрашивает у DRM новый барьер (барьер вывода), чтобы сигнализировать, когда предыдущий буфер (передний буфер для предыдущего кадра) больше не находится на экране.
Фиксирует атомарный запрос с помощью неблокирующего флага, чтобы основной поток мог продолжаться, пока графические подсистемы остаются синхронизированными.
Сохраняет новую внешнюю границу в контексте, чтобы 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. Рабочий процесс
Процесс включает следующее:
Внешний сервис запускается и регистрируется в основном цикле, определяя, какие теги Figma (например,
#cluster/3d-car) он отображает.Сервис ожидает сигналы
RenderStartот цикла, чтобы синхронизировать отрисовку с сигналом VSYNC дисплея.Сервис отрисовывает контент в буфере кадров, предоставленном
SurfacePool.Сервис вызывает
swap_buffersв своем контексте, что поворачивает пул и делает завершенный кадр доступным в качестве экземпляраSharedSurface.SharedSurfaceупаковывается вExternalImageи отправляется по каналу Rust MPSC в цикл.Основной модуль визуализации 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для создания контекста pbuffer за пределами экрана с дисплеем EGL по умолчанию. Это позволяет выполнять рендеринг в буфер за пределами экрана без необходимости в видимом окне или прямом доступе к оборудованию дисплея. В основном используется для автоматизированного тестирования или обработки на сервере.