Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 8901

RustでUnityプラグインを作って敗北する

$
0
0

なんか最近流行ってるらしい

浮世の変化には疎いのですが、なんか流行ってるらしいですねRust
実はUnityのネイティブプラグインを作ってみたかったのですが、CC++もやったことない上に勉強する気もないため踏み切れないでいました。
いい機会なのでRustで作ってみます。

目的

  • Rustを使ってみる
  • Unityのネイティブプラグインを作ってみる
  • 自分の学習軌道をメモしておく

書いている人

スマホ開発がメイン
CC++は未経験
値渡しと参照渡しはわかるけどぽいんた? とかいうのはわからん

情報取得

とりあえずインプットします。

Rust

Rustの日本語ドキュメント/Japanese Docs for Rust
プログラミング言語 Rust, 2nd Edition/ The Rust Programming Language, Second Edition
プログラミング言語Rust
The Rust Programming Language

必修言語Rustの他己紹介
Rust についてのメモ
Rustのポインタ(所有権・参照)・可変性についての簡単なまとめ
Rustはこうやって勉強するといいんじゃないか、という一例
Rustのクレート・ツールを探すための情報源

コンパイル通るまで殴ればいい! かんたん!
こんなにやさしいコンパイラ初めて見ました。cargoによる依存関係の管理もよく練れていていい感じです。
Kotlinなんかもそうですが近代の言語はユーザ獲得のため、チュートリアルがおそろしく充実していていいですね。
あとプロジェクト新規作成したらgit作ってくれるのすごい。

NativePlugin

Rustで実装したアルゴリズムをUnityから使う
C#からC++(DLL)に配列を渡す
How do I get Rust FFI to return array of structs or update memory?
How to return an array of structs from Rust to C#

プラグイン更新のたびにUnityEditorの再起動が必要とかつらい。

エディタ

intellij製品じゃないともうなにも書けない。
MEET INTELLIJ RUST

Vector3を100倍して返す

ひとまず手始めとして、C#から渡されたVector3Rust側で100倍して返します。

とりあえず作る

Rust側
// こんな感じで構造体を定義#[repr(C)]pubstructVector3{x:f32,y:f32,z:f32,}// 外部公開する関数#[no_mangle]pubexternfnsize_up(v:&Vector3)->Vector3{Vector3{x:v.x*100.0,y:v.y*100.0,z:v.z*100.0}}
C#側
[DllImport("libtest")]privatestaticexternVector3size_up(Vector3moto);

さっそく実行してみましょう。

テストコード
varv3=UnityEngine.Random.insideUnitSphere;varsizeUpV3=size_up(v3);Debug.Log($"{v3} -> {sizeUpV3}");
実行結果
(-0.1, -0.8, -0.2) -> (254563.7, 0.0, 1402342000000000000000000.0)

……なんか……なんだろう、よくないことが起こっているようですね。

借用をやめる

というわけで、Rust側を修正します。

引数を修正
#[no_mangle]pubexternfnsize_up(v:Vector3)->Vector3{Vector3{x:v.x*100.0,y:v.y*100.0,z:v.z*100.0}}

引数のv: Vector3をうっかり拝借していましたが、Rust内で完結するならばともかくC#から借りてくるのはあまりにも無茶な話でした。というわけでそのまま渡してみます。

実行結果
(-0.3, -0.7, 0.2) -> (-26.1, -66.8, 21.3)

……またよくないことが……いや、よく見たら四捨五入してそうな数字です。

nicely formatted

Rider先生にデコンパイルしてもらってVector3ToStringを覗いてみます。

Vector3.ToString
/// <summary>///   <para>Returns a nicely formatted string for this vector.</para>/// </summary>/// <param name="format"></param>publicoverridestringToString(){returnUnityString.Format("({0:F1}, {1:F1}, {2:F1})",(object)this.x,(object)this.y,(object)this.z);}

nicely formatted string

${\Large なに言うとるがじゃ!!!!!}$

しょうがないのでこんな感じの拡張メソッド定義してありのままの姿を見せてもらうことにします。

ToStringFloat
staticclassExtensions{publicstaticstringToStringFloat(thisVector3vector3){return$"({vector3.x}, {vector3.y}, {vector3.z})";}}
テストコード
varv3=UnityEngine.Random.insideUnitSphere;varsizeUpV3=size_up(v3);Debug.Log($"{v3.ToStringFloat()} -> {sizeUpV3.ToStringFloat()}");
実行結果
(-0.01608862, 0.5905958, 0.7266953) -> (-1.608862, 59.05958, 72.66953)

できました。

気になるのは借用をやめた修正です。
C#側では「もともとのVector3」「引数としてコピーされたVector3」の2つがあります。
「もともとのVector3」はC#が管理しているからいいとして、Rustの借用ではないということは「引数としてコピーされたVector3」をRust側で開放しちゃってそうな気がしますが、これってC#側の扱いはどうなっているのでしょうか。externだとそのあたり忖度されるんでしょうか。もしくはstructなのでC#から渡すときに値をコピーしてるから大丈夫なのか。まあいいか。

計測

ようやくNative Pluginを使いたい理由に入ります。
Meshの頂点座標を基準点からの相対位置に変換する処理ですが、この処理がやや重い……ような気がします。そう頻繁に行う処理でもないので無理に高速化する必要もないのですが、今回はやってみることそのものが目的です。
というわけで、以下のC#で書かれた関数をRust側へ計算処理を逃がす関数にするのが今回のゴールです。

TransWithCsharp
publicstaticVector3[]TransWithCsharp(Matrix4x4matrix,IReadOnlyList<Vector3>points){varret=newVector3[points.Count];for(varcount=0;count<points.Count;count++){ret[count]=matrix.MultiplyPoint(points[count]);}returnret;// LINQでこう書くと実際オサレ// return points.Select(matrix.MultiplyPoint).ToArray();}

matrix4x4

C#からMatrix4x4を受け取るため、Rust側で同じ構造体を定義します。
本来ならありものを使うのではなく、C#側でも自分でちゃんと受け渡すための構造体を定義するべきですが、Vector3もそのまま渡せたんだからMatrix4x4も行けるやろ! の精神です。

Matrix4x4
#[repr(C)]pubstructMatrix4x4{m00:f32,m01:f32,m02:f32,m03:f32,m10:f32,m11:f32,m12:f32,m13:f32,m20:f32,m21:f32,m22:f32,m23:f32,m30:f32,m31:f32,m32:f32,m33:f32,}

ちゃんとRust側で受け取れているか試すために、以下の関数を作ってC#と突き合わせてみます。

#[no_mangle]pubexternfnmatrix_add(matrix:Matrix4x4)->Vector3{Vector3{x:matrix.m00,y:matrix.m01,z:matrix.m02}}
テストコード
varAnchor=newGameObject().transform;Anchor.position=UnityEngine.Random.insideUnitSphere;Anchor.rotation=UnityEngine.Random.rotation;Anchor.localScale=UnityEngine.Random.insideUnitSphere+Vector3.one;varmatrix=Anchor.transform.localToWorldMatrix;vara=matrix_add(matrix);Debug.Log(a.ToStringFloat());varb=newVector3(matrix.m00,matrix.m01,matrix.m02);Debug.Log(b.ToStringFloat());
実行結果
(-0.9590587, -0.6367525, -0.6988028)
(-0.9590587, -0.2962521, 0.6547196)

最初だけ合っている。ということはつまり構造体のメンバの定義されている順番が違うのでしょう。
再びRider先生にデコンパイルしてもらいます。

publicstructMatrix4x4:IEquatable<Matrix4x4>{[NativeName("m_Data[0]")]publicfloatm00;[NativeName("m_Data[1]")]publicfloatm10;[NativeName("m_Data[2]")]publicfloatm20;[NativeName("m_Data[3]")]publicfloatm30;[NativeName("m_Data[4]")]publicfloatm01;[NativeName("m_Data[5]")]publicfloatm11;[NativeName("m_Data[6]")]publicfloatm21;[NativeName("m_Data[7]")]publicfloatm31;[NativeName("m_Data[8]")]publicfloatm02;[NativeName("m_Data[9]")]publicfloatm12;[NativeName("m_Data[10]")]publicfloatm22;[NativeName("m_Data[11]")]publicfloatm32;[NativeName("m_Data[12]")]publicfloatm03;[NativeName("m_Data[13]")]publicfloatm13;[NativeName("m_Data[14]")]publicfloatm23;[NativeName("m_Data[15]")]publicfloatm33;}

十の位から増えてるの……?
なんか感覚と違いますが、そう定義されている以上はしょうがありません。Rust側の構造体の定義の順番を変えます。

#[repr(C)]pubstructMatrix4x4{m00:f32,m10:f32,m20:f32,m30:f32,m01:f32,m11:f32,m21:f32,m31:f32,m02:f32,m12:f32,m22:f32,m32:f32,m03:f32,m13:f32,m23:f32,m33:f32,}
実行結果
(-0.04724042, -0.6401328, 0.3618424)
(-0.04724042, -0.6401328, 0.3618424)

一致しました。Matrix4x4はちゃんとC#からRustに渡せています。

ダブルキャスト

/// <summary>///   <para>Transforms a position by this matrix (generic).</para>/// </summary>/// <param name="point"></param>publicVector3MultiplyPoint(Vector3point){Vector3vector3;vector3.x=(float)((double)this.m00*(double)point.x+(double)this.m01*(double)point.y+(double)this.m02*(double)point.z)+this.m03;vector3.y=(float)((double)this.m10*(double)point.x+(double)this.m11*(double)point.y+(double)this.m12*(double)point.z)+this.m13;vector3.z=(float)((double)this.m20*(double)point.x+(double)this.m21*(double)point.y+(double)this.m22*(double)point.z)+this.m23;floatnum=1f/((float)((double)this.m30*(double)point.x+(double)this.m31*(double)point.y+(double)this.m32*(double)point.z)+this.m33);vector3.x*=num;vector3.y*=num;vector3.z*=num;returnvector3;}

肝心のMultiplyPointの処理です。
デコンパイラの結果というのもあると思いますが、なかなかにカオスな計算処理。
float -> double -> floatとキャストしている部分をRustでも再現するかは悩みどころですが、いったんは心を無にしてRustでも同様の処理を書きます。

#[no_mangle]pubexternfnmultiply_point(m:Matrix4x4,v:Vector3)->Vector3{letx=((m.m00asf64*v.xasf64+m.m01asf64*v.yasf64+m.m02asf64*v.zasf64)+m.m03asf64)asf32;lety=((m.m10asf64*v.xasf64+m.m11asf64*v.yasf64+m.m12asf64*v.zasf64)+m.m13asf64)asf32;letz=((m.m20asf64*v.xasf64+m.m21asf64*v.yasf64+m.m22asf64*v.zasf64)+m.m23asf64)asf32;letnum=(1.0/(m.m30asf64*v.xasf64+m.m31asf64*v.yasf64+m.m32asf64*v.zasf64)+m.m33asf64)asf32;Vector3{x:(x*num),y:(y*num),z:(z*num)}}
テストコード
varv3=UnityEngine.Random.insideUnitSphere;Anchor=newGameObject().transform;Anchor.position=UnityEngine.Random.insideUnitSphere;Anchor.rotation=UnityEngine.Random.rotation;Anchor.localScale=UnityEngine.Random.insideUnitSphere+Vector3.one;varmatrix=Anchor.transform.localToWorldMatrix;varwithU=matrix.MultiplyPoint(v3);Debug.Log(withU.ToStringFloat());varwithR=multiply_point(matrix,v3);Debug.Log(withR.ToStringFloat());
実行結果
(-0.02336239, 0.5018276, 0.7009525)
(-Infinity, Infinity, Infinity)

Infinity...

数回繰り返したところ正負は合っているので、キャストに失敗して無限の彼方に辿り着いているようです。
こんなもんの原因追求する気はさらさらないのでRustのコードをきれいに書き直します。

fnmultiple_float(a:f32,b:f32)->f64{((aasf64)*(basf64))}#[no_mangle]pubexternfnmultiply_point(m:Matrix4x4,v:Vector3)->Vector3{letx=multiple_float(m.m00,v.x)+multiple_float(m.m01,v.y)+multiple_float(m.m02,v.z)+m.m03asf64;lety=multiple_float(m.m10,v.x)+multiple_float(m.m11,v.y)+multiple_float(m.m12,v.z)+m.m13asf64;letz=multiple_float(m.m20,v.x)+multiple_float(m.m21,v.y)+multiple_float(m.m22,v.z)+m.m23asf64;leta=multiple_float(m.m30,v.x)+multiple_float(m.m31,v.y)+multiple_float(m.m32,v.z)+m.m33asf64;letnum=1.0/a;Vector3{x:(x*num)asf32,y:(y*num)asf32,z:(z*num)asf32}}

なんかもっときれいに書けるような、そうでもないような。
ともあれこれを実行してみます。

実行結果
(-0.1820646, -0.6444009, 0.6140736)
(-0.1820646, -0.6444009, 0.6140736)

一致しました。これでようやく完成です。

実験

さっそくC#と比べて早いのか遅いのか実験してみます。

テストコード
privateconstintPointCount=1000000;privateasyncvoidCheck(CancellationTokentoken){varanchor=newGameObject().transform;while(true){anchor.position=UnityEngine.Random.insideUnitSphere;anchor.rotation=UnityEngine.Random.rotation;anchor.localScale=UnityEngine.Random.insideUnitSphere+Vector3.one;varmatrix=anchor.transform.localToWorldMatrix;varrandomVectors=awaitTask.Run(()=>GenerateRandomVectorAsync(_cancellationTokenSource.Token,PointCount),token);// Rustによる変換Profiler.BeginSample("#ByRust");varr=TransByRust(matrix,randomVectors);Profiler.EndSample();// Rust(Releaseビルド)による変換Profiler.BeginSample("#ByRustR");varrr=TransByRustRelease(matrix,randomVectors);Profiler.EndSample();// C#による変換Profiler.BeginSample("#ByCSharp");varc=TransByCsharp(matrix,randomVectors);Profiler.EndSample();Debug.Log(r.Length+" - "+rr.Length+" - "+c.Length);}}// C#による変換privatestaticVector3[]TransByCsharp(Matrix4x4matrix,IReadOnlyList<Vector3>points){varret=newVector3[points.Count];for(varcount=0;count<points.Count;count++){ret[count]=matrix.MultiplyPoint(points[count]);}returnret;}// RustDebugビルドによる変換privatestaticVector3[]TransByRust(Matrix4x4matrix,IReadOnlyList<Vector3>points){varret=newVector3[points.Count];for(varcount=0;count<points.Count;count++){ret[count]=multiply_point(matrix,points[count]);}returnret;}// RustReleaseビルドによる変換privatestaticVector3[]TransByRustRelease(Matrix4x4matrix,IReadOnlyList<Vector3>points){varret=newVector3[points.Count];for(varcount=0;count<points.Count;count++){ret[count]=multiply_point_r(matrix,points[count]);}returnret;}// ランダムなVector3の配列を生成privatestaticTask<Vector3[]>GenerateRandomVectorAsync(CancellationTokencancellationToken,intlength){varrandom=newSystem.Random();varpoints=newVector3[length];for(varcount=0;count<points.Length;count++){cancellationToken.ThrowIfCancellationRequested();// UnityEngine.RandomのAPIはメインスレッドからしか呼べない...// なので無理矢理ランダムなVector3を生成するpoints[count].x=(float)(random.NextDouble()*random.Next(-100,100));points[count].y=(float)(random.NextDouble()*random.Next(-100,100));points[count].z=(float)(random.NextDouble()*random.Next(-100,100));}returnTask.FromResult(points);}

これがプロファイラの結果です。

スクリーンショット 2019-11-27 3.56.35.png

C#が一番速い……。
Rustのリリースビルドとデバッグビルドで差が出ている以上、NativePluginだからプロファイラがおかしくなっているわけでもないようです。

f32が溢れるようなことはまずないので、Rust側でキャストを止めてみます。

multiply_point_without_cast
#[no_mangle]pubexternfnmultiply_point_without_cast(m:Matrix4x4,v:Vector3)->Vector3{letx=m.m00*v.x+m.m01*v.y+m.m02*v.z+m.m03;lety=m.m10*v.x+m.m11*v.y+m.m12*v.z+m.m13;letz=m.m20*v.x+m.m21*v.y+m.m22*v.z+m.m23;leta=1.0/(m.m30*v.x+m.m31*v.y+m.m32*v.z+m.m33);Vector3{x:(x*a),y:(y*a),z:(z*a)}}

スクリーンショット 2019-11-29 3.33.15.png

ちょっとはやくなってる。

仮説

1. UnityのMatrix4x4.MultiplyPointはC++層で実行されている
デコンパイルするってことはコンパイルされてるんだよねこれ
2. C#がmatrix4x4をキャッシュしているのに対し、Rustは毎回受け渡しているため非効率
これは確実にあるはず
3. 言語間で受け渡すコスト > Rustによる恩恵
単純な計算処理では意味がなかった

仮説1が一番大きいと思います。わたしが戦っていたのはキャストしまくりのC#ではなくバチバチにチューニングされたC++だったのです……たぶん。なので自分で実装した重い処理とかだったら違う結果が出るかもしれません。

仮説2の解決としてVector3[]の配列を受け渡しできればいいのですが、ポインタがわからないからマーシャリングもわからないので諦めました。Rust側でポインタを復元する方法もわからないです。

仮説3もわりとありそうな気がしています。いちいち変換している分のコストはかなり大きい……はず。

あと、いくらVector3とはいえこの数なら結構なGCを誘発していると思うのですが、プロファイラのGC Allocはみんないっしょです。NativePlugin部分に対するプロファイラの動作もいまいち情報がないのでよくわからん。

まとめ

Rust

今更ですがedition2018です。
Rustの学習ですが、ヤバいと噂の所有権は自分はそんなにひっかかりませんでした。でもライフタイムは微妙にまだよくわかってないかもしれない。
あと、エラー処理と並列プログラミングはちらっと読んだだけで何言ってるかまったく理解してないので改めて読もうと思います。
Rustの学習コストは確かに高いですが、コンパイラさんが徹底的にチェックしてくれることで実行時に吹っ飛ぶのを防止してくれるのはとても好きです。

NativePlugin

Unity+C#+Rustの知識を要求されるのつらい。

敗北

プログラミングぢからは高まった気がしますが、結果が出せていません。
しかし現在の自分ではこれ以上は手が出ない……。ポインタを理解するためにCを諦めてやってみるべきか……。
なにはともあれ今回はここで敗北します。誰かなんか強い人がなんとかしてくれたら嬉しいな! サヨナラ!

とりあえず書いた分は置いておきます。
gist


Viewing all articles
Browse latest Browse all 8901

Trending Articles