Autor Wątek: Redukcja zużycia pamięci w grze - jakieś porady?  (Przeczytany 7460 razy)

Offline Vipa

  • Redaktor

# Kwiecień 14, 2014, 09:43:31
Absolutnie przerzucaj tekstury do VRAM. Tu nie ma dyskusji nawet. Kompresja jest i będzie, jeżeli to pixel art to zastosuj inną niż DXT1/3/5 (prosta zasada: 1 - alphy nie ma, 3 - alpha jest albo nie ma, 5 - alpha jest gradientem). Jako png trzymaj tylko (niektóre) normalmapy (o ile używasz).
Przy okazji to dałbyś przykładowego screena, bo tak to możemy strzelać. Platformówka platformówce nie równa.

Komputery teraz rzadko mają mniej niż 512 MB VRAM i raczej 64 bitowy OS. Chyba, że to projekt dla szkoły na komputery sprzed Quake'a. 512 MB VRAM, wliczając wykorzystanie OS, to dalej > 300 MB pamięci co jest ilością kosmiczną jak na platformówkę 2D, o ile animacja jest w ludzkim formacie, innym niż atlas 16 klatek animacji samego chodzenia. FMV nie stosowałem, ale pewnie nie wczytuje / dekompresuje jednej klatki na raz.

Co do samych atlasów, to o ile nie macie sporo elementów z alphą i nie dało by się gęściej dzięki atlasowi ich rozstawić, to faktycznie zostaje oszczędność na wydajności i wywołaniach. Jednakże nie traktuj tego jako oszczędność "tutaj" tylko jako dodatkowe zasoby dla "gdzie indziej".

Jeszcze uwaga. Niekiedy nasza grafika przekracza wymiary, w których jest prezentowana na ekranie i grafika 512x512 jest prezentowana jako mały boxik w rogu ekranu. Rozumiem HD i tak dalej, ale widziałem już przeginki totalne.

Często błąd to też stosowanie jednej tekstury dla lod. Przykład: mamy teksturę zamku, który na 1 levelu jest widoczny z daleka i dopiero na 3 levelu się do niego zbliżamy. A korzysta z tej samej tex. Od razu odpowiadam: tak, do 2D też stosuje się LOD :).

Kanały. Jeżeli tex przedstawia czerwoną krechę, to zostają 2-3 kanały niewykorzystane. Często różne efekty korzystają z takich tekstur, a można je zrobić jako jedną teksturę wykorzystując wszystkie kanały.

Popatrz po strukturach i klasach. Może jest coś co w trakcie gry rozbudowuje się do kosmicznych rozmiarów, a można zaoszczędzić stosując inne typy danych. Choć 500 MB zawalić jedną tablicą to raczej niemożliwe :).

Offline Mr. Spam

  • Miłośnik przetworów mięsnych

Offline Xirdus

  • Redaktor

# Kwiecień 14, 2014, 21:27:11
Dzięki wszystkim za porady.

Co do kompresji, to myślałem, żeby użyć DXT3, który jest natywnie wspierany przez OpenGL (niech mnie ktoś poprawi jak się mylę) i byłby dekodowany sprzętowo (tutaj też), a potem przejechać jakimś shaderem w celu wygładzenia krawędzi, ale problem w tym, że SDL2 nie wspiera ani skompresowanych tekstur, ani własnych shaderów. Tak więc ten pomysł do rozważenia w razie kompletnej reimplementacji silnika.

Co do trzymania pamięci w VRAMie - to znów zależy od SDL-a; nie wiem jak on to obsługuje, i jeśli robi to źle, to nic nie poradzę (chyba że napiszę wszystko od nowa - co poważnie rozważam, bo kod jest przeokropny).

Leniwe wczytywanie mogłoby być dobre, tylko że jak się wgłębiłem w kod, to jest on pełen wskaźników, zbyt rozbudowanych hierarchii klas, nigdzie nie sprawdzanych nieoczywistych zależności oraz zmiennych globalnych, za to enkapsulacji i podziału odpowiedzialności brak - po prostu nie wiem, jak zapewnić stabilne działanie i szybkie czyszczenie pamięci jednocześnie.

Ciasnego pakowania tekstur nie chcę, bo prawie każda grafika ma margines i miejsce wyświetlenia mocno od niego zależy - nie chce mi się tego wszędzie poprawiać. Może przy przepisaniu silnika od nowa.

LOD się nie przyda, bo nigdzie nie ma żadnego skalowania - wszystkie grafiki są wyświetlane w takim rozmiarze w jakim zostały zaczytane.

Gperftools ma strasznie problematyczną kompilację (spod Netbeansa z TDM-GCC nie działa, bo nie wykrywa pthreads i mam konflikt deklaracji). gprof zaraz wypróbuję. Jeszcze zrobię eksperyment, na ile pamięciożerne jest samo otworzenie archiwum w miniz.

Parę screenów dla ciekawskich:
http://i.imgur.com/zN5IdY4.png
http://i.imgur.com/ZsX25F8.png
I to jest przygodówka, Vipa, nie platformówka ;)

Offline Vipa

  • Redaktor

# Kwiecień 14, 2014, 22:22:02
Cytuj
I to jest przygodówka, Vipa, nie platformówka ;)
Heh, gdzie to ja posiałem okulary :).
Z tego co piszesz to faktycznie lipa :(. Może przepisz sam renderer? W sensie same wywołania wczytu / kasacji i wyświetlania? Kiedyś wszystkie projekty przerabiałem z SDLa i tylko na pierwszy rzut oka wygląda to na czasochłonne.
Normalnie poleciłbym ci odpalenie jakiegoś popularnego silnika i zrobienie gry na nim od nowa, ale nie wiem na ile skomplikowana to produkcja, a chodzi w sumie tylko o pamięć.

Offline Estivo

  • Użytkownik
    • Blog

# Kwiecień 14, 2014, 22:32:59
Vipa tylko i az. To jak by konstruktor silnika powiedzial - a on tylko pali 2 litry na kilometr, ale co tam to tylko spalanie.

Offline albireo

  • Użytkownik

# Kwiecień 14, 2014, 23:45:02
Patrząc na te screeny, to aż prosi się to o wektorówkę.

Offline kalicjusz

  • Użytkownik
    • Kidler3D

# Kwiecień 15, 2014, 00:35:50
Cytuj
Co do kompresji, to myślałem, żeby użyć DXT3, który jest natywnie wspierany przez OpenGL (niech mnie ktoś poprawi jak się mylę) i byłby dekodowany sprzętowo (tutaj też), a potem przejechać jakimś shaderem w celu wygładzenia krawędzi, ale problem w tym, że SDL2 nie wspiera ani skompresowanych tekstur, ani własnych shaderów. Tak więc ten pomysł do rozważenia w razie kompletnej reimplementacji silnika.

To może taka propozycja... Jeśli w tym kodzie istnieje obiekt Texture to wtedy wystarczy dodać metodę odpowiedzialną za ładowanie plików DDS. SDL ładowałby nieskompresowane obrazy, a skompresowane DDS'y ładowane by były oddzielnie poza SDL za pomocą tej metody. Taki kod do ładowania DDS'ów jest bardzo prosty, posiadam taki i mogę się podzielić jeśli będzie przydatny. :)
« Ostatnia zmiana: Kwiecień 15, 2014, 00:38:26 wysłana przez kalicjusz »

Offline Lerhes

  • Użytkownik

# Kwiecień 15, 2014, 00:48:09
Skoro projekt jest open source... to daj do tego kodu linka :)
Nawet z czystej ciekawości rzucę na niego okiem ^^

Lerhes

Offline kalicjusz

  • Użytkownik
    • Kidler3D

# Kwiecień 15, 2014, 00:59:23
texturedds.h
#include <GL/glew.h>

GLuint LoadDDSTexture(const char * imagepath, float maxAnisotropy, bool clampToEdge);

texturedds.cpp
#include "texturedds.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define FOURCC_DXT1 0x31545844 // Equivalent to "DXT1" in ASCII
#define FOURCC_DXT3 0x33545844 // Equivalent to "DXT3" in ASCII
#define FOURCC_DXT5 0x35545844 // Equivalent to "DXT5" in ASCII

GLuint LoadDDSTexture(const char * imagepath, float maxAnisotropy, bool clampToEdge)
{

unsigned char header[124];

FILE *fp;
 
/* try to open the file */
fp = fopen(imagepath, "rb");
if (fp == NULL)
return 0;
   
/* verify the type of file */
char filecode[4];
fread(filecode, 1, 4, fp);
if (strncmp(filecode, "DDS ", 4) != 0) {
fclose(fp);
return 0;
}

/* get the surface desc */
fread(&header, 124, 1, fp);

unsigned int height      = *(unsigned int*)&(header[8 ]);
unsigned int width      = *(unsigned int*)&(header[12]);
unsigned int linearSize = *(unsigned int*)&(header[16]);
unsigned int mipMapCount = *(unsigned int*)&(header[24]);
unsigned int fourCC      = *(unsigned int*)&(header[80]);

 
unsigned char * buffer;
unsigned int bufsize;
/* how big is it going to be including all mipmaps? */
bufsize = mipMapCount > 1 ? linearSize * 2 : linearSize;
buffer = (unsigned char*)malloc(bufsize * sizeof(unsigned char));
fread(buffer, 1, bufsize, fp);
/* close the file pointer */
fclose(fp);

unsigned int components  = (fourCC == FOURCC_DXT1) ? 3 : 4;
unsigned int format;
switch(fourCC)
{
case FOURCC_DXT1:
format = GL_COMPRESSED_RGBA_S3TC_DXT1_EXT;
break;
case FOURCC_DXT3:
format = GL_COMPRESSED_RGBA_S3TC_DXT3_EXT;
break;
case FOURCC_DXT5:
format = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
break;
default:
free(buffer);
return 0;
}

// Create one OpenGL texture
GLuint textureID;
glGenTextures(1, &textureID);

// "Bind" the newly created texture : all future texture functions will modify this texture
glBindTexture(GL_TEXTURE_2D, textureID);
glPixelStorei(GL_UNPACK_ALIGNMENT,1);

unsigned int blockSize = (format == GL_COMPRESSED_RGBA_S3TC_DXT1_EXT) ? 8 : 16;
unsigned int offset = 0;

/* load the mipmaps */
for (unsigned int level = 0; level < mipMapCount && (width || height); ++level)
{
unsigned int size = ((width+3)/4)*((height+3)/4)*blockSize;
glCompressedTexImage2D(GL_TEXTURE_2D, level, format, width, height, 
0, size, buffer + offset);

offset += size;
width  /= 2;
height /= 2;
}

free(buffer);

if (clampToEdge)
{
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
}

        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

if (maxAnisotropy != 0.0f)
{
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, maxAnisotropy);
}

return textureID;

}

Offline ArekBal

  • Użytkownik

# Kwiecień 15, 2014, 11:44:18
Patrząc na te screeny, to aż prosi się to o wektorówkę.
Albo porządną kompresję ala distance fields + palety.

Efekt wizualny i memory footprint lepszy od DXT o 4 razy?
Roboty sporo więcej...
« Ostatnia zmiana: Kwiecień 15, 2014, 11:49:46 wysłana przez ArekBal »

Offline laggyluk

  • Użytkownik
    • http://laggyluk.com

# Kwiecień 15, 2014, 11:49:04
patrząc po grafikach to uszło by w 256 kolorach ;)

Offline ArekBal

  • Użytkownik

# Kwiecień 15, 2014, 11:53:32
No tak myślę że i mniejszą paletę dało by się zrobić.

Na teksturze distance field to mniej niż jeden kanał. ;)

Takie trudne to to znowu też nie jest...
Jak ktoś ma ogarnięte shadery to da radę concept całości w jedną noc zlepić.


Offline Xirdus

  • Redaktor

# Kwiecień 15, 2014, 13:25:43
Skoro projekt jest open source... to daj do tego kodu linka :)
Nawet z czystej ciekawości rzucę na niego okiem ^^
http://www.equestriandreamers.com
https://github.com/GabuEx/my-little-investigations
Polecam zaczac od czegos innego niz main.cpp.

Patrząc na te screeny, to aż prosi się to o wektorówkę.
A jaki byłby narzut na CPU/GPU przy grafice wektorowej? Mam na myśli rasteryzację od nowa przy każdej klatce, nie rasteryzację przy wczytywaniu i trzymanie przetworzonej bitmapy, bo to się mija z celem...

To może taka propozycja... Jeśli w tym kodzie istnieje obiekt Texture to wtedy wystarczy dodać metodę odpowiedzialną za ładowanie plików DDS. SDL ładowałby nieskompresowane obrazy, a skompresowane DDS'y ładowane by były oddzielnie poza SDL za pomocą tej metody. Taki kod do ładowania DDS'ów jest bardzo prosty, posiadam taki i mogę się podzielić jeśli będzie przydatny. :)
Dalej pozostaje problem wygładzania krawędzi - na CPU tego nie będę robić, a do shaderów nie mam dostępu.

patrząc po grafikach to uszło by w 256 kolorach ;)
Tez o tym pomyslalem, ale sprawdzilem ze np. spritesheet animacji tej fontanny po lewej pierwszego screena ma czterdziesci iles tysiecy - wiec bez straty na jakosci sie nie da.

Offline ArekBal

  • Użytkownik

# Kwiecień 15, 2014, 14:10:34
No właśnie trzeba by renderować raz tak, raz siak.
Bez dostępu do shaderów tych najfajniejszych rzeczy nie zrobisz.
http://www.valvesoftware.com/publications/2007/SIGGRAPH2007_AlphaTestedMagnification.pdf
+
Tak jak pisałem palety kolorów dla koników... gradienty jeśli są na konikach to by musiały być osobno rysowane np. wektorowo... Trzeba grafikę by przejrzeć i zdecydować co jak renderować.
« Ostatnia zmiana: Kwiecień 15, 2014, 14:12:41 wysłana przez ArekBal »

Offline albireo

  • Użytkownik

# Kwiecień 15, 2014, 14:23:22
A jaki byłby narzut na CPU/GPU przy grafice wektorowej? Mam na myśli rasteryzację od nowa przy każdej klatce, nie rasteryzację przy wczytywaniu i trzymanie przetworzonej bitmapy, bo to się mija z celem...
Ciężko powiedzieć jaki będzie narzut, to mocno zależy od grafiki którą chce się rasteryzować (w szczególności jak dużo elementów jest rysowane, jaki overdraw), dla tych przykładowych screenów powinno działać płynnie. No i można też użyć też pośredniego rozwiązania, te elementy które się często nie zmieniają wyrenderować raz, a później używać wcześniej wyrenderowanych (a jak nie będzie już potrzebne to zwolnić).