次の方法で共有


ContentStreaming サンプル

ContentStreaming サンプルでは、常にビデオまたはシステム RAM で適正以上のデータを表示しなければならないアプリケーション用の、バックグラウンドのストリーミング コンテンツが示されます。

Ee416397.d3d10_sample_ContentStreaming(ja-jp,VS.85).jpg

パス

ソース SDK ルート\Samples\C++\Direct3D10\ContentStreaming
実行可能ファイル SDK ルート\Samples\C++\Direct3D10\Bin\プラットフォーム\ContentStreaming.exe

サンプルの概要

ゲームの複雑性が増し、ゲーマーに必要な世界が拡大する中、固定メモリーの制約の範囲内で、コンテンツの拡大化 (一般にはゲームの拡大化にすぎない)、つまり大きくてシームレスな世界を描画できる能力が必要不可欠になっています。ゲームレベルのリソースが GPU メモリーに収まらない、またはゲーム データがシステム メモリーに収まらない場合は、それらのデータのサイズを減らすという解決方法もあります。ただし、それが常に最適なソリューションだとは限りません。高品質だけでなく容量も必要な場合もあるのです。ContentStreaming サンプルでは、現在表示可能な、または近い将来に表示可能になるデータだけを、メイン レンダリング スレッドを可能な限り停止させることなくロードする最適な方法を示します。

データの詳細なロード方法について説明する前に、このトピックではさまざまなストリーミング方法で使用される基本的な概念をいくつか説明します。それらの概念とは、リソース再利用キャッシュメモリー マップ I/O、およびプレースホルダー メッシュです。これらを説明した後で、オンデマンド方式でデータのストリーミングを実行するさまざまな方法について説明します。

リソース再利用キャッシュ

新規データのロード時にアプリケーションによって作成される GPU リソースの数を制限するために、ContentStreaming サンプルではリソース再利用キャッシュを使用します。リソース再利用キャッシュでは、固定数の GPU リソースを追跡します。

アプリケーションは、リソースをロードする必要がある場合、リソース再利用キャッシュ内で現在使用されていない互換性のあるリソースを探し、それを代わりに使用するだけです。適切なリソース タイプが見つからなかった場合、アプリケーションでは該当するタイプの新しいリソースを作成してリソース再利用キャッシュに追加するか、互換性のあるリソースが解放されるまでそのリソースのロードを待ちます。このアプローチの利点は、新しいテクスチャーを作成しようとする際にレンダリング スレッドの停止を防止し、アプリケーションではそのメモリー使用量を特定の値に制限できる点です。ただし、テクスチャーを最大限再利用する場合に一定サイズのテクスチャーしか使用できないという制限がアーティストに課せられるという大きな欠点もあります。テクスチャーの再利用がさほど必要でない場合は、メイン レンダリング スレッドの停止回数が減ります。テクスチャーを作成するコストは、テクスチャーのサイズと共に増えます。

メモリー マップ I/O

メモリー マップ I/O は、メモリーにデータを移動する際に ReadFile が呼び出されないという点で、従来の I/O と異なっています。その代わり、該当するファイルは、アプリケーションの仮想アドレス空間にマッピングされ、必要に応じてページ インされます。メモリー マップ I/O を使用するには、CreateFile を使用して、通常と同じようにファイルを開きます。ただし、該当するファイルからデータを直接読み込むのではなく、ファイル ハンドルを指定して CreateFileMapping を呼び出します。この関数により、ファイル マッピング オブジェクトが作成されますが、ディスク上のデータがアプリケーションの仮想アドレス空間にマッピングされることはありません。それを実行するには、アプリケーションでは MapViewOfFile を呼び出す必要があります。この関数からは、該当ファイルの先頭を表す仮想アドレスへの実際のポインターが返されます。

メモリー マップ I/O の使用に関しては、いくつかの注意点があります。まず、MapViewOfFile の呼び出し時に指定する範囲が大きすぎると、アプリケーションで仮想アドレス空間を使い果たす可能性があります。たとえば、次のシナリオを検討してみましょう。あるアプリケーションが持つ 4 GB のパックされたファイルには、そのアプリケーションの全データが含まれています。このアプリケーションは、MapViewOfFile を呼び出し、マッピング対象のバイト数として 4 GB を渡すことにより、このファイルの内容全体をその仮想アドレス空間にマッピングしようとします。このような大きなマッピングを処理できるほど十分な仮想アドレス空間を Windows が見つけることができないので、32 ビット アプリケーションの場合は失敗します。ただし、64 ビット アプリケーションの場合、そのような問題は発生しません。

32 ビット プラットフォーム上でのアドレス空間の不足に対処するため、アプリケーションでは大きなファイルにアクセスする際、ファイル マッピングを複数使用する必要があります。たとえば、4 GB のファイルは、一度に 500 MB しかマッピングできないスライディング ウィンドウを使用してマッピングすることができます。プレイヤーが次の領域に移動する際、ファイル マッピングはパックされたファイル内で該当領域をストライドさせます。ContentStreaming サンプルの場合は、事前設定のチャンク サイズに基づき、同一のファイル内でファイル マッピングが複数使用されます。仮想アドレス空間がすべてマッピングによって消費されないように注意する必要があります。新しいチャンクのマッピングが必要な場合は、LRU スキームを使用して、マッピングから削除するチャンクが決定されます。

また、MapViewOfFile では、メモリー割り当て境界にアライメントされたデータしかマッピングすることができません。この点にも注意する必要があります。ほとんどのシステムでは、64 KB が境界になります。これは、パックされたファイルにデータをパックする場合に重要です。すべてのファイルが高密度にパックされた 4 GB のファイルの場合、500 MB のチャンクは、その先頭が 64 KB の境界上にないと、マッピングすることができません。これに対処するため、ContentStreaming サンプルで使用するパックされたファイルでは、マッピングの対象として事前に定義されるチャンクという領域が作成されます。どのチャンクにも、一連のリソース ファイルが含まれます。チャンクは、64 KB 境界に配置されるので、MapViewOfFile を使用して簡単にマッピングすることができます。

パックされたファイル

簡単なインストール、一貫したディスク アクセス、および圧縮を実現するために、ゲーム開発者は単一のパックされたファイルに一連のリソースをパックすることがよくあります。パックされたファイルの性質は、ゲームによって異なります。ContentStreaming サンプルの場合は、圧縮を省き、主にメディア フォルダー内のデータから構成される簡単なパックされたファイルを作成します。パックされたファイルは、ContentStreaming サンプルが初めて実行されるときに作成されます。パックされたファイルは、ユーザー アカウント制御と互換性のあるユーザーごとのディレクトリ (今の場合は CSIDL_LOCAL_APPDATA + "\ContentStreaming") 内に格納されます。パックされたファイル内のインデックスを見ると、そのパック内の全ファイルの位置とサイズがわかります。インデックスに従い、一連のリソース ファイルが一連のチャンクにパックされています。チャンク サイズは、メモリー マップ I/O の使用時にアプリケーションが仮想アドレス空間を使い果たさないような値に設定されています。前述のように、これらのチャンクは、MapViewOfFile を呼び出してマッピングするために 64 KB にアライメントされている必要があります。

実行時、パックされたファイルのインデックスは、ReadFile を呼び出してメモリーにロードされます。この処理は、メモリー マップ I/O の使用とは無関係に実行されます。パックされたファイルの取得時に、CPackedFile クラスはまずインデックスを調べ、パックされたファイルの中に該当するファイルが存在しているかどうかをチェックします。存在している場合、CPackedFile クラスは ReadFile を呼び出し、指定されたバッファーにファイル データを格納するか (通常の I/O の場合)、メモリーマップ ポインターを返します (メモリー マップ I/O の場合)。

このパックされたファイルは、スタートアップ UI 内の [Delete Packfile] ボタンを使用して、サンプルから簡単に削除できます。

オンデマンドのシングルスレッド ロード

シングルスレッド ロードは基本的に、シリアル ロードです。アプリケーションは、リソースが必要な場合、そのリソースを即座にロードしようとします。そのリソースのロード中は、他の処理を実行することができません。このようなタイプのロードを可能な限り効率的にかつ滞りなく実行するために、アプリケーションではリソース再利用キャッシュを使用します。アプリケーションは、リソース再利用キャッシュを使用すると、新しいリソースをゼロから作成するのではなく、既存のリソースをロックして使用することができます。既存のリソースをロックして使用すれば、まったく新しいリソースを作成する場合に比べて、コストを抑えることができます。

オンデマンドのマルチスレッド ロード

オンデマンドのシングルスレッド ロードのパフォーマンスはリソース再利用キャッシュによって実際に改善されますが、アプリケーションはディスクからデータをロードする際 (またはメモリー マップ I/O からデータを読み込む際) およびデバイス オブジェクトに対して実際のコピーを実行する際に一時停止するので、レンダリングが一時的に中断されます。そのため、レンダリング スレッドの一時停止時間を可能な限り短くする方が、ソリューションとして適しています。Direct3D 9 および Direct3D 10 では、レンダリング スレッドをリソースのロック時およびリソースのロック解除時にのみ停止させることができます。

Direct3D 10 の場合だけは、第 2 のオプションもあります。Direct3D 10 の場合は、オブジェクトに対して UpdateSubresource を呼び出す場合にだけグラフィックス スレッドを一時停止させることができます。それは、ResourceReuseCache.h の先頭にある USE_D3D10_STAGING_RESOURCES の定義をコメントアウトすることで行えます。該当するアプリケーションでは現在、ステージング リソースを使用して、D3D9 の動作を模倣しています。リソースをディスクからロードしてデータをデバイス オブジェクトにコピーする処理は、別のスレッドで実行できます。

3 つのタイプのスレッドが関与します。最初のスレッドは、レンダリング スレッドです。レンダリング スレッドの主なジョブは、停止することなくシーンを描画することです。レンダリング スレッドの小さなジョブとしては、要求キューを介して I/O 要求を I/O スレッドに送る処理や、I/O スレッドからのロック要求およびアンロック要求の処理などがあります。レンダリング スレッドの停止を防止するために、これらの小さなジョブはいずれも、レンダリング スレッドにとって都合が良いときに処理されます。

第 2 のタイプのスレッドは、I/O スレッドです。I/O スレッドは、レンダリング スレッドからの要求を処理します。また、パックされたファイルからファイルをロードしたり、それらのメモリーマップ ポインターを取得したりする処理も実行します。I/O スレッドは、さまざまなプロセス スレッドとのやり取りを処理したり、ロック/ロック解除要求をロック/ロック解除キューに格納してレンダリング スレッドに処理させたりすることも行います。また、Direct3D 9 の場合、I/O スレッドではデバイス オブジェクトに対するビットの実際のコピーも実行します。一般に、システム上で使用されるストレージ デバイスごとに、I/O スレッドが 1 つ存在します。I/O スレッドは、さまざまなストレージ デバイスから I/O 要求が返されるのを長時間待機することがあるため、I/O スレッドとプロセス スレッドの 1 つで単一のハードウェア スレッドを共有する方が妥当な場合もあります。

最後のタイプのスレッドは、プロセス スレッドです。プロセス スレッドは、I/O スレッドによってディスクからロードされた後のデータ処理というダーティな処理を実行します。プロセス スレッドは、データの解凍、データのプリフェッチ (メモリー マップ I/O の使用時)、スィズルなど、デバイス オブジェクトにコピーされる前のデータに対して実行する必要がある処理を実行できます。一般に、利用可能な各ハードウェア スレッドに対して、プロセス スレッドが 1 つ存在します。

マルチスレッド ロードで使用されるすべてのスレッドを、次の図に示します。

Figure 1.  マルチスレッド ロードのスレッド

Ee416397.d3d10_sample_ContentStreaming_Threads(ja-jp,VS.85).gif

典型的なイベントのチェーンを示すと、以下のようになります。

  1. I/O スレッドと一連のプロセス スレッドが、何らかの処理を待ってスリープ状態にあります。グラフィックス スレッドがレンダリングを終えようとしています。

    Ee416397.d3d10_sample_ContentStreaming_Threads1(ja-jp,VS.85).gif

  2. グラフィックス スレッドが、リソースが近い将来表示可能になると判断し、リソース要求を要求キューに入れます。

    Ee416397.d3d10_sample_ContentStreaming_Threads2(ja-jp,VS.85).gif

  3. I/O スレッドが、この要求を拾い、パックされたファイルから該当するデータをロードします。メモリー マップ I/O の使用時は、メモリーマップ ポインターが返されます。

    Ee416397.d3d10_sample_ContentStreaming_Threads3(ja-jp,VS.85).gif

  4. I/O スレッドが、待機中のプロセス スレッドの 1 つにプロセス要求を送ります。プロセス スレッドが、データに対する何らかの計算を実行します。メモリー マップ I/O の使用時にデータのプリフェッチが有効であれば、処理対象データの 4 KB ごとに 1 バイトをプロセス スレッドがチェックし、リソースが使用される前にメモリーに完全にページ インされたことを確認します。

    Ee416397.d3d10_sample_ContentStreaming_Threads4(ja-jp,VS.85).gif

  5. プロセス スレッドは、処理が終わると、ロック/ロック解除キューにロック要求を入れます。

    Ee416397.d3d10_sample_ContentStreaming_Threads5(ja-jp,VS.85).gif

  6. レンダリング スレッドは、適切な時期になるとロック要求を拾い、利用可能なリソースをリソース再利用キャッシュから見つけて、該当するリソースをロックします。

    Ee416397.d3d10_sample_ContentStreaming_Threads1(ja-jp,VS.85).gif

  7. レンダリング スレッドは次に、該当する要求を I/O スレッドに返送します。Direct3D 9 の場合、I/O スレッドは該当するデータをデバイス オブジェクトにコピーします。

    Ee416397.d3d10_sample_ContentStreaming_Threads3(ja-jp,VS.85).gif

  8. 次に I/O スレッドは、ロック解除要求をロック/ロック解除キューに入れます。

    Ee416397.d3d10_sample_ContentStreaming_Threads5(ja-jp,VS.85).gif

  9. レンダリング スレッドは、適切な時期になるとロック解除要求を拾い、該当するリソースをロック解除します。該当するリソースは、これで利用可能な状態になりました。

    Ee416397.d3d10_sample_ContentStreaming_Threads1(ja-jp,VS.85).gif