UPrimitiveComponent::GetLastRenderTime/ WasRecentlyRendered for nanite static meshes not working

Hi,

The following functions do not work in non-editor builds (due to FSceneProxyBase::SupportsAlwaysVisible):

  • UPrimitiveComponent::GetLastRenderTime()
  • UPrimitiveComponent::WasRecentlyRendered()
  • AActor::GetLastRenderTime()
  • AActor::WasRecentlyRendered()

They do not work properly (as expected from what I see in the code) when the primitive (or the actor contains any primitive) with a nanite static mesh.

We are considering adding a simple workaround like so, are there plans to handle this case in future versions of UE (this was raised here already: [Content removed]

These functions are really useful, however I understand that Nanite means GPU path, do you see a big performance impact in disabling the always visible path on a few nanite static mesh primitive components ? As a workaround, we could also create a dummy non-nanite primitive component to retrieve last draw time info if you think it’s really worth avoiding this workaround.

( bAllowAlwaysVisible being a property added to UPrimitiveComponent ).

void UPrimitiveComponent::AssignSceneProxy(FPrimitiveSceneProxy* InSceneProxy)
{
  check(SceneProxy == nullptr && SceneData.SceneProxy == nullptr);
  SceneProxy = InSceneProxy;
  SceneData.SceneProxy = InSceneProxy;
  if (SceneProxy)
  {
-->    SceneData.bAlwaysVisible = bAllowAlwaysVisible && SceneProxy->IsAlwaysVisible();
    SceneData.OwnerLastRenderTimePtr = FActorLastRenderTime::GetPtr(GetOwner());

    if (SceneData.bAlwaysVisible && SceneData.OwnerLastRenderTimePtr)
    {
      SceneData.OwnerLastRenderTimePtr->NumAlwaysVisibleComponents.fetch_add(1, std::memory_order_relaxed);
    }

#if WITH_EDITOR
    if (bWantsEditorEffects)
    {
      SetOverlayColor(OverlayColor);
    }
#endif
  }
}

Thanks in advance,

Hugo

Steps to Reproduce

Hi there,

The IsAlwaysVisible path for nanite excludes nanite primitives from many of the CPU side visibility calculations, as these are not necessary for Nanite meshes most of the time. However, I would expect disabling the IsAlwaysVisible optimization, on select primitives in a level, to produce a negligible performance impact. Still, you might want to consider using a non-nanite proxy for this anyway. This is because Nanite primitives also do not participate in the standard occlusion culling pipeline, and therefore you will only get frustum culling visibility results from WasRecentlyRendered, even when IsAlwaysVisible is false.

A note that your code modifications also won’t do what you want. You would need to change the value returned by FPrimitiveSceneProxy::IsAlwaysVisible on the scene proxy for this to work, as this is what is used by the render side code. I’d suggest adding the bAllowAlwaysVisible boolean to the UPrimitiveComponent, the FPrimitiveSceneProxyDesc, and FPrimitiveSceneProxy, and passing it through when constructing the FPrimitiveSceneProxy. Then changing FSceneProxyBase::SupportsAlwaysVisible() as follows:

bool FSceneProxyBase::SupportsAlwaysVisible() const
{
#if WITH_EDITOR
   // Right now we never use the always visible optimization
   // in editor builds due to dynamic relevance, hit proxies, etc..
   return false;
#else
  
   // START_OF_CHANGE
   if (!bAllowAlwaysVisible)
   {
      return false;
   }
   // END_OF_CHANGE
 
   // ... Rest of code
}

For the non-nanite proxy approach you would probably need to make a proxy mesh with an invisible material (so that it still technically counts as visible, and participates in frustum / occlusion culling). You can use an unlit masked material with 0 opacity for this.

One more gotcha with the WasRecentlyRendered method; it will also return true if the mesh was recently rendered in a shadow, or any other, pass. Often this isn’t the developer intent for gameplay logic, and what they really want is to know whether the mesh was recently rendered on screen. Fortunately, the functionality for checking this exists, but it is not exposed to blueprint currently. You can implement / expose this functionality yourself as follows:

UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Utils", meta = (DisplayName = "Was Recently Rendered On Screen"))
static bool WasRecentlyRenderedOnScreen(UPrimitiveComponent* Component, float Tolerance = 0.2f)
{
   if (!IsValid(Component))
   {
      return false;
   }
 
   if (const UWorld* const World = Component->GetWorld())
   {
      // Adjust tolerance, so visibility is not affected by bad frame rate / hitches.
      const float RenderTimeThreshold = FMath::Max(Tolerance, World->DeltaTimeSeconds + UE_KINDA_SMALL_NUMBER);
 
      // If the current cached value is less than the tolerance then we don't need to go look at the components
      return World->TimeSince(Component->GetLastRenderTimeOnScreen()) <= RenderTimeThreshold; // <-- Changed to use the OnScreen variant
   }
   return false;
}

Hopefully this gives you some paths forward with this issue

Regards,

Lance Chaney

Hi Lance,

Thanks for the detailed explanation, we ended up doing a simple frustum cull test manually as a workaround since experimentation with the hack I mentioned earlier indeed wasn’t enough in cooked builds. It’s a shame we don’t have occlusion information on top of frustum testing however it helps enough for our use case.

Best,

Hugo