TextureStreamingJob クラッシュ分析

##TextureStreaming
Q：TextureStreamingJob が Android、iOS、および PC でクラッシュしました。
クラッシュのコール スタックは次のとおりです。

```ruby:Crash
Unity.exe!TextureStreamingJob(struct TextureStreamingJobData *)
Unity.exe!JobQueue::Exec(struct JobInfo *,__int64,int)
Unity.exe!JobQueue::Steal(class JobGroup *,struct JobInfo *,__int64,int,bool)
Unity.exe!JobQueue::ExecuteJobFromQueue(void)
Unity.exe!JobQueue::ProcessJobs(void *)
Unity.exe!JobQueue::WorkLoop(void *)
Unity.exe!Thread::RunThreadWrapper(void *)
kernel32.dll!BaseThreadInitThunk()
ntdll.dll!RtlUserThreadStart()
```
また、iOSでは次とおります。

```ruby:Crash_iOS
1 app TextureStreamingJob (FloatConversion.h:127)
2 app Exec (JobQueue.cpp:412)
3 app Steal (JobQueue.cpp:673)
4 app ExecuteJobFromQueue (JobQueue.cpp:832)
5 app ProcessJobs (JobQueue.cpp:890)
6 app WorkLoop (JobQueue.cpp:976)
7 app RunThreadWrapper (Thread.cpp:76)
```
Androidでも大体同じようです。

>ここで、Bug Reportを提出した１つの例について話します。コールスタックによって問題のあるところを特定し、前後を整理して分析します。
>
疑似コードは大体次とおります。
>
```ruby:疑似コード
void TextureStreamingJob(TexutreStreamingJobData* jobdata)
{
    // 前文を省略する
    auto& sharedData = jobdata→sharedData;
    auto smallestMip = jobdata→smallestMip;
    auto largestMip = jobdata→largestMip;
    count = sharedData.textures.size();
    auto& currBatchDesiredMipLevels = jobdata→results→desiredMipLevels[jobdata→batchIndex];
    if (count)
    {
        for (int i = 0; i < count; i++)
        {
            int8_t mipLevel = -1;
            if (sharedData.textures[i].unknownFloatValue >= 0.0)
                mipLevel = sharedData.textures[i].mipLevel;
            if (mip < 0)
                mipLevel = -1;
            if (mip >= smallestMip)
                mipLevel = smallestMip;
            if (mip <= largestMip)
                mipLevel = largestMip;
            currBatchDesiredMipLevels[i].mipLevel = mipLevel;    // << ここでクラッシュした
            currBatchDesiredMipLevels[i].unknownField = CONST_VALUE;
        }
    }
    // 以下を省略する
}
```
>
クラッシュは currBatchDesiredMipLevels[i].mipLevel = mipLevel; この文で発生しました。
>
分析により、TextureStreamingManager は UnityEngine.QualitySettings の StreamingMipmapsRenderersPerFrame パラメータに従って現在のタスクをグループ化します。
[QualitySettings.streamingMipmapsRenderersPerFrame](https://docs.unity3d.com/ScriptReference/QualitySettings-streamingMipmapsRenderersPerFrame.html)
たとえば、streamingMipmapsRenderersPerFrame = 2；
(1) シーンに 2 つのstreamingオブジェクトがある場合、タスクのセットは 1 つだけです (後で batchCount と呼び、インデックスは batchIndex  と呼びます)、つまり、batchCount = 1;
(2) シーンに 3 つのstreamingオブジェクトがある場合、batchCount = 2、一組のタスク数は 2、二組のタスク数は 1 です。
(3) シーンに 4 つのstreamingオブジェクトがある場合、batchCount = 2、一組のタスク数は 2、二組のタスク数は 2 です。
(4) シーンに 5 つのstreamingオブジェクトがある場合、batchCount = 3、一組のタスク数は 2、一組のタスク数は 2 、三組のタスク数は 1 です。
等々。
>
ここで話した一組タスク、二組タスクは、この記事ではbatchと呼びます。
jobdata→results→desiredMipLevelsは１つの配列であり、batchによって保存します。
>
（1）jobdata→results→desiredMipLevels[
これから見ると、 jobdata→results→desiredMipLevels.size() はbatchCountと相当はずです。
>
```ruby:
// 型名は推測です、ご了承ください 🙁
struct TextureMipLevelInfo;
// エンジンには独自のデータ構造 dynamic_array があり、ここでは意味をより直感的に表すためにstd::vectorを使用します。
std::vector<std::vector<TextureMipLevelInfo>> desiredMipLevels;
```

>クラッシュ処により、クラッシュが発生する時にjobdat→results→desiredMipLevels[batchIndex]はNULLであります。ですから、さらにdesiredMipLevels的size()和capacity()を調査します。その中、sizeは1、capacityは8です。
jobdata→results→desiredMipLevels1
この時、jobdata→results→desiredMipLevels.size() == 1
インデックス範囲外になりました。だから、batchIndexのソースを分析する必要があります。
いくつかの調査の後、batchIndex は TextureStreamingManager::Update() から来ていることがわかりました。
>
```ruby:
TextureStreamingManager::Update()
{
    // 前文を省略する
    if (this->jobBatchIndex > this→results→batchCount)
    {
        this→jobBatchIndex = 0;
    }
    // 省略する
        TextureStreamingManager::InitJobData(/*パラメータを省略する...*/)    // this→jobDataを初期化する
        ScheduleJobInternal(this→jobFence, &TextureStreamingJob, this→jobData, 0);
    // 省略する
}
```

>これから推測します、タスクScheduleJobInternalの時、状態が正しいはずでした。あの時、もしjobBatchIndex = 1またthis->results→batchCount = 1なら、this→jobBatchIndexは 0 になるはずです。
>
では、ScheduleJobInternal以降、TextureStreamingJob を実行する前に、タスクデータが変更されていると推測できます。
batchIndex タスクは初期化後に変更されません。状態が変化する場合、それは this→results→batchCount でなければなりません。
batchCount に影響を与える可能性のある操作は何ですか？自然に、streamingMipmapsRenderersPerFrameパラメーターやstreamingに参与するテクスチャの数を思い出しました。
>
推測があったら、残ったのは検証することです。再現するために次の条件を満たす必要があります。
(1)TextureStreamingJobのタスクデータの各フレームが初期化された後、タスクが実際に実行される前に batchCount が減少します。
(2)タスク初期化時の BatchIndex >= 削減された batchCount。
>
いくつかの試行の後、最も穏やかな方法を選択しました。
(1) TextureStreamingJob の実行タイミングをフックで制御できる（各フレームのが終わる時）ようにします。
(2) 2 つの Quality を設定して [RenderersPerFrame](https://forum.unity.com/threads/cant-set-streamingmipmapsrenderersperframe-via-code.538545/) を切り替えます。
(3) QualitySettings の 1 つは、より小さな Renderers Per Frame を設定し、もう 1 つはより大きな値を設定します。
(4) シーン内にTextureStreaming に参加するいくつかのRendererを配置して、batchCount がちょうど 2 になるようにします。
(5) ボタンを追加し、ボタンがクリックされたときに、より大きな Renderers Per Frame パラメータを持つ QualitySettings に切り替えます。
>
ボタンをクリックしてこのフレームが終わりますと、TextureStreamingJobが実行されます——Crash!
![1.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/891cc66e-5c5b-e4d5-538e-9c7fb326f8b3.jpeg)

>####質問に少し補充します。
>
(1) 問題の再現に使用されたプロジェクトは、Renderers Per Frame を変更することでトリガーされますが、Renderers Per Frame を変更せずにトリガーすることもできます。
>
(2) TextureStreamingJob の最初のところでクラッシュすること以外に、desiredMipLevels は関数の後ろでもアクセスされるため、アドレスのより後のほうもクラッシュする可能性があります。
>
(3) TextureStreamingManager::InitJobData の SharedData は参照カウントが行っていて、そしてUnshare() が使用されているため、問題が発生しにくいです。しかし、resultsにはしていません。resultsがUnshare()をコールして、参照カウントは１ですが、実際にJobSystemのスレッドとメイン スレッドが同じ TextureStreamingResults にアクセスして変更するため、Bugが発生します。
>
TextureStreaming は効果的にメモリを削減できますが、Bugが多く、既知のクラッシュも一回あります。TextureStreaming は現在、[QualitySettings.masterTextureLimit](https://docs.unity3d.com/ScriptReference/QualitySettings-masterTextureLimit.html)設定と衝突しています。特定の状況下では、1/2 に設定すると実際の値が 1/4 になります (2 回有効した)。textureStreamingActive を切り替えると、予期しない現象も発生します。プロジェクトの開発が後期段階に近づいている場合、現時点で接続することはお勧めしません。

---
UWA Technologyは、モバイル/VRなど様々なゲーム開発者向け、**パフォーマンス分析**と**最適化ソリューション**及び**コンサルティングサービス**を提供している会社でございます。

今なら、UWA GOTローカルツールが**15日間に無償試用**できます！！
よければ、ぜひ！

UWA公式サイト：https://jp.uwa4d.com
UWA GOT OnlineレポートDemo：https://jp.uwa4d.com/u/got/demo.html
UWA公式ブログ：https://blog.jp.uwa4d.com