OpenXR IL2CPP Integration com.unity.inputsystem.ja - toydev/HC_VRTrial GitHub Wiki

1. 概要

キーボード、マウス、ゲームパッド、タッチ、VR など、さまざまな入力デバイスからのイベントを扱うための拡張性とカスタマイズ性を持った入力システムである。 HMD や VR コントローラーのトラッキングの前提となっているっぽい(未確認)。


2. ディレクトリ毎の分析と対応

InputSystem/Plugins 配下

プリプロセッサにより 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

InputSystem/Plugins/PlayerInput 配下

このプラグインは入力をイベントとして扱うことをサポートするもので、 入力を BroadcastMessage および SendMessage を使ってのメソッド呼び出しに結びつける役割を持っていると思います。

BroadcastMessage および SendMessage で送信するデータが UnityEngine.InputSystem.InputValue というマネージドコードのインスタンスであるため動かない可能性が高いですが、これは Unity Editor で入力を MonoBehaviour のメソッドにバインドして使うといった類の高レベルの機能と考えられ必須ではないので削除します。

これを使って定義する必要のある追加のイベントがあるとしたらそれはマネージドコード上で動くものと想像できるので、どうしても必要な場合は C# のデリゲートやイベントによる低レベル実装で置き換えます。

InputSystem/Devices/Remote 配下

シリアライズ周りのコンパイルを通すのが難しそうですが、MOD には無関係な実装であるため取り込みをスキップします。

使っている実装が以下の通り二か所あるのでコメントアウトします。

  • InputSystem/InputSystem.cs
        public static InputRemoting remoting => s_Remote;
        // ...
        internal static InputRemoting s_Remote;

3. コンパイルシンボルの決定

InputSystem/Plugins 配下のディレクトリ分析にてプラグインに関連したプリプロセッサの実装があることがわかりました。

それを元に以下の通りとします。

UNITY_2019_3_OR_NEWER;UNITY_2021_2_OR_NEWER;UNITY_2021_3_OR_NEWER;UNITY_INPUT_SYSTEM_ENABLE_XR;ENABLE_VR

4. ビルドエラーの除去

4.1. System.Attribute クラス

InputControl

com.unity.xr.openxr に使っている箇所が多数存在します。 以下がリフレクションを使って属性クラスを参照している箇所です。

  • InputBindingComposite.GetExpectedControlLayoutName
  • InputBindingComposite.GetPartNames
  • InputControlLayout.AddControlItemsFromMembers

OpenXR に関係しそうな UnityEngine.InputSystem.XR.XRLayoutBuilder から辿って使っている可能性高いです。 従って、System.Attribute の派生に切り替えて、属性クラスとして機能させる方針とします。

RuntimeInitializeOnLoadMethod(loadType: RuntimeInitializeLoadType.SubsystemRegistration)

関連するコードは以下の通りです。

  • 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)] はコメントアウトして構わないと判断します。

RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)

関連するコードは以下の通りです。

  • 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.BeforeSceneLoadSceneManager.sceneLoaded の実行タイミングは厳密には異なるが、 com.unity.inputsystem を使ったプログラムはマネージドコード側にしか存在しないため自分自身の作りで問題を排除できると判断します。


4.2. イベント関連

NativeInputSystem.onUpdate

Il2CppInterop の機能変更が必要になる本対応の一番の問題個所です

  • NativeInputSystem.onUpdateUnityEngineInternal.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;
            }
        }

        // ...
    }
}

UnityEngine.Pool.ListPool クラス(Stripping)

IL2CPP の stripping によって UnityEngine.Pool.ListPool がなくなっているためビルドエラーが出る。

UnityEngine.Pool.ListPool はリストの繰り返し利用を効率化するためのプール実装であるが、とりあえず単純に new List で起き変える。性能に問題が出た場合に独自のプール実装で置き換えることを考える。

UnityEngine.JsonUtility クラス

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) { ... }
    }
}

UnityEngine.InputSystem.LowLevel.InputEvent クラス

以下のフィールドを参照する箇所で多数のコンパイルエラーが出ています。

        [FieldOffset(0)]
        private NativeInputEvent m_Event;

Il2CppInterop によって UnityEngineInternal.Input.NativeInputEvent の詳細なフィールドの定義がなくなったのが原因です。

元々の NativeInputEvent の定義を再実装して置き換えます。

⚠️ **GitHub.com Fallback** ⚠️