GroomManager SelectValidLOD Hitching

We have some hitching when allocating render resources for hair strands when an LOD change occurs for hair (approaching characters). The hitching is pretty severe on the XSX > 100 ms on the Render Thread. Thankfully we were able to reduce the hitching more than 50% by utilizing async allocation of hair resources (CVars in GroomResource.cpp) but there remains a very significant hitch when GeometryType is set to EHairGeometryType::NoneGeometry in SelectValidLOD() in GroomManager.cpp (hair not visible).

The reason that function is still hitching is because GetHairResourceLoadingType() does not account for EHairGeometryType::NoneGeometry and just returns EHairResourceLoadingType::Sync as a result (no async path). Then in SelectValidLOD() the code falls through allocating resources synchronously.

There are two ways I can fix this locally with a code modification for now, but I was hoping someone here could give me a sanity check. Here are the two ways to fully reduce the hitching:

1) In GetHairResourceLoadingType() in GroomResources.cpp I could account for EHairGeometryType::NoneGeometry in the switch statement, and allow async allocation if the CVars allow it. This is the easiest and smallest mod footprint, but the downside is allocations still occur for hair that is not visible (possible wasted allocs).

2) In SelectValidLOD() in GroomManager.cpp I could return early if GeometryType is EHairGeometryType::NoneGeometry and bLODNeedsGuides is set to false. The early out would occur immediately after bLODNeedsGuides (see code snippet below). If I did the return early so as not to do unnecessary allocs am I being too heavy-handed filling out the instance data? Is it enough to just ensure the Instance GeometryType is set to EHairGeometryType::NoneGeometry?

const bool bSimulationEnable			= Instance->HairGroupPublicData->IsSimulationEnable(IntHairLODIndex);
const bool bDeformationEnable			= Instance->HairGroupPublicData->bIsDeformationEnable;
const bool bGlobalInterpolationEnable	= Instance->HairGroupPublicData->IsGlobalInterpolationEnable(IntHairLODIndex);
const bool bSimulationCacheEnable		= Instance->HairGroupPublicData->bIsSimulationCacheEnable;
const bool bLODNeedsGuides				= bSimulationEnable || bDeformationEnable || bGlobalInterpolationEnable || bSimulationCacheEnable;
 
// MODIFICATION - BEGIN
// If we're not rendering hair, and we don't need guides work, bail before any allocations.
// This prevents hitching for EHairGeometryType::NoneGeometry which isn't accounted for in GetHairResourceLoadingType() and just returns EHairResourceLoadingType::Sync
if (GeometryType == EHairGeometryType::NoneGeometry && !bLODNeedsGuides)
{
	const int32 IntPrevHairLODIndex = FMath::Clamp(FMath::FloorToInt(PrevHairLODIndex), 0, HairLODCount - 1);
	const EHairBindingType CurrBindingType = Instance->HairGroupPublicData->GetBindingType(IntHairLODIndex);
	const EHairBindingType PrevBindingType = Instance->HairGroupPublicData->GetBindingType(IntPrevHairLODIndex);
 
	Instance->HairGroupPublicData->SetLODVisibility(bIsVisible);
	Instance->HairGroupPublicData->SetLODIndex(HairLODIndex);
	Instance->HairGroupPublicData->SetLODBias(0);
	Instance->HairGroupPublicData->SetMeshLODIndex(MeshLODIndex);
 
	Instance->HairGroupPublicData->VFInput.GeometryType = GeometryType;
	Instance->HairGroupPublicData->VFInput.BindingType = CurrBindingType;
	Instance->HairGroupPublicData->VFInput.bHasLODSwitch = IntPrevHairLODIndex != IntHairLODIndex;
	Instance->HairGroupPublicData->VFInput.bHasLODSwitchBindingType = CurrBindingType != PrevBindingType;
 
	Instance->GeometryType = GeometryType;
	Instance->BindingType = CurrBindingType;
	return true;
}
// MODIFICATION - END

Thank you in advance!

- Dennis

Hi,

Thank you for sharing this. Indeed your second solution seems the most appropriate, and seems correct. I tested it locally on some project and seems to work as expected. I integrated your code in CL 49944888 in our mainline.

Thank you again for taking the time to share this!

/Charles.