TextureStreamingJob クラッシュ分析

TextureStreaming

Q:TextureStreamingJob が Android、iOS、および PC でクラッシュしました。

クラッシュのコール スタックは次のとおりです。

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では次とおります。

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を提出した1つの例について話します。コールスタックによって問題のあるところを特定し、前後を整理して分析します。

疑似コードは大体次とおります。

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

たとえば、streamingMipmapsRenderersPerFrame = 2;

(1) シーンに 2 つのstreamingオブジェクトがある場合、タスクのセットは 1 つだけです (後で batchCount と呼び、インデックスは batchIndex  と呼びます)、つまり、batchCount = 1;

(2) シーンに 3 つのstreamingオブジェクトがある場合、batchCount = 2、一組のタスク数は 2、二組のタスク数は 1 です。

(3) シーンに 4 つのstreamingオブジェクトがある場合、batchCount = 2、一組のタスク数は 2、二組のタスク数は 2 です。

(3) シーンに 5 つのstreamingオブジェクトがある場合、batchCount = 3、一組のタスク数は 2、一組のタスク数は 2 、三組のタスク数は 1 です。

等々。

ここで話した一組タスク、二組タスクは、この記事ではbatchと呼びます。

jobdata→results→desiredMipLevelsは1つの配列であり、batchによって保存します。

(1)jobdata→results→desiredMipLevels[

これから見ると、 jobdata→results→desiredMipLevels.size() はbatchCountと相当はずです。

// 型名は推測です、ご了承ください :(
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() から来ていることがわかりました。

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 を切り替えます。

(3) QualitySettings の 1 つは、より小さな Renderers Per Frame を設定し、もう 1 つはより大きな値を設定します。

(4) シーン内にTextureStreaming に参加するいくつかのRendererを配置して、batchCount がちょうど 2 になるようにします。

(5) ボタンを追加し、ボタンがクリックされたときに、より大きな Renderers Per Frame パラメータを持つ QualitySettings に切り替えます。

ボタンをクリックしてこのフレームが終わりますと、TextureStreamingJobが実行されます——Crash!

質問に少し補充します。

(1) 問題の再現に使用されたプロジェクトは、Renderers Per Frame を変更することでトリガーされますが、Renderers Per Frame を変更せずにトリガーすることもできます。

(2) TextureStreamingJob の最初のところでクラッシュすること以外に、desiredMipLevels は関数の後ろでもアクセスされるため、アドレスのより後のほうもクラッシュする可能性があります。

(3) TextureStreamingManager::InitJobData の SharedData は参照カウントが行っていて、そしてUnshare() が使用されているため、問題が発生しにくいです。しかし、resultsにはしていません。resultsがUnshare()をコールして、参照カウントは1ですが、実際にJobSystemのスレッドとメイン スレッドが同じ TextureStreamingResults にアクセスして変更するため、Bugが発生します。

TextureStreaming は効果的にメモリを削減できますが、Bugが多く、既知のクラッシュも一回あります。TextureStreaming は現在、QualitySettings.masterTextureLimit 設定と衝突しています。特定の状況下では、1/2 に設定すると実際の値が 1/4 になります (2 回有効した)。textureStreamingActive を切り替えると、予期しない現象も発生します。プロジェクトの開発が後期段階に近づいている場合、現時点で接続することはお勧めしません。


UWA公式サイト:https://jp.uwa4d.com

UWA公式ブログ:https://blog.jp.uwa4d.com

UWA公式Q&Aコミュニティ(中国語注意)https://answer.uwa4d.com