テクスチャキャッシュ2013/11/10

テクスチャを描画するときデータはVRAMに転送して使用しますが、VRAMの容量には限りがあります。アプリの規模がある程度大きくなり、一度に全てのテクスチャをVRAMに乗せられなくなると、場面によってVRAM上のテクスチャデータを入れ替える必要が出てきます。
場面がはっきりと分かれていて、一つ一つの場面で使用するテクスチャが十分小さい場合は自前でテクスチャを入れ替えても良いのですが、なかなかそうもいかなかったり、面倒だったりするのでテクスチャキャッシュという機構を使ってシステムにやらせてみます。

※"テクスチャキャッシュ"というと、VRAM上でキャッシュに乗せてGPUが高速にアクセス…みたいな話だったりしますが、ここではVRAMに乗せるテクスチャ自体をキャッシュするということになります。


テクスチャキャッシュ概要
・テクスチャが必要なとき(描画時)、キャッシュにそのテクスチャがあるか検索する。
・キャッシュにあった場合、すでにVRAMに読み込まれているのでそれを使用する。また、キャッシュ上での優先度を上げる。
・キャッシュに無かった場合、テクスチャをVRAMに読み込み、優先度を上げてキャッシュに登録する。このとき、登録されているテクスチャの総容量(枚数、またはサイズ)がキャッシュの容量を超えていた場合、優先度の低いテクスチャをVRAMとキャッシュから削除する。

Texture.h
/**************************
    テクスチャキャッシュ
 **************************/
class TexCache : public Texture
{
	static TexCache**	cache;				// キャッシュ
	static int			cache_num;			// キャッシュ数
	static u32			cache_mem_size;		// キャッシュ使用メモリサイズ

	static void		clear_cache(void);		// 最後尾のテクスチャを削除

public :

// リソース種類
enum
{
	RES_MEMORY	= 0,		// メモリ
	RES_ASSET,				// assetsファイル
};

	static void			init(void);							// 初期化
	static void			quit(void);							// 削除
	static Texture*		get_texture(short, const void*);	// テクスチャ取得

	short			type;			// リソース種類
	const void*		data;			// リソースデータ
	u32				mem_size;		// VRAM占有サイズ

		TexCache(short, const void*);		// コンストラクタ
};

Texture.cpp
TexCache**	TexCache::cache;				// キャッシュ
int			TexCache::cache_num;			// キャッシュ数
u32			TexCache::cache_mem_size;		// キャッシュ使用メモリサイズ

/**********************
    キャッシュ初期化
 **********************/
void	TexCache::init(void)
{
	cache			= new TexCache *[TEX_CACHE_NUM];		// キャッシュ作成
	cache_num		= 0;
	cache_mem_size	= 0;
}

/********************
    キャッシュ削除
 ********************/
void	TexCache::quit(void)
{
	for (int i = 0; i < cache_num; i++) {			// テクスチャ削除
		delete	cache[i];
	}
	delete[]	cache;
}

/****************************************
    テクスチャ取得
		引数	_type = リソース種類
				_data = リソースデータ
 ****************************************/
Texture*	TexCache::get_texture(short _type, const void* _data)
{
	int		i, j;

	for (i = 0; i < cache_num; i++) {
		if ( (_data == cache[i]->data) && (_type == cache[i]->type) ) {		// 登録済み
			if ( i > 0 ) {
				TexCache*	_tmp = cache[i];

				for (j = i; j > 0; j--) {
					cache[j] = cache[j - 1];
				}
				cache[0] = _tmp;					// 先頭に登録
			}
			return	(Texture*)cache[0];
		}
	}

	// 新規登録
	if ( cache_num == TEX_CACHE_NUM ) {				// 枚数オーバー
		clear_cache();
	}
	j = cache_num/2;
	for (i = cache_num; i > j; i--) {
		cache[i] = cache[i - 1];
	}
	cache[j] = new TexCache(_type, _data);			// リソース読み込み
	cache_num++;
	cache_mem_size += cache[j]->mem_size;
	while ( cache_mem_size > TEX_CACHE_MEM ) {		// メモリサイズオーバー
		clear_cache();
	}
	return	(Texture*)cache[j];
}

/******************************
    最後尾のテクスチャを削除
 ******************************/
void	TexCache::clear_cache(void)
{
	cache_num--;
	cache_mem_size -= cache[cache_num]->mem_size;
	delete	cache[cache_num];						// テクスチャ削除
}

/****************************************
    リソース読み込み
		引数	_type = リソース種類
				_data = リソースデータ
 ****************************************/
TexCache::TexCache(short _type, const void* _data)
{
	type = _type;					// リソース種類
	data = _data;					// リソースデータ

	switch ( _type ) {
	  case RES_MEMORY :				// メモリ
		load_png((const u8*)_data);
		break;

	  case RES_ASSET :				// assetsファイル
		{
			u8*	_p = (u8*)load_asset((const char*)_data);

			load_png(_p);
			free(_p);
		}
		break;

	  default :
		assert(FALSE);
		break;
	}
	switch ( format ) {				// VRAM占有サイズ計算
	  case FORMAT_RGBA :
		mem_size = width*height*4;
		break;

	  case FORMAT_RGB :
		mem_size = width*height*3;
		break;
	}
}


・テクスチャ取得
テクスチャリソースの指定は、メモリ(アドレス指定)または assetsのファイル(ファイル名指定)で行います。
まず同じリソースのテクスチャをキャッシュから検索します。このとき処理速度のことを考えて、ファイルの一致はファイル名文字列の中身の比較ではなく、アドレスを比較することにしています。
キャッシュから見つかった場合(179行目から)、優先度を上げて(他のテクスチャの優先度を下げて、先頭に登録する)そのテクスチャを返します。
見つからなかった場合(191行目から)、はじめにキャッシュ上の総数がオーバーしていたときは優先度最低の一枚を削除します。
次にキャッシュ上の総数の半分の優先度で登録します。優先度を最高ではなく半分にしているのは、今キャッシュに無いテクスチャは一時的なものかもしれないということによります。この値が最も効率が良いのかはわかりません。
最後に、キャッシュ上の総サイズがオーバーしていたときは、サイズを下回るまで優先度の低いテクスチャから削除します。テクスチャを登録してから削除しているので一時的にVRAM上でのサイズがオーバーしているのですが、流れをわかりやすくするためにこうしています。


スプライトをテクスチャキャッシュに対応させます。

Sprite.cpp
/******************************************
    設定(リソース指定)
		引数	_type   = リソース種類
				_data   = リソースデータ
				_coord  = UV座標
				_origin = 原点位置
 ******************************************/
void	Sprite::set(short _type, const void* _data, SRect const* _coord, int _origin)
{
	res_type = _type;					// リソース設定
	res_data = _data;
	status   = 1;						// UV座標未設定

	texcoord[0][X] = (GLfloat)_coord->x;			// 仮UV座標
	texcoord[0][Y] = (GLfloat)_coord->y;
	texcoord[1][X] = (GLfloat)(_coord->x + _coord->w);
	texcoord[2][Y] = (GLfloat)(_coord->y + _coord->h);
	width	= _coord->w;				// 幅
	height	= _coord->h;				// 高さ
	ox = _origin;
}

void	Sprite::set(short _type, const void* _data, int _origin)
{
	res_type = _type;					// リソース設定
	res_data = _data;
	status   = 2;						// 幅・高さ未設定

	texcoord[0][X] = texcoord[2][X] = 0.0f;			// UV座標
	texcoord[0][Y] = texcoord[1][Y] = 0.0f;
	texcoord[1][X] = texcoord[3][X] = 1.0f;
	texcoord[2][Y] = texcoord[3][Y] = 1.0f;
	ox = _origin;
}

リソース指定によるスプライトの設定部分です。
テクスチャを読み込まないと決定しない値があるので、状態を statusに覚えておいて読み込んだ時に設定するようにします。

void	Sprite::draw(GLfloat* _vertex)
{
	if ( res_type >= 0 ) {			// テクスチャキャッシュ
		Renderer::use_shader(Renderer::SHADER_TEXTURE);		// シェーダ
		texture = TexCache::get_texture(res_type, res_data);		// テクスチャ取得
		texture->bind();									// テクスチャ
		if ( status != 0 ) {
			switch ( status ) {
			  case 1 :				// UV座標設定
				texcoord[0][X] /= texture->width;  texcoord[2][X] = texcoord[0][X];
				texcoord[0][Y] /= texture->height; texcoord[1][Y] = texcoord[0][Y];
				texcoord[1][X] /= texture->width;  texcoord[3][X] = texcoord[1][X];
				texcoord[2][Y] /= texture->height; texcoord[3][Y] = texcoord[2][Y];;
				break;

			  case 2 :				// 幅・高さ設定
				width  = texture->width;
				height = texture->height;
				break;
			}
			set_origin(ox);
			status = 0;
		}
		Renderer::set_texcoord(&texcoord[0][0]);			// UV座標
	}
	else if ( texture ) {			// テクスチャ有り
		Renderer::use_shader(Renderer::SHADER_TEXTURE);		// シェーダ
		texture->bind();									// テクスチャ
		Renderer::set_texcoord(&texcoord[0][0]);			// UV座標
	}
	else {							// テクスチャ無し
		Renderer::use_shader(Renderer::SHADER_PLAIN);		// シェーダ
	}
	sys::Renderer::set_vertex(_vertex);						// 頂点
	Renderer::set_color((GLubyte*)spr_color);				// カラー

	glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);					// 描画
}

描画部分です。
キャッシュからテクスチャを読み込んで、未設定の値を設定します。あとは通常の描画と同じです。


使用例です。
テクスチャキャッシュの容量を5枚(def.hの TEX_CACHE_NUMで定義)にして、11枚のテクスチャを扱っています。
キャッシュの枚数が5枚なのはあくまでテストのためであって、通常はもっと大きくするものです。少なくとも、1度に画面に出るテクスチャの枚数を超える必要があります。

AppMain.cpp

#include "common.h"
#include "Sprite.h"


// スプライト
enum
{
	SPR_PHOTO	= 0,			// 背景
	SPR_FISH,					// 魚
	SPR_MAX		= SPR_FISH + 10,
};

static sys::Sprite	sprite[SPR_MAX];			// スプライト

static int	cnt;			// カウンタ


/************
    初期化
 ************/
void	init_app(void)
{
	static const
	char*	tex_name[SPR_MAX] =
			{
				"photo.png",
				"animal_dolphin.png",
				"animal_hoojirozame.png",
				"fish_buri.png",
				"fish_ishidai.png",
				"fish_katsuo.png",
				"fish_kingyo.png",
				"fish_mola.png",
				"fish_tako.png",
				"fish_tatsunootoshigo.png",
				"sakana_mure.png",
			};

	for (int i = 0; i < SPR_MAX; i++) {			// スプライト初期化(リソース指定)
		sprite[i].set(sys::TexCache::RES_ASSET, tex_name[i]);
	}

	cnt = 0;			// カウンタ
}

/******************************
    稼働
		戻り値	アプリ続行か
 ******************************/
Bool	update_app(void)
{
	sprite[SPR_PHOTO].draw(0.0f, 0.0f);			// 背景

												// 魚
	sprite[SPR_FISH + (((cnt*4 + 640)/1280)*2 % 10)].draw(640 - ((cnt*4 + 640) % 1280), -128.0f);
	sprite[SPR_FISH + (((cnt*4/1280)*2 + 1) % 10)].draw(640 - (cnt*4 % 1280), -128.0f);
	cnt++;

	return	TRUE;
}

/**************** End of File *************************************************/

テクスチャは持たず、リソースを指定してスプライトを設定しています。
描画方法に変更はありません。


さかなの行進


※魚スプライトの画像は「かわいいフリー素材集 いらすとや」から使わせていただきました。どうもありがとうございます。

プロジェクト一式は、こちらです。