C++のCovarient Return Typesを検証してみた
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の作業時間を測定してみた
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_TIMESTAMP
とD3D11_QUERY_TIMESTAMP_DISJOINT
が個別で必要です。
D3D11_QUERY_TIMESTAMP_DISJOINT
のQueryは、後で説明するID3D11DeviceContext::Begin
とID3D11DeviceContext::End
を順に呼び出す時に必要になります。D3D11_QUERY_TIMESTAMP
はID3D11DeviceContext::End
を呼び出す時のみ必要です。MSDNによるとこのタイプのクエリで、Begin
は無効になるそうです。
ID3D11DeviceContext
の上記のAPIを実行すると、GPUのクロックを記録するようになります。即ち、各自がストップウォッチみたいに動くわけではなく、Disjoint
クエリにより相対的なGPUクロック数が記録されるわけです。
D3D11_QUERY_TIMESTAMP_DISJOINT
のクエリは、Begin
とEnd
によって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として渡されて利用する形になります。gpuTime
とfragment
がそうですが、このインスタンスは始発のクエリと終わりのクエリを持っていて、RAIIにより自動的にBegin
とEnd
を呼ぶことになります。(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をクリアしました。
就活がほぼ最後に入り、暇が出来たのでスイッチでやってみました。今は難しいプラットフォームアクションゲームが多いし、ストーリー展開でのステージは平均のゲームに比べてはちょうど良い難易度のゲームだったと思います。 ストーリーだけ進行しようとすると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
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
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のモデルを沿ってレンダリングをする。
参考
UE4のマテリアルのCustomノードでの拡張テクニック
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はなんのことかわかりませんけどわかる次第に追記しますね。