今日こそ続きを書くぞって事で。


先に書いておくが、256色モード時に256色使ったビットマップイメージを正確に表示するのは、ほとんどの場合不可能だ。理由は読み進めていくうちに理解できるだろう。

さて、WindowsもUNIXでメジャーなウィンドウシステムであるXも、複数のプログラムが256個しかないカラーパレットをやりくりする必要があるのだが、その実現方法には多少思想の違いがある。のだが、結局やっている事は同じとも言える(どっちなんだよ?)。

Xではカラーパレットは共有財産で(いかにもUNIXな思想だ)、皆で使える色は「一緒につかおーねっ」ってね。ビットマップイメージデータ側で2と言う値が赤色を指していたとする。プログラムはとりあえすシステムに「赤色を2番に割り当ててくれ」って頼むさ。するとシステムは「赤は5番にあるみたいだから、それ使って」と言って来る。プログラムが「え〜、赤は2がいいのにな〜ちぇ」とか思ってもダメなんである(もちろん、むりやり「赤を2」にする逃げ道はある)。そこで、プログラムは仕方ないのでビットマップイメージデータの2だった所を5に書き換えたりするワケだ。

一方Windowsはというと、ビットマップイメージを表示する時に必要な色を、236色程をカラーパレットに割り当てて表示する。他のプログラムの事なんかお構いなしだ(これも逃げ道があって「ここの色はぜってー変えるな」ってカラーパレット作る時に指定する事もできる。が、それに対する逃げ道もあるので他のプログラムによって自分のカラーパレットが破壊されるのを防ぐのは諦めるべし)。

で、何で236色なのかというと、Windowsには「スタティックカラー」という20色の予約色がある。

+0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F
00                                
10                                
20                                
30                                
40                                
50                                
60                                
70                                
80                                
90                                
A0                                
B0                                
C0                                
D0                                
E0                                
F0                                

上図でグレーの部分がスタティックカラーだ。

これはウィンドウの枠を書いたりアイコンを書くのに必要な色なので変わると困るワケだ。プログラムが必要とする色の中にスタティックカラーと同じ色があった場合はソコの色が使用される。その場合は236色よりも多くの色が使われる事になる。前回のテストプログラムはグレースケールの表示であったが、標準のスタティックカラーの場合「白、黒、グレー3色」の5色がスタティックカラーから使用されるため241色のグレースケールで表示される事になる。また、パレット作成時に「スタティックカラーと共有するな」と指定する事もできる。

それでも結局元データである256色よりは少ないのでどうするんだ?って事になるが、これはシステム側で「似たような色は同じにする」という処理が動いて良きに計らうのである。つまりXで自前でやってた事をビットマップイメージを表示する時に毎回システムが肩代わりしてくれているわけだ。

つまりこれはどーゆー事か?冒頭で書いたXみたいな方式の場合。最初の1回だけシステムの都合に合わせてビットマップイメージデータを書き換えておけば良いワケだが、Windowsではシステムが勝手に毎回書き換えてるワケだ。しかもオリジナルのデータをシステムが勝手に変更なんてのはさすがのMicrosoftもダメって理解してるので、そのコピーに対して操作するという事で、メモリも沢山食ってると予想される。

「くあ〜っ、そんなフレームレートが落ちそうな余計な事するなよなーっ」って感じだ。

と言うワケで、Windowsにもソレを回避する道が残されている。そのタメのキーパーソンがStretchDIBits()APIのiUsageパラメータだ。Windowsはこの「iUsageパラメータの値がDIB_PAL_COLORSで且つ、ビットマップが要求するパレットとシステムのパレットが完全に一致する場合はビットマップデータの変更は行わない」のだ。

えぇっ?「ビットマップが要求するカラーパレットとシステムのカラーパレットが完全に一致」!?そんな殺生な。

ここで言うシステムのカラーパレットとは、プログラムがSelectPalette()APIでカラーパレットを設定した時点(実際にはRealizePalette()APIも呼ん直後)の状態の事だ。つまり、ビットマップイメージのカラーパレットの頭10色とケツ10色がスタティックカラーと同じならば、ビットマップイメージのカラーパレットとシステムのカラーパレットは同じになる(少々細工が必要だが)。しかし、Windowsでは[画面のプロパティ]でウィンドウの色が変更できるので、予めスタティックカラーの部分を考慮してビットマップイメージのカラーパレットを作るのは不可能だ。「そんな殺生な」と言った意味が理解できる事であろう。じゃぁ、どうするか?

「でっちあげる」のである。


とにかくシステムのパレットカラーパレットと同じにしたいので、それをゲットする必要がある。それに使うAPIはGetSystemPaletteEntries()APIだ。法則に則って以下にヘルプから引用。

UINT GetSystemPaletteEntries( HDC            hdc         // handle of device context
                            , UINT           iStartIndex // index of first entry to be retrieved
                            , UINT           nEntries    // number of entries to be retrieved
                            , LPPALETTEENTRY lppe        // array receiving system-palette entries
                            );

LPPALETTEENTRYってのはPALETTEENTRY構造体のポインタをtypedefしたモノだ。PALETTEENTRY構造体はLOGPALETTE構造体の所で説明したっけ?

そんなこんなで、ビットマップイメージのカラーパレットをシステムに設定してその結果をゲットするコードは以下のようになる。

--------------------------------------------------------------------------------
PALETTEENTRY pe[256];

SelectPalette( hDC, hPal, FALSE );
RealizePalette( hDC );
GetSystemPaletteEntries( hDC, 0, 256, pe );
--------------------------------------------------------------------------------

これを元にCreatePalette()APIでも使って作るべし。って、当然ソレだけじゃダメで、Xでシステムの都合に合わせてビットマップデータを書きかえるのと同様、ビットマップイメージデータを書きかえる必要がある。それに必要なAPIがGetNearestPaletteIndex()APIで、パレットの中から近い色のインデックスを返してくれる。まぁ、元のビットマップイメージを236色で書いておいて、先頭のスタティックカラーの10個だけズレルだろうって事で1ドット毎のデータに10を足しても良いかもしれないが(これをやるなら、ビットマップイメージのパレットを作成する時に、スタティックカラーと同じ色があっても使わない指定をする必要がある。詳しくはヘルプを読むように、って以下のサンプルで早速使ってるが)。

面倒だからGetNearestPaletteIndex()APIの書式は引用しないよ。

まぁ、この辺りを考慮して書いたサンプルソースを以下に記す。sample1.cにごちゃごちゃと追加したので多少キタナイが勘弁してもらいたい。

- sample2.c --------------------------------------------------------------------
#include <windows.h>

int main( void )
{
	BITMAPINFO *pBI;
	BYTE       *pBits;
	DWORD       cbBI;
	DWORD       cbBits;

	// 全部まとめてアロケート
	cbBI   = sizeof(BITMAPINFOHEADER) + (sizeof(RGBQUAD) * 256);
	cbBits = 256 * 256;
	pBI = (BITMAPINFO *)GlobalAlloc( GPTR, cbBI + cbBits );
	if ( pBI == NULL ) {
		return 1;
	}

	// BITMAPINFOHEADER設定
	pBI->bmiHeader.biSize          = sizeof( BITMAPINFOHEADER );
	pBI->bmiHeader.biWidth         = 256;
	pBI->bmiHeader.biHeight        = -256;
	pBI->bmiHeader.biPlanes        = 1;
	pBI->bmiHeader.biBitCount      = 8;
	pBI->bmiHeader.biCompression   = BI_RGB;
	pBI->bmiHeader.biSizeImage     = cbBits;
	pBI->bmiHeader.biXPelsPerMeter = 0;
	pBI->bmiHeader.biYPelsPerMeter = 0;
	pBI->bmiHeader.biClrUsed       = 0;
	pBI->bmiHeader.biClrImportant  = 0;

	// パレット設定
	{
		int nIdx;

		for ( nIdx = 0; nIdx < 256; nIdx++ ) {
			pBI->bmiColors[nIdx].rgbRed      = nIdx;
			pBI->bmiColors[nIdx].rgbGreen    = nIdx;
			pBI->bmiColors[nIdx].rgbBlue     = nIdx;
			pBI->bmiColors[nIdx].rgbReserved = 0;
		}
	}

	// ビットマップデータの位置
	pBits = &((BYTE *)pBI)[cbBI];

	// ビットマップデータ設定
	{
		int nIdx;

		for ( nIdx = 0; nIdx < 256; nIdx++ ) {
			BYTE *pTop = &pBits[256 * nIdx];
			FillMemory( pTop, 256, nIdx );
		}
	}

	// 表示
	{
		HDC          hDC = GetDC( 0 );
		LPLOGPALETTE lpLogPal;
		int          nIdx;
		HPALETTE     hPal;
		HPALETTE     hPalOld;
		DWORD        dwTick;


		// パレット作成
		lpLogPal = GlobalAlloc( GPTR
		                      , sizeof(LOGPALETTE)
		                      + sizeof(PALETTEENTRY) * 255
							  );
		if ( lpLogPal == NULL ) {
			return 1;
		}

		lpLogPal->palVersion    = 0x300;
		lpLogPal->palNumEntries = 256;

		for ( nIdx = 0; nIdx < 256; nIdx++ ) {
			lpLogPal->palPalEntry[nIdx].peRed   = pBI->bmiColors[nIdx].rgbRed;
			lpLogPal->palPalEntry[nIdx].peGreen = pBI->bmiColors[nIdx].rgbGreen;
			lpLogPal->palPalEntry[nIdx].peBlue  = pBI->bmiColors[nIdx].rgbBlue;
			lpLogPal->palPalEntry[nIdx].peFlags = 0;
		}
		hPal = CreatePalette( lpLogPal );

		hPalOld = SelectPalette( hDC, hPal, FALSE );
		RealizePalette( hDC );
		GetSystemPaletteEntries( hDC, 0, 256, lpLogPal->palPalEntry );
		if ( hPalOld ) {
			SelectPalette( hDC, hPalOld, FALSE );
		}
		DeleteObject( hPal );
		for ( nIdx = 10; nIdx < 236; nIdx++ ) {
			lpLogPal->palPalEntry[nIdx].peFlags = PC_NOCOLLAPSE;
		}
		hPal = CreatePalette( lpLogPal );
		GlobalFree( lpLogPal );

		// ビットマップイメージを新しいパレットの内容に書き換え
		{
			for ( nIdx = 0; nIdx < (256 * 256); nIdx++ ) {
				int r,g,b;

				r = pBI->bmiColors[pBits[nIdx]].rgbRed;
				g = pBI->bmiColors[pBits[nIdx]].rgbGreen;
				b = pBI->bmiColors[pBits[nIdx]].rgbBlue;
				pBits[nIdx] = GetNearestPaletteIndex( hPal, RGB(r,b,g) );
			}
		}
		// ビットマップイメージのパレットデータをパレットインデックスに書き換え
		{
			WORD *pw;

			pw = (WORD *)pBI->bmiColors;
			for ( nIdx = 0; nIdx < 256; nIdx++ ) {
				pw[nIdx] = (WORD)nIdx;
			}
		}

		hPalOld = SelectPalette( hDC, hPal, FALSE );
		RealizePalette( hDC );
		dwTick = GetTickCount();
		for ( nIdx = 0; nIdx < 1000; nIdx++ ) {
			StretchDIBits( hDC
				     , 0, 0, 256, 256
				     , 0, 0, 256, 256
				     , pBits, pBI
				     , DIB_PAL_COLORS, SRCCOPY
				     );
		}
		printf( "%dms\n", GetTickCount() - dwTick );
		if ( hPalOld ) {
			SelectPalette( hDC, hPalOld, FALSE );
		}
		DeleteObject( hPal );

		ReleaseDC( 0, hDC );
	}

	GlobalFree( pBI );

	return 0;
}
--------------------------------------------------------------------------------

ベンチマーク機能(笑)がついているのに気付くと思うが、sample1.c互換の表示方法の場合1000回表示に2866ミリ秒で今回の方法で2735ミリ秒だった。131ミリ秒の改善だ…ってオイ!たったの131ミリ秒かよっ!。

まぁ、昔の技術だって事で…。遅いCPUと遅いグラフィックカードの頃は有りがたかったんだろう。ちなみにテスト環境のCPUはMMX Pentium200MHzだ。今となっては超遅いCPUだが、これらの技術が云々の頃はi486-66MHzとかがサーバ用の頃だしね。まぁPentium166MHz以上か未満で大きく変わる気がする(Pentium166MHz未満は不当に遅い)。

ソースのコメントに「ビットマップイメージのパレットデータをパレットインデックスに書き換え」という個所があるが、これが何か書いておこう。

StretchDIBits()APIのiUsageがDIB_PAL_COLORSの時はBITMAPINFO構造体のパレットカラーなんて参照しない。が、代わりに理由は不明だがDIB_PAL_COLORSの時はパレットインデックスへのポインタを参照する。パレットインデックスへのポインタ?なんだそれわっ。

説明すると、ここまで色々しても、システムはビットマップイメージを直接描画してくれないと言うことだ。つまり、1ドットのデータが指す内容はパレットのインデックスでは無く、パレットインデックスへのポインタなのだ。

パレットインデックスへのポインタは本来ならパレットデータが置いてある場所、つまりBITMAPINFO構造体のbiColorsメンバに256個のWORDデータとして存在する。で、ここのデータを元に、例えば「2という内容のドットの場合そのパレットインデックスは5だ」とか調べるんである。サンプルソースでは「2だったら2じゃい!」という仕様なのでそのように設定している。

なんでこのような仕様なのかは知らないのだが、自分は以下のように便利使っている。

システムパレットは頭とケツの10個ずつという不便な場所に置かれているため、たまに面倒な処理を追加する必要がある。SetSystemPaletteUse()APIと併用してシステムパレットのうちの16色を0〜15に割り当ててシステムカラーとして、残りの16色×15ブロックとして管理(X680x0でスプライトを使った事がある人なら理解できると思う)するのだ。

SetSystemPaletteUse()APIが何かというと、冒頭に「256色モードで正しい256色ビットマップイメージ表示はほぼ不可能」と書いたが、これを正しくできる可能性があるAPIだ。このAPIを使うと254色まで使えるようになるのだ(0番=黒、255番=白ってのは変えられない)。だがらといって、そこに勝手な色を割り当てるとウィンドウ装飾やアイコンが大変な事になるので、16色はそのままスタティックカラーを割り当てている。そもそもWindows3.1の頃まではスタティックカラーは16色だったハズで、16色でも問題無いことが多い。


sample2.cではかなり端折ったんだが、Windowsの世界は色んなデバイスが使われているため、「これはコレで決め打ち」ってのが難しい。たとえば「スタティックカラーは20色」ったって「絶対」じゃない(だいたい16色モード時はどんなに頑張っても20色は確保できない)。

そーゆーのを考慮したりしてシステムのカラーパレットを作成する関数を作ったのが以下のものだ。参考にしてほしい。

--------------------------------------------------------------------------------
HPALETTE dibCreateIdentityPalette( DIB *pDIB )
{
	int i;
	int nStaticColors;
	int nPaletteColors;
	static struct {
		WORD Version;
		WORD NumberOfEntries;
		PALETTEENTRY aEntries[256];
	} Palette = {
		0x300,
		256
	};
	HDC      hDC = GetDC( NULL );
	HPALETTE hPal;
	HPALETTE hPalOld;

	SetSystemPaletteUse( hDC, SYSPAL_NOSTATIC );
	SetSystemPaletteUse( hDC, SYSPAL_STATIC );

	hPal    = dibCreatePalette( pDIB );
	hPalOld = SelectPalette( hDC, hPal, FALSE );
	RealizePalette( hDC );
	GetSystemPaletteEntries( hDC, 0, 256, Palette.aEntries );
	if ( hPalOld ) {
		SelectPalette( hDC, hPalOld, FALSE );
	}
	DeleteObject( hPal );
	nStaticColors  = GetDeviceCaps( hDC, NUMCOLORS ) / 2;
	nPaletteColors = GetDeviceCaps( hDC, SIZEPALETTE );
	ReleaseDC( NULL, hDC );

	for ( i = 0; i < nStaticColors; i++ ) {
		Palette.aEntries[i].peFlags = 0;
	}
	for (      ; i < (nPaletteColors - nStaticColors); i++ ) {
		Palette.aEntries[i].peFlags = PC_NOCOLLAPSE;
	}
	for (      ; i <  nPaletteColors; i++ ) {
		Palette.aEntries[i].peFlags = 0;
	}

	return CreatePalette((LOGPALETTE *)&Palette);
}
--------------------------------------------------------------------------------

DIB型の構造体は、このライブラリでビットマップイメージを管理する構造体として定義されているモノだ。


Windowのビットマップに関する話題はまだ少しあるのだが、本日で東京を去る事もあり本件についてはこれで終わりとする。