USD Hydra解析

USD Hydra解析

注:現在2020/7/12くらいにこの記事を書いている
これを書いている時点でUSDの知識は、全然使ったことないレベルなので、間違いがあったらすみません。


この記事では、膨大なUSDのうち、Hydra中心に見ていく。
目的は、USDシーングラフの構造と、その操作、及び、自アプリへのインテグレーションである。
USDでよく行われている、HdRenderDelegateを使用したレンダラプラグイン作成も、気になるところ。

https://github.com/PixarAnimationStudios/USD/tree/master/pxr/imaging/hd

これがHydra。

まずCMakeLists.txtを見る
クラスはいっぱいあるが、PULIC_HEADERSは3つなので、とりあえずこれを見ていく。

HdDriverは、アプリケーションによって所有され、HdRenderIndexに渡されるデバイスオブジェクト(通常はレンダリングデバイス)を表します。 RenderIndexはそれをレンダーデリゲートとレンダリングタスクに渡します。アプリケーションはHdDriverの生存期間を管理し、Hydraの実行中も有効であることを確認する必要があります。

というわけで、レンダリングデバイスのことらしい。

  • version.h
    単にAPIバージョンをdefineしているだけ

続いて目についたところを見ていく

Hydraにアクセスするための、アプリケーションに面したエントリポイントの最上位エントリポイント。
通常、アプリケーションはこれらの1つのみを作成します。

というわけで、アプリからHydraを使う場合に、まずこれを作って使うようだ。
メソッドとして以下がある
void SetTaskContextData(const TfToken &id, VtValue &data);
void RemoveTaskContextData(const TfToken &id);
void Execute(HdRenderIndex *index, HdTaskSharedPtrVector *tasks);
void ReloadAllShaders(HdRenderIndex& index);

TaskContextのデータをセットしたり消したりできるようだ。
ここで、TfTokenというのと、VtValueというのが出てくるが、
Tokenのほうは文字列のようだが、Valueのほうは、任意の型を表しそうな予感がする。
マチスレッド対応ということもあり、中をちらっと見ると、なかなかシンドイことになっている。

    • TfToken
      既知の文字列を効率的に比較、割り当て、ハッシュするためのトークン。
      TfTokenは登録された文字列のハンドルであり、一定の時間で比較、割り当て、およびハッシュできます。
      ~中略~
      TfTokenを使用するには、文字列またはconst char *からインスタンスを作成するだけです。

    • VtValue
      任意の型を保持できるコンテナを提供し、配列型の性質iterationを提供します。

    • HdEngine::Execute(HdRenderIndex *index, HdTaskSharedPtrVector *tasks)

  1. まず、DATA DISCOVERY PHASE ということで、
    必要なレンダリングプリム表現をレンダリングするために必要なすべての入力データを見つけます。この時点で、リソース依存関係グラフを確立するのに十分なデータを読み取る必要がありますが、まだCPUまたはGPUメモリにデータを入力していません。次の呼び出しの結果、リソースレジストリには、解決が必要な(おそらくCPUでデータを生成する)BufferSourceと、CPU / GPUで実行する計算の両方が入力されます。
    だそうだ。で、"次の呼び出し"にあたる、index->SyncAll(tasks, &_taskContext); が実行される

  2. PREPARE PHASEでは、
    すべてのプリムが現在の状態を取得したので、レンダリングのためにタスクシステムを準備できます。同期操作は変更が追跡されるため、何かがダーティな場合にのみ実行され、Prepare操作は実行ごとに行われます。タスクは最初に同期されるため、同期時にバインディングを解決できないため、ここでタスクがプリム間通信を実行します。また、Prepareフェーズでは、タスクがレンダリングフェーズに必要なリソースを管理します。
    size_t numTasks = tasks->size();

    for (size_t taskNum = 0; taskNum < numTasks; ++taskNum) {
    const HdTaskSharedPtr &task = (*tasks)[taskNum];
    task->Prepare(&_taskContext, index);
    }
    ということで、各タスクのPrepare()を呼んでいく。

  3. DATA COMMIT PHASE
    さまざまなリソースを更新するために必要なデータへのハンドルを取得したら、レンダーにこれらのリソースを「コミット」させます。これらのリソースは、CPU / GPU /両方に常駐できます。レンダリングデリゲートの実装によって異なります。
    HdRenderDelegate *renderDelegate = index->GetRenderDelegate();

    renderDelegate->CommitResources(&index->GetChangeTracker());

    ここで、Prepare中にCPU/GPUに
    リソースを送っているのではなく、この時点でレンダラーに送っているようだが、CPU側のリソースはPrepare時に確保したものをそのまま使っているのか、あるいはコピーして使っているのか、少し気になる。

  4. EXECUTE PHASE
    必要なデータバッファーをすべて更新したら、最終的にレンダリングタスクを実行できます。
    for (size_t taskNum = 0; taskNum < numTasks; ++taskNum) {
    const HdTaskSharedPtr &task = (*tasks)[taskNum];
    task->Execute(&_taskContext);
    }
    ということで、各タスクのExecute()を呼んでいく。


  1. まとめると、以下を呼ばれるたび(1フレーム描画?)に行っている。

[SyncAll(同期)]
→[Prepare(レンダリング準備及び、必要リソースの更新)]
→[変更されたデータをまとめて「コミット」]
→[Execute(レンダリング)]


Engineは、例えばこのあたりに使っているコードがある。

https://github.com/PixarAnimationStudios/USD/blob/master/extras/imaging/examples/hdTiny/testenv/testHdTiny.cpp


こうなると、次に気になるのはRenderIndexである。SyncAllって何?

  • renderIndex.h / renderIndex.cpp


Hydraレンダーインデックスは、クライアントシーングラフのフラット化された表現であり、複数の自己完結型シーングラフで構成される場合があります。それぞれのシーングラフは、データアクセス用のHdSceneDelegateアダプターを提供します。

だそうだ。フラット化された表現、つまりシーングラフの階層構造をflattenしたものか。そして、シーングラフは複数で構成され、各シーンは閉じている。のかな。中を見ていくと、 Subtree, RenderablePrims(rprim), RenderTag, MaterialTag, Instancer, Task, SceneStatePrim(sprim), BufferPrims(brim), そして RenderDelegateと多彩な顔触れ。これがシーングラフ本体といっても過言ではなさそうな感じで、色々入っているように見える。ヘッダのNew関数のコメント見ると、先ほどのdriverが登場する。

指定されたレンダーデリゲートでレンダーインデックスを作成します。 renderDelegateがnullの場合はnullを返します。レンダーデリゲートとレンダータスクは、アプリケーションによって提供されるレンダラーのデバイスへのアクセスを必要とする場合があります。オブジェクトは「driver」として渡すことができます。 HgiはHdDriverの例です。
hgi = Hgi::GetPlatformDefaultHgi()
hgiDriver = new HdDriver<Hgi*>(HgiTokens→renderDriver, hgi)
HdRenderIndex::New(_renderDelegate, {_hgiDriver})

生成時にHdRenderDelegateと、レンダラーデバイス(ドライバ)を渡す。ドライバのほうは後で見るとして、HdRenderDelegateというやつは、よく聞くHydraレンダーデリゲート  というやつであり、このクラスを継承してレンダープラグインを作成する。HdRenderDelegateを継承し、シーンのレンダリングに対応したレンダラーを、HdRenderDelegateとして渡す。また、ドライバというのを一緒に渡す。このドライバというのはPlatform固有だったりするようなので、DirectXでいうところのDeviceみたいなものかな?

  • void SyncAll(HdTaskSharedPtrVector *tasks, HdTaskContext *taskContext);
    入力タスク、B&Sプリム、(外部)計算を同期し、保留中のすべてのダーティリストを処理します(Rプリムを同期します)。このステップの最後では、更新が必要なすべてのリソースに、データソースへのハンドルがあります。
    というわけで、保留中の全ての変更リストを処理し、Renderableプリムを同期するそうだ。

    cppのほうを見ていく

  1. まずRenderDelegateからRenderParamを取得しつつ、BufferPrim(Bprim)を同期する。

  2. それから、
    このとき、テクスチャのBprimには、重複排除システムが含まれている。また、最大テクスチャサイズなどの特定のパラメータは、すべての参照を調べることによって解決されます。この時点で、新しいテクスチャがシステムに追加されている可能性がありますが、古い参照はまだ削除されていません。Material Sprimsはテクスチャの解決済み状態を必要とするため、Sprim処理の前に、古い参照がここでクリーンアップされます
    というわけで、テクスチャの古い参照を GarbageCollectBprims () / ClearBprimGarbageCollectionNeeded () などよりクリアする

  3. 続いて、SceneStatePrim(Sprim)を同期

  4. そして、全タスクを同期していく
    この処理により、各タスクは、_syncQueue に変更リストを追加していく。

  5. 全タスクから、RenderTagを集める。
    タスクリストは追跡された状態ではなく、ビューごとに異なるタスクリストを使用するため、同期ごとに異なる可能性があるためです。したがって、レンダータグはキャッシュできず、毎回Syncで収集する必要があります
    RenderTagというのは、このタスクはレンダリングを行う、という意味のような気がする。タスクリストは、ユーザーが勝手に追加した削除したりすることができ、USDライブラリ側からは追跡を行っていないので、毎回どれがレンダリングするタスクか収集しないといけない。という意味かな

  6. _syncQueue に入れられている変更リストを、マップ(dirtyIDs)に入れていく。マップのキーとしては、SdfPathを用いる。
    すべてのデリゲートIDがグループ化されるように、低速のSdfPathを使用してIDをマージします。残念ながら、FastLessThanは最適化の効果を低下させますが、辞書式の「より小」を使用すると、std :: mapを構築する時間が支配的になります。

  7. _RprimSyncRequestMap(syncMap ) というのを先ほどのdirtyIDsなどから頑張って作る。

  8. PythonのGILを破棄

  9. Pre-Sync Rprims
    シーンデリゲートに渡す前に、レンダリングデリゲートに同期リクエストを変更する機会を与えます。これにより、レンダリングデリゲートは、変更トラッカーでマークされた変更を処理するために必要な追加のデータを要求できます。そのため、変更をマークするエンティティは、レンダリングデリゲートの特定のデータ依存関係を認識する必要がありません。
    例えば、あるテクスチャが変更された場合に、そのテクスチャを使ってるマテリアルをもう1回送って貰わないとレンダリングできないといった場合?

  10. Delegateの同期
    頑張って作ったsyncMapを、並列に実行し、更新された値を各種Delegateに送り、同期させる。

  11. RPrimの同期、同期待ち
    WorkArenaDispatcher というのを使って、R
    primの同期処理を並列に実行し、結果を回収し、同期待ちを行う。

  12. Clean Up
    DelegateのPostSyncCleanup()を呼んだり、trackerのResetVaryingState ()を呼んだりして、同期で使用したステートをクリアしていく。

  • void EnqueuePrimsToSync( HdDirtyListSharedPtr const &dirtyList, HdRprimCollection const &collection);
    ダーティリストを同期キューに追加します。ダーティリストの実際の処理は、SyncAll()の後のほうで行われます。
    ついでに、この関数。変更リストを同期キューに追加する。実際は、_syncQueue に追加されていき、↑のSyncAllの(6)くらいから処理されていく。

だんだん分かってきた。続いて、少し謎だったdriverについてみていく。


使用例としてコメントに出てきたこの関数、これは何か。
プラグインシステムを使用して派生Hgiクラスを構築し、リンカーの複雑化を回避します。

これは、どうやら、レンダラープラグインの名前から、Hgiインスタンスを作成して、返しているようである。
Hgiインスタンスとは?

ここでようやく出てきた、ホンモノっぽい名前。これはめちゃくちゃ重要、というのが名前だけで感じられる。

HGIは、1つ以上の物理GPUデバイスとの通信に使用されます。 Hgiは、gpuデバイスが所有するリソースを作成/破棄するAPIを提供します。リソースの存続期間はHgiによって管理されないため、リソースを破棄し、それらのリソースが使用されなくなったことを確認するのは呼び出し元の責任です。コマンドは、コマンドバッファから取得されるエンコーダを介して記録されます。コマンドバッファは、即時モードまたは遅延モードで機能します。即時コマンドバッファは、そのエンコーダがグラフィックバックエンドで遅延なくコマンドを実行することを前提としています。遅延コマンドバッファは、グラフィックスバックエンドで後で実行されるコマンドを記録します。

物理GPUと通信を行っているクラス、つまりDirectXでいうDeviceみたいなやつのようだ。
以前出てきた例によると、これを元にDriverを作成するので
new HdDriver<Hgi*>(HgiTokens→renderDriver, hgi)
やはりDeviceみたいなやつに違いない。

イミディエイト用と、ディファード用のコマンドバッファを持っているようだ。
メソッドを見てみると、

描画コマンド関連のEncoderというのを作ったり、
テクスチャの生成破棄、バッファの生成破棄、シェーダの生成破棄、
リソースバインディングの生成破棄、パイプラインの生成破棄などが行えるようだ。

また、名前的に気になる、SceneDelegate。こちらも目を通さないといけない気がする。

クライアントシーングラフとのデータ交換を提供するアダプタークラス。

つまり、アプリ側で持っている独自のシーングラフと、USDのシーングラフのデータを交換するためのアダプターであるようだ。
こちらもかなり重要な感じがする。

レンダラープラグインは、HdRendererDelgateというクラスを継承して作成するので、HdSceneDelegateを継承すると、シーンプラグインならぬシーンの共有ができる。。のか?

そしてこのクラスは、中身は全部から実装であるが、privateメンバとして、

HdRenderIndex *_index;

を保持していて、publicなGetRenderIndex()によりそれを返している。

このSceneDelegateの実装例については、UnitTestのコードとして少しあった

https://github.com/PixarAnimationStudios/USD/blob/master/pxr/imaging/hd/unitTestDelegate.h
https://github.com/PixarAnimationStudios/USD/blob/master/pxr/imaging/hd/unitTestDelegate.cpp


つまり

HdRenderDelegate を継承した独自クラスを作成し、USDのレンダラとして使えるようだ。

HdRenderDelegate と
driver(必要無い場合は空でいい)により、HdRenderIndex を作成することができる。
HdRenderIndex がシーンの実体であるようだ。

ここで、HdSceneDelegateを継承した独自クラスを作成すると、renderIndex にあれこれ入れるコードを書く必要があるが、USDのシーンとして使えるようだ。これにより自作アプリのシーンとUSDのシーンを同期させることができる。
USDに内蔵のHydraのビューポートは、おそらく、このUsdImagingDelegateが使用されている。

HdEngine が全ての実行の根幹であり、HdEngine::Execute(renderIndex, tasks)により、シーンの同期、更新、バッファ転送、描画、を行う。


まずは、このコードを見ながら使ってみるのが良さそう…?

https://github.com/PixarAnimationStudios/USD/blob/master/extras/imaging/examples/hdTiny/testenv/testHdTiny.cpp