XNA Game Studio 4.0におけるRenderTargetの変更点

XNA Game Studio 4.0ではユーザービリティ向上、エラー数低減を目指してAPI設計をしました。そのひとつがRenderTarget関連のAPI変更です。

いまだに共通して混乱の元となっているのはRenderTargetUsage.DiscardContentsの振る舞いですが、この振る舞いは変わっていません。PreserveContentsモードはXbox 360上では非常に遅く、Windows Phone上では更に遅くなってしまいます。これはタイルレンダリングなどの分割レンダリングを採用するプラットフォームでは良くあることです。ですから、Windows PhoneではXbox 360と同じくRenderTargetUsage.DiscardContentsがデフォルトの振る舞いとなっています。特にメモリ帯域の狭いWindows PhoneではPreserveContentsモードは遅くなってしまいます。

シンプルなAPIを提供するのは良いことですが、パフォーマンスを犠牲にしてまで簡略化しようとするのは問題です。ですから、DiscardContentsは残っています。学び、愛し、共生せよ :-)

以下は変更点です。

Has-a対Is-a

よく以下のようなコード書こうとしているのを見かけます。

 RenderTarget2D rt = new RenderTarget2D(...);
List<Texture2D> textures = new List<Texture2D>();

// アニメーションフレームをレンダリングする
for (int i = 0; i < 100; i++)
{
    GraphicsDevice.SetRenderTarget(0, rt);
    DrawCharacterAnimationFrame(i);
    GraphicsDevice.SetRenderTarget(0, null);

    textures.Add(rt.GetTexture());
}

このコードは意図したとおりには動作しません。なぜなら、GetTextureメソッドは同じサーフェースメモリを返すだけで、別々のコピーを返すのではありません。ですから、同一のテクスチャに上書きすることになり、最終的にはすべてのテクスチャが同じになってしまいます。ですが、このセマンティクスは明確ではありません。GetTextureの実際の振る舞いは同じテクスチャを返すのに、APIの見た目は別々のテクスチャを返すように見えます。

これは典型的なis-a、has-aの違いです。RenderTargetは特殊なテクスチャですが(is-aの関係)、APIの見た目からはテクスチャと関連づけされたもの、もしくはテクスチャへ変換できるもののように見えます(has-aの関係)。

この問題はGetTextureメソッドを取り除き、RenderTarget2DをTexture2Dから継承すること(RenderTargetCubeはTextureCubeから継承している)で解決しました。このことにより、4.0ではセマンティクス的な間違いを犯す可能性が低くなりました。

 List<Texture2D> textures = new List<Texture2D>();

for (int i = 0; i < 100; i++)
{
    RenderTarget2D rt = new RenderTarget2D(...);

    GraphicsDevice.SetRenderTarget(rt);
    DrawCharacterAnimationFrame(i);
    GraphicsDevice.SetRenderTarget(null);

    textures.Add(rt);
}
 

原子性(Atomicity)

RenderTargetをGraphicsDeviceからどうやって外しますか?以前のGame Studioではよく

 GraphicsDevice.SetRenderTarget(0, null);

のように書いていました。これは殆どの場合で問題ないのですが、マルチレンダーターゲットを使っている場合は、もう少し複雑なコードを書く必要がありました。

 for (int i = 0; i < HoweverManyRenderTargetsIJustUsed; i++)
{
    GraphicsDevice.SetRenderTarget(i, null);
}

美しくない…。加えて正しいループカウントを設定しないと動作しない、という間違いを起しやすいという欠点もあります。

Game Studio 4.0ではSetRenderTargetメソッドをひとつにまとめました。常に必要な数だけのレンダーターゲットを設定し、以下のコードでは常に設定された全てのレンダーターゲットを外すようになりました。

 GraphicsDevice.SetRenderTarget(null);

ひとつのレンダーターゲットを設定するのにインデックス指定する必要はなくなりました。

 GraphicsDevice.SetRenderTarget(renderTarget);

複数のレンダーターゲットを設定したい場合(HiDefでサポート、Reachでは使えない)、以下のように使いたい分だけのレンダーターゲットを一度に指定します。

 GraphicsDevice.SetRenderTargets(diffuseRt, normalRt, depthRt);

このコードは短縮形で、以下のコードと同じ意味を持ちます。

 RenderTargetBinding[] bindings =
{
    new RenderTargetBinding(diffuseRt),
    new RenderTargetBinding(normalRt),
    new RenderTargetBinding(depthRt),
};

GraphicsDevice.SetRenderTargets(bindings);

SetRenderTargetをひとつのメソッドにまとめることで、以下の利点があります。

  • マルチレンダーターゲット使用時にGraphicsDeviceからの外し忘れミスを少なくする
  • パラメーターチェックが効率的になった。マルチレンダーターゲット使用時のパラメーターチェック、例えば全てのレンダーターゲットのサイズは同じであること、といったチェックをする箇所が一箇所で済むようになりました。今までは複数のメソッド呼び出しに分かれてしまっていたので、どの段階でパラメーターチェックをするかを決めることができませんでした。この問題は設定時にダーティーフラグをセットし、全ての描画メソッドが内で描画直前にこのダーティーフラグをによってパラメーターチェック(マルチレンダーターゲットを使っていなくとも)をするようになっていました。このチェックは影響はすくなくとも、測定可能なパフォーマンス差がありました。でも、今は一箇所でこのチェックをするので、効率的になりました。

消えた深度バッファクラス

クリエーターズクラブオンラインにあるブルームポストプロセスサンプルの以下の行にはバグが潜んでいます。

 renderTarget1 = new RenderTarget2D(GraphicsDevice, width, height, 1, format);

問題点として、このレンダーターゲットを指定した時に明示的に深度バッファを外していません。ブルームプロセスには深度バッファが必要ないのにも関わらず、デフォルトの深度バッファが使われ続けることになるので、このレンダーターゲットと深度バッファは互換性のあるフォーマットにしていないといけません。

もし、このサンプルのバックバッファフォーマットをマルチサンプリングにした場合、デフォルトの深度バッファもマルチサンプリング用のものになります。しかし、ブルーム用のレンダーターゲットはマルチサンプリングフォーマットではないので、実行時にエラーとなってしまいます。

この問題はブルーム用のレンダーターゲットにバックバッファフォーマットと同じマルチサンプリングフォーマットを指定するか、ブルーム処理をする時に以下のように明示的に深度バッファを外すことで解決できます。

 DepthStencilBuffer previousDepth = GraphicsDevice.DepthStencilBuffer;
GraphicsDevice.DepthStencilBuffer = null;

DrawBloom();

GraphicsDevice.DepthStencilBuffer = previousDepth;

美しくないし、わかりづらい。私たちはこのコードをサンプル内に入れるのを忘れてしまいました。多くの人たちが同様のミスを犯しているのを見かけます。

このことについて更に考え、いくつかのことに気づきました。

  • レンダーターゲットを変更する時に深度バッファの設定を忘れてしまうと、途端にバグ発生。
  • レンダーターゲットを外す時に深度バッファを外すのを忘れると、またバグ発生。
  • 多くのレンダーターゲットを使った手法は深度バッファを必要としていない。正しい方法は深度バッファをnullに設定することだが、忘れても問題なく動作することがあるし、問題が発生しても原因がわかりづらいことが多い。
  • 深度バッファを必要とするレンダーターゲットを使う場合、レンダーターゲットと深度バッファを同じサイズ、同じマルチサンプリングフォーマットになるように管理するのは面倒でバグの元。XNAフレームワークにはこのルールを守っているかをチェックするコードがあり、問題があると例外発生していますが、この例外を経験した人は多くいます。
  • DepthStencilBufferクラスには有意義なメソッドがない。実際、このオブジェクトはグラフィクスデバイスへ設定するだに必要なだけで、設定するときには常にレンダーターゲットと一緒に設定します。

私たちはDepthStencilBufferクラスは必要ないものだと考え、削除してしまうことにしました。その代わりに、深度バッファのフォーマットはレンダーターゲットを生成するときに指定できるようにしました。もし、以下のコードを書いた場合、

 new RenderTarget2D(GraphicsDevice, width, height);

深度バッファのないレンダーターゲットを生成することができます。深度バッファを使いたい場合、以下のコンストラクタのオーバーロードを使います。

 new RenderTarget2D(GraphicsDevice, width, height, false, SurfaceFormat.Color, DepthFormat.Depth24Stencil8);

注釈1: DepthFormat.Noneを指定することで明示的に深度バッファのないレンダーターゲットを生成することができます。

注釈2: MRTを使用する場合、深度バッファは最初のレンダーターゲットのものを使います。

この変更により、前述の問題が起きることはなくなりました。

  • レンダーターゲットと深度バッファのサイズやマルチサンプリングフォーマットが違うことによるエラーはなくなりました。また、これらのパラメーターチェックをする必要もなくなりました(レンダリングが速くなる)
  • ブルームなどの2D処理をする場合、深度バッファを気にする必要はなくなりました。深度バッファのないレンダーターゲットを設定すると、デバイスは自動的に深度バッファにnullを設定します。
  • レンダーターゲットを外した場合、自動的にデフォルトの深度バッファが設定されます。ですから、以前のように明示的に設定されている深度バッファを保存、設定しなおす必要がなくなりました。これで深度バッファに関する間違いは起きなくなりました。

このデザイン変更だと明示的に深度バッファを共有できなくなってしまうのでメモリの無駄遣いになってしまうのではないのか?と考える人もいると思います。

問題ありません、重要なのは命令文的APIから宣言的APIへとシフトしたことです。明示的に深度バッファを生成し、オブジェクトのライフタイムを管理し、いつ使いたいのかを指定する命令文的APIに対して、どんなフォーマットを使いたいのかを宣言するだけでフレームワーク側が常に最適な処理を行うことができるようになりました。

あなたが提供する重要な情報が二つあります。

  • レンダーターゲットを使うときに深度バッファが必要か?必要ならばどんなフォーマットなのか?
  • レンダーターゲットを再び設定したときに、以前の描画結果から続けて描画したいのか(RenderTargetUsage.PreserveContents)、それとも単に最終描画結果が必要なだけなのか(RenderTargetUsage.DiscardContents)?

この情報に基づいて、場面ごとによってXNAフレームワークは最適な実装戦略を選択します。

  • Xbox 360上の場合: Xbox 360には深度バッファなんてものは存在せず、EDRAMを流用し、PreserveContentsが指定された場合にのみ、メインメモリへ書き戻すだけです。ですから、以前のように深度バッファがあるかのような振る舞いを提供する必要がなくなったので、この新しいデザインは以前よりもメモリを必要としなくなりました。
  • Windows上の場合: PreserveContentsが指定された場合、レンダーターゲット毎に別々の深度バッファを確保します。
  • Windows上の場合: デフォルトのDiscardContentsを指定した場合、フレームワーク側で同じ深度バッファを複数のレンダーターゲット間で共有(サイズとフォーマットが同じ場合)するようにすることで、メモリ使用量を抑えることができます。

ぶっちゃけ、この深度バッファ共有最適化はまだ実装されていません。現在の予定では4.0のリリースまでには実装することになっていますが、なんらかの原因で間に合わなくなっても私(Shawnの事)叩かないで下さいね。

原文:

http://blogs.msdn.com/shawnhar/archive/2010/03/26/rendertarget-changes-in-xna-game-studio-4-0.aspx