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:
- Wstępne renderowanie: przetwarzanie grafu sceny, stosowanie dostosowań i rozwiązywanie układu.
- Generowanie poleceń: przekształcanie rozpoznanego wykresu sceny w niezależną od backendu listę wyświetlania.
- Renderowanie: wykonywanie poleceń rysowania za pomocą silnika graficznego Impeller.
- Prezentacja: zarządzanie buforami ramki i synchronizacja ze sprzętem wyświetlacza.
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ę
Presenterokreśla skrzynka ramowaharry, a implementacja referencyjna (UIModelPresenter) jest dostępna w skrzynceharry-app-core. - Model interfejsu: generuje dostosowania na podstawie bieżącego stanu. Kod modelu interfejsu jest generowany przy użyciu makra
DesignDocumentdostarczonego przez pakietderive_customizations. Przykładem tego jest strukturaUIModelwharry-app-core. - Squoosh: udostępnia strukturę danych
SquooshViewi repozytorium wariantów, które służą do renderowania interfejsu zgodnie z projektem. Zserializowany dokument projektu jest wczytywany przez pakietdc_bundlez biblioteki DesignCompose i konwertowany na drzewo strukturSquooshView, 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 CustomActions w harry-app-core crate.
Oto podstawowy zarys pętli reduktora:
- Przetwarzanie działania:
Reducerotrzymuje 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:
Presentermapuje nowy stan naUIModel.UIModelto 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
applymodelu interfejsu, aby wygenerować zestaw instancjiRenderCustomization. Są to szczegółowe instrukcje dotyczące modyfikowania projektu w Figmie (np. „Ustaw tekst węzła #speed na „65 mph””). UpdatePolicyna 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,UpdatePolicyoznacza, ż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
DesignComposeDefinitionna początkowe drzewoSquooshView. Jest to proces jednorazowy.Repozytorium:
SquooshVariantRepositoryzarzą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
SquooshViewodwoł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:
DynamicLayoutoblicza ostateczną pozycję i rozmiar (granice) każdego węzła w drzewieSquooshView.Układ tekstu:
TextHelperużywa implementacjiLayoutHelpercechy 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 podstawiemeter_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
SquooshViewjest porównywany z okresemprevious_squoosh_viewz dniaPreRenderCache.Interpolacja: jeśli właściwości uległy zmianie,
Squooshtworzy 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 drzewoSquooshView.Tłumaczenie:
- Kształty i ścieżki: przekształcane na
DisplayListEntryz odpowiednim wariantemDisplayListAppearance(np.RectlubPath). - Tekst: przekonwertowany za pomocą
TextHelperna wpisy rysunkowe. - Przekształcenia i klipy: przekształcane w pary
PushTransform3DiPopTransform3DlubPushClipRegioniPopClipRegion, aby zarządzać stosem stanów rysowania. - Maskowanie: przekształcone w pary
PushMaskLayeriPopMaskLayer, aby prawidłowo tworzyć i mieszać warstwy.
- Kształty i ścieżki: przekształcane na
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
impelleriimpeller-rs-bindgen).TypographyContext: zarządza rejestracją czcionek i kształtowaniem tekstu.
3.1 Inicjowanie i zarządzanie platformami
Tworzenie kontekstu: moduł renderujący inicjuje instancję
impeller::Contextz 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
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 powiązaniu instancji
EGLImagelub zewnętrznych tekstur OpenGL z obiektami ImpellerTexturew 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:
Czyści bufor i stosuje globalne przekształcenia na potrzeby skalowania DPI i obracania ekranu.
Iteruje po elementach wejściowych
DisplayListEntry:- Stan:
save()irestore()służą do przesuwania i wycofywania przekształceń oraz regionów przycinania. - Elementy podstawowe: elementy
RectiRoundedRectsą 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
TextiStyledTextsą renderowane za pomocąTypographyContext. - Obrazy: standardowe i zewnętrzne obrazy są rysowane za pomocą elementu
draw_texture_rect.
- Stan:
Przesyła utworzoną listę wyświetlania Impeller do powierzchni za pomocą funkcji
surface.draw_display_list(), generując podstawowe polecenia GL.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:
Bufor rysowania: bufor FBO poza ekranem, w którym Impeller renderuje scenę.
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.
Bufor renderowania: ogólny bufor 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 Zamiana łańcucha
Gdy wywoływana jest funkcja swap_buffers, HAR wykonuje te czynności:
Przenosi zawartość bufora rysowania do bufora renderowania (w razie potrzeby z pośrednim przeniesieniem do bufora rozwiązywania).
Wywołuje
glFlush()w kontekście GL i tworzy instancjęEGL_SYNC_NATIVE_FENCE_ANDROID, aby śledzić zakończenie pracy procesora graficznego.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.
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.
Zatwierdza żądanie niepodzielne za pomocą flagi nieblokującej, aby umożliwić kontynuowanie działania głównego wątku, podczas gdy podsystemy graficzne pozostają zsynchronizowane.
Zapisuje w kontekście nowy limit zewnętrzny, aby HAR mógł poczekać na jego zasygnalizowanie na początku procesu
swap_buffersw 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 zestawemLocalSurface(TextureplusEGLImage) buforów do podwójnego lub potrójnego buforowania.SharedSurfaceExternalImage: bezpieczna wątkowo otoczka do przekazywaniaEGLImageuchwytów między usługą zewnętrzną a głównym procesem renderowania.
4.4.2 Przepływ pracy
Proces przebiega w tej kolejności:
Usługa zewnętrzna uruchamia się i rejestruje w głównym looperze, określając, które tagi Figmy (np.
#cluster/3d-car) renderuje.Usługa czeka na sygnały
RenderStartz pętli, aby zsynchronizować renderowanie z sygnałem VSYNC wyświetlacza.Usługa renderuje treści poza ekranem w buforze ramki dostarczonym 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 opakowany wExternalImagei wysyłany przez kanał Rust MPSC do pętli.Główny moduł renderujący Impeller (faza 3) otrzymuje obraz zewnętrzny. Zamiast kopiować dane pikseli, wiąże on bazowy
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 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
winitdo zarządzania oknami i pętlami zdarzeń orazglutindo tworzenia kontekstów OpenGL ES i integrowania z systemem okienkowym.Tryb bez interfejsu graficznego: używa pakietu
har-gl-contextdo 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.