最初は、モデル ファイルの読み込みから VertexBuffer の作成までを 1 つの処理とし、Game Thread とは別の Thread へ渡してみましたが、上手く動きませんでした。Game Thread が呼び出す Game.Draw() では SpriteBatch などが描画処理を行いますが、その最中に別の Thread が VertexBuffer.SetData() を呼び出すと、どちらかの Thread の処理がコケます。
僕は GPU が絡む部分がよくわからないので、詳しい人からのツッコミが欲しい所ですが、恐らく、どちらも GPU へ命令を渡す部分であり、そこでの競合が発生するのだと思います。
こういう問題があったため、モデルのロード処理を、まずは以下の 2 つに分割しています。
- モデル ファイル読み込みから頂点データを作成
- 1 で作成された頂点データから VertexBuffer を作成
こうしておき、1 だけを別の Thread へ渡します。この時、コールバック メソッドも同時に渡し、1 を終えたら呼び出されるようにします。そして、コールバック メソッドは 2 を Game Thread で管理するキューへ入れます。
この時、更に 2 を分割してからキューに入れます。僕の用いるモデルは複数の VertexBuffer を用いるため、VertexBuffer の単位で処理を分割してキューに入れます。XNA の Model クラスで喩えるならば、ModelMeshPart を個別に構築する感じです。
キューに入れられた処理は、Game Thread が呼び出す Game.Update() で順に取り出され、VertexBuffer を作成します。そして、何度かの Game.Update() の呼び出しで全ての VertexBuffer の作成が完了したら、モデルのロードが完了したということになります。
要するに、1 だけを非同期にし、2 を Game.Update() 内で行うようにしたわけです。そして、2 を可能な限り細分化し、Game.Update() での負荷を下げたという感じです。
1 の非同期処理は、僕は ThreadPool を利用して Thread に割り当てています。ただし、そのまま使うと Thread 数の制御ができないため、ここでもいったんキューに入れ、Thread 数を制御しながら割り当てています。
で、ここまでやってデモ アプリを計測したら、1 を非同期にする程でもなかったというオチでしたが、今後のコード次第ではどうなるか分からないので、このパターンのままやろうかなと。
ContentManager.Load() でロードする場合でも、その単位でキューに入れれば Game.Update() の負荷を下げられるんじゃないかと思います。
なお、デモ動画の段階では、1 の処理完了で即座にコールバック メソッドを呼び出していますが、今はこれもいったんキューに入れ、Game.Update() 内でコールバックが呼び出されるようにしています。
基本的には、多少回り道をする処理となっても、同期をとる箇所をまとめてしまう方が、他の部分で lock を書いたりせずに済んで見通しが良いのではないかと思います。lock 漏れも怖いですし、それらのデッドロックも怖いですし。
ソースコードの例としては、非同期処理については、以下のコードを HTTP 通信の非同期処理に用いています。
https://github.com/willcraftia/Blocks/tree/master/Framework/Threading
上記はモデルのロード処理ではないですが、同様のパターンを用いて非同期処理を行なっています。
モデルのロード部分は、専用モデルを用いていることから複雑でありオススメできないサンプルですが、興味のある人は以下などをどうぞ。
https://github.com/willcraftia/Blocks/tree/master/Blocks/Content
上記にある InterBlockMeshLoadQueue が非同期処理、BlockMeshLoadQueue が VertexBuffer 分割キューです。
※master にあるコードなのである日突然消えたりするかもしれません。
なるべく短命なオブジェクトが生成されないように工夫してみたつもりですが、どうですかねぇ・・・。