Potok graficzny HAR

Na tej stronie znajdziesz szczegółowy opis pełnego potoku graficznego renderera o wysokiej dostępności (HAR), który śledzi przepływ danych z dokumentu projektu w Figma do końcowych pikseli wyświetlanych na ekranie.

Przegląd

Potok przekształca definicje interfejsu wysokiego poziomu w polecenia graficzne niskiego poziomu i wydajnie wyświetla je na ekranach sprzętowych. Potok jest przeznaczony do aplikacji o krytycznym znaczeniu dla bezpieczeństwa w motoryzacji. Kładzie nacisk na deterministyczne renderowanie, wydajne zarządzanie stanem i solidną interakcję z podsystemami graficznymi platformy, takimi jak Direct Rendering Manager (DRM) i Generic Buffer Management (GBM).

Proces można podzielić na 4 główne etapy:

  1. Wstępne renderowanie: przetwarzanie grafu sceny, stosowanie dostosowań i rozwiązywanie układu.
  2. Generowanie poleceń: przekształcanie rozpoznanego wykresu sceny w niezależną od backendu listę wyświetlania.
  3. Renderowanie: wykonywanie poleceń rysowania za pomocą silnika graficznego Impeller.
  4. Prezentacja: zarządzanie buforami ramki i synchronizacja ze sprzętem wyświetlacza.

Przepływ grafiki HAR

Rysunek 1. Schemat graficzny HAR.

Faza 1. Wstępne renderowanie

W tej fazie statyczny projekt w Figma i dynamiczny stan aplikacji są przekształcane w w pełni rozwiązane drzewo interfejsu w pamięci, gotowe do renderowania. Ta faza jest wykonywana w dedykowanym wątku reduktora, oddzielnym od głównej pętli wyświetlania.

1.1 Podstawy DesignCompose

Potok HAR jest oparty na ekosystemie DesignCompose.

  • Źródło: interfejs został zaprojektowany w Figmie i wyeksportowany za pomocą wtyczki DesignCompose.
  • Definicja: dane wyjściowe to instancja DesignComposeDefinition, czyli zserializowana reprezentacja projektu (węzły, style, warianty).
  • Powiązanie danych: model interfejsu aplikacji używa makr proceduralnych (np. #[Design(node = "#speed")]), aby jawnie powiązać pola struktury Rust z określonymi nazwanymi węzłami w dokumencie Figma. Dzięki temu stan aplikacji automatycznie określa właściwości elementów wizualnych.

Kluczowe elementy tej podstawy to:

  • Reduktor: działa jako centralna pętla zdarzeń, która przetwarza działania i aktualizuje bieżący stan. Platforma udostępnia DefaultReducer, ale w razie potrzeby można podać niestandardową implementację funkcji redukującej.
  • Presenter:łączy bieżący stan z modelem interfejsu. Cechę Presenter określa skrzynka ramowa harry, a implementacja referencyjna (UIModelPresenter) jest dostępna w skrzynce harry-app-core.
  • Model interfejsu: generuje dostosowania na podstawie bieżącego stanu. Kod modelu interfejsu jest generowany przy użyciu makra DesignDocument dostarczonego przez pakiet derive_customizations. Przykładem tego jest struktura UIModelharry-app-core.
  • Squoosh: udostępnia strukturę danych SquooshView i repozytorium wariantów, które służą do renderowania interfejsu zgodnie z projektem. Zserializowany dokument projektu jest wczytywany przez pakiet dc_bundle z biblioteki DesignCompose i konwertowany na drzewo struktur SquooshView, co zapewnia wydajne działanie w czasie działania.

1.2 Pętla ograniczenia

Potok jest oparty na działaniach. Framework określa Actionstyp wyliczeniowy, który definiuje wewnętrzne działania używane przez sam framework, ale zawiera też wariant CustomAction, który umożliwia użytkownikom definiowanie dodatkowych działań specyficznych dla aplikacji (np. UpdateVehicleSpeed lub ButtonPress).

Framework udostępnia też cechę StateAction, która upraszcza wdrażanie działań wpływających na stan aplikacji i opcjonalnie generujących efekty uboczne, które są następnie przekazywane z funkcji redukującej z powrotem do aplikacji w celu przetworzenia. Szczegółowy przykład znajdziesz w wyliczeniu CustomActionsharry-app-core crate.

Oto podstawowy zarys pętli reduktora:

  • Przetwarzanie działania: Reducer otrzymuje działanie i aktualizuje bieżący stan. Są to surowe dane, takie jak aktualna prędkość lub aktywne kontrolki (lampki ostrzegawcze). Może to również generować efekty uboczne (np. sygnał dźwiękowy może odtwarzać dzwonek, gdy miga kontrolka pasów bezpieczeństwa).
  • Prezentacja: Presenter mapuje nowy stan na UIModel. UIModel to model widoku, który zawiera dane sformatowane specjalnie na potrzeby interfejsu (np. formatowanie prędkości „120” do ciągu znaków „65 mph”).
  • Generowanie dostosowań: wywoływana jest metoda apply modelu interfejsu, aby wygenerować zestaw instancji RenderCustomization. Są to szczegółowe instrukcje dotyczące modyfikowania projektu w Figmie (np. „Ustaw tekst węzła #speed na „65 mph””).
  • UpdatePolicy na potrzeby optymalizacji: po każdym etapie wstępnego renderowania zwracana jest wartość UpdatePolicy, która wskazuje, kiedy wymagana jest następna aktualizacja renderowania. Jeśli nie ma oczekujących zmian stanu ani uruchomionych animacji, UpdatePolicy oznacza, że nie są potrzebne żadne dalsze aktualizacje. W takich przypadkach funkcja Reducer przestaje generować nowe listy wyświetlania, co zapobiega niepotrzebnym cyklom renderowania i oszczędza zasoby, dopóki nowe działanie lub zdarzenie nie spowoduje zmiany.

1.3 Wyświetlanie pozyskiwania i inicjowania repozytorium

Potok zaczyna się od instancji DesignComposeDefinition. Jest to dokument projektu Figma serializowany przez DesignCompose do struktury bufora protokołu.

  • Początkowe wczytywanie: podczas uruchamiania główny projekt (określony przez węzeł główny) jest konwertowany z DesignComposeDefinition na początkowe drzewo SquooshView. Jest to proces jednorazowy.

  • Repozytorium: SquooshVariantRepository zarządza wariantami komponentów wielokrotnego użytku i początkowo wczytanymi widokami.

  • Leniwe ładowanie: Aby zminimalizować czas uruchamiania i wykorzystanie pamięci, dodatkowe widoki (które nie są częścią początkowego drzewa węzła głównego) są ładowane z dokumentu tylko wtedy, gdy są wyraźnie przywoływane i potrzebne w logice renderowania (np. podczas dostosowywania listy).

1.4 Karta personalizacji

Drzewo SquooshView jest przeszukiwane w celu zastosowania dynamicznego stanu aplikacji:

  • Zamiana wariantów: instancje komponentów są zamieniane na określone warianty (np. zmiana ikony reprezentującej bieżący tryb jazdy z sportowego na ekologiczny) na podstawie logiki czasu działania.

  • Rozwijanie listy: pojedynczy element szablonu w Figma jest zastępowany dynamiczną listą elementów podrzędnych. Dla tych dzieci generowane są nowe unikalne identyfikatory, aby zapewnić stabilną tożsamość animacji.

  • Zastąpienia tekstu i stylu: zawartość tekstowa (np. wartość szybkości) i style (np. krycie, kolor) są aktualizowane na podstawie bieżącego stanu.

1.5 Zmienna rozdzielczość

Tokeny projektu i zmienne zdefiniowane w Figma lub lokalnie w aplikacji są rozwiązywane.

  • Powiązanie: właściwości SquooshView odwołujące się do zmiennych (np. kolorów lub wymiarów) są zastępowane konkretnymi wartościami dla bieżącej klatki.

1.6 Obliczanie układu

  • Układ dynamiczny: DynamicLayout oblicza ostateczną pozycję i rozmiar (granice) każdego węzła w drzewie SquooshView.

  • Układ tekstu: TextHelper używa implementacji LayoutHelper cechy do obliczania danych tekstowych, zawijania i kształtowania tekstu. Pomaga to sprawdzić, czy tekst mieści się w ograniczeniach przed renderowaniem.

1.7 Tarcze i wskaźniki

Jest to specjalny krok w przypadku interfejsów samochodowych.

  • MeterData: jeśli węzeł zawiera dane z licznika (zdefiniowane w Figma), jego geometria jest dynamicznie zmieniana na podstawie meter_value (np. prędkości pojazdu).
    • Łuki: dostosowywany jest kąt wycinka.
    • Obrót: transformacja obrotu jest obliczana na podstawie kątów początkowego i końcowego.
    • Paski postępu: szerokość lub wysokość prostokąta jest skalowana.
    • Wektory postępu: długość ścieżki wektora jest dostosowywana.

1.8 Animacja

  • Porównywanie: bieżący okres SquooshView jest porównywany z okresem previous_squoosh_view z dnia PreRenderCache.

  • Interpolacja: jeśli właściwości uległy zmianie, Squoosh tworzy interpolatory, aby płynnie zmieniać wartości (np. nieprzezroczystość lub przekształcenie) w czasie.

Faza 2. Generowanie poleceń

Po SquooshView całkowitym rozwiązaniu i animowaniu drzewa jest ono przekształcane w liniową sekwencję poleceń rysowania.

Kluczowym elementem tego etapu jest pakiet DisplayList:

  • generate_dl: ta funkcja rekurencyjnie przechodzi przez drzewo SquooshView.

  • Tłumaczenie:

    • Kształty i ścieżki: przekształcane na DisplayListEntry z odpowiednim wariantem DisplayListAppearance (np. Rect lub Path).
    • Tekst: przekonwertowany za pomocą TextHelper na wpisy rysunkowe.
    • Przekształcenia i klipy: przekształcane w pary PushTransform3DPopTransform3D lub PushClipRegionPopClipRegion, aby zarządzać stosem stanów rysowania.
    • Maskowanie: przekształcone w pary PushMaskLayerPopMaskLayer, aby prawidłowo tworzyć i mieszać warstwy.

Wynikiem jest instancja Vec<DisplayListEntry>, która opisuje co narysować, niezależnie od tego, jak to zrobić.

2.1 Przekazywanie do pętli

Po wygenerowaniu DisplayList funkcja Reducer umieszcza go w instancji ViewDescriptor i wysyła przez kanał Rust MPSC (LooperMessage) do wątku pętli. Za fazy renderowania i wyświetlania odpowiada Looper, co zapobiega blokowaniu potoku graficznego przez wątek Reducer.

Faza 3. Renderowanie

Niezależny od platformy DisplayList jest przekazywany do backendu renderowania, gdzie abstrakcyjne polecenia są tłumaczone na instrukcje GPU.

HAR korzysta z Impellera, czyli silnika renderowania pierwotnie stworzonego na potrzeby Fluttera. Impeller został zaprojektowany, aby rozwiązać problem z zakłóceniami liczby klatek na sekundę spowodowanymi kompilacją shaderów. W tym celu wstępnie kompiluje mały, wydajny zestaw shaderów w czasie kompilacji. Takie podejście w połączeniu ze skutecznym przetwarzaniem wsadowym i wysoce zoptymalizowanym backendem zapewnia:

  • Deterministyczna wydajność: praktycznie eliminuje błędy kompilacji shaderów w czasie działania.
  • Szybkie uruchamianie: zmniejsza koszty inicjowania.
  • Mały rozmiar: generuje kompaktowy rozmiar pliku binarnego.

Szczegółowe wprowadzenie do architektury Impellera znajdziesz w tym [filmie][impeller-video]. Chociaż w filmie omawiana jest platforma Flutter, te podstawowe zalety bezpośrednio wzmacniają stos motoryzacyjny HAR.

Kluczowe komponenty fazy renderowania to:

  • ImpellerRenderer: przekształca listę wyświetlania z fazy wstępnego renderowania w polecenia renderowania Impeller.

  • Impeller Rust API: otacza bibliotekę Impeller, aby można było jej używać w języku Rust (pakiety impellerimpeller-rs-bindgen).

  • TypographyContext: zarządza rejestracją czcionek i kształtowaniem tekstu.

impeller-video

3.1 Inicjowanie i zarządzanie platformami

  • Tworzenie kontekstu: moduł renderujący inicjuje instancję impeller::Context z backendem OpenGL ES, przekazując wywołanie zwrotne w celu rozwiązania wskaźników funkcji OpenGL ES z kontekstu GL platformy.

  • Zawinięta powierzchnia FBO: zamiast tworzyć własne okno, Impeller renderuje do istniejącego obiektu bufora ramki OpenGL (FBO) dostarczonego przez fazę 4. W tym celu wywołaj funkcję Surface::create_wrapped_fbo.

3.2 Zarządzanie zasobami

  • Grafika: obsługuje standardowe formaty i skompresowane tekstury KTX2. Są one przesyłane do tekstur GPU i zarządzane przez wewnętrzną strukturę Resources.

  • Czcionki: czcionki TrueType i OpenType są wczytywane i rejestrowane w TypographyContext na potrzeby renderowania tekstu.

  • Obrazy zewnętrzne: specjalna obsługa tekstur zewnętrznych (np. strumieni z kamery i zewnętrznych rendererów 3D) polega na powiązaniu instancji EGLImage lub zewnętrznych tekstur OpenGL z obiektami Impeller Texture w celu renderowania bez kopiowania.

3.3 Renderowanie

Pętla render tworzy instancję DisplayList Impellera (nie mylić z instancją Vec<DisplayListEntry> wygenerowaną w fazie wstępnego renderowania) za pomocą DisplayListBuilder:

  1. Czyści bufor i stosuje globalne przekształcenia na potrzeby skalowania DPI i obracania ekranu.

  2. Iteruje po elementach wejściowych DisplayListEntry:

    • Stan: save()restore() służą do przesuwania i wycofywania przekształceń oraz regionów przycinania.
    • Elementy podstawowe: elementy RectRoundedRect są rysowane przy użyciu standardowych operacji malowania.
    • Ścieżki: złożone ścieżki wektorowe (w tym dynamiczne instancje Arc) są tworzone i rysowane.
    • Tekst: znaki TextStyledText są renderowane za pomocą TypographyContext.
    • Obrazy: standardowe i zewnętrzne obrazy są rysowane za pomocą elementu draw_texture_rect.
  3. Przesyła utworzoną listę wyświetlania Impeller do powierzchni za pomocą funkcji surface.draw_display_list(), generując podstawowe polecenia GL.

  4. Wywołuje funkcję swap_buffers() w kontekście bazowym, aby uruchomić fazę 4.

Faza 4. Prezentacja

W tej ostatniej fazie następuje interakcja ze sprzętem wyświetlacza, aby pokazać wyrenderowaną klatkę. HAR korzysta z solidnej ścieżki bezpośredniego renderowania na platformie Android Automotive OS (AAOS) Software-Defined Vehicle (SDV).

Kluczowym elementem tego etapu jest HarDirectRenderingContext (w pakiecie har-gl-context).

4.1 Architektura

Warstwa prezentacji korzysta z podejścia z podwójnym buforowaniem z docelowym rysowaniem poza ekranem:

  1. Bufor rysowania: bufor FBO poza ekranem, w którym Impeller renderuje scenę.

  2. Bufor rozdzielczości (opcjonalny): opcjonalny bufor pomocniczy obsługujący wygładzanie wielopróbkowe (MSAA).

    • Można to włączyć w razie potrzeby przez implementację lub konfigurację OpenGL ES. W takich przypadkach służy jako cel pośredni do rozwiązania bufora rysowania z próbkowaniem wielokrotnym przed skopiowaniem (przeniesieniem bloku bitów) do bufora renderowania.
  3. Bufor renderowania: ogólny bufor obsługiwany przez obiekt GBM, który odpowiada buforowi tylnemu w typowym łańcuchu wymiany grafiki.

  4. Bufor przedni: bufor GBM, który jest skanowany na wyświetlaczu.

4.2 Zamiana łańcucha

Gdy wywoływana jest funkcja swap_buffers, HAR wykonuje te czynności:

  1. Przenosi zawartość bufora rysowania do bufora renderowania (w razie potrzeby z pośrednim przeniesieniem do bufora rozwiązywania).

  2. Wywołuje glFlush() w kontekście GL i tworzy instancję EGL_SYNC_NATIVE_FENCE_ANDROID, aby śledzić zakończenie pracy procesora graficznego.

  3. Tworzy atomowe żądanie DRM, aby zamienić bufor renderowania na ekranie. To żądanie zawiera deskryptor pliku bariery GPU (nazywany barierą wejściową), aby zapobiec wyświetlaniu przez kontroler wyświetlania bufora renderowania, zanim GPU zakończy rysowanie.

  4. Jednocześnie wysyła do DRM żądanie utworzenia nowego ogrodzenia (zwanego ogrodzeniem wyjściowym), aby zasygnalizować, kiedy poprzedni bufor (bufor przedni poprzedniej klatki) nie jest już wyświetlany na ekranie.

  5. Zatwierdza żądanie niepodzielne za pomocą flagi nieblokującej, aby umożliwić kontynuowanie działania głównego wątku, podczas gdy podsystemy graficzne pozostają zsynchronizowane.

  6. Zapisuje w kontekście nowy limit zewnętrzny, aby HAR mógł poczekać na jego zasygnalizowanie na początku procesu swap_buffers w kolejnej klatce. Zapobiega to rysowaniu przez procesor graficzny w buforze, który jest nadal wyświetlany.

4.3 Ustawienie trybu bezpośredniego

HAR wchodzi w bezpośrednią interakcję z jądrem systemu za pomocą podsystemów DRM i KMS (Kernel Mode Setting) w celu skonfigurowania rozdzielczości wyświetlacza AAOS SDV, pomijając interakcje z menedżerami okien, takimi jak SurfaceFlinger (w określonych konfiguracjach), co umożliwia wyłączne i priorytetowe sterowanie sprzętem wyświetlacza.

4.4 Renderowanie zewnętrzne

HAR obsługuje delegowanie renderowania określonych elementów interfejsu (zidentyfikowanych za pomocą tagów w Figmie) do zewnętrznych procesów lub wątków. Jest to przydatne w przypadku integrowania złożonych scen 3D (np. wizualizacji samochodu z perspektywy pierwszej osoby z silników takich jak Kanzi czy Unity) lub innych treści, które wymagają dedykowanego kontekstu OpenGL.

4.4.1 Kluczowe komponenty

  • HarExternalRenderContext: dedykowany kontekst EGL poza ekranem dla usługi zewnętrznej.
  • SurfacePool: Zarządza zestawem LocalSurface (Texture plus EGLImage) buforów do podwójnego lub potrójnego buforowania.
  • SharedSurfaceExternalImage: bezpieczna wątkowo otoczka do przekazywania EGLImageuchwytów między usługą zewnętrzną a głównym procesem renderowania.

4.4.2 Przepływ pracy

Proces przebiega w tej kolejności:

  1. Usługa zewnętrzna uruchamia się i rejestruje w głównym looperze, określając, które tagi Figmy (np. #cluster/3d-car) renderuje.

  2. Usługa czeka na sygnały RenderStart z pętli, aby zsynchronizować renderowanie z sygnałem VSYNC wyświetlacza.

  3. Usługa renderuje treści poza ekranem w buforze ramki dostarczonym przezSurfacePool.

  4. Usługa wywołuje swap_buffers w swoim kontekście, co powoduje rotację puli i udostępnia ukończoną ramkę jako instancję SharedSurface.

  5. SharedSurface jest opakowany w ExternalImage i wysyłany przez kanał Rust MPSC do pętli.

  6. Główny moduł renderujący Impeller (faza 3) otrzymuje obraz zewnętrzny. Zamiast kopiować dane pikseli, wiąże on bazowy EGLImage bezpośrednio z teksturą i rysuje go jako część głównej sceny, uzyskując kompozycję bez kopiowania.

4.5 Platformy programistyczne i testowe (har-platform-linux)

Na potrzeby tworzenia i testowania aplikacje HAR mogą być kierowane na standardowe środowiska Linux Desktop i konfiguracje bez monitora. Te platformy są zaimplementowane w pakiecie crates/reference/platforms/har-platform-linux.

W przeciwieństwie do docelowej wersji produkcyjnej AAOS SDV te platformy nie używają direct-renderingpodsystemu har-gl-context do wyświetlania danych wyjściowych. Zamiast tego korzystają ze standardowych pakietów Rust OpenGL:

  • Tryb okienkowy: używa winit do zarządzania oknami i pętlami zdarzeń oraz glutin do tworzenia kontekstów OpenGL ES i integrowania z systemem okienkowym.

  • Tryb bez interfejsu graficznego: używa pakietu har-gl-context do utworzenia kontekstu bufora ekranowego (pbuffer) poza ekranem z domyślnym wyświetlaczem EGL. Umożliwia to renderowanie do bufora poza ekranem bez konieczności korzystania z widocznego okna lub bezpośredniego dostępu do sprzętu wyświetlającego. Jest to używane głównie do testów automatycznych lub przetwarzania na serwerze backendu.