PSP

ふとしたことから PSPのファームのバージョンを確認してみたら 1.0だった。
ファーム1.0といえば自作プログラムを簡単に起動できるという特典(?)付のPSPではないか! 発売からだいぶたって購入したPSPなのでてっきり1.5以降かと思っていたのだが、うれしい誤算だ。(つい最近、1.50のファームでも自作プログラムを起動する穴が発見されたが、こちらは起動がかなりめんどい)
ということで、開発環境を整えて MOD のプレイヤーでも書いてみることにした。MOD再生エンジンのソースはベーマガ読者ならおなじみの「ファミベのよっしん」氏が公開されているのでありがたく利用させてもらう。

んが...普段VC++のぬるま湯環境でプログラム組んでる身からするとまじで苦痛。 Cygwin + GCC + テキストエディタでプログラミング。デバッガはなし。標準ライブラリもないので malloc あたりから自前で書かんといかん。4で割り切れないアドレスからint読もうとするとハングアップ。カーネルのAPIはほとんど判明してなくて使えない...etc
結局1日半ぐらいかけてやっとこさ音がでるようになったが、すごい音痴。 パーカッションはまともだが、メロディがすごい音痴。
理由はサンプリングレートの算出するのに元のソースでは、double pow(double, double)を呼び出しているのだが、ここをintで処理しているからだろう。
ここをしっかり書けばいけそうなのだが、pow書くのってすごいめんどそうで座礁してしまった。
あと少しなんだけどなあ。もう疲れた...
だれか 標準Cライブラリを移植してください...orz

プログラマのプチ本懐

PSPソフト開発がらみで、某掲示板に私の知っているプチ情報(たいした情報じゃないですが)を数日前に書き込んだ。
そして、そのあと某超有名プログラマのblogに、その書き込みを引用&改変した記事があってとても驚いた。

私の元の投稿はPSPでの開発で個人的にハマッタ部分をまとめたもので、これから開発にとりかかろうとする人たちに有用だと思って書いたものだ。 その記事が、匿名でなく、ある程度の責任も、知名度もある人からリファインされて再発信されたということで、情報の価値も飛躍的に上がったことだろう。
日ごろから他人の発信してくれる情報には本当にお世話になっているので、ほんの少しでも恩返しできてちょっとうれしかった。


ちなみに MODプレイヤー on PSP だが、サンプリングレートの算出に float を使うように変更してみたところ、".MOD"形式はバッチリ再生できるようになった。伝説の作品 "State of the Art"のBGMがPSPから鳴り響いたときはちょっと感動した。
だが、".XM"形式の再生がまだ音痴だ。問題のpow関数も基数が2.0限定の 手抜きpow を書いてみたんだがだめ。 どうもfloatでは全然精度が足りてないようだ。
ということで double を使いたいところだが、doubleを使うとビルドがまったく通らない罠。マジデ新鮮だなあ。こういう環境は。

Cg の最適化

今まで Hamanaのシェーダーは アセンブラ(?)で書いていたのだが、nVIDIAの高級言語 Cg も触っとかないとな!ってことで、暇をみてCgを触っている。うーん、仕事以外のプログラミングってなんでこんなに楽しいのだろう。
んが、しかし、Cgのコンパイラが効率の悪いコードを吐き出していて困っている。

具体的には以下のCgのコードだ。
void FragmentProgram(float4 color : COLOR0,
  in float2 texCoord0 : TEXCOORD0,
  out float4 colorO : COLOR0,
  const uniform sampler2D Texture0,
  const uniform sampler1D Texture1)
{
  float4 texCol = tex2D(Texture0, texCoord0);
  colorO.r = tex1D(Texture1, texCol.r).r;
  colorO.g = tex1D(Texture1, texCol.g).r;
  colorO.b = tex1D(Texture1, texCol.b).r;
  colorO.a = texCol.a * color.a;
}
これはトーン補正を行うためのピクセルシェーダー のコードで Texture0がソースの画像をもつ2Dテクスチャ、Texture1 が各RGB成分から実際に出力するトーン値へのマッピングを保持する1Dテクスチャである。

これをCgコンパイラでps2.0用のコードにコンパイルすると、以下のようなアセンブリコードを出力する。
ps_2_0
// cgc version 1.4.0000, build date Jun  9 2005 12:09:02
// command line args: -profile ps_2_0
// source file: FragmentProgram.cg
//vendor NVIDIA Corporation
//version 1.0.02
//profile ps_2_0
//program FragmentProgram
//semantic FragmentProgram.Texture0
//semantic FragmentProgram.Texture1
//var float4 color : $vin.COLOR0 : COL0 : 0 : 1
//var float2 texCoord0 : $vin.TEXCOORD0 : TEX0 : 1 : 1
//var float4 colorO : $vout.COLOR0 : COL : 2 : 1
//var sampler2D Texture0 :  : texunit 0 : 3 : 1
//var sampler2D Texture1 :  : texunit 1 : 4 : 1
//const c[0] = 0.5
dcl_2d s0
dcl_2d s1
def c0, 0.500000, 0.000000, 0.000000, 0.000000
dcl t0.xy
dcl v0.xyzw
texld r0, t0, s0
mov r1.x, r0.y
mov r0.y, c0.x
mov r1.y, c0.x
mov r2.x, r0.z
mov r2.y, c0.x
mul r0.w, r0, v0
texld r3, r2, s1
mov r2.x, r3
texld r3, r0, s1
mov r0.x, r3
texld r3, r1, s1
mov r1.x, r3
mov r0.z, r2.x
mov r0.y, r1.x
mov oC0, r0

なぜか謎の定数 c0 が定義されていて、意味もなく汎用レジスタにmovしたりしている。

以前の、Cgを使わないでアセンブラで記述していたコードは以下のようになっていて、Cgの吐き出すコードよりもだいぶ短い。
ps_2_0
dcl v0
dcl t0
dcl_2d s0
dcl_2d s1

texld r0, t0, s0    // r0 に2Dテクスチャのテクセルロード 
texld r1, r0, s1    // 2DテクスチャのR成分のトーンをr1に読み込む
mov r0.r, r0.gggg   // 2DテクスチャのG成分をr0のRにコピー
texld r2, r0, s1    // 2DテクスチャのG成分のトーンをr2に読み込む
mov r0.r, r0.bbbb   // 2DテクスチャのB成分をr0のRにコピー
texld r0, r0, s1    // 2DテクスチャのB成分のトーンをr0に読み込む
mov r0.r, r1        // r1に保持しておいたR成分のトーンをr0.rにコピー
mov r0.g, r2        // r2に保持しておいたG成分のトーンをr0.gにコピー
mul r0, r0, v0      // r0を頂点の色(1,1,1,a) と混合
mov oC0, r0         // 出力レジスタに色を書き出す

実際、ベンチマークをとってみると Cgの吐き出したコードのほうが重くなっている。謎の定数c0がらみのコードがなくなるだけで効率は同等になるっぽいんだが・・・

最適化があまいとかそういうレベルじゃなくて、脈絡のない無駄なコードが挿入されてしまっている。不思議だ。それとも何か僕がとんでもないミスをしてるんだろうか?

Cg の最適化(2)

Cg の吐き出すコードの件だが、どうも Sampler1Dを使っているのが原因のようだ。 Sampler1Dでもアセンブルされたコードでは2Dのテクスチャとして扱われているが、このときy座標を中央の0.5にしてしまうのが原因ってことかな?
どうせ1Dテクスチャのy座標はどんな値でも関係ない状態なので、以下のように意図的に座標を無視するようにSampler2Dを使ってみた。
void FragmentProgram(float4 color : COLOR0,
  in float2 texCoord0 : TEXCOORD0,
  out float4 colorO : COLOR0,
  const uniform sampler2D PictureTexture,
  const uniform sampler2D ToneCurveTexture)
{
  float4 texCol = tex2D(PictureTexture, texCoord0);
  colorO.r = tex2D(ToneCurveTexture, texCol.rr).r;
  colorO.g = tex2D(ToneCurveTexture, texCol.gg).g;
  colorO.b = tex2D(ToneCurveTexture, texCol.bb).b;
  colorO.a = texCol.a * color.a;
}


これをCgでコンパイルすると生成されるコードは以下のようになって、だいぶ効率がよくなった。
ps_2_0
dcl_2d s0
dcl_2d s1
dcl t0.xy
dcl v0.xyzw
texld r0, t0, s0
mov r1, r0.z
texld r1, r1, s1
mov r0.z, r1
mov r1, r0.y
texld r1, r1, s1
mov r0.y, r1
mov r1, r0.x
texld r1, r1, s1
mov r0.x, r1
mul r0.w, r0, v0
mov oC0, r0


ちなみに同じコードを DirectX9 のHLSLコンパイラ に吐き出させるとこんな感じ。(CgとHLSLの言語仕様が同じってのは助かるねえ!)
ps_2_0
dcl v0
dcl t0.xy
dcl_2d s0
dcl_2d s1
texld r1, t0, s0
mov r2.xy, r1.x
mov r1.x, r1.y
mov r0.xy, r1.z
texld r3, r2, s1
texld r2, r1, s1
texld r0, r0, s1
mov r0.x, r3.x
mov r0.y, r2.y
mul r0.w, r1.w, v0.w
mov oC0, r0
うーむ、結構ちがうな。Cgより mov命令が一つ少なくなってる。HLSLのほうが賢いんかなあ...
どのみち Cgのランタイムを配布するのは面倒なので、Hamanaに使うのはHLSLにしたほうが良さそうだ。

GDI+のビットマップ転送速度

GDI+ はとても強力な描画命令を持っていて便利だけども、使ってみるとかなり遅い。
かなり単純な図形しか描画しなくても遅い。ということで、描画自体の速度はともかく、描画したGDI+のオフスクリーンサーフェスから、ウインドウへの転送速度が足を引っ張っていそうだ。
webで探してみてもやはりオフスクリーンサーフェスからの転送速度が遅いということを書いてあるページがいくつか見つかった。(このへんとか)
ということで、自分でもいろいろ調査してみた。

まずは一番シンプルで一般的と思われるコードは以下であり、これが遅い。
// オフスクリーンサーフェスの生成
Gdiplus::Bitmap bitmap(rect.Width(), rect.Height(), PixelFormat24bppRGB);

// オフスクリーンサーフェスに Graphicsクラスを使って描画
Gdiplus::Graphics g(&bitmap);
...

// オフスクリーンサーフェスからウィンドウへの転送。pDCはウインドウのデバイスコンテキスト
Gdiplus::Graphics g2(pDC->GetSafeHdc());
g2.DrawImage(&bitmap, 0, 0);

真っ先に疑ったのはDrawImage関数の効率が悪いのではないかということだ。以前にもDirectXのD3DXLoadSurfaceFromMemory関数がとても遅くて、自前で書いたほうが遥かに速かったことがあり、かなり疑わしい。
ということで、オフスクリーンサーフェスからHBITMAPを取得して、BitBltでウインドウへ流し込んでみた。
// オフスクリーンサーフェスからウィンドウへの転送。pDCはウインドウのデバイスコンテキスト
HBITMAP hBitmap = NULL;
bitmap.GetHBITMAP(0, &hBitmap);
{
  CDC dcMemory;
  dcMemory.CreateCompatibleDC(NULL);
  dcMemory.SelectObject(hBitmap);
  pDC->BitBlt(0, 0, bitmap.GetWidth(), bitmap.GetHeight(), &dcMemory, 0, 0, SRCCOPY);
}
DeleteObject(hBitmap);
残念ながら速度に違いは見られなかった。

つづいて、オフスクリーンサーフェスのバッファのポインタを直接取得して、StretchDIBitsで転送してみた。
// オフスクリーンサーフェスからウィンドウへの転送。pDCはウインドウのデバイスコンテキスト
Gdiplus::BitmapData bitmapData;
bitmap.LockBits(NULL, Gdiplus::ImageLockModeRead, PixelFormat24bppRGB, &bitmapData);

BITMAPINFOHEADER bmpInfoHeader;
memset(&bmpInfoHeader, 0, sizeof(BITMAPINFOHEADER));
bmpInfoHeader.biSize = sizeof(BITMAPINFOHEADER);
bmpInfoHeader.biWidth = bitmapData.Width;
bmpInfoHeader.biHeight = bitmapData.Height;
bmpInfoHeader.biPlanes = 1;
bmpInfoHeader.biBitCount = 24;

StretchDIBits(pDC->GetSafeHdc(),
    0, 0, bmpInfoHeader.biWidth, bmpInfoHeader.biHeight,
    0, 0, bmpInfoHeader.biWidth, bmpInfoHeader.biHeight,
    bitmapData.Scan0, (BITMAPINFO*)&bmpInfoHeader,
    DIB_RGB_COLORS, SRCCOPY);

bitmap.UnlockBits(&bitmapData);

おおっ、速い。でも、画像の上下がさかさまになってしまった...GDI+のオフスクリーンサーフェスはWindows従来のDIBとは上下が反転しているようだ。
まあ、GDI+にはSetTransform()という便利なメソッドがあり、ここでY座標をミラーする行列を仕込んでおけばあまりコードを汚さずに上下さかさまに描画できそうだ。
あとオフスクリーンサーフェスのピクセルフォーマットは当然24bitRGBのときに効率が一番よさそうだが、それ以外のピクセルフォーマットのときはもしかしたら一番最初のシンプルなコードでも問題ないかもしれない(実験してない)。

ということで結局、GDI+のオフスクリーンサーフェスには上下さかさまに描画して、StretchDIBitsで転送するという方法にしたら、 1600x1200程度のウインドウを描画するのに、もともと40fps程度だったのが80fps程度まで向上した。

と、ここまでやったところで気づいたんだが、GDI+のGraphicsはHDCからコンストラクトすることもできるので、実はDIB section ビットマップから作れば上下さかさまにせずとも速いのではなかろうか。
まずDIBSectionビットマップとGraphicsの生成。DIB Sectionビットマップにはオリジナルのラッパクラスを使ってもいいけど、ATLのCImageを使うと便利。
// DIB section と Graphicsの生成
CImage image;
image.Create(rect.Width(), rect.Height(), 32);
HDC hDC = image.GetDC();
Gdiplus::Graphics g(hDC);
image.ReleaseDC();
オフスクリーンサーフェスからウインドウへの転送はとてもシンプル。
image.BitBlt(pDC->GetSafeHdc(), 0, 0);

速度自体は上下さかさまに描画したものとそんなに差はないようだけど、これが最善っぽいね。あとDIBSectionの色深度を24ビットにすると遅かった。これはアライメントが影響してるんだろうか。でもBitmapクラスに上下さかさまに描画したときは24ビットでも速かったんだよなあ。ちょっと不思議だな。暇なときにまた調査してみよう。


とても長いエントリになってしまったが、結論。

オフスクリーンサーフェスに GDI+の Bitmapクラスを使うと遅い。DIBセクションビットマップを使うべし。

1/2 >>