Czas ładowania gry i doczytywania jej poszczególnych poziomów nie jest tak ważny, jak wydajność w czasie samej rozgrywki, ale również ma znaczenie. Chcąc jak najbardziej przyspieszyć proces wczytywania danych stosowane bywa wiele różnych technik - począwszy od użycia odpowiednich funkcji systemowych wejścia/wyjścia, poprzez nieparsowanie i niekonwertowanie niczego na etapie ładowania tylko przygotowanie wszystkiego od razu w formacie docelowym, aż po dosłowną, binarną serializację obiektów gry, nawet o skomplikowanej strukturze powiązanej wskaźnikami.
Zastanawiając się nad wyborem formatu graficznego najlepszego do zapisania tekstur w grze postanowiłem przeprowadzić eksperyment, aby sprawdzić, które z formatów wczytywane są najszybciej przez funkcje D3DX z biblioteki Direct3D 9. Myślę, że wyniki tego eksperymentu mogą być pewną małą cegiełką w dalszej optymalizacji czasu wczytywania gry.
Do eksperymentu przygotowałem teksturę o rozmiarze 2048 x 2048 pikseli wypełnioną czterema różnego rodzaju obrazami - zdjęciem krajobrazu, schematem, tapetą i teksturą cegieł. Tam gdzie format pliku to wspierał, tam kanał alfa wypełniony był kopią obrazu w odcieniach szarości. Tekstura była tak duża, aby czas wczytywania był znaczny (dzięki temu pomiar obraczony mniejszym błędem) oraz aby jak najmniejszy wkład do niego miały czynniki stałe obejmujące wszelkie czynności przygotowawcze przeprowadzane przez DirectX. W pomniejszeniu tekstura wyglądała tak:
Teksturę zapisałem w kilku różnych formatach graficznych:
Gdzie:
Plik JPEG zapisany był w jakości 85%.
Program testowy napisany został w Visual C++ 2005 Professional z użyciem Direct3D 9 (November 2007). Program był oczywiście skompilowany w trybie Release (nie Debug) i uruchomiony bez debuggera, a DirectX przełączony był w oknie dxcpl na tryb Retail (nie Debug).
Program testowy wczytywał jednokrotnie każdy plik. Przed każdym kolejnym pomiarem ze zmienionymi parametrami wykonywany był restart systemu, aby zminimalizować ryzyko pozostania treści plików w pamięci podręcznej systemu, co znacznie skróciłoby mierzony czas wczytywania pliku i zafałszowało pomiary.
Wywołanie wczytywania tekstury używało, zależnie od wybranej metody, funkcji D3DXCreateTextureFromFileEx lub D3DXCreateTextureFromFileInMemoryEx i wyglądało podobnie do tego kodu:
HRESULT hr = D3DXCreateTextureFromFileEx( frame::Dev, // pDevice FILES[i], // pSrcFile D3DX_DEFAULT, // Width D3DX_DEFAULT, // Height g_MipLevels, // MipLevels 0, // Usage g_Format, // Format g_Pool, // Pool D3DX_DEFAULT, // Filter D3DX_DEFAULT, // MipFilter 0, // ColorKey NULL, // pSrcInfo NULL, // pPalette &Texture); // ppTexture
Do precyzyjnego odmierzania czasu użyte zostały funkcje systemowe QueryPerformanceFrequency i QueryPerformanceCounter, a prawidłowy pomiar na maszynie dwurdzeniowej zapewniony został poprzez użycie funkcji SetThreadAffinityMask.
Tabela z wynikami: Data.png (186 KB)
Podstawowym celem eksperymentu było porównanie czasu wczytywania tej samej tekstury zapisanej w różnych formatach graficznych. Wczytywanie odbyło się z domyślnymi parametrami:
Pomiar był powtarzany 3 razy, a z wyników wyliczone zostały średnie.
Jak widać na wykresie powyżej, najszybsze okazało się wczytanie tekstury w formacie JPEG (zapewne ze względu na bardzo mały rozmiar pliku, a pomimo konieczności dokonania skomplikowanej dekompresji) oraz tekstury w formatach BMP i DDS zapisane z 16-bitowym formatem piksela (dzięki któremu również ich pliki zajęły stosunkowo mało miejsca). Najwięcej czasu na wczytanie potrzebowały z kolei tekstury w formatach PNG i TGA, ponieważ te pliki były stosunkowo duże, a bezstratna kompresja stosowana w PNG i w TGA z włączonym RLE niewiele mogły wskurać na teksturze przestawiającej taki obraz jak ten użyty tutaj.
Porównanie czasu wczytywania poszczególnych plików z ich rozmiarem potwierdza przypuszczenie, że rozmiar pliku ma decydujący wpływ na czas wczytywania tekstury. Jednak nie do końca. Tekstury zapisane w formacie DDS wypadły lepiej niż sugerowałby to ich rozmiar - najprawdopodobniej dlatego, że jest to natywny format tekstur Direct3D i nie było potrzebne podczas wczytywania żadne przetwarzanie. Z drugiej strony wczytanie tekstur w formatach PNG i TGA z kompresją RLE zajęło bardzo dużo (najwięcej) czasu mimo że ich pliki nie są największe. Widocznie dekompresja zajmuje dużo czasu (mimo że eksperyment przeprowadzony był na stosunkowo nowoczesnej maszynie). Wyjątkiem jest w tym kontekście format JPEG, który mimo najwyższego stopnia kompresji wczytywał się najkrócej.
Porównane zostały trzy metody wczytywania tekstur:
Wczytanie danych najpierw do bloku pamięci, a dopiero potem do tekstury było w większości przypadków wolniejsze, niż wczytanie tekstury prosto z pliku za pomocą odpowiedniej funkcji D3DX. Z kolei wczytanie tekstury z pliku skompresowanego było prawie zawsze znacząco szybsze. To świadczy o sensie używania VFS. Ten fakt nie jest zaskakujący - wszak odczyt danych z dysku jest zawsze wolniejsze niż przetwarzanie danych przez procesor, dlatego warto wczytywać dane skompresowane. Wyjątkiem jes tutaj jedynie format PNG, który stosuje ten sam algorytm kompresji co Gzip (deflate, obsługiwany przez bibliotekę zlib). Dlatego ponowna kompresja tak zapisanej tekstury nie przyniosła żadnego zysku i podczas jej wczytywania dał o sobie znać dodatkowy narzut na niepotrzebną dekompresję.
Aby dokładniej zbadać, jaki procent czasu wczytywania tekstury potrzebny jest na odczytanie pliku z dysku, a jaki na przetworzenie danych przez procesor, te dwa etapy potraktowane zostały osobno. Czas wczytywania plików ma jak widać dominującą rolę w sumarycznym czasie. Wyjątkiem są formaty JPEG i PNG, których pliki zajęły najmniej miejsca, ale potrzebowały dużo przetwarzania. Najmniej przetwarzania wymaga format DDS.
W przypadku wczytywania tekstur z plików skompresowanych jako archiwa GZ, czas potrzebny na ich odczyt z dysku (liczony tu wraz z czasem dekompresji formatu Gzip, gdyż była to jedna i ta sama czynność) ulega znacznemu skróceniu. To jeszcze raz potwierdza, że bezwzględnie warto stosować VFS kompresujący wszelkie dane.
Dodatkowy eksperyment polegał na zmierzeniu, jaki wyływ na czas ładowania tekstur ma jawne zadeklarowanie formatu, w jakim utworzona ma zostać w pamięci tekstura. Wynik nie jest zaskakujący. Czas wczytywania był zdecydowanie krótszy, kiedy tekstura wczytywana była do takiego formatu piksela, jaki był też używany w danym pliku, ponieważ nie była wtedy potrzebna konwersja każego piksela obrazu. Najmniejszy czas ładowania miało praktycznie zawsze wywołanie pozwalające na automatyczne wybranie formatu tekstury na podstawie pliku.
Ustawienie MipLevels na DEFAULT oznacza wygenerowanie wszystkich poziomów MipMap dla wczytywanej tekstury (lub wczytanie, jeśli plik je zawiera). Ustawienie 1 oznacza brak generowania innych niż podstawowy poziomów MipMap. Ta druga opcja jest oczywiście szybsza, ale różnica jest nieznaczna. Z tego wynika, że generowanie MipMap nie jest czynnością czasochłonną w porównaniu z całym wczytywaniem tekstury.
Wyjątkiem są pliki DDS zawierające już zapisane w sobie MipMapy. Ich wczytywanie wraz z tymi MipMapami było znacznie dłuższe z pewnością dlatego, że z powodu zawierania MipMap rozmiar tych plików był większy. Z tego wniosek, że lepiej chyba pozostawić generowanie poziomów Mip karcie graficznej, niż zapisywać je w pliku. To jeszcze raz potwierdza zasadę "procesor jest szybszy niż dysk".
Pula pamięci | Średni czas [ms] |
---|---|
MANAGED | 333,39 |
DEFAULT | 348,44 |
SYSTEMMEM | 333,99 |
Ostatni pomiar miał na celu stwierdzenie, jaki wpływ na czas wczytywania tekstur ma docelowa pula pamięci tekstury. Ten wpływ okazał się niewielki. Wczytywanie do puli zarządzanej (MANAGED) jest praktycznie tak samo szybkie jak do pamięci RAM (SYSTEMMEM). Można to wytłumaczyć w ten sposób, że w przypadku puli zarządzanej dane również ładowane są tylko do pamięci RAM, a do karty graficznej trafiają dopiero kiedy są potrzebne. Z kolei wczytanie tekstury do pamięci karty graficznej (pula DEFAULT) było nieznacznie dłuższe.
Przeprowadzony eksperyment nie wyczerpuje oczywiście wszystkich możliwych kombinacji sposobu zapisywania tekstur w plikach i parametrów ich wczytywania. Poza tym otrzymane wyniki zależą od bardzo wielu czynników (program testujący, biblioteka DirectX, system operacyjny). Mimo tego myślę, że można wysnuć z niego kilka wniosków.