最近XHTML 1.1 Referenceの方もForms Moduleのところで時間かかってるし、こっちも暫く放っておいたなぁと反省して、ちょこっと小ネタでも。以前やっていたDirect Sound周りが一段落したので(Ogg Vorbisに加えてFLACまであまり必要でもないのにやってしまった)、この前までDirect3D関係を実装していました。その辺りで身を以て思い知らされた事などを少々。
- LoaderLock が検出されました
-
Visual Studio 2005とManaged DirectX 1.1を使っていると、デバッグ時に「ローダーロックが検出された」というエラーが出てきて、「
DLL '...\Microsoft.DirectX.Direct3D.dll' は、OS ローダー ロック内でマネージ実行を試行しています。DllMain またはイメージ初期化関数内でマネージ コードを実行しないでください。この動作は、アプリケーションをハングさせる原因になる可能性があります。
」と言われます。どうも.NET Framework 2.0とMDX 1.1は折りが合わないようで、そのままの設定ではうまくデバッグが出来ないようです。難しい内部事情は知りませんが、手っ取り早い解決策があります。- メインメニューで「デバッグ」「例外」を選び、「例外」ダイアログを開きます。
- 「Managed Debugging Assistants」の中の、「LoaderLock」のチェックを外します。
- 「OK」ボタンで確定します。
この設定はプロジェクト固有なので、新しいプロジェクトを作った場合には毎回設定しなければなりませんが、VS2003とCLR 1.1に戻るくらいならこっちの方がいいでしょう。
参考 : Why do I get a 'LoaderLock' Error when debugging my Managed DirectX application
- Microsoft.DirectX.Direct3D.Spriteの有用性
-
下手な自前のコードよりは速く動作するでしょう。ただ、アルファブレンドが自由に行えないなどの機能的欠点や、バッファリングして描画した時のDrawIndexedPrimitivesには敵わないなどのパフォーマンス的欠点は持ちます。
しかし、デバッグ時にちょっとテクスチャを貼り付けて表示したいとか、普通のアルファブレンドしか使わず、スプライトの数も多くないといった場合には十分有用でしょう。
- 大量のプリミティブの高速描画
-
D3DXのSpriteは確かに数が増えてくるとシーン時間が長くなってきますが、DrawPrimitivesやDrawIndexedPrimitivesを使ってベタに組んでも、大した差は出ません。ベタに組む、というのは、スプライトを描画する度に、頂点4つの頂点バッファとインデックス4つのインデックスバッファを使ってTriangleStripを2つ、DrawIndexedPrimitivesで描画するというものです。DirectXでは、DrawUserPrimitivesなども含めて、Draw系の関数の呼び出しは出来るだけ数を減らす方がいいようです。
これを実現するには、描画するスプライトの情報をバッファリングして、あとでまとめて頂点バッファに書き出して描画、ということが必要になります。シーンの中では、頂点バッファとインデックスバッファをセットして(ステートブロックを使うと良い)、TriangleListでDrawIndexedPrimitivesを呼び出すだけです。スプライト毎にワールド変換が異なるので、頂点変換はCPUで事前にやっておかなければなりませんが、GPUがせっせとレンダリングしてる裏で処理できるのでさほど問題ではないでしょう。
恐らく、一番のネックは頂点バッファへの書き込みとなります。C++ではポインタが何にでもキャストできる為気になりませんが、C#では、
CustomVertex.PositionColoredTextured
配列を書き込もうとするとキャストがうまく出来ない為につっかかります。かといって配列を返すバージョンのMicrosoft.DirectX.Direct3D.VertexBuffer.Lock
ではパフォーマンス的に不利です。GraphicsStreamを返すLockはbyte配列なら書き込めますが、頂点変換の時に一々頂点の構造体のデータの並びをbyte配列に手で直して代入するのも面倒です。これには抜け道があります。アンセーフコードを書いてポインタにするのではありません、StructLayoutを使えば一発です。
using Microsoft.DirectX.Direct3D; using System.InteropServices.Runtime; [StructLayout( LayoutKind.Explicit )] public struct VertexArray { public VertexArray( int length ) { this.VerticesAsByteArray = null; this.Vertices = new CustomVertex.PositionColoredTextured [ length ]; return; } [FieldOffset( 0 )] public CustomVertex.PositionColoredTextured[] Vertices; [FieldOffset( 0 )] public byte[] VerticesAsByteArray; }
何をやりたいか、見たら判ると思います。C#における共用体です(詳しくはUnions のサンプルを参照)。上のコードは、C++で書いたら次のようなものです。
struct PositionColoredTextured; union VertexArray { PositionColoredTextured* Vertices; unsigned char* VerticesAsByteArray; };
頂点変換の時はVerticesを、GraphicsStream.Writeに渡す時はVerticesAsByteArrayを使えばいい訳ですね。なんでこれくらいのことがC#では手軽に出来ないんでしょう。配列をbyte配列として見るかint配列としてみるかとかの変換くらいはキャストだけで出来てもいいような気がしますが……。僕が知らないだけですか? ちなみにオブジェクト配列は無理みたいですがbyte配列とプリミティブ間ならSystem.BitConverterとかいうものが、プリミティブ配列同士ならSystem.Bufferっていうのがあるみたいですね。どっちにしろ無駄なコピーが発生するみたいで残念ですけど。
2007-01-30追記: この時は配列の実体ばかり気にしていて、付随するプロパティに関心がありませんでしたが、問題になることがあるかも知れません。特に
System.Array.Length
。10000バイトのbyte配列を作り、上記の方法でushort配列として扱っても、その擬似ushort配列のLengthプロパティは10000を返します(Lengthプロパティのgetアクセサがプライベートメンバの値をただ返しているのでしょう)。勿論、インデクサはきちんと値を返してくれます(arrayContainer.UInt8Array[ 0 ]が0x34で、arrayContainer.UInt8Array[ 1 ]が0x12の時、arrayContainer.UInt16Array[ 0 ]は0x1234を返すという事)。プログラマは勿論実際の配列の長さを把握しているでしょうが、byte配列を要求する.NET Frameworkの関数(例えばSystem.IO.Stream.Readなど)に渡す時は、最初にbyte配列として作らないといけなくなるでしょう。
コメントする