FTextureCompilationManager:: Calling FinishCompilation is not allowed during PostCompilation() in 5.6

We are experiencing a frequent but highly-conditional crash in our project after upgrading to UE5.6.

If FTextureCompilationManager::PostCompilation executes while we have queued tasks in Task Graph that are executing on the GameThread, and those tasks deal with UTextures in anyway (in our case, applying them as brushes to a UImage - the engine will crash due to the recently added bIsRoutingPostCompilation check in FTextureCompilationManager::FinishCompilation.

Ultimately this starts becase a call to UTexture->UpdateResource() is made, invoking FlushRenderingCommands(), which then forces all EGameThread tasks to complete. This causes our User Interface code (which makes heavy of task graph via a third-party plugin), to invoke UImage::SetBrushFromTexture(), which internally calls FTextureCompilationManager::FinishCompilation

This seems extremely fragile, any task graph task that works with texture assets specfically would not be able to execute on the game thread, and would have to schedule a completion callback to run from somewhere inside the LevelTick. This hasn’t been an issue for us in prior releases as far as I’m aware - is there some new feature we can disable to avoid this check being hit?

Callstack. Some information about the tasks has to be redacted.

`[Inlined] UE::Logging::Private::BasicFatalLogV(const FLogCategoryBase &, const UE::Logging::Private::FStaticBasicLogRecord *, void *, char *) StructuredLog.cpp:1600
UE::Logging::Private::BasicFatalLog(const FLogCategoryBase &,const UE::Logging::Private::FStaticBasicLogRecord *,…) StructuredLog.cpp:1609
FTextureCompilingManager::FinishCompilation(TArrayView<…>) TextureCompiler.cpp:454
UImage::SetBrushFromTexture(UTexture2D *, bool) Image.cpp:195
UCommonLazyImage::SetBrushObjectInternal(UTexture *, bool) CommonLazyImage.cpp:280

[[ REDACTED ]]

UE::Tasks::Private::FTaskBase::TryExecuteTask() TaskPrivate.h:527
[Inlined] FBaseGraphTask::Execute(TArray<…> &, Type, bool) TaskGraphInterfaces.h:505
FNamedTaskThread::ProcessTasksNamedThread(int, bool) TaskGraph.cpp:779
FNamedTaskThread::ProcessTasksUntilIdle(int) TaskGraph.cpp:678
GameThreadWaitForTask(const UE::Tasks::Private::FTaskHandle &, bool) RenderingThread.cpp:1275
[Inlined] FRenderCommandFence::Wait(bool) RenderingThread.cpp:1319
FFrameEndSync::FRenderThreadFence::~FRenderThreadFence() RenderingThread.cpp:2043
[Inlined] DestructItem(FFrameEndSync::FRenderThreadFence *) MemoryOps.h:76
[Inlined] TArray::RemoveAtImpl(int) Array.h:2052
[Inlined] TArray::RemoveAt(int, EAllowShrinking) Array.h:2093
FFrameEndSync::Sync(EFlushMode) RenderingThread.cpp:2094
FlushRenderingCommands() RenderingThread.cpp:1362
UStreamableRenderAsset::WaitForPendingInitOrStreaming(bool, bool) StreamableRenderAsset.cpp:427
UTexture2D::UpdateResourceWithParams(EUpdateResourceFlags) Texture2D.cpp:738
FTextureCompilingManager::PostCompilation(UTexture *) TextureCompiler.cpp:234
FTextureCompilingManager::ProcessTextures(bool, int) TextureCompiler.cpp:742
FTextureCompilingManager::ProcessAsyncTasks(const AssetCompilation::FProcessAsyncTaskParams &) TextureCompiler.cpp:883
FTextureCompilingManager::ProcessAsyncTasks(bool) TextureCompiler.cpp:862
FAssetCompilingManager::ProcessAsyncTasks(bool) AssetCompilingManager.cpp:631
FEngineLoop::Tick() LaunchEngineLoop.cpp:5665
[Inlined] EngineTick() Launch.cpp:60
GuardedMain(const wchar_t *) Launch.cpp:189
LaunchWindowsStartup(HINSTANCE__ *, HINSTANCE__ *, char *, int, const wchar_t *) LaunchWindows.cpp:271
WinMain(HINSTANCE__ *, HINSTANCE__ *, char *, int) LaunchWindows.cpp:339`

Hi James,

Unfortunately, the renderer needs to execute some tasks on the GT while waiting on FlushRenderingCommands, that’s why we end up processing gamethread tasks while we wait. It always been this way, not sure why you’re seeing this as a new issue in 5.6.

To avoid this behavior, you can schedule your gamethread only tasks inside a tick instead using ExecuteOnGameThread available in Ticker.h, it will avoid having your gamethread tasks run inside waits like these and should resolve your issue.

Alternatively, you could make UCommonLazyImage async aware by looking at the Texture IsCompiling() function before doing the assignment and registering on the asset compilation manager to know when the texture is done and create it at that time. This way you will avoid hitches on the game thread and improve the responsiveness of the application by showing things only when they are ready.

Hope this helps

Danny