2019年2月2日土曜日

一つの正規表現でほしい結果を得るのが難しかったので、複数回に分けて正規表現を使えばいいと考えて作ったツールの制作メモ

HTMLとJavascriptで動作するツールを作ったので、その際に実装に困った部分とかをメモしていく。

contenteditable属性とdocument.execCommand関数


当初はtextareaタグを使おうとしたのだが、このままだと正規表現とマッチした箇所が色付けできないようで、別の方法を使わないといけない。

そんな時はcontenteditable属性を好きなタグに付けてあげると、HTML上から好きに編集できるようになる。Web上でテキストエディタを作る時に利用されているようだ。

それに加え、document.execCommand関数を使うと太字にしたり、背景色を変えたり、いろいろな操作ができる。

実際にWeb上で動作が確認できるサイトを以下に書いておく。

https://codepen.io/chrisdavidmills/full/gzYjag/
https://codepen.io/fukaminmin/pen/JyzOLz


document.execCommand関数は一部ブラウザでは使用できない機能がある。

そのため、実際に使えるか確認する必要があるのだが、そのときはdocument.queryCommandSupported関数を使うといい。

var cmd = "bold";
if(document.queryCommandSupported(cmd)){
   document.execCommand(cmd);
}
これで、ブラウザ上で正規表現とマッチした箇所を表示する準備ができた。

Javascript上からテキストを選択する (Seletion型とRange型)


document.execCommand関数は選択されているテキストに対して処理を行う関数なので、どうにかしてマッチした部分を選択する必要が出てきた。

その選択もマウスを使うわけにも行かず、Javascriptから行う必要がある。

そんな時はSelection型とRange型を利用することができる。

Range: https://developer.mozilla.org/en-US/docs/Web/API/Range
Selection: https://developer.mozilla.org/en-US/docs/Web/API/Selection

var range = document.createRange(); // Range型の作成
range.setStart(node, index);
range.setEnd(node, index + regex_pattern.value.length);
//Selection型を取得して、rangeで指定した範囲を選択部分として登録する
window.getSelection().addRange(range);
//選択を解除した時は以下の関数を呼ぶ。
window.getSelection().removeAllRanges();

注意点として、Range.setStart()とRange.setEnd()に渡すnodeの型によって、indexの意味合いが変わってくる。


TextComment、CDATASectionタイプのNodeならその中の文字列の添字と同じ意味合いになるが、それ以外のタイプのノードだと、子要素への添字になる。
今回はテキストの一部を選択したかったので、分岐でうまいこと処理を実装する必要があった。

Node.nodeTypeの種類はNode型の変数からアクセスできる


Javascript初心者丸出しだが、最初はグローバル領域にあるものだと勘違いしていた。


一度、document.execCommand関数で分割されたテキストをもとに戻すと複数の子ノードに分割されたままになる。


(なんだが、ブラウザ依存な気もするが...)

正規表現で一致した箇所をdocument.execCommand関数を使ってマークした後、別の正規表現を使って他の部分をマークしたいケースが当然出てくる。

そのときは以前マークした部分をクリアーしてあげる必要が出てくるが、そのクリアーした後のテキストノードは分割されたままであって、それ起因のエラーに出くわしてしまった。

Node.textContentを使っているとそれに気が付かずに、しばらく原因がわからず困ってしまった。

一度原因がわかれば、一つの子ノードにまとめれば解決できるので、まとめようとしたところ・・・添字アクセス違反のエラーが起きてしまった。なぜ?

子要素の削除にはChildNode.remove関数を使ったほうがいい。…?


子要素へのアクセスはNode.childNodesを利用している。

はじめは、Node.removeChild関数を使ったが、なぜかundefinedになっていた。

ここらへんはHTMLDivElement型からremoveChild関数を呼び出そうとしてのが間違いだった。

子ノードの削除はChildNode型のremove関数を使うとできるようだ。

終わりに


以上のことで、やりたいことを形に大体できた。
ここに書いていないが正規表現のパイプライン化の処理を追加するとツールはできる。

ただ一つ問題点があって、置換する時に正規表現のキャプチャー機能の扱いをどうするか決めなければいけない。

実際、この機能が欲しい。というゆうより無いととても不便である。

単純に各正規表現でキャプチャーしたものを指定できるようにするのか、オレオレ仕様の謎仕様に仕立て上げるのか、正規表現の仕様を頑なに守ろうとするのか。

どのようにすれば自然な形にキャプチャーできるのかはまた別に考えることにする。

けど、$1.2みたいにドットで区切って指定する形が無難でいいと思う。

2019年2月1日金曜日

FirebaseのonAuthStateChanged関数でページを開いたときだけ、違う処理をしたい!

firebaseを使って漢字パズル的なサイトを作りたいなって思ったので、少しずつ作業を進めていたところ、あるささいな問題に出くわした。

認証時に表示するスナックバーがページを開いたときにも出てしまう。  

書いたままなんだけど、結構鬱陶しかったので、なんとか解消できないか試してみた。

// ページを開いたときにもスナックバーが表示されてしまうコード。
firebase.auth().onAuthStateChanged(function(user){
  if(user) {
    showSnacbar("認証されているよ");
  } else {
    showSnacbar("認証されていないよ");
  }
});

onAuthStateChanged関数の戻り値を使ってみたが…

まずは、 firebase.auth().onAuthStateChanged関数の戻り値を使うと登録した関数を取り除くことができるようなので、初回限定の関数を登録して、そこからはじめの処理を行う関数を登録するようにしてみた。が、ダメだった。

// ダメだったコード。
var unsubscribe = firebase.auth().onAuthStateChanged(function(user){
  unsubscribe();
  //ページを開いたときにはスナックバーを表示しないようにしたつもり
  firebase.auth().onAuthStateChanged(function(user){
    if(user) {
      showSnacbar("認証されているよ");
    } else {
      showSnacbar("認証されていないよ");
    }
  });
});

関数自体を取り除くことはできたが、onAuthStateChangedオブサーバーの発行中にfirebase.auth().onAuthStateChangedで関数を登録すると、その発行中に即座に呼び出されてしまうみたいだ。

そういえば、クロージャってあったよね(解決)


どうしようかと悩んでいたら、Javascriptにはクロージャがあるじゃないかと気づき、以下のコードを書いてみた。
// クロージャを使った成功例
var smartShowSnacbar = function() {
  var counter = 0;
  return function(user) {
    counter++;
    if (counter <= 1) { return ; }

    if(user) {
      showSnacbar("認証されているよ");
    } else {
      showSnacbar("認証されていないよ");
    }
  };
};
firebase.auth().onAuthStateChanged(smartShowSnacbar());
これで無事、目標を達成できた。クロージャを初めて習う人が見ることになるコードだけど、上手に活用できたのではないかと思う。

2019年1月31日木曜日

gccで指定したアーキテクチャのバイナリをコンパイルする方法



自作エミュレータで学ぶx86アーキテクチャ コンピュータが動く仕組みを徹底理解!
という本を買って、はじめの部分を読んでいると実機と本とで出力される内容が違った部分にいきなり出くわしたので、その解決法をメモしていく。

注意点


原因はこちらが使っているCPU(x86_64)のアーキテクチャが本が想定しているもの(i386)と違うのが原因なので、本が誤っているわけではないので注意!


解決法


gccとldそれぞれにi386で処理してくれとオプションを指定することで解決できた。

gcc -c -o xxx.o input-file.c ..なんか色々なオプション.. -m32 -fno-pie
ld ..なんか色々なオプション.. -m elf_i386 --oformat=binary

GCC

gccのオプション-m32を指定することで、32bit環境のコードを生成してくれるようになる。
-m32のドキュメントはこちら

ただこれだけだと、 ldコマンドで

undefined reference to `_GLOBAL_OFFSET_TABLE_'

とエラー起きたので、 -fno-pieも追加で指定する必要がある。

gccはデフォルトでposition-independent executables (PIE)と呼ばれるプログラムコードが実際に動作するPC上のメモリの好きなところに配置されても正しく実行できるようにしている。

が、その分余分なコードを生成するのだが、今回は勉強のためなのでそのような仕組みはいらず、-fno-picを指定して上げる必要があった。

ld

ldでも-mオプションでエミュレートしたい環境を指定する。指定できるものはld -Vで確認できる。

今回は32bit環境なので、ld -Vにあったelf_i386を指定した。

--oformatオプションは出力されるオブジェクトファイルのバイナリフォーマットを指定するものだ。

指定できるものはobjdump -iで確認できる。binaryは何でもいいよ的なもの。



2019年1月26日土曜日

Git コミットの内容を分割して、コピー&ペーストする

作業をしていて、他のブランチでコミットした内容を使いたいという状況に陥った。
masterにあればいいのだがないので、なんとかして取り出したい。

さらに取り出したい内容は一つのコミットに色々な作業内容を含めたままプッシュしてしまっているときた。

 今までだと、諦めて作業を進めていたが、Gitは便利なもので他のコミットの内容をコピーできたり、コミット内容を後から分割できることを知ったので、そのやり方をメモしていく。

 参考サイト
gitで他ブランチの特定のコミットを取り込む方法
gitで1つのコミットを複数のコミットに分割する

コミットから取り出したい内容を分離


まず、取り出したい作業内容をコミットから分離する。

取り出したいコミットをする前の状態に戻る。
git reset <分離した作業内容があるコミットの一つ前>
あとは適時作業内容を分割してコミットしていき、取り出したい作業分をまとめたコミットを作成する。

git rebaseを使った方法もある。上のやり方は直前のコミット内容を分離したい時には便利だが、もっと昔のものとなると大変なので、そのときはgit rebaseを使用する。


Gitで数個前のcommitを遡って分割する


 git cherry-pick

次にgit logを使って取り出したいコミットのIDをメモしておく。

ここまで来たら、コミットのコピーを使いたいブランチに戻り、git cherry-pickを使う。

git cherry-pick <commit-id>

これで目的は達成できた。

2019年1月25日金曜日

git コミットの範囲を指定したPushのやり方。

gitでpushしようとしたときにプッシュしたくないコミットが出てきたので、うまいことそのコミットだけ除いてプッシュしたかったので、調べてみた。

参照仕様


Gitの公式ドキュメントの例にgit push origin HEADとあったので、じゃあHEAD~でひとつ前のコミットを省いてプッシュできるのかなと思ったら以下のようなエラーが起きた。

 remote part of refspec is not a valid name in HEAD~

HEAD~はrefspecのリモート部分では有効な名前ではないという英文で、悲しいかなHEAD~は使えないようだ。

git push origin HEADのHEADの部分はrefspec(参照仕様)を指定する部分だそうで、HEADはローカル環境の最新のコミットではないみたいだ。

参照仕様は+<src>:<dst>で表記される。

push時に参照仕様を使う場合は以下のようになる。

git push origin master:refs/heads/master
# こんな感じの意味になる。

git push リモート名 リモート先のブランチ名:ローカルブランチの参照の名前
ちなみにプルのときはプッシュとは反対でローカルの参照名を指定してからリモートの方を指定する形になる。

git fetch origin master:refs/remotes/origin/master
#こんな感じの意味になる
git fetch ローカル名 ローカルのブランチ名:リモートブランチの参照名

うん、git pushではコミットを指定してプッシュすることができないようだ。

ちなみに全ての参照の名前を確認したいときはgit show-refを使うといいようだ。

ローカルのHEADをずらしてからプッシュ

git pushではできないことが分かったので、ローカルのHEADをずらしてやってからプッシュすればできるのではないかと考えたので、HEADのずらし方を調べてみる。

参考サイト

HEADがローカルの最新コミットを指していない状態をGitではDetached HEADと呼んでいるそうだ。

今回の場合はわざとDetached HEAD状態にして、プッシュを行う。

Detached HEADにする方法はいくつかあるみたいだ。


  • git checkoutを使う方法
  • git reflogを使う方法
  • git reset --hardを使う方法
  • git revert を使う方法


今回はgit reflogを使う方法を試してみた。

git reflogを使うとコミットしたものがメッセージと共に表示される。

表示されるコミットにはそれぞれHEAD@{X}とXに番号が割り当てられている。(最新のコミットが0であとは昇順に並んでいる)

それで、HEADにしたいコミットの番号を使って

git checkout HEAD@{X}

とコマンドを打つと無事Detached HEADな状態にできる。(git checkoutは最終的には使っている)

あとはプッシュしてやると、初めの目的であるコミットの範囲を指定したプッシュができる。

と思ったらできなかった。

fatal: You are not currently on a branch.
To push the history leading to the current (detached HEAD)
state now, use

    git push origin HEAD:<name-of-remote-branch>


どうやら、Detached HEADな状態ではプッシュする際は参照仕様を指定してあげる必要があるみたいだ。

エラーメッセージにコマンドの使い方を教えてくれているので、その通りに実行してみよう。

git push origin HEAD:master

これで無事プッシュできた。

最後に


あとは、HEADを最新コミットに戻してあげるのを忘れずにすればOKだ。

一度この操作を行うと、ローカルでの最新コミットにHEADを移しても、Detached HEADな状態とGitは見るみたいなので、同じ操作が必要になる。

だから、なるべく区切り良いところでコミットすることを心がけることが大事だとは思うが、うっかりミスをするのが常なので、リカバーできるようになると安心でしょう。


2019年1月22日火曜日

Travis CIのMatrixについて

Travis CIについて簡単に調べたので、その中のmatrixについてメモ

Matrixとは


複数のjobを実行する機能のこと。

これを利用すると、言語のバージョンや異なる環境でのテストを実行することができる。

実行したい環境を列挙していくと、その組み合わせの分jobが自動的に実行される。

# rvmとgemfile,envの各値を組み合わせた数のjobが実行される。
# 下のだと2x2x2の8jobが実行される。
rvm:
  - 2.5
  - 2.2
gemfile:
  - gemfiles/Gemfile.rails-3.2.x
  - gemfiles/Gemfile.rails-3.0.x
env:
  - ISOLATED=true
  - ISOLATED=false

# matrixを使って、jobの環境を直接指定できる。
# 以下は2つのjobが実行される。
matrix:
  include:
  - rvm: 2.5
    gemfile: gemfiles/Gemfile.rails-3.2.x #値は省略せずに書くこと
    env: ISOLATED=true
# - で一つのjob環境になる。
  - rvm
    gemfile: gemfiles/Gemfile.rails-3.2.x
    env: ISOLATED=false

基本的に上のコードの初めのやり方が簡単なのだが、一部の組み合わせは除外したいという場合も出てくる。 そのようなときはmatrix.excludeを使うと除外したい環境を指定できる。
# rvmとgemfile,envの各値を組み合わせた数のjobが実行される。
# 下のだと2x2x2の8jobが実行される。
rvm:
  - 2.5
  - 2.2
gemfile:
  - gemfiles/Gemfile.rails-3.2.x
  - gemfiles/Gemfile.rails-3.0.x
env:
  - ISOLATED=true
  - ISOLATED=false

# 以下、除外したい環境を指定する。
matrix:
  exclude:
  - rvm: 2.5
    gemfile: gemfiles/Gemfile.rails-3.2.x #値は省略せずに書くこと
    env: ISOLATED=true

#以下のように書くとrvmが2.5のものはすべて除外される。
matrix:
  exclude:
    - rvm: 2.5

基本はバージョンや環境を列挙していけばあとはTravis CIがよろしくやってくれる。 matrixは除外したい環境があるときに使うのが、一番やりやすい書き方と思う。


トピック matrix.allow_failures


失敗してもいい組み合わせがあるときはmatrix.allow_failuresを使って指定するといい。
デフォルトだとmatrix.allow_failuresを指定されたjobが失敗しても、その時点では終了とは見なされず、そのjobは最後まで実行され、余計な時間を取ってしまうことになる。
失敗したら可能な限り早く終了したとマークしたいときは、matrix.fast_finishをtrueにするといい。
# rvmとgemfile,envの各値を組み合わせた数のjobが実行される。
# 下のだと2x2x2の8jobが実行される。
rvm:
  - 2.5
  - 2.2
gemfile:
  - gemfiles/Gemfile.rails-3.2.x
  - gemfiles/Gemfile.rails-3.0.x
env:
  - ISOLATED=true
  - ISOLATED=false

# rvmが2.5のものはすべて失敗してもOK
matrix:
  fast_finish: true  # 失敗したら、可能な限り早くjobが終わったことを告げるためのパラメータ
  allow_failures:
  - rvm: 2.5

2019年1月12日土曜日

Direct3D12について雑記 その2 GPUへのコマンド発行

・GPUへのコマンド発行

DX11とは違い、DX12ではGPUへコマンドを発行するために使用するインターフェイスは増えている。

DX11ではID3D11DeviceContextのみでコマンドの発行を行っていたが、DX12ではそれを以下のものへ分割したものになっている。

  • ID3D12CommandQueue : 実際にコマンドをGPUに発行するもの
  • ID3D12CommandAllocator : コマンドの記録領域
  • ID3D12GraphicsCommandList : コマンドを記述していくもの

この中でID3D12GraphicsCommandListがID3D11DeviceContextと似たインターフェイスになっているので先に紹介する。


・コマンドを記述するためのもの ID3D12GraphicsCommandList


使い方はID3D11DeviceContextと変わりない。
シェーダの設定がDX12で大きく変わったので、ID3D11DeviceContextのVSSetXXX()やPSSetXXX()がなくなって、ID3D12PipelineState、ID3D12RootSignatureの設定に置き換わっている。

その一方でレンダーターゲットや深度バッファのクリアー関数、頂点バッファのバインドは同じように使える。
(ただしDX12でビューの仕様が変わっているので、そこは異なる)

変わっていない部分は以下のものになる。
  • Input-Assembler Stage (IA) : 頂点バッファ、インディクスバッファ、Primitive Topologyの設定を行う。
  • Stream-Output Stage (SO) : ジオメトリシェーダで出力するバッファの設定を行う。
  • Rasterizer Stage (RS) : ビューポート/シザー領域の設定を行う。
  • Query : GPUのタイムスタンプやSOステージの出力結果の情報、描画で実際に書き込まれたピクセル数などを問い合わせることができる。ただし、ID3D12QueryHeapで結果を受け取る記録領域をこちらで用意しないといけない。
  • リソースのクリアー、コピー : そのまま。コピーに関しては専用のID3D12CommandQueue/ID3D12GraphicsCommandListを作ることができる。詳しくは下の方で。
  • Draw/Dispatchコール : バリエーションは減っている。ただし、Indirect Draw/DispatchはExecuteIndirect関数に置き換わっていて、やり方は変わっている。

変わった部分は以下のものになる。

  • シェーダ/パラメータの設定 : ID3D12RootSignature/ID3D12PipelineStateを使って設定するようになっている。
  • Output-Merger Stage (MO) : 上と同じ。Blend StateやDepth Stencil StateなどはID3D12PipelineStateで設定することになっている。レンダーターゲットの指定は変わっていないが、ID3D12PipelineStateでそのフォーマットを決める必要がある。

    シェーダ/リソースの設定をID3D12RootSignature/ID3D12PipelineStateで行うようなった影響を受けている。

    これら二つでシェーダの設定を行うようになったことで、Graphics Pipelineの切り替えコストが減っている。

    ただ、その分、同じシェーダを使うが不透明/半透明の二通りで描画したい、加算合成で描画したいなど、ちょっとした状態の違いを持つものでも、すべて異なるID3D12PipelineStateを作る必要があるので、管理はめんどうになっている。

    上のようなケースだと余分に容量を使用したり、コンパイルの時間がかかってしまう問題があるが、ID3D12PipelineLibraryを使うことで軽減は出来る。ただ、あくまでコンパイルとデータ容量の削減をしてくれるだけで、そのバリエーションの分ID3D12PipelineStateを作り、その切り替えはこちらでする必要がある。

    新しく増えたものは以下のものになる。

    • Reset/Close : コマンドを記述する際は毎回Resetを行い、明示的にCloseする必要がある。
    • リソースのバリアー指定 : DX12では各Draw/Dispatch時のリソースの使われ方を指定する必要がある。GPU内のキャッシュの扱いに影響する。DX11ではドライバーが行っていた。

    ・ID3D12GraphicsCommandListの種類。

    ID3D12GraphicsCommandListには用途に合わせて以下の種類がある。
    • D3D12_COMMAND_LIST_TYPE_DIRECT : 全コマンド記録可能
    • D3D12_COMMAND_LIST_TYPE_COMPUTE : コンピュートシェーダとコピー関連のコマンドを記録できる。
    • D3D12_COMMAND_LIST_TYPE_COPY : コピー関連のコマンドしか記録できない
    • D3D12_COMMAND_LIST_TYPE_BUNDLE : 毎フレーム同じ内容になるコマンドをまとめて使いまわせるようにするときに使う。c++でいうプリコンパイル済みヘッダーみたいなもの。使えるコマンドに制約がある。ID3D12GraphicsCommandList::ExecuteBundle()で記録したコマンドを他のID3D12GraphicsCommandListに書き込むことができる。
    • D3D12_COMMAND_LIST_TYPE_VIDEO_DECODE/D3D12_COMMAND_LIST_TYPE_VIDEO_PROCESS : 動画関係のコマンドを作るときに使われるみたい。

    上三つが基本的な種類になる。
    D3D12_COMMAND_LIST_TYPE_DIRECTを使っていれば問題はない。
    ただ、GPUにはグラフィックスパイプライン用のキューとコンピュートシェーダ用のキュー、コピー用のキューを持ち、GPU(によって)はそれぞれ独立して実行できる。

    このキューの種類はID3D12CommandQueueの作成時に指定でき、効率的なアプリを作るなら、意識してコマンドを作る必要が出てくる。

    以上がID3D12GraphicsCommandListの簡単な紹介である。

    基本的な使い方の流れは以下のものになる。
    1. ID3D12GraphicsCommandList::Resetする
    2. GPUにやらせたいことを記録していく。
    3. ID3D12GraphicsCommandList::Closeで記述を終える
    4. ID3D12CommandQueueを使ってGPUにコマンドを伝える。
    ID3D12GraphicsCommandListはあくまでGPUへのコマンドの記録のみを行うものである。

    記録のみで実行はしないので、複数のスレッドで同時にGPUへのコマンドを作成することができるようなった。(DX11でもDeffered Contextを使えばできるが、暗黙のルールが多くDX12でそれらが明示的になった感ある。)

    ちなみにDX11のID3D11DeviceContextにはImmediate ContextとDeffered Contextの二種類あったが、ID3D12GraphicsCommandListはDeffered Contextと同じものだ。


    ・コマンドの記録領域 ID3D12CommandAllocator

    ID3D12GraphicsCommandListを作るときとリセットするときは実行したコマンドの記録領域となるID3D12CommandAllocatorが必要になる。

    一つのID3D12CommandAllocatorに対して複数のID3D12GraphicsCommandListが利用できる。
    が、スレッドフリーではないので各スレッドごとに一つのID3D12CommandAllocatorを作る形になる。

    ID3D12CommandAllocatorはResetしない限りメモリを延々と確保し続ける。気が付いたらテクスチャよりメモリを取っていたことも出かねないので、必要な分のコマンドを記録したらResetしないといけない。

    ただし、ResetはGPUがID3D12CommandAllocatorが確保したメモリを使っていないときにしかできない。つまり、ID3D12CommandAllocatorにコマンドを記録しているID3D12GraphicsCommandListの内容をGPUが実行しているときはID3D12CommandAllocatorのリセットはできないので注意が必要だ。

    また、ID3D12CommandAllocatorにもID3D12GraphicsCommandListと同じ数の種類がある。
    同じ種類のID3D12CommandAllocatorは同じ種類のID3D12GraphicsCommandListしか使用できない。

    ・GPUへコマンドを発行する ID3D12CommandQueue


    実際にGPUへコマンドを発行するにはID3D12CommandQueueを使用する。

    ID3D12CommandQueueは基本的にID3D12GraphicsCommandListを受け取り、GPUへ発行する役割をもつ。

    あとはタイムスタンプの取得、Reserved Resourceと呼ばれる仮想GPUメモリへのコピー/更新機能がある。

    ID3D12CommandQueueにもID3D12GraphicsCommandListと同じく種類があり、以下の3種類だ。
    • D3D12_COMMAND_LIST_TYPE_DIRECT : 全てのコマンドが使用できる。グラフィックス関係に使われるのがほとんど。
    • D3D12_COMMAND_LIST_TYPE_COMPUTE : コンピュートシェーダとコピー関連のコマンドを実行できる。使っているデバイスによってはグラフィックスパイプラインと同時に実行できるものがあり、非同期コンピューティングと呼ばれている。
    • D3D12_COMMAND_LIST_TYPE_COPY : コピーコマンドを実行できる。GPUとCPU間を高速でデータを転送するPCI Expressに最適化されている。
    使用しているデバイスによっては種類が異なるID3D12CommandQueueを同時に実行できる場合がある。この特性を生かさないとDX11より性能を引き出すことはできない。

    特にコンピュートシェーダとグラフィックスパイプラインを並列で動作させることを非同期コンピューティングと呼び、最適化する上で重要な意味を持つ。

    ただ、AMDのGPUが最初に対応した歴史から(GCNアーキテクチャから)、AMD以外のGPUでは思ったより性能が出ないこともある。NVIDIAもPascal世代(GTX10XX)以降から対応しているが、AMDのものよりかは効果は出ない。

    (NVIDIAはVR向けのSingle Pass Stereo機能を先に実装したりと、進化の方向が異なっているので、AMDと比べてどちらが優秀とは言えない。)

    非同期コンピューティングは使用しているデバイスの影響を受けやすいものであることは覚えておいた方がいい。



    以上でGPUにコマンドを発行するために使用するものについてみてきた。
    がこのままだと、GPUがいつ処理を終えたのかがわからない。

    なので、次はID3D12Fenceについて見ていく。

    ・同期処理


    GPUに発行した処理が終わったことを確認するにはID3D12Fenceを使用する。

    上記のインターフェイスを使うことでGPUにコマンドを発行できる。
    しかし、このままだとGPUがその発行したものをいつ処理し終えたかは確認することはできない。

    このままだと、どのタイミングで前フレームの描画が完了したかわからず、でたらめのタイミングでID3D12CommandAllocatorをリセットせざる終えない。

    いわゆる同期待ちができない状態である。

    そのためまだGPUが使っていないのにコマンドが消えてしまったというNULLポインターエラーっぽいバグが発生してしまう。(しかも、どのタイミングで起きるかわからないおまけ付きで)

    このような場合のためにID3D12Fenceが用意されている。

    同期待ちをしたいコマンドを記録したID3D12CommandAllocatorをID3D12CommandQueueで発行した後にID3D12CommandQueue::Signal関数にID3D12Fenceを渡してあげることで、同期待ちのタイミングを作ることができる。

    同期待ちにはWindowsAPIのWaitForSingleObject関数を利用する。

    そのためID3D12FenceはWinAPIのCreateEvent関数で作成したHANDLEと一緒に作成する必要がある。

    //ソースコード
    
    //ID3D12Fenceの作成
    ID3D12Fence fence;
    auto hr = device->CreateFence(initialValue, fenceFlag, IID_PPV_ARGS(&fence));
    HANDLE handle = CreateHandle(nullptr, FALSE, FALSE, nullptr);
    
    // 実行したいコマンドを作成
    
    ...
    commandQueue->ExecuteCommandLists(...);
    
    //同期待ちしたいタイミングを指定
    commandQueue->Signal(fence, nextValue);
    
    
    
    //同期待ち
    
    if (fence->GetCompletedValue() < nextValue) {
      auto hr = fence->SetEventOnCompletion(nextValue, handle);
      if (SUCCESSED(hr)){
        WaitForSingleObject(handle, waitMilliseconds/*-1でタイムアウトなし*/):
      } else {
        //エラー発生
      }
    }
    
    commandQueue->Signal()で同期ポイントを作る。

    GPUがこのSignal部分までコマンドを実行すると、指定したfenceの内部カウンターをnextValueに更新する。

    fenceの内部カウンターはID3D12Fence::GetCompletedValue()で取得できる。

    後は内部カウンターの値がnextValueになっていないか確認したら、ID3D12Fence::SetEventOnCompletion()とWaitForSingleObject()を使って同期待ちする。

    余談だが、commandQueue->Signal()はGPU駆動の同期待ちである。
    CPU上からID3D12Fenceを利用して同期待ちしたいときはID3D12Fence::Wait()を使うといい。

    ・ID3D12Fenceの同期待ちはカウンターを使ったもの


    ID3D12Fenceを使った同期待ちはUINT64型のカウンター一つづつ進めていくものになる。
    そのため、一回同期ポイントを作るたびに1つカウンターを進める処理になるため、桁あふれという限界がいずれ来る。

    なので、ある周期ごとにリセットした方がいいのではないかと考えるだろう。

    しかし、UINT64型なら60FPSで毎フレーム当たり1回同期しても、
    一時間当たり21万ほどのカウンタが進むのだが、一年たっても限界には程遠いので問題にはならない。よほど細かく同期しないと限界は来ないと考えた方がいい。


    ・ID3D12FenceはマルチGPU間での同期待ちにも使える


    ちなみにID3D12Fence作成時に渡すD3D12_FENCE_FLAGSによっては複数のGPU間の同期用のID3D12Fenceも作ることができる。