Canalización de gráficos HAR

En esta página, se detalla la canalización de gráficos completa del renderizador de alta disponibilidad (HAR), y se realiza un seguimiento del flujo de datos desde un documento de diseño de Figma hasta los píxeles finales que se muestran en la pantalla.

Descripción general

La canalización convierte las definiciones de IU de alto nivel en comandos de gráficos de bajo nivel y los presenta de manera eficiente en pantallas de hardware. La canalización está diseñada para apps de seguridad crítica para vehículos, y enfatiza la renderización determinista, la administración eficiente del estado y la interacción sólida con los subsistemas de gráficos de la plataforma, como el administrador de renderización directa (DRM) y la administración de búferes genéricos (GBM).

La canalización se puede dividir en cuatro fases principales:

  1. Prerrenderización: Procesamiento del gráfico de escena, aplicación de personalizaciones y resolución del diseño
  2. Generación de comandos: Conversión del gráfico de escena resuelto en una lista de visualización independiente del backend
  3. Renderización: Ejecución de comandos de dibujo con el motor de gráficos Impeller
  4. Presentación: Administración de búferes de fotogramas y sincronización con el hardware de visualización

Flujo de gráficos de HAR

Figura 1: Flujo de gráficos de HAR

Fase 1: Prerrenderización

En esta fase, se transforma el diseño estático de Figma y el estado dinámico de la app en un árbol de IU completamente resuelto en la memoria, listo para la renderización. Esta fase se ejecuta en un subproceso de reductor dedicado, separado del bucle de visualización principal.

1.1 Base de DesignCompose

La canalización de HAR se basa en el ecosistema de DesignCompose.

  • Fuente: La IU se diseña en Figma y se exporta con el complemento DesignCompose.
  • Definición: El resultado es una instancia de DesignComposeDefinition, una representación serializada del diseño (nodos, estilos, variantes).
  • Vinculación de datos: El modelo de IU de la app usa macros de procedimiento (por ejemplo, #[Design(node = "#speed")]) para vincular de forma explícita los campos de estructura de Rust a nodos con nombres específicos en el documento de Figma. Esto permite que el estado de la app controle automáticamente las propiedades de los elementos visuales.

Los componentes clave de esta base son los siguientes:

  • Reductor: Actúa como el bucle de eventos central, procesa acciones y actualiza el estado actual. El framework proporciona DefaultReducer, pero se puede proporcionar una implementación de reductor personalizada si es necesario.
  • Presentador: Conecta el estado actual al modelo de IU. El Presenter rasgo es especificado por el harry framework crate, y se proporciona una implementación de referencia (UIModelPresenter) en el harry-app-core crate.
  • Modelo de IU: Genera personalizaciones según el estado actual. El código del modelo de IU se genera con la macro DesignDocument que proporciona el crate derive_customizations. La estructura UIModel en el crate harry-app-core proporciona un ejemplo de esto.
  • Squoosh: Proporciona la estructura de datos SquooshView y el repositorio de variantes, que se usan para renderizar la IU según el diseño. El crate dc_bundle carga un documento de diseño serializado desde la biblioteca de DesignCompose y lo convierte en un árbol de estructuras SquooshView para lograr un rendimiento eficiente del tiempo de ejecución.

1.2 Bucle del reductor

La canalización se controla mediante acciones. El framework especifica el tipo enumerado Actions, que define las acciones internas que usa el framework, pero también incluye una variante CustomAction que permite a los usuarios definir acciones adicionales específicas de la app (por ejemplo, UpdateVehicleSpeed o ButtonPress).

El framework también proporciona el rasgo StateAction que simplifica la implementación de acciones que afectan el estado de la app y, de manera opcional, genera efectos secundarios que luego se pasan de nuevo a la app desde el reductor para su procesamiento. La enumeración CustomActions en el crate harry-app-core proporciona un ejemplo detallado de esto.

Este es un esquema básico del bucle del reductor:

  • Procesamiento de acciones: Reducer recibe una acción y actualiza el estado actual. Estos son los datos sin procesar, como la velocidad actual o los indicadores (luces de advertencia) que están activos. Esto también puede generar efectos secundarios (por ejemplo, una señal que reproduce un sonido cuando parpadea la luz del cinturón de seguridad).
  • Presentación: Presenter asigna el estado nuevo a UIModel. UIModel es un modelo de vista que contiene datos con un formato específico para la IU (por ejemplo, dar formato a la velocidad "120" a una cadena "65 mph").
  • Generación de personalización: Se llama al método apply del modelo de IU para generar un conjunto de instancias RenderCustomization. Estas son instrucciones explícitas para modificar el diseño de Figma (por ejemplo, "Establecer el texto del nodo #speed en '65 mph'").
  • UpdatePolicy para la optimización: Después de cada paso de prerrenderización, se muestra un valor UpdatePolicy, que indica cuándo se requiere la próxima actualización de renderización. Si no hay cambios de estado pendientes y no se ejecutan animaciones, UpdatePolicy indica que no se necesitan más actualizaciones de inmediato. En esos casos, el reductor deja de generar listas de visualización nuevas, lo que evita ciclos de renderización innecesarios y conserva recursos hasta que una nueva acción o evento active un cambio.

1.3 Ingesta de vistas e inicialización del repositorio

La canalización comienza con una instancia DesignComposeDefinition. Este es el documento de diseño de Figma serializado por DesignCompose en una estructura de búfer de protocolo.

  • Carga inicial: Al inicio, el diseño principal (especificado por su nodo raíz) se convierte de DesignComposeDefinition en un árbol SquooshView inicial. Este es un proceso que solo deberá realizar una vez.

  • Repositorio: SquooshVariantRepository administra las variantes de componentes reutilizables y las vistas cargadas inicialmente.

  • Carga diferida: Para minimizar el tiempo de inicio y el uso de memoria, las vistas adicionales (las que no forman parte del árbol de nodos raíz inicial) se cargan de forma diferida desde el documento solo cuando la lógica de renderización las hace referencia y las necesita de forma explícita (por ejemplo, durante una personalización de lista).

1.4 Paso de personalización

Se recorre el árbol SquooshView para aplicar el estado dinámico de la app:

  • Intercambios de variantes: Las instancias de componentes se intercambian con variantes específicas (por ejemplo, cambiar un ícono que representa el modo de conducción actual de deportivo a ecológico) según la lógica del tiempo de ejecución.

  • Expansión de la lista: Un solo elemento de plantilla en Figma se reemplaza por una lista dinámica de elementos secundarios. Se generan IDs únicos nuevos para estos elementos secundarios para verificar una identidad estable para las animaciones.

  • Anulaciones de texto y estilo: El contenido de texto (por ejemplo, el valor de velocidad) y los estilos (por ejemplo, la opacidad y el color) se actualizan desde el estado actual.

1.5 Resolución de variables

Se resuelven los tokens de diseño y las variables definidas en Figma o de forma local en la app.

  • Vinculación: Las propiedades SquooshView que hacen referencia a variables (como colores o dimensiones) se reemplazan por sus valores concretos para el fotograma actual.

1.6 Cálculo del diseño

  • Diseño dinámico: DynamicLayout calcula la posición y el tamaño finales (límites) de cada nodo en el árbol SquooshView.

  • Diseño de texto: TextHelper usa una implementación del rasgo LayoutHelper para calcular las métricas, el ajuste y la forma del texto. Esto ayuda a verificar que el texto fluya correctamente dentro de sus restricciones antes de la renderización.

1.7 Dial y medidores

Este es un paso especializado para las IUs de vehículos.

  • MeterData: Si un nodo tiene datos de medidor (definidos en Figma), su geometría se altera de forma dinámica según meter_value (por ejemplo, la velocidad del vehículo).
    • Arcos: Se ajusta el ángulo de barrido.
    • Rotaciones: La transformación de rotación se calcula en función de los ángulos de inicio y finalización.
    • Barras de progreso: Se ajusta el ancho o la altura de un rectángulo.
    • Vectores de progreso: Se ajusta la longitud de una ruta de vector.

1.8 Animación

  • Diferenciación: El SquooshView actual se compara con previous_squoosh_view de PreRenderCache.

  • Interpolación: Si las propiedades cambiaron, Squoosh crea interpoladores para realizar una transición fluida de los valores (por ejemplo, la opacidad o la transformación) a lo largo del tiempo.

Fase 2: Generación de comandos

Una vez que el árbol SquooshView se resuelve y se anima por completo, se convierte en una secuencia lineal de comandos de dibujo.

El componente clave de esta fase es el crate DisplayList:

  • generate_dl: Esta función recorre de forma recursiva el árbol SquooshView.

  • Traducción:

    • Formas y rutas: Se convierten en DisplayListEntry con la variante DisplayListAppearance adecuada (por ejemplo, Rect o Path).
    • Texto: Se convierte con TextHelper en entradas de dibujo de texto.
    • Transformaciones y clips: Se convierten en pares PushTransform3D y PopTransform3D o PushClipRegion y PopClipRegion para administrar la pila de estado de dibujo.
    • Enmascaramiento: Se convierte en pares PushMaskLayer y PopMaskLayer para crear y combinar capas correctamente.

El resultado final es una instancia de Vec<DisplayListEntry> que describe qué dibujar, independientemente de cómo dibujarlo.

2.1 Handoff to looper

Después de generar DisplayList, el reductor lo incluye en una instancia de ViewDescriptor y lo envía a través de un canal MPSC de Rust (LooperMessage) al subproceso del looper. El Looper es responsable de las fases de renderización y visualización, lo que evita que el subproceso del reductor bloquee la canalización de gráficos.

Fase 3: Renderización

El DisplayList independiente de la plataforma se entrega al backend de renderización, donde los comandos abstractos se traducen en instrucciones de GPU.

HAR usa Impeller, un motor de renderización creado originalmente para Flutter. Impeller está diseñado para resolver el problema de las fallas de la velocidad de fotogramas debido a la compilación de sombreadores mediante la precompilación de un conjunto pequeño y eficiente de sombreadores en el tiempo de compilación. Este enfoque, combinado con el procesamiento por lotes eficaz y un backend altamente optimizado, ofrece lo siguiente:

  • Rendimiento determinista: Elimina prácticamente las fallas de compilación de sombreadores en el tiempo de ejecución.
  • Inicio rápido: Reduce la sobrecarga de inicialización.
  • Huella pequeña: Produce un tamaño binario compacto.

Para obtener una introducción completa a la arquitectura de Impeller, mira [Introducing Impeller - Flutter's new rendering engine][impeller-video]. Aunque el video analiza Flutter, estos beneficios principales potencian directamente la pila de vehículos HAR.

Los componentes clave de la fase de renderización son los siguientes:

  • ImpellerRenderer: Convierte la lista de visualización de la fase de prerrenderización en comandos de renderización de Impeller.

  • API de Impeller Rust: Incluye la biblioteca de Impeller para usarla en Rust (los crates impeller y impeller-rs-bindgen).

  • TypographyContext: Administra el registro de fuentes y la forma del texto.

impeller-video

3.1 Inicialización y administración de superficies

  • Creación de contexto: El renderizador inicializa una instancia de impeller::Context con un backend de OpenGL ES, y pasa una devolución de llamada para resolver los punteros de función de OpenGL ES desde el contexto GL de la plataforma.

  • Superficie FBO envuelta: En lugar de crear su propia ventana, Impeller renderiza en un objeto de búfer de fotogramas de OpenGL (FBO) existente que proporciona la fase 4. Para ello, se llama a Surface::create_wrapped_fbo.

3.2 Administración de recursos

  • Imágenes: Admite formatos estándar y texturas comprimidas KTX2. Estos se suben a las texturas de la GPU y se administran con una estructura Resources interna.

  • Fuentes: Las fuentes TrueType y OpenType se cargan y registran con TypographyContext para la renderización de texto.

  • Imágenes externas: El manejo especializado de texturas externas (por ejemplo, feeds de cámaras y renderizadores 3D externos) implica vincular instancias EGLImage o texturas OpenGL externas a objetos Texture de Impeller para la renderización sin copia.

3.3 Paso de renderización

El bucle render construye una instancia DisplayList de Impeller (que no debe confundirse con el Vec<DisplayListEntry> que genera la fase de prerrenderización) con DisplayListBuilder:

  1. Borra el búfer y aplica transformaciones globales para el ajuste de PPP y la rotación de la pantalla.

  2. Itera a través de los elementos DisplayListEntry de entrada:

    • Estado: Se usan save() y restore() para insertar y extraer transformaciones y regiones de recorte.
    • Primitivas: Rect y RoundedRect se dibujan con operaciones de pintura estándar.
    • Rutas: Se compilan y dibujan rutas de vectores complejas (incluidas las instancias Arc dinámicas).
    • Texto: Text y StyledText se renderizan con TypographyContext.
    • Imágenes: Se dibujan imágenes estándar y externas con draw_texture_rect.
  3. Envía la lista de visualización de Impeller compilada a la superficie con surface.draw_display_list(), lo que genera los comandos GL subyacentes.

  4. Llama a swap_buffers() en el contexto subyacente para activar la fase 4.

Fase 4: Presentación

En esta fase final, se controla la interacción con el hardware de visualización para mostrar el fotograma renderizado. HAR usa una ruta de renderización directa sólida en el vehículo definido por software (SDV) de Android Automotive OS (AAOS).

El componente clave de esta fase es HarDirectRenderingContext (en el crate har-gl-context).

4.1 Arquitectura

La capa de presentación usa un enfoque de doble búfer con un destino de dibujo fuera de la pantalla:

  1. Búfer de dibujo: FBO fuera de la pantalla donde Impeller renderiza la escena.

  2. Búfer de resolución (opcional): Búfer auxiliar opcional para admitir el suavizado de muestras múltiples (MSAA)

    • Se puede habilitar cuando la implementación o configuración subyacente de OpenGL ES lo necesite. En esos casos, sirve como un destino intermedio para resolver el búfer de dibujo de muestras múltiples antes de la transferencia de bloques de bits (blitting) al búfer de renderización.
  3. Búfer de renderización: Búfer genérico respaldado por un objeto GBM, que corresponde al búfer posterior en una cadena de intercambio de gráficos típica.

  4. Búfer frontal: Búfer GBM que se analiza en la pantalla.

4.2 Cadena de intercambio

Cuando se llama a swap_buffers, HAR sigue estos pasos:

  1. Transfiere el contenido del búfer de dibujo al búfer de renderización (con una transferencia intermedia al búfer de resolución, si la implementación lo necesita).

  2. Llama a glFlush() en el contexto GL y crea una instancia de EGL_SYNC_NATIVE_FENCE_ANDROID para hacer un seguimiento de la finalización de la GPU.

  3. Compila una solicitud atómica de DRM para intercambiar el búfer de renderización a la pantalla. Esta solicitud contiene el FD de la barrera de la GPU (llamada barrera de entrada) para evitar que el controlador de pantalla muestre el búfer de renderización antes de que la GPU termine de dibujar.

  4. Solicita simultáneamente una barrera nueva de DRM (llamada barrera de salida) para indicar cuándo el búfer anterior (el búfer frontal del fotograma anterior) ya no está en la pantalla.

  5. Confirma la solicitud atómica con la marca sin bloqueo para permitir que el subproceso principal continúe mientras los subsistemas de gráficos permanecen sincronizados.

  6. Almacena la nueva barrera de salida en el contexto para que HAR pueda esperar a que se indique al comienzo del proceso swap_buffers en el fotograma posterior. Esto evita que la GPU dibuje en un búfer que aún se muestra.

4.3 Configuración del modo directo

HAR interactúa directamente con el kernel mediante los subsistemas DRM y Kernel Mode Setting (KMS) para configurar la resolución de pantalla del SDV de AAOS, omitiendo las interacciones con administradores de ventanas como SurfaceFlinger (en configuraciones específicas), lo que permite un control exclusivo y de alta prioridad del hardware de visualización.

4.4 Renderización externa

HAR admite la delegación de la renderización de elementos de IU específicos (identificados por etiquetas en Figma) a procesos o subprocesos externos. Esto es útil para integrar escenas 3D complejas (por ejemplo, una visualización de un auto ego de motores como Kanzi o Unity) o cualquier otro contenido que requiera un contexto de OpenGL dedicado.

4.4.1 Componentes clave

  • HarExternalRenderContext: Un contexto EGL dedicado fuera de la pantalla para el servicio externo.
  • SurfacePool: Administra un conjunto de búferes LocalSurface (Texture más EGLImage) para el almacenamiento en búfer doble o triple.
  • SharedSurfaceExternalImage: Un wrapper seguro para subprocesos para pasar controladores EGLImage entre el servicio externo y el renderizador principal.

4.4.2 Flujo de trabajo

El flujo de trabajo sigue esta secuencia:

  1. Se inicia el servicio externo y se registra con el looper principal, y se identifica qué etiquetas de Figma (por ejemplo, #cluster/3d-car) renderiza.

  2. El servicio espera las señales RenderStart del looper para alinear su renderización con la señal VSYNC de la pantalla.

  3. Fuera de la pantalla, el servicio renderiza su contenido en un búfer de fotogramas que proporciona SurfacePool.

  4. El servicio llama a swap_buffers en su contexto, que rota el grupo y hace que el fotograma completado esté disponible como una instancia de SharedSurface.

  5. SharedSurface se incluye en ExternalImage y se envía a través de un canal MPSC de Rust al looper.

  6. El renderizador principal de Impeller (fase 3) recibe la imagen externa. En lugar de copiar datos de píxeles, vincula el EGLImage subyacente directamente a una textura y la dibuja como parte de la escena principal, lo que logra una composición sin copia.

4.5 Plataformas de desarrollo y pruebas (har-platform-linux)

Para fines de desarrollo y pruebas, las apps de HAR pueden orientarse a entornos de escritorio Linux estándar y configuraciones sin interfaz gráfica. Estas plataformas se implementan en el crate crates/reference/platforms/har-platform-linux.

A diferencia del destino de producción de SDV de AAOS, estas plataformas no usan el subsistema direct-rendering de har-gl-context para la salida de la pantalla. En su lugar, dependen de los crates estándar de Rust OpenGL:

  • Modo de ventana: Usa winit para la administración de ventanas y los bucles de eventos, y glutin para crear contextos de OpenGL ES y la integración con el sistema de ventanas.

  • Modo sin interfaz gráfica: Usa el crate har-gl-context para crear un contexto de pbuffer fuera de la pantalla con la pantalla EGL predeterminada. Esto permite la renderización en un búfer fuera de la pantalla sin necesidad de una ventana visible ni acceso directo al hardware de visualización, que se usa principalmente para pruebas automatizadas o procesamiento de backend.