Na tej stronie opisujemy pełny potok graficzny renderera o wysokiej dostępności (HAR), śledząc przepływ danych od dokumentu projektu w Figma do końcowych pikseli wyświetlanych na ekranie.
Przegląd
Potok przekształca definicje interfejsu użytkownika wysokiego poziomu na polecenia graficzne niskiego poziomu i efektywnie prezentuje je na wyświetlaczach sprzętowych. Potok jest przeznaczony do aplikacji o krytycznym znaczeniu dla bezpieczeństwa w motoryzacji. Kładzie nacisk na deterministyczne renderowanie, efektywne zarządzanie stanem i solidną interakcję z podsystemami graficznymi platformy, takimi jak Direct Rendering Manager (DRM) i Generic Buffer Management (GBM).
Potok można podzielić na 4 główne fazy:
- Wstępne renderowanie: przetwarzanie grafu sceny, stosowanie dostosowań i rozwiązywanie układu.
- Generowanie poleceń: przekształcanie rozwiązanego grafu sceny w listę wyświetlania niezależną od backendu.
- Renderowanie: wykonywanie poleceń rysowania za pomocą silnika graficznego Impeller.
- Prezentacja: zarządzanie buforami ramki i synchronizacja ze sprzętem wyświetlającym.
Rysunek 1. Przepływ grafiki 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 użytkownika w pamięci, gotowe do renderowania. Ta faza jest wykonywana w osobnym wątku ograniczenia, niezależnym od głównej pętli wyświetlania.
1.1 Podstawa DesignCompose
Potok HAR jest oparty na ekosystemie DesignCompose.
- Źródło: interfejs użytkownika jest projektowany w Figma i eksportowany za pomocą wtyczki DesignCompose.
- Definicja: dane wyjściowe to instancja
DesignComposeDefinition, czyli serializowana reprezentacja projektu (węzły, style, warianty). - Wiązanie danych: model interfejsu użytkownika aplikacji używa makr proceduralnych (np.
#[Design(node = "#speed")]) do jawnego wiązania pól struktury Rust z konkretnymi nazwanymi węzłami w dokumencie Figma. Dzięki temu stan aplikacji może automatycznie sterować właściwościami elementów wizualnych.
Kluczowe komponenty tej podstawy to:
- Ograniczenie: działa jako centralna pętla zdarzeń, przetwarzając działania i aktualizując bieżący stan. Framework udostępnia
DefaultReducer, ale w razie potrzeby można podać niestandardową implementację ograniczenia. - Prezenter: łączy bieżący stan z modelem interfejsu użytkownika. Cechę
Presenterokreśla pakietharryframework, a implementacja referencyjna (UIModelPresenter) jest dostępna w pakiecieharry-app-core. - Model interfejsu użytkownika: generuje dostosowania na podstawie bieżącego stanu. Kod modelu interfejsu użytkownika jest generowany za pomocą makra
DesignDocumentudostępnianego przez pakietderive_customizations. Przykładem jest strukturaUIModelw pakiecieharry-app-core. - Squoosh: udostępnia strukturę danych
SquooshViewi repozytorium wariantów, które służą do renderowania interfejsu użytkownika zgodnie z projektem. Serializowany dokument projektu jest wczytywany przez pakietdc_bundlez biblioteki DesignCompose i przekształcany w drzewo strukturSquooshView, co zapewnia wydajne działanie w czasie wykonywania.
1.2 Pętla ograniczenia
Potok jest sterowany przez działania. Framework określa wyliczeniowy typ Actions, 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 implementację działań wpływających na stan aplikacji i opcjonalnie generuje efekty uboczne, które są następnie przekazywane z ograniczenia do aplikacji w celu przetworzenia. Szczegółowy przykład znajdziesz w wyliczeniu CustomActions w pakiecie harry-app-core.
Oto podstawowy zarys pętli ograniczenia:
- Przetwarzanie działania:
Reducerodbiera działanie i aktualizuje bieżący stan. Są to dane pierwotne, takie jak bieżąca prędkość lub aktywne kontrolki (lampki ostrzegawcze). Może to też generować efekty uboczne (np. sygnał odtwarzający dzwonek, gdy miga lampka pasów bezpieczeństwa). - Prezentacja:
Presentermapuje nowy stan naUIModel.UIModelto model widoku, który zawiera dane sformatowane specjalnie na potrzeby interfejsu użytkownika (np. formatowanie prędkości „120” na ciąg znaków „65 mph”). - Generowanie dostosowań: wywoływana jest metoda
applymodelu interfejsu użytkownika, aby wygenerować zestaw instancjiRenderCustomization. Są to wyraźne instrukcje modyfikowania projektu w Figma (np. „Ustaw tekst węzła #speed na „65 mph””). UpdatePolicyna potrzeby optymalizacji: po każdej operacji 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,UpdatePolicysygnalizuje, że nie są potrzebne żadne dalsze aktualizacje. W takich przypadkach ograniczenie 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 Wczytywanie widoku i inicjowanie repozytorium
Potok zaczyna się od instancji DesignComposeDefinition. Jest to dokument projektu w Figma serializowany przez DesignCompose do struktury bufora protokołu.
Wczytywanie początkowe: podczas uruchamiania główny projekt (określony przez węzeł główny) jest przekształcany z
DesignComposeDefinitionw początkowe drzewoSquooshView. Jest to jednorazowa operacja.Repozytorium:
SquooshVariantRepositoryzarządza wariantami komponentów wielokrotnego użytku i początkowo wczytanymi widokami.Wczytywanie leniwe: aby zminimalizować czas uruchamiania i zużycie pamięci, dodatkowe widoki (które nie są częścią początkowego drzewa węzła głównego) są wczytywane z dokumentu leniwie, tylko wtedy, gdy są jawnie przywoływane i potrzebne przez logikę renderowania (np. podczas dostosowywania listy).
1.4 Operacja dostosowywania
Drzewo SquooshView jest przechodzone w celu zastosowania dynamicznego stanu aplikacji:
Zamiana wariantów: instancje komponentów są zamieniane na konkretne warianty (np. zmiana ikony reprezentującej bieżący tryb jazdy z trybu sportowego na tryb ekonomiczny) na podstawie logiki czasu wykonywania.
Rozwijanie listy: pojedynczy element szablonu w Figma jest zastępowany dynamiczną listą elementów podrzędnych. Dla tych elementów podrzędnych generowane są nowe unikalne identyfikatory, aby zapewnić stabilną tożsamość animacji.
Zastępowanie tekstu i stylu: zawartość tekstowa (np. wartość prędkości) i style (np. nieprzezroczystość, kolor) są aktualizowane na podstawie bieżącego stanu.
1.5 Rozdzielczość zmienna
Rozwiązywane są tokeny projektowe i zmienne zdefiniowane w Figma lub lokalnie w aplikacji.
- Wiązanie: właściwości
SquooshViewodwołujące się do zmiennych (takich jak kolory lub wymiary) są zastępowane ich konkretnymi wartościami dla bieżącej ramki.
1.6 Obliczanie układu
Układ dynamiczny:
DynamicLayoutoblicza ostateczne położenie i rozmiar (granice) każdego węzła w drzewieSquooshView.Układ tekstu:
TextHelperużywa implementacji cechyLayoutHelperdo obliczania wymiarów tekstu, zawijania i kształtowania. Pomaga to sprawdzić, czy tekst prawidłowo przepływa w ramach ograniczeń przed renderowaniem.
1.7 Wskaźniki i mierniki
Jest to specjalny krok w przypadku interfejsów użytkownika w motoryzacji.
MeterData: jeśli węzeł ma dane miernika (zdefiniowane w Figma), jego geometria jest dynamicznie zmieniana na podstawiemeter_value(np. prędkości pojazdu).- Łuki: kąt zamiecenia jest dostosowywany.
- Rotacje: transformacja rotacji 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 wektorowej jest dostosowywana.
1.8 Animacja
Porównywanie: bieżący
SquooshViewjest porównywany zprevious_squoosh_viewzPreRenderCache.Interpolacja: jeśli właściwości uległy zmianie,
Squooshtworzy interpolatory, aby płynnie przechodzić między wartościami (np. nieprzezroczystością lub transformacją) w czasie.
Faza 2. Generowanie poleceń
Gdy drzewo SquooshView jest w pełni rozwiązane i animowane, jest przekształcane w liniową sekwencję poleceń rysowania.
Kluczowym komponentem tej fazy jest pakiet DisplayList:
generate_dl: ta funkcja rekurencyjnie przechodzi przez drzewoSquooshView.Tłumaczenie:
- Kształty i ścieżki: przekształcane w
DisplayListEntryz odpowiednim wariantemDisplayListAppearance(np.RectlubPath). - Tekst: przekształcany za pomocą
TextHelperw elementy rysowania tekstu. - Transformacje i wycinki: przekształcane w pary
PushTransform3DiPopTransform3DlubPushClipRegioniPopClipRegion, aby zarządzać stosem stanu rysowania. - Maskowanie: przekształcane w pary
PushMaskLayeriPopMaskLayer, aby prawidłowo tworzyć i łączyć warstwy.
- Kształty i ścieżki: przekształcane w
Wynikiem końcowym jest instancja Vec<DisplayListEntry>, która opisuje co
należy narysować, niezależnie od sposobu rysowania.
2.1 Przekazywanie do pętli
Po wygenerowaniu DisplayList ograniczenie opakowuje go w instancję ViewDescriptor i wysyła przez kanał Rust MPSC (LooperMessage) do wątku pętli. Looper odpowiada za fazy renderowania i wyświetlania, co zapobiega blokowaniu potoku graficznego przez wątek ograniczenia.
Faza 3. Renderowanie
Niezależna od platformy DisplayList jest przekazywana do backendu renderowania, gdzie abstrakcyjne polecenia są przekształcane w instrukcje GPU.
HAR używa Impeller, silnika renderowania pierwotnie stworzonego na potrzeby Fluttera. Impeller został zaprojektowany, aby rozwiązać problem z zacinaniem się klatek spowodowanym kompilacją shaderów. W tym celu podczas kompilacji wstępnie kompiluje mały, wydajny zestaw shaderów. To podejście w połączeniu z efektywnym grupowaniem i wysoce zoptymalizowanym backendem zapewnia:
- Deterministyczną wydajność: praktycznie eliminuje zacinanie się spowodowane kompilacją shaderów w czasie wykonywania.
- Szybkie uruchamianie: zmniejsza obciążenie związane z inicjowaniem.
- Mały rozmiar: tworzy kompaktowy plik binarny.
Szczegółowe wprowadzenie do architektury Impeller znajdziesz w filmie Introducing Impeller – Flutter's new rendering engine. Chociaż film dotyczy Fluttera, te podstawowe zalety bezpośrednio wzmacniają stos HAR w motoryzacji.
Kluczowe komponenty fazy renderowania to:
ImpellerRenderer: przekształca listę wyświetlania z fazy wstępnego renderowania w polecenia renderowania Impeller.Impeller Rust API: opakowuje bibliotekę Impeller do użytku w Rust (pakiety
impelleriimpeller-rs-bindgen).TypographyContext: zarządza rejestracją czcionek i kształtowaniem tekstu.
3.1 Inicjowanie i zarządzanie powierzchnią
Tworzenie kontekstu: renderer inicjuje instancję
impeller::Contextz backendem OpenGL ES, przekazując wywołanie zwrotne, aby rozwiązać wskaźniki funkcji OpenGL ES z kontekstu GL platformy.Powierzchnia FBO opakowana: zamiast tworzyć własne okno, Impeller renderuje do istniejącego obiektu bufora ramki OpenGL (FBO) udostępnianego przez fazę 4. Odbywa się to przez wywołanie
Surface::create_wrapped_fbo.
3.2 Zarządzanie zasobami
Obrazy: 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
TypographyContextna potrzeby renderowania tekstu.Obrazy zewnętrzne: specjalna obsługa tekstur zewnętrznych (np. strumieni z kamery i zewnętrznych rendererów 3D) polega na wiązaniu instancji
EGLImagelub zewnętrznych tekstur OpenGL z obiektamiTextureImpeller w celu renderowania bez kopiowania.
3.3 Operacja renderowania
Pętla render tworzy instancję DisplayList Impeller (nie mylić z Vec<DisplayListEntry> wygenerowaną w fazie wstępnego renderowania) za pomocą DisplayListBuilder:
Czyści bufor i stosuje globalne transformacje na potrzeby skalowania DPI i rotacji wyświetlacza.
Iteruje po elementach wejściowych
DisplayListEntry:- Stan:
save()irestore()służą do wypychania i wysuwania transformacji oraz regionów wycinków. - Elementy pierwotne:
RectiRoundedRectsą rysowane za pomocą standardowych operacji malowania. - Ścieżki: złożone ścieżki wektorowe (w tym dynamiczne instancje
Arc) są tworzone i rysowane. - Tekst:
TextiStyledTextsą renderowane za pomocąTypographyContext. - Obrazy: standardowe i zewnętrzne obrazy są rysowane za pomocą
draw_texture_rect.
- Stan:
Przesyła utworzoną listę wyświetlania Impeller do powierzchni za pomocą
surface.draw_display_list(), generując podstawowe polecenia GL.Wywołuje
swap_buffers()w podstawowym kontekście, aby uruchomić fazę 4.
Faza 4. Prezentacja
Ta ostatnia faza obsługuje interakcję ze sprzętem wyświetlającym w celu wyświetlenia renderowanej ramki. HAR używa solidnej ścieżki renderowania bezpośredniego w systemie operacyjnym Android Automotive (AAOS) Software-Defined Vehicle (SDV).
Kluczowym komponentem tej fazy jest HarDirectRenderingContext (w pakiecie har-gl-context).
4.1 Architektura
Warstwa prezentacji używa podejścia z podwójnym buforowaniem z celem rysowania poza ekranem:
Bufor rysowania: FBO poza ekranem, w którym Impeller renderuje scenę.
Bufor rozwiązywania (opcjonalny): opcjonalny bufor pomocniczy do obsługi wygładzania wielopróbkowego (MSAA).
- Można go włączyć w razie potrzeby przez podstawową implementację lub konfigurację OpenGL ES. W takich przypadkach służy jako cel pośredni do rozwiązywania bufora rysowania wielopróbkowego przed blitowaniem (przenoszeniem bloków bitów) do bufora renderowania.
Bufor renderowania: bufor ogólny obsługiwany przez obiekt GBM, który odpowiada buforowi tylnemu w typowym łańcuchu wymiany grafiki.
Bufor przedni: bufor GBM, który jest skanowany na wyświetlaczu.
4.2 Łańcuch wymiany
Gdy wywoływana jest funkcja swap_buffers, HAR wykonuje te czynności:
Blituje zawartość bufora rysowania do bufora renderowania (z pośrednim blitowaniem do bufora rozwiązywania, jeśli jest to wymagane przez implementację).
Wywołuje
glFlush()w kontekście GL i tworzy instancjęEGL_SYNC_NATIVE_FENCE_ANDROID, aby śledzić zakończenie pracy GPU.Tworzy atomowe żądanie DRM, aby zamienić bufor renderowania na ekran. To żądanie zawiera deskryptor pliku ogrodzenia GPU (nazywany ogrodzeniem wejściowym), aby zapobiec wyświetlaniu bufora renderowania przez kontroler wyświetlania, zanim GPU zakończy rysowanie.
Jednocześnie żąda nowego ogrodzenia od DRM (nazywanego ogrodzeniem wyjściowym), aby zasygnalizować, kiedy poprzedni bufor (bufor przedni poprzedniej ramki) nie jest już wyświetlany.
Zatwierdza żądanie atomowe za pomocą flagi nieblokującej, aby umożliwić wątkowi głównemu kontynuowanie pracy, podczas gdy podsystemy graficzne pozostają zsynchronizowane.
Przechowuje nowe ogrodzenie wyjściowe w kontekście, aby HAR mógł poczekać na jego zasygnalizowanie na początku procesu
swap_buffersw następnej ramce. Zapobiega to rysowaniu przez GPU w buforze, który jest nadal wyświetlany.
4.3 Ustawienie trybu bezpośredniego
HAR wchodzi w interakcję bezpośrednio z jądrem za pomocą podsystemów DRM i Kernel Mode Setting (KMS), aby skonfigurować rozdzielczość wyświetlacza AAOS SDV, pomijając interakcje z menedżerami okien, takimi jak SurfaceFlinger (w określonych konfiguracjach), co umożliwia wyłączną i priorytetową kontrolę nad sprzętem wyświetlającym.
4.4 Renderowanie zewnętrzne
HAR obsługuje delegowanie renderowania określonych elementów interfejsu użytkownika (identyfikowanych za pomocą tagów w Figma) do procesów lub wątków zewnętrznych. Jest to przydatne do integrowania złożonych scen 3D (np. wizualizacji samochodu ego 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 buforówLocalSurface(TextureplusEGLImage) na potrzeby podwójnego lub potrójnego buforowania.SharedSurfaceExternalImage: opakowanie bezpieczne dla wątków do przekazywania uchwytówEGLImagemiędzy usługą zewnętrzną a głównym rendererem.
4.4.2 Przepływ pracy
Przepływ pracy jest następujący:
Usługa zewnętrzna uruchamia się i rejestruje w głównej pętli, określając, które tagi Figma (np.
#cluster/3d-car) renderuje.Usługa czeka na sygnały
RenderStartz pętli, aby zsynchronizować renderowanie z sygnałem VSYNC wyświetlacza.Poza ekranem usługa renderuje zawartość do bufora ramki udostępnianego przez
SurfacePool.Usługa wywołuje
swap_buffersw swoim kontekście, co powoduje rotację puli i udostępnia ukończoną ramkę jako instancjęSharedSurface.SharedSurfacejest opakowywany wExternalImagei wysyłany przez kanał Rust MPSC do pętli.Główny renderer Impeller (faza 3) odbiera obraz zewnętrzny. Zamiast kopiować dane pikseli, wiąże podstawowy
EGLImagebezpoś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 programowania i testowania aplikacje HAR mogą być kierowane na standardowe środowiska komputerowe z systemem Linux i konfiguracje bez ekranu. Te platformy są zaimplementowane w pakiecie crates/reference/platforms/har-platform-linux.
W przeciwieństwie do produkcyjnego celu AAOS SDV te platformy nie używają podsystemu direct-rendering pakietu har-gl-context do wyświetlania. Zamiast tego korzystają ze standardowych pakietów Rust OpenGL:
Tryb okienkowy: używa
winitdo zarządzania oknami i pętlami zdarzeń orazglutindo tworzenia kontekstów OpenGL ES i integracji z systemem okien.Tryb bez ekranu: używa pakietu
har-gl-contextdo tworzenia kontekstu pbuffer poza ekranem z domyślnym wyświetlaczem EGL. Umożliwia to renderowanie do bufora poza ekranem bez konieczności używania 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 backendu.