LoginSignup
13
14

More than 5 years have passed since last update.

C++ AMPでGPGPU!

Last updated at Posted at 2015-12-20

この記事はC++ Advent Calendar 2015の20日目の記事です.
昨日はろっさむ(4_mio_11)さんの「つまづいたっていいじゃないか、C++だもの」でした.


I(@wx257osn2)です.また20日です1

ところで今年はやたらとGPUを燃やそうとする人が多いんですが一体どうしたんですか?2
ネタが被るんじゃないかとびくびくしながら1ヶ月過ごすのは中々つらかったです.

おことわり

当記事ではVS2015を使ってC++ AMPで遊びます.当記事でC++ AMPと言ったらMSVC上でのそれです.
というわけでWindows73以降とVisual Studio 20154,及びDirectX11.0以降に対応したGPUが必要です.
持ってない人は諦めてください.
ちなみに私の環境はWin8とVS2015,GTX780です.

C++ AMPについて

3日目にりやさん(Riyaaaaa)さんが入門記事を書いてらっしゃるのでそちらを読んでください(丸投げ)

気になる記事が

C++ AMPを学ぶ際,やはり1次資料的な位置付けとなるのがMSDNとかMSDN Blogsですが,そんなMSDNの中に面白い記事が.

グラフィックス (C++ AMP)
https://msdn.microsoft.com/ja-jp/library/hh913015.aspx

要約すると,

  1. 計算バッファとして空間局所性の高い「テクスチャ」を利用できる
  2. norm/unorm5,short vectorクラス6が提供されている
  3. get_texturemake_textureを使うことでD3Dのtexture1d/texture2d/texture3dと相互運用できる7

の3つです.
つまり,GPGPUで2次元テクスチャの各ピクセルに対して何らかの処理をして値を書き込むみたいなことも出来るというわけ.
…あれ?それなんてピクセルシェーダー89

というわけで,C++ AMPによるGeneral-Purpose computing on Graphics Processing UnitsをGraphical-purpose computing10に使ってみましょう.

C++ AMPグラフィックスの概要 : short vectorクラスとテクスチャクラス

というわけでまずは件の記事の内容をざざっと.
先におことわりですが,今回はシェーダープログラミングに必要な最低限の内容のみを取り扱っていきます.
簡単のために,多数のオーバーロードやオプションの引数,使わなかった要素については大胆に解説を削っていきますのでご了承ください.
まず,グラフィックス関連の諸々はみなamp_graphics.h内でconcurrency::graphics名前空間に入ってます11
concurrency::graphics::normconcurrency::graphics::unormは「5」の補足の通りです.それ以上の詳細はMSDN行ってください.

short vectorクラス

short vectorクラスはC++ AMPで使える「6」です.
1次元についてはそれぞれの型そのものであり,実際に所謂ベクトルクラスっぽくなるのは2次元以上です.
命名は,T_Nの形になります.int型2次元ならint_2unorm型4次元ならunorm_4…という感じです.
これらがconcurrency::graphics名前空間に属するので,using等をしない場合にはconcurrency::graphics::int_2等といった形になります12
基本的にそこらのベクトルクラスみたいなものですが,特徴的な機能としてスウィズリング式(swizzling expression)があります.
例えば,norm_4の第0要素と第2要素を取り出したnorm_2が欲しい…と言ったとき,以下のように記述することが出来ます.

const graphics::concurrency::norm_4 vector = {0.f, .1f, .2f, .3f};
auto vec = vector.xz; //decltype(vec) : graphics::concurrency::norm_2
vector.xz = graphics::concurrency::norm_2{.4f, .5f};
// vector == {.4f, .1f, .5f, .3f}

このように,特定の識別子をメンバのごとく連ねることでアクセスが簡単にできるようになっています.
これら識別子は以下のように対応しています.

アクセス先 識別子1 識別子2
0 x r
1 y g
2 z b
3 w a

識別子はxyzwrgbaでグループとなっており,これらを混ぜて使うことは出来ません.

vec.xy //OK
vec.xg //NG

また,vec.xx等のように同じ識別子を連ねることも出来ません13

そして,concurrency::graphics::short_vector_traits<T>はshort vectorクラスT_Nに対して(擬似コードですが)大体以下のような定義がなされています.

struct short_vector_traits<T_N>{
  using type = T;
  static cosnt int value = N;
};

これを用いることでtemplateを用いつつ処理を分岐したりまとめたり,といったことが可能です.

concurrency::graphics::texture<T, N>concurrency::graphics::texture_view<T, N>

続いてテクスチャクラスについてです.
texture<T, N>Tにはdouble_4を除く1・2・4次元のshort vectorクラスが,Nには1~3が入り,これによってテクセル形式T,次元Nのテクスチャが生成されます.
まぁ大体concurrency::array<T, N>と大差無いです.しかし,唯一大幅に違うのは,textureクラスには実質的に14writeアクセスが出来ないということです.
textureのメンバは,operator[]operator()get()も全てを返します.参照は返してくれません.
あとstd::vectorへの変換とかは出来ません.基本的には,テクスチャはアクセラレーター固有のバッファとして扱うことになります.

さて,書き込めないtextureをどう使えばいいのかというと,concurrency::array_view<T, N>の如く,concurrency::graphics::texture_view<T, N>というクラスが存在し,こちらからset()メンバ関数を通して書き込むことが出来ます15
texture_viewも概ねarray_viewみたいなものですが,CPUや異なるアクセラレーターへのデータの自動的な移動などは行ってくれません.
テクスチャはあくまでアクセラレーター固有の(ry

D3Dとの相互運用

さて,C++ AMP単体だと完全に「アクセラレーターに強く紐付けられた速い一時バッファ」でしかないテクスチャですが,D3Dとの相互運用によってただのバッファからGUIやらゲームやらで使える「テクスチャ」に変貌します.
concurrency::direct3d名前空間には一般的なD3Dとの相互運用性のためのユーティリティ群が,concurrency::graphics::direct3dにはグラフィックス関連のD3Dとの相互運用性のためのユーティリティ群が入っています.
concurrency::direct3d::create_accelerator_view関数16にD3DのDeviceインターフェースを投げるとconcurrency::accelerator_viewが返ってきます.
このacclerator_viewを用いて以後のC++ AMPプログラミングを進めることが可能です17
そして,D3D側で作成したテクスチャと上述のcreate_accelerator_viewにて作成したaccelerator_viewオブジェクトをconcurrency::graphics::direct3d::make_texture関数16に渡すことで,C++ AMPのconcurrency::graphics::textureを得ることが出来ます.
これを使えば,C++ AMPでの画像処理に入力画像を渡すことも容易です.
また,C++ AMPで処理したconcurrency::graphics::textureを渡すことで元になるD3Dのテクスチャインターフェースを返す関数がconcurrency::graphics::direct3d::get_texture16です.
一応補足しておくと,create_accelerator_viewでD3D Deviceから生成したaccelerator_viewを用いて作成したconcurrency::graphics::textureであれば,make_textureを使っていなくともget_texture可能です.

C++でシェーダーを書こう

それではようやく本題です.C++のみでシェーダーアートを楽しんでみましょう.

大量の関門

と言いたいところですが,実はそのままシェーダーを書くには問題がメチャクチャたくさんあります.

  1. short vector用の関数が無い
    無いです.float用の数学関数などはamp_math.hconcurrency::fast_math名前空間に用意されているので,諦めて自分で各次元用に自作しましょう.
    ちなみに,minmaxfloorabsなども当然無いですが,そもそもvectorとして大変重要なdotcrossも存在しません.自作しましょう.
  2. modが負値を返しうる
    メチャクチャ落とし穴ですが,C++の剰余は負値を返すことがあります.
    いや,それ自体は別に落とし穴でもなんでもないんですが,問題なのはGLSLのmodは負値を返しません.
    というわけで,実はconcurrency::fast_math::fmodを直接使うと,剰余として常に正の値が返ってくることを想定したプログラムが上手く動作しないことがあります.
  3. C++ AMPはインデックスの順番が逆
    縦横のサイズを指定するconcurrency::graphics::textureのコンストラクタをよく見てみると,コメントに引数の順番がheight, widthの順であるとの記述があります.
    同様に,concurrency::graphics::texture<int, 2> textureに対するtexture.extentによって得られるconcurrency::extent<2>から取り出したconcurrency::index<2>[0]y, [1]xの順に並びます.
    よって,x = idx[0], y = idx[1]のようなコードをうっかり書いてしまうと,解像度が縦横同一でない限りほぼ確実にうまく動かなくなります.
  4. short vectorのコンストラクタが貧弱
    特にGLSLのコードを移植する際に弊害となりえます.1次元,2次元,1次元の3つのshort vectorをコンストラクタに渡して4次元のshort vectorを作る…といったことがそのままでは出来ません.
  5. 行列型が存在しない
    やる気あんのかって感じですが,行列型もありません.
  6. テクセル形式
    本来は問題では無いのですが,D2Dと組み合わせようとするとここで問題が発生します.
    C++ AMPのconcurrency::graphics::textureから最終的にD2DのID2D1Bitmap1インターフェースに落としこむことを考えた時,D2D側のピクセル形式は実質的にDXGI_FORMAT_B8G8R8A8_UNORM一択となります.
    一方,get_textureで得られるD3D Textureインターフェースのテクセル形式はDXGI_FORMAT_R8G8B8A8_UNORM(Creating textures with specific DXGI_FORMAT参照)です.
    つまり,一度何らかの方法でD3D Textureのテクセル形式をRGBAからBGRAに変更する必要があります.

と.このように問題が山積しています.

めんどうくさいので

テンプレ作りました.

Shampoo
https://github.com/wx257osn2/shampoo

git submoduleを使っているのでよしなにしてください.
shader.hpp内にいい感じにシェーダーを書くとなんかそれっぽく動きます.
サンプルとしてネット上に公開されているGLSLシェーダーを幾つか(一部除き作者の皆様には無断で)C++コードに移植して入れておいたので参考にしてください.
そこそこ素直に移植できます.さらに,C++なのでautoとか使い放題です.

TODO

  1. 関数が足りない
    サンプルで必要な分は最低限作りましたが,その程度です.足りません.
  2. 行列型がない
    サンプルの一つに行列を使ったものがありましたが,定数だった上に面倒だったので関数として直書きしてしまいました.そのうち行列型も作ります.
  3. スウィズリング式がアレ
    上述の通り,vec.xxなどのように同じ識別子を連ねることが出来ないため問題となる部分がいくつか出ています.そのうち実装します.

まとめ

  1. C++ AMPにはグラフィックスに特化した機能がある
  2. C++ AMPでシェーダーアートを楽しむことが出来る
  3. 標準のみで書きやすいとは言っていない

謝罪

がむさん,tomohiroさん,i-saintさん,勝手にシェーダ使ってごめんなさい…
なにか問題がございましたらおっしゃってください,リポジトリごと作りなおして消します…


というわけで,Advent Calendarも残すところあと5日間となりました.
明日は21日目,みやたけ ゆきさんです.



  1. 3年連続20日目担当 

  2. 私は自室が北側で寒いので燃やしてます. 

  3. Win7はPlatform Update適用済みの場合のみ可…このご時世に未適用の環境なんてそんなに残ってるとは思いませんが. 

  4. 「VC++はC++ではない」的な話をしたい方,最近のMSさんは以前に比べればメチャクチャ頑張ってるので,そろそろVC++のことも許してあげてください.まだTwo Phase Lookup実装されてませんが,実装するつもりではいるらしいです. 

  5. 値の範囲が[-1.f,1.f]/[0.f,1.f]にクランプされるfloat 

  6. int,uint,float,double,norm,unormの1~4次元のベクトルクラス,HLSLやったことある人にはお馴染みのアレ(機能は制限されてるとかなんとか,私HLSL知らないので…) 

  7. 流石はMS製という感じ.実際MSVCにおけるC++ AMPはDirectComputeをバックエンドとして実装されているので,こういった芸当が可能なのだと思われます. 

  8. フラグメントシェーダーとも言う.ぶっちゃけ何処理すんだかちっともわからない名前なので「ピクセルシェーダー」の方が好きです. 

  9. 先述の通りバックエンドはDirectComputeなので,C++ AMPはコンピュートシェーダー相当のことが出来ます.テクスチャに対してピクセル単位で処理を行えば,ある程度ピクセルシェーダーのように振る舞うことも可能ということです. 

  10. 「C++ AMPでGraphical-Purpose computing on Graphics Processing Units!」 

  11. 厳密にはshort vectorクラスはamp_short_vectors.hに入ってますがamp_graphics.hでincludeされているのでとりあえずそっちincludeしとけばいいと思います 

  12. 一応typedefとしてconcurrency::graphics::direct3d::int2という形もあるのですが,今回は扱いません. 

  13. 正直これ欠陥だと思う…ちなみに内部実装は__declspec(property(get=getter, put=setter))って感じ.こう見てみると,いざsetter定義しようとするとどうすんだこれってなる(std::tie(x, x) = std::make_pair(1, 2)みたいなもんなので)のも事実なので難しいところではあります(まぁsetter定義しなきゃいいんじゃ…とも思うんですが) 

  14. 実際にはTintとかfloatとかだったら書き込めるらしいですが,そんな状況私が考えうる限りZバッファぐらいしか無い気がしますし,後述の方法でいくらでも出来るので… 

  15. 決して書き込み専用のクラスというわけではなく,特にtexture_view<const T, N>とすることで読み取り専用のtexture_viewが作れたりするのですが,今回は使ってないのでパスします 

  16. 恐らくD3Dのバージョンが上がった際にも対応できるように,或いは次元ごとに関数を分けたくないという考えからか,D3DのインターフェースはIUnknown*になっています. 

  17. 同時にD3D DeviceとC++ AMP Accelerator Viewを操作する場合にはconcurrency::direct3d::d3d_access_lockなどを用いてロックを取る必要があるようですが,今回は使ってないのでパス 

13
14
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
14