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 gráficos de bajo nivel y los presenta de manera eficiente en las pantallas de hardware. La canalización está diseñada para apps automotrices críticas para la seguridad, y enfatiza la renderización determinística, la administración eficiente del estado y la interacción sólida con los subsistemas gráficos de la plataforma, como Direct Rendering Manager (DRM) y Generic Buffer Management (GBM).

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

  1. Renderización previa: Procesa el grafo de la escena, aplica personalizaciones y resuelve el diseño.
  2. Generación de comandos: Convierte el 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 pantalla.

Flujo de gráficos de HAR

Figura 1: Flujo de gráficos HAR.

Fase 1: Renderización previa

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 en memoria completamente resuelto y listo para la renderización. Esta fase se ejecuta en un subproceso de reductor dedicado, independiente 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ñó en Figma y se exportó con el complemento de 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 struct 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, procesando acciones y actualizando 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 con el modelo de IU. El crate del framework harry especifica el rasgo Presenter, y el crate harry-app-core proporciona una implementación de referencia (UIModelPresenter).
  • Modelo de IU: Genera personalizaciones según el estado actual. El código del modelo de IU se genera con la macro DesignDocument proporcionada por el crate derive_customizations. El struct 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 structs SquooshView para lograr un rendimiento de tiempo de ejecución eficiente.

1.2 Bucle del reductor

La canalización se basa en 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, generan efectos secundarios que luego se pasan de vuelta a la app desde el reductor para su procesamiento. El enum 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 la acción: Reducer recibe una acción y actualiza el estado actual. Son los datos sin procesar, como la velocidad actual o qué luces de advertencia están activas. Esto también puede generar efectos secundarios (por ejemplo, un indicador puede reproducir un sonido cuando parpadea la luz del cinturón de seguridad).
  • Presentación: Presenter asigna el nuevo estado a UIModel. UIModel es un modelo de vista que contiene datos con un formato específico para la IU (por ejemplo, el formato de 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 de RenderCustomization. Son instrucciones explícitas para modificar el diseño de Figma (por ejemplo, "Establece el texto del nodo #speed en "65 mph"").
  • UpdatePolicy para la optimización: Después de cada paso de procesamiento previo, se devuelve un valor de UpdatePolicy, que indica cuándo se requiere la próxima actualización de la renderización. Si no hay cambios de estado pendientes ni animaciones en ejecución, UpdatePolicy indica que no se necesitan más actualizaciones de inmediato. En estos casos, el reductor deja de generar nuevas listas de visualización, lo que evita ciclos de renderización innecesarios y conserva recursos hasta que una nueva acción o evento desencadena un cambio.

1.3 Visualiza la inicialización del repositorio y la transferencia

La canalización comienza con una instancia de 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ás 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 referencia y las necesita de forma explícita (por ejemplo, durante una personalización de la lista).

1.4 Pase 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 económico) según la lógica de 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 nuevos IDs únicos 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 variable

Se resuelven las variables y los tokens de diseño definidos 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 (límites) finales 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 Controles y medidores

Este es un paso especializado para las IU automotrices.

  • MeterData: Si un nodo tiene datos de medición (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 inicial y final.
    • Barras de progreso: Se ajusta el ancho o el alto de un rectángulo.
    • Vectores de progreso: Se ajusta la longitud de una ruta de vector.

1.8 Animación

  • Comparación de diferencias: El SquooshView actual se compara con el 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, opacidad o transformación) a lo largo del tiempo.

Fase 2: Generación de comandos

Después de 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 estados de dibujo.
    • Enmascaramiento: Se convirtió en pares de 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 Transferencia al looper

Después de generar el DisplayList, el reductor lo encapsula en una instancia de ViewDescriptor y lo envía a través de un canal MPSC de Rust (LooperMessage) al subproceso de bucle. Looper es responsable de las fases de procesamiento 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 para la GPU.

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

  • Rendimiento determinístico: Elimina prácticamente los errores de compilación del sombreador en 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 detallada a la arquitectura de Impeller, mira Introducing Impeller - Flutter's new rendering engine (Presentamos Impeller: el nuevo motor de renderización de Flutter). Si bien el video analiza Flutter, estos beneficios principales potencian directamente la pila automotriz de HAR.

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

  • ImpellerRenderer: Convierte la lista de visualización de la fase de procesamiento previo en comandos de procesamiento de Impeller.

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

  • TypographyContext: Administra el registro de fuentes y la composición de texto.

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 de GL de la plataforma.

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

3.2 Administración de recursos

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

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

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

3.3 Pase de renderización

El bucle render construye una instancia de DisplayList de Impeller (no confundir con el Vec<DisplayListEntry> generado por la fase de renderización previa) con DisplayListBuilder:

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

  2. Itera los elementos de entrada DisplayListEntry:

    • Estado: save() y restore() se usan 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: Las imágenes estándar y externas se dibujan 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 de GL subyacentes.

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

Fase 4: Presentación

Esta fase final controla la interacción con el hardware de la pantalla para mostrar el fotograma renderizado. HAR usa una ruta de renderización directa y sólida en el vehículo definido por software (SDV) del SO Android Automotive (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 pantalla:

  1. Búfer de dibujo: FBO fuera de pantalla en el que Impeller renderiza la escena.

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

    • Se puede habilitar cuando la implementación o configuración subyacente de OpenGL ES lo requieran. En esos casos, sirve como un destino intermedio para resolver el búfer de dibujo con múltiples muestras antes de realizar la transferencia de bloques de bits 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 de fondo en una cadena de intercambio de gráficos típica.

  4. Búfer frontal: Es el búfer de GBM que se explora y se envía a 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 requiere).

  2. Llama a glFlush() en el contexto de 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 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 nueva barrera del 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 de bloqueo desactivado para permitir que el subproceso principal continúe mientras los subsistemas gráficos permanecen sincronizados.

  6. Almacena la nueva valla externa en el contexto para que HAR pueda esperar a que se señale al comienzo del proceso swap_buffers en el fotograma siguiente. Esto evita que la GPU dibuje en un búfer que aún se está mostrando.

4.3 Configuración del modo directo

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

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 del automóvil del usuario desde motores como Kanzi o Unity) o cualquier otro contenido que requiera un contexto de OpenGL dedicado.

4.4.1 Componentes clave

  • HarExternalRenderContext: Es un contexto EGL dedicado fuera de 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: Es un wrapper seguro para subprocesos que se usa para pasar identificadores de EGLImage entre el servicio externo y el renderizador principal.

4.4.2 Flujo de trabajo

El flujo de trabajo sigue esta secuencia:

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

  2. El servicio espera las señales RenderStart del bucle 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 proporcionado por SurfacePool.

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

  5. SharedSurface se une a 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 lo dibuja como parte de la escena principal, lo que permite lograr una composición sin copias.

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 de Linux estándar y configuraciones sin encabezado. Estas plataformas se implementan en el crate crates/reference/platforms/har-platform-linux.

A diferencia del objetivo de SDV de AAOS de producción, estas plataformas no usan el subsistema direct-rendering de har-gl-context para la salida de la pantalla. En cambio, se basan en crates estándar de OpenGL de Rust:

  • 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 realizar la integración con el sistema de ventanas.

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