v0.87 RuntimeImport 非同期ロード

awaitCaller 引き数

public virtual async Task<RuntimeGltfInstance> LoadAsync(IAwaitCaller awaitCaller = null, ...)

awaitCaller によりロード時の挙動をカスタマイズできます。

 1using System;
 2using System.Threading.Tasks;
 3
 4namespace VRMShaders
 5{
 6    /// <summary>
 7    /// ImporterContext の 非同期実行 LoadAsync を補助する。
 8    /// この関数を経由して await すること。
 9    /// そうしないと、同期実行 Load 時にデッドロックに陥るかもしれない。
10    /// (SynchronizationContext に Post された 継続が再開されない)
11    /// </summary>
12    public interface IAwaitCaller
13    {
14        /// <summary>
15        /// フレームレートを維持するために1フレーム待つ
16        /// </summary>
17        /// <returns></returns>
18        Task NextFrame();
19
20        /// <summary>
21        /// 非同期に実行して、終了を待つ
22        /// </summary>
23        /// <param name="action"></param>
24        /// <returns></returns>
25        Task Run(Action action);
26
27        /// <summary>
28        /// 非同期に実行して、終了を待つ
29        /// </summary>
30        /// <param name="action"></param>
31        /// <typeparam name="T"></typeparam>
32        /// <returns></returns>
33        Task<T> Run<T>(Func<T> action);
34
35        /// <summary>
36        /// 指定した時間が経過している場合のみ、NextFrame() を使って1フレーム待つ
37        /// </summary>
38        /// <returns>タイムアウト時はNextFrame()を呼び出す。そうではない場合、Task.CompletedTaskを返す</returns>
39        Task NextFrameIfTimedOut();
40    }
41}
  • NextFrame: 処理を中断して次のフレームで再開します。ロード処理が長い場合に長時間アプリケーションが固まることを防ぎます

  • Run: UnityEngine.Object にアクセスしない処理を別スレッドで実行します。タスクが終了したら await で Unity Script スレッドで続きを実行します。

ImmediateCaller

デフォルトでは、ImmediateCaller が使われます。 ImmediateCaller タスクを即時に実行するので、同期実行となります。

  • Play(Editor Play, build), Editor(not play), UnitTest でデッドロックしないための実装です。

 1using System;
 2using System.Threading.Tasks;
 3
 4namespace VRMShaders
 5{
 6    /// <summary>
 7    /// 同期実行
 8    /// </summary>
 9    public sealed class ImmediateCaller : IAwaitCaller
10    {
11        public Task NextFrame()
12        {
13            return Task.FromResult<object>(null);
14        }
15
16        public Task Run(Action action)
17        {
18            action();
19            return Task.FromResult<object>(null);
20        }
21
22        public Task<T> Run<T>(Func<T> action)
23        {
24            return Task.FromResult(action());
25        }
26
27        public Task NextFrameIfTimedOut() => NextFrame();
28    }
29}

RuntimeOnlyAwaitCaller

  • Play(Editor Play, build) 時に非同期実行する実装です

  • VRM10_Samples/VRM10Viewer に使用例があります。

 1using System;
 2using System.Threading.Tasks;
 3
 4namespace VRMShaders
 5{
 6    /// <summary>
 7    /// Runtime (Build 後と、Editor Playing) での非同期ロードを実現する AwaitCaller.
 8    /// NOTE: 簡便に実装されたものなので、最適化の余地はある.
 9    /// </summary>
10    public sealed class RuntimeOnlyAwaitCaller : IAwaitCaller
11    {
12        private readonly NextFrameTaskScheduler _scheduler;
13        private readonly float                  _timeOutInSeconds;
14        private          float                  _lastTimeoutBaseTime;
15
16        /// <summary>
17        /// タイムアウト指定可能なコンストラクタ
18        /// </summary>
19        /// <param name="timeOutInSeconds">NextFrameIfTimedOutがタイムアウトと見なす時間(秒単位)</param>
20        public RuntimeOnlyAwaitCaller(float timeOutInSeconds = 1f / 1000f)
21        {
22            _scheduler = new NextFrameTaskScheduler();
23            _timeOutInSeconds = timeOutInSeconds;
24            ResetLastTimeoutBaseTime();
25        }
26
27        public Task NextFrame()
28        {
29            ResetLastTimeoutBaseTime();
30            var tcs = new TaskCompletionSource<object>();
31            _scheduler.Enqueue(() => tcs.SetResult(default));
32            return tcs.Task;
33        }
34
35        public Task Run(Action action)
36        {
37            return Task.Run(action);
38        }
39
40        public Task<T> Run<T>(Func<T> action)
41        {
42            return Task.Run(action);
43        }
44
45        public Task NextFrameIfTimedOut() => CheckTimeout() ? NextFrame() : Task.CompletedTask;
46
47        private void ResetLastTimeoutBaseTime() => _lastTimeoutBaseTime = 0f;
48
49        private bool LastTimeoutBaseTimeNeedsReset => _lastTimeoutBaseTime == 0f;
50
51        private bool CheckTimeout()
52        {
53            float t = UnityEngine.Time.realtimeSinceStartup;
54            if (LastTimeoutBaseTimeNeedsReset)
55            {
56                _lastTimeoutBaseTime = t;
57            }
58            return (t - _lastTimeoutBaseTime) >= _timeOutInSeconds;
59        }
60    }
61}