UE4
oga at 18:35
前回の続きです。

・前回: UE4 プログラミング言語 Blueprint (1)


●イベントグラフと関数グラフ

Blueprint 命令の編集画面には 2種類あります。イベントグラフと関数グラフです。

↓イベントグラフの画面。イベント応答、カスタムイベントの定義などを行う。

EventGraph

↓関数グラフの画面。関数単位の定義を行う。

FuncGraph

イベントグラフでは、BeginPlay や Tick といった特定のイベントを受け取るだけでなく、自分でカスタムのイベントを作ることもできます。

関数グラフでは、出力を持った関数の定義が可能です。Pure 関数もこちらの画面で作ります。

イベントグラフや関数グラフで定義した命令はどちらもサブルーチンとして呼び出すことができます。呼び出し方に違いはなく、見た目もほぼ同じです。Blueprint 上は機能差がありますが、C++ API から見ると関数もイベントも同じものです。そのため説明中ではイベントのことも含めて関数と呼ぶ場合があります。

FuncCall

カスタムイベントと関数どちらも親クラスの命令をオーバーロード可能で、どちらもインターフェースの実装を行うことができます。イベントは戻り値を返すことができないので、戻り値がある場合は関数になり、無い場合はイベントが優先されるようです。

Blueprint で最初に触るのはイベントグラフなので、イベントグラフに命令を置いていくだけでも十分プログラムを作ることができます。特にイベントグラフの場合は、1つのグラフに複数のイベントのコードを記述できます。関数グラフでは1関数だけなので、複数まとめて俯瞰しながら作っていく場合もイベントグラフの方が便利になります。



●イベントグラフと関数グラフの大きな違い

イベントグラフと関数グラフの一番大きな違いは、プログラムを途中から再実行できるかどうかにあります。イベントグラフは任意の場所で実行の中断と再開ができます。

その代表例が Delay 命令です。Delay は後続の命令の実行を遅らせるためにその場で一時停止しします。

Delay

↑ FuncA の 1秒後に FuncB を実行

中断した場合、再開できるように途中の状態をどこかに保存して置く必要があります。一般的な言語のクロージャーだと参照している変数の値がキャプチャされるのですが、Blueprint の場合は最初からローカル変数を排除することで仕組みを簡単にしています。

イベントグラフ内ではローカル変数を使うことができません。変数はすべてメンバ変数になるので、中断しても内容は保持されています。その代わりイベントグラフではリエントラントにしづらく再帰呼び出しにも向いていないことになります。

またイベントは関数と違い戻り値を返すことができません。途中で中断した場合はそのまま呼び出し元に返ってくるので、そのタイミングでは戻り値が確定していないからです。結果が必要な場合は、メンバ変数など別の手段を用いることになります。


関数グラフの場合は途中で止めたり再実行することができません。もちろん Delay 系の命令は使えないことになります。

その代わり一度呼び出せば最後まで実行するので戻り値を返すことができます。同様にローカル変数も利用可能です。インスタンスメモリを増やさないし名前の衝突も防げます。再帰呼び出しも容易です。



●中断の仕組みと Delay 命令

イベントグラフの Delay 命令は一見その場で停止して待っているように見えますが、内部では Event (Latent Action) を登録して即座に呼び出し元に戻ります。Action に登録された後続の命令は、あとから異なるコンテキストで実行されます。

もともと Blueprint の関数やイベントは、後続の命令がない場合に暗黙の Return とみなします。

Return

↑後ろの実行ピンがつながっていないと Return 相当。FuncA → FuncB → FuncC の順で実行する。

Delay も全く同じです。Delay の場合は後続の命令と実行線がつながっていても Return になります。

Return2

↑実行順に注意。FuncA → FuncC → 1秒 → FuncB の順で実行する。FuncB は 1秒後に World の Tick から呼ばれる。


一部のフロー制御命令は関数呼び出しに似た働きを内包しています。下記は Sequence ノードの例です。

Sequence

↑FuncB のあと、暗黙の return で次の実行ピン(Then 1)へつながる。FuncA → FuncB → FuncC の順で実行する。

Sequence は複数繋いだ命令実行線を順番に実行します。それぞれの実行線は関数呼び出しと同じ暗黙の Return で終了しています。そのため Delay も Sequence の実行を終了する命令として機能します。

下記のように Sequence を使っても順番に時間待ちをしながら実行することはありません。Delay 命令のたびにすぐ次の実行ピンに飛ぶからです。3つの実行インスタンス FuncA, FuncB, FuncC を同時に Action に登録することになります。(「Sequence の例 A」)


Sequence

↑ 1秒経ってから同じタイミングで FuncA, FuncB, FuncC が呼ばれる


この Sequence の挙動をうまく活用すると、Return の代わりに即時実行線を持った新しい Delay 命令を作ることができます。↓はマクロ "Delay2" です。

Macro Delay2

この Delay2 ↑ は暗黙の Return を行いません。Delay の直後に Then 1 を続けて実行するからです。そのため Delayed を Action に登録したあと Immediate 以降を続けて実行します。実行線が 2分岐しているので、Delay は新しい実行インスタンスを作り出す命令に相当します。

Delay2 で置き換えてみると、既存の命令の挙動もより理解しやすくなります。例えば上に挙げた「Sequence の例 A」は↓次のように Delay2 で置き換えることができます。すべて即時実行線でつながっていることになります。

Delay2

↓もし順番に時間待ちするような Sequence を作りたいなら、次のように実行線の接続先を変更すればよいわけです。

Delay2

試しに、上と同じ働きをする Sequence を MultiGate で作ってみます。↓マクロ "Sequence2" です。

Macro Sequence2

使用例

Macro Sequence2

↑これで 1秒待ちながら 1秒 → FuncA → 1秒 → FuncB → 1秒 → FuncC の順番で実行するようになります。ただし元に戻る線が必要なのであまり使いやすくありません。Sequence 本来の良さが失われてしまうため、これだけだとあまり意味がないかもしれません。



● Loop と非同期処理

Sequence だけでなく、関数呼び出しに似た構造を持っているフロー命令は他にもあります。ForLoop や ForEach などのループ命令です。それもそのはずで、ループ命令はマクロで定義されており内部で Sequence が使われています。

一回のループは暗黙の return で終了します。よって Delay も「即座に次のループに進む命令」として機能することになります。時間待ちしながらループするような処理を作ることができません。

Loop

↑ループ自体は同一フレームで完了し、FuncA → FuncC を呼び出したあと、1秒後 に FuncB が「一回だけ」呼ばれます。Delay はインスタンスのノード毎に Action を多重登録できない仕組みなので、ループ回数に依存せず FuncB は一度しか呼ばれません。(Sequence の例では Delay ノードが 3個あったので 3回呼ばれます)

↓Sequence2 と同じ方法で Delay 待ちしながら loop 実行できる命令を作ってみます。マクロ "ForLoopWithBreak2"

Macro ForLoopWithBreak2

↑これを使うと時間待ちしながら Loop できるようになります。

使用例

Loop2

↑ FuncA → 1秒 → FuncB → 1秒 → FuncB → 1秒 → FuncB → FuncC

LoopBody の最後を必ず Next pin に繋げる必要があります。Sequence2 と違い、この手法はいろいろ応用できます。


中断機能付き Delay を作ってみます。

Delay3

Delay のように時間待ちしますが、内部ではポーリングして条件が成立したら即時終了します。例えば何らかのボタンを押したらスキップして次に進むようなイメージです。上の例では 10秒待ってから FuncB に進みますが、その間に [Q] key が押されるとすぐに FuncC を実行します。

中身は下記の通りです。↓マクロ "Delay3"。

Macro Delay3


もう少しシンプルにして、async/await のような非同期待ち命令を作ってみます。ポーリングなので効率は良くないですが、一連の非同期処理を並べて書けるようになります。↓マクロ "Await"

Macro Await

Interval ごとに Exp をポーリングし、結果が True になるまで待ちます。True になった時点で Completed に進みます。途中で Break が True になった場合は Breaked の方に進みます。何かを待ちながら処理を継続するようなケースを簡単に書くことができます。

必要になることは無いと思いますが、例えば RPC でサーバーを呼び出したあと結果を待つなど。(RepNotify OnRep の代わり)

null

例えば Actor が目的地まで移動し、その後アニメーションを再生、最後にアニメーション流しながら音声が終わるのを待つような処理を考えます。時間のかかる処理を順番に実行しますが、下記のように直列に書けます。

Await

動作効率は良くないですが、パフォーマンスよりも見た目でわかりやすい方を優先したい、といった場合に使えます。C++ だけだとこういうコードを書けないので、スクリプト言語らしくて面白いと思います。



●イベントと Callback

イベントグラフでは、イベントノードから赤い線を引っ張ることで関数オブジェクトを渡せるようになります。登録したイベントは Callback として呼び出されます。

下記の例では TimerEvent (FuncA) が一定時間ごとに呼ばれます。

Timer2

本来想定されている非同期処理のしかたはポーリングではなくこちらです。イベントグラフで複数のイベントをまとめて定義できる理由もわかります。

Event Dispatcher を使えば、Blueprint から Callback 関数を呼び出すこともできます。下記は "Delegate" という名の Event Dispatcher に Delegate_Event を登録し、直後に呼び出しています。

Delegate

↑ FuncA → FuncB → FuncC の順で実行

Event Dispatcher は C# でいう delegate のことです。UE4 でも C++ API では Delegate と呼ばれています。Blueprint の Event Dispatcher は UE4 C++ API の Dynamic Multicast Delegate に相当します。Event Dispatcher はインターフェースが一致していればよいので、呼び出し側が相手の class の詳細を知っている必要がありません。参照の依存を断ち切る働きもあります。

順番に非同期待ちを行う例に Event Dispatcher を使ってみます。各動作が最後に NextAction を呼び出しているものとします。

Dispatcher

↑最後は 2つの動作を待っているため NextAction が2回呼ばれるのを待っています。

Dispatcher への登録が長いのでマクロで置き換えてみます。また、指定回数呼ばれてから次に進むノードも作ってみます。

↓マクロ "BindEventM"

Macro BindEvent

↓マクロ "CounterMacro"

Macro CounterMacro

↓マクロを使ってシンプルになりました。

Dispatcher3

同じように NextEvent を待つ Await3 も作ってみます。↓マクロ "Await3"

Macro Await3

使用例

Await3

こちらもある程度簡単にできました。さらに回数のカウントも自動化できそうです。Blueprint も色々工夫できるので面白いのではないかと思います。

次回に続きます。


関連エントリ
UE4 プログラミング言語 Blueprint (1)
4倍速い Ryzen 9 3950X の UE4 コンパイル速度
UE4 UnrealBuildTool の設定 BuildConfiguration.xml
UE4 UnrealBuildTool VisualStudio の選択を行う
UE4 UnrealBuildTool *.Build.cs のコードを共有する



UE4
oga at 23:00
UE4 ではスクリプト言語として Blueprint を使った開発ができます。Visual 言語となっており、画面上に命令を並べて線でつなぐだけで動作フローを作成することができます。

BP_Prog01

三角マークでつながった白い線が実行順序を表しています。それ以外の丸い終端でつながっている線は、引数や返り値といったデータの流れです。データの型によって線の色が変わります。Blueprint のプログラムはコードではなくグラフと呼ばれます。

高レベルな命令をバッチのように並べるだけでも良いし、Native 言語のように低レベルな命令を駆使して計算やフローを直接記述することもできます。非常に自由度が高くなっており、修正してすぐ Editor 上で実行できるので便利です。

すべて Blueprint だけで記述する必要はなく、C++ と密な連携ができる仕組みになっています。C++ で定義した class を Blueprint で継承したり interface を実装することも可能。命令も C++ で増やせるので、基礎部分やパフォーマンスが必要な処理を C++ で実装し、それらを Blueprint でまとめあげるような役割分担も可能です。


●関数ノードと Pure 関数ノード

白い線で繋いでいくのが通常の関数ノードです。一般的な命令の実行に相当します。線で繋いだ方向に順次呼び出されていき、順番が勝手に入れ替わることはありません。呼び出し回数も白い実行線の接続によって決まります。手続き型言語と同じです。

null

白い実行線を使わずに、引数と返り値だけを持っているのが Pure 関数ノードです。定義時にプロパティの「純粋」にチェックを入れると Pure 関数になります。C++ で作る場合は UFUNCTION() の中に BlueprintPure を入れます。引数と返り値だけを繋いでいくので、Pure 関数ノードの挙動は関数型言語によく似ています。

null

C++ 風の書き方に展開すると違いがわかります。通常の関数ノードの例を展開すると下記のとおり。返り値は必ず一時変数を経由して次の命令に渡されます。

int tempA= FuncA();
int tempB= FuncB( tempA, 3 );
PrintInt( tempB );


Pure 関数ノードの場合は、変数を経由しないで引数にそのまま渡しているケースに相当します。B の例を展開すると下記のとおりです。

PrintInt( FuncB( FuncA(), 3 );


Blueprint では返り値を返す関数の場合、通常の関数か Pure 関数か選ぶことができます。プロパティの参照のなど実行順序を気にしなくてもよい場合は、いちいち実行線を繋がなくてもすむ Pure 関数は便利です。

その代わり Pure 関数ノードは実行順番が明確ではなく、呼び出し回数も直感的な見た目と一致しません。そのため副作用を持つ Pure 関数ノードを作る場合は注意が必要です。

例えば下記の例は、深さ優先で考えると A->D->C->B の順序で呼ばれるような気がしますが、実際に走らせてみると FuncA->FuncC->FuncD->FuncB で呼ばれていました。

null

LogBlueprintUserMessages: [Prog03_2] Pure Func A
LogBlueprintUserMessages: [Prog03_2] Pure Func C
LogBlueprintUserMessages: [Prog03_2] Pure Func D
LogBlueprintUserMessages: [Prog03_2] Pure Func B


Native 変換しても A,C,D,B の順序で呼ばれていることがわかります。

void AProg03_C__pf3730294777::bpf__PureFunc__pf(int32 bpp__A__pf, int32 bpp__B__pf, /*out*/ int32& bpp__R__pf)
{
    int32 bpfv__CallFunc_PureFuncA_R__pf{};
    int32 bpfv__CallFunc_PureFuncC_R__pf{};
    int32 bpfv__CallFunc_PureFuncD_R__pf{};
    int32 bpfv__CallFunc_PureFuncB_R__pf{};
    bpf__PureFuncA__pf(/*out*/ bpfv__CallFunc_PureFuncA_R__pf);
    bpf__PureFuncC__pf(bpp__B__pf, /*out*/ bpfv__CallFunc_PureFuncC_R__pf);
    bpf__PureFuncD__pf(bpp__A__pf, bpfv__CallFunc_PureFuncA_R__pf, /*out*/ bpfv__CallFunc_PureFuncD_R__pf);
    bpf__PureFuncB__pf(bpfv__CallFunc_PureFuncD_R__pf, bpfv__CallFunc_PureFuncC_R__pf, /*out*/ bpfv__CallFunc_PureFuncB_R__pf);
    bpp__R__pf = bpfv__CallFunc_PureFuncB_R__pf;
}



●Pure 関数ノードと呼び出し回数

乱数の値を表示するプログラムを書いてみます。組み込み関数の Random Integer を使います。これは Pure 関数です。同じ Random Integer から値を取り出して 2回表示してみます。

null

実行結果
LogBlueprintUserMessages: [None] 62
LogBlueprintUserMessages: [None] 23


同じノードから線を引っ張っているにもかかわらず、2回とも異なる値が表示されています。画面上には 1個しか置いていないのに「Random Integer」は2回呼ばれているわけです。

「Random Integer」と同じ機能を持つ、「Pure ではない」通常の関数を作ってみます。

null

実行結果
LogBlueprintUserMessages: [None] 20
LogBlueprintUserMessages: [None] 20


今度は 2回とも同じ値が表示されており、見た目通り一回しか呼ばれていないことがわかります。

C++ 風の書き方で違いを表現すると下記のとおりです。

Pure 関数の場合

PrintInt( RandomInteger( 100 ) );
PrintInt( RandomInteger( 100 ) );


通常の関数ノードの場合

int tempA= RandomInt( 100 );
PrintInt( tempA );
PrintInt( tempA );


通常の関数は白い線でつながった順番で一度しか実行せず、また返り値は必ず一時変数を経由します。そのため同じ値を何度も参照することが可能です。

Pure 関数ノードの場合は、同じノードでも「通常の関数が結果を参照」した回数だけ毎回呼び出しが行われます。

もし Pure 関数で乱数の結果を再利用したい場合は、自分で変数に保存しておく必要があります。

null

また、通常の関数ノードの返り値が暗黙の一時変数に束縛されるという性質を利用することもできます。まず、何もしないすダミー関数 TempInt を作っておきます。

null

引数をそのまま返しているだけでですが、通常関数は返り値が一時変数に束縛されるので何度も参照できるようになります。

null

この場合も同じ乱数値を表示します。

このように Pure 関数は実行回数が見た目と異なるので、意図しないところで何度も呼び出してしまう可能性があります。負荷の高い命令や副作用のある命令は Pure 関数にしない方が良いでしょう。


●返り値を複数もつ Pure 関数

全く同じように、複数の値を返す Pure 関数も問題になりがちです。下記のように、一度に Position と Rotation を返す Pure 関数 GetPosition を作ります。

null

返り値を 1度しか参照していないように見えるのですが、この場合も GetPosition 関数は 2回呼ばれます。一度に複数の値をまとめて返した方が一見効率が良さそうな気がしますが、必ずしもそうではありません。

例外もあります。2つの返り値を、同じ命令の引数で同じタイミングで受け取る場合は 1回しか呼ばれません。下記の場合 GetPosition が呼ばれるのは一回だけになります。

null

構造体の展開も同様です。Blueprint では、構造体を展開して要素ごとに個別に参照することができます。(右クリックメニューから展開できます)

null

展開した構造体の値を、下記のように別の命令から個別に参照した場合も複数回呼ばれます。

null

例えば現在時刻を表示する時計のプログラムを作ってみます。現在時刻の取得は組み込み命令の Now です。これは Pure 関数です。

null

上の例では時、分、秒毎に区切って数字の描画を行っています。Now の結果である 時、分、秒 をそれぞれ一回ずつしか参照していないのに、Now はその都度毎回呼ばれてしまいます。

つまり、時、分、秒の描画時にそれぞれ異なる時刻を見ていることになります。タイミングによっては分を表示した直後に秒が桁上りして、秒だけが先に 00 になる場合があり得えます。表示だけならまだしも、アラームのように時刻を厳密に判定したい場合は問題になるでしょう。

10:05:59
  ↓
10:05:00
  ↓
10:06:00


上のようなケースでは、Now の呼び出しを最初の一回だけにして、予め変数に保存しておく必要があります。


・負荷の高い命令、副作用のある命令は Pure 関数にしない。

・複数の値を返す場合も、Pure 関数を避けた方がよい。

・動的に変化する関数の場合、同じ値を複数参照するには通常の関数にするか事前に保存する。


ちなみにゲームで管理している時間 (tick や GetGameTime) はフレーム単位で固定なので、同一のフレームであれば同じ値です。何度呼び出しても問題ありません。



●同じ Pure 関数ノードが再利用される期間を詳しく調べる

複数値を返す Pure 関数でも、同じ命令の引数から同時に参照される場合は一度しか呼ばれませんでした。この挙動をもう少し詳しく調べてみます。

下記の例は、同一式内の同一引数の複数回参照です。このケースでは Random Integer が一回だけ呼ばれて、必ず同じ値を加算した結果を返します。つまり表示される値は常に偶数です。

null

次のように Random Integer ノードを 2個置くとそれぞれ 1回ずつ呼ばれます。つまり乱数は 2回生成され、異なる乱数値の和を返します。もちろん表示される値が奇数になる場合もあります。

null

同じ命令の引数でなくても構いません。下記のように、同じ式であれば異なる場所から参照しても GetPositon2 は一度しか呼ばれません。

null

↓もちろん 2回書けばそれぞれの GetPosition2 が個別に呼ばれます。

null

↓1つの加算から GetPosition2 が 2回参照されていますが、同じ式なので一度しか呼ばれません。ただし異なる SetActorLocation から 2回参照されているので、結局 GetPosition2 は合計 2回呼ばれることになります。

null

↓複数の引数から同じノードを参照しています。式グラフとしては異なりますが、白い実行線を通過する前なので GetPosition2 は一度しか呼ばれません。

null

まとめると

・同じ式の中では、同じノードの Pure 関数を複数参照しても一度しか呼ばれない。

・白い実行線を通過することでリセットされる。同じノードでも再び Pure 関数が呼ばれる。


また同一の式グラフの中で用いる場合は、Pure 関数のノードは共有してできるだけ同じ場所から線を引いた方が効率が良いこともわかります。メモリも呼び出し回数も節約できます。

もちろんプロパティ参照のような単純な命令では全く気にする必要は全く無いと思います。それでも Now 命令のようにバグの原因となる場合もあるので、挙動はある程度理解しておいた方が良いでしょう。


●通常の関数ノードの実行回数

Pure ではない通常の関数ノードは、白い線の接続で呼び出し回数が明示的に決まると書きましたが例外があります。class object のメソッド呼び出しの場合、Target に複数のオブジェクトを与えると複数回実行を繰り返します。

下記の例では ActorA と ActorB それぞれの PrintActorPosition メソッドを呼び出します。つまりグラフ上のノードは 1つでも 2回実行します。

null

Target は関数を呼び出す対象のオブジェクトで、C++ の this pointer に相当します。戻り値がある場合、一時格納変数が共有されるので最後に呼ばれた値が有効となるようです。呼び出し順番は不定です。

次回に続きます。


関連エントリ
4倍速い Ryzen 9 3950X の UE4 コンパイル速度
UE4 UnrealBuildTool の設定 BuildConfiguration.xml
UE4 UnrealBuildTool VisualStudio の選択を行う
UE4 UnrealBuildTool *.Build.cs のコードを共有する


Windows 10 May 2020 Update (2004) で WSL2 がリリースされました。

Microsoft: WSL 2 の新機能

I/O が速くなっているらしいので、いつも Android + Termux でテストしている「コンパイル時間の計測」をしてみました。

PC WSL1 WSL2
Core i7-6700K 4.0GHz (4C8T) 40s 29s
Ryzen 7 1800X 3.6GHz (8C16T) 26s 21s

・「WSL1」「WSL2」はビルド時間で単位は秒。値が小さい方が高速
・Windows 10 + WSL (Ubuntu 18.04LTS) clang 8.0 での比較

WSL1 にくらべて WSL2 の方がだいぶ速くなっています。Android の UserLAnd と Termux の関係に似ているかもしれません。

さらに直接 install した Linux と比較してみました。

PC clang WSL1 WSL2 Linux
Core i7-6700K (4C8T) clang 8 40s 29s 27s
Core i7-6700K (4C8T) clang 6 40s 28s 26s

・「WSL1」「WSL2」「Linux」はビルド時間で単位は秒。値が小さい方が高速
・WSL = Windows 10 + WSL (SATA SSD: MX500 500GB)
・Linux = Ubuntu 18.04LTS (SATA SSD: TS256GMTS400)

WSL2 のビルド時間が Linux を直接 install した場合に近くなっています。

なおテストは同じ PC を使っていますが、Windows と Linux で使用している SSD が違うので同一条件になっていません。速度差は OS の違いではなく SSD 性能差の可能性もあります。


以下スマートフォン他との比較

Device OS clang ビルド時間
Ryzen 9 3950X 3.5GHz WSL1 8 10s
Ryzen 7 1800X 3.6GHz WSL2 8 21s
Ryzen 7 1800X 3.6GHz WSL1 8 26s
Core i7-6700K 4.0GHz Ubuntu 18.04 8 27s
Core i7-6700K 4.0GHz WSL2 8 29s
Core i7-4790K 4.0GHz Ubuntu 18.04 8 31s
Google Pixel 3 Snapdragon 845 Termux 8 35s
Core i7-6700K 4.0GHz WSL1 8 40s
Essential Phone PH-1 Snapdragon 835 Termux 8 40s
Google Pixel 3 Snapdragon 845 UserLAnd 8 51s
Essential Phone PH-1 Snapdragon 835 UserLAnd 8 62s
A10-7870K 3.9GHz Ubuntu 18.04 8 69s
Huawei P30 Lite Kirin 710 Termux 9 71s
Huawei P30 Lite Kirin 710 UserLAnd 8 85s
Jetson Nano Tegra X1 Ubuntu 18.04 8 118s
ZenFone AR ZS571KL Snapdragon 821 Termux 8 125s
Raspberry Pi 4 BCM2711 (arm64) Ubuntu 19.10 8 146s
Nexus 5X Snapdragon 808 Termux 8 178s
Raspberry Pi 4 BCM2711 (armv7l) Raspbian 10 8 203s
Raspberry Pi 3 BCM2837 (arm64) Ubuntu 18.04 8 340s

・「ビルド時間」の単位は秒、値が小さい方が高速
・clang = clang の Version (コンパイラの Version によって速度が変わるので注意)

I/O 速度を見ると WSL2 だけで十分な気もしますが、WSL1 の方が手軽で便利な点もあります。例えばネットワークの場合 WSL2 には内部のローカルな IP Address が割り振られます。サーバーを立てて外部の PC からアクセスしたい場合、WSL1 は簡単に実現できますが WSL2 では外から直接見えなくなります。VirtualBox などの仮想マシンと同じで、port forwarding の設定が必要になるようです。

WSL1 同様、WSL2 からも Windows の exe を呼び出すことができました。つまり WSL 上で ssh server を起動しておき、Linux 側の ssh 経由で cmd.exe を実行することもできます。この辺が VirtualBox のような閉じた仮想マシンと異なるところです。

しばらくは使い分けながら併用してみたいと思います。


関連ページ
Compile Benchmark


関連エントリ
4倍速い Ryzen 9 3950X の UE4 コンパイル速度
Jetson Nano / Clang の Version とコンパイル速度の比較
Snapdragon 835 と 845 のコンパイル時間の比較&浮動小数点演算能力
Snapdragon 845 の浮動小数点演算速度
ARM CPU 上の開発環境とコンパイル時間の比較 (2) Pixel 3/UserLAnd
ARM CPU 上の開発環境とコンパイル時間の比較
AMD CPU Ryzen とコンパイル時間の比較 (2)
AMD CPU Ryzen とコンパイル時間の比較
ARM CPU の浮動小数点演算能力まとめ


Debian が Buster になっているので、1年前と比べて mozc の install が簡単になりました。Ubuntu と同じ手順でインストールできます。Wiki の手順を更新しました。

SSH + uim-mozc
VNC + fcitx-mozc

UserLAnd は Android 端末内に Linux 環境を構築するためのアプリです。Chromebook (ChromeOS) が Linux アプリに対応したり Windows 10 が WSL をサポートするのと同じように、共存しながら Android 上で Linux のソフトが走るようになります。

GitHub: UserLAnd


● lxde + VNC で mozc の環境を作る

◎ 1. UserLAnd を起動したら Debian または Ubuntu を選択。

◎ 2. Username, Password, VNC Password を入力して SSH を選択する

◎ 3. Terminal にログインして下記のスクリプトを実行

#!/bin/sh
. /etc/os-release

if [ "$ID" = "ubuntu" ];then
sudo apt update
sudo apt -y upgrade
sudo apt -y install lxde
sudo apt -y install language-pack-ja
sudo update-locale LANG=ja_JP.UTF-8
sudo apt -y install fcitx-mozc
sudo dpkg-reconfigure tzdata
fi

if [ "$ID" = "debian" ];then
sudo apt update
sudo apt -y upgrade
sudo apt -y install lxde
sudo apt -y install task-japanese
sudo apt -y install task-japanese-desktop
sudo dpkg-reconfigure locales
. /etc/default/locale
sudo apt -y install fcitx-mozc
sudo apt -y install dbus-x11
sudo dpkg-reconfigure tzdata
fi

if [ "$GTK_IM_MODULE" = "" ]; then
cat >> $HOME/.profile <<END
export GTK_IM_MODULE=fcitx
export QT_IM_MODULE=fcitx
export XMODIFIERS=@im=fcitx
export DefaultIMModule=fcitx
END

fi


ファイルに保存してから下記のように実行する。時間がかかります。

$ sh ./userland_fcitx.sh


◎ 4. 途中でいくつか選択肢あり

・Debian か Ubuntu かによって若干違います。選択例は Debian の場合です。

・[More] が出たら Enter (改行) キーで進めてください。

・以下選択例です。必ずしも下記の通りでなく環境に合わせて選んでください。(番号が異なっている場合があります)

Keyboard layout: 21  (Other)
 ↓
Country of origin for the keyboard: 55  (Japanese)
 ↓
Keyboard layout: 6  (Japanese - OADG 109A)

Users to add to the netdev group: 1

Locales to be generated: 285  (ja_JP.UTF-8 UTF-8)
 ↓
Default locale for the system environment: 3  (ja_JP.UTF-8)

地理的領域: 6  (アジア)
 ↓
時間帯: 79  (東京)


◎ 5. 解像度を選択します。

~/.vncrc に解像度が書き込んであるので任意の値に変更してください。

ハイエンドスマートフォンの場合、デフォルトだと文字が小さすぎる場合があるので最初に変更しておくことをおすすめします。

$geometry = "1280x720";


◎ 6. 一旦 exit

終了して UserLAnd のメイン画面に戻ります。

$ exit


◎ 7. SSH から VNC に変更して起動し直します。

 ・UserLAnd のメイン画面に戻ったら、Debian (または Ubuntu) を長押しして「Stop App」

 ・もう一度同じ場所を長押しして「App Info」を選択し、VNC を選択 (SSH → VNC)

 ・UserLAnd のメイン画面から Debian (または Ubuntu) を起動


◎ 8. bVNC Free をまだインストールしていない場合はストアに飛ぶのでインストール

 インストール完了したらもう一度 UserLAnd から起動する


◎ 9. VNC が起動するが lxde のデスクトップにならない場合は一旦手動で起動する

 コンソールから lxsession を実行する。

$ lxsession &

エラーダイアログが1つ出ますが無視して構いません。

また下記の内容で ~/.xsessionrc を作っておく。

. ~/.profile
lxsession &


◎ 10. メニューから LXTerminal を開く

 左下のアプリケーションメニューから「システムツール」→「LXTerminal」


◎ 11. LXTerminal 内で fcitx-autostart を実行

$ fcitx-autostart


◎ 12. fcitx の設定の確認

・左下のアプリケーションメニューから「設定」→「Fcitx 設定」

・「全体の設定」タブ→「入力メソッドのオンオフ」で任意のキーを選択する

・Android 9.0 以降は CTRL + SPACE が使えないので注意。

・切り替えキーが正しく反応するかどうかは LXTerminal 上で確認できます。


●その他

もし fcitx が自動で立ち上がらない場合 (毎回手動で fcitx-autostart を呼び出さないと日本語が入力できない場合) は、自動起動に登録してください。

・左下のアプリケーションメニューから「設定」→「LXSession のデフォルトアプリケーション」
・「自動立ち上げ」タブ→ 「+Add」の欄に "fcitx-autostart" を入れてから [+Add]

細かいメニューボタンの選択は、bVNC Free の「入力モード」→「タッチパッドシミュレーション」が便利です。


関連ページ
HYPERでんち: UserLAnd

関連エントリ
Android UserLAnd で PyTorch を使う。C++ API
Huawei P30 Lite/Fire HD 10(2019) のコンパイル速度と UserLAnd
Android: UserLAnd + Termux を Note PC 代わりに使う
Oculus Quest も文章書き&開発マシンにする
Android UserLAnd の更新と VNC 画面設定
UserLAnd : Android 9.0 で Ctrl + SPACE を使えるようにする
Android Termux で日本語入力を行う / UserLAnd との併用
Android 9.0 と Bluetooth Keyboard による日本語入力
Android で動く Linux 環境 UserLAnd が XServer XSDL に対応
Oculus Go を文章書き&開発マシンにする


数ヶ月走らせていますが安定して動いています。時間を意識してドアを慌てて閉めなくても良くなり、出入りのときに余裕ができました。

● Jetson Nano (5W mode)

作成時は Windows PC を使っていましたが、コード修正無しにそのまま動くことがわかったので今は Jetson Nano を使っています。Web カメラも PC で使っていたものそのままで USB 接続です。

常時起動しているので Jetson Nano は少電力モード (5W モード) に切り替えました。ワットモニターで計測したら本当に上限 5W でした。


● Logicool HD C270

使用しているカメラは Logicool の C270 です。室内の撮影は全体的に暗いので、わかりやすいように明るさを調整しています。Windows の場合標準のカメラ アプリの設定から「プロモード(フォト&ビデオの詳細コントロールを表示)」を有効にすると明るさの調整ができるようになります。Jetso Nano では v4l-utils を使っています。

$ v4l2-ctl -d /dev/video0 -c brightness=160


●判定の改良など

ロック中に更にロックコマンドを送信するのは問題がありそうだったので、事前に状態を確認するように変更しました。これで誤認識の度に lock コマンドを発行してしまう問題がなくなります。状態を確認する分だけ若干反応は遅れます。

・状態の確認
・結果の 'locked' が false の場合のみ 'lock' command 送信

ただし稀に正しく動かないケースがあります。鍵があいているのに 'locked' に true が返っているようです。手で直接ドアの鍵をあけた場合に起こるので、おそらく「解錠→ドア閉め」の時間が短すぎて、同期される前に状態を確認しているためだと思われます。


◎解錠からドア閉めまでの時間が極めて短い場合

人間 Sesame Server 開け閉め判定プログラム
手で解錠 解錠 true
ドアを開ける 状態の同期 開いたことを認識してフラグを立てる
ドアを閉める 閉めたことを認識。locked → true が返る。何もしない
同期完了 false
↑同期が完了する前に Server の状態を調べてしまう

確実に対処するなら 'sync' コマンドが必要になるようです。

・'sync' command 送信、状態をサーバーに同期する
・状態の確認
・結果の 'locked' が false の場合のみ 'lock' command 送信

ただ sync はあまり頻繁に呼ばない方が良いらしく、この手順だと誤認識の度に何度も sync してしまう可能性があります。気持ちゆっくりドアを開ければ今のところ問題ないので sync は一旦保留としました。


●解錠コマンド

判定を遅らせる代わりに解錠のタイミングを早める方法もあります。例えば直接手で鍵を開けないで、スマートスピーカーの音声コマンドで鍵を開けるようにすると十分な同期時間を稼げます。

せっかく認識用のカメラがあるので、オートロックと同じように何らかのハンドサインを認識して鍵を開けることもできそうです。

実際に解錠用の判定コードを追加してみたのですが、テストで走らせるにはかなりリスクが高いことがわかったので諦めました。施錠 (lock) だと誤動作してもさほど影響がないのですが、解錠 (unlock) の誤動作は問題になります。認識率の問題もありますし、プログラムのちょっとしたバグでも致命的になるかもしれません。時間をかけて検証する必要があります。

Amazon Echo (Alexa) のスマートスピーカーと連動した場合も、解錠は音声コマンドだけでなく合言葉(暗証番号)も必要な仕様となっていました。やはり解錠の自動化は慎重になった方が良さそうです。


●学習データ

判定対象となった画像を自動保存します。その中から定期的に判定が間違っているものだけ追加しておりデータは徐々に増えています。学習には時間がかかるので従来どおり PC を使っています。



関連エントリ
セサミmini、Web Camera でセンサー対応オートロックを作ってみる (PyTorch)
セサミmini、スマートロックを使って1年


| 次のページ(日付が古い方向)>>