Wednesday, November 2, 2016

Vertex Array Object(VAO)を使わない理由

OpenGL ES 3.xではVertex Array Object(VAO)が導入され、APIコールが減って効率的ということになっています。ところが、コーディングの特殊さによるVAOの弊害が多く、多分実行速度も変わらない気がしたので、VAOを使わなくてもいい理由を書いてみようと思いました。(ちなみに、Unreal Engine 4もVAOを使っていないようです)プラットフォームは主にAndroidを想定しています。

1. VAOが頂点バッファと結びつくのが困る


頂点バッファをシェーダに結びつけるいわゆる"Input Layout"はAPI毎に格納されるオブジェクトが違います。DX11ではID3D11InputLayout、DX12やVulkanではPSO(pipeline state object)、ES 3.xはVAOに格納されます。

そのうちVAOだけの厄介な点は特定の頂点バッファと結びつく仕様になっていることです。実務でもありそうな例として、モンスターA、B、Cを、Gバッファ生成シェーダー、シャドウマップ生成シェーダ、光学迷彩シェーダで描画するとします。頂点バッファから送るのはGバッファは全情報、シャドウマップは座標だけ送れば十分、光学迷彩はテクスチャマッピングを省くことにします。

VAOを使って必要な情報のみ必要なシェーダに送るためには、モンスター3種とシェーダー3種、3x3=9個のVAOが必要になります。さすがにこのような煩雑なことはしたくないので、実際にはモンスター毎に1つずつVAOを作って、それぞれ頂点の全情報を送るように設定し、あとはドライバレベルでの最適化に託す、という書き方になりそうです。

2. わかりにくいバグを作りやすい


経験上、描画が終わったらglBindVertexArray(0)でVAOのバインドを忘れず解除すべきです。なぜなら、バインドしっぱなしにすると他の描画モジュールがバインド中のVAOを書き換えることが出来てしまい、どこでどう地雷を踏んでいるかわからない難解なバグとなります。

描画の為のglBindBufferやglVertexAttribPointerのみならず、glBindBufferの周辺はVAOを書き換える可能性に気を使います。例えばインデックスバッファを生成したり書き換えたりする時にglBindBufferを呼びますが、これがよそのVAOを壊してしまうかもしれません。


3. VAOでAPIコールを減らしても(多分)速くならない


VAOを採用する動機は、なんとなく速そうという期待ではないでしょうか。しかし、本当に速くなるでしょうか。VAOが無いES2.0は、頂点バッファを切り替える度にglBindBufferとglVertexAttribPointerの複数回に及ぶコールで毎回”Input Layout"に該当する情報をドライバに伝えます。それが省ける点はVAOが有利に見えます。

ところが、DX12やVulkanを見るとVAOは少なくとも最近のハードウェアの実装からかけ離れている事が想像できます。バッファはコマンドリストに乗るGPUアドレスに過ぎず、Input LayoutはPSOの一部に過ぎません。この2つを取り出して1つにまとめる事そのものに最適化的な利点は期待できなさそうです。

OpenGLのドライバの実装を想像してみます。GLも内部でPSOのような物を持っているはずです。PSOの生成は重い処理なので、一回作ったらハッシュ値などで探せるようにして実体をキャッシュしておくでしょう。他のステートが決まらないとPSOが確定できないので、ハッシュ値の計算もPSOの生成もすぐには行われず、ドローコール(glDrawElements等)のタイミングで行われるはずです。

こう考えると、glVertexAttribPointerにしろ、glBindVertexArrayにしろ、ハッシュ値を計算するための元データの提供に過ぎず、バッファのバインドを除いてCPU内で完結しています。VAOを使ったほうがAPIの呼び出しは減るかもしれませんが、無理にVAOを使って得られるものがあるわけではなさそうです。

別の視点から見てみると、また、現状多くのゲームが互換性のためにES2.0で実装されています。ドライバ開発者の立場としても、VAOありきで最適化できないと思われます。

4. ES2.0がまだまだ現役、そしてVulkanの登場


従来はES2.0をベースとして上位機種はES3.x採用という戦略がありました。今後は上位機種はVulkanで互換性のためにES2.0、という組み合わせが増えると思います。対応機種を狭める上に将来的にVulkanに置き換えられる運命にある、ES3.xの必要性が薄れてきました。

現在のようにAPIが移行期にある中では特定APIに依存しないように抽象化を試みることも多いと思いますが、VAOはその特殊さゆえに抽象化がとても難しいです。ES2.0のglVertexAttribPointerも非常に変則的ですが、使うシェーダが決まるまで頂点レイアウトの決定を保留できる分、VAOよりは抽象化はしやすいと言えるかもしれません。

Saturday, September 3, 2016

[DX12] VBV、IBVはAPI呼出し後すぐ破棄してもOK

頂点バッファの定義で、例えば以下のように2つの変数を定義して使うのを見かけると思います。

ID3D12Resource* vertexBuffer;
D3D12_VERTEX_BUFFER_VIEW vertexBufferView;
view raw DefineABuffer.h hosted with ❤ by GitHub
頂点バッファはID3D12ResourceとD3D12_VERTEX_BUFFER_VIEW、インデックスバッファはID3D12ResourceとD3D12_INDEX_BUFFER_VIEWが必要です。一つのリソースに2つの宣言が必要で煩雑なのですが、実はVIEWの宣言は省略してもよさそうです。

DirectX11ではSRVやDSVなど、GPUから参照するリソースに付加情報を付けてバインドする構造をViewと呼んでいました。 DirectX12になってD3D12_VERTEX_BUFFER_VIEW(VBV)やD3D12_INDEX_BUFFER_VIEW(IBV)という構造体がいわゆるViewの一種に加わりましたが、Viewの概念が若干変わったようです。DX11ではViewはその実体がGPU側にあるのかCPU側にあるのかは隠蔽されていました。それが、DX12ではGPU側なのかCPU側なのかは明確に区別されます。

この辺は、マイクロソフトのYouTubeのチャンネル「Microsoft DirectX 12 and Graphics Education」に登録されている「Resource Binding in DirectX 12 (pt.1)」に詳しいです。



8:30付近で出る表と13:00の解説によると、CBV、SRV、UAV、SAMPLERはGPUに配置され、それ以外のIBV、VBV、SOV、RTV、DSVはCPU側にのみ存在し、更にはドライバもVIEW(descriptor)の複製をコマンドリストに積むのでVIEWへの参照を保持しないとのことです。

MicrosoftのDirectX-Graphics-Samplesサンプル中、D3D12HelloWorldソリューションでは頂点バッファのID3D12Resourceに加えてD3D12_VERTEX_BUFFER_VIEWやD3D12_INDEX_BUFFER_VIEWを保持していますが、1つのバッファに対して2つの変数を管理するのは二度手間に感じます。

この二度手間を解決するため、上の動画から得られた事実を利用します。アプリケーションはID3D12Resourceのみ保持し、VIEWはスタック上に毎回作ることにします。例えばインデックスバッファをコマンドリストに積むためにこんな関数を作れば、今後D3D12_INDEX_BUFFER_VIEW構造体の存在を忘れてしまうことができます。

void SetIndexBuffer(ID3D12GraphicsCommandList* list, ID3D12Resource* indexBuffer)
{
D3D12_RESOURCE_DESC desc = indexBuffer->GetDesc();
D3D12_INDEX_BUFFER_VIEW indexBufferView = { indexBuffer->GetGPUVirtualAddress(), (UINT)desc.Width, AFIndexTypeToDevice };
list->IASetIndexBuffer(&indexBufferView);
}
同じDirectX-Graphics-SamplesでもMiniEngineの中では、スタック上に作ったD3D12_VERTEX_BUFFER_VIEWやD3D12_INDEX_BUFFER_VIEWを直接コマンドリストに流すようになっているようです。

ところで、D3D12_INDEX_BUFFER_VIEWとD3D12_VERTEX_BUFFER_VIEWは構造体でしたが、RTVやDSVはID3D12DescriptorHeapの形をとっています。これは実に奇妙に見えます。なぜなら、同じID3D12DescriptorHeapを使うSRVやCBVの場合、DescriptorがGPUメモリ上に存在し、シェーダーから参照されるため、プログラマはフェンスを駆使してID3D12DescriptorHeapの寿命管理を行わなければいけません。ところが、RTVやDSVはAPI呼び出し後はコマンドリストが実行前でもID3D12DescriptorHeapを破棄してもRTVやDSVを書き換えても構わないという事です。

内部の実装がも異なるオブジェクトが同じID3D12DescriptorHeapの形を取っているため、混乱の元になりそうです。

ともかく、この事実を利用して管理するオブジェクトを減らすことができます。以下はID3D12DescriptorHeapをスタック上に作ってOMSetRenderTargetsした後バッファをクリアする例です。ComPtrなのでID3D12DescriptorHeapは関数を抜けると消滅します。

残念ながら以下のコードはDebug Layerがエラーを出します。コマンドリスト実行終了前にID3D12DescriptorHeapを解放していないか検出するためにDebug Layerが参照しているようです。Debug Layerを外せば問題無さそうですが、まともにデバッグできないのでやめたほうがよさそうです。ただし、ID3D12DescriptorHeapを使わないVBVやIBVは前述の通りMiniEngineでもスタックに作って即破棄いるのでAPI呼出し後の破棄が合法と見て間違いなさそうです。

// DON'T DO THIS!!
// It seems working fine, however the DX12 debug layer treat this as following error.
//
// "An ID3D12DescriptorHeap object referenced in a command list ID3D12GraphicsCommandList Object "..." was
// deleted prior to executing the command list. This is invalid and can result in application instability.
// [ EXECUTION ERROR #921: OBJECT_DELETED_WHILE_STILL_IN_USE]"
//
ComPtr<ID3D12DescriptorHeap> rtvHeap, dsvHeap;
const D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = { D3D12_DESCRIPTOR_HEAP_TYPE_RTV, 1 };
device->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(&rtvHeap));
D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle = rtvHeap->GetCPUDescriptorHandleForHeapStart();
device->CreateRenderTargetView(renderTarget.Get(), nullptr, rtvHandle);
const D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc = { D3D12_DESCRIPTOR_HEAP_TYPE_DSV, 1 };
device->CreateDescriptorHeap(&dsvHeapDesc, IID_PPV_ARGS(&dsvHeap));
D3D12_CPU_DESCRIPTOR_HANDLE dsvHandle = dsvHeap->GetCPUDescriptorHandleForHeapStart();
device->CreateDepthStencilView(depthStencil.Get(), nullptr, dsvHandle);
commandList->OMSetRenderTargets(1, &rtvHandle, FALSE, &dsvHandle);
const float clearColor[] = { 0.0f, 0.2f, 0.3f, 1.0f };
commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
commandList->ClearDepthStencilView(dsvHandle, D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f, 0, 0, nullptr);

使い捨てにするとDebug Layerでエラーが出ますが、ID3D12DescriptorHeapをそれぞれ1個だけ作って毎回書き換えるように使うのはエラーにはなりません。上の動画の13:00に大丈夫と書いてあります。(もちろんSRVやCBVでこれをやるとアウトです)

ComPtr<ID3D12DescriptorHeap> rtvHeap, dsvHeap;
ID3D12GraphicsCommandList* commandList;
ID3D12Device* device;
void CreateHeaps()
{
D3D12_DESCRIPTOR_HEAP_DESC rtvDesc = { D3D12_DESCRIPTOR_HEAP_TYPE_RTV, 1 };
device->CreateDescriptorHeap(&rtvDesc, IID_PPV_ARGS(&rtvHeap));
D3D12_DESCRIPTOR_HEAP_DESC dsvDesc = { D3D12_DESCRIPTOR_HEAP_TYPE_DSV, 1 };
device->CreateDescriptorHeap(&dsvDesc, IID_PPV_ARGS(&rtvHeap));
}
void SetRenderTarget(ComPtr<ID3D12Resource> color, ComPtr<ID3D12Resource> depthStencil)
{
D3D12_CPU_DESCRIPTOR_HANDLE dsvHandle = dsvHeap->GetCPUDescriptorHandleForHeapStart();
D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle = rtvHeap->GetCPUDescriptorHandleForHeapStart();
// this overwrites descriptor heaps in every single call of SetRenderTarget, but it's no problem.
// because, the descriptor heap for RTV or DSV is in CPU side, not in VRAM like SRV and CBV.
// refer https://youtu.be/Uwhhdktaofg for more detail.
device->CreateRenderTargetView(color.Get(), nullptr, rtvHandle);
device->CreateDepthStencilView(depthStencil.Get(), nullptr, dsvHandle);
commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
commandList->ClearDepthStencilView(dsvHandle, D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f, 0, 0, nullptr);
commandList->OMSetRenderTargets(1, &rtvHandle, FALSE, &dsvHandle);
}

DX12はアプリケーションがローレベルを意識するAPIなので、RTVやDSVもD3D12_VERTEX_BUFFER_VIEWのようにただの構造体にしてもよかったのではないでしょうか。そうすればGPU側はID3D12DescriptorHeap、CPU側はただの構造体、という風に住み分けができて分かりやすくなると思うからです。この辺は、おそらくはDX11時代のID3D11RenderTargetViewやID3D11DepthStencilView等の歴史的な経緯も関係がありそうです。

Saturday, August 27, 2016

[DX12] HLSLにRoot Signatureを定義する

HLSLにRoot Signatureをattributeとして書く方法は、MSDNに詳しい説明があります。

Specifying Root Signatures in HLSL

MSDNのサンプルは機能紹介のためか「全部入り」の複雑なものになっていますが、ここではもっとシンプルなHLSLにRoot Signatureを書いてみました。キューブマップを空とみなして画面全体にレンダリングしています。

cbuffer perObject : register(b0)
{
row_major matrix invVP;
}
TextureCube texCube : register(t0);
SamplerState samplerState : register(s0);
struct VsToPs
{
float4 pos : SV_POSITION;
float3 dir : DIR;
};
#define RSDEF "CBV(b0), DescriptorTable(SRV(t0)), StaticSampler(s0)"
[RootSignature(RSDEF)]
VsToPs VSMain(uint id : SV_VertexID)
{
VsToPs ret;
ret.pos = float4(id & 2 ? 1 : -1, id & 1 ? -1 : 1, 1, 1);
ret.dir = normalize(mul(ret.pos, invVP)).xyz;
return ret;
}
[RootSignature(RSDEF)]
float4 PSMain(VsToPs inp) : SV_Target
{
return texCube.Sample(samplerState, inp.dir);
}

Vertex ShaderとPixel Shaderの両方に同じ定義をするために#defineで文字列を定義します。文字列1つで短く書けて、C++側で構造体と列挙体を駆使して書くより圧倒的に読みやすくで直感的です。

文字列の全体が、D3D12_ROOT_SIGNATURE_DESCに該当します。

StaticSamplerが、D3D12_STATIC_SAMPLER_DESCに該当する記述になります。フィルタリングの方法なども書けますが、レジスタ以外は省略可能です。嬉しいのはHLSL内でサンプラーの定義から使用まで完結することです。C++側から何もする必要がありません。

今回は書いていませんが、RootFlagsがD3D12_ROOT_SIGNATURE_FLAGSに該当します。ハマりそうな点としては、ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUTを書き忘れるとおそらく頂点入力が出来なくなるので気をつけます。今回は頂点はシェーダー内で生成しているので不要です。

RootFlagsとStaticSamplerを除いたそれ以外が、D3D12_ROOT_PARAMETERの配列と同等になります。この並びがC++側から参照するインデックス、つまりSetGraphicsRootConstantBufferViewやSetGraphicsRootDescriptorTableの引数RootParameterIndexに該当します。

こうしてHLSLのattributeに書いたRoot Signatureはシェーダのバイナリに含まれます。MSDNではfxcを使っていますが、D3DCompileFromFile等でID3DBlobを生成している場合にもD3DGetBlobPartでRoot Signatureを取り出せます。Vertex ShaderとPixel Shaderのどちらから取り出しても構いません。

...
ComPtr<ID3DBlob> vertexShader = afCompileHLSL(shaderName, "VSMain", "vs_5_0");
ComPtr<ID3DBlob> pixelShader = afCompileHLSL(shaderName, "PSMain", "ps_5_0");
ComPtr<ID3DBlob> rootSignatureBlob;
ComPtr<ID3D12RootSignature> rootSignature;
if (S_OK == D3DGetBlobPart(vertexShader->GetBufferPointer(), vertexShader->GetBufferSize(), D3D_BLOB_ROOT_SIGNATURE, 0, &rootSignatureBlob))
{
device->CreateRootSignature(0, rootSignatureBlob->GetBufferPointer(), rootSignatureBlob->GetBufferSize(), IID_PPV_ARGS(&rootSignature));
}
...
ComPtr<ID3DBlob> afCompileHLSL(const char* name, const char* entryPoint, const char* target)
{
char path[MAX_PATH];
sprintf_s(path, sizeof(path), "hlsl/%s.hlsl", name);
#ifdef _DEBUG
UINT flags = D3DCOMPILE_ENABLE_STRICTNESS | D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#else
UINT flags = D3DCOMPILE_ENABLE_STRICTNESS;
#endif
ComPtr<ID3DBlob> blob, err;
WCHAR wname[MAX_PATH];
MultiByteToWideChar(CP_ACP, 0, path, -1, wname, dimof(wname));
HRESULT hr = D3DCompileFromFile(wname, nullptr, nullptr, entryPoint, target, flags, 0, &blob, &err);
if (err) {
MessageBoxA(nullptr, (const char*)err->GetBufferPointer(), name, MB_OK | MB_ICONERROR);
}
return blob;
}

D3DGetBlobPartにD3D_BLOB_ROOT_SIGNATUREを渡すことでシェーダーバイナリからroot signatureを取り出します。 これは、以前はD3D12SerializeRootSignatureで作っていたroot signatureのバイナリを置き換えるもので、どちらも最後はCreateRootSignatureでID3D12RootSignatureを生成します。

ところで、MSDNによるとDX11でもRoot Signature入りのシェーダを問題なく使えるようで、Root Signatureを単に無視すると書いてあります。別の視点から見ると、DX12でのみ必要だったコードをC++から追い出して、プラットフォームの差を吸収するのにも役立ってくれそうです。

Sunday, August 21, 2016

DX12でDDSファイルからテクスチャ生成

DirectX11でのDDSロードの記事はこちらです。

以前、GetCopyableFootprintsの記事では手抜きで生成していたテクスチャですが、今回はミップマップやキューブマップも考慮して真面目に生成してみます。

ところで、テクスチャのアップロードは既にMicrosoftのD3DX12.hのUpdateSubresources関数があります。D3DX12.hが優れている所としては、必要なリソースはUpdateSubresourcesの引数を通してのみ取得しており、透明であること、マルチスレッド化が容易であることが挙げられます。反面、やや使いにくい点としては、D3DX12.hはDX12特有の概念であるリソースバリアやアップロードバッファの管理を呼び出し側に委ねています。使用者はDX12のアーキテクチャを理解した上で使わなければいけないので、手軽に使えるものではないです。また、DX12を隠蔽するものではないので、マルチプラットフォーム化を目指した設計の中では使いにくそうです。

今回はD3DX12.hを使用しないで実装します。

DDSのロード


DDSファイルをメモリ上にまるごとロードして解析します。以前やったDX11版とほぼ同じです。

struct DDSHeader {
uint32_t h3[3];
int h, w;
uint32_t h2[2];
int mipCnt;
uint32_t h13[13];
uint32_t fourcc, bitsPerPixel, rMask, gMask, bMask, aMask, caps1, caps2;
bool IsCubeMap() const { return caps2 == 0xFE00; }
int GetArraySize() const { return IsCubeMap() ? 6 : 1; }
int GetMipCnt() const { return std::max(mipCnt, 1); }
};
struct TexDesc {
IVec2 size;
int arraySize = 1;
bool isCubeMap = false;
};
struct AFTexSubresourceData
{
const void* ptr;
uint32_t pitch;
uint32_t pitchSlice;
};
static ComPtr<ID3D12Resource> LoadDDSTexture(const char* name, TexDesc& texDesc)
{
int size;
void* img = LoadFile(name, &size);
if (!img) {
aflog("LoadDDSTexture failed! %s", name);
return nullptr;
}
const DDSHeader* hdr = (DDSHeader*)img;
AFDTFormat format = AFDT_INVALID;
int(*pitchCalcurator)(int, int) = nullptr;
switch (hdr->fourcc) {
case 0x31545844: //'1TXD':
format = AFDT_BC1_UNORM;
pitchCalcurator = [](int w, int h) { return ((w + 3) / 4) * ((h + 3) / 4) * 8; };
break;
case 0x33545844: //'3TXD':
format = AFDT_BC2_UNORM;
pitchCalcurator = [](int w, int h) { return ((w + 3) / 4) * ((h + 3) / 4) * 16; };
break;
case 0x35545844: //'5TXD':
format = AFDT_BC3_UNORM;
pitchCalcurator = [](int w, int h) { return ((w + 3) / 4) * ((h + 3) / 4) * 16; };
break;
default:
ArrangeRawDDS(img, size);
format = AFDT_R8G8B8A8_UNORM;
pitchCalcurator = [](int w, int h) { return w * h * 4; };
break;
}
texDesc.size.x = hdr->w;
texDesc.size.y = hdr->h;
texDesc.arraySize = hdr->GetArraySize();
texDesc.isCubeMap = hdr->IsCubeMap();
int arraySize = hdr->GetArraySize();
int mipCnt = hdr->GetMipCnt();
std::vector<AFTexSubresourceData> r;
int offset = 128;
for (int a = 0; a < arraySize; a++) {
for (int m = 0; m < mipCnt; m++) {
int w = std::max(1, hdr->w >> m);
int h = std::max(1, hdr->h >> m);
int size = pitchCalcurator(w, h);
r.push_back({ (char*)img + offset, (uint32_t)pitchCalcurator(w, 1), (uint32_t)size });
offset += size;
}
}
ComPtr<ID3D12Resource> tex = afCreateTexture2D(format, texDesc, mipCnt, &r[0]);
assert(tex);
free(img);
return tex;
}
static const D3D12_HEAP_PROPERTIES defaultHeapProperties = { D3D12_HEAP_TYPE_DEFAULT, D3D12_CPU_PAGE_PROPERTY_UNKNOWN, D3D12_MEMORY_POOL_UNKNOWN, 1, 1 };
ComPtr<ID3D12Resource> afCreateTexture2D(AFDTFormat format, const struct TexDesc& desc, int mipCount, const AFTexSubresourceData datas[])
{
D3D12_RESOURCE_DESC resourceDesc = { D3D12_RESOURCE_DIMENSION_TEXTURE2D, 0, (UINT64)desc.size.x, (UINT)desc.size.y, (UINT16)desc.arraySize, (UINT16)mipCount, format, {1, 0} };
ComPtr<ID3D12Resource> tex;
HRESULT hr = deviceMan.GetDevice()->CreateCommittedResource(&defaultHeapProperties, D3D12_HEAP_FLAG_NONE, &resourceDesc, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, nullptr, IID_PPV_ARGS(&tex));
afWriteTexture(tex, desc, mipCount, datas);
return tex;
}
フォーマット、幅、高さ、配列数、ミップマップ、キューブマップかどうかの情報を取得してD3D12_RESOURCE_DESCを作り、CreateCommittedResourceを呼びます。また、サブリソース毎のピクセルデータの先頭位置とサイズをAFTexSubresourceData配列にまとめておきます。

テクスチャメモリへの転送


ここの処理の詳細は、以前やったGetCopyableFootprintsの記事を見てもらうと分かり易いと思います。違いは、今回の実装はミップマップやキューブマップもサポートします。

void afWriteTexture(ComPtr<ID3D12Resource> tex, const TexDesc& desc, int mipCount, const AFTexSubresourceData datas[])
{
const int maxSubresources = 100;
const UINT subResources = mipCount * desc.arraySize;
assert(subResources <= maxSubresources);
D3D12_PLACED_SUBRESOURCE_FOOTPRINT footprints[maxSubresources];
UINT64 rowSizeInBytes[maxSubresources], uploadSize;
UINT numRows[maxSubresources];
D3D12_RESOURCE_BARRIER transitions1[maxSubresources], transitions2[maxSubresources];
deviceMan.GetDevice()->GetCopyableFootprints(&tex->GetDesc(), 0, subResources, 0, footprints, numRows, rowSizeInBytes, &uploadSize);
ComPtr<ID3D12Resource> uploadBuf = afCreateBuffer((int)uploadSize);
assert(uploadBuf);
uploadBuf->SetName(__FUNCTIONW__ L" intermediate buffer");
D3D12_RANGE readRange = {};
BYTE* ptr;
HRESULT hr = uploadBuf->Map(0, &readRange, (void**)&ptr);
assert(ptr);
for (UINT i = 0; i < subResources; i++)
{
transitions1[i] = { D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, D3D12_RESOURCE_BARRIER_FLAG_NONE,{ tex.Get(), i, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_COPY_DEST } };
transitions2[i] = { D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, D3D12_RESOURCE_BARRIER_FLAG_NONE,{ tex.Get(), i, D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE } };
}
ID3D12GraphicsCommandList* list = deviceMan.GetCommandList();
list->ResourceBarrier(subResources, transitions1);
for (UINT i = 0; i < subResources; i++)
{
assert(datas[i].pitch == rowSizeInBytes[i]);
assert(datas[i].pitch <= footprints[i].Footprint.RowPitch);
for (UINT row = 0; row < numRows[i]; row++) {
memcpy(ptr + footprints[i].Offset + footprints[i].Footprint.RowPitch * row, (BYTE*)datas[i].ptr + datas[i].pitch * row, datas[i].pitch);
}
D3D12_TEXTURE_COPY_LOCATION uploadBufLocation = { uploadBuf.Get(), D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT, footprints[i] };
D3D12_TEXTURE_COPY_LOCATION nativeBufLocation = { tex.Get(), D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX, i };
list->CopyTextureRegion(&nativeBufLocation, 0, 0, 0, &uploadBufLocation, nullptr);
}
list->ResourceBarrier(subResources, transitions2);
uploadBuf->Unmap(0, nullptr);
deviceMan.AddIntermediateCommandlistDependentResource(uploadBuf);
deviceMan.AddIntermediateCommandlistDependentResource(tex);
}
GetCopyableFootprintsでアップロードバッファの大きさと各サブリソースの配置方法を取得します。forループ内ではサブリソースをアップロードバッファに配置し、CopyTextureRegionで転送させます。リソースバリアはCopyTextureRegionのforループの前後で全てのサブリソースに対して一括で発行します。その為にD3D12_RESOURCE_BARRIERを配列で作っておきます。forループ内でサブリソース毎に個別にResourceBarrierを呼び出しても動きますが、マイクロソフトの開発者の動画 https://youtu.be/Db2TaG49SRgによるとまとめて一回呼ぶほうが良いとのことです。ここは、日本語で要約した記事も参考にしてください。

コマンドリスト実行前のID3D12Resourceの保護


最後に、アップロードバッファからテクスチャへの転送を行うコマンドリストの実行が終わるまで、転送元であるアップロードバッファと転送先であるテクスチャの双方が間違って解放されないように保護しておく必要があります。

AddIntermediateCommandlistDependentResourceがそれをやっていて、何をするかというとAddRefしてコンテナに追加しておくだけで、コマンドリストの終了を確認したら全てReleaseします。

アップロードバッファが保護されるべきであるのは分かり易い所ですが、「生成したばかりのテクスチャが即不要になる」というのは無さそうで有り得るケースです。GPUがピクセルを転送しようとしたら転送先のテクスチャがなくなっていて不正なメモリアクセスというケースはDX12では発生します。コマンドリストから参照されている以上、テクスチャも忘れずに保護しておきます。

ちなみに、GitHubにあるマイクロソフトのDirectX-Graphics-Samplesではその場でコマンドリストを実行してフェンスで待つようになっています。その為アップロードバッファを生成した同じ関数内で使用済みになりその場で解放できるメリットがありますが、CPUはGPUがテクスチャを作る間ブロックされています。

まとめと課題


DX12のテクスチャの生成で分かり難いのは、アップロードバッファやリソースバリアなどの存在があると思いますが、この辺を隠蔽してかつてのDirectX SDKの感覚でテクスチャを生成できるようにしてみました。ただし今回の実装は、関数の引数が簡潔な反面、コマンドリストやコマンドリストから参照中のアップロードバッファの保持などを外部のモジュールに依存しているので、透明ではなく、そのままマルチスレッド化できないものになってしまいました。テクスチャのアップロード1つ取ってもどんな設計を選択するか、これもまたDX12時代にゲームエンジン開発者に委ねられた課題となりました。

Monday, August 15, 2016

DirectX 12: Resources Barriers and You を要約

MicrosoftのYouTubeチャンネル「Microsoft DirectX 12 and Graphics Education」より、「DirectX 12: Resources Barriers and You」を要約してみました。



リソースバリアの3つの役割

  1. リソースのステートの変更(例:RT=レンダーターゲットからSRVへの変更等)
  2. キャッシュコヒーレンシの確保
  3. パイプラインストール(例:書き込みの後に読み込みをする場合、順序が前後しないことを保証)


パフォーマンスの為の3つのルール

  1. D3D12_RESOURCE_STATE_COMMONとD3D12_RESOURCE_STATE_GENERIC_READステートは避けます。

    GENERIC_READは、D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER、D3D12_RESOURCE_STATE_INDEX_BUFFER、D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE、D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE、D3D12_RESOURCE_STATE_INDIRECT_ARGUMENT、D3D12_RESOURCE_STATE_COPY_SOURCEというフラグを全て含みます。これはリソースがあらゆる場所で使われる可能性があると見なされて多くのパイプラインストールを発生させます。GENERIC_READを使うのは"Upload heap"、すなわち、CPUからGPU側のバッファを更新する時に限るべきです。

    D3D12_RESOURCE_STATE_COMMONを使うべき場面はCPUがテクスチャにアクセスする場合と、"Copy engine"※に渡すCopy queueにリソースを渡す場合に限ります。

    SRVはD3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCEとD3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCEの二種類あります。通常は本当に使われるもの一つを指定したほうがよいですが、両方で使われることがわかっていればor演算子で両方指定します。1つだけ指定するのがよいと思ってRT→PS→NON PSのようにステートを渡り歩くのは良くないです。 
  2. 不必要なトランジションは避ける。例えば実装の都合で任意に中間ステートなどを置いたりしないことです。高くつく可能性があります。
  3. 複数リソースのトランジションを纏めて発行してパフォーマンスアップ  ID3D12GraphicsCommandList::ResourceBarrierには複数のバリアを引数として渡せます。例えば DS=depth stencilとRTをそれぞれSRVにするなら2つ同時に指定するべきです。また、複数リソースをSRVからD3D12_RESOURCE_STATE_COPY_DESTに変更して書き換える場合も、個別にResourceBarrierを呼ぶのではなくResourceBarrierの一度のコールで複数のリソースを指定すべきです。

おまけ
Depth bufferはハードウェア内部で圧縮されていて、SRVに変更をするとその時点から解凍作業が始まってすぐに使えません。 そこで、D3D12_RESOURCE_BARRIER_FLAG_NONEの替わりに、D3D12_RESOURCE_BARRIER_FLAG_BEGIN_ONLYとD3D12_RESOURCE_BARRIER_FLAG_END_ONLY の2つに分けて発行して、BEGINとENDの間にGPUに別の計算をさせると、計算と解凍を並行に走らせることができます。

※ "Copy engine"が何なのかわからなくて調べてみたのですが、どうやらここが解説ページです。

https://msdn.microsoft.com/en-us/library/windows/desktop/dn899217(v=vs.85).aspx

モダンなGPUハードウェア内に実装された3つのエンジン"Copy engine"、"3D engine"、"Compute engine"のうちの1つであり、D3D12_RESOURCE_STATE_COMMONステートはその"Copy engine"内部で行われる全ての内部的なステートを内包している、ということのようです。

Thursday, July 28, 2016

[DX12] GetCopyableFootprintsの謎に迫る

DirectX12ではテクスチャを作る時にメインメモリでもテクスチャでもない、中間バッファ(UPLOAD heap)を経由します。その際、ID3D12Device::GetCopyableFootprintsという関数で取得したレイアウトに従ってピクセルを中間バッファに格納します。

GetCopyableFootprints関数と共にいまいち存在意義がわかりにくい中間バッファがなぜ必要なのでしょうか。

答えはMSDNにありました。
https://msdn.microsoft.com/en-us/library/windows/desktop/dn899215(v=vs.85).aspx

テクスチャはキャッシュ効率を上げるためGPU上ではDDSファイルのように一直線(linear)に配置されておらず、non-linearな未知のレイアウト(unknown layout)として隠蔽されています。中間バッファの内容をそのGPUに都合の良い形式に変換してくれるのが、ID3D12GraphicsCommandList::CopyTextureRegionというわけです。

中間バッファに配置するピクセルもまたGPUに都合の良いようにアラインメントを揃える必要があり、そのための定数が定義されています。

  • D3D12_TEXTURE_DATA_PITCH_ALIGNMENT 
  • D3D12_TEXTURE_DATA_PLACEMENT_ALIGNMENT

MSDNによると、サブリソース(Subresource、ミップマップやキューブマップなどのテクスチャの構成単位)ごとに512バイト境界、テクスチャの各Row(行、つまりX方向一列)は256バイト境界に揃っている必要があります。

GetCopyableFootprintsを呼び出すと、各サブリソースをどのように一次元のバッファに格納すればいいのかD3D12_PLACED_SUBRESOURCE_FOOTPRINT構造体で教えてくれます。つまり、GetCopyableFootprintsはただのヘルパー関数であって、デバイスを操作したりデバイスに問い合わせたりしません。また、GetCopyableFootprintsを使わず自力で配置しても構いません。

中間バッファは二次元や三次元ではない、Constant Bufferなどと同じただの一次元バッファ(D3D12_RESOURCE_DIMENSION_BUFFERで、D3D12_HEAP_TYPE_UPLOAD)として生成するのですが、その中にGPUが読めるようにサブリソースを間違いなく配置する必要があります。

ところで、DDSなどから読み込んだテクスチャは多くの場合既にRowが256バイト境界に揃っています。そこに注目するとこんな手抜きアップローダーも書けます。

void afWriteBuffer(const ComPtr<ID3D12Resource> res, const void* buf, int size)
{
void* p;
D3D12_RANGE readRange = {};
res->Map(0, &readRange, &p);
memcpy(p, buf, size);
D3D12_RANGE wroteRange = {0, (SIZE_T)size};
id->Unmap(0, &wroteRange);
}
ComPtr<ID3D12Resource> afCreateBuffer(int size, const void* buf)
{
D3D12_HEAP_PROPERTIES prop = { D3D12_HEAP_TYPE_UPLOAD, D3D12_CPU_PAGE_PROPERTY_UNKNOWN, D3D12_MEMORY_POOL_UNKNOWN, 1, 1 };
D3D12_RESOURCE_DESC desc = { D3D12_RESOURCE_DIMENSION_BUFFER, 0, (UINT64)size, 1, 1, 1, DXGI_FORMAT_UNKNOWN, { 1, 0 }, D3D12_TEXTURE_LAYOUT_ROW_MAJOR, D3D12_RESOURCE_FLAG_NONE };
UBOID o;
deviceMan.GetDevice()->CreateCommittedResource(&prop, D3D12_HEAP_FLAG_NONE, &desc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&o));
afWriteBuffer(o, buf, size);
return o;
}
void UploadTexture(ComPtr<ID3D12Resource> tex, const void* buf)
{
D3D12_PLACED_SUBRESOURCE_FOOTPRINT footprint;
UINT64 uploadSize, rowSizeInBytes;
const D3D12_RESOURCE_DESC destDesc = tex->GetDesc();
deviceMan.GetDevice()->GetCopyableFootprints(&destDesc, 0, 1, 0, &footprint, nullptr, &rowSizeInBytes, &uploadSize);
assert(rowSizeInBytes == footprint.Footprint.RowPitch); // It is safe in most case!
ComPtr<ID3D12Resource> uploadBuf = afCreateBuffer((int)uploadSize, buf);
D3D12_TEXTURE_COPY_LOCATION uploadBufLocation = { uploadBuf.Get(), D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT, footprint }, nativeBufLocation = { tex.Get(), D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX, 0 };
ID3D12GraphicsCommandList* list = deviceMan.GetCommandList();
D3D12_RESOURCE_BARRIER transition1 = { D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, D3D12_RESOURCE_BARRIER_FLAG_NONE, { tex.Get(), 0, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_COPY_DEST } };
list->ResourceBarrier(1, &transition1);
list->CopyTextureRegion(&nativeBufLocation, 0, 0, 0, &uploadBufLocation, nullptr);
D3D12_RESOURCE_BARRIER transition2 = { D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, D3D12_RESOURCE_BARRIER_FLAG_NONE, { tex.Get(), 0, D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE } };
list->ResourceBarrier(1, &transition2);
deviceMan.AddIntermediateCommandlistDependentResource(uploadBuf); // keep until the command queue has been flushed
}
assert(rowSizeInBytes == footprint.Footprint.RowPitch) の行でひっかかるテクスチャは横が256バイト単位でない場合です。実はある程度のX方向の大きさがあって、サイズがPower of Twoで、ミップマップを無視するならひっかからないはずです。

(AddIntermediateCommandlistDependentResourceは、コマンドバッファの実行が終わるまで中間バッファを保持しておくためのものです。今回の主題ではないので説明は省略します)

Saturday, July 23, 2016

[DX12] 実例で学ぶ ID3D12Fence ダブルバッファリング編

前回やった、フェンスが指定の値に達するまで待つ関数を再掲載します。

void WaitFenceValue(ComPtr<ID3D12Fence> fence, UINT64 value)
{
HANDLE fenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
fence->SetEventOnCompletion(value, fenceEvent);
WaitForSingleObject(fenceEvent, INFINITE);
CloseHandle(fenceEvent);
}

フェンスは通常、渡す値をインクリメントしながら使います。現在GPUに渡したコマンドリストが全て終了するまで待機するには以下のようにします。

commandQueue->Signal(fence, fenceValue);
WaitFenceValue(fence, fenceValue++);
view raw flush_gpu.cpp hosted with ❤ by GitHub

fenceValueはアプリケーション開発者が管理します。UINT64型で宣言しておき、インクリメントしながら使います。

DX12ではダブルバッファリングを講じない限り、GPUが参照しているリソースをCPUが壊してしまわないように毎フレームの描画ごとにこれを実行する必要があります。例えば、ID3D12CommandAllocatorなどは、GPUが対象のコマンドリストを実行している間は破棄したり書き換えたりするとData Raceが発生することになります。それを防ぐ為にも上のコードを毎フレームを実行します。

しかし、こうすると毎フレームCPUとGPUがお互いの処理を待つような動作になるためフレームレートが低下します。そこで、GPUの処理中には保持しなければいけないリソースは2つずつ用意することにします。以下のようなクラスを用意すると便利です。

class FrameResources {
public:
~FrameResources();
ComPtr<ID3D12Resource> renderTarget;
ComPtr<ID3D12CommandAllocator> commandAllocator;
ComPtr<ID3D12Resource> constantBuffer;
ComPtr<ID3D12DescriptorHeap> srvHeap;
struct { char buf[256]; } *mappedConstantBuffer = nullptr;
UINT64 fenceValueToGuard = 0;
} frameResources[2];
このクラスのインスタンスを2つ作って、現在描画対象のバックバッファ(IDXGISwapChain3::GetCurrentBackBufferIndexで取得)によって参照を切り替えたいリソースを宣言に追加していきます。 fenceValueToGuardは該当フレームに対する描画が終了したときのfenceValueを保持しておくためのもので、フェンスの値がfenceValueToGuardを超えたらこれらのリソースに対するGPU側の処理が終了したと知ることができます。

constantBufferは前々回の記事で書いた巨大なConstant Bufferで、アプリケーションはここから256バイト単位で割り当てながら使うものです。mappedConstantBufferはconstantBufferをMapしっぱなしにしたメモリの先頭アドレスです。これらも2フレーム分作って置きます。ID3D12DescriptorHeapも同様な理由で前々回に動的に割り当てるようにしてあり、同様の理由で二重化するためここに追加します。

これで、Constant Bufferの書き換えに関してGPUを待機するコードがなくなりました…と、言いたいところですが、2フレーム前のGPUの処理が終わっていない場合を想定しなければなりません。

frameIndex = swapChain->GetCurrentBackBufferIndex();
FrameResources& res = frameResources[frameIndex];
WaitFenceValue(fence, res.fenceValueToGuard);
view raw begin_frame.cpp hosted with ❤ by GitHub
毎回フレームの描画開始前にFrameResourcesに保存しておいたfenceValueToGuardを取得してを呼び出します。ダブルバッファリングしているときはIDXGISwapChain3::GetCurrentBackBufferIndexが0と1が交互に来るので、結果的に2フレーム前のフェンスの値でWaitFenceValueを呼ぶことになります。

フレームの描画終了時には、以下のようにしてコマンドキューとfenceValueToGuard双方にfenceValue値を記録します。

FrameResources& res = frameResources[frameIndex];
commandQueue->Signal(fence.Get(), res.fenceValueToGuard = fenceValue++);
view raw end_frame.cpp hosted with ❤ by GitHub
こうすることで、GPUを待つことが完全になくなるわけではありませんが、真のダブルバッファリングが完成しました。

これで、どれだけ速くなるかみてみます。垂直同期をなくすためには、swapChain->Present(0, 0) と、最初のパラメータに0を指定します。うちのGTX 650ではダブルバッファリング前は400フレーム台だったのが500~600フレーム台にまで上がるようでした。

Wednesday, July 20, 2016

[DX12] 実例で学ぶ ID3D12Fence 基本編

DX11以前やOpenGLでは、基本的にCPUとGPU間でのいわゆるData Raceが生じないようにAPIレベルである程度保証してくれますが、DX12では同期を取る事は開発者に委ねられることになります。

そこで、GPUがどこまで実行したのか調べるのに使うフェンスですが、ID3D12Fence、HANDLE、ID3D12CommandQueue、それにUINT64型の変数と登場するオブジェクトが多くて分かり難いです。

結局、大雑把に何をするか書きだすと以下の2点に絞られます。

  • GPUが実行位置に合わせてフェンスの値をインクリメント
  • CPUはフェンスが期待した値になるまで待機する

GPUがフェンスの値をインクリメントと書きましたが、コマンドをGPUに送るのはCPUなのでその値もCPUから指定します。以下のようにコマンドリストの実行に混じって呼び出しているSignalがそれです。

commandQueue->ExecuteCommandLists(1, &A);
commandQueue->Signal(fence, 1);
commandQueue->ExecuteCommandLists(1, &B);
commandQueue->Signal(fence, 2);
commandQueue->ExecuteCommandLists(1, &C);
commandQueue->Signal(fence, 3);
A、B、Cがそれぞれ描画命令等が記録されたID3D12CommandListです。CPUから見て、フェンスの値が1になっていたらGPUでコマンドリストAの実行が終了しています。同様に2ならBが、3ならCの終了が保証されます。

GPUが設定したフェンスの値が指定値以上になるまで待つ関数はこんな感じです。

void WaitFenceValue(ComPtr<ID3D12Fence> fence, UINT64 value)
{
HANDLE fenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
fence->SetEventOnCompletion(value, fenceEvent);
WaitForSingleObject(fenceEvent, INFINITE);
CloseHandle(fenceEvent);
}
ここでvalueに1を指定した場合、Aの実行が完了していなければAが終わるまで待機します。Aの実行が終わっている状態、すなわちフェンスの値が1以上になったらすぐに戻ってきます。「1」ではなく「1以上」であることは重要です。Aが終了したかを知りたいためで、Bの実行終了時の「2」であってもやはりすぐ戻ってこなければいけないからです。

また、Signal呼び出しは常にvalueをインクリメントしながら行うのが定石で、その値を管理するためUINT64型の変数を用意することになります。

同期に使うHANDLEは使い捨てにしています。生成と破棄のオーバーヘッドが気になるなら保持しておく方法もありますが、何か意味のある状態をグローバルに保持するかのように見えるため可読性が落ちる気がします。個人的には使い捨てがおすすめです。
DirectX-Graphics-Samplesでは保持するように書かれています。

上のWaitFenceValue関数は必要最低限のコードでしたが、通常はGetCompletedValueを組み合わせて使うようです。待機する必要が無い場合を切り分けると少しパフォーマンスが上がる為と思われます。

void WaitFenceValue(ComPtr<ID3D12Fence> fence, UINT64 value)
{
if (fence->GetCompletedValue() >= value) {
return;
}
HANDLE fenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
fence->SetEventOnCompletion(value, fenceEvent);
WaitForSingleObject(fenceEvent, INFINITE);
CloseHandle(fenceEvent);
}

Saturday, July 9, 2016

DX12でGPUメモリを自分で管理する

今回のソースは https://github.com/yorung/dx12playground/tree/huge_global_constant です。

問題提起


前回シンプルなRoot Signatureをやってみましたが、毎回書き換えるConstant bufferのアドレスを固定してしまうのは何かと不都合です。

DX11までは度々以下のようなコードで描画することがありました。

ConstantBuffer cb;
for (obj : objs) {
void *p;
cb.Map(&p);
memcpy(p, obj.data, sizeof());
cb.Unmap();
Draw()
}
見た目のConstantBufferは1個なのですが、内部的なConstant Bufferがobjの数だけ確保され、それぞれ別のアドレスを持ちます(リネーム)。もし内部でも見た目通り1個のConstant Bufferしかなければ、Mapする時に前回のDrawが終わっていなければいけません。前回のDrawを待つなら遅くなりますし、待たなければGPUが参照中のメモリを壊す事態となります。

DX12で同じことをやるとGPUが参照中のメモリを壊してしまいます。複数のobjを描画するためにはobjの数だけConstantBufferを確保しなければならず、結局Constant bufferはアドレスを変えながらバインドしなければなりません。そうなると、Constant bufferのアドレスを保持するDescriptor Heapもまたバインドの数だけ必要になります。


GPUメモリのアロケータを作る


そこで、Constant BufferとDescriptor Heapを動的に確保するシステムを作ります。 

DX12のいいところは直接GPUのアドレスを指定できることです。巨大なConstant BufferやDescriptor Heapを1個ずつだけ作って、その中から適当な先頭アドレスをGPUに渡して描画、ということが出来るようになりました。

以下はその唯一のConstant Bufferを作るところです。

static const UINT maxConstantBufferBlocks = 1000;
ComPtr<ID3D12Resource> constantBuffer;
struct { char buf[0x100]; } *mappedConstantBuffer = nullptr;
int size = maxConstantBufferBlocks * 0x100;
D3D12_HEAP_PROPERTIES prop = { D3D12_HEAP_TYPE_UPLOAD, D3D12_CPU_PAGE_PROPERTY_UNKNOWN, D3D12_MEMORY_POOL_UNKNOWN, 1, 1 };
D3D12_RESOURCE_DESC desc = { D3D12_RESOURCE_DIMENSION_BUFFER, 0, (UINT64)size, 1, 1, 1, DXGI_FORMAT_UNKNOWN, { 1, 0 }, D3D12_TEXTURE_LAYOUT_ROW_MAJOR, D3D12_RESOURCE_FLAG_NONE };
device->CreateCommittedResource(&prop, D3D12_HEAP_FLAG_NONE, &desc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&constantBuffer));
D3D12_RANGE readRange = {};
HRESULT hr = constantBuffer->Map(0, &readRange, (void**)&mappedConstantBuffer);

Constant Bufferの先頭アドレスとサイズは256バイトのアラインメントの制限があります。ここでは256 x 1000バイト確保しました。また、MapしっぱなしにしてmemcpyだけすればGPUから変更が見えているというのもDX12のいいところです。

次は、唯一のDescriptor Heapを作ってみます。

static const UINT maxSrvs = 1024;
ComPtr<ID3D12DescriptorHeap> srvHeap;
const D3D12_DESCRIPTOR_HEAP_DESC srvHeapDesc = { D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV, maxSrvs, D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE };
device->CreateDescriptorHeap(&srvHeapDesc, IID_PPV_ARGS(&srvHeap));

1024個作ってみます。個数に特に意味はありません。
使用者はここから必要な数だけ確保し、CBVやSRVを書き込んでAPIに渡します。

int DeviceManDX12::AssignDescriptorHeap(int numRequired)
{
if (numAssignedSrvs + numRequired > maxSrvs) {
assert(0);
return -1;
}
int head = numAssignedSrvs;
numAssignedSrvs = numAssignedSrvs + numRequired;
return head;
}
void DeviceManDX12::AssignSRV(int descriptorHeapIndex, ComPtr<ID3D12Resource> res)
{
D3D12_CPU_DESCRIPTOR_HANDLE ptr = srvHeap->GetCPUDescriptorHandleForHeapStart();
ptr.ptr += device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV) * descriptorHeapIndex;
device->CreateShaderResourceView(res.Get(), nullptr, ptr);
}
void DeviceManDX12::SetAssignedDescriptorHeap(int descriptorHeapIndex)
{
ID3D12DescriptorHeap* ppHeaps[] = { srvHeap.Get() };
deviceMan.GetCommandList()->SetDescriptorHeaps(_countof(ppHeaps), ppHeaps);
D3D12_GPU_DESCRIPTOR_HANDLE addr = srvHeap->GetGPUDescriptorHandleForHeapStart();
addr.ptr += descriptorHeapIndex * device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
deviceMan.GetCommandList()->SetGraphicsRootDescriptorTable(0, addr);
}

AssignDescriptorHeapが確保関数で、先頭から順に返しているだけで、足し算しかしていません。使用者は「先頭から何番目を何個」という情報を知っているだけで十分だからです。ちなみに、numAssignedSrvsは毎フレーム0にリセットしています。

AssignSRVは確保したDescriptor HeapにSRVを書き込みます。CreateShaderResourceViewはDX11と名前が同じですが、何か新たにインスタンスが生成されたりするわけではなく、書き込むだけです。

最後に、SetAssignedDescriptorHeapでDescriptor HeapをGPUに渡します。今回のサンプルも前回のように単純化のためにDescriptor Tableは1個だけということにしています。descriptorHeapIndexを先頭位置として、何個のSRVやCBVを参照するかはDescriptor Tableの定義次第です。

以下はSRVの代わりにConstant Buffer開始位置をDescriptor Heapに書き込み、更にそれも唯一のConstant Bufferから動的に確保したものを使う関数です。

void DeviceManDX12::AssignConstantBuffer(int descriptorHeapIndex, const void* buf, int size)
{
int sizeAligned = (size + 0xff) & ~0xff;
int numRequired = sizeAligned / 0x100;
if (numAssignedConstantBufferBlocks + numRequired > maxConstantBufferBlocks) {
assert(0);
return;
}
int top = numAssignedConstantBufferBlocks;
numAssignedConstantBufferBlocks += numRequired;
memcpy(mappedConstantBuffer + top, buf, size);
D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc = {};
cbvDesc.BufferLocation = constantBuffer->GetGPUVirtualAddress() + top * 0x100;
cbvDesc.SizeInBytes = sizeAligned;
assert((cbvDesc.SizeInBytes & 0xff) == 0);
D3D12_CPU_DESCRIPTOR_HANDLE ptr = srvHeap->GetCPUDescriptorHandleForHeapStart();
ptr.ptr += deviceMan.GetDevice()->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV) * descriptorHeapIndex;
deviceMan.GetDevice()->CreateConstantBufferView(&cbvDesc, ptr);
}
唯一のConstant Bufferから256バイトにアラインメントされた単位で先頭から確保しますが、これもただの足し算です。渡されたデータを書き込むのはmemcpyです。これでGPUから見えます。
最後に、CreateConstantBufferViewでDescriptor Heapに先頭アドレスを書き込んで完了です。

実際の描画コードはこんな感じです。

afSetPipeline(pipelineState, rootSignature);
Mat invVP = inv(matV * matP);
int descriptorHeapIndex = deviceMan.AssignDescriptorHeap(2);
deviceMan.AssignConstantBuffer(descriptorHeapIndex, &invVP, sizeof(invVP));
deviceMan.AssignSRV(descriptorHeapIndex + 1, texId);
deviceMan.SetAssignedDescriptorHeap(descriptorHeapIndex);
afDraw(PT_TRIANGLESTRIP, 4);
1個だけのDescriptor Tableは2つの連続したDescripor Heapを参照し、最初がCBV、次がSRVになっています。使用者がGPUメモリを管理する手間が減ってコードがすっきりしました。

まとめ


GPUメモリのアロケータをやってみました。

こうしてみると、最後にGPUに渡すのはGPUのアドレスになっていて、GPUメモリの管理方法はほぼアプリケーション開発者に委ねられているのがわかります。

上では複数オブジェクトを描画するときのDX11のリネームを例にあげましたが、単一のオブジェクトを描画していても次のフレームのDraw Callの時点で前のフレームをまだGPUが処理していた場合、リネームが発生します。
つまり、DX12ではダブルバッファリングやトリプルバッファリングを行う際にレンダーターゲットだけでなくコンスタントバッファもプログラマの責任で多重化します。

今回の作業はDX12を使ったダブルバッファリングやトリプルバッファリングの下準備でもあります。

今回はシンプルに実装するために全てを動的に確保するようにしましたが、マテリアル情報のような変化しないConstant Bufferを変化しないDescriptor Heapに入れるなど、変化の有無でDescriptor Tableを分けるような工夫が必要になりそうです。

Saturday, April 16, 2016

DirectX12のRoot Signatureをシンプルに定義する方法を考える

DirectX12はAPIが難解ですが、複雑さを隠蔽して使いやすくラップする方法を考えてみます。


例として、Photo Sphereと三角形ポリゴンを描画してみました。
ソースは https://github.com/yorung/dx12playground/tree/simple にアップしました。

Root SignatureとPSOの定義


Root Signatureをどう設計するかはMSDNの以下の記事が参考になります。

https://msdn.microsoft.com/en-us/library/windows/desktop/dn899123(v=vs.85).aspx

DirectX 12の難解さの第一はおそらくRoot Signatureだと思います。実行効率を最大化するには確かにRoot Signatureを理解するのは有用かもしれませんが、とりあえず三角ポリゴンを描きたいだけの時など、細かい事はとりあえず後回しにしたい時もあります。

そこで、必要最低限の機能でAPIを単純化したラッパーを設計してみます。それは、以下の要件を満たすものにします。
  • Descriptor Heap及びDescriptor Tableはそれぞれ1個のみ
  • static samplerのみを使う
  • SRV、CBV、static samplerの使用個数はRoot Signature毎に必要数定義できる

MSDN風に図にするとこうなります。


この仕様でRoot Signature及びPipeline State Object(PSO)まで定義するコードをラッパーで記述すると以下のようになります。

D3D12_DESCRIPTOR_RANGE descriptors[] = { // The descriptor table
CDescriptorCBV(0), // constant buffer view bound to register b0
CDescriptorSRV(0), // shader resource view bound to register t0
};
D3D12_STATIC_SAMPLER_DESC samplers[] = {
CSampler(0, D3D12_FILTER_MIN_MAG_LINEAR_MIP_POINT, D3D12_TEXTURE_ADDRESS_MODE_WRAP), // sampler bound to register s0
};
ComPtr<ID3D12RootSignature> rootSignature = afCreateRootSignature(dimof(descriptors), descriptors, dimof(samplers), samplers);
ComPtr<ID3D12PipelineState> pipelineState = afCreatePSO(shader, nullptr, 0, BM_NONE, DSM_DEPTH_CLOSEREQUAL_READONLY, CM_DISABLE, rootSignature);
view raw create_pso.cpp hosted with ❤ by GitHub
構造体の定義にラッパークラスを作って使っています。Shader Resource View(SRV)やConstant Buffer View(CBV)のD3D12_DESCRIPTOR_RANGEはレジスタのみ引数に取るように、サンプラーはレジスタとフィルタとアドレスモードを取るように単純化しています。
class CSampler : public D3D12_STATIC_SAMPLER_DESC
{
public:
CSampler(int shaderRegister, D3D12_FILTER samplerFilter, D3D12_TEXTURE_ADDRESS_MODE wrap)
{
Filter = samplerFilter;
AddressU = wrap;
AddressV = wrap;
AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP;
MipLODBias = 0;
MaxAnisotropy = 1;
ComparisonFunc = D3D12_COMPARISON_FUNC_NEVER;
BorderColor = D3D12_STATIC_BORDER_COLOR_TRANSPARENT_BLACK;
MinLOD = 0;
MaxLOD = D3D12_FLOAT32_MAX;
ShaderRegister = shaderRegister;
RegisterSpace = 0;
ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;
}
};
class CDescriptorCBV : public D3D12_DESCRIPTOR_RANGE {
public:
CDescriptorCBV(int shaderRegister) {
RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_CBV;
NumDescriptors = 1;
BaseShaderRegister = shaderRegister;
RegisterSpace = 0;
OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;
}
};
class CDescriptorSRV : public D3D12_DESCRIPTOR_RANGE {
public:
CDescriptorSRV(int shaderRegister) {
RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV;
NumDescriptors = 1;
BaseShaderRegister = shaderRegister;
RegisterSpace = 0;
OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;
}
};
Root Signatureを生成するラッパー関数は以下の通りです。本来複数個使えるD3D12_ROOT_PARAMETERを1個に制限しています。また、本来細かに設定すべきアクセス制限は単にD3D12_SHADER_VISIBILITY_ALLを指定しています。もちろん単純化のためですが、性能低下に繋がるようであれば今後の課題です。
ComPtr<ID3D12RootSignature> afCreateRootSignature(int numDescriptors, D3D12_DESCRIPTOR_RANGE descriptors[], int numSamplers, D3D12_STATIC_SAMPLER_DESC samplers[])
{
ComPtr<ID3D12RootSignature> rs;
ComPtr<ID3DBlob> signature;
ComPtr<ID3DBlob> error;
D3D12_ROOT_PARAMETER rootParameter = {};
rootParameter.ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE;
rootParameter.ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL;
rootParameter.DescriptorTable.NumDescriptorRanges = numDescriptors;
rootParameter.DescriptorTable.pDescriptorRanges = descriptors;
D3D12_ROOT_SIGNATURE_DESC rsDesc = { (UINT)(numDescriptors ? 1 : 0), &rootParameter, (UINT)numSamplers, samplers, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT };
HRESULT hr = D3D12SerializeRootSignature(&rsDesc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error);
assert(hr == S_OK);
hr = deviceMan.GetDevice()->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&rs));
assert(hr == S_OK);
return rs;
}

リソースの準備


テクスチャとコンスタントバッファを生成し、上のD3D12_DESCRIPTOR_RANGEの配列の順に並べてDescriptor Heapを生成します。
ComPtr<ID3D12Resource> texId = afLoadTexture(texFileName, texDesc);
ComPtr<ID3D12Resource> uboId = afCreateUBO(sizeof(Mat));
ComPtr<ID3D12Resource> srvs[] = {
uboId,
texId,
};
ComPtr<ID3D12DescriptorHeap> heap = afCreateDescriptorHeap(dimof(srvs), srvs);
Descriptor Heapを生成するヘルパー関数は以下のように定義しています。内部でSRVかCBVかでCreateShaderResourceViewとCreateConstantBufferViewに分岐していますが、呼び出し側は区別せずID3D12Resourceの配列として渡します。
ComPtr<ID3D12DescriptorHeap> afCreateDescriptorHeap(int numSrvs, ComPtr<ID3D12Resource> srvs[])
{
ComPtr<ID3D12DescriptorHeap> heap;
D3D12_DESCRIPTOR_HEAP_DESC srvHeapDesc = {};
srvHeapDesc.NumDescriptors = numSrvs;
srvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
srvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
HRESULT hr = deviceMan.GetDevice()->CreateDescriptorHeap(&srvHeapDesc, IID_PPV_ARGS(&heap));
assert(hr == S_OK);
for (int i = 0; i < numSrvs; i++) {
D3D12_RESOURCE_DESC desc = srvs[i]->GetDesc();
auto ptr = heap->GetCPUDescriptorHandleForHeapStart();
ptr.ptr += deviceMan.GetDevice()->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV) * i;
if (desc.Dimension == D3D12_RESOURCE_DIMENSION_BUFFER) {
D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc = {};
cbvDesc.BufferLocation = srvs[i]->GetGPUVirtualAddress();
cbvDesc.SizeInBytes = (UINT)desc.Width;
assert((cbvDesc.SizeInBytes & 0xff) == 0);
deviceMan.GetDevice()->CreateConstantBufferView(&cbvDesc, ptr);
} else {
deviceMan.GetDevice()->CreateShaderResourceView(srvs[i].Get(), nullptr, ptr);
}
}
return heap;
}

描画


Root SignatureとPSO、Descriptor Heapを指定し、Draw Callして描画完了です。
テクスチャ及びコンスタントバッファのバインドはDescriptor Heapを1個指定するだけで終了します。これは、Descriptor TableとしてSRV及びCBVの並びが既に定義されているので、定義通りにSRV及びCBVを並べたDescriptor Heapを指定するだけでバインドが終わります。
ID3D12GraphicsCommandList* list = deviceMan.GetCommandList();
list->SetPipelineState(pipelineState.Get());
list->SetGraphicsRootSignature(rootSignature.Get());
afSetDescriptorHeap(heap);
afWriteBuffer(uboId, &invVP, sizeof(invVP));
list->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
list->DrawInstanced(numVertices, 1, 0, 0);
view raw draw.cpp hosted with ❤ by GitHub
こうして見ると、Descriptor HeapはSRVとCBVを配列の中に保持するコンテナとも言えますし、描画時の各種リソースのバインドをリソース生成時に完了させる媒体とも言えそうです。

Descriptor Heapのバインドは以下のヘルパー関数を使っています。
void afSetDescriptorHeap(ComPtr<ID3D12DescriptorHeap> heap)
{
ID3D12GraphicsCommandList* list = deviceMan.GetCommandList();
ID3D12DescriptorHeap* ppHeaps[] = { heap.Get() };
list->SetDescriptorHeaps(_countof(ppHeaps), ppHeaps);
list->SetGraphicsRootDescriptorTable(0, heap->GetGPUDescriptorHandleForHeapStart());
}
view raw heap_binder.cpp hosted with ❤ by GitHub
バインドするDescriptor Heapを1個に制限しています。また、GetGPUDescriptorHandleForHeapStartで先頭アドレスをオフセットを指定したりせずそのまま渡すようになっています。

今回、頂点バッファは使っていません。頂点4つは頂点シェーダーで適当に作っています。通常ならここに頂点バッファやインデックスバッファのバインドが加わり、PSOの定義にInput Layoutの定義が入りますが、本題ではないので省略します。

考察


短いコードでRoot Signatureを定義できる反面、いくつかの課題があります。

1. Constant bufferのダブルバッファリングが考慮されていない
2. per draw, per material, per frameのConstant bufferの使い分けをどうするか

今回、Constant bufferのダブルバッファリングが為されていないので、毎回GPU側の描画の終了を待ってからConstant Bufferを書き換えています。ダブルバッファリングのためには描画の度にConstant Bufferを切り替える必要があるのですが、現在のConstant Bufferはテクスチャと一緒に1つのDescriptor Heapに紐付けられています。テクスチャは書き換えないリソースで、Constant Bufferは通常書き換えるリソースと見た場合、Descriptor Heapも書き換えの有無で分割するのがよいかもしれません。

また、通常レンダリングエンジンの設計上、per frame, per materialのように適用範囲に合わせてConstant Bufferを分割することが多いと思いますが、適用範囲毎にDescriptor Heapも分割するのが効率が良さそうに見えるので、今回のようにDescriptor Heapを1個に限ってしまうと不都合です。

逆に言うとパフォーマンスを気にしないのであれば必要十分な機能を有しているので、勉強用、プロトタイピング用としてDescriptor Table1個で済ますのは方法としてアリと言えるのではないでしょうか。

Tuesday, March 29, 2016

OpenGLでDirectX風に頂点レイアウトを書く方法

以前の記事で、OpenGLの頂点レイアウトやInput Layout Qualifiersの事を書きましたが、もうちょっといい方法を思いついたので書きます。以下のようにDirectX11のように頂点レイアウトを構造体の配列として書きます。

struct InputElement {
const char* name;
ShaderFormat format;
int offset;
int inputSlot;
bool perInstance;
};
static InputElement elements[] = {
{"POSITION", SF_R32G32B32_FLOAT, 0, 0, false},
{"COLOR", SF_R8G8B8A8_UNORM, 12, 0, false},
{"TEXCOORD", SF_R32G32_FLOAT, 16, 0, false},
};
GLuint CreateProgramWithElements()
{
GLuint program = glCreateProgram();
for (int i = 0; i < numElements; i++) {
glBindAttribLocation(program, i, elements[i].name);
}
return program;
}
void CreatePrograms()
{
GLuint a = CreateProgramWithElements();
GLuint b = CreateProgramWithElements();
...
この方法のキモは、elementsの配列の並び順にglBindAttribLocationで0から順に番号を割り当てることです。この例では、"POSITION"が0番、"COLOR"が1番、"TEXCOORD"が2番です。例えば以下のような頂点シェーダのための定義になります。inのところだけ見てください。glBindAttribLocationを使うので当然Input Layout Qualifiersは不要です。

#version 310 es
precision highp float;
in vec3 POSITION;
in vec2 TEXCOORD;
in vec4 COLOR;
out vec2 texcoord;
out vec4 color;
layout (binding = 0) uniform matUbo {
layout (row_major) mat4 matProj;
};
void main() {
gl_Position = vec4(POSITION.xyz, 1) * matProj;
texcoord = TEXCOORD;
color = COLOR;
}
この過程を同じ頂点レイアウトを使用する複数のシェーダーに適用することで、シェーダが変わっても同じVAO(Vertex Array Object)を使用できます。言い換えると、シェーダーからglGetAttribLocationで"POSITION"は何番か、と問い合わせる方法でVAOを生成してしまうとそのシェーダ専用のVAOしか作れなくなってしまう為、InputElementという仲介役を立てたと言えます。

以上を踏まえたVAOを生成する実装の実例を紹介します。長いですがやってることはInputElementの配列を解釈しながらVAOを作っているだけです。こうして作ったVAOは同じInputElement配列を指定して作った頂点シェーダならどれにでもバインドできるようになります。

void afSetVertexAttributes(const InputElement elements[], int numElements, int numBuffers, GLuint const vertexBufferIds[], const int strides[])
{
for (int i = 0; i < numElements; i++) {
const InputElement& d = elements[i];
glBindBuffer(GL_ARRAY_BUFFER, vertexBufferIds[d.inputSlot]);
GLenum r = glGetError();
if (r != GL_NO_ERROR) {
aflog("glBindBuffer error! i=%d inputSlot=%d vbo=%d\n", i, d.inputSlot, vertexBufferIds[d.inputSlot].x);
}
afHandleGLError(glEnableVertexAttribArray(i));
switch (d.format) {
case SF_R32_FLOAT:
case SF_R32G32_FLOAT:
case SF_R32G32B32_FLOAT:
case SF_R32G32B32A32_FLOAT:
glVertexAttribPointer(i, d.format - SF_R32_FLOAT + 1, GL_FLOAT, GL_FALSE, strides[d.inputSlot], (void*)d.offset);
break;
case SF_R8_UNORM:
case SF_R8G8_UNORM:
case SF_R8G8B8_UNORM:
case SF_R8G8B8A8_UNORM:
glVertexAttribPointer(i, d.format - SF_R8_UNORM + 1, GL_UNSIGNED_BYTE, GL_TRUE, strides[d.inputSlot], (void*)d.offset);
break;
case SF_R8_UINT_TO_FLOAT:
case SF_R8G8_UINT_TO_FLOAT:
case SF_R8G8B8_UINT_TO_FLOAT:
case SF_R8G8B8A8_UINT_TO_FLOAT:
glVertexAttribPointer(i, d.format - SF_R8_UINT_TO_FLOAT + 1, GL_UNSIGNED_BYTE, GL_FALSE, strides[d.inputSlot], (void*)d.offset);
break;
case SF_R8_UINT:
case SF_R8G8_UINT:
case SF_R8G8B8_UINT:
case SF_R8G8B8A8_UINT:
#ifdef AF_GLES31
glVertexAttribIPointer(i, d.format - SF_R8_UINT + 1, GL_UNSIGNED_BYTE, strides[d.inputSlot], (void*)d.offset);
#endif
break;
case SF_R16_UINT_TO_FLOAT:
case SF_R16G16_UINT_TO_FLOAT:
case SF_R16G16B16_UINT_TO_FLOAT:
case SF_R16G16B16A16_UINT_TO_FLOAT:
glVertexAttribPointer(i, d.format - SF_R16_UINT_TO_FLOAT + 1, GL_UNSIGNED_SHORT, GL_FALSE, strides[d.inputSlot], (void*)d.offset);
break;
case SF_R16_UINT:
case SF_R16G16_UINT:
case SF_R16G16B16_UINT:
case SF_R16G16B16A16_UINT:
#ifdef AF_GLES31
glVertexAttribIPointer(i, d.format - SF_R16_UINT + 1, GL_UNSIGNED_SHORT, strides[d.inputSlot], (void*)d.offset);
#endif
break;
case SF_R32_UINT:
case SF_R32G32_UINT:
case SF_R32G32B32_UINT:
case SF_R32G32B32A32_UINT:
#ifdef AF_GLES31
glVertexAttribIPointer(i, d.format - SF_R32_UINT + 1, GL_UNSIGNED_INT, strides[d.inputSlot], (void*)d.offset);
#endif
break;
default:
assert(0);
break;
}
#ifdef AF_GLES31
if (d.perInstance) {
glVertexAttribDivisor(i, 1);
}
#endif
}
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
GLuint afCreateVAO(const InputElement elements[], int numElements, int numBuffers, GLuint const vertexBufferIds[], const int strides[], GLuint ibo)
{
VAOID vao;
glGenVertexArrays(1, &vao.x);
glBindVertexArray(vao);
afSetVertexAttributes(elements, numElements, numBuffers, vertexBufferIds, strides);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
glBindVertexArray(0);
return vao;
}
view raw vao_maker.cpp hosted with ❤ by GitHub
afCreateVAOは、InputElement構造体の配列、インデックスバッファ、頂点バッファの配列(それぞれ順にInputElement構造体のslotに対応、OpenGLにslotという概念は無いが、DirectX風に定義)、strideの配列を受け取ってVAOを作ります。

ほぼ同じ引数を受け取るafSetVertexAttributesは、頂点バッファを現在のVAOにバインドする部分です。VAOの無いOpenGL ES 2.0でもそのまま使えるコードです。

こうすることでInput Layout Qualifiersを使う必要もなくなり、glGetAttribLocationも必要なくなり、OpenGL ES 2.0、OpenGL ES 3.0双方で同じコードが動き、DirectXとのコードの共通化にも役立ちます。

Saturday, February 6, 2016

DirectXとOpenGLとrow majorとcolumn majorのまとめ

DirectXの行列は、移動が4行目に来ます。
   

対して、OpenGLはちょうどDirectXの行列を転置したもので、移動が4列目に来ます。
 

ところが、行列をfloat型16個の配列として見てみると、両者のメモリレイアウトは全く同じです。





例えばXMMatrixLookAtRHで生成したビュー行列をgluLookAtと置き換える正常に動作します。OpenGLはcolumn major(列優先)で、DirectXはrow major(行優先)なので、数学的に1回、メモリレイアウトで1回、2回転置してメモリレイアウトが同じになったと見ることもできます。

では両者は同じかと言うと、数学的な定義とメモリレイアウトは別の話なので注意が必要です。頂点v、ワールド(モデル)行列W、ビュー行列V、射影行列Pの計算式はOpenGLとDirectXで逆の並びになっています。

DirectX:

OpenGL:


この並びはシェーダーを書く時の慣習にもなっていて、多くの参考書はこの順で書かれているはずです。両者のメモリレイアウトは同じはずなので、掛け算の順番が違うということは、GLSLはcolumn major、HLSLはrow majorの並びと推測できます。

ところが、GLSLもHLSLもcolumn majorがデフォルトになっています。OpenGLはC/C++側もGLSLもcolumn majorなので良いのですが、DirectXはC/C++側がrow major、HLSLがcolumn majorと異なっています。

しかし、これはおかしいです。row majorではないと、

として正しい結果が得られないはずだからです。どうも、DirectXはシェーダに行列を渡す時点で転置させる習慣があったようで、結果としてこの順で掛け算して正しくなっていました。おそらく、DirectX9時代はD3DXあたりで勝手にやってくれたのでプログラマが気にする必要が無かったのだと思います。

しかし、DirectX11はConstant Bufferの配置までプログラマに委ねられているので、理解して使う必要があります。実は、DirectX11ではOpenGLと同じように、

の順で掛けると正しい結果になります。あるいはHLSLで行列を宣言するときにrow_majorを指定すると、

この従来の順で計算しても正しくなります。

Saturday, January 9, 2016

続・Visual StudioからAndroid開発を実践してみた

Visual Studio 2015からAndroidの開発をしてみましたが、思わぬエラーに遭遇して悩みました。そんな経験を書き連ねてみます。


Clangで内部エラー


Visual Studio 2015でのデフォルトのコンパイラがClangなのですが、特定のソースファイルをコンパイルする時に以下のような内部エラーを発生させるようです。
1>  This application has requested the Runtime to terminate it in an unusual way.
1>  Please contact the application's support team for more information.
1>  Assertion failed!
1>
1>  Program: C:\ProgramData\Microsoft\AndroidNDK\android-ndk-r10e\toolchains\llvm-3.6\prebuilt\windows\bin\clang.exe
1>  File: /s/ndk-toolchain/src/llvm-3.6/llvm/tools/clang/lib/AST/DeclBase.cpp, Line 1299
1>
1>  Expression: DeclKind != Decl::LinkageSpec && "Should not perform lookups into linkage specs!"
1>clang.exe : error : clang frontend command failed with exit code 3 (use -v to see invocation)
1>  clang version 3.6
1>  Target: armv7-none-linux-androideabi
1>  Thread model: posix
1>  clang.exe: note: diagnostic msg: PLEASE submit a bug report to http://source.android.com/source/report-bugs.html and include the crash backtrace, preprocessed source, and associated run script.
1>  clang.exe: note: diagnostic msg:
1>  ********************
1>
1>  PLEASE ATTACH THE FOLLOWING FILES TO THE BUG REPORT:
1>  Preprocessed source(s) and associated run script(s) are located at:
1>  clang.exe: note: diagnostic msg: C:\Users\Jin0103\AppData\Local\Temp\bvh-1eb1f7.cpp
1>  clang.exe: note: diagnostic msg: C:\Users\Jin0103\AppData\Local\Temp\bvh-1eb1f7.sh
1>  clang.exe: note: diagnostic msg:
1>
1>  ********************

回避方法が無さそうなので、やむを得ずGCCに切り替えました。プロジェクトのプロパティのPlatform Toolsetで変更可能です。



libmのリンク


Visual Studioではsin, cos等の数学ライブラリは明示的にリンクしなければ以下のようなリンクエラーを出すようです。

1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lmathlib.c(51): error : undefined reference to 'sin'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lmathlib.c(56): error : undefined reference to 'cos'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lmathlib.c(61): error : undefined reference to 'tan'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lmathlib.c(66): error : undefined reference to 'asin'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lmathlib.c(71): error : undefined reference to 'acos'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lmathlib.c(78): error : undefined reference to 'atan2'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lmathlib.c(109): error : undefined reference to 'floor'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lmathlib.c(120): error : undefined reference to 'ceil'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lmathlib.c(138): error : undefined reference to 'fmod'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lmathlib.c(157): error : undefined reference to 'ceil'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lmathlib.c(157): error : undefined reference to 'floor'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lmathlib.c(167): error : undefined reference to 'sqrt'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lmathlib.c(183): error : undefined reference to 'log'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lmathlib.c(186): error : undefined reference to 'log10'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lmathlib.c(187): error : undefined reference to 'log'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lmathlib.c(187): error : undefined reference to 'log'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lmathlib.c(194): error : undefined reference to 'exp'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lobject.c(103): error : undefined reference to 'pow'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lobject.c(104): error : undefined reference to 'floor'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lobject.c(108): error : undefined reference to 'fmod'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/ltable.c(92): error : undefined reference to 'floor'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/ltable.c(103): error : undefined reference to 'frexp'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lvm.c(858): error : undefined reference to 'fmod'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lvm.c(883): error : undefined reference to 'pow'

Androidのライブラリはファイル名が全てlibGLESv3, libOpenSLESのようにlibで始まるのですが、libを省略してGLESv3, OpenSLESのように指定する習慣があるようです。数学ライブラリはlibmなので、mとだけ指定するとリンクできます。


Precompiled Headerは切ってみる


なぜ発生するかわからないコンパイルエラーが出るときは、Precompiled Headerを切ってみます。(デフォルトで使うようになっている)


GCCのプリコンパイル済みヘッダはWindows+Visual Studioのものと仕組みが違います。

Windows+Visual Studioでは、例えばstdafx.hを各ソースの最上位でincludeすることを強制して重複する作業を省略しようとします。

GCCでは、各ソースの最上位で強制的にpch.hをincludeしたことにしてしまうようです。

個人的な経験なので参考にならないかもしれませんが、なぜかCであるLuaのソースコードで__cplusplusが定義されたり、Luaのソースコードで最上位で定義されなければならないLUA_COREやLUA_LIB等のマクロ定義に先駆けてlua.hがincludeされたことになっていました。

ぱっと見で分かり難いエラーの原因がプリコンパイル済みヘッダの間違った使い方であったというのはありがちなシチュエーションなので、仕組みを理解したうえで適用するか決めるのがよさそうです。

Sunday, January 3, 2016

Visual StudioからAndroid開発を実践してみた

実際に開発を進めてみるとハマることが多く、メモを残します。

assetsやresフォルダの扱い


Android Studioのようにエクスプローラー上にassetsやresフォルダがあっても自動的に取り込まれません。


Visual Studio上で明示的にassetsやresフォルダを作ってファイルを一つ一つ追加します。

ところで、例えばdrawableフォルダを追加する時に既にエクスプローラー上にdrawableフォルダがある場合エラーが発生します。


これは、Visual Studioがプロジェクト上の概念的なフォルダではなく、本当にディスク上にフォルダを生成しようとしているためです。よって、既存のdrawableフォルダを一度別の場所に移動してからVisual Studio上でdrawableフォルダを作るという過程が必要です。

この辺はWindowsアプリを開発しているときと挙動が違ってややこしいです。


Activityが無いアプリの実機へのインストール


Visual Studio上からapkを実機で実行する場合、起動するActivityをプロジェクトのプロパティに指定するようになっています。



ところが、ウィジェットやライブ壁紙の場合起動可能なアクティビティが存在しない場合があります。その際、Ctrl-F5で実行しようとすると以下のようなエラーが発生してインストールすらしてくれません。

Could not retrieve the launch activity from the package (apk)

こういう時は、以下のようにコマンドラインから入力してインストールします。(バッチを作ります)
adb install -r ARM\Release\KiriWidget.apk

Visual Studio 2015から使われるadb.exeは以下のパスにあるようです。必要に応じて環境変数PATHに通しておきます。

C:\Program Files (x86)\Android\android-sdk\platform-tools