377 lines
13 KiB
C++
377 lines
13 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
/*=============================================================================
|
|
LandscapeTextureHash.cpp: Track a Custom Hash on each landscape texture.
|
|
This hash tries to be insensitive to changes that are less than the thresholds,
|
|
and also ignores normal data channels on the heightmaps.
|
|
=============================================================================*/
|
|
|
|
#include "LandscapeTextureHash.h"
|
|
#include "LandscapePrivate.h"
|
|
#include "Engine/Texture2D.h"
|
|
|
|
#include UE_INLINE_GENERATED_CPP_BY_NAME(LandscapeTextureHash)
|
|
|
|
#if WITH_EDITORONLY_DATA
|
|
|
|
extern TAutoConsoleVariable<int32> CVarLandscapeDirtyHeightmapHeightThreshold;
|
|
extern TAutoConsoleVariable<int32> CVarLandscapeDirtyWeightmapThreshold;
|
|
extern int32 GPatchEdges;
|
|
extern int32 GPatchStreamingMipEdges;
|
|
|
|
namespace UE::Landscape::Private
|
|
{
|
|
// produces a Valid (non-zero) FGuid from a 64 bit hash
|
|
static inline FGuid Hash64ToGUID(uint64 Hash)
|
|
{
|
|
uint32 LowBits = Hash & 0xffffffff;
|
|
uint32 HighBits = Hash >> 32;
|
|
return FGuid(HighBits, HighBits + LowBits + 0xbb4824dc, HighBits ^ LowBits, LowBits);
|
|
}
|
|
|
|
// converts a GUID into a 64 bit hash (inverse of Hash64ToGUID)
|
|
static inline uint64 GUIDToHash64(const FGuid& GUID)
|
|
{
|
|
return (static_cast<uint64>(GUID.A) << 32) + GUID.D;
|
|
}
|
|
};
|
|
|
|
void ULandscapeTextureHash::SetInitialStateOnPostLoad(UTexture2D* LandscapeTexture, ELandscapeTextureUsage TextureUsage, ELandscapeTextureType TextureType)
|
|
{
|
|
ULandscapeTextureHash* TextureHash = LandscapeTexture->GetAssetUserData<ULandscapeTextureHash>();
|
|
if (TextureHash == nullptr)
|
|
{
|
|
// if there is no texture hash recorded, create a new one (using default SourceID as the hash) and record it in the recent serialized hashes
|
|
TextureHash = NewObject<ULandscapeTextureHash>(LandscapeTexture);
|
|
LandscapeTexture->AddAssetUserData(TextureHash);
|
|
|
|
FGuid LandscapeTextureSourceId = LandscapeTexture->Source.GetId();
|
|
TextureHash->TextureHashGUID = LandscapeTextureSourceId;
|
|
TextureHash->LastSourceID = LandscapeTextureSourceId;
|
|
TextureHash->TextureType = TextureType;
|
|
TextureHash->TextureUsage = TextureUsage;
|
|
TextureHash->RecentlySerializedHashes.Add(LandscapeTextureSourceId, LandscapeTextureSourceId);
|
|
}
|
|
}
|
|
|
|
void ULandscapeTextureHash::CheckHashIsUpToDate(UTexture2D* LandscapeTexture)
|
|
{
|
|
ULandscapeTextureHash* TextureHash = LandscapeTexture->GetAssetUserData<ULandscapeTextureHash>();
|
|
check(TextureHash != nullptr);
|
|
check(LandscapeTexture->Source.GetId() == TextureHash->LastSourceID);
|
|
}
|
|
|
|
FGuid ULandscapeTextureHash::CalculateTextureHashGUID(UTexture2D* LandscapeTexture, ELandscapeTextureType TextureType)
|
|
{
|
|
uint64 Hash64 = CalculateTextureHash64(LandscapeTexture, TextureType);
|
|
return UE::Landscape::Private::Hash64ToGUID(Hash64);
|
|
}
|
|
|
|
uint64 ULandscapeTextureHash::CalculateTextureHash64(UTexture2D* LandscapeTexture, ELandscapeTextureType TextureType)
|
|
{
|
|
const int32 MipIndex = 0;
|
|
const int64 MipSizeInBytes = LandscapeTexture->Source.CalcMipSize(MipIndex);
|
|
const int64 MipSizeInPixels = MipSizeInBytes / 4;
|
|
const FColor* MipData = (const FColor*)LandscapeTexture->Source.LockMipReadOnly(MipIndex);
|
|
uint64 Hash = CalculateTextureHash64(MipData, MipSizeInPixels, TextureType);
|
|
LandscapeTexture->Source.UnlockMip(MipIndex);
|
|
return Hash;
|
|
}
|
|
|
|
uint64 ULandscapeTextureHash::CalculateTextureHash64(const FColor* Mip0Data, int32 PixelCount, ELandscapeTextureType TextureType)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(ULandscapeTextureHash::CalculateTextureHash);
|
|
|
|
uint64 NewHash = 0;
|
|
|
|
switch (TextureType)
|
|
{
|
|
case ELandscapeTextureType::Unknown:
|
|
check(TextureType != ELandscapeTextureType::Unknown);
|
|
break;
|
|
case ELandscapeTextureType::Heightmap:
|
|
{
|
|
uint32 CRCHashG = 0;
|
|
uint32 CRCHashR = 1;
|
|
|
|
for (int32 i = 0; i < PixelCount; i++)
|
|
{
|
|
// the height is the Red and Green channels (ignore the normal data in the other channels)
|
|
CRCHashG = FCrc::TypeCrc32(Mip0Data->G, CRCHashG);
|
|
CRCHashR = FCrc::TypeCrc32(Mip0Data->R, CRCHashR);
|
|
Mip0Data++;
|
|
}
|
|
|
|
NewHash = (((uint64)CRCHashR) << 32) + CRCHashG;
|
|
}
|
|
break;
|
|
case ELandscapeTextureType::Weightmap:
|
|
{
|
|
NewHash = CityHash64(reinterpret_cast<const char*>(Mip0Data), PixelCount * sizeof(FColor));
|
|
}
|
|
break;
|
|
default:
|
|
checkf(false, TEXT("TextureType is invalid: %d"), TextureType);
|
|
break;
|
|
}
|
|
|
|
return NewHash;
|
|
}
|
|
|
|
bool ULandscapeTextureHash::DoesTextureDataChangeExceedThreshold(
|
|
const FColor* Mip0Data, const FColor* OldMip0Data, int32 PixelCount, ELandscapeTextureType TextureType, uint64 OldHash, uint64 NewHash, TOptional<uint8>& OutChangedWeightmapChannelsMasks)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(ULandscapeTextureHash::DoesTextureDataChangeExceedThreshold);
|
|
|
|
bool bExceedsThreshold = false;
|
|
|
|
switch (TextureType)
|
|
{
|
|
case ELandscapeTextureType::Unknown:
|
|
check(TextureType != ELandscapeTextureType::Unknown);
|
|
break;
|
|
case ELandscapeTextureType::Heightmap:
|
|
{
|
|
int32 DirtyHeightmapHeightThreshold = CVarLandscapeDirtyHeightmapHeightThreshold.GetValueOnGameThread();
|
|
if (DirtyHeightmapHeightThreshold <= 0)
|
|
{
|
|
// at at threshold of zero, any change at all will exceed
|
|
bExceedsThreshold = (OldHash != NewHash);
|
|
break;
|
|
}
|
|
|
|
for (int32 i = 0; i < PixelCount; i++)
|
|
{
|
|
const FColor& OldColor = *OldMip0Data;
|
|
const FColor& NewColor = *Mip0Data;
|
|
|
|
if (OldColor != NewColor)
|
|
{
|
|
uint16 OldHeight = ((static_cast<uint16>(OldColor.R) << 8) | static_cast<uint16>(OldColor.G));
|
|
uint16 NewHeight = ((static_cast<uint16>(NewColor.R) << 8) | static_cast<uint16>(NewColor.G));
|
|
if (uint16 Diff = (NewHeight > OldHeight) ? (NewHeight - OldHeight) : (OldHeight - NewHeight); Diff > DirtyHeightmapHeightThreshold)
|
|
{
|
|
bExceedsThreshold = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
Mip0Data++;
|
|
OldMip0Data++;
|
|
}
|
|
}
|
|
break;
|
|
case ELandscapeTextureType::Weightmap:
|
|
{
|
|
int32 DirtyWeightmapThreshold = CVarLandscapeDirtyWeightmapThreshold.GetValueOnGameThread();
|
|
if ((DirtyWeightmapThreshold <= 0) && !OutChangedWeightmapChannelsMasks.IsSet())
|
|
{
|
|
// at at threshold of zero, any change at all will exceed
|
|
bExceedsThreshold = (OldHash != NewHash);
|
|
break;
|
|
}
|
|
|
|
for (int32 Index = 0; Index < PixelCount; ++Index)
|
|
{
|
|
const FColor& OldColor = *OldMip0Data;
|
|
const FColor& NewColor = *Mip0Data;
|
|
|
|
if (OldColor != NewColor)
|
|
{
|
|
auto DiffChannel = [DirtyWeightmapThreshold](uint8 InOldValue, uint8 InNewValue) -> bool
|
|
{
|
|
uint8 Diff = (InNewValue > InOldValue) ? (InNewValue - InOldValue) : (InOldValue - InNewValue);
|
|
return (Diff > DirtyWeightmapThreshold);
|
|
};
|
|
|
|
uint8 DiffMask =
|
|
((uint8)(DiffChannel(OldColor.R, NewColor.R) ? 1 : 0) << 0)
|
|
| ((uint8)(DiffChannel(OldColor.G, NewColor.G) ? 1 : 0) << 1)
|
|
| ((uint8)(DiffChannel(OldColor.B, NewColor.B) ? 1 : 0) << 2)
|
|
| ((uint8)(DiffChannel(OldColor.A, NewColor.A) ? 1 : 0) << 3);
|
|
|
|
if (DiffMask != 0)
|
|
{
|
|
bExceedsThreshold = true;
|
|
if (OutChangedWeightmapChannelsMasks.IsSet())
|
|
{
|
|
*OutChangedWeightmapChannelsMasks |= DiffMask;
|
|
}
|
|
else
|
|
{
|
|
// no need to report which channel has been changed, early out :
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
Mip0Data++;
|
|
OldMip0Data++;
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
checkf(false, TEXT("TextureType is invalid: %d"), TextureType);
|
|
break;
|
|
}
|
|
|
|
return bExceedsThreshold;
|
|
}
|
|
|
|
void ULandscapeTextureHash::SetHash64(UTexture2D* LandscapeTexture, uint64 NewHash64, ELandscapeTextureUsage TextureUsage, ELandscapeTextureType TextureType)
|
|
{
|
|
ULandscapeTextureHash* TextureHash = LandscapeTexture->GetAssetUserData<ULandscapeTextureHash>();
|
|
if (TextureHash == nullptr)
|
|
{
|
|
// create a new one (with LandscapeTexture as outer)
|
|
TextureHash = NewObject<ULandscapeTextureHash>(LandscapeTexture);
|
|
LandscapeTexture->AddAssetUserData(TextureHash);
|
|
}
|
|
else
|
|
{
|
|
// pre-existing -- should have the same type
|
|
check(TextureHash->TextureType == TextureType);
|
|
check(TextureHash->TextureUsage == TextureUsage);
|
|
}
|
|
FGuid LandscapeTextureSourceId = LandscapeTexture->Source.GetId();
|
|
|
|
// cached hashes take precedence -- this ensure that if the texture is brought back to a recently serialized state, it will have exactly the same hash that it was serialized with
|
|
FGuid NewHashGUID;
|
|
if (FGuid* CachedHash = TextureHash->RecentlySerializedHashes.Find(LandscapeTextureSourceId))
|
|
{
|
|
NewHashGUID = *CachedHash;
|
|
}
|
|
else
|
|
{
|
|
NewHashGUID = UE::Landscape::Private::Hash64ToGUID(NewHash64);
|
|
}
|
|
TextureHash->TextureHashGUID = NewHashGUID;
|
|
TextureHash->LastSourceID = LandscapeTextureSourceId;
|
|
TextureHash->TextureType = TextureType;
|
|
TextureHash->TextureUsage = TextureUsage;
|
|
}
|
|
|
|
void ULandscapeTextureHash::UpdateHash(UTexture2D* LandscapeTexture, ELandscapeTextureUsage TextureUsage, ELandscapeTextureType TextureType, bool bForceUpdate)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(ULandscapeTextureHash::UpdateHash);
|
|
|
|
FGuid LandscapeTextureSourceId = LandscapeTexture->Source.GetId();
|
|
|
|
bool bNewlyCreated = false;
|
|
ULandscapeTextureHash* TextureHash = LandscapeTexture->GetAssetUserData<ULandscapeTextureHash>();
|
|
if (TextureHash == nullptr)
|
|
{
|
|
// create a new one (with LandscapeTexture as outer)
|
|
TextureHash = NewObject<ULandscapeTextureHash>(LandscapeTexture);
|
|
LandscapeTexture->AddAssetUserData(TextureHash);
|
|
bNewlyCreated = true;
|
|
}
|
|
else
|
|
{
|
|
if (!bForceUpdate && (LandscapeTextureSourceId == TextureHash->LastSourceID))
|
|
{
|
|
// no need to update, it's the same
|
|
check(TextureHash->TextureUsage == TextureUsage || TextureUsage == ELandscapeTextureUsage::Unknown);
|
|
check(TextureHash->TextureType == TextureType || TextureType == ELandscapeTextureType::Unknown);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (TextureUsage == ELandscapeTextureUsage::Unknown)
|
|
{
|
|
TextureUsage = TextureHash->TextureUsage;
|
|
}
|
|
if (TextureType == ELandscapeTextureType::Unknown)
|
|
{
|
|
TextureType = TextureHash->TextureType;
|
|
}
|
|
|
|
FGuid NewHash;
|
|
if ((TextureUsage != ELandscapeTextureUsage::FinalData) ||
|
|
(TextureType == ELandscapeTextureType::Unknown))
|
|
{
|
|
// non-final data and/or unknown types don't need to use a hash, as we just use the SourceID directly
|
|
NewHash = LandscapeTextureSourceId;
|
|
}
|
|
else
|
|
{
|
|
// if this SourceId is familiar, use the corresponding hash
|
|
if (FGuid* CachedHash = TextureHash->RecentlySerializedHashes.Find(LandscapeTextureSourceId))
|
|
{
|
|
NewHash = *CachedHash;
|
|
}
|
|
else
|
|
{
|
|
// otherwise compute a new one
|
|
NewHash = CalculateTextureHashGUID(LandscapeTexture, TextureType);
|
|
}
|
|
}
|
|
|
|
TextureHash->TextureHashGUID = NewHash;
|
|
TextureHash->LastSourceID = LandscapeTextureSourceId;
|
|
TextureHash->TextureType = TextureType;
|
|
TextureHash->TextureUsage = TextureUsage;
|
|
|
|
if (bNewlyCreated)
|
|
{
|
|
TextureHash->RecentlySerializedHashes.Add(LandscapeTextureSourceId, NewHash);
|
|
}
|
|
}
|
|
|
|
FGuid ULandscapeTextureHash::GetHash(UTexture2D* LandscapeTexture)
|
|
{
|
|
// if no texture hash exists, or it's not a final layer-merged texture, just use the source ID
|
|
ULandscapeTextureHash* TextureHash = LandscapeTexture->GetAssetUserData<ULandscapeTextureHash>();
|
|
if ((TextureHash == nullptr) || (TextureHash->TextureUsage != ELandscapeTextureUsage::FinalData) || (TextureHash->TextureType == ELandscapeTextureType::Unknown))
|
|
{
|
|
// fallback to using the Source ID (matches old behavior)
|
|
check(LandscapeTexture->Source.IsValid());
|
|
return LandscapeTexture->Source.GetId();
|
|
}
|
|
if (LandscapeTexture->Source.GetId() != TextureHash->LastSourceID)
|
|
{
|
|
// NOTE: this can happen in WP mode when a final data texture is transacted for undo/redo.
|
|
// It can also happen when we are using non-WP mode when directly modifying the final texture source on the CPU (as we don't rehash on all CPU modifications)
|
|
|
|
// in either case we can just force update the hash to get an ok hash to use.
|
|
// This won't take change thresholds into account, but that's as good as we can do in these cases.
|
|
UpdateHash(LandscapeTexture, TextureHash->TextureUsage, TextureHash->TextureType, /*bForceUpdate=*/ true);
|
|
}
|
|
return TextureHash->TextureHashGUID;
|
|
}
|
|
|
|
void ULandscapeTextureHash::Serialize(FArchive& Ar)
|
|
{
|
|
bool bUpdateRecentlySerializedHashes = Ar.IsPersistent() && !Ar.IsObjectReferenceCollector() && !HasAnyFlags(RF_ArchetypeObject | RF_ClassDefaultObject | RF_DefaultSubObject);
|
|
if (bUpdateRecentlySerializedHashes && Ar.IsSaving())
|
|
{
|
|
UTexture2D* ParentTexture = Cast<UTexture2D>(GetOuter());
|
|
if (ParentTexture && (ParentTexture->Source.GetId() != LastSourceID))
|
|
{
|
|
// The stored hash is out of date (it was modified without explicitly updating the hash) Update it before serializing!
|
|
// This won't take change thresholds into account, but that's as good as we can do in these cases.
|
|
UpdateHash(ParentTexture, TextureUsage, TextureType, /*bForceUpdate=*/ true);
|
|
}
|
|
|
|
// as we're about to save this, make it an official recent value
|
|
// (this guarantees that if we get back to the current state, we will get the same texture hash, despite the threshold-change shenanigans that might go on in the meantime)
|
|
if (!RecentlySerializedHashes.Contains(LastSourceID))
|
|
{
|
|
RecentlySerializedHashes.Add(LastSourceID, TextureHashGUID);
|
|
}
|
|
}
|
|
|
|
Super::Serialize(Ar);
|
|
|
|
if (bUpdateRecentlySerializedHashes && Ar.IsLoading())
|
|
{
|
|
// as we just loaded this, make it an official recent value
|
|
// (this guarantees that if we get back to the current state, we will get the same texture hash, despite the threshold-change shenanigans that might go on in the meantime)
|
|
if (!RecentlySerializedHashes.Contains(LastSourceID))
|
|
{
|
|
RecentlySerializedHashes.Add(LastSourceID, TextureHashGUID);
|
|
}
|
|
}
|
|
}
|
|
#endif // WITH_EDITORONLY_DATA
|