
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 です。
(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 > 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
これも興味あるかも
-
原理から応用まで ゲームでの動的解像度
January 4, 2023 -
Unityゲームの使用メモリを最適化しよう
December 21, 2022 -
ASTC テクスチャ圧縮形式の紹介
December 14, 2022