Pipeline de gráficos HAR

Esta página detalha o pipeline gráfico completo do renderizador de alta disponibilidade (HAR, na sigla em inglês), rastreando o fluxo de dados de um documento de design do Figma até os pixels finais exibidos na tela.

Visão geral

O pipeline converte definições de interface de alto nível em comandos gráficos de baixo nível e os apresenta com eficiência em telas de hardware. Ele foi projetado para apps automotivos de segurança crítica, enfatizando a renderização determinística, o gerenciamento eficiente de estados e a interação robusta com subsistemas gráficos de plataforma, como o Direct Rendering Manager (DRM) e o Generic Buffer Management (GBM).

O pipeline pode ser dividido em quatro fases principais:

  1. Pré-renderização:processamento do gráfico de cena, aplicação de personalizações e resolução do layout.
  2. Geração de comandos:conversão do gráfico de cena resolvido em uma lista de exibição independente do back-end.
  3. Renderização:execução de comandos de desenho usando o mecanismo gráfico Impeller.
  4. Apresentação:gerenciamento de framebuffers e sincronização com o hardware de exibição.

Fluxo de gráficos HAR

Figura 1. Fluxo de gráficos do HAR.

Fase 1: pré-renderização

Essa fase transforma o design estático do Figma e o estado dinâmico do app em uma árvore de interface totalmente resolvida e na memória, pronta para renderização. Essa fase é executada em uma linha de execução de redutor dedicada, separada do loop de exibição principal.

1.1 Base do DesignCompose

O pipeline do HAR é criado com base no ecossistema do DesignCompose.

  • Origem:a interface é projetada no Figma e exportada usando o plug-in DesignCompose.
  • Definição:a saída é uma instância de DesignComposeDefinition, uma representação serializada do design (nós, estilos, variantes).
  • Vinculação de dados: o modelo de interface do app usa macros procedurais (por exemplo, #[Design(node = "#speed")]) para vincular explicitamente campos de struct Rust a nós nomeados específicos no documento do Figma. Isso permite que o estado do app conduza automaticamente as propriedades dos elementos visuais.

Os principais componentes dessa base são:

  • Redutor:atua como o loop de eventos central, processando ações e atualizando o estado atual. O framework fornece DefaultReducer, mas uma implementação de redutor personalizada pode ser fornecida, se necessário.
  • Apresentador:conecta o estado atual ao modelo de interface. O Presenter trait é especificado pelo harry crate de framework, e uma implementação de referência (UIModelPresenter) é fornecida no harry-app-core crate.
  • Modelo de interface:gera personalizações com base no estado atual. O código do modelo de interface é gerado usando a macro DesignDocument fornecida pelo crate derive_customizations. A struct UIModel no crate harry-app-core fornece um exemplo disso.
  • Squoosh:fornece a estrutura de dados SquooshView e o repositório de variantes, usados para renderizar a interface de acordo com o design. Um documento de design serializado é carregado pelo crate dc_bundle da biblioteca DesignCompose e convertido em uma árvore de structs SquooshView para uma performance eficiente do ambiente de execução.

1.2 Loop do redutor

O pipeline é controlado por ações. O framework especifica o tipo enumerado Actions, que define ações internas usadas pelo próprio framework, mas também inclui uma variante CustomAction que permite aos usuários definir outras ações específicas do app (por exemplo, UpdateVehicleSpeed ou ButtonPress).

O framework também fornece o trait StateAction, que simplifica a implementação de ações que afetam o estado do app e, opcionalmente, gera efeitos colaterais que são transmitidos de volta ao app do redutor para processamento. A enumeração CustomActions no crate harry-app-core fornece um exemplo detalhado disso.

Este é um esboço básico do loop do redutor:

  • Processamento de ações:Reducer recebe uma ação e atualiza o estado atual. Esses são os dados brutos, como a velocidade atual ou quais indicadores (luzes de advertência) estão ativos. Isso também pode gerar efeitos colaterais (por exemplo, um sinal toca um som quando a luz do cinto de segurança pisca).
  • Apresentação:Presenter mapeia o novo estado para UIModel. UIModel é um modelo de visualização que contém dados formatados especificamente para a interface (por exemplo, formatar a velocidade "120" para uma string "65 mph").
  • Geração de personalização:o método apply do modelo de interface é chamado para gerar um conjunto de instâncias RenderCustomization. Essas são instruções explícitas para modificar o design do Figma (por exemplo, "Definir o texto do nó #speed como '65 mph'").
  • UpdatePolicy para otimização:após cada passagem de pré-renderização, um valor UpdatePolicy é retornado, indicando quando a próxima atualização de renderização é necessária. Se não houver mudanças de estado pendentes e nenhuma animação em execução, UpdatePolicy vai sinalizar que não são necessárias mais atualizações imediatamente. Nesses casos, o redutor deixa de gerar novas listas de exibição, evitando ciclos de renderização desnecessários e conservando recursos até que uma nova ação ou evento acione uma mudança.

1.3 Ingestão de visualização e inicialização do repositório

O pipeline começa com uma instância DesignComposeDefinition. Esse é o documento de design do Figma serializado pelo DesignCompose em uma estrutura de buffer de protocolo.

  • Carregamento inicial:na inicialização, o design principal (especificado pelo nó raiz) é convertido de DesignComposeDefinition em uma árvore SquooshView inicial. Esse processo ocorre só uma vez.

  • Repositório:SquooshVariantRepository gerencia variantes de componentes reutilizáveis e as visualizações carregadas inicialmente.

  • Carregamento lento:para minimizar o tempo de inicialização e o uso da memória, outras visualizações (que não fazem parte da árvore de nós raiz inicial) são carregadas lentamente do documento somente quando são referenciadas explicitamente e necessárias pela lógica de renderização (por exemplo, durante uma personalização de lista).

1.4 Passagem de personalização

A árvore SquooshView é percorrida para aplicar o estado dinâmico do app:

  • Trocas de variantes:as instâncias de componentes são trocadas por variantes específicas (por exemplo, mudar um ícone que representa o modo de direção atual de esportivo para econômico) com base na lógica de execução.

  • Expansão da lista:um único item de modelo no Figma é substituído por uma lista dinâmica de filhos. Novos IDs exclusivos são gerados para esses filhos para verificar uma identidade estável para animações.

  • Substituições de texto e estilo:o conteúdo de texto (por exemplo, o valor da velocidade) e os estilos (por exemplo, opacidade, cor) são atualizados do estado atual.

1.5 Resolução variável

Os tokens e variáveis de design definidos no Figma ou localmente no app são resolvidos.

  • Vinculação:as propriedades SquooshView que referenciam variáveis (como cores ou dimensões) são substituídas pelos valores concretos do frame atual.

1.6 Cálculo do layout

  • Layout dinâmico:DynamicLayout calcula a posição e o tamanho finais (limites) de cada nó na árvore SquooshView.

  • Layout de texto:TextHelper usa uma implementação do trait LayoutHelper para calcular métricas de texto, quebra de linha e formatação. Isso ajuda a verificar se o texto flui corretamente dentro das restrições antes da renderização.

1.7 Indicadores e medidores

Esta é uma etapa especializada para interfaces automotivas.

  • MeterData: se um nó tiver dados de medidor (definidos no Figma), a geometria dele será alterada dinamicamente com base em meter_value (por exemplo, velocidade do veículo).
    • Arcos:o ângulo de varredura é ajustado.
    • Rotações:a transformação de rotação é calculada com base nos ângulos inicial e final.
    • Barras de progresso:a largura ou altura de um retângulo é dimensionada.
    • Vetores de progresso:o comprimento de um caminho vetorial é ajustado.

1.8 Animação

  • Diferenciação:o SquooshView atual é comparado com previous_squoosh_view de PreRenderCache.

  • Interpolação:se as propriedades tiverem mudado, Squoosh vai criar interpoladores para fazer a transição suave dos valores (por exemplo, opacidade ou transformação) ao longo do tempo.

Fase 2: geração de comandos

Depois que a árvore SquooshView é totalmente resolvida e animada, ela é convertida em uma sequência linear de comandos de desenho.

O componente principal dessa fase é o crate DisplayList:

  • generate_dl: essa função percorre recursivamente a árvore SquooshView.

  • Tradução:

    • Formas e caminhos:convertidos em DisplayListEntry com a variante DisplayListAppearance apropriada (por exemplo, Rect ou Path)
    • Texto:convertido com TextHelper em entradas de desenho de texto.
    • Transformações e clipes:convertidos em pares PushTransform3D e PopTransform3D ou PushClipRegion e PopClipRegion para gerenciar a pilha de estados de desenho.
    • Máscara:convertida em pares PushMaskLayer e PopMaskLayer para criar e mesclar camadas corretamente.

O resultado final é uma instância de Vec<DisplayListEntry> que descreve o que desenhar, independente de como desenhar.

2.1 Handoff para o looper

Depois que a DisplayList é gerada, o redutor a envolve em uma instância de ViewDescriptor e a envia por um canal MPSC do Rust (LooperMessage) para a linha de execução do looper. O Looper é responsável pelas fases de renderização e exibição, o que impede que a linha de execução do redutor bloqueie o pipeline gráfico.

Fase 3: renderização

A DisplayList independente da plataforma é entregue ao back-end de renderização, em que comandos abstratos são traduzidos em instruções de GPU.

O HAR usa o Impeller, um mecanismo de renderização originalmente criado para o Flutter. O Impeller foi projetado para resolver o problema de falhas na taxa de frames devido à compilação de shaders, pré-compilando um conjunto pequeno e eficiente de shaders no tempo de build. Essa abordagem, combinada com o loteamento eficaz e um back-end altamente otimizado, oferece:

  • Performance determinística:elimina praticamente as falhas de compilação de sombreadores no ambiente de execução.
  • Inicialização rápida:reduz o overhead de inicialização.
  • Pegada pequena:produz um tamanho binário compacto.

Para uma introdução completa à arquitetura do Impeller, assista a [Apresentação do Impeller: o novo mecanismo de renderização do Flutter][impeller-video] (link em inglês). Embora o vídeo discuta o Flutter, esses benefícios principais capacitam diretamente a pilha automotiva do HAR.

Os principais componentes da fase de renderização são:

  • ImpellerRenderer: converte a lista de exibição da fase de pré-renderização em comandos de renderização do Impeller.

  • API Impeller Rust: envolve a biblioteca Impeller para uso no Rust (os crates impeller e impeller-rs-bindgen).

  • TypographyContext: gerencia o registro de fontes e a formatação de texto.

impeller-video

3.1 Inicialização e gerenciamento de superfícies

  • Criação de contexto:o renderizador inicializa uma instância de impeller::Context com um back-end OpenGL ES, transmitindo um callback para resolver ponteiros de função OpenGL ES do contexto GL da plataforma.

  • Superfície FBO encapsulada:em vez de criar a própria janela, o Impeller renderiza em um objeto de framebuffer OpenGL (FBO) fornecido pela Fase 4. Isso é feito chamando Surface::create_wrapped_fbo.

3.2 Gerenciamento de recursos

  • Imagens:oferece suporte a formatos padrão e texturas compactadas KTX2. Elas são enviadas para texturas de GPU e gerenciadas por uma struct Resources interna.

  • Fontes:as fontes TrueType e OpenType são carregadas e registradas no TypographyContext para renderização de texto.

  • Imagens externas:o tratamento especializado de texturas externas (por exemplo, feeds de câmera e renderizadores 3D externos) envolve a vinculação de instâncias EGLImage ou texturas OpenGL externas a objetos Texture do Impeller para renderização sem cópia.

3.3 Passagem de renderização

O loop render constrói uma instância DisplayList do Impeller (não confundir com o Vec<DisplayListEntry> gerado pela fase de pré-renderização) usando DisplayListBuilder:

  1. Limpa o buffer e aplica transformações globais para escalonamento de DPI e rotação de exibição.

  2. Itera pelos itens DisplayListEntry de entrada:

    • Estado:save() e restore() são usados para enviar e remover transformações e regiões de clipe.
    • Primitivos: Rect e RoundedRect são desenhados usando operações de pintura padrão.
    • Caminhos:caminhos vetoriais complexos (incluindo instâncias Arc dinâmicas) são criados e desenhados.
    • Texto: Text e StyledText são renderizados usando TypographyContext.
    • Imagens:imagens padrão e externas são desenhadas usando draw_texture_rect.
  3. Envia a lista de exibição do Impeller criada para a superfície usando surface.draw_display_list(), gerando os comandos GL subjacentes.

  4. Chama swap_buffers() no contexto subjacente para acionar a Fase 4.

Fase 4: apresentação

Essa fase final processa a interação com o hardware de exibição para mostrar o frame renderizado. O HAR usa um caminho de renderização direta robusto no veículo definido por software (SDV, na sigla em inglês) do Android Automotive OS (AAOS).

O componente principal dessa fase é HarDirectRenderingContext (no crate har-gl-context).

4.1 Arquitetura

A camada de apresentação usa uma abordagem de buffer duplo com um destino de desenho fora da tela:

  1. Buffer de desenho:FBO fora da tela em que o Impeller renderiza a cena.

  2. Buffer de resolução (opcional) : buffer auxiliar opcional para oferecer suporte ao antisserrilhamento multiamostra (MSAA, na sigla em inglês)

    • Isso pode ser ativado quando necessário pela implementação ou configuração do OpenGL ES subjacente. Nesses casos, ele serve como um destino intermediário para resolver o buffer de desenho multiamostra antes da transferência de blocos de bits (blitting) para o buffer de renderização.
  3. Buffer de renderização:buffer genérico com suporte de um objeto GBM, que corresponde ao buffer traseiro em uma cadeia de troca de gráficos típica.

  4. Buffer frontal:buffer GBM que é verificado na tela.

4.2 Cadeia de troca

Quando swap_buffers é chamado, o HAR segue estas etapas:

  1. Transfere o conteúdo do buffer de desenho para o buffer de renderização (com uma transferência intermediária para o buffer de resolução, se necessário para a implementação).

  2. Chama glFlush() no contexto GL e cria uma instância de EGL_SYNC_NATIVE_FENCE_ANDROID para rastrear a conclusão da GPU.

  3. Cria uma solicitação atômica de DRM para trocar o buffer de renderização pela tela. Essa solicitação contém o FD de barreira da GPU (chamado de barreira de entrada) para impedir que o controlador de exibição mostre o buffer de renderização antes que a GPU termine de desenhar.

  4. Solicita simultaneamente uma nova barreira do DRM (chamada de barreira de saída) para sinalizar quando o buffer anterior (o buffer frontal do frame anterior) não estiver mais na tela.

  5. Confirma a solicitação atômica usando a flag de não bloqueio para permitir que a linha de execução principal continue enquanto os subsistemas gráficos permanecem sincronizados.

  6. Armazena a nova barreira de saída no contexto para que o HAR possa esperar que ela seja sinalizada no início do processo swap_buffers no frame subsequente. Isso impede que a GPU desenhe em um buffer que ainda está sendo exibido.

4.3 Configuração do modo direto

O HAR interage diretamente com o kernel usando os subsistemas DRM e Kernel Mode Setting (KMS) para configurar a resolução de exibição do SDV do AAOS, ignorando interações com gerenciadores de janelas como o SurfaceFlinger (em configurações específicas), permitindo o controle exclusivo e de alta prioridade do hardware de exibição.

4.4 Renderização externa

O HAR oferece suporte à delegação da renderização de elementos de interface específicos (identificados por tags no Figma) para processos ou linhas de execução externos. Isso é útil para integrar cenas 3D complexas (por exemplo, uma visualização de carro ego de mecanismos como Kanzi ou Unity) ou outro conteúdo que exige um contexto OpenGL dedicado.

4.4.1 Principais componentes

  • HarExternalRenderContext: um contexto EGL dedicado fora da tela para o serviço externo.
  • SurfacePool: gerencia um conjunto de buffers LocalSurface (Texture mais EGLImage) para buffer duplo ou triplo.
  • SharedSurfaceExternalImage: um wrapper seguro para linhas de execução para transmitir identificadores EGLImage entre o serviço externo e o renderizador principal.

4.4.2 Fluxo de trabalho

O fluxo de trabalho segue esta sequência:

  1. O serviço externo é iniciado e registrado no looper principal, identificando quais tags do Figma (por exemplo, #cluster/3d-car) ele renderiza.

  2. O serviço aguarda os sinais RenderStart do looper para alinhar a renderização com o sinal VSYNC da tela.

  3. Fora da tela, o serviço renderiza o conteúdo em um framebuffer fornecido por SurfacePool.

  4. O serviço chama swap_buffers no contexto, que gira o pool e disponibiliza o frame concluído como uma instância de SharedSurface.

  5. SharedSurface é encapsulado em ExternalImage e enviado por um canal MPSC do Rust para o looper.

  6. O renderizador principal do Impeller (Fase 3) recebe a imagem externa. Em vez de copiar dados de pixels, ele vincula o EGLImage subjacente diretamente a uma textura e a desenha como parte da cena principal, alcançando uma composição sem cópia.

4.5 Plataformas de desenvolvimento e teste (har-platform-linux)

Para fins de desenvolvimento e teste, os apps HAR podem segmentar ambientes de computadores Linux padrão e configurações sem tela. Essas plataformas são implementadas no crate crates/reference/platforms/har-platform-linux.

Ao contrário do destino de produção do SDV do AAOS, essas plataformas não usam o subsistema direct-rendering de har-gl-context para saída de exibição. Em vez disso, elas dependem de crates OpenGL padrão do Rust:

  • Modo de janela:usa winit para gerenciamento de janelas e loops de eventos, e glutin para criar contextos OpenGL ES e integrar ao sistema de janelas.

  • Modo sem tela:usa o crate har-gl-context para criar um contexto de pbuffer fora da tela com a tela EGL padrão. Isso permite a renderização em um buffer fora da tela sem precisar de uma janela visível ou acesso direto ao hardware de exibição, usado principalmente para testes automatizados ou processamento de back-end.