FRegisterComponentContext::Process() in ActorComponent.cpp iterates over SendRenderDynamicDataPrimitives and calls SendRenderDynamicData_Concurrent() on each. The existing guard checks ::IsValid(Primitive), which catches destroyed/pending-kill objects but not the case where the render state has been torn down while the object is still alive. SendRenderDynamicData_Concurrent() has check(bRenderStateCreated) at the top, which fires intermittently when a component's render state is destroyed between being queued and processed.
The proposed fix is to add Primitive->IsRenderStateCreated() to the existing validity check (Engine/Source/Runtime/Engine/Private/Components/ActorComponent.cpp, ~line 256):
if (::IsValid(Primitive) && Primitive->IsRenderStateCreated())
{
Primitive->SendRenderDynamicData_Concurrent();
}
This is consistent with the pattern already used in the AddPrimitiveBatches loop ~20 lines above in the same function, which checks Component->IsRenderStateCreated() before operating on the component. A similar pattern existed in the now-deleted AsyncRegisterLevelContext.cpp. That file has been fully removed (deprecated in 5.8).
No consistent repro. The check fires occasionally while playing the game.
Potential scenarios:
From Level.cpp:
OnIncrementalRegisterComponentsDone() → Process() // flushes current batches
After that, IncrementalRunConstructionScripts() runs, which calls Actor->RerunConstructionScripts(). This destroys and recreates components — including their render state. If a component was already in SendRenderDynamicDataPrimitives from a prior Process() but hadn't been consumed yet (e.g., re-queued via the batch-size threshold path), it could have its render state destroyed by construction script re-execution.
RecreateRenderState_Concurrent() calls DestroyRenderState_Concurrent() then CreateRenderState_Concurrent(). If something triggers a render state recreation (e.g., material change, LOD change, mesh swap) on a component that's already queued in SendRenderDynamicDataPrimitives, the destroy half sets bRenderStateCreated = false. Even though it gets recreated immediately after, the queue still holds a stale reference to a moment when it expected the old render state.
This is less likely to trigger the check since CreateRenderState_Concurrent sets it back to true, but there's a window if Process() runs between the destroy and recreate on another thread.
In PreEditChange() and PostEditChange() , if the component is found to be invalid, ExecuteUnregisterEvents() is called directly — tearing down render state without destroying the UObject. The component could still be in the dynamic data queue from a prior registration batch.
There's no existing public thread on this issue, so head over to Questions & Answers just mention UE-380279 in the post.
| 0 |
| Component | UE - Rendering - Architecture - RHI |
|---|---|
| Affects Versions | 5.6, 5.7, 5.8 |
| Created | May 18, 2026 |
|---|---|
| Updated | May 20, 2026 |