Twitter @HigenekoTwitter #XNA
今までのXNAにはEffect.CompileEffectFromSourceメソッドがありました。Xbox 360上ではランタイム時にシェーダーのコンパイルは使えず、Windows上でのみ使えるメソッドでXbox 360とwindows用のシェーダーをコンパイルすることができました。しかし、この方法では以下の問題がありました。
Game Studio 4.0ではシェーダーコンパイラーをWindows版 XNAフレームワークからコンテント・パイプラインへと移しました。Effect.CompilerEffectFromSourceの代わりにEffectProcessorを使うようになりました。
コンテント・パイプライン内でエフェクトをコンパイルするには幾つかの方法があります。
ContentPipelineの機能を使うにはMicrosoft.Xna.Framework.Content.Pipeline.dllを参照する必要があります。しかし、参照追加のダイアログを開いても、このアセンブリは表示されないでしょう。どこへ行ったのでしょうか?
.Net Framework 4.0にはClient Profileと呼ばれるものがあります��これは.Net フレームワーク全体ではなく、クライアント向けアプリケーション用に最適化されたサブセットとなっていて、フレームワークのダウンロードサイズが小さいなどの利点があります。XNA Game Studio 4.0ではWindows向けのゲームを作った場合、デフォルトでClient Profileを使うようになっています。コンテント・パイプラインのアセンブリを参照するには対象プラットフォームを.Net Framework 4にする必要があります。
やり方は以下のとおりです。
まずはテクスチャをインポートするサンプルを紹介します。まず最初にusingステートメントを記述します。
using Microsoft.Xna.Framework.Content.Pipeline; using Microsoft.Xna.Framework.Content.Pipeline.Graphics; using Microsoft.Xna.Framework.Content.Pipeline.Processors;
次にカスタム・ロガー(ビルド状態をログするもの)を作ります。ここでは宣言だけで、なんの処理もしていませんが、実際にはこれらの情報をどこかに表示する必要があります。
class MyLogger : ContentBuildLogger { public override void LogMessage( string message, params object[] messageArgs) { } public override void LogImportantMessage (string message, params object[] messageArgs) { } public override void LogWarning( string helpLink, ContentIdentity contentIdentity, string message, params object[] messageArgs) { } }
ようやっとカスタム・インポーター・コンテキストの記述です。このコンテキストを通してインポーターとホスト(インポーターの呼び出し側)との、情報のやりとりをします。
class MyImporterContext : ContentImporterContext { public override string IntermediateDirectory { get { return string.Empty; } } public override string OutputDirectory { get { return string.Empty; } } public override ContentBuildLogger Logger { get { return logger; } } ContentBuildLogger logger = new MyLogger(); public override void AddDependency(string filename) { } }
これらのヘルパークラスをいったん記述すれば、テクスチャ・インポーターを簡単に使うことができます。
TextureImporter importer = new TextureImporter(); TextureContent texture = importer.Import("cat.tga", new MyImporterContext());
プロセッサーの呼び出しはインポーターと似ていますが、プロセッサー・コンテキストはインポーター・コンテキストより複雑になっています。
class MyProcessorContext : ContentProcessorContext { public override TargetPlatform TargetPlatform { get { return TargetPlatform.Windows; } } public override GraphicsProfile TargetProfile { get { return GraphicsProfile.Reach; } } public override string BuildConfiguration { get { return string.Empty; } } public override string IntermediateDirectory { get { return string.Empty; } } public override string OutputDirectory { get { return string.Empty; } } public override string OutputFilename { get { return string.Empty; } } public override OpaqueDataDictionary Parameters { get { return parameters; } } OpaqueDataDictionary parameters = new OpaqueDataDictionary(); public override ContentBuildLogger Logger { get { return logger; } } ContentBuildLogger logger = new MyLogger(); public override void AddDependency(string filename) { } public override void AddOutputFile(string filename) { } public override TOutput Convert<TInput, TOutput>( TInput input, string processorName, OpaqueDataDictionary processorParameters) { throw new NotImplementedException(); } public override TOutput BuildAndLoadAsset<TInput, TOutput>( ExternalReference<TInput> sourceAsset, string processorName, OpaqueDataDictionary processorParameters, string importerName) { throw new NotImplementedException(); } public override ExternalReference<TOutput> BuildAsset<TInput, TOutput>( ExternalReference<TInput> sourceAsset, string processorName, OpaqueDataDictionary processorParameters, string importerName, string assetName) { throw new NotImplementedException(); } }
TargetPlatformとTargetProfileにはビルドするプラットフォームやHiDefやReachなどのプロファイルを指定します。
AddDependencyメソッドを使うことによって、インポーターやプロセッサーで処理したファイルが依存しているファイルを追加することができます。例えば、エフェクトコンパイラーがエフェクトソースコード内に#inlcudeステートメントを見つけたときなどに呼び出されます。この情報はインクリメンタルビルドをする時に使用します。.fxファイルを変更していなくても、#includeで指定されたファイルが変更された場合にはエフェクトをコンパイルします。
以下の単純なサンプルでは、Convert、BuildLoadAndAssetやBuildAssetメソッドは使っていません。これらのメソッドは独立した単純なプロセッサーでは必要ありません。ですが、ModelProcessorなどのビルド中にMaterialProcessor、EffectProcessor、TextureProcessorなどの複数のプロセッサーを使う場合には呼び出す必要があります。
このカスタム・プロセッサー・コンテキストを実装したら、HLSLソースコード文字列をEffectContent.EffectCodeに設定し、EffectProcessorを使ってコンパイルします。
EffectContent effectSource = new EffectContent { Identity = new ContentIdentity { SourceFilename = "myshader.fx" }, EffectCode = @" float4 MakeItPink() : COLOR0 { return float4(1, 0, 1, 1); } technique Technique1 { pass Pass1 { PixelShader = compile ps_2_0 MakeItPink(); } } ", }; EffectProcessor processor = new EffectProcessor(); CompiledEffectContent compiledEffect = processor.Process(effectSource, new MyProcessorContext());
プロセッサー・パラメーターを設定することにより、コンパイラーの設定を変更することができます。
EffectProcessor processor = new EffectProcessor(); processor.Defines = "ENABLE_FOG;NUM_LIGHTS=3"; processor.DebugMode = EffectProcessorDebugMode.Optimize; CompiledEffectContent compiledEffect = processor.Process(effectSource, new MyProcessorContext());
コンテント・パイプラインは再配布用のランタイムには含まれていないので、このコードを動作させるにはXNA Game Studioがインストールされた環境が必要です。
原文: http://blogs.msdn.com/b/shawnhar/archive/2010/05/07/effect-compilation-and-content-pipeline-automation-in-xna-game-studio-4-0.aspx
XNAでモデル描画する場合は、一般的に以下のようなコードを書きます。
Matrix[] transforms = new Matrix[model.Bones.Count]; model.CopyAbsoluteBoneTransformsTo(transforms); foreach (ModelMesh mesh in model.Meshes) { foreach (BasicEffect effect in mesh.Effects) { effect.World = transforms[mesh.ParentBone.Index] * world; effect.View = view; effect.Projection = projection; } mesh.Draw(); }
しかし、このコードのままだとモデルデータがBasicEffectを使っているときしか使えません。もしモデルデータがBasicEffect以外のエフェクトを使っている場合、「foreach (BasicEffect effect in mesh.Effects)」の部分で例外が発生します。この場合は以下のようなコードに変更します。
foreach (ModelMesh mesh in model.Meshes) { foreach (Effect effect in mesh.Effects) { if (effect is BasicEffect) { // 前と一緒 } else { effect.Parameters["World"].SetValue(transforms[mesh.ParentBone.Index] * world); effect.Parameters["View"].SetValue(view); effect.Parameters["Projection"].SetValue(projection); } } mesh.Draw(); }
カスタムエフェクトのパラメーターをどのように設定するかはエフェクトによって異なります。多くの人達は上のコードのように命名規約を使ったり、他にはEffectAnnotationを使ってデータバインディングの仕組み作っていたりします。
私たちがSkinnedEffect、EnvironmentMapEffect、DualTextureEffectとAlphaTestEffectを追加したとき、モデル描画コードは汚くなってしまいました。それぞれのビルトイン・エフェクト毎に「if (effect is BasicEffect)」のようにエフェクトの型をチェックするコードを書き足す必要がありました。ぐはっ
この問題は以下の三つのインターフェースを追加することで解決しました。
public interface IEffectMatrices { Matrix World { get; set; } Matrix View { get; set; } Matrix Projection { get; set; } } public interface IEffectFog { bool FogEnabled { get; set; } float FogStart { get; set; } float FogEnd { get; set; } Vector3 FogColor { get; set; } } public interface IEffectLights { bool LightingEnabled { get; set; } Vector3 AmbientLightColor { get; set; } DirectionalLight DirectionalLight0 { get; } DirectionalLight DirectionalLight1 { get; } DirectionalLight DirectionalLight2 { get; } void EnableDefaultLighting(); }
すべてのビルトイン・エフェクトはIEffectMatrices、IEffectFogインターフェースを実装しています。また、BasicEffect、SkinnedEffect、そしてEnvironmentMapEffectはIEffectLightsインターフェースを実装しています。このことによって、ひとつのモデル内に型の違うビルトインエフェクトがが複数あっても以下のようにモデル描画を簡潔に記述することができます。
Matrix[] transforms = new Matrix[model.Bones.Count]; model.CopyAbsoluteBoneTransformsTo(transforms); foreach (ModelMesh mesh in model.Meshes) { foreach (IEffectMatrices effect in mesh.Effects) { effect.World = transforms[mesh.ParentBone.Index] * world; effect.View = view; effect.Projection = projection; } mesh.Draw(); }
model.Draw(world, view, projection);
ただし、このヘルパーメソッドはモデル内で使われているエフェクトすべてがIEffectMatricesインターフェースを実装している必要があります。IEffectMatricesを実装していないカスタムエフェクトがあった場合は例外を発生します。この場合は、以前のようにループコードを書いて、それぞれのパラメーターを自前で設定して描画する必要があります。
原文: http://blogs.msdn.com/b/shawnhar/archive/2010/05/05/effect-interfaces-in-xna-game-studio-4-0.aspx
とりあえず、今月19回目の投稿になります。月に19回というのは、ひにけにXNA始まって以来の記録更新になります。別に本業の方が暇になったというわけではなく(Twitter見てる人には忙しさが伝わってると思いますが)、ビルド待ち時間やテストする為のネットワークサービス待ちというのがやたら多かったので、その待ち時間の間に沢山投稿することができました。
さて、ShawnのXNA Game Studio 4.0の新機能の紹介投稿も収まってきて、翻訳したい投稿も三つ程と減ってきました。
ちなみに私がメモしてある、それ以外に書きたい記事はこんな感じです。
だいたい私の書きたい順番に並んでいますが、ひにけにXNAはXNA Game Studioを使ってゲーム製作をしている人達のためのサイトなので、このサイトの記事を読んでくれている人達の要望にできる限り応えていきたいと思います。ですから、「この記事が速く読みたい」とか、他にも取り上げてほしい記事があれば、コメントやメール、Twitterなどで連絡してもらうと助かります。
今までのXNAではコンテント・パイプライン内では以下のようにしてマテリアルを読み込んでいました。
モデルデータがエフェクトファイルを参照していない時にカスタムエフェクトを使いたい場合は以下の二つの方法があります。
Game Studio 4.0も同じですが、新しいプロセッサー・パラメーターを追加してあり、ビルトイン・エフェクトの中から選ぶことができます(ただし、元のモデルが.fxファイルを参照していない場合)。
これに加えて、コンテント・パイプライン内の型として、SkinnedMaterialContent、EnvironmentMapMaterialContentなどの型が追加されています。
選択するエフェクトによって以下の追加作業をする必要があります:
原文: http://blogs.msdn.com/b/shawnhar/archive/2010/05/03/new-effects-in-the-content-pipeline.aspx
私のMIXでのトークから拝借して、XNA Game Studio 4.0の五つのビルトイン・エフェクトと、その最適化のための組み合わせの情報をまとめて紹介します。
簡単に言えば、シェーダー命令数が少ないほど実行速度が速くなります。ただし、CPUバウンドだったり、GPUの別の部分がボトルネックの場合は効果がないことに注意してください。
頂点ライティングx1になるのは、パラメーターが以下の組み合わせの時です:
以下の設定によって、前述の命令数に加えて以下の命令数が追加されます。
ライティングの構成によって主に三つの組み合わせがあります。
これらはWeightsPerVertex = 1の時の値です。規定値は4で、ModelProcessorの振る舞いと一緒です。少ないウェイト数で正しく動作させるためには正しい頂点データになっている必要があります。ちょい技: WeightsPerVertex = 1の設定にしたSkinnedEffectを使うと、インスタンシングを実装することができます。
そして、以下は設定による追加命令数です。
BasicEffectと違ってSkinnedEffectの場合は、ライティングなしや、テクスチャなしの設定をすることはできません。
EnvironmentMapEffectにはライティングの設定によって以下の二つの組み合わせがあります。
以下は設定による追加命令数です。
フレネル(Fresnel)とスペキュラについては別の機会に説明します。
これは単純で、基本設定はひとつしかありません。
これに加えてフォグの有無によって以下の命令数が追加されます。
シンプルなエフェクトですが、実は見た目も良く、速度も速いエフェクトでもあります。ライトマップやディテールマップ(別の機会に紹介します)に適しています。
AlphaFunctionの設定によって以下の二つの組み合わせになります。
これにフォグの有無によって以下の命令数が追加されます。
原文:
http://blogs.msdn.com/b/shawnhar/archive/2010/04/30/built-in-effects-permutations-and-performance.aspx
とあるプロジェクトで処理落ち問題が発生し、最適化をすることになりました。二人の技術者が同じ時間を掛けてプログラムの違う箇所の最適化を行い、その結果、二人の技術者は以下のように報告しました。
技術者A「俺はAの部分を5倍速くしたよ」
技術者B「僕はBの部分を2倍の速度に最適化しました」
さて、仮にあなたがこのプロジェクトの管理者だとして、どちらか一人の技術者に成功報酬をあげるとしたら、どっちの技術者を選びますか?
もしAかBのどちらかをこれだけの質問で選ぶことができたら、管理能力に難ありです。どっちも選ばずに自分の懐に入れると思った人は管理者というよりも人としてなにか間違っているでしょう。
まともな管理者であれば「もっと情報が必要」とか「プロジェクト全体の貢献度による」と答えるでしょう。
ここで陥りやすいのは数字のマジックです。人間はどうしても「2倍」、「5倍」という数字を聞くと、その印象が強く残ってしまいます。
ですが、ここで重要なのは「プロジェクト全体にどれだけの影響を与えたか」ということです。
仮にAの部分がプロジェクト処理時間の20%、Bの部分が80%使っていたとします。この場合、Aの部分を5倍速くした場合、プロジェクト全体の処理時間としてみると84%になるので、約1.2倍のパフォーマンスアップになります。次にBの部分を2倍速くした場合、プロジェクト全体で見ると約1.7倍のパフォーマンスアップになるわけです。
要するに、最適化をする場合は常にボトルネックとなっている部分を最適化するのが効率的であるということになります。このことをアムダールの法則と言います。
余談になりますが、「何倍速くした」というのはセールストークで、SSE/VMXになって四つの演算が同時にできる!とか、6コアだから爆速!みたいな使われ方をしますが、実際のプロジェクト全体の速度が4倍になったり、6倍になったりすることは無いのが現実です。逆にセールストークを鵜呑みにして安易に使おうとすると、作業量に見合わない程度のパフォーマンスアップにしかならなくてガッカリで済むならまだしも、逆にパフォーマンスが出なくなったなんて言うこともありえるので注意が必要です。
え?両方の最適化をすれば、2.3倍になるだろうですって?
確かにそうですが、それは人材と時間と予算が許せばそういうことも可能でしょう。ですが、大きなゲーム開発スタジオでさえ常にそれらのリソースを確保するのに苦労していますし、ましてやリソースに限りのあるインディーズゲーム開発では、リソースは賢く使う必要があります。
また、ここでの目標はどれだけパフォーマンスアップできるかではなく、「処理落ちしない」ようにすることです。ですから、5倍の最適化が無駄だと言っているようにも見えますが、必ずしもそうではありません。1.2倍の速度にして処理落ちがなくなるのであれば、この最適化を施したとしても目的は達成できる訳です。ただし、どう考えてもボトルネックになっていない箇所に貴重な時間を割いて最適化するよりも、ボトルネックとなっている部分を最適化する方が少ない労力で大きな効果を得られることが遥かに多いということです。
さて、複数に渡ってパフォーマンスを殺した犯人(CPUかGPU)とその方法(ボトルネック)の発見の仕方を紹介してきました。これらを簡単にまとめると以下のようになります。
処理落ちしたときにやるべきこと
処理落ちした時にやってはいけないこと
前回はCPUのボトルネックを割り出す方法を紹介しましたが、今回はGPUのボトルネック部分を割り出す方法を紹介します。
今日、取調室に呼び出されたのはGPUです。CPUと違ってGPUは決して自供することはありません。どんな質問を投げかけてもかたくなに黙秘権を行使します。ただし、いくつかの質問をすると微妙に表情が変わることが判りました。
グラフィクスプログラマーはこのGPUの微妙な表情の違いからGPUのボトルネック部分を探し出す必要があります。
GPUのボトルネックを見つけ出すにはGPUのパイプラインがどのようになっているのかを知る必要があります。実際には様々なグラフィクスカードがあり、それぞれに多少の違いはありますが、共通して以下の重要なステージがあります。
これらのいずれもがボトルネックになる可能性があり、このボトルネックを知ることは非常に役立ちます。例えば頂点シェーダーがボトルネックになっていると判っているのなら、テクスチャフェッチ数を減らすよりも頂点シェーダー内の命令数を減らすほうが効果的です。また、ピクセルシェーダーがボトルネックとなっている状態であれば、フレームレートを減らすことなくモデルのポリゴン数を増やすこともできると判断できます。
では、それぞれのパイプライン・ステージでどんな要素がパフォーマンスに影響を与えるのでしょうか?
GPUのボトルネックを見つけるには、これらの要素をCPUで処理される部分に大きな変化を起こすことなく変える必要があります(CPUパフォーマンスが変わればGPUにも影響を与えることになるので、正確な判断ができなくなる)。
まずは、思いっきり小さな解像度、例えば100x50くらいの解像度にしてゲームを実行してみます。小さな解像度にしてもCPUの処理、頂点フェッチ、頂点シェーダーのパフォーマンスには変化がありません。
もし、低解像度にしてもフレームレートが変わらない場合(GPUバウンドであることが前提)、頂点処理がボトルネックになっている証拠です。
頂点処理を軽くするには、少ないポリゴン数のモデルに変更するか、頂点シェーダーを簡略化します。もし、頂点フェッチが問題だと思う場合はカスタムモデルプロセッサーを作り、VertexChannelCollection.ConvertChannelContentを使ってPackedVectorフォーマットを使って頂点データを小さくします。例えばNormalized101010は法線データには向いていますし、HalfVector2はテクスチャ座標データの圧縮によく使われます。
もし、低解像度にしてフレームレートが上がった場合、ピクセル処理がボトルネックになっている証拠です。
ミップマップを使っている場合(使っていない場合はミップマップを使うようにするだけでパフォーマンスアップになります)、SamplerStates[n].MipMapLevelofDetailBiasに4や5を設定してみましょう。これでテクスチャがぼやけた感じになります。フレームレートがあがっていればテクスチャの読み込みがボトルネックになっているということです。この場合、DXT圧縮にしたり、テクスチャサイズを小さくしたり、テクスチャの数を減らすことでパフォーマンスアップになります。
次にピクセルシェーダーを単色を返すだけの単純なものにする。これはピクセルシェーダーとテクスチャフェッチに影響しますが、テクスチャフェッチについては既にテスト済みなので、このテストはミップマップバイアスを変更してもフレームレートが上がらなかった場合にします。この変更でフレームレートが上がった場合は、ピクセルシェーダーがボトルネックになっています。
まだ、ボトルネックが見つからない?と、いうことは残った可能性は#3、#6、もしくは#7になります。
マルチサンプルを有効にしてみて、フレームレートが変わらなければラスタライザがボトルネックになっています。
フレームバッファのピクセルフォーマットをSurfaceFormat.Bgr565などの小さいものに変更してみます。これでフレームレートが上がればフレームバッファがボトルネックになっています。
これでもフレームレートが上がらないということは消去法で深度/ステンシルバッファがボトルネックになっているということです。
元ネタ:
http://blogs.msdn.com/shawnhar/archive/2008/04/11/santa-s-production-line.aspx
前回と前々回で紹介した方法で処理落ちの犯人を割り出すことができました。犯人を割り出したら、次はどのようにして犯行現場を特定する必要があります。
今日、取調室に呼び出されたのはCPU。CPUは多彩な知能犯で、数百から数千もある複数の箇所を駆け巡り、ありとあらゆる方法で、時にはこちらが想像もしない方法で犯行を行います。CPUの犯行現場は数百から数千もある複数の候補箇所から割り出す必要があります。これら全ての候補ををしらみつぶしに探していくには途方もない労力と時間が必要になってしまいます。
では、どうすればCPUの犯行現場を押さえることができるのでしょうか?
実はこのCPU、根は正直者で「はい」「いいえ」で答えられる質問には常に正しい返事をしてくれます。この性質を利用することによって、数百、数千、たとえ犯行現場が1万箇所あったとしても14回以下の質問で犯行現場を見つけ出すことができます。
その方法とは、犯行現場になりそうな箇所に番号をつけてから「パフォーマンスを殺した現場は○番より下か?」という質問を繰り返すだけです。もし、犯行現場になりそうな場所が8ヵ所あった場合、最初に「犯行現場は4番以下か?」と質問し、答えが「はい」の場合は次に「2番以下か?」と質問します。最初の質問の答えが「いいえ」なら「6番以下か?」と質問になります。
言い換えれば、最初に犯行現場になりそうな箇所を半分にわけて、どっちのグループの場所で犯行を行ったのかを質問、その答えによって更に半分に分けて…という風に繰り返すわけです。
ここまでで気づいた人もいると思いますが、これは検索アルゴリズムである二分探索の方法そのものです。
実際にはタイムルーラーを使って常に二箇所の処理時間を測定していきます。まず最初にGame.UpdateとGame.Drawに掛かった時間を測定します。Game.Updateの処理時間がGame.Drawの処理時間より長く掛かっている場合は次にGame.Updateの中で処理している内容を二分して時間の測定をします。以下のコードは一例です。
// 上半分の処理時間測定 timerRuler.BeginMark("Upper", Color.Red); UpdatePlayers(); // プレイヤーキャラクターの更新 UpdateEnemyAI(); // 敵キャラクターAIの更新 timerRuler.EndMark("Upper"); // 下半分の処理時間測定 timerRuler.BeginMark("Lower", Color.Purple); UpdateEnemies(); // 敵キャラクターの更新 UpdateParticles(); // パーティクルシステムの更新 timerRuler.EndMark("Lower");
この結果、上半分(Upper)の処理時間が下半分(Lower)よりも時間が掛かっているとすれば、処理落ちの原因はプレイヤーキャラクターの更新部分か、敵キャラクターの更新部分のいずれかになります。次にそれぞれの結果を測定し、更にそれぞれのメソッド内でGame.Updateメソッド内で測定位置を変えていったようにしていきます。
タイムルーラーの特徴として、処理時間を視覚的に見ることができることです。下図のようにタイムルーラーの状態を見るだけで、処理落ちしているのか、どれくらいの時間を処理に費やしているのかというのが視覚的にすぐにわかります。
この例では処理落ちはしていませんが、仮にこの状態から更に最適化をしようとした場合、更新部分の処理時間が描画より掛かっていることが判ります(一段目の黄色い部分にはデバッグ情報表示に掛かった時間も含まれているので、二段目のバーにゲーム自体の処理時間を測定している)。
このコードのゲーム更新部分ではアニメーションの更新とキャラクターの移動しかしていないので、それぞれに掛かった時間を測定した結果が以下のようになっています。
アニメーション処理に時間が掛かっていることが判ったので、次にアニメーション処理内で時間を掛かっている場所測定するという風に繰り返します。
このコード内で犯行現場になりそうな箇所、つまり処理落ちしそうな原因となる箇所は大体20ヶ所程ありますが、「更新部分の処理時間の方が多いか?」と「アニメーション処理時間の方が多いか?」と、たった二つの質問しかしていないこの段階で残り4ヵ所程度まで絞り込むことができています。
このようにCPUパウンドの場合、処理速度に掛かった時間を測定をする箇所変えながら二分探索を使うことで、すばやく犯行現場を割り出すことができます。
つづく……
タイムルーラーを使ってゲームの更新部分と描画部分に掛かった時間は以下のコードで測定することができます。
/// <summary> /// ゲームの更新(衝突判定、入力処理、オーディオの再生など) /// </summary> /// <param name="gameTime">タイミング値のスナップショット</param> protected override void Update( GameTime gameTime ) { // タイムルーラーにフレーム開始を伝える timerRuler.StartFrame(); // 更新期間、"Update"の測定開始 timerRuler.BeginMark( "Update", Color.Blue ); // 他の処理... // 他のコンポーネントを更新 base.Update( gameTime ); // 更新期間、"Update"の測定終了 timerRuler.EndMark( "Update" ); } /// <summary> /// ゲームの描画 /// </summary> /// <param name="gameTime">タイミング値のスナップショット</param> protected override void Draw( GameTime gameTime ) { // 描画期間、"Draw"の測定開始 timerRuler.BeginMark( "Draw", Color.Yellow ); // 他の処理... // 他のコンポーネントを描画 base.Draw( gameTime ); // 描画期間、"Draw"の測定終了 timerRuler.EndMark( "Draw" ); }
また、以前の投稿で述べたようにCPUがGPUを待っている時間を測定することができます。PresentはGame.EndDraw内で呼ばれるので、EndDrawメソッドをオーバーライドして、タイムルーラーでマークすることでPresentに掛かった時間を測定することができます。
/// <summary> /// EndDrawをオーバーライドする /// </summary> protected override void EndDraw() { // EndDrawの中でPresentが呼ばれるのでbase.EndDrawメソッドを // マークすることでPresentを呼び出すのに掛かった時間を測定できる timerRuler.BeginMark("Present", Color.Red); base.EndDraw(); timerRuler.EndMark("Present"); }
これで、タイムルーラーを使って以下の時間を測定することができ、この情報を元にしてCPUバウンドかGPUバウンドなのかを知ることができます。
1の更新に掛かった時間が多すぎる場合、特にこれだけで16.6msを超えているようであれば、CPUバウンドということになります。
2の描画に時間が掛かりすぎている場合、描画中にGPUをブロックするようなコード(SetData、GetDataメソッド)がない限りは、これもCPUバウンドということになります。
3のPresentに時間が掛かった場合は、CPUがGPUを待っている状態なのでGPUバウンドということになります。
最後に1と2に掛かった時間が16.6msより少なく、3に殆ど時間が掛かってない場合は、CPUバウンドか、CPUとGPUがバランスの取れている状態の二つの可能性があります。この二つの可能性から絞り込むには前回と同じ手法を使います。
タイムルーラーはリアルタイムに処理時間を測定するので、判りやすいように動画にしてみました。
この動画では、GPUバウンドからバランスの取れた状態へ変更する様子と、描画方法を変更することによって、CPUとGPUのロードバランスが変わるという例を紹介しています。
具体的にはマテリアルバッチを用いた複数のキャラクターの描画からHWインスタンスを使った手法に変更することによって、CPUの処理速度が三倍になった変わりにCPUがGPUを待っている時間が倍になるというロードバランスがCPUからGPUへと変化したのが判ると思います。
ただし、CPUの待ち時間が倍になっただけで、GPUの処理量が二倍になった訳ではないということに注意する必要があります。GPUの処理量がどれだけ増えたかを推測するには、変更前のCPU待ち時間が14ミリ秒まで掛かっているに対して、変更後は15ミリ秒付近で終わっているのでGPUの処理量は1ミリ秒程増えたということになります。
これで処理落ちしている原因がCPUバウンドなのかGPUバウンドなのかが判りました。
つづく……。
パフォーマンス殺害の容疑者は常に二人居ます。CPUとGPUです。この二人の容疑者から考えられるのは以下の三つ。
まずはパフォーマンスを殺した犯人を特定するのが先決で、その後に犯行の動機や手段を洗い出します。
CPUバウンドかGPUパウンドを見つけだすには、FPSを測定する方法と、タイムルーラーを使う二つの方法があります。
FPSを測定する場合は、コードに簡単な変更を加える必要があります。このときに注意しないといけないのは、描画コードをコメントアウトしただけではCPUバウンドかGPUバウンドかは特定できないことです。コメントアウトをすることでFPS値は高くなりますが、描画にはCPUとGPUの両方が仕事をするので、どちらが犯人かは特定できません。
もっとも簡単な方法はGame.Updateメソッド内に以下の一文を加えるだけです。
// 1ミリ秒休むThread.Sleep(1);
もし、この行を加えてもFPS値が変化しないのであれば、GPUバウンドということになります。更にこのアイドル時間を少しずつ増やしていき、FPSが変更した時点のアイドル時間がそのままCPUがヒになっている時間を測定することができます。
もし、FPS値に影響するようであれば、CPUバウンドか、CPUとGPUのバランスが取れている可能性があります。
この場合、今度はCPUの作業量を減らしてCPUバウンドか、CPUとGPUのバランスが取れているのかを特定することになります。
CPU作業を減らす簡単な方法は、更新部分の処理を丸ごとスキップすることです(この方法はCPUがある程度の時間を使っている状態じゃないと使えないことに注意)。とは言っても、更新部分のコードは描画部分のコードにも影響するので(更新部分が呼ばれないと、敵とかが出現しなくなっちゃうから)、常に更新部分をスキップするのではなく、指定した期間だけ更新部分をスキップすることにします。
例えば、以下のコードのように、ゲームに関係ないキーなどを使うと良いでしょう。ここでは前回紹介したPROFILE構成を使っています。
protected override void Update(GameTime gameTime) { #if PROFILE // 1ミリ秒スリープ if (currentKeyboardState.IsKeyDown(Keys.PageUp)) Thread.Sleep(1); // 更新処理を丸ごとスキップする if (currentKeyboardState.IsKeyDown(Keys.PageDown)) return; #endif ....
この変更を加えた後にゲームを実行し、CPUバウンドかGPUバウンドかを特定したい場所まできたら、これらのキーを押してFPS値の変化をみます。
もし、スリープしてもFPSが変化しなければ、GPUバウンド
もし、更新処理をスキップしてFPS値が上がったら、CPUバウンド
もし、更新処理をスキップしてもFPSに変化がなく、スリープしてFPS値が下がるようであれば、CPUとGPUはバランスがとれた状態。
次回はタイムルーラーを使用してCPUバウンドかGPUバウンドなのかを判別する方法を紹介します。
元ネタ: http://blogs.msdn.com/shawnhar/archive/2008/04/07/how-to-tell-if-you-are-cpu-or-gpu-bound.aspx
PCやゲーム機の仕組みが非常に複雑になった現在、処理落ちしている状態、つまりパフォーマンス問題がある場合、その原因究明にはシャーロック・ホームズ(もしくは、コナン君、金田一君、夢羽など)と同じような探偵としてのスキルが求められます。 もちろん、ここでいうスキルというのは、殺人事件に出くわす確立が異常に高いというものではなく、事件を解決する能力です。
この事件を解決するには、探偵と同じように仮定を立て、証拠を集め、推理力を働かせることで犯人(パフォーマンス低下の原因)を特定しないといけません。
幸い、小説やマンガの中の探偵のように生命の危機に立たされることはないし(っていうか、コナン君とか金田一君に出会うこと自体が死亡フラグだよね)、なにより時間を巻き戻して殺害現場(パフォーマンス問題がある場所)を何度も繰り返し再生することで、パフォーマンスを殺す凶器になりそうなものを断定することができます。
例えば、氷の塊(氷の剣のトリックのタネ)があったとしたら、この氷の塊に該当するコードを一時的に取り除いて、殺害シーンを再生。もし、パフォーマンスが死ななかったら、この氷の塊がパフォーマンスを殺した凶器と断定できます。氷の塊を取り除いてもパフォーマンスが死んだ場合、氷の塊は凶器ではないということなので、つぎに凶器となりそうなコードを見つける作業を続けます。
犯罪捜査では犯人を特定するのにプロファイリングと呼ばれるものがありますが、プログラムの世界でもパフォーマンスを殺した犯人を特定する方法を同じくプロファイリング(性能解析)と呼びます。
XNA Game Studioでプロファイリングをするには、以下の下準備をします。
プロファイル用のソリューション構成をするには以下のステップを踏みます。
1. ビルドメニューから、構成マネージャを選択 して、構成マネージャを表示する
2. 構成マネージャダイアログ内の、アクティブ ソリューション構成のコンボボックス内の「<新規作成...>」を選択して「新しいソリューション構成」を表示する。
3. 新しいソリューション構成ダイアログで、名前を「Profile」、設定のコピー元にReleaseを設定し、OKボタンを押して決定。
4. プロジェクト・プロパティ画面で構成にProfileを選択し、条件付コンパイル シンボルに「PROFILE」を追加する。
これで、プロファイル用の構成ができました。
次に、プロファイルをするのに必要なUpdate設定にします。これは、IsFixedTimeStepにfalseを指定して可変更新に設定し、SynchronizeWithVerticalRetraceにfalseを指定して垂直帰線期間同期(V-Sync)を待たない設定にします。
#if PROFILE // プロファイル用コード // ここではV-Syncを解除し、可変更新モードに変更している graphics.SynchronizeWithVerticalRetrace = false; IsFixedTimeStep = false; #endif
上のコードでは、プロファイル用構成を設定したときに作ったPROFILEシンボルを使って#if、#endifでコードを囲むことによって、このコードがプロファイル用構成にしたときにのみにコンパイルされるようにしています。
ここで気をつけないといけないのは、プロファイル時には可変更新にする必要があるので、ゲームが固定更新向けに作られている、つまりGameTimeの値を無視している場合にはゲームの実行速度が変わってしまうということです。処理落ちがない状態だと、ゲーム更新速度が速くなりすぎて、時にはゲームをプレイするのが不可能なくらい速くなってしまうことがあります。処理落ちしている場合は逆にゲームの実行速度が遅くなるので、スローモーションでプレイしているようになります。プロファイルが必要なのは処理落ちしている状態なので、ゲームが速すぎるという問題はないでしょう。
また、IsFixedTimeStepはゲーム実行中に変更することができるので、プロファイルしたい場面までは固定更新にして、必要になったら可変更新に切り替えるということもできます。
つづく…
https://blogs.msdn.com/shawnhar/archive/2008/03/14/understanding-gpu-performance.aspx
もしあなたが60FPSのゲームを作ると決めた場合、ゲーム内全ての処理を1/60秒、つまり16.66ミリ秒以内に終わらせないといけません。ゲームの規模が大きくなればなるほど、処理する内容も多くなるとともに16.66ミリ以内に処理を終えることが難しくなってきて、ついには16.66ミリ秒以内に処理を終えることのできない状態、つまり処理落ち状態になってしまいます。
XNAフレームワーク上でゲームを作っている場合、固定更新設定にしていれば処理落ちした場合は描画部分をスキップするという機能があるので、ある程度の処理落ちは軽減することができます。ですが、この方法は描画をスキップしてごまかしているだけなので、ゲームのシミュレーション部分は処理落ちしなくとも、コマ落ち状態になってしまいます。また、隠蔽できないほどの処理落ちが発生している場合は効果がありません。
この処理落ちや、コマ落ち状態を防ぐには、最適化をする必要があります。
さて、どこから最適化をするべきでしょうか?コーディングするのに苦労した���雑な部分?それともポリゴン数を沢山使っているモデルを調べる?
答えはどちらでもありません。最適化をするときに最も重要なのは、ボトルネックとなっている部分を特定することです。あてずっぽうにコードを変えたり、データを変えたりするのは、あなたが超能力者でもないかぎり時間の無駄でしかありません。
<ボトルネックを見つける重要性> ある日、XNA向けのゲームを個人で作っている同僚のジェイスが処理落ち問題に直面していた時に私のところに相談してきたことがありました。 ジェイス「処理落ちしているんだ。原因は複雑な計算部分だと思うんだけど、良いアルゴリズム知らない?」 私「ボトルネックは特定した?」 ジェイス「きっと、複雑な計算部分だよ、それ意外に考えられない」 結局、その同僚は処理落ちの原因となっていると思われる箇所の最適化を始めました。 二日後…… ジェイス「計算部分を最適化したけど、ダメだった。なんかいい手はない?」 私「タイムルーラーを使って、ボトルネックを調べてみたら?」 ジェイス「うーん、とりあえず使ってみるよ」 30分後… ジェイス「ボトルネック箇所を見つけた!ぜんぜん関係ないところでパフォーマンス的に手を抜いていたところが原因だった!」 これはあてずっぽうに最適化するのに二日掛けても解決しなかったのが、正しい最適化手法を使うことでボトルネックの発見、修正まで30分しか掛からなかったという好例です。 </ボトルネックを見つける重要性>
<ボトルネックを見つける重要性>
ある日、XNA向けのゲームを個人で作っている同僚のジェイスが処理落ち問題に直面していた時に私のところに相談してきたことがありました。
ジェイス「処理落ちしているんだ。原因は複雑な計算部分だと思うんだけど、良いアルゴリズム知らない?」
私「ボトルネックは特定した?」
ジェイス「きっと、複雑な計算部分だよ、それ意外に考えられない」
結局、その同僚は処理落ちの原因となっていると思われる箇所の最適化を始めました。
二日後……
ジェイス「計算部分を最適化したけど、ダメだった。なんかいい手はない?」
私「タイムルーラーを使って、ボトルネックを調べてみたら?」
ジェイス「うーん、とりあえず使ってみるよ」
30分後…
ジェイス「ボトルネック箇所を見つけた!ぜんぜん関係ないところでパフォーマンス的に手を抜いていたところが原因だった!」
これはあてずっぽうに最適化するのに二日掛けても解決しなかったのが、正しい最適化手法を使うことでボトルネックの発見、修正まで30分しか掛からなかったという好例です。
</ボトルネックを見つける重要性>
処理落ちする原因は様々ですが、大別すると以下の三つしかありません。
CPUバウンドとはGPUに比べてCPUの処理量が多すぎてGPUがヒマになっている状態、つまりGPUがCPUに縛られている(バウンド)状態のことです。図にすると以下のようになっています。
この図の場合はゲームの更新部分(Update)の処理時間が掛かっているので、その間にGPUが前フレームの描画処理を終え、次のフレームの描画命令待ちになっています。
この状態で、描画するモデルの頂点数を少なくしたりする最適化を施しても、GPUが更にヒマになるだけでゲーム全体のパフォーマンスアップにはなりません。
これとは逆にGPUへの負荷が掛かり過ぎている状態をGPUバウンドと呼び、以下の図の状態になっています。
この場合によく見受けられる現象としては、Present内でGPUが前フレームの描画が終了するまでの待ちが入るので、見かけ上はCPUの処理時間が掛かっているように見えることです。こういった、CPUがGPUを待つ状態というのは動的頂点バッファへの書き込み時などにも発生することがあるので、CPUバウンドと勘違いしてしまうことがあることに注意しないといけません。
ですが、Presentの処理時間が掛かっているからといって、常にGPUバウンドであるとは限らないことです。Present内ではコマンドバッファ発行処理も含まれるので、多くの描画命令を発行している場合にもPresentの処理時間が掛かることもあるということです。
最後に3のケースですが、この状態になること自体が少ないうえに、仮にこの状態になった場合、更に二つのケースに分類されます。現状の方法が使っているプラットフォームに合わない方法で負荷が掛かりすぎているか、単にプラットフォームの性能限界に達しているかのいずれかです。前者の場合は、プラットフォームに合った方法に最適化することもできますが、後者の場合はコード的な最適化は限界に達しているので、ゲーム中のオブジェクト数などを少なくしたり、モデルデータを小さくするなどをします。
この三つのケース、特に処理落ちの原因がCPUバウンドかGPUバウンドであるかを判別することが最適化の最初の一歩とになります。
次回に続く……
訳者注:今回はちょっと口語訳っぽくしてみました。途中までフォーマルな感じで訳していたのですが、原文の雰囲気と合わなかったので、合わせて口語訳にしてみました。
この記事は記録(?)されたもので、Shanwは今いません(今週末に結婚するんだ、俺)(訳注:死亡フラグみたいですが、Shawnは無事に結婚して、新婚旅行から帰ってきました)
チェス盤と麦(もしくは米)の問題を聞いたことがあるかい?シェーダープログラマーは同じ問題を抱えているんだ。
<チェス盤と麦の問題とは>
13世紀のアラビアの数学者イブン・アル・バンナがチェス(西洋将棋)の問題解法で等比級数の解法について述べています。それによると、
むかし、チェスの発案者が、そのほうびに何がほしいといわれたとき、その人は控えめに、 「チェス盤の第一のマスに1粒のムギ、第二のマスに2粒のムギ、第三のマスに4粒のムギ、・・・・・・というように次々に二倍してチェス盤の全部を埋める分のムギ粒をください。」 と言ったといいます。
チェス盤は64に仕切られていますので、 1+2+22+23+・・・+263=264-1=18 446 744 073 709 551 615(粒) となり、大豊作のすべての麦を集めてもまかないきれない量になります。
引用元: http://www.nichinoken.co.jp/column/essay/sansu/2008_m02.html
</チェス盤と麦の問題とは>
僕たちは対応したいパラメーター(ライティング、テクスチャ、頂点カラー、フォグ、アニメーション、環境マッピング、マルチテクスチャ)並べることから始めたんだ。しかし、ブーリアン値をひとつ加えるごとに書かなければいけないシェーダーの数も、倍々に増えていき、”2のN乗”とか言う前に手に負えない数になってしまう。
プログラマブルシェーダーはこのジレンマを解決する最良の方法だ。確かに全ての組み合わせの数は途方もないものになるけど、実際のゲームではチェス盤の数マス分程度しか必要としないからね。プログラマブルということは、ゲームにあわせて開発者が必要な分だけ自由に実装できるってことだね。
しかし、現状、Windows Phoneではプログラマブル・シェーダーは使うことはできないんだ。
これはコメント欄での突っ込みどころ:僕個人的にシェーダーは大好きだし、将来的にサポートしたい機能のひとつ。でも、僕たちは時間的な問題でこの機能を今回のリリース向けに完成させることはできなかったんだ。将来リリースについて言える事はなにもないというのが現状(訳注:正式発表以外でスケジュールに関することに言及してはダメという大人の事情)。
そのかわりに、幾つかのビルトイン・エフェクトを提供することにした。これらのエフェクト(特に初心者にとって便利だと思うよ)はWindows、Xbox 360の両方でも使えるけど、目的としてはプログラマブル・シェーダーの使えないWindows Phone向けに設計、最適化されているんだ。
ビルトイン・エフェクトとして必要な機能はなにか?
時には妥協しなくてはいけない。スケジュールは変更できない。僕らは対応したい機能リストから、しぶしぶながら削除を繰り返し、6個の機能まで絞り込み、さらに考えられるパラメーターの組み合わせに対応することを諦めるしかなかった。本当に必要とする機能のみに絞り込んだお陰で、2のN乗という指数的な量の作業量をせずに済んだ。幾つかのパラメーター、World、View、Projectionとフォグ関連のパラメーターは全てのエフェクトに共通してあり、その他は多少の差がある。例えば、BasicEffectとSkinnedEffectは同じライティングモデルを使っているけど、DualTextureEffectはライティングをサポートしていないなど。
最終的にXNA Game Studio 4.0では5つのビルトイン・エフェクトをサポートすることになり、それらは78のシェーダーの組み合わせからなっている。
http://blogs.msdn.com/shawnhar/archive/2010/04/28/new-built-in-effects-in-xna-game-studio-4-0.aspx
Game Studio 4.0で、BasicEffect API自体は変わっていませんが、今までより更に最適化が進んでいます。
以前のバージョンでのBasicEffectはシェーダープログラムの手始めとして初心者向けに設計されたものでした。なぜならシェーダープログラムの知識のある人達は直ぐに自前のシェーダーを書くだろうと思っていたからです。ですから、BasicEffectの最適化には多くの時間を割かず、WindowsとXbox 360上でそれなりの速度がでる程度の最適化しかしていませんでした。
Windows Phone対応によって、この優先度は以下の二つの理由によって変更されました。
その名前にも関わらずBasicEffectの実装は複雑です。公開されているHLSLソースコードを見れば分かりますが、以前のバージョンでは12の頂点シェーダーが以下のパラメーターの組み合わせに対応しており、
4つのピクセルシェーダーが以下の組み合わせに対応しています。
BasicEffectには複数のパラメーターがありますが、これら全ての組み合わせごとに最適化はしていませんでした。例えばフォグ計算は常にしており、FogEnabledプロパティによってパラメーターの値を変えてフォグの有無を再現しているだけでした。
私たちは最適化された複数のシェーダーを提供する(GPU命令数を少なくする)のと、少数のシェーダー(メモリオーバーヘッドを減らし、開発/テストコストを抑える)との間でバランスを取らないといけません。私達がWindows Phone向けに、このバランスを再検討した結果、最適化されたシェーダーを追加することにしました。その結果、XNA Game Studio 4.0のBasicEffectは32のシェーダーの組み合わせを持つことになりました。
頂点シェーダーは以下の20の組み合わせがあります。
これに加えて10個のピクセルシェーダーがあります。
つまり:
シェーダープログラマーが気をつけなければいけないことのひとつとして、綺麗で使いやすいEffectパラメーターを設計した場合、これらのパラメーターフォーマットが常にHLSL最適化に向いたものにならないということです。
D3Dの場合、”preshader(プリシェーダー)”と呼ばれる機能を使って最適化を試みます。HLSLコンパイラーが計算結果が全ての頂点、ピクセルで同一となる部分を抜き出し、この部分を描画直前にCPUで実行するという仕組みです。この仕組みは素晴らしいのですが、幾つかの弱点があります。
Game Studio 4.0では、このプリシェーダーと同等の機能をC#で実行できるようになりました。新しく追加された以下のメソッドをオーバーロードすることで、このメソッドはEffectPass.ApplyがパラメーターをGraphicsDeviceに設定する直前に呼び出されます。
protected override void OnApply()
この新しい仕組みによって、BasicEffectではプロパティがどのようにHLSLパラメーターに割り当てられているかを気にすることなく、BasicEffectに必要なプロパティを用意することができます。プロパティが変更時に、私たちは単にダーティフラグを設定するだけです。後でOnApplyメソッドが呼ばれたときに、このダーティーフラグの結果によって必要な分だけのHLSLパラメータを計算、設定しています。この機能を使って私たちは他にも以下のパラメーターを計算しています。
更に、数式の簡略化をし、行列計算を使って三つの平行光源計算を一度にする最適化も施しました。読みづらくなってしまいましたが、命令数はより少なくなりました。
また、フォグの計算方法を変えました。以前は視点から頂点までの距離によってフォグの掛かり具合が変わるDistance Fog(フォグの境界線が曲線になっているところに注目)を使用していました。4.0では単純な深度フォグ、つまり頂点のZ値によってフォグの掛かりかたが変わります。見た目の変化は少ないのですが、深度フォグの方が計算量は遥かに少なくなっています。
最適化の一例として、ライティングなし、フォグなし、頂点カラーとテクスチャありの場合の使用命令数を比較すると以下のようになります。
質問される前に自分で質問: 「ソースをZipでくれ」
もちろん、公開したいねぇ。でも、まだ公開する準備を始めていないので、追って連絡があるまで待たれよ!
http://blogs.msdn.com/shawnhar/archive/2010/04/25/basiceffect-optimizations-in-xna-game-studio-4-0.aspx
XNA Game Studio 4.0ではViewportオブジェクトを使う場面で便利な機能を追加しました。まずは以下のコンストラクタを追加しました。
public Viewport(Rectangle bounds);public Viewport(int x, int y, int width, int height);
そして、BoundsプロパティをViewport、Texture2DとPresentationPrametersへ追加しました。
public Rectangle Viewport.Bounds{ get; set; }public Rectangle Texture2D.Bounds{ get; }public Rectangle PresentationParameters.Bounds{ get; }
このことにより、レンダーターゲットと同じサイズのビューポートを設定するのが、以下のように簡単にできるようになりました。
GraphicsDevice.Viewport = new Viewport(renderTarget.Bounds);
もしくは、テクスチャをビューポートいっぱいに描画するのも、簡単に書けるようになりました。
spriteBatch.Draw(cat, GraphicsDevice.Viewport.Bounds, Color.White);
原文: http://blogs.msdn.com/shawnhar/archive/2010/04/14/viewport-tweaks-in-xna-game-studio-4-0.aspx
前回のポストの原文の方には「2時間って時間掛かりすぎじゃない?」や「いやいや、2時間は短い方でしょ」などのコメントが寄せられていました。
このコメントに対してShawnは以下のように答えています。
2時間というのは単に開発に掛かった時間で、非常に短い時間です。実際にはこの他にもドキュメントの更新や、テストにも時間が掛かっています。
この変更で開発に掛かった時間を細かく分けると以下のようになります。
ですが、新しい機能を追加するには更に時間が掛かります。たとえばステートオブジェクトの実装を例にすると、
APIの設計とレビュー: 2日
C# APIの実装: 1日
Windows C++/CLIの実装: 半日
Xbox C++ interopの実装: 半日
Windows Phone C++interopの実装: 半日
新しいユニットテストを書く: 1日
既存のテストの更新: 半日
クリーンビルド: 1時間
自動テストの実行: 20分
while(テスト通らず)
{
問題修正: 最大で5分くらい
ビルド: 1時間
if ( 繰り返し回数 > 10)
彼女や奥さんに電話: 「今日は遅くなる…」
}
コードレビュー: 1時間
チェックイン!!
のようになります。
このビルド待ちの間はブログの記事を書いたり、フォーラムに返信していたりします。
と、Shawnは答えています。
ちなみに私の場合、ビルド待ちの間は次の作業の内容確認やメールなどの雑務処理をしています。また、作業用PCは2台あるので、忙しいときにはこのプロセスを2台同時進行でやってたりします。
新しい機能についてのブログポストを書くとき、よく、今までに書いたデザインドキュメントや、バグデータベースを参考にします。殆どの場合はその内容をそのまま引用することはないのですが、今回は既存の文章がそのまま使えるので、盗用することにしました。
すべでは私が登録したバグの記述から始まります。
「GameTimeクラスのRealTimeメンバーは混乱のもとだし、特に良い使い道もない。更に通常、常にRealTime以外のメンバーを使うことが良いというのであれば、混乱を少なくするためにRealTimeメンバーを削除するのがいいのではないか?」
これは既存のAPIに変更を加えることになるので、DCR(Design Change Request、つまり設計変更要求)のプロセスを通す必要がありました。このプロセスでは変更による影響を理解し、大きな問題とならないことを確認し、または変更を拒否する理由なども考慮します。
以下の文章は私がDCR向けに書いたものです。
GameTimeクラスは以下の四つのプロパティがある。
このDCRではElapsedRealTimeとTotalRealTimeプロパティの削除と、それらのプロパティを引数として使っているコンストラクタの削除を提案する。
これらのプロパティは最適に計算されるゲーム内時間ではなく、実時間経過を直接使いたい場合に用意されたプロパティであるが、以下の問題がある。
開発コスト: 2時間
このDCRは承認され、Gamer Studio 4.0でElapsedRealTimeとTotalRealTimeプロパティは削除されました。
原文: http://blogs.msdn.com/shawnhar/archive/2010/04/13/elapsedrealtime-and-totalrealtime-in-xna-game-studio-4-0.aspx
Game Studio 4.0ではResolveBackBuffer APIとResolveTexture2Dクラスが削除されました。
このAPIは基本的なRenderTarget2D(GPUによって描画された結果がテクスチャとなる)と同様のもので、多少の違いがあるだけでした。重複する機能を提供することが常に悪いこととは限りませんが、完全に同じ機能を提供するときには注意が必要です。
重複している機能を提供する場合、それらの機能を提供したと仮定して、フレームワークを使っている人たちからの「どっちを使ったら良いの?」という質問にどう答えるかが判断基準となります。もし「BよりもAを使うと良いよ。Bのことは忘れちゃってください」や「どっちを使っても緒だよ」と答えるようであれば、二つとも提供する必要はないということになります。
以下は、ResolveBackbufferとRenderTargetの二つを提供した場合のガイダンスです。
結果として、パフォーマンスを気にするのであればレンダーターゲットを常に使うべきということになります。ResolveBackBufferは場合によって変わってきますが、レンダーターゲットを使うよりも速くなることはありません。
<余談>
パフォーマンス差を隠蔽するような互換性を提供することは非常に複雑な問題です。すべてのプラットフォームで同一のパフォーマンスを提供することは明らかに無理っ!(Windows PhoneはXbox 360よりも確実に遅いプラットフォーム)。また同等のパフォーマンス特性を提供することもできません(PC上でCPUがボトルネックなっているゲームを、他のPCで動作させるとGPUがボトルネックになったりする)。だからといって、パフォーマンス差を完全に無視するというわけではありません。例えば
アリスが「非線形四元張寸(Flobydryglap)アルゴリズムの実装方法」というチュートリアルを書いたとします。アリスはXbox 360開発者で、レンダーターゲットとResolveBackBufferの二つにパフォーマンス差がないことに気づいたので、たまたまResolveBackBufferを使うことに決めました。
このチュートリアルを見たボブは、Windows Phone上で使ってみることにしました。ですが、あまりのパフォーマンスの低さにがっかりしてしまいました。友人のチャーリーが見てみると、ボブがランドスケープモードを使っていることが原因だと気づきます。レンダーターゲットを使う方式に変更したところ、数倍の速度を得ることができました。
厳密にいえば、このコードは「動いた」訳ですが、パフォーマンス差が特定のプラットフォームに特化したコードとなってしまいました。
</余談>
では、パフォーマンスを気にする必要がない人たちにとってはどうでしょうか?パフォーマンス差を気にせずにバックバッファからデータを読み込みたいケースというのはあるのでしょうか?私たちは以下の二つのケースがあると考えました。
両方ともGPUのテクスチャメモリではなく、バックバッファーのデータをCPUによってメインメモリにコピーします。そこで、私たちは以下の変更をすることにしました。
GetBackBufferDataはCPUによる読み込みをします。ですから、どのプラットフォームでも速度はでませんが、スクリーンショットやユニットテストをするのには便利なAPIです。このことによって、「余談」の中にあるようなプラットフォーム間でのパフォーマンス差に驚くような場面もなくなった訳です。
と、いうのが計画だったのですが、
最近になってから、D3Dランタイム、コンポジッター、そしてイメージスケーラーの関係でWindows Phone上ではGetBackBufferData(同様にResolveBackBufferも)を提供できないということに気づきました。
そんな訳なので、しぶしぶながらこの機能をReachプロファイルからGetBackBufferDataを外すことになりました。Game Studio 4.0としてはサポートしていますが、Windows、Xbox 360上のHiDefプロファイルのみでのサポートとなります。これはハードウェアではなく、ソフトウェアの問題なので将来のバージョンではReachプロファイルでも使用できる可能性があります。ですが、他に優先度の高い作業で忙殺されているので、4.0のリリース向けにはこの問題は解決できませんでした。
http://blogs.msdn.com/shawnhar/archive/2010/03/30/resolvebackbuffer-and-resolvetexture2d-in-xna-game-studio-4-0.aspx
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は残っています。学び、愛し、共生せよ :-)
以下は変更点です。
よく以下のようなコード書こうとしているのを見かけます。
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); }
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をひとつのメソッドにまとめることで、以下の利点があります。
クリエーターズクラブオンラインにあるブルームポストプロセスサンプルの以下の行にはバグが潜んでいます。
renderTarget1 = new RenderTarget2D(GraphicsDevice, width, height, 1, format);
問題点として、このレンダーターゲットを指定した時に明示的に深度バッファを外していません。ブルームプロセスには深度バッファが必要ないのにも関わらず、デフォルトの深度バッファが使われ続けることになるので、このレンダーターゲットと深度バッファは互換性のあるフォーマットにしていないといけません。
もし、このサンプルのバックバッファフォーマットをマルチサンプリングにした場合、デフォルトの深度バッファもマルチサンプリング用のものになります。しかし、ブルーム用のレンダーターゲットはマルチサンプリングフォーマットではないので、実行時にエラーとなってしまいます。
この問題はブルーム用のレンダーターゲットにバックバッファフォーマットと同じマルチサンプリングフォーマットを指定するか、ブルーム処理をする時に以下のように明示的に深度バッファを外すことで解決できます。
DepthStencilBuffer previousDepth = GraphicsDevice.DepthStencilBuffer; GraphicsDevice.DepthStencilBuffer = null; DrawBloom(); GraphicsDevice.DepthStencilBuffer = previousDepth;
美しくないし、わかりづらい。私たちはこのコードをサンプル内に入れるのを忘れてしまいました。多くの人たちが同様のミスを犯しているのを見かけます。
このことについて更に考え、いくつかのことに気づきました。
私たちはDepthStencilBufferクラスは必要ないものだと考え、削除してしまうことにしました。その代わりに、深度バッファのフォーマットはレンダーターゲットを生成するときに指定できるようにしました。もし、以下のコードを書いた場合、
new RenderTarget2D(GraphicsDevice, width, height);
深度バッファのないレンダーターゲットを生成することができます。深度バッファを使いたい場合、以下のコンストラクタのオーバーロードを使います。
new RenderTarget2D(GraphicsDevice, width, height, false, SurfaceFormat.Color, DepthFormat.Depth24Stencil8);
注釈1: DepthFormat.Noneを指定することで明示的に深度バッファのないレンダーターゲットを生成することができます。
注釈2: MRTを使用する場合、深度バッファは最初のレンダーターゲットのものを使います。
この変更により、前述の問題が起きることはなくなりました。
このデザイン変更だと明示的に深度バッファを共有できなくなってしまうのでメモリの無駄遣いになってしまうのではないのか?と考える人もいると思います。
問題ありません、重要なのは命令文的APIから宣言的APIへとシフトしたことです。明示的に深度バッファを生成し、オブジェクトのライフタイムを管理し、いつ使いたいのかを指定する命令文的APIに対して、どんなフォーマットを使いたいのかを宣言するだけでフレームワーク側が常に最適な処理を行うことができるようになりました。
あなたが提供する重要な情報が二つあります。
この情報に基づいて、場面ごとによってXNAフレームワークは最適な実装戦略を選択します。
ぶっちゃけ、この深度バッファ共有最適化はまだ実装されていません。現在の予定では4.0のリリースまでには実装することになっていますが、なんらかの原因で間に合わなくなっても私(Shawnの事)叩かないで下さいね。
http://blogs.msdn.com/shawnhar/archive/2010/03/26/rendertarget-changes-in-xna-game-studio-4-0.aspx
突然ですが、Twitterでつぶやきはじめました。アカウント自体は一年くらい前に作ったのですが、つぶやくことがなくて放置していたのですが、今年に入ってXNAチームでもTwitter使いましょうという話がきたので、遅ればせながらはじめることにしました。
以下はXNAチームメンバでXNAのことをつぶやいている人達のTwitterアカウントです。
Shwan Hargreaves
Michael Klucher
Tom Miller
Jason Kepner
Mitch Walker
Nick Gravelyn
Rebecca Heineman
なんとなく、趣味でやってるプログラムや、うちのねこさん達に対するつぶやきが多くなりそうな気がしますが、XNAのこともつぶやける範囲でつぶやいていこうと思っています。
以前のXNAでは、VertexBuffer(頂点バッファ)は弱い方定義がされたバイト列の入れ物でしかありませんでした。これとは別にVertexDeclaration(頂点宣言)オブジェクトによって、これらのバイト列をどのようなフォーマットとして扱うのかを指定していました。
XNA Game Studio 4.0ではVertexBuffer生成時にVertexDeclarationを指定することでVertexBufferとVertrexDeclaratoinを関連付けるようになりました。このことによってVertexBufferは強い型定義となり、頂点宣言を含む必要な情報を提供できるようになりました。
以下は4.0以前での頂点バッファの使用例です。赤でハイライトされた部分が問題点です。
// 生成 VertexDeclaration decl = new VertexDeclaration(device, VertexPositionColor.VertexElements); int size = VertexPositionColor.SizeInBytes * vertices.Length; VertexBuffer vb = new VertexBuffer(device, size, BufferUsage.None); vb.SetData(...); // 描画 device.VertexDeclaration = decl; device.Vertices[0].SetSource( vb, 0, VertexPositionColor.SizeInBytes ); device.DrawIndexedPrimitives(...);
何回も扱う頂点フォーマットを指定しているので、間違いを起こす危険性があります。また、つねに正しいVertexDeclarationや頂点ストライドをVertices.SetSourceで指定しないと正しく動作しません。
この弱い型定義設計はランタイム実装を難しくします。なぜなら、VertexBuffer自体には頂点フォーマット情報がないので、フレームワークが異なるプラットフォームやハードウェア用に最適化することができません(少なくとも実際に描画命令が発行するまでの間、しかも描画時には既に正しい最適化をするには遅すぎる)
例えば、コンテントパイプラインの中で頂点バッファはXbox 360用にエンディアン変換をしますが、ContentTypeWriter<VertexBufferContent>自体にはこの変換を行うための十分な情報がありません。そのかわりに、VertexContent.CreateVertexBufferにターゲットプラットフォームを指定する必要がありました。
新しい対応プラットフォームを増やしたことにより、それぞれのプラットフォームに特化したデータを柔軟に生成する必要性が大きくなり、その為には扱うデータの情報が必要になってきます。強い型定義の頂点バッファはAPIをシンプルにし、エラーが少なくなり、しかもランタイム実装の柔軟性を上げてくれるという良いこと尽くめです。
VertexElementやVertexDeclarationは4.0でもありますが、以下の変更が加えられています。
そして、重要な変更点として、
これは単純にVertexBuffer自体にVertexDeclarationの情報があるので必要がなくなったからです。私自身、Game Studio 3.1から4.0へ変更するのはとても楽だということに気づきました。なぜならGraphicsDevice.VertexDeclarationを使っている行を削除するだけで動作するからです。
VertexBufferのコンストラクタの引数が変わりました。
また、
4.0では前述のコードが
// 生成 VertexBuffer vb = new VertexBuffer(device, typeof(VertexPositionColor), vertices.Length, BufferUsage.None); vb.SetData(...); // 描画 device.SetVertexBuffer(vb); device.DrawIndexedPrimitives(...);
と、なります。頂点フォーマットを指定する場所が一箇所になり、コード量が少なくなり、潜在的なエラーの可能性も減っています。
この変更により、ひとつの頂点バッファの中に異なる頂点フォーマットをオフセットを利用して複数格納するということができなくなりました。この手法は頂点バッファの切り替えコストが高いとき(DirectX 7時代)は有効な手段で、複数のモデルをひとつの大きな頂点バッファに格納するなどして使われていました。しかし、頂点バッファの切り替えコストが低くなっている現在では最適化手法としての効果は薄くなりました。
前述のコードサンプルの中でtypeof(VertexPositionColor)を指定していることに気づいたでしょうか?VertexBufferはどうやってタイプ情報からVertexDeclaration情報を得ているのでしょうか?
それは新しく追加されたインターフェースを介して行われます。
public interface IVertexType { VertexDeclaration VertexDeclaration { get; } }
このインターフェースはあらかじめ用意されている頂点構造体の全てで実装されています。このインターフェースを実装しているタイプからはVertexDeclaration情報を得ることができます。
もし、頂点バッファ生成時にIVertexTypeが実装されていないタイプを指定した場合、例外が発生します。モデルデータを読み込む時などによくあるシチュエーションとして、.Netタイプでは指定できない頂点フォーマットを使いたい場合にバイト列として読み込みたい時があります。この場合、VertexDeclarationを指定するコンストラクタを使います。
カスタム頂点構造体を作るときには、あらかじめある頂点構造体と同じように使えるようにIVertexTypeを実装すると良いでしょう。
struct MyVertexThatHasNothingButPosition : IVertexType { public Vector3 Position; public readonly static VertexDeclaration VertexDeclaration = new VertexDeclaration ( new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0), ); VertexDeclaration IVertexType.VertexDeclaration { get { return VertexDeclaration; } } };
DrawUserPrimitives<T>とDrawUserIndexedPrimitives<T>は頂点バッファと違ってマネージ配列を使用します。VertexBuffer以外を指定した場合にどうやってVertexDeclarationを取得できるのでしょうか?
4.0以前は複数の頂点ストリームをVertexElement.Streamプロパティを使うことで、ひとつのVertexDeclarationに格納し、以下のように頂点ストリームを指定していました。
device.Vertices[0].SetSource(vb1, 0, stride1); device.Vertices[1].SetSource(vb2, 0, stride2);
4.0ではVertexBufferとVertexDeclarationは一対一の関係になっています。つまり、複数のVertexBufferがあれば、同じ数だけVertexDeclarationがあることになります。ですから、VertexElement.Streamプロパティは必要がなくなり、複数の頂点ストリームは以下のように指定します。
device.SetVertexBuffers(vb1, vb2);
新しい頂点バッファの変更にともなって、コンテント・パイプラインにもいくつかの変更がなされています。
原文: http://blogs.msdn.com/shawnhar/archive/2010/04/19/vertex-data-in-xna-game-studio-4-0.aspx