3 lata metalu

Trzy lata temu przenieśliśmy nasz renderer na Metal. Nie zajęło to dużo czasu, było to świetną zabawą i działało naprawdę dobrze na iOS. Napisaliśmy więc artykuł opisujący, jak podjęliśmy tę decyzję i jak to się skończyło (uwaga, spoiler: naprawdę dobrze!). Większość tej pierwotnej retrospektywy nadal ma zastosowanie, ale dziś Metal jest w lepszej formie niż kiedykolwiek – postanowiliśmy więc opublikować ten artykuł ponownie, dodając aktualizację z ostatnich trzech lat.
Cofnijmy się więc w czasie, udajmy, że jest grudzień 2016 roku i właśnie wydaliśmy wersję naszego renderera Metal na iOS.
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, a chociaż Apple twierdziło, że rozwiązuje to problemy z wydajnością procesora, optymalizacja pod kątem najmniejszej części rynku oznaczałaby, ż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 Apple, a także zaczynaliśmy przenosić się na Androida.
Dwa i pół roku później tak wygląda udział Metal w rynku wśród naszych użytkowników:

Wygląda to 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, a treści, które uruchamiamy na tych starych urządzeniach, często różnią się od tych na najnowszych, więc warto poświęcić trochę wysiłku, żeby je przyspieszyć. 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+; choć 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, a my zadowalamy się uruchamianiem tam ścieżki renderowania z niższej półki (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 dość znaczącą rolę – aby korzystać z Metal, trzeba używać OSX 10.11+, a połowa naszych użytkowników po prostu ma starszy system operacyjny – chodzi 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 mamy 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 prawdopodobnie byłoby szybsze; inną jest portowanie do Vulkan (aby uzyskać lepszą wydajność na PC, a ostatecznie na Androidzie) i użycie MoltenVK. Krótko przetestowałem MoltenGL i nie byłem zbyt zachwycony wynikami – wymagało to pewnego wysiłku, aby nasz kod w ogóle działał, a 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 próba zaimplementowania jednego niskopoziomowego API jako warstwy nad innym jest błędna – na pewno dojdzie do niedopasowania impedancji, co spowoduje nieoptymalną wydajność – może będzie to lepsze niż wysokopoziomowe API, którego używałeś wcześniej, ale raczej nie będzie tak szybkie, jak to możliwe, a przecież właśnie dlatego wybierasz niskopoziomowe API! 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. Oznacza to, że korzystanie z OpenGL pozwala zaoszczędzić tylko 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
Ogólnie rzecz biorąc, portowanie do Metal było bardzo proste. Mamy duże doświadczenie w pracy z różnymi interfejsami API grafiki, od wysokopoziomowych, takich jak Direct3D 9/11, po niskopoziomowe, takie jak PS4 GNM. 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 twórcę aplikacji.
Jedynym prawdziwym wyzwaniem 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. Wdrożenie 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:
- Kod można tworzyć 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 określone „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 są znane, łatwe do zrozumienia i wymagają bardzo niewielkiej ilości kodu, aby szybko zacząć działać.
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 etapy 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 target 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 potrzebnych 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ę implementacji1 – jakie są wyniki? Wyniki są świetne – Metal absolutnie spełnia obietnicę dotyczącą wydajności. Po pierwsze, wydajność wysyłania w jednym wątku 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ż konwertujemy 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 rozwiązać inne problemy z wydajnością, zapewniając łatwo dostępne i niezawodne narzędzia. Jednym z centralnych elementów 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 zduplikować całej tekstury i musimy polegać na tym, jak sterownik implementuje 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, którego można użyć, jeśli GPU nie odczytuje aktualnie tekstury, lub MTLBlitCommandEncoder (copyFromBufferToTexture lub copyFromTextureToTexture), który może przesłać region asynchronicznie, w samą porę, aby GPU mogło zacząć korzystać z tekstury.


Na koniec ogólna uwaga: utrzymanie kodu Metal jest również dość łatwe – 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 Metal na iOS 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 we wszystkich aspektach.
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 to w zasadzie jak debugowanie Direct3D — znane w środowisku Direct3D, ale praktycznie nieznane w świecie OpenGL (teoretycznie ARB_debug_callback ma to rozwiązać, w praktyce jest ono w większości niedostępne, a gdy już jest, nie jest zbyt pomocne)
- 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 CPU/GPU (Metal System Trace), który pokazuje harmonogram obciążenia renderowania CPU 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, konwertuje shadery na pliki binarne, które ładują się dość szybko w czasie wykonywania i są dodatkowo 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 traktować każdą z tych rzeczy jako coś oczywistego – uwierz 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 przydatnego profilera GPU, brakiem możliwości gromadzenia danych o harmonogramowaniu GPU oraz koniecznością pracy z tekstowym językiem shaderów, dla którego każdy dostawca ma nieco inny parser.
Metal to świetny interfejs API zarówno do pisania kodu, jak i do dostarczania aplikacji. Jest łatwy w użyciu, ma przewidywalną wydajność, solidne sterowniki i solidny zestaw narzędzi. Przewyższa OpenGL pod każdym względem z wyjątkiem przenośności, ale rzeczywistość z OpenGL jest taka, że naprawdę powinieneś go używać tylko na trzech platformach (iOS, Android i Mac), a dwie z nich obsługują teraz Metal; dodatkowo obietnica przenośności OpenGL w dużej mierze nie jest spełniona, ponieważ kod, który piszesz 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ę.
Metal teraz
Z naszego punktu widzenia największe zmiany, jakie zaszły w Metal w ciągu ostatnich trzech lat, dotyczą wdrożenia na masową skalę.
Trzy lata temu jedna czwarta urządzeń musiała korzystać z OpenGL. Dzisiaj, w przypadku naszej grupy odbiorców, liczba ta wynosi około 2% — co oznacza, że nasz backend OpenGL praktycznie nie ma już znaczenia. Nadal go utrzymujemy, ale nie potrwa to długo.
Sterowniki są również lepsze niż kiedykolwiek – ogólnie rzecz biorąc, nie obserwujemy problemów ze sterownikami na iOS, a jeśli już się pojawiają, to często dotyczą wczesnych prototypów, a zanim prototypy trafią do produkcji, problemy są zazwyczaj naprawione.
Poświęciliśmy również trochę czasu na ulepszenie naszego backendu Metal, skupiając się na trzech obszarach:
Przeprojektowanie łańcucha narzędzi do kompilacji shaderów
Kolejną rzeczą, która wydarzyła się w ciągu ostatnich trzech lat, jest wydanie i rozwój Vulkan. Chociaż mogłoby się wydawać, że interfejsy API są zupełnie inne (i tak jest), ekosystem Vulkan dał społeczności zajmującej się renderowaniem fantastyczny zestaw narzędzi open source, które w połączeniu tworzą łatwy w użyciu zestaw narzędzi kompilacyjnych o jakości produkcyjnej.
Wykorzystaliśmy te biblioteki do stworzenia łańcucha narzędzi kompilacyjnych, który może pobierać kod źródłowy HLSL (wykorzystujący różne funkcje DX11, w tym shadery obliczeniowe), kompilować go do formatu SPIRV, optymalizować ten SPIRV i konwertować wynikowy SPIRV do MSL (Metal Shading Language). Zastępuje on nasz poprzedni łańcuch narzędzi, który mógł wykorzystywać jako dane wejściowe jedynie kod źródłowy DX9 HLSL i miał różne problemy z poprawnością w przypadku skomplikowanych shaderów.
To trochę ironiczne, że Apple nie miało z tym nic wspólnego, ale oto jesteśmy. Ogromne podziękowania dla autorów i opiekunów glslang (https://github.com/KhronosGroup/glslang), spirv-opt (https://github.com/KhronosGroup/SPIRV-Tools) oraz SPIRV-Cross (https://github.com/KhronosGroup/SPIRV-Cross). Wnieśliśmy również zestaw poprawek do tych bibliotek, aby pomóc nam w dostarczeniu nowego zestawu narzędzi, a także wykorzystać go do przekierowania naszych shaderów do interfejsów API Vulkan, Metal i OpenGL.
Obsługa systemu macOS
Port na macOS zawsze był możliwy, ale nie był dla nas priorytetem, dopóki nie zaczęło nam brakować niektórych funkcji. Zdecydowaliśmy więc, że powinniśmy zainwestować w Metal na macOS, aby uzyskać szybsze renderowanie i otworzyć sobie pewne możliwości na przyszłość.
Z punktu widzenia implementacji nie było to wcale trudne. Większość API jest dokładnie taka sama; poza zarządzaniem oknami jedynym obszarem, który wymagał znacznych poprawek, było przydzielanie pamięci. Na urządzeniach mobilnych istnieje wspólna przestrzeń pamięci dla buforów i tekstur, podczas gdy na komputerach stacjonarnych API zakłada dedykowany procesor graficzny z własną pamięcią wideo.
Można to szybko obejść, korzystając z zasobów zarządzanych, gdzie środowisko uruchomieniowe Metal zajmuje się kopiowaniem danych za Ciebie. W ten sposób wydaliśmy naszą pierwszą wersję, ale później przerobiliśmy implementację, aby bardziej wyraźnie kopiować dane zasobów przy użyciu buforów tymczasowych, tak aby zminimalizować obciążenie pamięci systemowej.
Największą różnicą między systemami macOS i iOS była stabilność. W systemie iOS mieliśmy do czynienia tylko z jednym dostawcą sterowników na jednej architekturze, podczas gdy w systemie macOS musieliśmy obsługiwać wszystkich trzech dostawców (Intel, AMD, NVidia). Dodatkowo na iOS – na szczęście! – pominęliśmy *pierwszą* wersję iOS, w której Metal był dostępny, czyli iOS 8, a na macOS nie było to praktyczne, ponieważ mielibyśmy wtedy zbyt mało użytkowników korzystających z Metal. Z powodu połączenia tych problemów napotkaliśmy znacznie więcej problemów ze sterownikami zarówno w stosunkowo nieszkodliwych, jak i stosunkowo mało znanych obszarach API na macOS.
Nadal obsługujemy wszystkie wersje macOS Metal (10.11+), chociaż zaczęliśmy wycofywać obsługę i przechodzić na starszy backend OpenGL dla niektórych wersji z znanymi błędami kompilatora shaderów, które trudno nam obejść, np. w 10.11 wymagamy teraz macOS 10.11.6, aby Metal działał.
Korzyści w zakresie wydajności były zgodne z naszymi oczekiwaniami; jeśli chodzi o udział w rynku, obecnie na platformie macOS mamy około 25% użytkowników OpenGL i około 75% użytkowników Metal, co jest całkiem zdrowym podziałem. Oznacza to, że w przyszłości praktycznym rozwiązaniem może być całkowite zaprzestanie obsługi OpenGL na komputerach stacjonarnych, ponieważ żadna inna platforma, którą obsługujemy, nie korzysta z niego, co jest świetnym rozwiązaniem, pozwalającym nam skupić się na interfejsach API, które są łatwiejsze w obsłudze i zapewniają dobrą wydajność.
Iteracja w zakresie wydajności i zużycia pamięci
Od dawna jesteśmy dość konserwatywni, jeśli chodzi o funkcje API grafiki, z których korzystamy, a Metal nie jest tu wyjątkiem. Na przestrzeni lat Metal zyskał kilka dużych aktualizacji funkcji, w tym ulepszone API alokacji zasobów z jawnymi stertami, shadery kafelkowe w Metal 2, bufory argumentów i generowanie poleceń po stronie GPU itp.
W większości nie korzystamy z żadnej z nowszych funkcji. Jak dotąd wydajność była zadowalająca i chcielibyśmy skupić się na ulepszeniach, które mają zastosowanie we wszystkich obszarach, więc coś takiego jak shadery kafelkowe, które wymagają od nas wdrożenia bardzo specjalnej obsługi w całym rendererze i są dostępne tylko na nowszym sprzęcie, jest mniej interesujące.
Mimo to poświęcamy trochę czasu na dostrajanie różnych części backendu, aby po prostu działał *szybciej* – wykorzystujemy całkowicie asynchroniczne przesyłanie tekstur, aby zmniejszyć zacinanie się podczas ładowania poziomów, co było całkowicie bezbolesne, przeprowadzamy wspomniane wcześniej optymalizacje pamięci na macOS, optymalizujemy dysponowanie procesorem w różnych miejscach backendu poprzez zmniejszenie liczby nieudanych odwołań do pamięci podręcznej itp., oraz – jedna z niewielu nowszych funkcji, dla których mamy wyraźne wsparcie – korzystanie z bezpamięciowego przechowywania tekstur, gdy jest to możliwe, aby znacznie zmniejszyć ilość pamięci wymaganej dla naszego nowego systemu cieni.
Przyszłość
Ogólnie rzecz biorąc, fakt, że nie musieliśmy poświęcać zbyt wiele czasu na ulepszenia Metal, jest w rzeczywistości dobrą rzeczą – kod napisany 3 lata temu, ogólnie rzecz biorąc, działa, jest szybki i stabilny, co jest świetnym znakiem dojrzałego API. Przeniesienie na Metal było świetną inwestycją, biorąc pod uwagę ilość czasu, jaką to zajęło, oraz ciągłe korzyści, jakie daje nam i naszym użytkownikom.
Nieustannie na nowo oceniamy równowagę między nakładem pracy, jaki wkładamy w różne API – jest bardzo prawdopodobne, że w przyszłych projektach związanych z renderowaniem będziemy musieli zagłębić się w nowocześniejsze elementy API Metal; jeśli tak się stanie, na pewno napiszemy o tym kolejny post!
- No dobrze, i może tydzień na naprawienie kilku błędów wykrytych podczas testów ↩
- Liczby dotyczą 128 KB danych aktualizowanych na klatkę (dwa regiony 32x16x32 RGBA8) na A10 ↩
Ani Roblox Corporation, ani ten blog nie promują ani nie wspierają żadnej firmy ani usługi. Ponadto nie udziela się żadnych gwarancji ani obietnic dotyczących dokładności, wiarygodności lub kompletności informacji zawartych w tym blogu.
Ten wpis na blogu został pierwotnie opublikowany na blogu Roblox Tech Blog.


