We’re looking at how FStaticMeshSceneProxy guarantees the lifetime of the UStaticMesh (and FStaticMeshRenderData) it references from the render thread.
FStaticMeshSceneProxy stores plain raw pointers copied at construction:
// StaticMeshSceneProxy.h
FStaticMeshRenderData* RenderData; // :220
const UStaticMesh* StaticMesh; // :280 (private)
// StaticMeshRender.cpp
: RenderData(InComponent->GetStaticMesh()->GetRenderData()) // :202
: StaticMesh(InComponent->GetStaticMesh()) // :218
UStaticMesh::RenderData is a TUniquePtr<FStaticMeshRenderData> (StaticMesh.h:593), i.e. it is owned by the mesh UObject and deleted when the mesh is destroyed (~UStaticMesh at StaticMesh.cpp:3379). The proxy then dereferences StaticMesh / RenderData on the render thread in several places (e.g. StaticMesh->GetLightMapCoordinateIndex() :480, StaticMesh->SpeedTreeWind :499/512, StaticMesh->GetNavCollision() :1825, RenderData->LODResources throughout GetDynamicMeshElements).
Our understanding of the guarantees (please confirm/correct)
UStaticMeshComponent::StaticMesh is a TObjectPtr<UStaticMesh> UPROPERTY (StaticMeshComponent.h:354), i.e. a strong GC reference, so the mesh cannot be collected while the component is alive.
When the component is destroyed, DestroyRenderState_Concurrent → FScene::RemovePrimitive fences the render-thread deletion of the proxy before the component finishes destruction, so the proxy is gone before the mesh loses its last reference and can be GC’d.
When the mesh is GC’d, UStaticMesh::BeginDestroy → ReleaseResources (StaticMesh.cpp:4591/3882) starts releasing RHI resources and begins a ReleaseResourcesFence; IsReadyForFinishDestroy (:4601) blocks FinishDestroy until ReleaseResourcesFence.IsFenceComplete(). So the actual delete of RenderData is delayed until the render thread has finished releasing RHI resources.
Questions
In the runtime SetStaticMesh() path (StaticMeshComponent.cpp:2237), swapping the mesh only calls SetStaticMeshInternal(NewMesh) + MarkRenderStateDirty() (:2277) — there is no FlushRenderingCommands and no FStaticMeshComponentRecreateRenderStateContext (that class appears to be editor-only). The old mesh can therefore become unreachable immediately, while the old proxy is not deleted until the end-of-frame proxy recreation. Is there an explicit ordering/fence that guarantees the old proxy is deleted before the old mesh finishes FinishDestroy? Or is the safety here relying on something else (e.g. GC periodicity, the fact that runtime mesh swaps are rare and Mobility=Static rejects them at :2249)?
The ReleaseResourcesFence only gates the release of the RHI resources of RenderData; it does not protect the proxy’s render-thread access to the RenderData object itself (e.g. RenderData->LODResources, which is plain object data, not RHI). Is that reading correct? If so, what actually prevents the proxy from dereferencing a freed RenderData* in the SetStaticMesh swap case?
More generally: is the intended invariant simply “a scene proxy may keep raw pointers into a mesh only for as long as the owning component keeps a strong UPROPERTY reference to that mesh, and the proxy is guaranteed to be destroyed before that UPROPERTY reference is dropped”? If that’s the invariant, SetStaticMesh/SetAsset-style runtime swaps break it by dropping the UPROPERTY reference while the proxy still holds raw pointers — should such components instead hold an independent strong reference (e.g. a TSharedPtr/TRefCountPtr to the render data) across the swap?
[Attachment Removed]