OpenXR IL2CPP Integration com.unity.inputsystem.ja - toydev/HC_VRTrial GitHub Wiki
キーボード、マウス、ゲームパッド、タッチ、VR など、さまざまな入力デバイスからのイベントを扱うための拡張性とカスタマイズ性を持った入力システムである。 HMD や VR コントローラーのトラッキングの前提となっているっぽい(未確認)。
プリプロセッサにより Plugins
配下のビルドを制御しています。
OpenXR 対応に必要な最小限の条件付きコンパイルシンボルを検討する必要があります(後述)。
検討に当たって以下の実装が全体の外観に役に立ちます。
InputSystem/InputSystem.cs
#if !UNITY_DISABLE_DEFAULT_INPUT_PLUGIN_INITIALIZATION
private static void PerformDefaultPluginInitialization()
{
UISupport.Initialize();
#if UNITY_EDITOR || UNITY_STANDALONE || UNITY_WSA || UNITY_ANDROID || UNITY_IOS || UNITY_TVOS
XInputSupport.Initialize();
#endif
#if UNITY_EDITOR || UNITY_STANDALONE || UNITY_PS4 || UNITY_PS5 || UNITY_WSA || UNITY_ANDROID || UNITY_IOS || UNITY_TVOS
DualShockSupport.Initialize();
#endif
#if UNITY_EDITOR || UNITY_STANDALONE || UNITY_WSA
HIDSupport.Initialize();
#endif
#if UNITY_EDITOR || UNITY_ANDROID
Android.AndroidSupport.Initialize();
#endif
#if UNITY_EDITOR || UNITY_IOS || UNITY_TVOS
iOS.iOSSupport.Initialize();
#endif
#if UNITY_EDITOR || UNITY_STANDALONE_OSX
OSX.OSXSupport.Initialize();
#endif
#if UNITY_EDITOR || UNITY_WEBGL
WebGL.WebGLSupport.Initialize();
#endif
#if UNITY_EDITOR || UNITY_STANDALONE_OSX || UNITY_STANDALONE_WIN || UNITY_WSA
Switch.SwitchSupportHID.Initialize();
#endif
#if UNITY_INPUT_SYSTEM_ENABLE_XR && (ENABLE_VR || UNITY_GAMECORE) && !UNITY_FORCE_INPUTSYSTEM_XR_OFF
XR.XRSupport.Initialize();
#endif
#if UNITY_EDITOR || UNITY_STANDALONE_LINUX
Linux.LinuxSupport.Initialize();
#endif
#if UNITY_EDITOR || UNITY_ANDROID || UNITY_IOS || UNITY_TVOS || UNITY_WSA
OnScreen.OnScreenSupport.Initialize();
#endif
#if (UNITY_EDITOR || UNITY_STANDALONE) && UNITY_ENABLE_STEAM_CONTROLLER_SUPPORT
Steam.SteamSupport.Initialize();
#endif
#if UNITY_EDITOR
UnityRemoteSupport.Initialize();
#endif
}
#endif // UNITY_DISABLE_DEFAULT_INPUT_PLUGIN_INITIALIZATION
このプラグインは入力をイベントとして扱うことをサポートするもので、
入力を BroadcastMessage
および SendMessage
を使ってのメソッド呼び出しに結びつける役割を持っていると思います。
BroadcastMessage
および SendMessage
で送信するデータが UnityEngine.InputSystem.InputValue
というマネージドコードのインスタンスであるため動かない可能性が高いですが、これは Unity Editor で入力を MonoBehaviour
のメソッドにバインドして使うといった類の高レベルの機能と考えられ必須ではないので削除します。
これを使って定義する必要のある追加のイベントがあるとしたらそれはマネージドコード上で動くものと想像できるので、どうしても必要な場合は C# のデリゲートやイベントによる低レベル実装で置き換えます。
シリアライズ周りのコンパイルを通すのが難しそうですが、MOD には無関係な実装であるため取り込みをスキップします。
使っている実装が以下の通り二か所あるのでコメントアウトします。
InputSystem/InputSystem.cs
public static InputRemoting remoting => s_Remote;
// ...
internal static InputRemoting s_Remote;
InputSystem/Plugins
配下のディレクトリ分析にてプラグインに関連したプリプロセッサの実装があることがわかりました。
それを元に以下の通りとします。
UNITY_2019_3_OR_NEWER;UNITY_2021_2_OR_NEWER;UNITY_2021_3_OR_NEWER;UNITY_INPUT_SYSTEM_ENABLE_XR;ENABLE_VR
com.unity.xr.openxr
に使っている箇所が多数存在します。
以下がリフレクションを使って属性クラスを参照している箇所です。
InputBindingComposite.GetExpectedControlLayoutName
InputBindingComposite.GetPartNames
InputControlLayout.AddControlItemsFromMembers
OpenXR に関係しそうな UnityEngine.InputSystem.XR.XRLayoutBuilder
から辿って使っている可能性高いです。
従って、System.Attribute
の派生に切り替えて、属性クラスとして機能させる方針とします。
関連するコードは以下の通りです。
InputSystem/InputSystem.cs
static InputSystem()
{
#if UNITY_EDITOR
InitializeInEditor();
#else
InitializeInPlayer();
#endif
}
////FIXME: Unity is not calling this method if it's inside an #if block that is not
//// visible to the editor; that shouldn't be the case
[RuntimeInitializeOnLoadMethod(loadType: RuntimeInitializeLoadType.SubsystemRegistration)]
private static void RunInitializeInPlayer()
{
// We're using this method just to make sure the class constructor is called
// so we don't need any code in here. When the engine calls this method, the
// class constructor will be run if it hasn't been run already.
// IL2CPP has a bug that causes the class constructor to not be run when
// the RuntimeInitializeOnLoadMethod is invoked. So we need an explicit check
// here until that is fixed (case 1014293).
#if !UNITY_EDITOR
if (s_Manager == null)
InitializeInPlayer();
#endif
}
コメントによると IL2CPP 環境(ここでいう IL2CPP 環境とは本来の意味での IL2CPP 環境であり MOD の IL2CPP 環境とは異なる点に注意)において静的コンストラクタの動きに対する対応として RuntimeInitializeOnLoadMethod
を使っています。
MOD として上記コードを取り込んだ場合、マネージドコードとして動くので静的コンストラクタも普通に動くので [RuntimeInitializeOnLoadMethod(loadType: RuntimeInitializeLoadType.SubsystemRegistration)]
はコメントアウトして構わないと判断します。
関連するコードは以下の通りです。
InputSystem/InputSystem.cs
// [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void RunInitialUpdate()
{
// Request an initial Update so that user methods such as Start and Awake
// can access the input devices.
//
// NOTE: We use InputUpdateType.None here to run a "null" update. InputManager.OnBeforeUpdate()
// and InputManager.OnUpdate() will both early out when comparing this to their update
// mask but will still restore devices. This means we're not actually processing input,
// but we will force the runtime to push its devices.
Update(InputUpdateType.None);
これは無視できないので SceneManager.sceneLoaded
で置き換えます。
RuntimeInitializeLoadType.BeforeSceneLoad
と SceneManager.sceneLoaded
の実行タイミングは厳密には異なるが、
com.unity.inputsystem
を使ったプログラムはマネージドコード側にしか存在しないため自分自身の作りで問題を排除できると判断します。
Il2CppInterop の機能変更が必要になる本対応の一番の問題個所です。
-
NativeInputSystem.onUpdate
はUnityEngineInternal.Input.NativeUpdateCallback
である。 -
UnityEngineInternal.Input.NativeUpdateCallback
の元々の定義は以下の通りである。
internal unsafe delegate void NativeUpdateCallback(NativeInputUpdateType updateType, NativeInputEventBuffer* buffer);
-
Il2CppInterop.Runtime.DelegateSupport.ConvertDelegate<NativeUpdateCallback>(new Action<NativeInputUpdateType, NativeInputEventBuffer*>( ... ))
でdelegate
インスタンスを取得したいが、ポインター(NativeInputEventBuffer*
のこと)はジェネリクスで扱えないという制限が C# にあるため書けない。 - 対応策として
IntPtr
で置き換えたいが、Il2CppInterop.Runtime.DelegateSupport.ConvertDelegate
の内部に IL2CPP が持っている型定義との厳密な一致チェックがあり、置き換えができない。
現時点ではいったん Il2CppInterop に以下の変更を加えた独自改変バージョンを使った環境で試しています。
public static class DelegateSupport
{
// ...
public static TIl2Cpp? ConvertDelegate<TIl2Cpp>(Delegate @delegate) where TIl2Cpp : Il2CppObjectBase
{
// ...
for (var i = 0; i < nativeParameters.Count; i++)
{
// ...
if (nativeType.IsPrimitive || managedType.IsPrimitive)
{
// 変更前の if 文:if (nativeType.FullName != managedType.FullName)
// ポインター型だったら IntPtr への変更を許容する。
if ((nativeType.IsPointer && managedType.FullName != typeof(IntPtr).FullName) || (!nativeType.IsPointer && nativeType.FullName != managedType.FullName))
throw new ArgumentException(
$"Parameter type mismatch at parameter {i}: {nativeType.FullName} != {managedType.FullName}");
continue;
}
// ...
}
// ...
}
}
上記を前提に以下の通り実装します。
namespace UnityEngine.InputSystem.LowLevel
{
internal class NativeInputRuntime : IInputRuntime
{
// ...
public unsafe InputUpdateDelegate onUpdate
{
get => m_OnUpdate;
set
{
if (value != null)
NativeInputSystem.onUpdate = DelegateSupport.ConvertDelegate<NativeUpdateCallback>(new Action<NativeInputUpdateType, IntPtr>(
(updateType, eventBufferIntPtr) =>
{
unsafe
{
// IntPtr で受けたものを unsafe 内で NativeInputEventBuffer* にキャストする。
var eventBufferPtr = (NativeInputEventBuffer*)eventBufferIntPtr;
// ...
}
}));
else
NativeInputSystem.onUpdate = null;
m_OnUpdate = value;
}
}
// ...
}
}
IL2CPP の stripping によって UnityEngine.Pool.ListPool がなくなっているためビルドエラーが出る。
UnityEngine.Pool.ListPool はリストの繰り返し利用を効率化するためのプール実装であるが、とりあえず単純に new List で起き変える。性能に問題が出た場合に独自のプール実装で置き換えることを考える。
JsonUtility(UnityEngine.JSONSerializeModule.dll) を使って JSON の読み書きを行っている箇所がいくつか存在するが、マネージドコードのインスタンスを渡して動くとは思えないので Json.NET で置き換えます。
実装すべきメソッドは以下の通りです。
namespace UnityEngine.InputSystem
{
internal class JsonUtility
{
public static T FromJson<T>(string json) { ... }
public static string ToJson(object obj) { ... }
public static string ToJson(object obj, bool prettyPrint) { ... }
}
}
以下のフィールドを参照する箇所で多数のコンパイルエラーが出ています。
[FieldOffset(0)]
private NativeInputEvent m_Event;
Il2CppInterop によって UnityEngineInternal.Input.NativeInputEvent の詳細なフィールドの定義がなくなったのが原因です。
元々の NativeInputEvent の定義を再実装して置き換えます。