Quantcast
Channel: shobomalog
Viewing all 40 articles
Browse latest View live

OpenGL 4.xを少し

$
0
0

とりあえず横線を書くだけ。

Mac OS 10.9.2 (Marvericks) + Xcode 5.1 + Mac mini late 2012 (Intel HD Graphics 4000)

多少手直しすればWindowsでも動くはず(注意:Windows版のIntelドライバはOpenGL 4.0までしか対応していないので、バージョン指定を変えること)

OpenGLリソースの解放処理がないので注意

C++コード

// OpenGL test

// No need to use GLEW on Mac OS.
#define USE_GLEW 0
// if GLEW does not compiled as .dll(Windows) or .dylib(Mac OS), define this.
#define GLEW_STATIC

#include <unistd.h>
#include <iostream>
#include <fstream>
#include <memory>
#include <sstream>
#if USE_GLEW
# include <GL/glew.h>
#else
# define GL_DO_NOT_WARN_IF_MULTI_GL_VERSION_HEADERS_INCLUDED
# include <OpenGL/gl3.h>
#endif
#include <GLFW/glfw3.h>

#ifdef _WIN32
#pragma comment( lib, "glfw3.lib" )
#pragma comment( lib, "opengl32.lib" )
#if USE_GLEW
# ifdef NDEBUG
# pragma comment( lib, "glew32s.lib" )
# else
# pragma comment( lib, "glew32sd.lib" )
# endif
#endif
#endif // _WIN32

#define SCREEN_WIDTH (800)
#define SCREEN_HEIGHT (600)

#define SHADER_DIR "/path/to/my/shader/files/dir"

namespace
{
 std::unique_ptr<char[]> loadFile(const char* path)
 {
 auto fp = std::ifstream(path, std::ios::in | std::ios::binary);
 if(!fp)
 throw "File cannot open.";

 auto len = fp.seekg(0, std::ios::end).tellg();
 if(len <= 0)
 throw "File is empty.";
 fp.seekg(0, std::ios::beg);

 std::unique_ptr<char []> cs(new char[len]);
 fp.read(cs.get(), len);
 if(fp.fail())
 throw "Reading file failed.";

 if(fp.gcount() != len)
 throw "File cannot read to the end.";

 return cs;
 };

 const auto glshader_deletor = [](GLuint *shader) {
 glDeleteShader(*shader);
 delete shader;
 };

 std::unique_ptr<GLuint, decltype(glshader_deletor)> loadShader(const char* code, GLenum type)
 {
 GLuint shader = glCreateShader(type);
 glShaderSource(shader, 1, &code, nullptr);
 glCompileShader(shader);

 GLint compiled;
 glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
 if(compiled == GL_FALSE)
 {
 GLchar log[1024];
 GLsizei len = sizeof(log) / sizeof(*log);

 glGetShaderInfoLog(shader, len, &len, log);
 std::cerr << "glCompileShader() gets error." << std::endl << log;

 shader = GL_INVALID_VALUE;
 throw "Shader compile error.";
 }

 std::unique_ptr<GLuint, decltype(glshader_deletor)> g(new GLuint(shader), glshader_deletor);
 return g;
 }
}

struct DrawContext
{
 struct
 {
 GLuint tess;
 } prog;
 struct
 {
 GLuint vaoNull;
 } common;
};

static void error_callback(int error, const char* description)
{
 std::cerr << "GLFW gets error(" << error << ")." << std::endl << description;
 throw "GLFW error.";
}

static void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods)
{
 if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
 {
 glfwSetWindowShouldClose(window, GL_TRUE);
 }
}

static void init(DrawContext &context)
{
 // add current diretory to env
 {
 std::stringstream ss;
 ss << getenv("PATH") << ":" << SHADER_DIR;
 setenv("PATH", ss.str().c_str(), 1);
 }
 // create shaders
 {
 auto srcVS = loadFile("VertexShader.glsl");
 auto srcFS = loadFile("FragmentShader.glsl");
 context.prog.tess = glCreateProgram();
 auto vs = loadShader(srcVS.get(), GL_VERTEX_SHADER);
 auto fs = loadShader(srcFS.get(), GL_FRAGMENT_SHADER);
 glAttachShader(context.prog.tess, *vs.get());
 glAttachShader(context.prog.tess, *fs.get());
 glLinkProgram(context.prog.tess);

 GLint linked;
 glGetProgramiv(context.prog.tess, GL_LINK_STATUS, &linked);
 if(linked == GL_FALSE)
 {
 GLsizei len;
 GLchar log[1024];

 glGetProgramInfoLog(context.prog.tess, sizeof(log) / sizeof(*log), &len, log);
 std::cerr << "glLinkProgram() gets error." << std::endl << log;

 throw "Shader link error.";
 }
 }
 // create common
 {
 glGenVertexArrays(1, &context.common.vaoNull);
 }
}

static void paint(DrawContext &cont)
{
 glClearColor(0.0f, 0.0f, 0.5f, 1.0f);
 glClearDepth(1.0f);
 glClearStencil(0);
 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

 //glEnable(GL_CULL_FACE);
 //glCullFace(GL_BACK);

 glUseProgram(cont.prog.tess);
 glBindVertexArray(cont.common.vaoNull);
 glDrawArrays(GL_LINES, 0, 2);

 GLint error = glGetError();
 if(error != GL_NO_ERROR)
 {
 std::cout << "OepnGL gets error(0x" << std::hex << ")." << std::endl;
 }
}

int main(int argc, char* argv[])
{
 glfwSetErrorCallback(error_callback);
 if (!glfwInit())
 {
 return -1;
 }

 glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
 glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);
 glfwWindowHint(GLFW_RED_BITS, 8);
 glfwWindowHint(GLFW_GREEN_BITS, 8);
 glfwWindowHint(GLFW_BLUE_BITS, 8);
 glfwWindowHint(GLFW_ALPHA_BITS, 8);
 glfwWindowHint(GLFW_DEPTH_BITS, 24);
 glfwWindowHint(GLFW_STENCIL_BITS, 0);
 glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
 glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
 glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GL_TRUE);
 GLFWwindow* window = glfwCreateWindow(SCREEN_WIDTH, SCREEN_HEIGHT, "GL Sample", NULL, NULL);
 if (!window)
 {
 glfwTerminate();
 return -1;
 }

 glfwMakeContextCurrent(window);
 glfwSetKeyCallback(window, key_callback);
 glfwSwapInterval(1);

#if USE_GLEW
 auto glewErr = glewInit();
 if (glewErr != GLEW_OK)
 {
 puts((const char*)glewGetErrorString(glewErr));
 return 1;
 }
 // glewInit() occurs glGetError() == GL_INVALID_ENUM
 GLint glErr;
 while((glErr = glGetError()) != GL_NO_ERROR)
 {
 }
#endif

 DrawContext cont;

 init(cont);

 while (!glfwWindowShouldClose(window))
 {
 paint(cont);
 glfwSwapBuffers(window);
 glfwPollEvents();
 }

 glfwDestroyWindow(window);
 glfwTerminate();
 return 0;
}
<pre>

フラグメントシェーダ

</pre>
#version 410 core

layout(location = 0) out vec4 fragColor;

void main()
{
 vec4 color = vec4(0.9, 0.2, 0.1, 1.0);
 fragColor = color;
}
<pre>

頂点シェーダ

</pre>
#version 410 core

void main()
{
 vec4 pos = vec4(0.0, 0.0, 0.0, 1.0);
 if(gl_VertexID == 0)
 {
 pos.xy = vec2(-1.0, 0.0);
 }
 else if(gl_VertexID == 1)
 {
 pos.xy = vec2(1.0, 0.0);
 }
 else if(gl_VertexID == 2)
 {
 pos.xy = vec2(0.0, 1.0);
 }
 gl_Position = pos;
}
<pre>

■謎のシェーダコンパイルエラー

3回に1回程度の確率で、glCompileShader()が失敗する。

   ERROR: 0:25: ‘<’ : syntax error syntax error

シェーダコードのどこにも’<’という文字は存在しない。しかも、コードを少し変えると’<’が’_’とか’Apple_f’とか’cd’とか全く見覚えのない文字になる。そもそもsyntax errorが2回出てくる時点で挙動が怪しい。ドライバのバグとしか思えないが、検索しても情報が全く出てこないので困っている。

GL_ARB_get_program_binaryも非対応のようなので、事前コンパイルで逃げることもできない。HLSLは素晴らしかった。

GLEWを使うとなぜかエラーの頻度が上がるので、ON/OFFできるようにした。それでもコンパイルが通らないときは、シェーダ末尾に適当な改行を入れると大抵動く。それでも動かないときは、Intelドライバへの信仰心が足らないか、GeForceかRadeonへ乗り換える気力が足らない。



Maxwell (GM107) の命令スループット

$
0
0

CUDAのドキュメントが更新されて、GM107ことGeForce GTX 750 / GeForce GTX 750Tiの命令スループットが追加されていたのでまとめました。

【重要】情報の正確性は保障しません。

■命令スループット

CUDA C Programming Guide, Low Level Optimizations for AMD GCN, A look at instruction issue rates in graphics contexts, etc

1コアが1cycleあたりに演算できる回数の逆数を示しています(小さいほど高速)。

表がうまく描けないので図にしました。PDF版はこちら

201405_1_GM107_1

注釈:(**)GeForceは”below 20 instructions”との記述あり、おそらく100cycle前後。(***)意味なし。ごめんなさい。

F32超越関数のスループットも調べたので記載しておきます。

201405_1_GM107_2

■感想

FermiからKepler(GK104)のときは、F32以外のスループットが極端にごっそりと落とされてしまいました。GK110ことGeForce GTX Titanでは、シフト演算のみ2倍速くなりました。

Keplerでは1SMXあたり192 CUDA Coreです。推測ですが、内部的には32 CUDA Coreが6セットになっており、遅い命令は6セットのうち1セットでしか動作できず、スループット6の命令が多かったのだと思われます。

Maxwellでは、全体的にF32以外のスループットが改善しています。1SMMあたり128 CUDA Coreで、NVIDIAの図を見て分かるように32 CUDA Coreが4セットになっているので、最低スループットが4になりました。この恩恵を最も受けた命令は、シフト演算32bit同士の型変換一部のビット操作です。超越関数も入れていいかもしれません。特にシフトはスループット2なので、初代Fermi並の速さを取り戻したことになります。

一方で、Keplerでのスループット8/7の命令のほとんどがスループット2まで遅くなりました。SAD、比較、最大・最小が該当します。また、8/16bitから32bitへの変換は6倍遅くなり、整数乗算ビットスキャン(BSF/BSR)、16bit SADはソフトウェアエミュレーションになってしまいました。特に痛いのは整数乗算でしょう。ドキュメントには「複数命令」としか書かれておらず、どんな実装になっているか全くわかりません。Compute Shaderでは整数乗算が頻発することも多いので、整数乗算重視のアルゴリズムの場合、シフト演算をうまく使うなど再設計する必要があるかもしれません。

64bit演算はKepler以上に捨てています。スループット128なので、これ以上できないほどに性能を切り捨てた命令になっています。

■まとめ

MaxwellではKeplerで必要以上に切り捨てられた性能を取り戻したように見えます。整数演算の速度には要注意です。


C++11のconstexprで遊ぶ

$
0
0

関数呼び出しの結果をコンパイル時に計算し、定数として扱えてしまうconstexprで遊びました。

■constexprの利点

テンプレートメタプログラミングのような難しいテクニックが不要で人間にやさしく、コンパイル速度が速い

■例:数字で表現された文字列の総和


constexpr unsigned int sumNumberStep(const char *p, int index, int len)
{
return index >= len ? 0 : sumNumberStep(p, index + 1, len) + (*(p + index) - '0');
}

constexpr unsigned int sumNumber(const char *p, int len)
{
return sumNumberStep(p, 0, len);
}

// sumNumber("123", strlen("123")); ===> 6

C++11のconstexprはステートメントが書けません(C++14では許可されるらしい)。
つまり、ifやforやwhiteを使えません。

代わりにどうするかというと、ifやswitchは三項演算子、forやwhileは再帰関数呼び出しに置き換えます。
上の例では、カウンタindexが文字列の長さlenに達するまで再帰させています。

ほかにも、変数宣言、仮想関数、new/delete等は禁止だそうです。

■例:CRC32

文字列からCRC32した値をコンパイル時に計算します。
yoshida_eth0さんのブログにあるcrc関数をconstexprに対応させます。


constexpr unsigned int crc32constStep(unsigned int crc, const char *p, int index, int len)
{
return index >= len ? crc : crc32constStep(((crc >> 8) & 0x00FFFFFF) ^ crc32tab[(crc ^ (*(p + index))) & 0xFF], p, index + 1, len);
}

constexpr unsigned int crc32const(const char *p, int len)
{
return crc32constStep(0xFFFFFFFF, p, 0, len) ^ 0xFFFFFFFF;
}

// crc32const("123", strlen("123")); ===> 0x884863d2

crc32()のループを再帰関数化しただけでconstexpr化できます。

■おまけ:VisualStudioでconstexpr

Visual C++コンパイラはconstexprに対応していません。
2015年発売予定のVisual Studio “14″からの対応です。

Clangやgccは対応済みですが、Windowsの場合Eclipseなど開発環境の移行が必要で、ハードルが高いです。

調べてみると、Clang for WindowsというVisualStudioのプラットフォーム ツールセットにLLVMが使える素晴らしいツールが配布されていました。
今回はClang 3.4.1のClang for WindowsをViualStudio 2013で使いました。
残念ながらiostreamなどのC++ヘッダをincludeするとコンパイルエラーになってしまいましたが、stdio.hは使えました。


【日本語訳】Direct3D 12 概要 Part3:リソースバインド

$
0
0

Intelのブログに”Direct3D 12 Overview Part 3: Resource Binding“という記事が上がっています。

Direct3D 12に関してはMicrosoftがPowerPointと動画を公開していますが、文章による詳細な解説はほとんど見られません。このブログの記事はDirect3D 12の前情報としては貴重なものなので、日本語訳に挑戦しました。

が、筆者の英語力が致命的に低いため、ほとんど意味不明です……雰囲気だけ読み取ってもらえれば幸いです。修正歓迎。

なお、図は元の記事を参照してください。また、Part1/2については図を見れば大体内容が理解できるので、訳しません。

—–

Part2では、我々はPSO(Pipeline State Object)とハードウェアミスマッチのオーバーヘッドを削減することの利益について議論しました。
これから我々はリソースのバインドとD3D 12チームがこの分野でCPUオーバーヘッドを削減する計画をどう立てたかに移る。
この議論をするために、我々はD3D 11で使われたリソースバインディングモデルを迅速に批評する必要がある。
下図は、左側がD3D 12のPSOによるレンダーコンテクストの図で、右側がD3D 11のリソースバインディングモデルの図である。

(Render Context: Pipeline State Object (PSO))

各シェーダの右側を見ると、明示的なバインドポイントが見られる。
明示的なバインドモデルは、パイプライン内の各ステージが参照できる明確なリソースを持っている。
それらのバインドポイントはGPUメモリの中のリソースを次々に参照する。
それらはテクスチャになれ、レンダーターゲット、バッファ、UAV等になれる。
リソースバインディングはずっと前から存在していたが、実際はD3Dより前に来る。
このアイデアは、シーンの裏で複数のプロパティを取り扱うこととゲームが効率的に描画コマンドをサブミットするのを助けることである。
しかし、システムは3つの鍵の領域で多くのバインドの精査をすることが必要になる。
それらの領域と、D3DチームがD3D 12をどう最適化したかを見ていこう。

リソースハザード:

ハザードは通常、遷移(トランシジョン)である。レンダーターゲットからテクスチャへの移動のように。
仮にゲームがあるフレームを描画していて、シーンを取り巻く環境マップになる予定である。
ゲームが環境マップを描画し終えてそれをテクスチャとして使いたい。
この仕事の間、何かがレンダーターゲットとテクスチャのどちらかとしてバインドされた時、ランタイムとドライバの両方が追跡している。
もしそれらが永久に何かへ(訳注:レンダーターゲットとテクスチャの)両方にバインドされているのが見られるなら、最も古い設定がアンバインドされ、最も新しいものを尊重するだろう。
この方法はゲームが必要なだけスイッチでき、またソフトウェアスタックがシーンの裏でそのスイッチを管理する。
加えて、ドライバはレンダーターゲットをテクスチャとして使うためにGPUパイプラインをフラッシュしなければならない。
そうでなければ、GPUでピクセルを読む前に(訳注:描画結果を)リタイアさせなければ、一貫した状態を手に入れられない。
本質的に、ハザードは一貫したデータを保証するためにGPUに余計な仕事を要求する。
これは一例にすぎず、もちろん沢山の考えられる例が存在する。
簡単のため、我々はただこの例を使う。

D3D 12での他の機能と強化と同様に、ここの解決法はゲームにより多くの制御を与えることである。
なぜAPIとドライバが全てのこの仕事をしたりフレーム内の1つのポイントがあるときに追跡したりすべきなのか?
我々は、1つのリソースから他のリソースへ切り替えるところのおおよそ1秒間の60番目について話している。
リソースの遷移をゲームが作りたいとき、制御をゲームに返すことにより、全てのあのオーバーヘッドが削減され、コストは一度だけ払われ払われなければならない。
下にD3D 12で追加された実際のリソースバリアAPIを示す。

D3D12_RESOURCE_BARRIER_DESC Desc;
Desc.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
Desc.Transition.pResource   = pRTTexture;
Desc.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
Desc.Transition.StateBefore = D3D12_RESOURCE_USAGE_RENDER_TARGET;
Desc.Transition.StateAfter  = D3D12_RESOURCE_USAGE_PIXEL_SHADER_RESOURCE;
pContext->ResourceBarrier( 1, &Desc );

このAPIはいくぶん率直で、リソースを宣言してそのソースとターゲットとして使う宣言をする。
遷移についてランタイムとドライバの問い合わせの呼び出しが続く。
たくさんの条件付きロジックで描画するフレーム間をまたいて追跡されるものの代わりに、それは明示的なものとなる。
いくぶんかのゲームは既に知っている。
すべての追加のロジックを引き出すことの追加の利益で。
フレームあたり1回、あるいは遷移するのが必要な任意の周期でそれをする。

リソースの生存時間:

D3D 11(とそれ以前のバージョン)は呼び出しがキューされるように振る舞う。
呼び出しが作られたとき、ゲームはAPIが即座にその呼び出しを実行すると信じている。
しかし、それは違う。
全てが遅延され、GPUにより後になって実行されるコマンドのキューがある。
GPUとCPUの間でより良い並列性と効率性を出せるが、たくさんリファレンスカウントしたり追跡したりする。
すべてのこのカウントと追跡はCPUの労力が必要である。

これを直すため、ゲームがリソースの生存時間について明示的な制御を得る。
D3D 12はキューされたGPUの性質をもはや隠しておらず、ゲームはそれがあとで実装されるであろうコマンドのリストをサブミットすることを知る。
GPUの進捗を追跡するためにFence APIが追加された。
ゲームは(多分1フレームに1回)与えられたポイントでチェックでき、どのリソースがもう必要ないか見ることができ、そしてそのメモリを解放することができる。
代わりに、解放するための追加のロジックを使い続ける限りリソースを追跡し、メモリが不要になったときに解放する。

リソース常在管理:

GPUは大量のビデオメモリを使うことを好み、実際よく手に入る。
これはメモリのある程度を持つ外付けGPUでより問題になる。
そこで我々はコマンドの流れとしてページイン・ページアウトするもののリソース常在管理を持つ。
それが本当に単なるメモリ管理のときは、ゲームにとってそれは無制限のメモリがあるように見える。
もう一度、これはリファレンスカウントと追跡のコストに来る。

リソースの生存時間と同様に、ゲームはリソースの常在を超えて厳密な制御を得る。
D3D 11はOSのなかで常在カウントとコントロールフローを追跡する。
一般にゲームはリソースの集合を指す描画コマンドのひとつながりをすでに知っている。
D3D 12では、メモリにそれらを移動するためにゲームがOSへ明示的に問い合わせることができる。
後ほどコマンドが実行されたときは、ゲームがメモリから取り除かれたリソースを持つことができる。

状態反映:

1度上の3つのエリアが最適化されたら、(小さなパフォーマンス増加ではあるが)より効率を生み出せる4つ目の箇所が姿を現す。
バインドポイントが設定されたとき、ランタイムはゲームがパイプラインに何がバインドされているか見つけ出すためにあとでGetを呼び出せるのを追跡する。
バインドポイントは反映されるかコピーされる。
ミドルウェアのために物事を簡単にする機能が設計された。
だから、コンポーネント化されたソフトウェアは描画コンテクストの現在の状態の外を知ることができる。
一度リソースのバインドが最適化されたら、反映された状態のコピーはもう不要である。
だから、前の3つのエリアから取り除かれたフロー制御に加えて、状態の反映のためのGetも同様に取り除かれる。

これは改善に当てはまり、D3D 12のリソースのバインドの中で効率的である。
たくさんの撹乳が取り除かれている。
前もって必要な制御フローや追跡のロジックを必要とするリソースのバインドや、ランタイムとドライバでのリソースのsetとget。
すべてのそれはゲームに制御を返すことに味方して取り除かれている。
GPUのキューされた性質を隠すつもりはD3Dにはもはやない。
リソース管理の全てのそれらの粒度はゲームの中でそのように発生し、ゲーム開発者は次々に必要と思うだろう。

—–

ここまで


Direct3D 12

HLSLで浮動小数点数のアトミック演算

$
0
0

Direct3D 11のシェーダでバッファや共有メモリに対してアトミック演算を行うことがありますが、HLSLのAtomic演算(Interlocked関数)はuintかint型を対象としていて、floatには対応していません。今回は、無理やりInterlocked関数を使って浮動小数点数の比較を行ってみました。

■方法

実は、浮動小数のビットの並びをそのまま整数とみなして比較することができます[参考文献1]。HLSLではasuint() / asint()関数で浮動小数のビット列のまま整数に変換できます。

ただし、負の小数との比較をする場合は二の補数をとる必要があります。また、非正規化数や非数は誤った比較結果を引き起こします。

■検証

適当に乱数を振って0.0から100.0までの浮動小数点数を作り、2つの配列に格納し、アトミック演算で小さい方の値を求める(HLSLのInterlockedMin()に相当する)プログラムをC++ AMPで書きました(C++ AMPのコードはコンパイラによってCompute Shaderに変換されます)。

CPUでも同じ計算を行い、誤りがあれば異常終了しますが、こちらの環境では数回実行して全て正常終了しました。

ソースコードはこちら

(こちらの環境では、しばしばコンパイラがメモリ不足でビルドに失敗する現象が起きました。何回かビルドし直してみてください。)

本当にアトミック演算が実行されているのかを確認するため、Shader Assemblyを確認しました。


unsigned int iv = *reinterpret_cast<unsigned int*>(&v);
0x000000EC iadd [precise(y)] r0.y, r0.x, cb1[7].x
0x0000010C ishl [precise(y)] r0.y, r0.y, l(2)
0x00000128 ld_raw_indexable [precise(y)](raw_buffer)(mixed,mixed,mixed,mixed) r0.y, r0.y, u0.xxxx
concurrency::atomic_fetch_min(&viewResult(threadIdx), iv);
0x0000014C iadd [precise(x)] r0.x, r0.x, cb1[16].x
0x0000016C ishl [precise(x)] r0.x, r0.x, l(2)
0x00000188 atomic_umin u1, r0.x, r0.y
0x000001A4 endif
0x000001A8 ret

atomic_uminという命令があることから、確かにアトミック命令が使われていることが分かります。ただしDebugビルドではCPUエミュレーション実行になるので注意してください。

ちなみにatomic_fetch_min()をatomic_fetch_max()に置き換えると、HLSLのInterlockedMax()に相当する処理が行えます。もちろんこの場合も正常終了することを確認しています。

asfloat()をC++ AMPで表現するには、reinterpret_castすれば良いようです。HLSLにはポインタの概念がないのに上手く裁けるC++ AMPすごい。

■余談

アトミック加算に限れば、CUDAはCompute Capability 1.1以降でatomicAdd(float)をサポートし、OpenGLもGL_NV_shader_atomic_float拡張でimageAtomicAdd(float)をサポートしています。HLSLは非サポートです。

■参考文献

  1. How to optimize for the Pentium family of the microprocessors (In Japanese)

 


頂点バッファを使わず全画面に描画する頂点シェーダ

$
0
0

いつも忘れるのでメモしておきます。

バリエーションは今後増える予定。

■GLSL ES 3.0

#version 300 es
in int gl_VertexID;
void main() {
int x = (gl_VertexID & 1) * -2 + 1;
int y = (gl_VertexID & 2) * -1 + 1;
gl_Position = vec4(x, y, 0, 1);
}
#version 300 es
in int gl_VertexID;
out vec2 vs_texcoord;
void main() {
int x = (gl_VertexID & 1) != 0 ? 1.0 : 0.0;
int y = (gl_VertexID & 2) != 0 ? 1.0 : 0.0;
gl_Position = vec4(vec2(x, y) * 2.0 - 1.0, 0, 1);
vs_texcoord = vec2(x, y);
}

TODO


IDF14で公開されていたGDC2014からのDierct3D 12のアップデート

$
0
0

Intelが2014年9月に行ったIntel Developer Summit 14で、Direct3D 12に関する情報がアップデートされていたことが分かりました。

ソース:IDF2014の資料ダウンロードページ

以前、筆者はGDC2014の資料を基にSlideshareでDirect3D 12についてまとめた資料を公開しましたが、ほとんどはその資料と同じ内容です。

しかし1点だけ、リソースの設定機構については、基本的な設計から見直されており、全くの別物になっているようです。
資料に”Need more flexible parameterization”と書かれていることから、GDC2014の時点からIDF2014までの間に設計変更があったことが伺えます。

■おさらい – GDC2014の時点でのリソースの設定

GDC2014時点でのリソースの設定機構(OpenGLのように「バインド」と呼ぶ)は、巨大なDescriptorHeapをVRAMから確保し、Descriptor単位で切り出してバインド先を記録し、それをDescriptorTableで参照することでシェーダに紐づける、という仕組みでした。

図では、シェーダステージ間でDescriptorを共有できるような描かれ方をしていましたが、誰得な気がします。

■すべてのリソースにViewの概念がつく

これもほぼおさらいですが、より厳密な表記がされているので、先にまとめておきます。

Direct3D 11ではSRV(ShaderResourceView)、UAV(UnorderedAccessView)、RTV(RenderTargetView)、DSV(DepthStenilView)というViewが存在します。
いずれもcreateBuffer()やcreateTexture()で作ったリソースをシェーダでどう扱うか、その型と範囲を決定するために使われていました。

Direc3D 12ではVRAM(Direct3D 12用語ではHeap?GDC2014では「アロケータチャンク」という表現も見られる)をアプリケーションが自由に扱えるためか、すべてのHeapにViewの概念が追加されています。

CBV(ConstantBufferView)
IBV(IndexBufferView)
VBV(VertexBufferView)
SOV(StreamOutputView)

CBVについては、Direct3D 11.1で似た概念が追加されています。
しかし、それはSetConstantBuffers1()で設定の度に範囲を指定するというもので、Viewが存在したわけではありませんでした。
Direct3D 12では、Descriptorに設定するリソースは(インデックスバッファのようにシェーダステージで明示的に使うことがないものであっても!)すべてViewを通してアクセスする仕組みになります。

ただし、SamplerだけはDX11と変わらないようです。

■Root Signature & Root Parameter

GDC2014からの最大の変更点は間違いなくこれです。
“Arrayed indexing of multiple descriptors and constants not allowed”とあることから、GDC2014のリソース設定機構はボツになったと見て間違いなさそうです。

Direct3D 12の新しいバインドの仕組みでは、頻繁に更新されるパラメータにGPUのレジスタやリネーミングパスが効率よく動作できるようにするため、シグネチャパラメータという2つの概念が追加されました。

Root Signatureは、Pipeline(全てのシェーダステージをまとめたもの、いわゆるPipeline State Object(PSO))と対になっていて、Pipelineの型に合わせてアプリケーションが作る必要があるものです。
構造は以下のようになっています。

RootSignature
├Descriptor Tables
├Descriptors
└Constants

RootSignatureに設定するDesciptorsやConstantsの型宣言を”Root Descriptors”や”Root Constants”と呼ぶようですが、資料の中ではやや曖昧です。

RootSignatureに設定する値をRootParameterと呼びます。

コマンドバッファの中でこのRoot Signatureを変更されると、Pipeline StateでのParameterの再利用効率が落ちるため、変更は最小限にしなさい(”Minimize signature changes”)、と書かれています。
RootSignatureをどう設計するかが、Direct3D 12アプリケーションのパフォーマンスに大きな影響を与えるようです。

なんとなくOpenGLのUniform Buffer Objectの設計に似ている気がします。

■Descriptor Tables & Descriptors & Constants

Descriptor Tablesと、Descriptors・Constantsの2つは、明確に用途が異なります。

Descriptorsは、Viewが設定できます。
つまり、SRV/CBV/UAV/Samplerを設定するために使います。

Constantsは即値をそのまま設定します。
FLOATとUINTとSINTが設定できます。
HLSL registerのb0-b15に直接アサインされるようです。
1DWORDだけ設定できて何がうれしいのか疑問に思いますが、資料の中に次のようなコードがあることから推測できるかもしれません。

cbuffer DrawConstants
{
    UINT ConstantBufferOffset;
} : register(b0)

一方、DescriptorTablesは、DescriptorかConstantsへの参照を持ちます。
GDC2014の資料とほぼ同じ役割です。
ただし、”Table can identify mix of CBV/SRV/UAVs”という表記から、1つのDescriptorの中にSamplerこの混在はできないようです。

idf2014_direct3d12_32

(Microsoft Direct3D 12: New API Details and  Intel Optimizations, p.32)

idf2014_direct3d12_34

(Microsoft Direct3D 12: New API Details and  Intel Optimizations, p.34)

上の図を見ると、「Descriptor Tablesなくてもリソースバインドできるじゃん」と思います。
実際それも許されると思います。

しかし、Root Constant / Root Descriptorが増えれば、描画のたびに設定するDescriptorも大きくなりCPUの転送時間かGPUのストール時間かのどちらかが増えるのでしょう。
「静的なリソースはDescriptor Tableを使え」「巨大なDescriptor Tablesを作って可能な限りDrawCall間で共有せよ」と書かれています。
また、「動的に更新される定数バッファは、GPUのシェーダ遅延を省くためにRoot Constantsを使い、Root Constantsを使い切ったらRoot Descriptorを使え」とも書かれています。

メッシュのようにImmutableなものはDescriptorTableで、DynamicなものはConsntant / Descriptorで、というのが基本戦略のようです。
これはCommandListsとBundlesの関係に似ているのでしょう。

このように、DX12のリソースバインドはDX11のSet***()と違って非常に柔軟な(ただしユーザが手間をかける必要がある分面倒くさい)設計になっています。

■RootSignatureの容量制限

実は、Root Desciptor Tableを活用せざるを得ない理由がもう一つあります。

Root Signatureには「16DWORDまで」という非常に厳しい制限があります。
また、Root Parameterが消費する容量も厳密に定義されています。
Root Descriptor Tableは1DWORD、Root Descriptorは4DWORD、Root Constantは1DWORD*必要な数、となっています。

つまり、Root DescriptorはRoot Signatureに最大4つしか入りません
定数バッファの場合でもfloat4x4を1つ使った時点でRoot Signatureを使い切ります

この時点で、Root Desciptor Tableに頼るのはマストであるといえます。

■Descriptor Tables /Descriptors / Constantsの設定

Root Signatureに対応したDescriptorたちは、CPUから書き込め、シェーダからアクセスできるHeapに用意します。
このHeapへの書き込みは、GPUが使用中でなければ(たとえコマンドバッファへのDrawCall等の記録が終わった後でも)可能な一方、その管理はアプリケーションの責任となっています。
描画中に書き込んだら、たぶんデバイスリムーブが待っているのでしょう。
そのため、「アプリケーションがDesciptorをバージョン管理しなさい」と書かれています。

面倒くさい話です。

なお、シェーダから参照されないIBV / VBV / SOV / RTV / DSVについては、CPUから任意に読み込むことも許可されています。
RTVに適当な演算結果を書いて、StagingバッファなしでいきなりMap()して値を取ってこれるのだと思います。
とはいえCompute Shaderで使うUAVや、SRV / UAVもCPU Write Onlyなので、たいていのケースではStagingバッファのようなものを自分で作ることになるでしょう。

■サンプルコード

PDF資料の中から抜粋しました。
一部表記を省略しています。

1. RootSignatureの作成

struct D3D12_ROOT_PARAMETER {
    D3D12_ROOT_PARAMETER_TYPE ParameterType;
    union {
        D3D12_ROOT_DESCRIPTOR_TABLE DescriptorTable;
        D3D12_ROOT_CONSTANTS        Constants;
        D3D12_ROOT_DESCRIPTOR       Descriptor;
    }
    ...
}

D3D12_DESCRIPTOR_RANGE DescriptorRange[2];
D3D12_ROOT_PARAMETER   Params[4];
ID3D12RootSignature* pRootSignature;

DescriptorRange[0].Init(D3D12_DESCRIPTOR_RANGE_SAMPLER, 2, 0);
DescriptorRange[1].Init(D3D12_DESCRIPTOR_RANGE_SRV, 5, 2);

Params[0].InitAsDescriptorTable(1, &DescriptorRange[0]);
Params[1].InitAsDescriptorTable(1, &DescriptorRange[1]);
Params[2].InitAsConstantBufferView(7);
Params[3].InitAsConstants(1, 0);

pDevice->CreateRootSignature(Params, ARRAYSIZE(Params), &pRootSignature);

D3D12_DESCRIPTOR_RANGEがDescriptorTableの中身の定義だと思います。
RootSignatureのスロット0に複数のSamplerを指すDescriptorTableを、スロット1に複数のSRVを指すDescriptorTableを、スロット2にCBVを、スロット3に即値を渡しているようです。

この書き方だと、DescriptorTableの中身がSRV/UAV/CBVのいずれかでなければならないように見えるので、第2引数と第3引数に何かありそうですが、これだけではよく分かりません。

2. Pipeline State Objectの作成

D3D12_GRAPHICS_PIPELINE_STATE_DESC PipelineDesc;
ID3D12PipelineState*               pPipelineState;

pDevice->CreteGraphicsPipelineState(pRootSignature, &PipelineDesc, &pPipelineState);

RootSignatureがPipelineStateObjectより上位の概念であることが分かります。

3. Root Signatureとパラメータの設定

pCommandList-&gt;SetGraphicsRootSignature(pSignature);

pCommandList-&gt;SetGraphicsDescriptorTable(0, SamplerTableHandle);
pCommandList-&gt;SetGraphicsDescriptorTable(1, TextureTableHandle);
pCommandList-&gt;SetGraphicsRootConstantBufferView(2, CBVHandle);
pCommandList-&gt;SetGraphicsRoot32bitConstant(3, ConstValue, 0);

1.のParamsとの対になっているのが分かります。

CommandListについては先のSlideshareを参考にしてください。ここでは説明を省きます。

4. ドローコール

pCommandList->SetPipelineState(FirstPipelineState);
pCommandList->SetGraphicsRoot32bitConstant(0, FirstCBVOffset, 0);
pCommandList->Draw(...);

pCommandList->SetGraphicsRoot32bitConstant(0, NextCBOffset, 0);
pCommandList->Draw(...);

pCommandList->SetPipelineState(LastPipelineState);
pCommandList->SetGraphicsRoot32bitConstant(0, LastCBVOffset, 0);
pCommandList->Draw(...);

ConstantBufferOffsetの伏線がここで回収されます。
Root ConstantでCBVをまるでポインタへのアクセスのように使っています。
おそらく結果はDrawInstanced()と同じようなものになるでしょう。
ただしこちらは、Graphics Pipelineの変更もはさむことができます。

■Direct3D 11ハードウェアとの互換性

Direct3D 11.2のTiled Resourceにあったのと同様に、リソースバインドにもTierが存在します。

Tier1 : Direct3D 11と同じ制限、Heapが2^16、Descriptor Tableが5つまで
Tier2 : Heapが2^20、SRVとSamplerが無制限、PSOあたりのUAVが64まで
Tier3 : Heapが2^20以上、すべてのリソースが無制限

DX12はDX11ハードウェアでも動作できるのでTierで区切られてしまうのは少々残念ですが、Descriptor Tablesが無制限に使えるわけではない点には注意が必要そうです。

なお、Intel GPUの場合、”Feature Level 12″は”Next Generation Intel Core processors”と”Next Gen 14nm Intel processor for Tables”で使えるそうです。
おそらく、Skylake / Braswellで対応し、Haswell / Broadwell / Bay Trail / Cherry TrailはTier1なのでしょう。

■まとめ

Root Signatureに関するDirect3D 12の新しいリソースバインドについて説明しました。

ちなみに明日からGDC2015が始まり、そこでDirect3D 12とOpenGL Nextに関する新しい情報が公開されるらしいので、必見です。
ここでRoot Signatureが跡形もなく消えていたら…泣きます。



Direct3D 12の実験コード

$
0
0

DirectX 12を使ったプログラムをGithubで公開しています

公式のサンプルがないので手探りですが、何かの参考にしてください。

DirectXのチュートリアルに似た感じで、プロジェクトごとにDirect3D 12の各機能にスポットを当てています。

解説記事は近いうちに書く予定です。


Direct3D 12を始める – Command

$
0
0

Windows 10 Technical Preview向けのWindows SDKには、最新のDirectXのライブラリが同梱されていますその中でもまだ情報が少ないDirect3D 12に焦点を当て、筆者が調べたことをまとめて記事にしていきます。

現時点でSDKはプレビュー版であり、リリース版では仕様変更の可能性があります。
書きかけにつき今後図を追加するかもしれません。


■コンテクストからコマンドへ

Direct3D 11では、DeviceContextにリソースビューとステートオブジェクトを複数設定し、DrawCallをサブミットしていました
Direct3D 12では、CommandListに複数のデスクリプタ、1つのRootSignature、1つのPSO(パイプラインステートオブジェクト)を設定し、DrawCallし、それを必要な回数繰り返してからCommandQueueにサブミットします。

ここでは、DeviceContextに代わるD3D12のコマンドについて掘り下げます。


■Command List

DeviceContextに代わり、GPUに命令を送るときの情報を蓄える仕組みがCommand Listです。

Command Listの役割はDeviceContextとほぼ同じです。
D3D11では、Deviceがスレッドセーフ、DeivceContextが非スレッドセーフなAPIとして設計されていました。
D3D12でも、Deviceはスレッドセーフ、CommandListが非スレッドセーフなAPIとなっています。

DeviceContextにはいくつかの問題がありました。

D3D11のDeviceContextは、Immediate ContextとDeferred Contextの2種類がありました。
Immediate Contextは1つしか存在せず、コマンド構築をマルチスレッドで行うことが不可能でした。
GPUの性能はまだ伸び続けていますが、CPUのシングルスレッド性能は伸び悩んでいるため、GPUを空転させる可能性が高くなってきています。
また、Deferred Contextのサブミットには必ずImmediate Contextを介する必要があり、Context間で発生するリソースの依存性解決がドライバに大きな負荷を掛けていました。

実際、ドライバがDeferred Contextを真にサポートしたのは、NVIDIAだけでした。
ただし、使用にはドライバの特性に気を付ける必要がありました。(参考文献1)
IntelやAMDの場合、Deferred Contextを使う利点がありませんでした。

一方、Command Listは、スレッド間の依存性がありません。
Command List間でやり取りするAPIはなく、GPUのステートはCommand Listの開始前にリセットされます。
Command ListをCPUのスレッド数用意し、並列にコマンド構築することで、マルチコアの恩恵を最大限受けることができます。

Command ListはID3D12Device::CreateCommandList()から作成します

Command Listはレコード状態とクローズ状態があります。
作成直後はレコード状態で、ID3D12GraphicsCommandList::Close()を呼び出すとクローズ状態となり、ID3D12GraphicsCommandList::Reset()を呼び出すとレコード状態に戻ります。
クローズ状態では、一切のコマンドを呼び出すことができません。


■Comnand Queue

GPUに描画を開始させるには、グラフィックドライバからGPUにコマンドを転送する必要があります。
しかし、DrawCallのたびにGPUと通信するのはCPUコストが高くなるため、最近のグラフィックスAPIは複数のDrawCallをまとめて一括でコマンドをサブミットするのが一般的です。

D3D11では、このサブミットは暗黙的に行われていました。
D3D12では、サブミットを明示的に行います(ただしハードウェアキューと直接対応しているかはおそらく実装依存)。

Command QueueはID3D12Device::CreateCommandQueue()から作成します
MSDNにあるサンプルコードではID3D12Device::GetDefaultCommandQueue()から作成している例がありますが、現在のWindows 10 SDKでは廃止されているようです。

Command ListをCommand Queueにサブミットするには、クローズ状態にする必要があります。
クローズ状態のCommand Listは、ID3D12CommandQueue::ExecuteCommandLists()でサブミットできます。

CommandList_CommandQueue
Command ListとCommand Queueの関係

Queueは”Direct Queue”と”Compute Queue”と”Copy Queue”の3種類があります。
Direct Queueは、すべてのCommand Listをサブミットできます。MSDNドキュメントでは”Graphics Queue”や”3D Queue”と書かれることがあります。
Compute QueueはCompute ShaderとCopy系コマンドのみを実行でき、Copy QueueはCopy系コマンドのみを実行できるQueueですが、現在のWindows 10 SDKでは未実装でした。

いずれのQueueも作成時にタイプを指定します。
Command Listも作成時にタイプを指定します。
たとえば、DirectタイプのCommand ListをDirectタイプのCommand Queueにサブミットすることができますが、ComputeタイプのCommand Queueにサブミットすることはできません。

Command Queueの消化はサブミットされた順に行われます。[1]
Command QueueがあるCommand Listを実行しているとき、別のCommand Queueは割り込めません。
ただし、異なるQueueにCommand Listをサブミットした場合、順番は保障されません。
それだけでなく、並列で実行される可能性もあります。
GCN世代のRadeonにはACE(Asynchronous Compute Engine)が2つ或は8つ搭載されており、複数のComputeタスクが並列実行されます。
描画タスクは並列実行されません。

Queueには優先度をつけることができます。
ハードウェアが並列実行できるタスク数を上回るサブミットが行われた場合、優先度の高いQueueから実行されます。

また、キューにTDR(シェーダの無限ループ等によりGPUがハングアップしたことを検出してドライバを再起動させるWindowsの仕組み)を無効にさせるフラグがあり、シェーダデバッグ時のステップ実行などに活用できるようです。
おそらく製品で使ってはいけないと思います。


■Command Allocator

Command Listがハードウェアネイティブな描画コマンドに変換され、グラフィックスメモリ上に確保される領域を確保するインターフェイスがCommand Allocatorです。

Command AllocatorはID3D12Device::CreateCommandAllocator()から作成します

Command Listの作成時、またはReset()によりレコード状態に戻すときは、必ず1つのCommand Allocatorとバインドする必要があります。
Command ListをClose()したとき、バインドされたCommand Listに描画コマンドが変換され書き込まれます。

Command AllocatorはID3D12CommandAllocator::Reset()でクリアすることができます。
Command Allocatorがどれだけのグラフィックスメモリを消費したかを知る術はありませんが、全く同じ描画コマンドの記録とReset()を繰り返した場合、2回目以降のメモリの消費量が変動しないことが保証されています。

Command AllocatorのReset()は、必ず、すべてのバインドされたCommand Listがクローズ状態になっていなければなりません。
また、今までバインドされたすべてのCommand ListがGPUでの実行を終えるまで、Reset()を呼び出してはいけません。
この順序を守らないと、デバイスのリムーブが発生し、継続動作できなくなります。

GPUの実行状態を検知するためにFenceという仕組みが用意されていますが、ややこしい話なのでここでは説明しません。

Command ListをReset()するとき、バインドするCommand Allocatorを前のCommand Allocatorと別のものに変えることができます。[2]
このとき、指定するCommand Allocatorは作成直後かReset()済みでなければなりません。
逆に、作成直後かReset()済みのCommand Allocatorならば、前にバインドしたCommand AllocatorがまだGPUで使用中であっても、直ちにReset()を呼んでも問題なく動作します。

また、バインドされたCommand Allocatorは、同時に2つ以上のCommand Listをレコード状態にすることができません。[3]

つまり、最大のパフォーマンスを発揮するには、Command Listをスレッドの数以上、Command Allocatorをスレッドの数×2以上作る必要があります。

ただとりあえず動かすだけであれば、Command ListのClose、Command ListをCommand Queueへサブミット、Fenceで同期待ち、Command AllocatorのReset、Command ListのReset、といった手順を踏めばよいようです。


■まとめ

Command List、Command Queue、Command Allocatorを紹介しました。

■ソース

  1. Any thread may submit a command list to any command queue at any time, and the runtime will automatically serialize submission of the command list in the command queue while preserving the submission order.  [URL]
  2. A typical pattern is to submit a command list and then immediately reset it to reuse the allocated memory for another command list. [URL]
  3. Note that only one command list associated with each command allocator may be in a recording state at one time. [URL]

■参考文献

  1. D3D11 Deferred Contexts Primer & Best Practices (Bryan Dudash, GDC2013)

Direct3D 12を始める – Fence (1)

$
0
0

Windows 10 Technical Preview向けのWindows SDKには、最新のDirectXのライブラリが同梱されています。
その中でもまだ情報が少ないDirect3D 12に焦点を当て、筆者が調べたことをまとめて記事にしていきます。

前回はCommand Queue、Command List、Command Allocatorについて説明しました。
今回はコマンドを管理する上で重要となるフェンスについて説明します。

現時点でSDKはプレビュー版であり、リリース版では仕様変更の可能性があります。
書きかけにつき今後図を追加するかもしれません。


■フェンスとは

Fenceは、Command QueueにサブミットしたCommand Listの完了を検知するために使います。

ID3D12Device::CreateFence()で作成します。

前回の記事で、

Command AllocatorのReset()は、…今までバインドされたすべてのCommand ListがGPUでの実行を終えるまで、Reset()を呼び出してはいけません。 この順序を守らないと、…継続動作できなくなります。

と説明しましたが、なぜ正しく動作できないかを詳しく説明します。

Command Allocatorに溜め込まれたDrawCallは、GPUの大部分を占めるコア(ALU)を制御する専用プロセッサ(NVIDIA GeForceのGiga Thread Engine、AMD RadeonのCommand Processor、Intel HD GraphicsのCommand Streamer)が理解できる命令に変換され、グラフィックスメモリに転送され、そのあとプロセッサをキックしてコマンドが消化されていきます。
Command AllocatorをReset()すると、このグラフィックスメモリを先頭領域まで巻き戻し、コマンドのサブミットによって上書きしていきます。
つまり、コマンドが完全に消化される前にReset()してしまうと、プロセッサが実行中の命令を上書きし、何が起きるか分からなくなります。

そこで、Command AllocatorのReset()を呼び出す前にFenceを使って同期を取り、サブミットしたコマンドが完了した後にリセットします。


■Fenceの仕組み

Fenceをうまく扱うには、FenceとCommand Queueがお互いにどんな振る舞いをしているかを知る必要があります。

Fenceは内部でUINT64のカウンタを持っています。
デフォルト値は0です。
このカウンタの値はID3D12Fence::GetCompletedValue()で取得できます。
また、ID3D12Fence::Signal()で設定できます。

ただ、FenceのカウンタをCPUから操作しても、あまり使い道がありません。
大抵の場合は、Command Queueと、CreateEvent()で作成したHANDLEと連携して使います。

ID3D12CommandQueue::Signal()を呼び出すと、以前にサブミットされたコマンドがGPU上での実行を完了している、あるいは完了し終わったとき、自動的に第1引数で指定したFenceの値を第2引数の値に更新します。
ドキュメントでは、この値を”fence value”と呼んでいます。
“fence value”は、原則として前の値より大きい値を指定します。
シンプルなアプリケーションでは、1から順に増えていくフレーム番号を指定すれば良いでしょう。
(ドキュメントにはfence valueを1ずつ増やすID3D12CommandQueue::AdvanceFence()というAPIが紹介されていましたが、VS2015 RCの登場とともに消滅しました。)

この値の更新をCPUが検出するには、2つの方法があります。
イベントオブジェクトを使う方法と、ポーリングする方法です。

ID3D12Fence::SetEventOnCompletion()を使うと、fence valueが第1引数の値になったとき、第2引数のイベントオブジェクトをシグナル状態に遷移させます。
イベントオブジェクトはCreateEvent()で作成します。初期状態は非シグナル状態にしておきましょう。
あとは、通常のスレッドプログラミング同様、WaitForSingleObject()でスレッドを寝かせれば、GPUの実行完了後に叩き起こしてくれます。

一方、ID3D12Fence::GetCompletedValue()の値が変化するまでビジーループを回せば、これと同等の結果が得られます。
ですが、格好悪いのでお勧めしません。

別のケースとして、Command Queueが別のCommand Queueの実行完了を待ちたい場合を考えます。
例えば、Compute Command Queueで物理演算を実行し、Graphics Command Queueでその結果を使うような場合です。
先の例と違い、GPU内で完結するケースなので、CPUと同期を取る必要がありません。
この場合、ID3D12CommandQueue::Wait()を呼び出します。
引数はSignal()で渡したものと同じ値を使います。
すると、通常は実行順序が保証されないCommand Queue同士の実行順を保証させることができます。
(なお、リソース間の同期を取る仕組みとしてBarrierもありますが、これはQueueを跨いだ同期には使えません。)

試しにWait()で自分のCommand Queueの完了を待たせた場合も正しく動作しているように見えましたが、Microsoftが想定する実装ではないと思うので、止めておいたほうが無難です。


■遅延の考慮

ほぼすべてのGPUは、CPUからのコマンドのサブミットと、GPUでのコマンドの消化が、並列して実行できるようになっています。
サブミット直後にFence同期を入れてしまうと、並列実行が阻害されてしまいます。
格ゲーや音ゲーのように、遅延が1フレームも許されないゲームではこれで問題ないかもしれません。
しかし、最近の映画品質のFPSゲーやオープンフィールドアクションゲームでは、CPUもGPUもフル稼働させないと処理が追いつきません。
そこで、最初に1/60秒かけてCPUからコマンドをサブミットし、次の1/60秒かけてGPUでコマンドを消化します。
Direct3D 12でこのスタイルを実現するには、Command AllocatorをN個作り、1フレームずつ順番に使い、各フレームの先頭でCommand AllocatorをReset()します。

また、Windowsの場合、サブミットされたコマンドの実行は3フレーム遅延されます。
(この挙動についてはNVIDIAの中の人がいろいろ調べて記事にしています。)
IDXGIDevice1::SetMaximumFrameLatency()で変更することができますが、残念ながら現在のSDKではID3D12DeviceからIDXGIDevice1をQueryInterfaceすることができませんでした。

GPUでのコマンド消化が3フレーム遅延する可能性があるということは、Command Allocatorの解放も3フレーム遅延させた方が、フレームレートが不安定な時のカクつきを低減できます。
また、AMDのAlternative Frame Randering (AFR)のように、マルチGPU環境で描画が複数フレームを跨がって実行する場合、最低でも2フレーム遅延しなければマルチGPUの効果がなくなってしまいます。


■サンプルコード

とりあえず動かす方法です。CPUでコマンドをサブミットし、GPUの処理が完了するまで待ちます。

//===== 初期化 =====//
void Init()
{
	D3D12CreateDevice(...);
	mDev->CreateCommandAllocator(...);
	mDev->CreateCommandQueue(...);
	mDev->CreateCommandList(...);
	mDev->CreateFence(...);
	mFenceEveneHandle = CreateEvent(...);
	mFrameCount = 0ull;
}

//===== 描画 =====//
void Draw()
{
	mFrameCount++;
	/* ここでmCmdListへコマンドを記録 */
	mCmdList->Close();
	mCmdQueue->ExecuteCommandLists(...);
	mCmdQueue->Signal(mFence, mFrameCount);
	mFence->SetEventOnCompletion(mFrameCount, mFenceEveneHandle);
	WaitForSingleObject(mFenceEveneHandle, INFINITE);
	mCmdAlloc->Reset();
	mCmdList->Reset(mCmdAlloc, nullptr);
}

次に、CPUがコマンドをサブミットした後、GPUの完了を待たずに、次のフレームの描画準備を開始する方法です。

//===== 初期化 =====//
void Init()
{
	D3D12CreateDevice(...);
	for (int i = 0; i < MaxFrameLatency; i++) {
		mDev->CreateCommandAllocator(...); // mCmdAlloc[MaxFrameLatency]に作成
	}
	mDev->CreateCommandQueue(...);
	mDev->CreateCommandList(...);
	mCmdList->Close();
	mDev->CreateFence(...);
	mFenceEveneHandle = CreateEvent(...);
	mFrameCount = 0ull;
}

//===== 描画 =====//
void Draw()
{
	mFrameCount++;
	const int cmdIndex = mFrameCount % MaxFrameLatency;
	// 前のCommandAllocatorを使いまわすとき
	if (mFrameCount > MaxFrameLatency) {
		// CommandAllocatorを使用したCommandQueueに対し、実行完了時にFenceへ通知するよう命令する
		mFence->SetEventOnCompletion(mFrameCount - MaxFrameLatency, mFenceEveneHandle);
		WaitForSingleObject(mFenceEveneHandle, INFINITE);
		mCmdAlloc[cmdIndex]->Reset();
	}
	mCmdList->Reset();
	/* ここでmCmdListへコマンドを記録 */
	mCmdList->Close();
	mCmdQueue->ExecuteCommandLists(...);
	mCmdQueue->Signal(mFence, mFrameCount);
}

CommandAllocatorを許容する遅延フレーム分だけ用意しているところが、最大の変更点です。
あとは、フレームの終端で同期待ちしていたところを、次のフレームの開始時に行うように変更しています。

CommandListは作成時にCommandAllocatorを要求し、自動的にコマンド記録開始状態になってしまうので、作成直後にCloseしています。
何かカッコ悪いので、早くMicrosoftのサンプル実装を見てみたいところです。

ちなみに、ID3D12Fence::SetEventOnCompletion()の引数に指定する整数は、1以上でなければならないようです。
0を指定すると、描画コマンドをサブミットしていなくてもFenceに対して実行完了であることを通知してしまい、同期として機能しませんでした。
(その挙動を知らずにGithubにコードをコミットしたところ、海外の方から指摘を頂きました。
 仮想PCではそれでも動いていましたが、Radeon搭載PCで動かしたところ、デバッグレイヤーからエラーが通知されました。)

■まとめ

CommandQueueとCPUの間で同期を取る方法について紹介しました。

なお、この説明のために実装したソースコードはgithubにコミットしてあります
MeshサンプルとParallelFrameサンプルを参考にしてください。


Direct3D 12を始める – Pipeline State

$
0
0

Windows 10 Technical Preview向けのWindows SDKには、最新のDirectXのライブラリが同梱されています。
その中でもまだ情報が少ないDirect3D 12に焦点を当て、筆者が調べたことをまとめて記事にしていきます。

前回はFenceについて説明しました。
今回はPipelineStateついて説明します。


パイプラインステートとは

Direct3D 11の入力ストリーム、シェーダ、ステート等をすべてひとつのオブジェクトのまとめたものです。
つまり、Direct3D 12には、頂点シェーダやピクセルシェーダの単独設定や、InputLayoutやRasterizer/Blend/DepthStencilStateの設定のAPIはありません。
すべて、Pipeline Stateの作り直しになります。

Direct3D 12の公開当初は、Pipeline State Object (PSO)と呼ばれていました。
このアイデアの初出はAMDのMantleで、Monolithic Pipelineと表現されていました。


パイプラインステートが必要な理由

最近のGPUでは、シェーダとステートのハードウェア実装とAPIに差が出ています。
例えば、入力レイアウトを変更すると、頂点シェーダ入力との辻褄合わせのためにパッチ処理が行われます。
ブレンドステートやレンダーターゲットの数が変わると、ピクセルシェーダ出力との辻褄合わせのためにパッチ処理が行われます。
ハルシェーダを変更すると、頂点シェーダ出力とドメインシェーダ入力との辻褄合わせのためにパッチ処理が行われます。
ステート等の確定はドローコールまで遅延されるので、変更時に予めパッチ処理しておくことはできません。
このように、ドライバはAPIから見えないところで多くのコストを支払っていて、これがCPU負荷を高める原因になっています。

パイプラインステートは、このようなドライバコストを削減することができます。


パイプラインステートの作成に必要な情報

パイプラインには”Graphics Pipeline”と”Compute Pipeline”の2種類があります。
ComputePipelineは簡単で、基本的にコンパイルしたComputeShaderのバイナリ(とRootSignatureというもの)があれば作成できます。

面倒なのはGraphicsPipelineです。

typedef struct D3D12_GRAPHICS_PIPELINE_STATE_DESC {
  ID3D12RootSignature                *pRootSignature;
  D3D12_SHADER_BYTECODE              VS;
  D3D12_SHADER_BYTECODE              PS;
  D3D12_SHADER_BYTECODE              DS;
  D3D12_SHADER_BYTECODE              HS;
  D3D12_SHADER_BYTECODE              GS;
  D3D12_STREAM_OUTPUT_DESC           StreamOutput;
  D3D12_BLEND_DESC                   BlendState;
  UINT                               SampleMask;
  D3D12_RASTERIZER_DESC              RasterizerState;
  D3D12_DEPTH_STENCIL_DESC           DepthStencilState;
  D3D12_INPUT_LAYOUT_DESC            InputLayout;
  D3D12_INDEX_BUFFER_STRIP_CUT_VALUE IBStripCutValue;
  D3D12_PRIMITIVE_TOPOLOGY_TYPE      PrimitiveTopologyType;
  UINT                               NumRenderTargets;
  DXGI_FORMAT                        RTVFormats[8];
  DXGI_FORMAT                        DSVFormat;
  DXGI_SAMPLE_DESC                   SampleDesc;
  UINT                               NodeMask;
  D3D12_CACHED_PIPELINE_STATE        CachedPSO;
  D3D12_PIPELINE_STATE_FLAGS         Flags;
} D3D12_GRAPHICS_PIPELINE_STATE_DESC;

5種類のシェーダ、3種類のステート、入力レイアウトに加え、トポロジの種類、レンダーターゲットの種類と数、深度バッファのフォーマット、MSAA、プリミティブリスタートインデックスの設定が必要です。
これだけでDirect3D 11からの移植が面倒くさそうな感じがしてきます。
Direct3D 11を前提に設計されているゲームエンジンでは、これらのパラメータのハッシュを求めて、一致すればキャッシュから取得する戦略を取ることでしょう。
ただし、シェーダバイトコードや入力レイアウトはポインタなので、単にデスクリプタ全体をハッシュ関数に投げるだけでは不十分なので気を付けてください。


パイプラインステートのシリアライズ

Graphics Pipelineにほんのちょっとの変更を加えるだけでPipelineStateを作り直すことになるので、PipelineStateが消費するバイナリサイズは肥大化する可能性があります。
PipelineStateの作成は非常に遅いので、あまり頻繁に作成・破棄することもできません。

そこで有用なのが、Direct3D 12にはPipelineをシリアライズしてID3DBlobで受け取る機能です。
ID3D12PipelineState::GetCachedBlob()で取得できます。
このバイナリをメインメモリやHDDにキャッシュしておくことで、VRAMの浪費を避けることができます。

シリアライズ化したバイナリは、D3D12_GRAPHICS_PIPELINE_STATE_DESCのCachedPSOに指定することで再利用されます。
DESCの中身は同一で、CachedPSOを追加で指定します。
…のはずなのですが、現状ではエラーになってしまいました。
原因は分かりません。

あと気になるのは、GPUの付け替えやドライバのアップデートを行った後にキャッシュが使えるかどうかです。
誰か試してください。

参考までに、DICEのMantle対応での運用方法は以下のようになっているそうです。
・PipelineStateのバイナリサイズは100MB以上
・必要に応じてHDDにキャッシュする
・アプリケーション終了後もバイナリを保持する
・起動時にドライバのメーカー名とバージョンを取得するAPIを使い、変更があればバイナリを破棄し再生性する


まとめ

PipelineStateについて説明しました。
ソースコードはGithubにコミットする予定です。


Windows 10でDirect3Dのデバッグランタイムをインストールする方法

$
0
0

Windows 10でD3D11_CREATE_DEVICE_DEBUGフラグを指定してD3D11CreateDevice()を呼び出したところ、HRESULT 0x887a002dを返して失敗しました。

ログにはこんなメッセージが出ていました。

D3D11CreateDevice: Flags (0x2) were specified which require the D3D11 SDK Layers for Windows 10, but they are not present on the system.
These flags must be removed, or the Windows 10 SDK must be installed.
Flags include: D3D11_CREATE_DEVICE_DEBUG

“Graphics Tools”というのをインストールすれば良いらしいので、手順をメモしておきます。


  1. Windowsボタン -> 設定
  2. システム
  3. アプリと機能
  4. オプション機能の管理
    graphics_tools_1
  5. 機能の追加
    graphics_tools_2
  6. グラフィックスツール -> インストール
    graphics_tools_3

これでデバッグ版ランタイムを指定してデバイスを作成することができます。


.NET Standardで旧来のWindows向け.NET DLLを使う

$
0
0

.NET Standardで、VisualStudio 2015以前で作成した.NET FrameworkのDLLを使う方法です。

が、なぜかdotnet runできないので、ビルドするところまでです。実行はできません

■まえおき

.NET Coreでは、標準ライブラリがBCL (Base Class Library)から整理し直したCore Library、別名CoreFXに変わりました。
BCLとCoreFXは、APIは同じですが、参照するライブラリが違うため、そのまま流用することはできません。
例えば、System.ObjectはBCLではmscorlib.dll、CoreFXではSystem.Runtime.dllを参照するため、C#からHello Worldすることすらできません。
一方、.NET Standardが登場し、将来的にBCLとCoreFXはPCL (Portable Class Library)に統合されることになったため、この問題は将来的に解決される見込みです。
しかし将来ではなく、今、この問題を解決するための方法も用意されているので、ここに記録しておきます。

■プロファイルの互換性

Microsoftのドキュメントに一覧が用意されています。
例えば、.NET Framework 4.5.1は.NET Standard 1.2と互換性があります。
.NET Framework 4.5未満と互換性のある.NET Standradはありません。
現在のところ.NET Frameworkと.NET Coreは互換性がありませんが、PCL 2.0から統合される予定だそうです。

■プロジェクトの編集

ここでは.NET Core SDK 1.0.1 Preview 3を使います。
リンクが切れたときはここから探してください
現在最新のSDKは1.1ですが、プロジェクトファイルがcsprojではなく廃止予定のproject.jsonにしか対応しておらず、project.jsonでNuGetに登録されていないライブラリをNuGetパッケージマネージャーに登録せずに参照する方法が分からなかった(どうやら出来ないらしい?)ので避けています。
project.jsonの場合の編集方法は英語のMSDNブログに書かれています。

“dotnet new”すると生成されるcsprojに、以下のような要素が含まれていると思います。

<TargetFramework>netcoreapp1.0</TargetFramework>

<ItemGroup>
<PackageReference Include=”Microsoft.NETCore.App”>
<Version>1.0.1</Version>
</PackageReference>
<PackageReference Include=”Microsoft.NET.Sdk”>
<Version>1.0.0-alpha-20161104-2</Version>
<PrivateAssets>All</PrivateAssets>
</PackageReference>
</ItemGroup>

TargetFrameworkを互換性のある.NET Standardプロファイル(netcoreapp1.0など)に変更します。
そして、PackageRefernceのうち.NET Core用である”Microsoft.NETCore.App”を削除して、次のように置き換えます。

<PackageReference Include=”Microsoft.NETCore.Portable.Compatibility”>
<Version>1.0.*</Version>
</PackageReference>
<PackageReference Include=”NETStandard.Library”>
<Version>1.6.*</Version>
</PackageReference>

保存してから、”dotnet restore”で参照するDLLをNuGetからインストールします。

そして、.NET Frameworkで作られたDLLを参照に追加します。

<Reference Include=”HogeLib”>
<HintPath>../HogeLib/bin/Debug/HogeLib.dll</HintPath>
</Reference>

“dotnet build”すると、ビルドできるはずです。

■実行

なぜかできません。
“dotnet run”すると”Unable to run your project.”と言われます。
exe直叩きでも実行できませんでした。
方法が分かれば追記します。


Unityで深度バッファからカメラZを取得

$
0
0

忘れないようにメモ。
Unity 2017.1


float LinearEyeDepth(float rawdepth)
{
  float x, y, z, w;
#if UNITY_REVERSED_Z
  x = -1.0 + _NearClip / _FarClip;
  y = 1;
  z = x / _NearClip;
  w = 1 / _NearClip;
#else
  x = 1.0 - _NearClip / _FarClip;
  y = _NearClip / _FarClip;
  z = x / _NearClip;
  w = y / _NearClip;
#endif

  return 1.0 / (z * rawdepth + w);
}

float d = _CameraDepthTexture.Load(int3(xy, 0));
float camera_z = LinearEyeDepth(d);

_NearClipと_FarClipの値は、CameraコンポーネントのnearClipPlaneとfarClipPlaneプロパティをそのまま渡せばよい。


Triangle Culling

$
0
0

Triangle Cullingを実装してみました。

アイデア

出典はOptimizing the Graphics Pipeline with Compute [Graham16]です。
Radeon GPU(PS4/XboxOne世代)のプリミティブ処理能力が低いから、余っているALU性能を活用してプリミティブをカリングし、高速化しようぜという話が書かれています。

今回は、スライドの中盤に書かれているTriangle Cullingのうち、Backface Culling, Frustum Culling, Small Primitive Cullingの3つを実装しました。

ソースコードはこちらあとでアップする

目的

今回はDirect3D 11を使い、ベンダー拡張を含む高速化のアイデア(ballot, MBCNT, Ordered Count, Multi draw indirect, Asynchronous Computeなど)はすべて無視し、カリングができること自体に重きを置きます。

3つのカリング自体は容易に実装できます。
また、プロファイラーにかけ、ラスタライザのカウンターを確認することで、正しくカリングができていることを証明できます。
高速化の効果は測定しません。

実装

面倒なのは、このカリングを仕込むほうです。
カリング結果を格納するIndex BufferとIndirect Draw Argsを新しく用意し、Compute Shaderから適切に出力する必要があります。
後述しますが他にも面倒な点がいくつかあり、アイデアのシンプルさに反して必要なステップ数が多いです。
折衷案として、Geometry Shaderでカリングする方法[Iwasaki18]が提案されています。
残念ながら資料は非公開ですが、このアイデアは容易に実装できます。
双方の利点・欠点を表にしました。

シェーダステージ CS GS
容易か? No(Dispatch Callが増える、レンダリングパイプラインの変更が必要) Yes(シェーダステージが増える、シグネチャ受け渡しが必要)
追加のメモリ 必要 不要
Draw call 増えない(Multi drawの使用が必須) 増えない
非同期コンピュートへのオフロード Yes No
頂点再利用の効率 減る(Radeonならば256単位の分割数であれば影響がない) ?(Geometry Shaderステージがあっても効くか?)
テセレーターとの併用 Yes(Factorに応じた事前仕分け・分割も可能) Yes
拡張性 Yes No

評価はしていませんが、前者はAsynchronous Computeが十分高速な環境を要求し、後者はGeometry Shaderのスループットが十分高速な環境を要求しているように思えます。

今回は、最初にGeometry Shaderを使って実装し、後でCompute Shaderを使って同じ結果が得られるものを実装します。

実装

Backface Culling

行列式を使うと三角形の面積を求めることができ、表向きなら正、裏向きなら負、縮退ポリゴンなら0になることを利用しているようです。
どういうわけか、判定条件の正負を逆にしないと正しくカリングされませんでした。
理由は分かっていません。

float3x3 m = { input[0].position.xyw, input[1].position.xyw, input[2].position.xyw };
float d = determinant(m);
if (-d ≦ 0.0) {
	return;
}

実装して気づきましたが、浮動小数点数の誤差があり、ゼロ面積を期待するところで+0.6e7等になってしまい、カリングされないケースがありました。
0.0以外の閾値も検討した方がよいかもしれません。

表か裏かを判定するのならば、外積を使って面法線を求め、射影空間でのZ方向との内積を利用すれば同様に判定できるのではないかと考えました。
試したところ、問題なく動作しました。
座標系は射影空間でもカメラ空間でも問題ないようです(表・裏の関係は変わらない)。

float3 n = cross(positions[2] - positions[0], positions[1] - positions[0]);
float d = dot(float3(0, 0, 1), n);
if (d ≦ 0.0) {
	return;
}

Frustum Culling

ここから先は正規化デバイス座標系(NDC)で計算するので、w除算が必要です。
上に書いたコードを流用します。

Direct3Dでは、NDCでの視錐台の範囲は(-1, -1, 0)から(+1, +1, +1)なので、全ての頂点がこの範囲外ならばカリングできます。

float3 pmin = min(positions[0], min(positions[1], positions[2]));
float3 pmax = max(positions[0], max(positions[1], positions[2]));
if (any(pmax.xy < -1) || any(pmin.xy > 1) || pmax.z < 0 || pmin.z > 1) {
	return;
}

資料では、xyのみチェックしていて、zはチェックしていません。
確かにzがFar Clipを超える状況は限定的でしょうが、Near Clipを超える状況は割とある気がする(部屋や洞窟の壁とか)ので、入れてあげても良いのではないでしょうか。

Small Primitive Culling

NDCでxy座標が[-0.5, +0.5)に収まっていれば、ラスタライザのサンプリングポイントにヒットせず、何も描かれないので、カリングすることができます。
Viewportの大きさに依存するため、定数バッファ等を使ってシェーダの外から情報を与えてあげる必要があります。

float2 vmin = pmin.xy * ViewportScale.xy + ViewportScale.zw;
float2 vmax = pmax.xy * ViewportScale.xy + ViewportScale.zw;
if (any(round(vmin) == round(vmax))) {
	return;
}

シンプルすぎて直感で理解しにくいコードですが、資料の図を見ると簡単に理解できます。
私は図を見たとき目からウロコでした。
これを見て実装しようと思い立ったほどです。
ただし、資料ではfalse negativeが発生するケースが存在することを説明しています。

MSAAを使う場合、特にcentroid指定をしない通常のMSAAでは注意が必要です。
サンプリングポイントが0.5固定ではなくなるため、false positiveが発生します。
資料では、保守的にピクセル中心からサブピクセル位置の差を判定条件として使うよう紹介していますが、これではfalse negativeが大量発生しそうに思えます。
2xと4xに限れば、ピクセル中心を軸としてサブピクセルを回転させれば、xy軸に沿って等間隔に配置させることができるので、厳密に判定できそうですが、明らかに面倒そうなのでパスします。

Geometry Shaderでの実装

Direct3D 11では、シェーダとそのConstant Bufferの設定の追加のみで対応できます。
(正確にはVertex ShaderのSV_Positionセマンティクスを別名に変える必要もあります。)
シグネチャを一致させて値を受け渡すコードを用意するのが面倒ですが、既存のコードを破壊せず、レンダリングパイプラインの変更も不要なので、手軽です。

Compute Shaderでの実装

Compute ShaderではIndex BufferとVertex Bufferを自分で取得し、カリングを通り抜けた三角形のみを新しいIndex Bufferへ書き出します。
プリミティブ速度の改善という当初の目標を達成するため、新しいIndex Bufferはカリングを通り抜けた三角形が隙間なく詰まっている状態にする「コンパクション」が必要があります。
縮退ポリゴン扱いにしてしまっては意味がありません。
今回は、1スレッドが1三角形(3インデックス)を担当するよう実装します。
資料では、コンパクションを256三角形(768インデックス)単位で行っていますが、今回はデバッグの都合で128単位にしました。

頂点の座標値が必要になるので、Vertex Bufferのフォーマットが定義されていなければなりません。
Compute ShaderにInput Layoutという便利なものはないので、自分で与えてやる必要があります。
普通は先頭12byte(offset 0、float3)が座標値なので、今回はその仮定をもとにstrideだけ与えました。
資料では、頂点レイアウトをAoSからSoAに変更しているので、これならoffsetもstrideも自明ですが、アイデアの本質ではないのでAoSのまま使います。

さらに、Draw Indirect Argsも用意します。
これは、コンパクションによりインデックス数が可変になるため、DrawIndexed()ではなくDrawIndexedInstancedIndirect()を使う必要があるからです。
インデックス数はコンパクションの途中で勝手に求まるので、Indirect ArgsのInstance Countを1に固定することを忘れなければ、atomic演算1回の追加で実装できます。
ただし、今回は資料に沿ってより最適なコードを実装します。

groupthread uint triangleCounter;
// ...
if (groupThreadID == 0)
{
	triangleCounter = 0;
}
sync();
// ...
if (triangle is survived) {
	InterlockedAdd(triangleCounter, 1, myThreadID); // 本質的にはこのコードだけで十分
}
sync();
// ...
if (groupThreadID == 0)
{
	IndirectArgs.Store(20 * groupID, triangleCounter * 3);
}

Geometry Shaderでは(既に完了しているため)不要だったWVP変換が必要になります。
2度WVP変換を行うことになり、とても微妙です。
もしスキニングを行っていれば、これも2回必要になり、非常に微妙です。

2byte Index Bufferの扱いは面倒です。
HLSLでは4byte単位でないByteAddressBufferへのオフセットは利用できないので、工夫が必要です。
読み込みの場合、8byteまとめて読んだ後に2byte×3へ分解します。
書き込みの場合、groupsharedメモリにキャッシュし、1スレッドが2スレッド分のデータを出力します(2×2byte×3=12byte)。
お隣のスレッドとのデータ交換なので、資料のDepth Tile Cullingの項で説明している最適化がここでも適用できそうですが、Direct3D 11では使えないので見送ります。

uint indices[3];
uint2 temp;
temp = IndexBuffer.Load2((dtid * 6) & ~3);
indices[0] = (groupThreadID & 1) ? (temp.x ≫ 16) : (temp.x & 0xFFFF);
indices[1] = (groupThreadID & 1) ? (temp.y & 0xFFFF) : (temp.x ≫ 16);
indices[2] = (groupThreadID & 1) ? (temp.y ≫ 16) : (temp.y & 0xFFFF);

float3 vertices[3];
vertices[0] = asfloat(VertexBuffer.Load3(indices[0] * VertexStride));
vertices[1] = asfloat(VertexBuffer.Load3(indices[1] * VertexStride));
vertices[2] = asfloat(VertexBuffer.Load3(indices[2] * VertexStride));
groupshread uint3 indexBufferData[NUM_THREADS];
if (half threads) {
	uint3 index0 = indexBufferData[groupThreadID * 2];
	uint3 index1 = indexBufferData[groupThreadID * 2 + 1];

	uint3 indexPacked;
	indexPacked.x = index0.x | (index0.y ≪ 16);
	indexPacked.y = index0.z | (index1.x ≪ 16);
	indexPacked.z = index1.y | (index1.z ≪ 16);
	IndexBufferCulled.Store3(12 * groupThreadID + 6 * NUM_THREADS * groupID, indexPacked);
}

ちなみに、生き残った三角形が奇数個となった場合、グループの最後のスレッドがカリングされた三角形の最初のインデックス(index1.x)を参照してしまいますが、Index Countの範囲外になり描画には利用されないので、groupsharedメモリは未初期化のまま参照しても問題ありません。
ただ、ちょっと少し気持ち悪いので今回はgroupsharedメモリを初期化しています。

結果

絵が正しいかどうか確認したうえで、プロファイラーを使ってカリング直後の三角形数と実際にラスタライズされた三角形数を比較します。

Backface Culling

球をモデルとして使うと、深度バッファを使わずかつRasterizer StateのCullModeをNoneにしてレンダリングしたとき、もしBackface Cullingの実装が誤っていると表示がダブるので、テストにはちょうど良いです。
凸方でないモデルを使うとこの検証方法は破綻します。

auto rd = CD3D11_RASTERIZER_DESC(CD3D11_DEFAULT());
rd.CullMode = D3D11_CULL_NONE;
hr = mDevice->CreateRasterizerState(&rd, &mRasterizerState));

カリング適用前
triangle-culling-backface-off
カリング適用後
triangle-culling-backface-on
実装は問題なさそうです。
ちなみに、Renderdocのフレームキャプチャからバッチを確認すると、カリングされて半分穴抜けになったメッシュが確認できます。
特に、Compute Shader版ではモデルがちょっとずつ描かれていく様子が分かるので面白いです。
triangle-culling-backface-capture

では、ラスタライズされた三角形数を確認します。
Queryを使って実装してもよいのですが、面倒なのでRenderdocのプロファイラに数えてもらいます。
GPAなどの詳細な計測ツールを使ってもよいのですが、慣れていないのでRenderdocに頼ります。
ちなみに、Intel HD Graphics 620で動かしています。

計測のためにRasterizer StateのBackface CullingをCULL_BACKに戻します。
通常、1つのメッシュのうち約半数のポリゴンは裏を向いていますから、半分くらいの三角形が消滅するはずです。
なお、上の画像の通りDraw callが3つに分かれているので、プロファイラの結果も3つ出てきます。
脳内で加算してください。

カリング適用前
triangle-culling-backface-prof-offカリング適用後
triangle-culling-backface-prof-on

予想通り、大体半分になりました。
PS Invokationsが全く同じなので、ズルはしていません。
しかしRasterized Primitivesの数字がBackface Cullingの結果を考慮してくれないのは予想外でした。
本当はちゃんとしたプロファイラを使った方が良いです。

Frustum Culling

さすがに上の方法では全て視錐台に入ってしまいテストにならないので、モデルを3倍スケールしてテストしました。
面倒なので比較画像はなしです。

カリング適用前
triangle-culling-frustum-prof-offカリング適用後
triangle-culling-frustum-prof-on

こちらは分かりやすい数字です。
Rasterizer Invocationsの値がVertex Shader(あるいはGeometry Shader)からの出力、Rasterized Primitivesの値がカリングを通過してPixel Shaderに渡った三角形数です。
カリング無効のときは、ハードウェアでカリングされて数が減少しているのがわかります。
一方、カリング有効のときは、ハードウェアでカリングされたものが一つもなく、しかもカリング無効のときと同じ数字なので、期待できる最大の効率が得られています。

3番目のDraw callはtriangle countが0になってしまいました。
このような場合はDraw call自体を止めてしまいたいですが、果たして実装可能なのでしょうか。
NVIDIAの実装では、CPU側で数を指定するので、無理そうです。
AMDの実装では、CPU側で指定するものとBufferから指定するものがあるので、後者を使えば実現できそうです。
Direct3D 12の実装では、Comand Signatureに1つのdraw callを入れておき、ExecuteIndirect()でCounter bufferに数を指定すれば実現できそうです(ただし、Bundleは使えない)。

Small Primitive Culling

画面に極小ポリゴンが生まれる状況にするため、解像度を縦横半分、モデル分割数を約2.5倍にしました。
3倍スケールはしません。
triangle-culling-small-wip

カリング適用前
triangle-culling-small-prof-offカリング適用後
triangle-culling-small-prof-on

カリングはできてはいますが、28しか減りませんでした。
かなり高密度なポリゴンを描画するケースでしか大きな効果はなさそうですね。
そしてDirect3D 11のQueryはここでも役立たないことを知りました。

Trianlge Cullingを実装し、資料に書かれたアルゴリズが問題ないことを確認し、Geometry ShaderとCompute Shaderでのシンプルな実装方法を説明しました。

DirectX12の新しいBarrier

$
0
0

昨日、DirectX Agility SDK 1.700.10で追加された、ResourceBarrier()を刷新する新しいバリアAPI(Enhanced Barrier)が公開されました。
しかしSDKはあるもののドキュメントが見当たりません。
そこで、ヘッダの中身を見ながら考察してみました。

    ID3D12GraphicsCommandList7 : public ID3D12GraphicsCommandList6
    {
    public:
        virtual void STDMETHODCALLTYPE Barrier( 
            UINT32 NumBarrierGroups,
            _In_reads_(NumBarrierGroups)  const D3D12_BARRIER_GROUP *pBarrierGroups) = 0;
        
    };

Barrier()というAPIで指定するみたいです。
構造体を見てみましょう。

typedef struct D3D12_BARRIER_GROUP
    {
    D3D12_BARRIER_TYPE Type;
    UINT32 NumBarriers;
    union 
        {
        _In_reads_(NumBarriers)  const D3D12_GLOBAL_BARRIER *pGlobalBarriers;
        _In_reads_(NumBarriers)  const D3D12_TEXTURE_BARRIER *pTextureBarriers;
        _In_reads_(NumBarriers)  const D3D12_BUFFER_BARRIER *pBufferBarriers;
        _In_reads_(NumBarriers)  const D3D12_RESOURCE_STATE_BARRIER *pStateBarriers;
        } 	;
    } 	D3D12_BARRIER_GROUP;

4種類のバリアタイプGlobal, Texture, Buffer, Resource Stateがあるようです。
同一タイプのバリアは配列で指定できます。
各タイプの構造体を見てみましょう。

typedef struct D3D12_GLOBAL_BARRIER
    {
    D3D12_BARRIER_SYNC SyncBefore;
    D3D12_BARRIER_SYNC SyncAfter;
    D3D12_BARRIER_ACCESS AccessBefore;
    D3D12_BARRIER_ACCESS AccessAfter;
    } 	D3D12_GLOBAL_BARRIER;

まずはGlobal。SyncとAccessをペアとして、従来のResourceBarrier()と同じようにBeforeとAfterを指定するようです。
列挙子を見てみましょう。

typedef 
enum D3D12_BARRIER_SYNC
    {
        D3D12_BARRIER_SYNC_NONE	= 0,
        D3D12_BARRIER_SYNC_ALL	= 0x1,
        D3D12_BARRIER_SYNC_DRAW	= 0x2,
        D3D12_BARRIER_SYNC_INPUT_ASSEMBLER	= 0x4,
        D3D12_BARRIER_SYNC_VERTEX_SHADING	= 0x8,
        D3D12_BARRIER_SYNC_PIXEL_SHADING	= 0x10,
        D3D12_BARRIER_SYNC_DEPTH_STENCIL	= 0x20,
        D3D12_BARRIER_SYNC_RENDER_TARGET	= 0x40,
        D3D12_BARRIER_SYNC_COMPUTE_SHADING	= 0x80,
        D3D12_BARRIER_SYNC_RAYTRACING	= 0x100,
        D3D12_BARRIER_SYNC_COPY	= 0x200,
        D3D12_BARRIER_SYNC_RESOLVE	= 0x400,
        D3D12_BARRIER_SYNC_EXECUTE_INDIRECT	= 0x800,
        D3D12_BARRIER_SYNC_PREDICATION	= 0x800,
        D3D12_BARRIER_SYNC_ALL_SHADING	= 0x1000,
        D3D12_BARRIER_SYNC_NON_PIXEL_SHADING	= 0x2000,
        D3D12_BARRIER_SYNC_EMIT_RAYTRACING_ACCELERATION_STRUCTURE_POSTBUILD_INFO	= 0x4000,
        D3D12_BARRIER_SYNC_VIDEO_DECODE	= 0x100000,
        D3D12_BARRIER_SYNC_VIDEO_PROCESS	= 0x200000,
        D3D12_BARRIER_SYNC_VIDEO_ENCODE	= 0x400000,
        D3D12_BARRIER_SYNC_BUILD_RAYTRACING_ACCELERATION_STRUCTURE	= 0x800000,
        D3D12_BARRIER_SYNC_COPY_RAYTRACING_ACCELERATION_STRUCTURE	= 0x1000000,
        D3D12_BARRIER_SYNC_SPLIT	= 0x80000000
    } 	D3D12_BARRIER_SYNC;

typedef 
enum D3D12_BARRIER_ACCESS
    {
        D3D12_BARRIER_ACCESS_COMMON	= 0,
        D3D12_BARRIER_ACCESS_VERTEX_BUFFER	= 0x1,
        D3D12_BARRIER_ACCESS_CONSTANT_BUFFER	= 0x2,
        D3D12_BARRIER_ACCESS_INDEX_BUFFER	= 0x4,
        D3D12_BARRIER_ACCESS_RENDER_TARGET	= 0x8,
        D3D12_BARRIER_ACCESS_UNORDERED_ACCESS	= 0x10,
        D3D12_BARRIER_ACCESS_DEPTH_STENCIL_WRITE	= 0x20,
        D3D12_BARRIER_ACCESS_DEPTH_STENCIL_READ	= 0x40,
        D3D12_BARRIER_ACCESS_SHADER_RESOURCE	= 0x80,
        D3D12_BARRIER_ACCESS_STREAM_OUTPUT	= 0x100,
        D3D12_BARRIER_ACCESS_INDIRECT_ARGUMENT	= 0x200,
        D3D12_BARRIER_ACCESS_PREDICATION	= 0x200,
        D3D12_BARRIER_ACCESS_COPY_DEST	= 0x400,
        D3D12_BARRIER_ACCESS_COPY_SOURCE	= 0x800,
        D3D12_BARRIER_ACCESS_RESOLVE_DEST	= 0x1000,
        D3D12_BARRIER_ACCESS_RESOLVE_SOURCE	= 0x2000,
        D3D12_BARRIER_ACCESS_RAYTRACING_ACCELERATION_STRUCTURE_READ	= 0x4000,
        D3D12_BARRIER_ACCESS_RAYTRACING_ACCELERATION_STRUCTURE_WRITE	= 0x8000,
        D3D12_BARRIER_ACCESS_SHADING_RATE_SOURCE	= 0x10000,
        D3D12_BARRIER_ACCESS_VIDEO_DECODE_READ	= 0x20000,
        D3D12_BARRIER_ACCESS_VIDEO_DECODE_WRITE	= 0x40000,
        D3D12_BARRIER_ACCESS_VIDEO_PROCESS_READ	= 0x80000,
        D3D12_BARRIER_ACCESS_VIDEO_PROCESS_WRITE	= 0x100000,
        D3D12_BARRIER_ACCESS_VIDEO_ENCODE_READ	= 0x200000,
        D3D12_BARRIER_ACCESS_VIDEO_ENCODE_WRITE	= 0x400000,
        D3D12_BARRIER_ACCESS_NO_ACCESS	= 0x80000000
    } 	D3D12_BARRIER_ACCESS;

ドキュメントがないので推測ですが、Barrier Syncは同期待ちするパイプラインステージ、Barrier Accessはリソースの使用用途を指定するようです。
レガシーBarrierではSRV/UAV/CBVのステージ指定はありませんでした。
シェーダリソース系以外は、SyncとAccessで有効な組み合わせが限られそうで、なんだか冗長に感じます。
また、リソースの指定はないので、レガシーBarrierでいうnull UAVバリアやnull Aliasバリアの代わりなのかなと思います。

次にTextureバリアを見てみましょう。

typedef struct D3D12_TEXTURE_BARRIER
    {
    D3D12_BARRIER_SYNC SyncBefore;
    D3D12_BARRIER_SYNC SyncAfter;
    D3D12_BARRIER_ACCESS AccessBefore;
    D3D12_BARRIER_ACCESS AccessAfter;
    D3D12_BARRIER_LAYOUT LayoutBefore;
    D3D12_BARRIER_LAYOUT LayoutAfter;
    _In_  ID3D12Resource *pResource;
    D3D12_BARRIER_SUBRESOURCE_RANGE Subresources;
    D3D12_TEXTURE_BARRIER_FLAGS Flags;
    } 	D3D12_TEXTURE_BARRIER;

リソースとサブリソースの指定があります。
また、LayoutとTexture Barrier Flagsいう新しい列挙子も増えています。

typedef 
enum D3D12_BARRIER_LAYOUT
    {
        D3D12_BARRIER_LAYOUT_UNDEFINED	= 0xffffffff,
        D3D12_BARRIER_LAYOUT_COMMON	= 0,
        D3D12_BARRIER_LAYOUT_PRESENT	= 0,
        D3D12_BARRIER_LAYOUT_GENERIC_READ	= ( D3D12_BARRIER_LAYOUT_PRESENT + 1 ) ,
        D3D12_BARRIER_LAYOUT_RENDER_TARGET	= ( D3D12_BARRIER_LAYOUT_GENERIC_READ + 1 ) ,
        D3D12_BARRIER_LAYOUT_UNORDERED_ACCESS	= ( D3D12_BARRIER_LAYOUT_RENDER_TARGET + 1 ) ,
        D3D12_BARRIER_LAYOUT_DEPTH_STENCIL_WRITE	= ( D3D12_BARRIER_LAYOUT_UNORDERED_ACCESS + 1 ) ,
        D3D12_BARRIER_LAYOUT_DEPTH_STENCIL_READ	= ( D3D12_BARRIER_LAYOUT_DEPTH_STENCIL_WRITE + 1 ) ,
        D3D12_BARRIER_LAYOUT_SHADER_RESOURCE	= ( D3D12_BARRIER_LAYOUT_DEPTH_STENCIL_READ + 1 ) ,
        D3D12_BARRIER_LAYOUT_COPY_SOURCE	= ( D3D12_BARRIER_LAYOUT_SHADER_RESOURCE + 1 ) ,
        D3D12_BARRIER_LAYOUT_COPY_DEST	= ( D3D12_BARRIER_LAYOUT_COPY_SOURCE + 1 ) ,
        D3D12_BARRIER_LAYOUT_RESOLVE_SOURCE	= ( D3D12_BARRIER_LAYOUT_COPY_DEST + 1 ) ,
        D3D12_BARRIER_LAYOUT_RESOLVE_DEST	= ( D3D12_BARRIER_LAYOUT_RESOLVE_SOURCE + 1 ) ,
        D3D12_BARRIER_LAYOUT_SHADING_RATE_SOURCE	= ( D3D12_BARRIER_LAYOUT_RESOLVE_DEST + 1 ) ,
        D3D12_BARRIER_LAYOUT_VIDEO_DECODE_READ	= ( D3D12_BARRIER_LAYOUT_SHADING_RATE_SOURCE + 1 ) ,
        D3D12_BARRIER_LAYOUT_VIDEO_DECODE_WRITE	= ( D3D12_BARRIER_LAYOUT_VIDEO_DECODE_READ + 1 ) ,
        D3D12_BARRIER_LAYOUT_VIDEO_PROCESS_READ	= ( D3D12_BARRIER_LAYOUT_VIDEO_DECODE_WRITE + 1 ) ,
        D3D12_BARRIER_LAYOUT_VIDEO_PROCESS_WRITE	= ( D3D12_BARRIER_LAYOUT_VIDEO_PROCESS_READ + 1 ) ,
        D3D12_BARRIER_LAYOUT_VIDEO_ENCODE_READ	= ( D3D12_BARRIER_LAYOUT_VIDEO_PROCESS_WRITE + 1 ) ,
        D3D12_BARRIER_LAYOUT_VIDEO_ENCODE_WRITE	= ( D3D12_BARRIER_LAYOUT_VIDEO_ENCODE_READ + 1 ) ,
        D3D12_BARRIER_LAYOUT_DIRECT_QUEUE_COMMON	= ( D3D12_BARRIER_LAYOUT_VIDEO_ENCODE_WRITE + 1 ) ,
        D3D12_BARRIER_LAYOUT_DIRECT_QUEUE_GENERIC_READ	= ( D3D12_BARRIER_LAYOUT_DIRECT_QUEUE_COMMON + 1 ) ,
        D3D12_BARRIER_LAYOUT_DIRECT_QUEUE_UNORDERED_ACCESS	= ( D3D12_BARRIER_LAYOUT_DIRECT_QUEUE_GENERIC_READ + 1 ) ,
        D3D12_BARRIER_LAYOUT_DIRECT_QUEUE_SHADER_RESOURCE	= ( D3D12_BARRIER_LAYOUT_DIRECT_QUEUE_UNORDERED_ACCESS + 1 ) ,
        D3D12_BARRIER_LAYOUT_DIRECT_QUEUE_COPY_SOURCE	= ( D3D12_BARRIER_LAYOUT_DIRECT_QUEUE_SHADER_RESOURCE + 1 ) ,
        D3D12_BARRIER_LAYOUT_DIRECT_QUEUE_COPY_DEST	= ( D3D12_BARRIER_LAYOUT_DIRECT_QUEUE_COPY_SOURCE + 1 ) ,
        D3D12_BARRIER_LAYOUT_COMPUTE_QUEUE_COMMON	= ( D3D12_BARRIER_LAYOUT_DIRECT_QUEUE_COPY_DEST + 1 ) ,
        D3D12_BARRIER_LAYOUT_COMPUTE_QUEUE_GENERIC_READ	= ( D3D12_BARRIER_LAYOUT_COMPUTE_QUEUE_COMMON + 1 ) ,
        D3D12_BARRIER_LAYOUT_COMPUTE_QUEUE_UNORDERED_ACCESS	= ( D3D12_BARRIER_LAYOUT_COMPUTE_QUEUE_GENERIC_READ + 1 ) ,
        D3D12_BARRIER_LAYOUT_COMPUTE_QUEUE_SHADER_RESOURCE	= ( D3D12_BARRIER_LAYOUT_COMPUTE_QUEUE_UNORDERED_ACCESS + 1 ) ,
        D3D12_BARRIER_LAYOUT_COMPUTE_QUEUE_COPY_SOURCE	= ( D3D12_BARRIER_LAYOUT_COMPUTE_QUEUE_SHADER_RESOURCE + 1 ) ,
        D3D12_BARRIER_LAYOUT_COMPUTE_QUEUE_COPY_DEST	= ( D3D12_BARRIER_LAYOUT_COMPUTE_QUEUE_COPY_SOURCE + 1 ) ,
        D3D12_BARRIER_LAYOUT_VIDEO_QUEUE_COMMON	= ( D3D12_BARRIER_LAYOUT_COMPUTE_QUEUE_COPY_DEST + 1 ) 
    } 	D3D12_BARRIER_LAYOUT;

typedef 
enum D3D12_TEXTURE_BARRIER_FLAGS
    {
        D3D12_TEXTURE_BARRIER_FLAG_NONE	= 0,
        D3D12_TEXTURE_BARRIER_FLAG_DISCARD	= 0x1
    } 	D3D12_TEXTURE_BARRIER_FLAGS;

typedef struct D3D12_BARRIER_SUBRESOURCE_RANGE
    {
    UINT IndexOrFirstMipLevel;
    UINT NumMipLevels;
    UINT FirstArraySlice;
    UINT NumArraySlices;
    UINT FirstPlane;
    UINT NumPlanes;
    } 	D3D12_BARRIER_SUBRESOURCE_RANGE;

LayoutがAccessと似ていて何が違うのかよくわかりませんが、どうもVulkanのVkImageMemoryBarrierを手本にしているように見えます。
Layoutはビットマスクではないので、指定できるのは1つだけです。
Readonly DepthとSRVを同時に使う場合はどうしたら良いのでしょうか?

また、明示的にDirect QueueかCompute Queueかを指定できるようになっています。
レガシーBarrierでは、Direct QueueでCopy系のステートに遷移すると、DMA転送を可能にするために高負荷なキャッシュフラッシュが実行されることがありました。
新しいBarrierではその問題を回避できそうです。

新しいBarrierではDiscardフラグを付与できます。
D3D12_RENDER_PASS_BEGINNING_ACCESS_TYPE_DISCARDの代わりでしょうか?
DirectX12に従来からあるRender Passの立ち位置が良く分からなくなってきました。

サブリソースでは範囲を指定できるようになっています。
レガシーBarrierは、1つだけか全てかのどちらかしかなく、例えばミップマップのダウンサンプルを1パスでやろうとすると、ミップマップの数だけBarrierを書く必要がありましたが、これからは1つにまとめられます。

次にBufferを見てみましょう。

typedef struct D3D12_BUFFER_BARRIER
    {
    D3D12_BARRIER_SYNC SyncBefore;
    D3D12_BARRIER_SYNC SyncAfter;
    D3D12_BARRIER_ACCESS AccessBefore;
    D3D12_BARRIER_ACCESS AccessAfter;
    _In_  ID3D12Resource *pResource;
    UINT64 Offset;
    UINT64 Size;
    } 	D3D12_BUFFER_BARRIER;

リソースの指定とオフセット、サイズが増えています。

ついに1つのバッファの部分的な読み書きが可能になりそうです。
レガシーBarrierでは、バリアがサブリソース単位になっているため、バッファは全体で単一のステートにしか遷移できず、同時に読み書きすることはできませんでした。

最後にResource Stateです。

typedef struct D3D12_RESOURCE_STATE_BARRIER
    {
    D3D12_RESOURCE_STATES State;
    _In_  ID3D12Resource *pResource;
    UINT Subresource;
    D3D12_BARRIER_SYNC Sync;
    D3D12_BARRIER_ACCESS Access;
    D3D12_BARRIER_LAYOUT Layout;
    } 	D3D12_RESOURCE_STATE_BARRIER;

他のバリアと違って、BeforeとAfterの指定がありません。
また、D3D12_RESOURCE_STATEはレガシーBarrierの列挙子です。
特殊な用途でしょうか?
いまひとつ目的が分かりません。

新しいBarrierの説明はここまでです。

最後にデバッグ関係です。

    ID3D12Debug6 : public ID3D12Debug5
    {
    public:
        virtual void STDMETHODCALLTYPE SetForceLegacyBarrierValidation( 
            BOOL Enable) = 0;
        
    };

DirectX Developer Blogsによると、レガシーBarrierと新しいBarrierは併用できるので、GPU Based Validationがどちらを使っているか判定できないので、新しいBarrierが使える環境では新しいBarrierを使っているものとしてバリデーションするそうです。
おそらくこのAPIを使うことで、どの環境でもレガシーBarrierが使われるものとしてバリデーションできるのだと思います。

取り急ぎ新しいAPIを見てみました。
何か分かったら追記するかもしれません。

Game Porting Toolkitメモ

$
0
0

私のGitHub上で遊んでいるDirectX 12プログラムを、Macで動かしたくなりました。

今回動かしたのは、このシンプルなRust + DX12プログラムです
MacからVisual Studioプロジェクトをビルドするのは面倒そうなので、GitHub ActionsでArtifactsにアップロードして取り出しました。

実行エラーの類は全くありませんでした。
DX12のニッチな機能を使っていなければ、問題はなさそうです。

デバッグレイヤーは未実装でした(当然)。
なぜかGPUはAMDの振りをするようです。

インストール

同梱のRead me.rtfの通りに行えばインストールできます。
途中、Homebrewのパスが/usr/local/bin/brewになっているかどうか確認するところがありますが、Rosettaを常用しない私としては/opt/homebrew/bin/brewから変更したくありません。
そういう人のための補足も書かれているのがありがたいです。

セットアップ中にLLVMのコンパイルが始まるので、時間がかかります。
Mac Book Airの4コアで巨大リポジトリのビルドは流石に非力です。
しかも1度ビルド中にカーネルパニックしました…

実行

ビルド済みのexeを/Users/[your user name]/my-game-prefix/drive_c/にcopyします。
私のプログラムの場合、実行中にdxcompiler.dllを参照するので、それもプログラムから参照されるディレクトリにcopyします。

Read meによれば、実行時にgameportingtoolkitコマンドをそのまま実行していますが、よくわからないエラーが出ます。

Error: No available formula with the name “game-porting-toolkit”

これが解決できなかったので、別の方法としてwine64を使う方法を試したら起動できました。
こちらのサイトに記載されていたSteam起動用のコマンドのパスを変更しただけです。

MTL_HUD_ENABLED=1 WINEESYNC=1 WINEPREFIX=~/my-game-prefix  /usr/local/Cellar/game-porting-toolkit/1.0.4/bin/wine64 ‘C:\test\exe\simple_triangle.exe’

これで起動できました。

しかし、なぜかc0000135エラーで起動できなくなることがあります。
時間を空けて何度か試すと直るのですが、原因不明です。

デバッグ?

Metal APIのキャプチャをしようと試行錯誤したのですが、今のところ無理でした。
Metalには直接的には用意されていない、DX12のClearRTV/DSVがどう実装されているのか興味があったのですが…。

Xcodeでプロセス(PIDはWine起動時のログで確認できます)にアタッチすると、何度もSIGUSR1に引っかかります。
LLDBに引っかからないようにするには、以下のコマンドを叩けば良いようです。

process handle –stop false SIGUSR1

https://groups.google.com/g/capnproto/c/gXhsjsnKj88

しかし自分のexeがどのスレッドなのかは全然分かりませんでした。

まとめ

Game Porting Toolkitを使ってDX12アプリケーションが動くことを確認しました。

Tang Nano 9KのBSRAMのタイミング

$
0
0

シングルポートBSRAMが仕様書通りの挙動になっているかどうかを、実際に動かして確認する。
今回はRead-before-writeのBypassモードのみ確認している。
(Pipelineモードはどこで使うのだろうか?)

公式のBlock SRAM (BSRAM) の仕様書はこちら

なお、テストにはIPを使ったが、自作してもBSRAMに置き換わることは確認済みである。
仕様書はこちら。

まず、書き込みをenableにしてから読みだされるまで3サイクルことを確認する。
正しく読めたかどうかは、書き込んだ値をLEDで2進数表示することで目視確認する。

module top(
	input wire clk,
	input wire resetn,
	output wire OUT_A,
	output wire OUT_B,
	output wire OUT_C,
	output wire OUT_D,
	output wire OUT_E
);

reg [5:0] counter0;
reg [24:0] counter1;
reg [4:0] counter2;
reg [4:0] counter3;

assign OUT_A = (counter3[0] == 0);
assign OUT_B = (counter3[1] == 0);
assign OUT_C = (counter3[2] == 0);
assign OUT_D = (counter3[3] == 0);
assign OUT_E = (counter3[4] == 0);

wire [7:0] data_read;
reg data_write_en;
reg [7:0] data_write;
reg [9:0] data_addr;

always @(posedge clk) begin
	if (!resetn) begin
		counter1 <= 0;
		counter2 <= 0;
		counter3 <= 0;
		data_addr <= 0;
	end else begin
		counter1 <= counter1 + 1'b1;
		if ((&counter1) == 1) begin
			counter2 <= counter2 + 1'b1;
			data_write_en <= 1;
			data_write <= counter2[4:0];
		end else begin
			data_write_en <= 0;
			data_write <= 0;
			if ((counter1 == 2)) begin // 書き込みは3サイクル後に取得
			counter3 <= data_read[4:0];
			end
		end
	end
end

    Gowin_SP bsram_instance(
        .dout(data_read), //output [7:0] dout
        .clk(clk), //input clk
        .oce(1'b1), //input oce
        .ce(1'b1), //input ce
        .reset(!resetn), //input reset
        .wre(data_write_en), //input wre
        .ad(data_addr), //input [9:0] ad
        .din(data_write) //input [7:0] din
    );
endmodule

counter1が2(counter1が1{25}になった時にwrite enableにして、そこから3サイクル後)以上ならば、LEDが順番に光っていくが、0や1ならば光らないので、仕様書通り3サイクル待つ必要があることが確認できる。

ところで、書き込みはせず読み込みだけしたい場合は、何サイクル待てば良いのだろうか?
仕様書のold MEM[ad_0]は、1サイクル後にもう参照可能になることを意味しているのだろうか?
ステートマシンっぽいコードで確認してみる。

if (counter0 == 0) begin
			counter0 <= counter0 + 1'b1;
			data_write_en <= 1;
			data_write <= 11;
			data_addr <= 3;
		end else if (counter0 <= 5) begin
			counter0 <= counter0 + 1'b1;
			data_write_en <= 0;
			data_addr <= 0;
		end else if (counter0 <= 6) begin
			counter0 <= counter0 + 1'b1;
			data_addr <= 3; // read start
		end else if (counter0 <= 7) begin // 読み込みは1サイクル後に反映
			counter0 <= counter0 + 1'b1;
			counter3 <= data_read[4:0]; // read end
			data_addr <= 0;
		end else if (counter0 <= 20) begin
			counter0 <= counter0 + 1'b1;
		end else begin
			// nop
		end

アドレスを切り替え、1サイクルだけ適切なアドレスを参照し、遅延サイクルを計ってみる。
すると、1サイクル後にはもうデータが読み込めることが分かった。
これは便利。

End

Tang Nano 9KからMacへUART送信する

$
0
0

UARTのことを何も知らないところから始めて、通信を成功させるまでに色々罠にハマったので、メモ。
LEDチカチカデバッグだけではしんどいこともあると思って実装したが、思ったほど手軽ではなかったので、諸学者は後回しでも良い気がする。

最初に、成功したコードを丸ごと貼り付ける。
次のWebページのコードを参考にした。【Tang Nano 9K】uart送信

module top (
    input wire clk,
    input wire IN_B, // reset_n
...
    output reg uart_tx,
    input wire uart_rx
);

//////////////////////////
// UART

    localparam    UART_DURATION = 234; // 27MHz / 115200bps
    localparam    UART_CLOCK_UP = UART_DURATION; // 速いと通信失敗する、遅い方が成功しやすい
    reg [8:0] uart_tx_clock_counter; // 234 => 9bit
    wire   uart_tx_clock_up;
    assign uart_tx_clock_up = (uart_tx_clock_counter == UART_CLOCK_UP);
    wire [127:0] str = { "SE", 16'h0d0a }; // 送りたい文字列
    localparam    UART_TX_STATE_IDLE = 0;
    localparam    UART_TX_STATE_BUSY = 1;
    localparam    UART_TX_STATE_END = 2;
    reg uart_tx_valid; // 送る文字列が確定したら立てる
    reg [3:0] uart_tx_char_length; // 送る文字列の長さ、16文字まで
    reg [1:0] uart_tx_state;
    reg [3:0] uart_tx_char_counter; // 今送っている文字のインデックス
    reg [3:0] uart_tx_sending_counter; // 今送っているビット番号
    reg [7:0] uart_tx_sending_data; // 今送っている文字
    reg [25:0] uart_tx_wait_counter; // 適当な送信間隔
    always @(posedge clk) begin
        case (uart_tx_state)
        UART_TX_STATE_IDLE: begin
            if (uart_tx_valid && uart_tx_clock_up) begin
                uart_tx_state <= UART_TX_STATE_BUSY;
                uart_tx_char_counter <= 0;
                uart_tx_sending_counter <= 0;
                uart_tx_char_length <= 4; // とりあえずCRLFまで含めた4文字
                uart_tx_wait_counter <= 0;
            end
        end
        UART_TX_STATE_BUSY: begin
            if (uart_tx_clock_up) begin // 次の文字へ
                if ((uart_tx_sending_counter + 1'b1) == 4'd10) begin // 10回送ったら次の文字へ
                    if ((uart_tx_char_counter + 1'b1) == uart_tx_char_length) begin // 全文字送ったら終了
                        uart_tx_state <= UART_TX_STATE_END;
                    end else begin
                        uart_tx_char_counter <= uart_tx_char_counter + 1'b1;
                        uart_tx_sending_counter <= 0;
                    end
                end else begin
                    uart_tx_sending_counter <= uart_tx_sending_counter + 1'b1;
                end
            end
        end
        UART_TX_STATE_END: begin // 適当に待つ
            uart_tx_wait_counter <= uart_tx_wait_counter + 1'b1;
            if (&uart_tx_wait_counter == 1)
                uart_tx_state <= UART_TX_STATE_IDLE;
        end
        endcase
        // 27MHzから115200bpsの信号を作る
        uart_tx_clock_counter <= uart_tx_clock_counter + 1'b1;
        if (uart_tx_clock_up) begin
            uart_tx_clock_counter <= 0;
        end
        // リセット
        if (!IN_B) begin
            uart_tx_clock_counter <= 0;
            uart_tx_state <= UART_TX_STATE_IDLE;
            uart_tx_valid <= 1; //////////////////テストなので、すぐ文字列送信を開始する
        end
    end
    always @(*) begin
        case (uart_tx_char_counter) // 16文字まで対応
            4'd0: uart_tx_sending_data <= str[7:0];
            4'd1: uart_tx_sending_data <= str[15:8];
            4'd2: uart_tx_sending_data <= str[23:16];
            4'd3: uart_tx_sending_data <= str[31:24];
            4'd4: uart_tx_sending_data <= str[39:32];
            4'd5: uart_tx_sending_data <= str[47:40];
            4'd6: uart_tx_sending_data <= str[55:48];
            4'd7: uart_tx_sending_data <= str[63:56];
            4'd8: uart_tx_sending_data <= str[71:64];
            4'd9: uart_tx_sending_data <= str[79:72];
            4'd10: uart_tx_sending_data <= str[87:80];
            4'd11: uart_tx_sending_data <= str[95:88];
            4'd12: uart_tx_sending_data <= str[103:96];
            4'd13: uart_tx_sending_data <= str[111:104];
            4'd14: uart_tx_sending_data <= str[119:112];
            4'd15: uart_tx_sending_data <= str[127:120];
        endcase
    end
    always @(*) begin
        case (uart_tx_sending_counter)
            4'd0: uart_tx <= 1'b1; // デフォルトでHI信号
            4'd1: uart_tx <= 1'b0; // LO信号でスタート
            4'd2: uart_tx <= uart_tx_sending_data[0];
            4'd3: uart_tx <= uart_tx_sending_data[1];
            4'd4: uart_tx <= uart_tx_sending_data[2];
            4'd5: uart_tx <= uart_tx_sending_data[3];
            4'd6: uart_tx <= uart_tx_sending_data[4];
            4'd7: uart_tx <= uart_tx_sending_data[5];
            4'd8: uart_tx <= uart_tx_sending_data[6];
            4'd9: uart_tx <= uart_tx_sending_data[7];
            default: uart_tx <= 1'b1; // 10回送ったら次の文字に進んでOK
        endcase
    end

物理制約ファイルは以下の通り

IO_LOC "clk" 52;

IO_LOC "IN_B" 4;
IO_PORT "IN_B" IO_TYPE = LVCMOS33 PULL_MODE = UP;

IO_LOC "uart_tx" 17;
IO_PORT "uart_tx" IO_TYPE=LVCMOS33 PULL_MODE=UP;

IO_LOC "uart_rx" 18;
IO_PORT "uart_rx" IO_TYPE=LVCMOS33 PULL_MODE=UP;

確認方法(Mac)

ls -l /dev/tty.* を実行し、接続先を確認。
sudo cu -l /dev/tty.usbserial-foobar -s 115200 -d を実行し、シリアル通信を開始。
~を押してから.を押して、ポートを閉じる。

ハマったこと

  1. RXとTXを逆にしていた
    入力ポートにoutputしても何も警告は出ない。
    気づけるチャンスは、Lチカさせると輝度(電圧?)が異様に高いこと。
    直したら普通の輝度になった。
  2. シリアルポートが複数あってどれが正しいのか分からない
    公式のUARTサンプルコードを動かして、ちゃんと使えるポートを確認しておくのがお勧め。
    USBポートを変えなければ、抜き差ししても同じポートが使えるようだ。
  3. 通信が成功しない
    115200bpsより少しでも速いと送信できない様子。
    ちょっと遅いくらいなら問題ない。
    ハードの仕様なのか、ドライバの仕様なのか、UARTのお決まりなのかは分からない。
  4. Pythonでのシリアル通信のやろうとしたが、環境構築ができない
    pyserialを導入すれば良いことは分かるが、OSの環境を壊す云々の警告が出て、その回避のためにpipxを使ったが、pipxで導入したライブラリを使う方法が分からなかった。
    結局OS標準のコマンドを使った。
    プログラムを作るならC言語でopen()する方が簡単かも?
  5. サンプルコードがそもそも難しい
    コンソールに文字を出そうすると、必要なカウンタレジスタが増えてコードが複雑になってしまう。
    同じ1文字を出し続けるのなら簡単に出来るが、改行(CR)を送らないとコンソール側がflushされない(かもしれない)ので、厳しい。

以上

Viewing all 40 articles
Browse latest View live