Treści na tej stronie zostały przetłumaczone przy użyciu sztucznej inteligencji (AI) lub technologii tłumaczenia maszynowego i mogą zawierać błędy.

Skip to content

Retrospektywa metalu

Udało nam się udostępnić backend renderujący Metal milionom użytkowników i chciałbym napisać o tym kilka słów. W branży istnieją różne opinie na temat Metal — niektórzy twierdzą, że Metal nie byłby potrzebny, gdyby tylko Apple poświęciło więcej uwagi OpenGL i Vulkan, inni mówią, że jest to najłatwiejszy interfejs API grafiki, jaki kiedykolwiek istniał. Niektórzy pytają, po co w ogóle zawracać sobie głowę Metal, skoro można po prostu napisać kod OpenGL lub Vulkan i użyć MoltenGL lub MoltenVK, aby uzyskać ten sam efekt? Oto moje przemyślenia na temat tego interfejsu API.

Dlaczego Metal?

Kiedy Apple ogłosiło Metal na WWDC w 2014 roku, moją pierwszą reakcją było zignorowanie tego. Był on dostępny tylko na najnowszym sprzęcie, którego większość naszych użytkowników nie miała. I chociaż Apple twierdziło, że Metal rozwiązuje problemy z wydajnością procesora, optymalizacja pod kątem najmniejszej części rynku oznaczałaby dla nas, że różnica między najszybszymi a najwolniejszymi urządzeniami jeszcze bardziej by się powiększyła. W tamtym czasie korzystaliśmy z OpenGL ES 2 tylko na urządzeniach Apple, a także zaczynaliśmy przenosić aplikację na Androida.

Dwa i pół roku później tak wygląda udział Metal w rynku wśród naszych użytkowników:

To wygląda o wiele lepiej niż kiedyś. Wciąż jest tak, że wdrożenie Metal nie pomaga najstarszym urządzeniom, ale rynek GL na iOS ciągle się kurczy. Ponadto treści, które wyświetlamy na najstarszych urządzeniach, często różnią się od tych wyświetlanych na najnowszych urządzeniach, więc zdecydowanie warto poświęcić trochę wysiłku, aby przyspieszyć działanie tych urządzeń. Biorąc pod uwagę, że kod Metal dla iOS będzie działał na komputerach Mac po wprowadzeniu bardzo niewielkich zmian, warto byłoby używać go również na komputerach Mac, nawet jeśli skupiasz się na urządzeniach mobilnych (obecnie dostarczamy kompilacje Metal tylko na iOS).

Myślę, że warto nieco dokładniej przeanalizować udział w rynku. Na iOS obsługujemy Metal dla iOS 8.3+. Chociaż są użytkownicy, którzy nie mogą uruchomić Metal z powodu ograniczeń wersji systemu operacyjnego, większość z tych 25%, którzy nadal korzystają z GL, po prostu używa starszych urządzeń wyposażonych w sprzęt SGX. Nie mają one również żadnych funkcji OpenGL ES 3 i zadowalamy się tam ścieżką renderowania niższej klasy (chociaż chcielibyśmy, aby wszystkie urządzenia przeszły na Metal. Na szczęście podział na GL i Metal będzie się tylko poprawiał). Na komputerach Mac API Metal jest nowsze, a system operacyjny odgrywa tu dość znaczącą rolę. Aby korzystać z Metal, trzeba mieć OSX 10.11+, a połowa naszych użytkowników po prostu ma starszy system operacyjny. Chodzi tu mniej o sprzęt, a bardziej o oprogramowanie (95% naszych użytkowników komputerów Mac korzysta z OpenGL 3.2+).

Biorąc pod uwagę udział w rynku, nadal istnieją inne opcje, które nie wymagają portowania do Metal. Jedną z nich jest po prostu użycie MoltenGL, które wykorzystywałoby kod OpenGL, który już mamy, ale rzekomo byłoby szybsze; inną jest portowanie do Vulkan (aby uzyskać lepszą wydajność na PC, a ostatecznie na Androidzie); lub użycie MoltenVK. Krótko oceniłem MoltenGL i nie byłem zbyt zachwycony wynikami. Wymagało to pewnego wysiłku, aby nasz kod w ogóle działał, i chociaż wydajność była nieco lepsza w porównaniu ze standardowym OpenGL, liczyłem na więcej. Jeśli chodzi o MoltenVK, uważam, że błędem jest próba wdrożenia jednego niskopoziomowego API jako warstwy nad innym. Nieuchronnie dojdzie do niedopasowania impedancji, co spowoduje nieoptymalną wydajność. Być może będzie to lepsze niż API wysokiego poziomu, z którego korzystałeś wcześniej, ale raczej nie będzie tak szybkie, jak to tylko możliwe, a przecież właśnie dlatego wybierasz API niskiego poziomu! Innym ważnym aspektem jest to, że implementacja Metal jest znacznie prostsza niż Vulkan – więcej na ten temat później – więc w pewnym sensie wolałbym opakowanie Metal -> Vulkan zamiast Vulkan -> Metal.

Warto również zauważyć, że najwyraźniej w systemie iOS 10 na najnowszych iPhone'ach nie ma sterownika GL. GL jest zaimplementowane na bazie Metal, co oznacza, że korzystanie z OpenGL pozwala zaoszczędzić jedynie trochę wysiłku związanego z programowaniem – nie aż tak dużo, biorąc pod uwagę, że obietnica OpenGL „napisz raz, uruchom wszędzie” nie sprawdza się w rzeczywistości na urządzeniach mobilnych.

Portowanie

Powiedziałbym, że portowanie do Metal było ogólnie bardzo proste. Mamy duże doświadczenie w pracy z różnymi interfejsami API grafiki, od wysokopoziomowych, takich jak Direct3D 9/11, po niskopoziomowe na platformach konsolowych. Daje to wyjątkową przewagę w postaci możliwości wygodnego korzystania z interfejsu API takiego jak Metal, który jest jednocześnie dość wysokopoziomowy, ale pozostawia też niektóre zadania, takie jak synchronizacja procesora z kartą graficzną, do wykonania przez programistę aplikacji.

Jedyną przeszkodą było skompilowanie naszych shaderów. Gdy to się udało i przyszedł czas na pisanie kodu, okazało się, że API jest tak proste i intuicyjne, że kod praktycznie napisał się sam. W ciągu jednego dnia, w około 10 godzin, udało mi się uruchomić port, który renderował większość elementów w sposób nieoptymalny, a kolejne dwa tygodnie spędziłem na porządkowaniu kodu, naprawianiu problemów z walidacją, profilowaniu i optymalizacji oraz ogólnym dopracowywaniu. Opracowanie implementacji API w tak krótkim czasie wiele mówi o jakości API i zestawu narzędzi. Uważam, że przyczynia się do tego kilka aspektów:

  • Można tworzyć kod stopniowo, otrzymując dobre informacje zwrotne na każdym etapie. Nasz kod początkowo ignorował całą synchronizację między procesorem a kartą graficzną, był naprawdę nieoptymalny w niektórych częściach konfiguracji stanu, korzystał z wbudowanego śledzenia odwołań do zasobów i nigdy nie uruchamiał procesora i karty graficznej równolegle, aby uniknąć problemów. Faza optymalizacji i dopracowywania przekształciła to następnie w coś, co mogliśmy wypuścić, nie tracąc przy tym zdolności do renderowania.
  • Narzędzia są do Twojej dyspozycji; działają i działają dobrze. Nie jest to aż tak wielkim zaskoczeniem dla osób przyzwyczajonych do Direct3D 11, ale po raz pierwszy na urządzeniach mobilnych miałem do dyspozycji profiler procesora, profiler procesora graficznego, debugger procesora graficznego oraz warstwę walidacji API procesora graficznego, które wszystkie działały dobrze w połączeniu, wychwytując większość problemów podczas tworzenia i pomagając zoptymalizować kod.
  • Chociaż API jest nieco niższego poziomu niż Direct3D 11 i pozostawia deweloperowi pewne kluczowe decyzje niskopoziomowe (takie jak konfiguracja renderowania czy synchronizacja), nadal korzysta z tradycyjnego modelu zasobów, w którym każdy zasób ma pewne „flagi użycia”, z którymi został utworzony, ale nie wymaga barier potoku ani przejść układu, oraz z tradycyjnego modelu wiązania, w którym każdy etap shadera ma kilka slotów, do których można dowolnie przypisywać zasoby. Oba te elementy są znane, łatwe do zrozumienia i wymagają bardzo niewielkiej ilości kodu, aby szybko rozpocząć pracę.

Kolejną pomocną rzeczą było to, że nasz interfejs API był gotowy na API typu Metal. Jest bardzo oszczędny, ale ujawnia wystarczająco dużo szczegółów (takich jak przejścia renderowania), aby można było łatwo napisać wydajną implementację. W żadnym momencie naszej implementacji nie musiałem zapisywać/przywracać stanu (wiele interfejsów API boryka się z tym problemem, szczególnie z powodu traktowania konfiguracji render targetu jako zmian stanu oraz utrzymywania się powiązań zasobów/stanu w tym procesie) ani podejmować skomplikowanych decyzji dotyczących czasu życia zasobów/synchronizacji. Jedynym „skomplikowanym” fragmentem kodu potrzebnym do renderowania jest ten, który tworzy stan potoku renderowania poprzez haszowanie bitów niezbędnych do jego utworzenia – obiekty stanu potoku nie są częścią naszej abstrakcji API. Nawet to jest dość proste i szybkie. Więcej o naszym interfejsie API napiszę w osobnym poście.

Więc tydzień na skompilowanie shaderów, dwa tygodnie na dopracowanie i optymalizację implementacji (no dobrze, może jeszcze tydzień na naprawienie kilku błędów wykrytych podczas testów) – jakie są wyniki? Wyniki są świetne. Metal całkowicie spełnia obietnicę dotyczącą wydajności. Po pierwsze, wydajność wysyłania jednowątkowego jest zauważalnie lepsza niż w przypadku OpenGL (zmniejszając część wysyłania rysowania naszej klatki renderowania o 2-3 razy w zależności od obciążenia), a to przy założeniu, że nasza implementacja OpenGL jest dość dobrze dostrojona pod kątem redukcji zbędnej konfiguracji stanu i dobrej współpracy ze sterownikiem poprzez wykorzystanie szybkich ścieżek. Ale to nie wszystko – wielowątkowość w Metal jest banalna w użyciu, pod warunkiem, że kod renderowania jest na to gotowy. Nie przeszliśmy jeszcze na wielowątkowe wysyłanie rysowania, ale już przekształcamy niektóre inne części, które przygotowują zasoby do działania poza wątkiem renderowania, co w przeciwieństwie do OpenGL jest praktycznie bez wysiłku.

Poza tym Metal pozwala nam naprawić inne problemy z wydajnością, dając łatwo dostępne i niezawodne narzędzia. Jedną z kluczowych części naszego kodu renderującego jest system, który oblicza dane oświetlenia na procesorze w przestrzeni świata i przesyła je do regionów tekstury 3D (co musimy emulować na sprzęcie OpenGL ES 2). Aktualizacje są częściowe, więc nie możemy duplikować całej tekstury i musimy polegać na tym, jak sterownik implementuje funkcję „glTexSubImage3D”. W pewnym momencie próbowaliśmy użyć PBO, aby poprawić wydajność aktualizacji, ale napotkaliśmy poważne problemy ze stabilnością na wszystkich platformach, zarówno na Androidzie, jak i iOS. W Metal istnieją dwa wbudowane sposoby przesyłania regionu — MTLTexture.replaceRegion, z którego można skorzystać, jeśli GPU nie odczytuje aktualnie tekstury, lub MTLBlitCommandEncoder (copyFromBufferToTexture  lub copyFromTextureToTexture), które mogą przesłać region asynchronicznie, w samą porę, aby GPU mogło zacząć korzystać z tekstury. 

Obie te metody były wolniejsze, niż bym sobie tego życzył. Pierwsza z nich nie wchodziła w grę, ponieważ musieliśmy zapewnić wydajne aktualizacje częściowe, a działała ona wyłącznie na procesorze, wykorzystując coś, co wyglądało na bardzo powolną implementację translacji adresów. Druga działała, ale wydawało się, że wykorzystuje serię operacji 2D blit do wypełnienia tekstury 3D, co było dość kosztowne pod względem konfiguracji poleceń po stronie procesora, a także z jakiegoś powodu powodowało bardzo duże obciążenie procesora graficznego. Gdyby to było OpenGL, byłoby po wszystkim – w rzeczywistości wydajność tych dwóch metod mniej więcej odpowiadała obserwowanemu kosztowi podobnej aktualizacji w OpenGL. Na szczęście ponieważ jest to Metal, ma łatwy dostęp do shaderów obliczeniowych – a super prosty shader obliczeniowy dał nam możliwość wykonania operacji buffer -> 3D texture upload, która była bardzo szybka zarówno na procesorze, jak i na karcie graficznej i zasadniczo rozwiązała nasze problemy z wydajnością w tej części kodu na dobre((Liczby dotyczą 128 KB danych aktualizowanych na klatkę (dwa regiony 32x16x32 RGBA8) na A10)):

Na koniec ogólna uwaga: utrzymanie kodu Metal również nie wymaga większego wysiłku. Wszystkie dodatkowe funkcje, które musieliśmy dotychczas dodać, były łatwiejsze do wdrożenia tam niż w jakimkolwiek innym API, które obsługujemy, i spodziewam się, że ta tendencja się utrzyma. Pojawiły się pewne obawy, że dodanie kolejnego API będzie wymagało ciągłej konserwacji, ale w porównaniu z OpenGL nie wymaga to tak naprawdę wiele pracy. W rzeczywistości, ponieważ nie będziemy już musieli obsługiwać OpenGL ES 3 na iOS, oznacza to, że możemy również uprościć część naszego kodu OpenGL.

Stabilność

Obecnie na iOS Metal wydaje się bardzo stabilny. Nie jestem pewien, jak wyglądała sytuacja w momencie premiery w 2014 roku ani jak wygląda ona obecnie na Macu, ale zarówno sterowniki, jak i narzędzia dla iOS wydają się dość solidne.

Mieliśmy jeden problem ze sterownikiem na iOS 10, który dotyczył ładowania shaderów skompilowanych w Xcode 7 (co naprawiliśmy, przechodząc na Xcode 8), oraz jedną awarię sterownika na iOS 9, która okazała się wynikiem niewłaściwego użycia API „nextDrawable”. Poza tym nie zauważyliśmy żadnych błędów w działaniu ani żadnych awarii. Jak na stosunkowo nowe API, Metal działa bardzo solidnie pod każdym względem.

Ponadto narzędzia dostępne w ramach Metal są zróżnicowane i bogate; w szczególności można korzystać z:

  • Dość kompleksową warstwę walidacyjną, która identyfikuje typowe problemy związane z użyciem API. Działa ona zasadniczo jak debugowanie Direct3D — znane w środowisku Direct3D, ale praktycznie nieznane w świecie OpenGL (teoretycznie problem ten ma rozwiązać funkcja „ARB_debug_callback”; w praktyce jednak jest ona w większości niedostępna, a nawet jeśli jest, nie jest zbyt pomocna).
  • Działający debugger GPU, który pokazuje wszystkie wysłane polecenia wraz z ich stanem, zawartością render targetu, zawartością tekstur itp. Nie wiem, czy ma działający debugger shaderów, bo nigdy tego nie potrzebowałem, a sprawdzanie buforów mogłoby być trochę łatwiejsze, ale ogólnie spełnia swoje zadanie.
  • Działający profiler GPU, który pokazuje statystyki wydajności dla poszczególnych przejść (czas, przepustowość), a także czas wykonania dla poszczególnych shaderów. Ponieważ GPU jest tilerem, nie można oczywiście oczekiwać pomiarów czasu dla poszczególnych wywołań rysowania. Posiadanie takiego poziomu widoczności – zwłaszcza biorąc pod uwagę całkowity brak jakichkolwiek informacji o czasie działania GPU w interfejsach API grafiki na iOS – jest świetne.
  • Działający ślad osi czasu procesora/GPU (Metal System Trace), który pokazuje harmonogram obciążenia renderowania procesora i GPU (podobny do GPUView, ale w rzeczywistości łatwy w użyciu), pomijając pewne osobliwości interfejsu użytkownika.
  • Kompilator shaderów offline, który sprawdza poprawność składni shaderów, czasami wyświetla przydatne ostrzeżenia i konwertuje shadery na pliki binarne, które ładują się dość szybko w czasie wykonywania i są wcześniej dość dobrze zoptymalizowane (co skraca czas ładowania, bo kompilator sterownika może działać szybciej).

Jeśli pochodzisz ze świata Direct3D lub konsoli, możesz uznać każdą z tych rzeczy za oczywistą. Zaufaj mi, w OpenGL każda z tych rzeczy jest niezwykła i spotyka się z entuzjazmem, zwłaszcza na urządzeniach mobilnych, gdzie jesteś przyzwyczajony do radzenia sobie z czasami uszkodzonymi sterownikami, brakiem walidacji, brakiem debuggera GPU, brakiem pomocnego profilera GPU, brakiem możliwości gromadzenia danych dotyczących planowania GPU oraz zmuszonymi do pracy z tekstowym językiem shaderów, dla którego każdy dostawca ma nieco inny parser.

Wniosek

Metal to świetny interfejs API do pisania kodu i dostarczania aplikacji. Jest łatwy w użyciu, ma przewidywalną wydajność oraz solidne sterowniki i zestaw narzędzi. Przewyższa OpenGL pod każdym względem z wyjątkiem przenośności, ale w rzeczywistości OpenGL naprawdę należało używać tylko na trzech platformach (iOS, Android i Mac). Dwie z tych platform obsługują obecnie Metal; ponadto obietnica przenośności OpenGL w dużej mierze nie jest spełniona, ponieważ kod napisany na jednej platformie bardzo często nie działa na innej z różnych powodów.

Jeśli korzystasz z silnika innej firmy, takiego jak Unity lub UE4, Metal jest już tam obsługiwany; jeśli nie korzystasz z nich, a lubisz programowanie grafiki lub bardzo zależy Ci na wydajności i poważnie traktujesz iOS lub Mac, gorąco zachęcam do wypróbowania Metal. Nie zawiedziesz się.