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:
- Prerrenderización: Procesamiento del gráfico de escena, aplicación de personalizaciones y resolución del diseño
- Generación de comandos: Conversión del gráfico de escena resuelto en una lista de visualización independiente del backend
- Renderización: Ejecución de comandos de dibujo con el motor de gráficos Impeller
- Presentación: Administración de búferes de fotogramas y sincronización con el hardware de visualización
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
Presenterrasgo es especificado por elharryframework crate, y se proporciona una implementación de referencia (UIModelPresenter) en elharry-app-corecrate. - Modelo de IU: Genera personalizaciones según el estado actual. El código del modelo de IU se genera con la macro
DesignDocumentque proporciona el cratederive_customizations. La estructuraUIModelen el crateharry-app-coreproporciona un ejemplo de esto. - Squoosh: Proporciona la estructura de datos
SquooshViewy el repositorio de variantes, que se usan para renderizar la IU según el diseño. El cratedc_bundlecarga un documento de diseño serializado desde la biblioteca de DesignCompose y lo convierte en un árbol de estructurasSquooshViewpara 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:
Reducerrecibe 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:
Presenterasigna el estado nuevo aUIModel.UIModeles 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
applydel modelo de IU para generar un conjunto de instanciasRenderCustomization. Estas son instrucciones explícitas para modificar el diseño de Figma (por ejemplo, "Establecer el texto del nodo #speed en '65 mph'"). UpdatePolicypara la optimización: Después de cada paso de prerrenderización, se muestra un valorUpdatePolicy, 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,UpdatePolicyindica 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
DesignComposeDefinitionen un árbolSquooshViewinicial. Este es un proceso que solo deberá realizar una vez.Repositorio:
SquooshVariantRepositoryadministra 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
SquooshViewque 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:
DynamicLayoutcalcula la posición y el tamaño finales (límites) de cada nodo en el árbolSquooshView.Diseño de texto:
TextHelperusa una implementación del rasgoLayoutHelperpara 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únmeter_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
SquooshViewactual se compara conprevious_squoosh_viewdePreRenderCache.Interpolación: Si las propiedades cambiaron,
Squooshcrea 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 árbolSquooshView.Traducción:
- Formas y rutas: Se convierten en
DisplayListEntrycon la varianteDisplayListAppearanceadecuada (por ejemplo,RectoPath). - Texto: Se convierte con
TextHelperen entradas de dibujo de texto. - Transformaciones y clips: Se convierten en pares
PushTransform3DyPopTransform3DoPushClipRegionyPopClipRegionpara administrar la pila de estado de dibujo. - Enmascaramiento: Se convierte en pares
PushMaskLayeryPopMaskLayerpara crear y combinar capas correctamente.
- Formas y rutas: Se convierten en
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
impelleryimpeller-rs-bindgen).TypographyContext: Administra el registro de fuentes y la forma del texto.
3.1 Inicialización y administración de superficies
Creación de contexto: El renderizador inicializa una instancia de
impeller::Contextcon 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
Resourcesinterna.Fuentes: Las fuentes TrueType y OpenType se cargan y registran con
TypographyContextpara 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
EGLImageo texturas OpenGL externas a objetosTexturede 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:
Borra el búfer y aplica transformaciones globales para el ajuste de PPP y la rotación de la pantalla.
Itera a través de los elementos
DisplayListEntryde entrada:- Estado: Se usan
save()yrestore()para insertar y extraer transformaciones y regiones de recorte. - Primitivas:
RectyRoundedRectse dibujan con operaciones de pintura estándar. - Rutas: Se compilan y dibujan rutas de vectores complejas (incluidas las instancias
Arcdinámicas). - Texto:
TextyStyledTextse renderizan conTypographyContext. - Imágenes: Se dibujan imágenes estándar y externas con
draw_texture_rect.
- Estado: Se usan
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.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:
Búfer de dibujo: FBO fuera de la pantalla donde Impeller renderiza la escena.
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.
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.
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:
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).
Llama a
glFlush()en el contexto GL y crea una instancia deEGL_SYNC_NATIVE_FENCE_ANDROIDpara hacer un seguimiento de la finalización de la GPU.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.
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.
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.
Almacena la nueva barrera de salida en el contexto para que HAR pueda esperar a que se indique al comienzo del proceso
swap_buffersen 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úferesLocalSurface(TexturemásEGLImage) para el almacenamiento en búfer doble o triple.SharedSurfaceExternalImage: Un wrapper seguro para subprocesos para pasar controladoresEGLImageentre el servicio externo y el renderizador principal.
4.4.2 Flujo de trabajo
El flujo de trabajo sigue esta secuencia:
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.El servicio espera las señales
RenderStartdel looper para alinear su renderización con la señal VSYNC de la pantalla.Fuera de la pantalla, el servicio renderiza su contenido en un búfer de fotogramas que proporciona
SurfacePool.El servicio llama a
swap_buffersen su contexto, que rota el grupo y hace que el fotograma completado esté disponible como una instancia deSharedSurface.SharedSurfacese incluye enExternalImagey se envía a través de un canal MPSC de Rust al looper.El renderizador principal de Impeller (fase 3) recibe la imagen externa. En lugar de copiar datos de píxeles, vincula el
EGLImagesubyacente 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
winitpara la administración de ventanas y los bucles de eventos, yglutinpara crear contextos de OpenGL ES y la integración con el sistema de ventanas.Modo sin interfaz gráfica: Usa el crate
har-gl-contextpara 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.