Жили себе поживали на пк и проблем с памятью не знали, но тут приходит тренд мобильных игр и всё говно всплыло
Наша история сегодня о функции glTexImage2D (и glCompressedTexImage2D тоже), алгоритм работы с ней прост и понятен, казалось бы :
1) открываем файл с помощью libpng, libjpeg, или еще чего-то, есть способы как с полной предварительной загрузкой файла в память, так и с переопределением функций чтения файла для библиотеки загрузки
2) получаем указатель на данные текстуры в памяти
3) кидаем указатель в glTexImage2D
4) освобождаем данные по указателю
5) PROFIT !
А теперь давайте подумаем об очевидных вещах, все мы отдаём себе отчёт что у нас тут есть промежуточный буфер для целого изображения, когда оно 2048*2048*rgba то это 16 мегабайт, и время жизни этого буфера весьма малое, от загрузки из файла до загрузки в opengl, буквально 100-200 мс
Ладно если это на пк то ничего сильно плохого такой подход не вносит кроме фрагментации и небольшой потери скорости, но на мобильниках (ios и android, особенно последнее) часто бывает ситуация, когда у нас на всю игру 50-70 мегабайт и места для буфера на 16 метров просто нет !
(в случае превышения на ios ваше приложение просто грохнут из-за отсутствия swap файла)
Давайте глянем нагляднее : (скриншот из профайлера под ios)
В пункте 1 происходит инициализация приложения, движка, игры и прочей мелочи. В пункте 2 начинается загрузка текстур (зачастую самые большие ресурсы в мобильных играх), как видим количество занимаемой памяти резко подскочило (создавались временные буфера для изображений), конкретно в этом тесте грузились 5 изображений 1024*1024 формата tga на ipad 3. В пункте 3 уже начала работать игра, как видим количество выделенной памяти резко поехало вниз.
А теперь как решать всё это, как мы знаем с появлением защищенного режима (i386 привет), мы перешли от сегментной адресации памяти к страничной памяти, страничная память хороша тем что есть page fault, а как мы знаем page fault происходит когда странички то в памяти нету, а лежит она где-то в недоступном месте. Хороший, приятный, быстро работающий в ring0 механизм. А теперь прикинемся шлангом, и скажем системе чтобы страницей был файл, да, физический файл может быть страницей виртуальной памяти в выгруженном состоянии, в чём разница то ? По-сути для приложения это выглядит как указатель, мы же не знаем что на деле за тем указателем лежит, а ring0 уже сам обрабатывает page fault и грузит при этом файл с диска тоже сам. Всё это называется
memory mapped file.
Конечно мы не изобрели это, мы всего лишь будем использовать такой механизм чтобы надурить OpenGL, ведь glTexImage2D принимает указатель, значит всё что представляется как указатель должно подойти за хранилище данных. Только вот придется на диске хранить изображения без сжатия (или в аппаратном сжатии : dxt, pvrtc, и тд), но в последних тенденциях App Store подняли максимальный размер приложения доступного по 3G связи с 20 до 50 мбайт, так что овчина стоит выделки, особенно на андроидах где другого выхода нет.
Выглядит в коде это так : (даю сразу заготовку для win\posix решений)
#if defined(WIN)
#include <windows.h>
typedef struct
{
HANDLE f;
HANDLE m;
void * p;
} SIMPLE_UNMMAP;
void * simple_mmap(const char * filename, int * length, SIMPLE_UNMMAP * un)
{
HANDLE f = CreateFileA(filename, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
HANDLE m;
void *p;
if(!f) return NULL;
m = CreateFileMapping(f, NULL, PAGE_READONLY, 0,0, NULL);
if(!m) { CloseHandle(f); return NULL; }
p = MapViewOfFile(m, FILE_MAP_READ, 0,0,0);
if(!p) { CloseHandle(m); CloseHandle(f); return NULL; }
if(length) *length = GetFileSize(f, NULL);
if(un)
{
un->f = f;
un->m = m;
un->p = p;
}
return p;
}
void simple_unmmap(SIMPLE_UNMMAP * un)
{
UnmapViewOfFile(un->p);
CloseHandle(un->m);
CloseHandle(un->f);
}
#else
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
typedef struct
{
int fd;
int size;
void * p;
} SIMPLE_UNMMAP;
void * simple_mmap(const char * filename, int * length, SIMPLE_UNMMAP * un)
{
int fd = open(filename, O_RDONLY);
if(fd < 0)
return NULL;
fcntl(fd, F_NOCACHE, 1); // не кешируем мы наше чтение текстур, потому что читаем линейно один раз то
fcntl(fd, F_RDAHEAD, 1); // заставляем читать на перёд
struct stat statbuf;
if(fstat(fd, &statbuf) < 0)
return NULL;
void * p = mmap(0, statbuf.st_size, PROT_READ, MAP_SHARED, fd, 0);
if(length)
*length = statbuf.st_size;
if(un)
{
un->fd = fd;
un->size = statbuf.st_size;
un->p = p;
}
return p;
}
void simple_unmmap(SIMPLE_UNMMAP * un)
{
munmap(un->p, un->size);
close(un->fd);
}
#endif
..........................
SIMPLE_UNMMAP mmap;
int mmapLength;
simple_mmap("image.raw", &mmapLength, &mmap);
// тут вам надо самим узнать размер и формат вашего изображения, для этого у меня есть небольшой заголовок в начале моего файла
glTexImage2D(.........., mmap.p);
simple_unmmap(&mmap);
Как видим реализация в общем проста и банальна, кроме разве что некоторых флагов для POSIX.
Еще Кармак писал
http://www.bethblog.com/2010/10/29/j...padipod-touch/ что F_NOCACHE под ios работает как надо, в итоге должно обеспечить максимальную производительность линейного одноразового чтения.
В итоге результат можно видеть на картинке :
Как видим мы избавились от критичного места - пика использования памяти при загрузке, но как плюс получили увеличении скорости загрузки по сравнению с обычным способом ! Увеличение в среднем на 20-50%, точные тесты не проводил.
ps. Геймдев это сфера разработки realtime софта, так что давайте и подходить к нему как в realtime сфере - у нас не игра, а программно-апаратный комплекс, у узлов есть свои особенности и ими нужно пользоваться и задавать вопросы, вплоть до "
Почему трансатлантический пинг быстрее, чем вывод пиксела на экран"