Issue with UHLODBuilder::ComputeHLODHash

Hi,

We’ve noticed that there seems to be a bug in the HLOD hashing code where effectively the result of UHLODBuilderSettings::ComputeHLODHash is ignored. This seems to happen because of the logic in the static UHLODBuilder::ComputeHLODHash method which creates a FHLODHashScope with FHLODHashScope::EFlags::ResetHash. FHLODHashScope never restores the previous CRC in the FHLODHashBuilder it’s passed so that essentially ignores any values inside FHLODHashBuilder prior to this point when computing the result so effectively only ComponentsHashes matters.

The logic seems to be the same upstream so we wanted to report it.

Thanks,

Lucas

[Attachment Removed]

Steps to Reproduce
Make a HLOD layer using UHLODBuilderInstancing, build it, then modify a setting on UHLODBuilderInstancingSettings which changes the result of ComputeHLODHash. Observe that the actors do not detect they need to rebuild.

[Attachment Removed]

Hi Lucas,

You are correct the settings are lost when the FHLODHashScope resets the HashBuilder. The final CRC also includes the CRC of the last Component twice as the HashBuilder is not reset (or restored) after the last iteration.

I have logged a bug report for the engine team: https://issues.unrealengine.com/issue/UE\-388016

We have found a workaround that keeps the settings part of the hash.

void UHLODBuilder::ComputeHLODHash(FHLODHashBuilder& HashBuilder, const TArray<UActorComponent*>& InSourceComponents)
{
	// The per-component scope below resets the builder so each component is hashed in isolation.
	// Preserve the hash already accumulated by the caller (base key, HLOD settings, ...) so it is not
	// lost by that reset, and reincorporate it once the component hashes have been gathered.
	uint32 AccumulatedHash = HashBuilder.GetCrc();
 
	// We get the hash of each component
	TArray<uint32> ComponentsHashes;
 
	for (UActorComponent* SourceComponent : InSourceComponents)
	{
		FHLODHashScope HashScope(HashBuilder, SourceComponent, FHLODHashScope::EFlags::ResetHash);
 
		TSubclassOf<UHLODBuilder> HLODBuilderClass = SourceComponent->GetCustomHLODBuilderClass();
		if (!HLODBuilderClass)
		{
			HLODBuilderClass = UHLODBuilder::StaticClass();
		}
 
		bool bValidHash = HLODBuilderClass->GetDefaultObject<UHLODBuilder>()->ComputeHLODHash(HashBuilder, SourceComponent);
		if (bValidHash)
		{
			ComponentsHashes.Add(HashBuilder.GetCrc());
		}
		else
		{
			UE_LOGF(LogHLODBuilder, Warning, "Can't compute HLOD hash for component of type %ls, assuming it is dirty.", *SourceComponent->GetClass()->GetName());
			ComponentsHashes.Add(FMath::Rand());
		}
	}
 
	// Sort the components hashes to ensure the order of components won't have an impact on the final hash
	ComponentsHashes.Sort();
 
	// Reincorporate the caller's accumulated hash (dropped by the per-component reset above),
	// then add the order-independent component hashes.
	HashBuilder.Reset();
	HashBuilder << AccumulatedHash;
	HashBuilder << ComponentsHashes;
}

Regards,

Martin

[Attachment Removed]

Thanks for confirming! Yeah, we made a similar quick fix but I didn’t even notice it was adding the last component twice. Whoops.

Thanks,

Lucas

[Attachment Removed]