The deep inside of Windows 〜 Windowsの深淵


Lesson 2.超高速描画の謎【後編】  '01/11/03

DirectDrawSurface + functorでの描画が基本という結論を得たので、案外盲点になっている注意点を書いてみたいと思います。

1.DirectDrawSurfaceのLockのバグ

LockするRectを指定してのLockは正しくlockされない(バグ)のため、使わないようにしましょう。

2.システムメモリ上のDirectDrawSurfaceは、Lostしない。

そのため、IsLostで検出できません。注意しましょう。

3.システムメモリ上のDirectDrawSurfaceは、Lockしなくても読み書きできる

出来るようです。VRAMが非リニアなマシンではどうなるのかって?というと、初期のPC-9801シリーズとかですかね..そんなマシンは死滅していて存在しません^^;

4.DirectDrawSurfaceのGetDCはバグ有り。

何とGetDCはひどいバグがあります。それは、DirectDrawがCreateされたときの画面とコンパチなDCを内部的に作るようです。すなわち、256色モードでDirectDrawCreateを行ない、そのあと16bppモードに切り替えても、DCは依然として256色しか扱えません。これ、非常にひどいバグですが、HELモードでも再現性があります。よって、HDC経由で描画なんてことはやめたほうが無難です。ということは、自前でビットマップを読み込むことになるので、それぞれのサーフェースごとの転送ルーチンが必要になります。当然、変換子にはfunctorを使いましょう。

5.WM_ACTIVATEAPPをフックする

画面解像度が切り替わったときには、リストア処理が必要となります。WM_ACTIVATEAPPをフックして、画面解像度切り替えのときは、サーフェースのwrapperクラスのchain ( ⇒ std::set < CSurfaceWrapper* > ) すべてのrestoreメソッドを呼び出すような処理が必要になります。ただし、システムメモリ上のサーフェースならばLostしないので(ただし、現画面モードのサーフェースと非コンパチであれば、Blt転送自体は出来ない)、現画面モードのサーフェースに変換してやることは可能です。ただし、16bpp -> 32bppのようにbit深度が深くなる場合、32bppモードなのに、16bpp(65536色)しか出ていないことになるので、この場合は、ビットマップ画像をファイルから読み込んでいるサーフェースならば、ファイルから読み込みなおす処理が必要になります。

6.プライマリとピクセルフォーマットを合わせる

プライマリサーフェースと、ピクセルフォーマットは違うものでも構いません。たとえば、画面モードがどのモードであれ、内部的には、RGB555にして、それ専用のルーチンを書き、あとは現在の画面モードに合わせて、そこから転送するルーチンを用意するという方式です。これは、それなりに効果を発揮します。しかし、Windowモードであると、プライマリサーフェースをLockして直接転送すると正しくクリッピングされませんので、いったんセカンダリサーフェースに対して転送して、そのあとセカンダリサーフェースから、プライマリサーフェースへIDIRECTDRAWSURFACE::Bltで転送してやる必要があります。こうなると、その部分のオーバーヘッドが無視できなくなってきます。フルスクリーンモードで、クリップが不要である場合はこの限りではありませんが。

DirectDrawSurfaceで注意すべき点は、それくらいです。


次にfunctorについてです。functorについては、いまさら私がここで語る必要は無いと思いますが、一応、ご存知のない方のために、簡単におさらいしておきましょう。(スーパープログラマへの道 第C7回  functorは函数合成の夢を見るか(何のこっちゃ^^;) も合わせてご覧ください)

functorとは、operator ( ) をオーバーロードしたクラスのことを言います。たとえば、転送元から転送先へコピーするfunctorならば、

// 転送元からのコピーのためのfunctor
class CFastPlaneCopySrc {
public:
 template <class _DST,class _SRC>
 inline void operator () (_DST& dst,_SRC& src) const {
  dst = src;
 }
};

と書きます。ここをテンプレートにしておくのがミソです。そうすれば、通常の転送は、

CFastPlaneEffect::Blt(
CFastPlaneRGB555(),lpSrc->GetPlaneInfo(),
CFastPlaneRGB888(),GetPlaneInfo(),
CFastPlaneCopySrc(),
x,y,lpSrcRect,lpDstSize,lpClipRect);

のように書けます。(詳しい実装の詳細については、yaneGameSDK2ndのなかに含まれる、yaneFastPlane.h / cpp と yaneGTL.h 等を見てください)

この、CFastPlaneCopySrc( ) の部分は、テンポラリオブジェクトを作る構文で、 ( ) は、引数無しのコンストラクタを呼び出しています。引数有りならば、ここにそのパラメータが入ります。CFastPlaneRGB555とCFastPlaneRGB888との変換子( operator = )は、オーバーロードしてあるので、この部分も、適切に動きます。

functorの最大の特徴は、それがコンパイル段階において、inline展開されるということです。(うまくやれば)

そこで、汎用性があるのに、そこそこの実行速度が得られるわけです。ただし、VC++の場合、Debugモードでbuildした場合は(ディフォルトでは)インライン展開されないので、この呼び出しオーバーヘッドはかなり大きく、その効果は現れません。(むしろ、非常に遅いです) Releaseモードにしたときは、inline展開されるために、うまくレジスタ割付されて、かなり良質のコードが生成されます。アセンブラでカリカリに書くのには及びませんが、お手軽にコピペで書けるという点は大きく評価できると思います。

そもそも、プライマリサーフェースと同じサーフェースタイプにしないといけないわけで、そうなってくると、各ピクセルフォーマットごとにルーチンが欲しくなります。考えられうるピクセルフォーマットすべてと、そして、α値を含んだ独自形式に対しても、です。

具体的には、RGB555,RGB565,RGB888,BGR888,XRGB8888,XBGR8888と、256色モード(これは内部的にはRGB555のサーフェースを用意しても良い。私のCFastPlaneはそういう実装にしてあります)、そしてα値を持ったサーフェース、これも、現在の画面モードに適合するように、ARGB4555,ARGB4565,ARGB8888を用意します。(実際は、ここに不明ピクセルフォーマットというのが必要なのだが、そんなビデオカードはサポートしなくても良いと思われる)

これらすべてに対して、基本的とも言える、抜き色無し転送、抜き色有り転送、半透明、加色合成、減色合成、フェード、ホワイトフェード、フィルカラー等を用意して、かつそれは、ジオメトリ変換、すなわち、等倍以外に、矩形の拡大縮小、affine変換(回転拡大縮小変換もこれに含まれる)の組み合わせがあり、かつPentiumとPentiumII、MMXとSSE等のプロセッサ固有の最適化されたルーチンを用意するとなると、実にその組み合わせは、

サーフェースタイプ(基本9種類) × エフェクトの種類(基本8種類) × ジオメトリ変換(等倍・拡縮・affineの3種) × プロセッサ固有のルーチン(4種) = 864通り!

さらに、ピクセルフォーマットの異なるサーフェースに転送するときのエフェクトとかも考えると、

864×サーフェースタイプ(基本9種類) = 7776通り!

こんなものは、現実的には作成困難かつ保守不可能です。実際は、これよりさらに細かいエフェクトも必要になります。ジオメトリ変換も、もっといろいろ欲しくなります。

functorを使う意義は、ここにあります。functorを使えば誰でも簡単に自分の欲しい画面効果を簡単に追加できます。アセンブラでカリカリに書いたものと比べれば最適にはほど遠いですが、十分に満足いくスピードだと思いますし、保守性に極めて優れています。

ということで、描画に関する問題は、ひとまず解決!しかし、functorなので下手に部分的なinlineアセンブラを使うと、レジスタ割付が阻害されてあまりよろしく無いので、完全にC++のコードとして記述しなくてはなりません。そうなってくると、RGB565で飽和加算をテーブルを使わずに高速に実行するC++のコードとはどんなものかとか、そういう疑問も出てくると思います。

というか、MMXを使おうが、SSEを使おうが、そういう可変ビット長に対するpacked演算は、ソフト的に処理するしか無いのです。こういう処理は、テーブル化するのが一番速いかのように言う人がいますが、1次キャッシュに入らなければ、非常に遅いです。RGB565の飽和加算テーブルがどれだけのサイズになってどれだけの速度になるのか、よく考えてみてください。ブレンド(半透明)についても同様です。ビットを間引いてテーブル化するのがベストだとか言う人がいますが、そうとも限りません。ビットを間引く処理が必要な上、ブレンド比率=10/255の場合など、転送元の画像がほぼそのまま表示されなくてはならないのですが、ビットを間引いているために、非常に汚くなっています。スピード重視の画像品質無視で良いならともかく、こんなものでは、やねうらおのクライアントからのokは出そうにありません。もちろん、ビットを間引かなければ、テーブルは1MBを超えるでしょう。到底1次キャッシュにも2次キャッシュにも入りません。(いまどきのマシンなら入るかな..?)

だから、テーブルを用いないアプローチが必要になってくるというのには、それ相応の必然性があるのです。DirectXが黒魔術だとしたら、そういうSoftware packed演算は、白魔術だと勝手に思っています^^;

そのへんについては、また機会を改めて書きたいと思っています。というか、少し書き出したものの、凄い量になってきたので、システマティックにまとめなおして、Cマガの投稿記事にしようと思っています。読者投稿なので、没になったら原稿はホームページ上で公開^^; 没になることを期待しててください^^;;;

その他、何か質問やアドバイスがありましたら、お気軽にやねうらおまでどうぞ。

P.S.
ちなみに、yaneSDK2ndのCFastPlaneは、まだそういう実装にしてないですぞ。自分が実装して無いものを、さも実装するのが当たり前のように語るなとか言ってくるなよ!ヽ(`Д´)ノ Cマガの投稿記事書けてからするんだかんな。時間無いからやって無いだけだかんな。


戻る