LoginSignup
0
0

More than 1 year has passed since last update.

TextureStreamingJob クラッシュ分析

Posted at

TextureStreaming

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

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

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

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

疑似コード
void TextureStreamingJob(TexutreStreamingJobData* jobdata)
{
    // 前文を省略する
    auto& sharedData = jobdatasharedData;
    auto smallestMip = jobdatasmallestMip;
    auto largestMip = jobdatalargestMip;
    count = sharedData.textures.size();
    auto& currBatchDesiredMipLevels = jobdataresultsdesiredMipLevels[jobdatabatchIndex];
    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 です。
(4) シーンに 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 > thisresultsbatchCount)
    {
        thisjobBatchIndex = 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.jpeg

質問に少し補充します。

(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 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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0