NEUROMANTIC

自分でC/C++/UE4/Graphics/ゲームなどをやったことをメモするブログ

C++のCovarient Return Typesを検証してみた

en.m.wikibooks.org

C++でvirtualの関数を継承(Override)したりして関数を書き直すことになると、普通はリターンタイプを同じにして返すのが普通ですね。 しかし継承したタイプのOverrideした関数のリターン値がベース仮想関数の返す型を継承としたものとならば、そのままに使うには外部から再度タイプキャストをしなければならないため、ややめんどいです。

Convarient Return Typesイディオムは、Overrideした仮想関数のリターンタイプは元となる仮想関数のタイプを継承したタイプどれも容認することを使用したテクニックとなります。これで継承しているタイプに併せてまたキャスティングする必要がなくなります。

早速コードを見ますね。

#include <type_traits>

struct A { static constexpr int value = 13; };
struct B : public A { static constexpr int value = 19; };
struct C : public B { static constexpr int value = 137; };
struct D final { static constexpr int value = 251; };

struct AVirtual {
    virtual ~AVirtual() = default;
    virtual A* Get() { return this->pInstance; };
private:
    A* pInstance = nullptr;
};

struct BVirtual : public AVirtual {
    virtual ~BVirtual() = default;
    virtual B* Get() override { return this->pInstance; }
private:
    B* pInstance = nullptr;
};

struct CVirtual : public BVirtual {
    virtual ~CVirtual() = default;
    virtual C* Get() override final { return this->pInstance; }
private:
    C* pInstance = nullptr;
};

/*
struct DError : public BVirtual {
    virtual ~DError() = default;
    virtual D* Get() override final { return this->pInstance; }
private:
    D* pInstance = nullptr;
}*/

int main()
{
    BVirtual* b = new BVirtual();
    auto* pB = b->Get();
    AVirtual* a = new BVirtual();
    auto* pA = a->Get();

    static_assert(std::remove_pointer_t<decltype(pB)>::value == B::value);
    static_assert(std::remove_pointer_t<decltype(pA)>::value == A::value);

    delete b;
}

このイディオムの問題は、変数のコンパイル時タイプによって仮想関数のリターンタイプをどれにするかを決定することです。これは仮想関数の引数のデフォルト値を決めるときに、変数のコンパイル時タイプを見てから決めることと一致してますよね。でもデフォルト値よりはリターンタイプによってCovarientにタイプが違うようにするのは良い方法だと思います。autoを使っても、インテリセンスでどのタイプかはっきりと伝えてくれるし、デフォルト値みたいに暗示的に適用させるものではないからです。

ちなみに継承してないタイプを入れようとするとこうなります。

<source>:32:16: error: return type of virtual function 'Get' is not covariant with the return type of the function it overrides ('D *' is not derived from 'B *')
    virtual D* Get() override final { return this->pInstance; }
            ~~ ^

Covariantでないと言われてエラーが出ます。リターンタイプが継承しているかないかをコンパイル時に判断してくれますから良いですね。

DirectX11でGPUの作業時間を測定してみた

f:id:neuliliilli:20190506215256p:plain
ImGuiで作業時間を測定する

reedbeta.com

DirectX11を勉強しながら、サンプルのGPUの各作業時間を測定するため検索してみたところ、ID3D11Queryというのを用いてGPUの時間を測定することがわかりました。 SH-D3D11サンドボックスプロジェクトのサンプル1と2を実装しながらID3D11Queryでどうやって測定してのかちょっとメモしたいと思います。

本論に入る前に、GPUの作業時間の測定をCPUの作業時間の測定値として一緒に扱ってはいけません。なぜならCPUのプラットフォームで提供している各種の時間測定APIはGPUを考慮しないからです。たとえQueryPerformanceCounterまたはstd::chrono::...::now()で最初時間と最後時間を測定したとしても、GPUはCPUとは別として動いてますからこの測定値数が本当に信頼性あるかわかりません。単にFPSでこれをOFFしたら10FPSが上がったというようにパフォーマンスを測定してはいけません。

まずID3D11Queryを使用するにはこのクエリタイプのインスタンスを作らなければなりません。ID3D11Device::CreateQueryで簡単に作ることが出来ます。 ここでディスクリプタにQueryの種類を書きますが、GPUのクロックを測定するためにはD3D11_QUERY_TIMESTAMPD3D11_QUERY_TIMESTAMP_DISJOINTが個別で必要です。

docs.microsoft.com

D3D11_QUERY_TIMESTAMP_DISJOINTのQueryは、後で説明するID3D11DeviceContext::BeginID3D11DeviceContext::Endを順に呼び出す時に必要になります。
D3D11_QUERY_TIMESTAMPID3D11DeviceContext::Endを呼び出す時のみ必要です。MSDNによるとこのタイプのクエリで、Beginは無効になるそうです。 ID3D11DeviceContextの上記のAPIを実行すると、GPUのクロックを記録するようになります。即ち、各自がストップウォッチみたいに動くわけではなく、Disjointクエリにより相対的なGPUクロック数が記録されるわけです。

D3D11_QUERY_TIMESTAMP_DISJOINTのクエリは、BeginEndによってGPUデバイスのクロック周波数を渡されます。これにより補助のタイムスタンプクエリから記録したクロックに用いて実際の時間として変換することが出来ます。また、これはあくまでもGPUからのコマンドによる実行になりますのでCPUとの時差などに気にしなくても良いそうです。

僕のサンプルコードはこう書いて、Disjointと一般Timestampクエリを作成しました。

// Make timestamp queries (disjoint and start-end queries)
// https://docs.microsoft.com/ko-kr/windows/desktop/api/d3d11/nf-d3d11-id3d11device-createquery
IComOwner<ID3D11Query> ownDisjointQuery       = *FD3D11Factory::CreateTimestampQuery(mD3DDevice.Get(), true);
IComOwner<ID3D11Query> ownGpuStartFrameQuery  = *FD3D11Factory::CreateTimestampQuery(mD3DDevice.Get(), false);
IComOwner<ID3D11Query> ownGpuEndFrameQuery    = *FD3D11Factory::CreateTimestampQuery(mD3DDevice.Get(), false);

std::optional<IComOwner<ID3D11Query>> FD3D11Factory::CreateTimestampQuery(
  ID3D11Device& device,
  bool isDisjoint)
{
  IComOwner<ID3D11Query> result = nullptr;

  D3D11_QUERY_DESC queryDesc;
  queryDesc.Query = isDisjoint == true ? D3D11_QUERY_TIMESTAMP_DISJOINT : D3D11_QUERY_TIMESTAMP;
  queryDesc.MiscFlags = 0;

  if (device.CreateQuery(&queryDesc, &result) != S_OK)
  {
    result.Release();
    return std::nullopt;
  }

  return result;
}

作成されたクエリはこのように実行出来ます。

auto gpuTime = MTimeChecker::CheckGpuD3D11Time(
  "GpuFrame", 
  ownDisjointQuery.Get(), mD3DImmediateContext.Get(), 
  false);
{
  auto fragment = 
    gpuTime.CheckFragment("Overall", ownGpuStartFrameQuery.Get(), ownGpuEndFrameQuery.Get());

  mD3DImmediateContext->ClearRenderTargetView(
    &mRenderTargetView.Get(), 
    std::array<FLOAT, 4>{bgCol.X, bgCol.Y, bgCol.Z, 1}.data());
  mD3DImmediateContext->ClearDepthStencilView(&mDepthStencilView.Get(), 
    D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL,
    1.0f,
    0);

  MGuiManager::Render();
  // Present the back buffer to the screen.
  HR(mD3DSwapChain->Present(1, 0));
} // RAII!
// And gpuTime will be destroyed and RAII happen.

私のサンプルのフレームワークでは素のままじゃなくてラッピングしてから、別のタイプのインスタンスをRAIIとして渡されて利用する形になります。gpuTimefragmentがそうですが、このインスタンスは始発のクエリと終わりのクエリを持っていて、RAIIにより自動的にBeginEndを呼ぶことになります。(DisjointクエリのみBeginを呼び出すことになっています)

そしてDisjointクエリのRAIIインスタンスが消滅になると、補助のタイムスタンプクエリを終了することと同時にクエリから貰ったGPUクロックを実際時間に変換します。

std::unordered_map<std::string, double>
FD3D11TimeHandle::CalculateTimestamps(
  ID3D11DeviceContext& dc, 
  ID3D11Query& disjoint, 
  TTimeFragments& fragments)
{
  std::unordered_map<std::string, double> results;

  // Stall until disjointQuery is available.
  while (dc.GetData(&disjoint, nullptr, 0, 0) == S_FALSE)
    ;

  // Check whether timestamps were disjoint during the last frame
  D3D11_QUERY_DATA_TIMESTAMP_DISJOINT tsDisjoint;
  dc.GetData(&disjoint, &tsDisjoint, sizeof(tsDisjoint), 0);
  if (tsDisjoint.Disjoint)
  {
    return results;
  }

  for (auto& [fragmentName, pair] : fragments)
  {
    auto& [start, end] = pair;

    // Get All the timestamps.
    UINT64 tsBeginFrame, tsEndFrame;
    dc.GetData(start, &tsBeginFrame, sizeof(UINT64), 0);
    dc.GetData(end, &tsEndFrame, sizeof(UINT64), 0);

    // Convert to real time (ms).
    const auto msGpuFrame = 
        double(tsEndFrame - tsBeginFrame) / double(tsDisjoint.Frequency) 
      * 1'000.0;
    results.try_emplace(fragmentName, msGpuFrame);
  }

  return results;
}

resultは各タイムスタンプクエリ(2つのクエリの参照を持つ)の作業時間をmsとして表したものをハッシュマップに入れたものとなります。これにより、ID3D11Queryを使用して各GPU作業の処理時間を求められるようになりました。

ちなみに

GPUのクロック数値を得るにはDisjointクエリの作業が終わらなければなりません。この為、上のコードではSpinlockみたいなコードを書いてDisjointが終わったかを検証してから値を得ます。

while (dc.GetData(&disjoint, nullptr, 0, 0) == S_FALSE)
  ;

こういったコードはCPUのStallを起こす恐れがありますので、スレッドを分けてプロファイリングスレッドで独自的に測定させるのがいいかもしれませんね。またこうなると、値を書き下ろすにはAtomicな演算を行わなければならなくなりますし、せめてBackbufferの測定値などを実装してより安全にGPU処理時間値を測らなければならなさそうです。(こっちのプロジェクトではまだやっていませんが、サンプルが複雑になるとやってみる考えはあります)

今更ですがCELESTEのA-Sideをクリアしました。

f:id:neuliliilli:20190505224241p:plain

就活がほぼ最後に入り、暇が出来たのでスイッチでやってみました。今は難しいプラットフォームアクションゲームが多いし、ストーリー展開でのステージは平均のゲームに比べてはちょうど良い難易度のゲームだったと思います。 ストーリーだけ進行しようとすると1000回ぎりぎりで死んでクリア出来たんじゃないかなと思いますね。でも初見のくせに「いちご狩り」を無理矢理して1666回死でクリアしました。

このゲームの個人的な感想は、とにかく音楽が凄いことと面白さとうんざりさの境界線でギリギリとレベルデザインをやり尽くしたということですかね。。。A-Sideはそう感じました。 ちょっと残念だった点はSwitchのプロコンでの操作性がちょっと悪いのか斜線ダッシュがうまく出来ないこと、そして日本語翻訳が外国人の僕としてもちょっと不自然に感じられたという点です。 中盤からは英語で進行しました。

今はB-Sideの部分(裏ステージ)をちょいちょいやりながら他のゲームもやっています。もちろんエンジンも作っているし、D3D11のサンドボックスプロジェクトとRTR4thも読んでいますよ。

Primitive ShaderとかMesh Shaderとかを調べてみた。

今のGPUそしてグラフィックスで通用されているシェーダーレンダリングのパイプラインを纏めると以下になりそうです。

レンダリングパイプライン 独立パイプライン
~OpenGL 3.0 固定パイプライン なし
~OpenGL 4.0 VS > GS > FS なし
OpenGL 4.0~ VS > TCS > TES > GS > FS Compute
DX9 VS > FS (PS) なし
DX10 VS > GS > FS (PS) なし
DX11, 12 VS > HS > DS > GS > PS Compute

元々はVertex Shader(頂点シェーダー)とFragment (Pixel) Shaderしかなかったのですが、GSを始めラスタライズ以前に頂点をいじれるシェーダーステージが入りながらちょっとコントロールがしにくくなったそうです。

そしてこのTCS、GS、TES(HS、DS)の付加ステージはEarly Depth TestingみたいなGPUが素早くレンダリングするための最適化または技法を使えなくなる可能性を上げるそうです。 また、このステージ自体は無理やり入れ込んだものとしてGPUとは相性がうまく合わないらしいです。

GSを例として短く話せば、GPUのスレッドはWARPというものとして団体的に動くようになっていますね。 しかしこの3つのステージで何かを追加したり削除することにより、バッファを捨てられなくなるとか、スレッドグループをまた取り直さなければならなくなる、ということによって性能が落ちる恐れもあるということです。 (INTELではこういう性能落ちを防ぐようにハードウェアの最適化を行います。)

で、こういう問題を解決するためNVさんとAMDさんが最近(~3年)新しいシェーダーパイプラインを発表し始めました。こういうことを気付いたのは1ヶ月前からなんだったんですが、自分的にちょっとまとめて記事で上げたかったですので書いてみました。不正確であるかもしれませんが、ご了承ください

Primitive Shader

f:id:neuliliilli:20190425222225p:plain

AMDのVegaシリーズGPUから開発が始まったと言われる、Primitive Shaderという物があります。これを使用してレンダリングをすると、既存のシェーダーを使って頂点の深度テストかあらゆる作業を行うより最大2倍の性能が上がるそうです。

Primitive ShaderはVSはGSを合わせたものとして、VSとGSの機能を行うが複雑でなく性能が秀でるらしいです。ただしこのシェーダーを使うには既存のグラフィックスのAPIから支援しなければなりません。OpenGLでの支援は多分しないかもしれませんが、VulkanかD3D12のバージョンアップデートで支援出来ることがあるでしょうね。

  • Primitive Shaderを用いて、既存の非効率的なWorld-space pipelineを効率化していること。
  • ハードウェアからのPrimitiveのZテストをさせて、素早くレンダリング出来るようにしたこと。

Task Shader / Mesh Shader

f:id:neuliliilli:20190425225557p:plain

NVはTuringアーキテクチャから、Mesh Shaderという新たなグラフィックスシェーダーパイプラインを発表し、実装しました。Mesh Shaderというのは、既存では分離されていたCompute Shaderの仕組みを頂点レンダリングに溶け込んで、もっと素早くレンダリングさせるようにするものだそうです。

大きなメッシュをレンダリングする時に、一気にレンダリングせずにハードウェアから演算を行ってMeshletsというConvexな小さなメッシュを生成しレンダリングします。

...The mesh shader stage produces triangles for the rasterizer, but uses a cooperative thread model internally instead of using a single-thread program model, similar to compute shaders. Ahead of the mesh shader in the pipeline is the task shader. The task shader operates similarly to the control stage of tessellation, in that it is able to dynamically generate work. However, like the mesh shader, it uses a cooperative thread model and instead of having to take a patch as input and tessellation decisions as output, its input and output are user defined.

  • Task Shader:テセレーションのHSに似ているが、もっと動的に頂点をいじることが出来る。そしてInputとOutputはテセレーションで指定されたものだけを取って行うのではなく、ユーザーが指定できるそうだ。
  • Mesh Shader:実質的に三角形Primitiveを生成するステージ。
  • Mesh Shaderは従来のCompute Shaderのモデルを沿ってレンダリングをする。

参考

www.joshbarczak.com

m.blog.naver.com

www.pcper.com

devblogs.nvidia.com

UE4のマテリアルのCustomノードでの拡張テクニック

forums.unrealengine.com

UE4でいろいろとマテリアルを触る時に、基本的に与えられたノードだけじゃ複雑なものがちゃんと書けないことがありました。という時に使ってこなすのがCustomというノードで、これは直接HLSLコードを書いてシェーダーを作成することが出来ます。

しかしCustomノード自体にもただ使うにはいくつかの限界があります。Functionが書けないとかですね。しかし上の記事ではCustomノードのHLSLをコードなどでいくつかのリミットを乗り越えることが出来たそうです。自分も勉強兼ねてメモしてみました。

1. Custom Functions

UE4で全域のマテリアルシェーダー関数を作成するには.usfファイルを弄って追加か削除することが出来る。しかしこうすると.usfが適用される全てのマテリアルアセットが再度コンパイル通さなければならない。

しかしHLSLコードでstructみたいな構造体を宣言、その中に関数を定義することで関数を使用してより構造化されたマテリアルが作れる。

struct Functions
{

  float3 OrangeBright(float3 c)
  {
      return c * float3(1, .7, 0);
  }

  float3 Out()
  {
    return OrangeBright(InColor);
  }

};

Functions f;
return f.Out();

上のコードではインプット引数としてInColorだけを入れればこれで終わる。こうして得られる長所は全域の.usfを弄らずすばやくマテリアルを仕上げることが出来るというところだ。

2. External Code and #include

#include "Your Path...\Test.hlsl"
return 0;
// enter spaces here and press enter to retrigger compilation

内部のエディタからコードを作成する必要なく、ただ外部からhlslファイルを#includeして作成する。ただし記事のコメントによると外部のhlslファイルは#pragma onceを付けないとならないらしい。

そしてコードのreturn 0;は意図的なもので、UE4の独特なシェーダーコンパイル環境で追加コードなし#includeをさせるために使われるものだそう。また最後の行で追加的なコードを書かないとしたらスペースを一個書かなければならないらしい。


No.3とNo.4はなんのことかわかりませんけどわかる次第に追記しますね。