這能解決哪些問題?

由於記憶體的限制,我們在創造遊戲玩法的過程中總是需要整合紋理內容。可能是出於複雜的3d世界的約束,開發者必須將物件順暢地整合進關卡中,或者在我們自己的案例中,為了使用2d渲染,我們就必須整合進更多紋理(我們預先渲染了遊戲世界,物件和角色動畫的紋理集,並將其對映到布告牌上以獲得更高質量的視覺效果,燈光以及更復雜的環境/角色)。

如果你的問題是以毫秒的速度在3d世界中載入紋理,你便可以使用壓縮PVR紋理去解決這一問題。除此之外關於這一方法還存在其它更棒的效能優勢。

如果你和我一樣是為了呈現布告牌紋理去載入這些內容,你便可以基於無損耗模式,如PNG(最普遍的一種),BMP,TGA或未壓縮的PVR去載入它們。基於本篇文章所提及的技術,你便能夠通過改變你所使用的格式或載入方式而獲得大量的效能優勢。

步驟1:檔案格式

如果你打算在一個3d世界中使用物件紋理,你便可以選擇PVRTC格式,因為這是一種有損耗且固定的紋理壓縮格式,同時支援4bpp和2bpp的ARGB資料。

而對於無損耗紋理,你的最佳選擇則是未壓縮的PVR檔案格式。這一格式包含了PVR檔案標題以及基於RGBA4444,RGBA8888或者BGRA8888等格式的資料。如果你使用的是32bit資料,你就沒有理由使用RGBA8888格式,因為驅動程式將處理其中的cpu內容,這時候你可以預先將資料轉變成BGRA8888格式,從而將其直接上傳至記憶體中。

Apple_A6(from macdailynews)

Apple_A6(from macdailynews)

註釋

–如果你使用RGB8資料格式去處理CPU,那便等於你在執行一些無用功,因為你必須為此新增一個填充位元組,所以你必須確保始終使用RGBA/BGRA格式,即使你並不需要使用這一渠道。

–使用未壓縮紋理格式意味著將佔據更多磁碟空間。雖然這不會影響你的應用下載規格(IPA/APK則會壓縮你的內容),但是在安裝時卻會佔用更多的磁碟空間。如果你發現磁碟空間的使用已經超出了自己的預想,你便可以將紋理整合進.zip格式的程式包中,在初始載入時壓縮你所需要的內容,並在應用終止時刪除這些內容。

步驟2:檔案I/O

在我閱讀過的大多數網上文章或者別人所編寫的程式碼,我發現他們都遵循著相同的紋理資料載入過程,即開啟一份檔案,閱讀資料,根據需要壓縮資料,然後呼叫glTexImage2d(…) 並明確RGBA格式。如果你是預先載入所有紋理資料,那麼這種方法便非常有效,但是當你需要在遊戲執行過程中整合紋理內容,你便會遇到一些嚴重的瓶頸。同時你還需要想辦法避免一些不必要的分配/複製(基於格式可能會出現壓縮)操作——可能會影響你在建造框架時的時間分配。

一種方法便是使用記憶體對映檔案I/O。也就意味著檔案內容並不是從磁碟中讀取,並且也未使用實體記憶體,而是在記憶體空間經由OS進行快取,並在需要的時候置入或置出。這可能會造成檔案訪問的延遲,假設你的核心載入頁面所需要的平均時間為0.012毫秒(我甚至看過最長時間為0.135毫秒),但是如果它減少了記憶體分配/記憶體塊拷貝(將佔用記憶體頁面載入時間),你便能因此獲得較高的效能(更別說當你使用平臺分配記憶體/自由呼叫時無需再擔心記憶體碎片問題)。

為了做到這一點你可以遵循以下內容(為了簡化例子我省略了錯誤處理):

#include < sys/stat.h >

#include < sys/mman.h >
#include < fcntl.h >
#include < unistd.h >

int32_t file = open(“my_texture_file.pvr”, O_RDONLY);
struct stat file_status;
fstat(file, &file_status);
int32_t file_size = (int32_t)file_status.st_size;
void* data = mmap(0, file_size, PROT_READ, MAP_PRIVATE, file, 0);
// Note this will not close the file/mapping right now, as it will be held until unmapped.
close(file);

// When finished with this data you call this to unmap/close.
munmap(data, file_size);

現在我們已經創造一種虛擬地圖,並承諾只會在只讀訪問時使用這一地圖,從而幫助我們獲得了最優化的利益。

註釋:

大多數情況下,記憶體對映檔案只有在面對擁有比頁面大小多出幾倍規格(如4096位元組)的大型檔案時才能起作用,這也是為了避免浪費頁面空間。顯然很多情況下紋理都未能遵守這一規定,但是在我們的案例中這卻不會造成多大影響,我們也總是能夠獲得最佳效能。

步驟3:紋理載入

首先你需要從檔案中獲得PVR標題結構(遊戲邦注:包含了所有你所需要的資訊)。如此你便可以使用4CC去核實格式,並獲得所需的後設資料。當你獲得這一資料後你便可以將紋理資料載入到GPU以供使用。

以下方法能夠幫助你輕鬆地做到這一點(為了簡化例子我省略了錯誤處理,並在常量上做了假設等)

struct PvrHeader
{

uint32_t    header_length;
uint32_t    height;
uint32_t    width;
uint32_t    mipmap_count;
uint32_t    flags;
uint32_t    data_length;
uint32_t    bpp;
uint32_t    bitmask_red;
uint32_t    bitmask_green;
uint32_t    bitmask_blue;
uint32_t    bitmask_alpha;
uint32_t    pvr_tag;
uint32_t    surface_count;
};

static const uint32_t kPVRTC2 = 24;
static const uint32_t kPVRTC4 = 25;
static const uint32_t kBGRA8888 = 26;

PvrHeader* header = (PvrHeader*)data;    // data being your mapped file from step two
uint32_t pvr_tag = header->_pvr_tag;
// Here you would check the pvr_tag against the 4CC “PVR!” to verify

uint32_t flags = header->flags;
uint32_t format_flag = flags & 0xFF;

void* data_start = data + sizeof(PvrHeader);
if(format_flags == kBGRA8888)
{
// Note: I am assuming that you have already generated, bound, and set texture parameters.
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, header->width, header->height, 0, GL_BGRA, GL_UNSIGNED_BYTE, data_start);
}
else if(format_flags == kPVRTC4 || format_flags == kPVRTC2)
{
// You would do the same as above but using glCompressedTexImage2d(…);
}

步驟4:有效利用

這時候你便能夠根據自己的需要使用最有效的紋理格式,你擁有對映檔案,你可以使用核心去載入頁面並進行復制而將資料上傳到GPU。但是你應該不希望每次載入紋理時都重複這幾個步驟。為了達到高效利用,你需要在最初載入遊戲時對映所有紋理檔案,而以此建立一個對映紋理檔案快取(遊戲邦注:John Carmack發現在iOS平臺上開發者只擁有700 MB的空間,如果你需要更多空間,就要使用mmap/munmap更謹慎地管理快取)。當一個紋理需要你呼叫glTexImage2d並使用核心去載入頁面,並基於原生格式將資料上傳到GPU時,你就應該為其做好準備。當執行終止時,你可以通過取消所有檔案的對映而摧毀快取。

接下來呢?

根據你所整合的紋理數量以及它們的規格,你可以通過遵循上述解決方法而獲得最佳效能,儘管使用iOS 6有可能幫助你獲得更出色的效能。而對於那些願意執行這些步驟的人來說,我敢保證他們將無需經歷平常的繫結過程便能夠有效地載入紋理資料。這種方法能夠推動驅動程式發揮最佳功效,從而確保記憶體的有效管理,並避免記憶體分配或儲存殘片的出現。你可以建立更有效率的快取系統並預先完成GPU的記憶體分配,而無需重複勞作。

via:遊戲邦/gamerboom.com